mirror of
https://github.com/grub-basket/SP.git
synced 2026-07-22 07:46:27 +00:00
0.121.13: settings overhaul — tabs, polish, icon search, hotkey tri-state, date-format fix
This commit is contained in:
parent
0510541589
commit
727b78891e
8 changed files with 480 additions and 269 deletions
168
main.js
168
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "stashpad",
|
||||
"name": "Stashpad",
|
||||
"version": "0.121.1",
|
||||
"version": "0.121.13",
|
||||
"minAppVersion": "1.13.0",
|
||||
"description": "A chat-style, nested-notes workspace: rapid capture, outliner navigation, fast search, tasks, and per-folder templates, with one-click Open Knowledge Format (OKF) export for LLMs and agents.",
|
||||
"author": "Human",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stashpad-obsidian",
|
||||
"version": "0.121.1",
|
||||
"version": "0.121.13",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
51
release-notes/0.121.13.md
Normal file
51
release-notes/0.121.13.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# 0.121.13 — Settings overhaul: clearer structure, polish, and a date-format fix
|
||||
|
||||
A settings-focused release: the tabs are reorganized, long-standing alignment
|
||||
and padding issues are fixed, the per-folder icon picker becomes searchable, the
|
||||
hotkey UI is reworked, and a date-format bug is fixed.
|
||||
|
||||
## Reorganized settings tabs
|
||||
- **Behavior** — the six toggle-only sections (List & Display, Moving Notes,
|
||||
Composer & Copying, Deleting, Windows & Tabs, Misc) are now one **🎛️ Behavior**
|
||||
tab, each kept as a labelled sub-heading.
|
||||
- **Organization Systems** — **JD Index** and **Open Knowledge Format (OKF)** are
|
||||
folded into one **🗂️ Organization Systems** tab.
|
||||
- **Note Titles** is no longer its own tab — the "Slug stop-words" setting moves
|
||||
to the end of **Folders & Storage** under a "Note Titles" heading.
|
||||
|
||||
## Per-folder icon picker is now searchable
|
||||
- The **Folder tab icon** box is type-ahead: start typing and matching Lucide
|
||||
icons appear with live previews — no need to know icon ids by heart.
|
||||
|
||||
## Date & time
|
||||
- **Fix:** with "Use Templates plugin date/time formats" OFF, the notes-list
|
||||
timestamps now follow the **Date display format** dropdown (and timezone) like
|
||||
the Tasks/detail panels do — previously the list ignored the dropdown and was
|
||||
hardcoded to `YYYY.MM.DD`.
|
||||
- The Dates & Time section is reordered (display format first, then the Templates
|
||||
toggle) with clearer descriptions.
|
||||
|
||||
## Hotkeys
|
||||
- The two key slots now **stack vertically** with the active-binding control on
|
||||
the left, so the binding inputs sit at the right edge.
|
||||
- The old L/R pill + "Use both" checkbox are replaced by a single
|
||||
**Left | Both | Right** segmented control (disabled until both slots are bound).
|
||||
- Chord slots are styled as compact rounded key-chips; the clear/revert buttons
|
||||
stack on mobile.
|
||||
|
||||
## Layout & padding polish
|
||||
- Settings sections that previously sat flush against the modal edge are now
|
||||
inset to align with the rest: the Cross-Stashpad Search Scope and Folder Panel
|
||||
Placement lists, the JD Index description, the live date sample, and the
|
||||
"Folders you've worked in" / Known Authors lists (the authored-folders list
|
||||
also gains a heading).
|
||||
- Each settings page gets breathing room at the bottom; folded-tab sub-headings
|
||||
get top padding so the first one isn't cramped under the page title.
|
||||
- The encryption no-recovery advice banner gets more padding.
|
||||
- The JD "Actions" buttons wrap on mobile instead of running off the edge.
|
||||
|
||||
## Internal
|
||||
- The JD Index description is now built with safe DOM calls instead of an HTML
|
||||
string (clears the last `no-inner-html` lint the store review flags).
|
||||
- Clarified OKF copy to point at **Stashpad's own Templates section**, not
|
||||
Obsidian's core Templates plugin.
|
||||
58
src/icon-suggest.ts
Normal file
58
src/icon-suggest.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { AbstractInputSuggest, App, getIconIds, setIcon } from "obsidian";
|
||||
|
||||
/** Lucide icon-name autocomplete for a settings text input — type a few letters
|
||||
* and pick from matching icons (each shown WITH its preview) instead of having
|
||||
* to know icon ids by heart. Mirrors {@link FolderSuggest}.
|
||||
*
|
||||
* Stores the bare name (strips the internal "lucide-" prefix) since that's what
|
||||
* `setIcon` + the rest of the plugin already use (`rocket`, not `lucide-rocket`).
|
||||
*
|
||||
* Wire it up:
|
||||
* ```
|
||||
* .addText((t) => {
|
||||
* new IconSuggest(this.app, t.inputEl);
|
||||
* t.setValue(...).onChange(...);
|
||||
* })
|
||||
* ```
|
||||
* 0.121.10. */
|
||||
export class IconSuggest extends AbstractInputSuggest<string> {
|
||||
constructor(app: App, private inputEl: HTMLInputElement) {
|
||||
super(app, inputEl);
|
||||
}
|
||||
|
||||
private static norm(id: string): string {
|
||||
return id.replace(/^lucide-/, "");
|
||||
}
|
||||
|
||||
protected getSuggestions(query: string): string[] {
|
||||
// all-tokens, any-order match so "arrow up" finds "arrow-up", "move-up", etc.
|
||||
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const seen = new Set<string>();
|
||||
const ids: string[] = [];
|
||||
for (const raw of getIconIds()) {
|
||||
const id = IconSuggest.norm(raw);
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
ids.push(id);
|
||||
}
|
||||
const match = (id: string): boolean => tokens.every((t) => id.toLowerCase().includes(t));
|
||||
const out = tokens.length ? ids.filter(match) : ids;
|
||||
// Cap the list — the popover only renders a window anyway, and an unfiltered
|
||||
// getIconIds() is ~1.5k entries.
|
||||
return out.sort().slice(0, 50);
|
||||
}
|
||||
|
||||
renderSuggestion(id: string, el: HTMLElement): void {
|
||||
el.addClass("stashpad-icon-suggest-item");
|
||||
const ic = el.createSpan({ cls: "stashpad-icon-suggest-icon" });
|
||||
setIcon(ic, id);
|
||||
el.createSpan({ cls: "stashpad-icon-suggest-label", text: id });
|
||||
}
|
||||
|
||||
selectSuggestion(id: string): void {
|
||||
this.setValue(id);
|
||||
// Fire `input` so the caller's onChange listener persists + repaints.
|
||||
this.inputEl.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
266
src/settings.ts
266
src/settings.ts
|
|
@ -6,6 +6,7 @@ function osFileManagerName(): string {
|
|||
}
|
||||
import { buildJdIndexPreview, buildJdIndexNotes, scanForJdNotes, JdBuildConfirmModal, buildJdPreviewNotice } from "./index-builder";
|
||||
import { FolderSuggest } from "./folder-suggest";
|
||||
import { IconSuggest } from "./icon-suggest";
|
||||
import type StashpadPlugin from "./main";
|
||||
import { RESERVED_FRONTMATTER, type ViewMode } from "./types";
|
||||
import { type SplitMode } from "./view-helpers";
|
||||
|
|
@ -650,29 +651,25 @@ export function getTemplatesFormats(app: App): { dateFormat: string; timeFormat:
|
|||
* is the source of truth for both the bar at the top and the
|
||||
* search-mode group order. Order here = display order. */
|
||||
export type SettingsTabId =
|
||||
| "foldersStorage" | "importExport" | "noteTitles" | "datesTime" | "listDisplay"
|
||||
| "movingNotes" | "deleting" | "composerCopy" | "windowsTabs" | "notifications"
|
||||
| "encryption" | "authorship" | "templates" | "jdindex" | "okf"
|
||||
| "maintenance" | "diagnostics" | "misc" | "hotkeys";
|
||||
| "foldersStorage" | "importExport" | "datesTime" | "behaviors"
|
||||
| "notifications" | "encryption" | "authorship" | "templates"
|
||||
| "organizationSystems" | "maintenance" | "diagnostics" | "hotkeys";
|
||||
export const SETTINGS_TABS: Array<{ id: SettingsTabId; label: string }> = ([
|
||||
{ id: "foldersStorage", label: "📁 Folders & Storage" },
|
||||
{ id: "importExport", label: "🔄 Import & Export" },
|
||||
{ id: "noteTitles", label: "🏷️ Note Titles" },
|
||||
{ id: "datesTime", label: "🕒 Dates & Time" },
|
||||
{ id: "listDisplay", label: "📋 List & Display" },
|
||||
{ id: "movingNotes", label: "↕️ Moving Notes" },
|
||||
{ id: "deleting", label: "🗑️ Deleting" },
|
||||
{ id: "composerCopy", label: "✍️ Composer & Copying" },
|
||||
{ id: "windowsTabs", label: "🪟 Windows & Tabs" },
|
||||
// 0.121.9: the six toggle-only sections (List & Display, Moving Notes,
|
||||
// Deleting, Composer & Copying, Windows & Tabs, Misc) are folded into one
|
||||
// "Behavior" tab as sub-headings (see itemsForTab).
|
||||
{ id: "behaviors", label: "🎛️ Behavior" },
|
||||
{ id: "notifications", label: "🔔 Notifications" },
|
||||
{ id: "encryption", label: "🔒 Encryption" },
|
||||
{ id: "authorship", label: "✒️ Authorship" },
|
||||
{ id: "templates", label: "📄 Templates" },
|
||||
{ id: "jdindex", label: "🔢 JD Index" },
|
||||
{ id: "okf", label: "📚 Open Knowledge Format (OKF)" },
|
||||
// 0.121.9: JD Index + OKF folded into one "Organization Systems" tab.
|
||||
{ id: "organizationSystems", label: "🗂️ Organization Systems" },
|
||||
{ id: "maintenance", label: "🛠️ Maintenance" },
|
||||
{ id: "diagnostics", label: "🩺 Diagnostics" },
|
||||
{ id: "misc", label: "⚙️ Misc" },
|
||||
{ id: "hotkeys", label: "⌨️ Hotkeys" },
|
||||
// 0.112.9: sections shown alphabetically by label. Display order only — the
|
||||
// `id`-keyed itemsForTab dispatch is unaffected, and new tabs auto-sort in.
|
||||
|
|
@ -792,23 +789,32 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
// General split into focused categories (0.110.0) — each pulls its bucket.
|
||||
case "foldersStorage": return this.buildGeneralCategories().foldersStorage;
|
||||
case "importExport": return this.buildGeneralCategories().importExport;
|
||||
case "noteTitles": return this.buildGeneralCategories().noteTitles;
|
||||
case "datesTime": return this.buildGeneralCategories().datesTime;
|
||||
case "listDisplay": return this.buildGeneralCategories().listDisplay;
|
||||
case "movingNotes": return this.buildGeneralCategories().movingNotes;
|
||||
case "deleting": return this.buildGeneralCategories().deleting;
|
||||
case "composerCopy": return this.buildGeneralCategories().composerCopy;
|
||||
case "windowsTabs": return this.buildGeneralCategories().windowsTabs;
|
||||
case "maintenance": return this.buildGeneralCategories().maintenance;
|
||||
case "misc": return this.buildGeneralCategories().misc;
|
||||
// 0.121.9: "Behavior" merges the six toggle-only sections, each kept as a
|
||||
// labelled sub-heading so the groupings still read clearly.
|
||||
case "behaviors": {
|
||||
const c = this.buildGeneralCategories();
|
||||
return [
|
||||
this.headingDef("📋 List & Display"), ...c.listDisplay,
|
||||
this.headingDef("↕️ Moving Notes"), ...c.movingNotes,
|
||||
this.headingDef("✍️ Composer & Copying"), ...c.composerCopy,
|
||||
this.headingDef("🗑️ Deleting"), ...c.deleting,
|
||||
this.headingDef("🪟 Windows & Tabs"), ...c.windowsTabs,
|
||||
this.headingDef("⚙️ Misc"), ...c.misc,
|
||||
];
|
||||
}
|
||||
// 0.121.9: "Organization Systems" merges JD Index + OKF as sub-sections.
|
||||
case "organizationSystems": return [
|
||||
this.headingDef("🔢 JD Index (Johnny Decimal)"), ...this.jdIndexItems(),
|
||||
this.headingDef("📚 Open Knowledge Format (OKF)"), ...this.okfItems(),
|
||||
];
|
||||
case "encryption": return this.encryptionItems();
|
||||
// 0.99.15: authorship/templates/jdindex decomposed too — static fields as
|
||||
// per-setting items, the per-folder editors as sectionDefs (rendered fresh
|
||||
// at display) — so individual settings are searchable, not just page names.
|
||||
case "authorship": return this.authorshipItems();
|
||||
case "templates": return this.templatesItems();
|
||||
case "jdindex": return this.jdIndexItems();
|
||||
case "okf": return this.okfItems();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -820,8 +826,8 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
switch (tab) {
|
||||
case "authorship": this.renderAuthorshipSection(parent); break;
|
||||
case "templates": this.renderTemplatesTab(parent); break;
|
||||
case "jdindex": this.renderJdIndexSection(parent); break;
|
||||
// hotkeys migrated to declarative items (itemsForTab) — never routed here.
|
||||
// jdindex/okf now live under the "Organization Systems" tab (declarative
|
||||
// items via itemsForTab); hotkeys are declarative too — neither routed here.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1192,7 +1198,7 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
* bucket to its own settings page. Anything that doesn't fit a precise
|
||||
* category goes to `misc`. */
|
||||
private buildGeneralCategories(): Record<
|
||||
"foldersStorage" | "importExport" | "datesTime" | "noteTitles" | "listDisplay"
|
||||
"foldersStorage" | "importExport" | "datesTime" | "listDisplay"
|
||||
| "movingNotes" | "deleting" | "composerCopy" | "windowsTabs" | "maintenance" | "misc",
|
||||
SettingDefinitionItem[]
|
||||
> {
|
||||
|
|
@ -1206,7 +1212,6 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
foldersStorage: [] as SettingDefinitionItem[],
|
||||
importExport: [] as SettingDefinitionItem[],
|
||||
datesTime: [] as SettingDefinitionItem[],
|
||||
noteTitles: [] as SettingDefinitionItem[],
|
||||
listDisplay: [] as SettingDefinitionItem[],
|
||||
movingNotes: [] as SettingDefinitionItem[],
|
||||
deleting: [] as SettingDefinitionItem[],
|
||||
|
|
@ -1250,7 +1255,7 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
// 0.118.6: per-folder tab icon — moved here from the Encryption tab's
|
||||
// per-folder panel so it's searchable. Pick a folder, then enter a Lucide
|
||||
// icon id; a live preview shows whether it's valid.
|
||||
cats.foldersStorage.push(this.renderDef("Folder tab icon", "Give a folder its own Lucide icon (e.g. rocket, star, book-open) shown on its tab, the folder switcher, and its folder-panel row. Pick a folder, then enter an icon id (browse at lucide.dev). Blank = the default icon. Set per folder.", (s) => {
|
||||
cats.foldersStorage.push(this.renderDef("Folder tab icon", "Give a folder its own Lucide icon (e.g. rocket, star, book-open) shown on its tab, the folder switcher, and its folder-panel row. Pick a folder, then start typing in the icon box — matching icons appear with previews; pick one. Blank = the default icon. Set per folder.", (s) => {
|
||||
const folders = this.plugin.discoverStashpadFolders();
|
||||
if (folders.length === 0) { s.setDesc("No Stashpad folders found yet."); return; }
|
||||
if (!this.iconPickFolder || !folders.includes(this.iconPickFolder)) this.iconPickFolder = folders[0];
|
||||
|
|
@ -1264,9 +1269,11 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
});
|
||||
s.addText((t) => {
|
||||
textComp = t;
|
||||
t.setPlaceholder("list-tree");
|
||||
t.setPlaceholder("type to search… (e.g. rocket)");
|
||||
// 0.121.10: type-ahead icon search with previews — no need to know ids.
|
||||
new IconSuggest(this.app, t.inputEl);
|
||||
t.setValue(this.plugin.getFolderIcon(this.iconPickFolder!) ?? "");
|
||||
t.onChange(async (v) => { paint(v); await this.plugin.setFolderIcon(this.iconPickFolder!, v); });
|
||||
t.onChange(async (v) => { paint(v); await this.plugin.setFolderIcon(this.iconPickFolder!, v.trim()); });
|
||||
});
|
||||
paint(this.plugin.getFolderIcon(this.iconPickFolder) ?? "");
|
||||
}, ["icon", "folder", "tab", "lucide", "emoji", "switcher"]));
|
||||
|
|
@ -1306,24 +1313,16 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.writeRecoveryLinks = v; await set();
|
||||
})), ["recovery", "parentlink", "children", "frontmatter"]));
|
||||
|
||||
cats.datesTime.push(this.renderDef("Use Templates plugin date/time formats", "When on, timestamps use the formats configured in the core Templates plugin. Off: YYYY.MM.DD + HH:mm A.", (s) => {
|
||||
s.addToggle((t) => t.setValue(this.plugin.settings.useTemplatesFormat).onChange(async (v) => {
|
||||
this.plugin.settings.useTemplatesFormat = v; await set();
|
||||
}));
|
||||
const fmt = getTemplatesFormats(this.app);
|
||||
s.descEl.createDiv({ cls: "stashpad-settings-note" }).setText(fmt
|
||||
? `Templates plugin: date = "${fmt.dateFormat}", time = "${fmt.timeFormat}"`
|
||||
: "Templates plugin not enabled.");
|
||||
}, ["templates", "date", "time", "format"]));
|
||||
|
||||
// Date display block — dropdown + timezone share a live sample element.
|
||||
// Date display block — dropdown leads (it now drives EVERY timestamp surface:
|
||||
// notes list, Tasks, detail panel), then the Templates-format toggle, the
|
||||
// timezone, and a live sample. 0.121.7: reordered + copy updated.
|
||||
{
|
||||
let sampleEl: HTMLElement | null = null;
|
||||
const refreshSample = () => {
|
||||
if (!sampleEl) return;
|
||||
sampleEl.setText(`Sample: ${formatDateTime(Date.now(), this.plugin.settings)}`);
|
||||
};
|
||||
cats.datesTime.push(this.renderDef("Date display format", "How due dates and created/modified times are shown in the Tasks and detail panels.", (s) => {
|
||||
cats.datesTime.push(this.renderDef("Date display format", "How created / edited timestamps and due dates are shown — in the notes list, the Tasks panel, and the detail panel. Overridden by the Templates-plugin formats when the option below is on.", (s) => {
|
||||
s.addDropdown((d) => {
|
||||
d.addOption("locale", "Locale, short (Mar 5, 9:00 AM)");
|
||||
d.addOption("long", "Locale, long (Thursday, March 5…)");
|
||||
|
|
@ -1334,6 +1333,15 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
d.onChange(async (v) => { this.plugin.settings.dateDisplayFormat = v as any; await set(); refreshSample(); });
|
||||
});
|
||||
}, ["date", "format", "display"]));
|
||||
cats.datesTime.push(this.renderDef("Use Templates plugin date/time formats", "Use the date/time formats configured in the core Templates plugin instead of the Date display format above. Off = the Date display format above is used everywhere.", (s) => {
|
||||
s.addToggle((t) => t.setValue(this.plugin.settings.useTemplatesFormat).onChange(async (v) => {
|
||||
this.plugin.settings.useTemplatesFormat = v; await set(); refreshSample();
|
||||
}));
|
||||
const fmt = getTemplatesFormats(this.app);
|
||||
s.descEl.createDiv({ cls: "stashpad-settings-note" }).setText(fmt
|
||||
? `Templates plugin: date = "${fmt.dateFormat}", time = "${fmt.timeFormat}"`
|
||||
: "Templates plugin not enabled.");
|
||||
}, ["templates", "date", "time", "format"]));
|
||||
cats.datesTime.push(this.renderDef("Display timezone", "IANA timezone name (e.g. America/New_York, Europe/London, Asia/Kolkata). Leave blank to use your system timezone.", (s) => {
|
||||
s.addText((t) => {
|
||||
t.setPlaceholder("(system timezone)");
|
||||
|
|
@ -1344,8 +1352,10 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
cats.datesTime.push({
|
||||
name: "Date sample", searchable: false,
|
||||
render: (s: Setting) => {
|
||||
const host = s.settingEl; host.empty(); host.removeClass("setting-item");
|
||||
sampleEl = host.createDiv({ cls: "setting-item-description stashpad-settings-note" });
|
||||
// 0.121.5: mark the host as a settings-section so the sample picks up
|
||||
// the standard 14px inset (otherwise it sits flush against the edge).
|
||||
const host = s.settingEl; host.empty(); host.removeClass("setting-item"); host.addClass("stashpad-settings-section");
|
||||
sampleEl = host.createDiv({ cls: "setting-item-description stashpad-settings-note stashpad-date-sample" });
|
||||
refreshSample();
|
||||
},
|
||||
});
|
||||
|
|
@ -1372,26 +1382,6 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
cats.deleting.push(toggle("Offer to delete attachments with note", "When a note references attachments, the delete modal includes an \"Also delete attachments\" checkbox so orphaned files don't pile up in your vault. Attachments are detected from both ![[…]] embeds in the body and the frontmatter attachments: list. Off = attachments are always preserved on delete (no checkbox shown), and a single childless note with attachments deletes silently.",
|
||||
() => this.plugin.settings.confirmAttachmentDelete, (v) => { this.plugin.settings.confirmAttachmentDelete = v; }, ["delete", "attachment", "orphan"]));
|
||||
|
||||
cats.noteTitles.push(this.renderDef("Slug stop-words", "Words removed from auto-generated note titles (filenames). One per line.", (s) => {
|
||||
let textarea: HTMLTextAreaElement | null = null;
|
||||
const initial = (this.plugin.settings.slugStopWords?.length ? this.plugin.settings.slugStopWords : DEFAULT_STOPWORDS).join("\n");
|
||||
s.addTextArea((t) => {
|
||||
t.setValue(initial);
|
||||
textarea = (t as any).inputEl as HTMLTextAreaElement;
|
||||
textarea.rows = 6;
|
||||
textarea.setCssStyles({ fontFamily: "var(--font-monospace)" });
|
||||
t.onChange(async (v) => {
|
||||
this.plugin.settings.slugStopWords = (v || "").split(/\r?\n/).map((x) => x.trim().toLowerCase()).filter(Boolean);
|
||||
await set();
|
||||
});
|
||||
}).addExtraButton((b) =>
|
||||
b.setIcon("rotate-ccw").setTooltip("Reset to defaults").onClick(async () => {
|
||||
this.plugin.settings.slugStopWords = [...DEFAULT_STOPWORDS];
|
||||
if (textarea) textarea.value = DEFAULT_STOPWORDS.join("\n");
|
||||
await set();
|
||||
}));
|
||||
}, ["slug", "stopwords", "filename", "title"]));
|
||||
|
||||
cats.foldersStorage.push(this.sectionDef("Cross-Stashpad Search Scope", "Toggle each Stashpad's pill to choose whether its notes contribute to cross-folder search. Excluded folders are still valid move destinations. Also: create a new Stashpad.", (host) => {
|
||||
const folders = this.plugin.discoverStashpadFolders();
|
||||
new Setting(host)
|
||||
|
|
@ -1433,6 +1423,29 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
this.renderFolderPlacementList(host);
|
||||
}, ["folder", "panel", "pin", "pinned", "downrank", "hide", "hidden", "restore", "placement"]));
|
||||
|
||||
// 0.121.6: Note Titles folded into Folders & Storage as a trailing
|
||||
// sub-section (was its own tab). Keep the labelled "🏷️ Note Titles" header.
|
||||
cats.foldersStorage.push(this.headingDef("🏷️ Note Titles"));
|
||||
cats.foldersStorage.push(this.renderDef("Slug stop-words", "Words removed from auto-generated note titles (filenames). One per line.", (s) => {
|
||||
let textarea: HTMLTextAreaElement | null = null;
|
||||
const initial = (this.plugin.settings.slugStopWords?.length ? this.plugin.settings.slugStopWords : DEFAULT_STOPWORDS).join("\n");
|
||||
s.addTextArea((t) => {
|
||||
t.setValue(initial);
|
||||
textarea = (t as any).inputEl as HTMLTextAreaElement;
|
||||
textarea.rows = 6;
|
||||
textarea.setCssStyles({ fontFamily: "var(--font-monospace)" });
|
||||
t.onChange(async (v) => {
|
||||
this.plugin.settings.slugStopWords = (v || "").split(/\r?\n/).map((x) => x.trim().toLowerCase()).filter(Boolean);
|
||||
await set();
|
||||
});
|
||||
}).addExtraButton((b) =>
|
||||
b.setIcon("rotate-ccw").setTooltip("Reset to defaults").onClick(async () => {
|
||||
this.plugin.settings.slugStopWords = [...DEFAULT_STOPWORDS];
|
||||
if (textarea) textarea.value = DEFAULT_STOPWORDS.join("\n");
|
||||
await set();
|
||||
}));
|
||||
}, ["slug", "stopwords", "filename", "title", "note titles"]));
|
||||
|
||||
cats.composerCopy.push(toggle("Autofocus composer after sending", "After Enter-submitting a note, return focus to the composer so you can keep typing. Off keeps focus in the list — useful if you want arrow keys to work without an extra click.",
|
||||
() => this.plugin.settings.autofocusComposerAfterSend, (v) => { this.plugin.settings.autofocusComposerAfterSend = v; }, ["composer", "focus", "send"]));
|
||||
cats.windowsTabs.push(toggle("Open in new window — duplicate tab", "ON: the new-window button (in the time-filter row) duplicates the current Stashpad tab — original stays open in the main window. OFF: the leaf is MOVED to the new window, closing the original tab.",
|
||||
|
|
@ -1845,12 +1858,29 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
private renderJdIndexSection(containerEl: HTMLElement): void {
|
||||
const header = new Setting(containerEl).setName("JD Index Builder").setHeading();
|
||||
header.settingEl.id = "stashpad-jd-index-section";
|
||||
const blurb = containerEl.createEl("p", { cls: "setting-item-description" });
|
||||
blurb.innerHTML =
|
||||
'Builds a Johnny-Decimal-style index inside a designated Stashpad folder. Two commands:' +
|
||||
'<br/><strong>Preview</strong> overwrites the designated folder’s HOME note body with the would-be hierarchy + everything that didn’t match. Frontmatter is preserved; everything below it is replaced.' +
|
||||
'<br/><strong>Build</strong> creates an actual hierarchy of Stashpad notes (one per prefix), with child→parent relationships matching the dotted segments.' +
|
||||
'<br/>Matches strict prefixes only: all-digits (<code>10 Life</code>) or alphanumeric-with-dots (<code>1.2 Family</code>, <code>animal.duck.yellow Eggs</code>). Mixed schemes sort numbers first, then alphabetically.';
|
||||
// 0.121.2: render the description into a .stashpad-settings-section host so
|
||||
// it picks up the standard 14px inset (a bare <p> on containerEl sits flush
|
||||
// against the modal edge — it's not a Setting row and not a section child).
|
||||
const blurb = containerEl.createEl("div", { cls: "stashpad-settings-section" })
|
||||
.createEl("p", { cls: "setting-item-description" });
|
||||
// 0.121.4: built with safe DOM helpers instead of innerHTML (the Obsidian
|
||||
// store linter flags raw HTML strings). Same rendered output: <br> breaks,
|
||||
// <strong> command names, <code> prefix examples.
|
||||
blurb.appendText("Builds a Johnny-Decimal-style index inside a designated Stashpad folder. Two commands:");
|
||||
blurb.createEl("br");
|
||||
blurb.createEl("strong", { text: "Preview" });
|
||||
blurb.appendText(" overwrites the designated folder’s HOME note body with the would-be hierarchy + everything that didn’t match. Frontmatter is preserved; everything below it is replaced.");
|
||||
blurb.createEl("br");
|
||||
blurb.createEl("strong", { text: "Build" });
|
||||
blurb.appendText(" creates an actual hierarchy of Stashpad notes (one per prefix), with child→parent relationships matching the dotted segments.");
|
||||
blurb.createEl("br");
|
||||
blurb.appendText("Matches strict prefixes only: all-digits (");
|
||||
blurb.createEl("code", { text: "10 Life" });
|
||||
blurb.appendText(") or alphanumeric-with-dots (");
|
||||
blurb.createEl("code", { text: "1.2 Family" });
|
||||
blurb.appendText(", ");
|
||||
blurb.createEl("code", { text: "animal.duck.yellow Eggs" });
|
||||
blurb.appendText("). Mixed schemes sort numbers first, then alphabetically.");
|
||||
|
||||
const stashpadFolders = this.plugin.discoverStashpadFolders();
|
||||
|
||||
|
|
@ -1923,7 +1953,8 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
|
||||
// Preview line: shows current counts before building.
|
||||
const scan = scanForJdNotes(this.app, this.plugin, this.plugin.settings);
|
||||
const previewEl = containerEl.createEl("p", { cls: "setting-item-description" });
|
||||
const previewEl = containerEl.createEl("div", { cls: "stashpad-settings-section" })
|
||||
.createEl("p", { cls: "setting-item-description" });
|
||||
const skippedSuffix = scan.skippedStashpadNotes.length > 0
|
||||
? ` (${scan.skippedStashpadNotes.length} Stashpad-folder note${scan.skippedStashpadNotes.length === 1 ? "" : "s"} excluded by default)`
|
||||
: "";
|
||||
|
|
@ -1934,6 +1965,7 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(containerEl)
|
||||
.setName("Actions")
|
||||
.setClass("stashpad-jd-actions")
|
||||
.setDesc("Preview aggressively overwrites the designated folder's HOME note body (frontmatter preserved). Build creates Stashpad notes (existing notes with the same jdPrefix are updated, not duplicated).")
|
||||
.addButton((b) => {
|
||||
b.setButtonText("Preview");
|
||||
|
|
@ -2158,6 +2190,8 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
/** The "folders you've worked in" list, extracted so the authorship sectionDef
|
||||
* can render it fresh at display time. */
|
||||
private renderAuthoredFolders(parent: HTMLElement): void {
|
||||
// 0.121.5: this sectionDef renders no heading of its own, so label the list.
|
||||
new Setting(parent).setName("Folders you've worked in").setHeading();
|
||||
const folders = this.plugin.collectAuthoredFolders();
|
||||
if (folders.length === 0) { parent.createEl("p", { cls: "setting-item-description", text: "No authored or contributed folders yet." }); return; }
|
||||
const list = parent.createDiv({ cls: "stashpad-authored-folders-list" });
|
||||
|
|
@ -2227,7 +2261,7 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
|
||||
new Setting(parent)
|
||||
.setName("Enable OKF")
|
||||
.setDesc(this.codeDesc("Master switch. When on, you choose which folders use OKF by assigning the OKF template to them in Settings → Templates (all / some / none — your call). Those folders then get OKF frontmatter and a maintained `index.md`. Turning this off leaves existing OKF files in place; it just stops maintaining them."))
|
||||
.setDesc(this.codeDesc("Master switch. When on, you choose which folders use OKF by assigning the OKF template to them in Stashpad's own Templates section (the 📄 Templates settings here — NOT Obsidian's core Templates plugin) — all / some / none, your call. Those folders then get OKF frontmatter and a maintained `index.md`. Turning this off leaves existing OKF files in place; it just stops maintaining them."))
|
||||
.addToggle((t) => t.setValue(this.plugin.settings.okfEnabled).onChange(async (v) => {
|
||||
this.plugin.settings.okfEnabled = v;
|
||||
await this.plugin.saveSettings();
|
||||
|
|
@ -2244,7 +2278,7 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
const steps = parent.createEl("div", { cls: "setting-item-description stashpad-okf-howto" });
|
||||
steps.createEl("p", { text: "How to use OKF in a folder:" });
|
||||
const ol = steps.createEl("ol");
|
||||
this.appendCode(ol.createEl("li"), `Open Templates and set a folder's template to \`${okfPath}\` (archive folders are skipped).`);
|
||||
this.appendCode(ol.createEl("li"), `Open Stashpad's 📄 Templates section (here in Stashpad's settings — not Obsidian's core Templates plugin) and set a folder's template to \`${okfPath}\` (archive folders are skipped).`);
|
||||
this.appendCode(ol.createEl("li"), "Hit Rebuild below to write OKF frontmatter (`okfParent`/`okfChildren` + `okfType`/`okfTitle`/`okfTimestamp`) and generate that folder's `index.md`.");
|
||||
this.appendCode(ol.createEl("li"), "Right-click a note (or a selection) → “Export as OKF…” to save a `.zip` / `.tar.gz` bundle (or `.stash`).");
|
||||
steps.createEl("p", { cls: "stashpad-okf-soon", text: "OKF frontmatter + index.md refresh automatically a few seconds after you add, move, or delete notes — NOT instantly. Use Rebuild for an immediate pass." });
|
||||
|
|
@ -2817,15 +2851,28 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
/** One settings row: label + 2 chord recorders + active-slot toggle. */
|
||||
private renderBindingRow(row: Setting, meta: CommandMeta): void {
|
||||
row.setName(meta.label).setDesc(meta.desc);
|
||||
row.settingEl.addClass("stashpad-binding-row");
|
||||
const get = () => this.plugin.settings.bindings[meta.id];
|
||||
|
||||
let primaryInput: HTMLInputElement;
|
||||
let secondaryInput: HTMLInputElement;
|
||||
// Late-bound: assigned once the pill toggle is built below.
|
||||
let refreshToggle = (): void => {};
|
||||
// 0.121.11 (#8/#9): a tri-state segmented control (Left | Both | Right) on
|
||||
// the LEFT, then the two key slots STACKED on the right — replacing the old
|
||||
// L/R pill + separate "Use both" checkbox. `setState` is assigned once the
|
||||
// slots exist (below); the buttons just call it.
|
||||
let setState = async (_s: "left" | "both" | "right"): Promise<void> => {};
|
||||
const seg = row.controlEl.createDiv({ cls: "stashpad-binding-seg", attr: { role: "group", "aria-label": "Active binding" } });
|
||||
const segBtns = {
|
||||
left: seg.createEl("button", { cls: "stashpad-binding-seg-btn", text: "Left" }),
|
||||
both: seg.createEl("button", { cls: "stashpad-binding-seg-btn", text: "Both" }),
|
||||
right: seg.createEl("button", { cls: "stashpad-binding-seg-btn", text: "Right" }),
|
||||
};
|
||||
(["left", "both", "right"] as const).forEach((k) => { segBtns[k].onclick = () => void setState(k); });
|
||||
const slotsCol = row.controlEl.createDiv({ cls: "stashpad-binding-slots" });
|
||||
|
||||
const renderSlot = (which: "primary" | "secondary"): HTMLInputElement => {
|
||||
const wrap = row.controlEl.createDiv({ cls: "stashpad-binding-slot" });
|
||||
const wrap = slotsCol.createDiv({ cls: "stashpad-binding-slot" });
|
||||
const input = wrap.createEl("input", { type: "text" });
|
||||
input.readOnly = true;
|
||||
input.placeholder = "Click & press a key";
|
||||
|
|
@ -2850,7 +2897,10 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
syncRevert();
|
||||
});
|
||||
};
|
||||
const clearBtn = wrap.createEl("button", { cls: "stashpad-binding-clear", text: "×" });
|
||||
// 0.121.12 (#4): clear (×) + revert (↺) share a wrapper so they can stack
|
||||
// vertically on mobile instead of widening the row.
|
||||
const slotBtns = wrap.createDiv({ cls: "stashpad-binding-slot-btns" });
|
||||
const clearBtn = slotBtns.createEl("button", { cls: "stashpad-binding-clear", text: "×" });
|
||||
clearBtn.title = "Clear this slot";
|
||||
clearBtn.onclick = async () => {
|
||||
this.plugin.settings.bindings[meta.id][which] = "";
|
||||
|
|
@ -2866,7 +2916,7 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
// one click. Hidden when the slot already matches its default (nothing to
|
||||
// revert). A slot with no default ("") only shows it after the user binds
|
||||
// something, and reverting then clears the slot.
|
||||
const revertBtn = wrap.createEl("button", { cls: "stashpad-binding-revert" });
|
||||
const revertBtn = slotBtns.createEl("button", { cls: "stashpad-binding-revert" });
|
||||
setIcon(revertBtn, "rotate-ccw");
|
||||
const syncRevert = (): void => {
|
||||
const cur = get()[which];
|
||||
|
|
@ -2892,23 +2942,14 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
secondaryInput = renderSlot("secondary");
|
||||
void primaryInput; void secondaryInput;
|
||||
|
||||
// Active-slot pill toggle: a rounded track with a sliding knob whose
|
||||
// label is "L" when on the left (primary active) and "R" when on the
|
||||
// right (secondary active). Greyed out unless BOTH slots are bound.
|
||||
const pill = row.controlEl.createDiv({ cls: "stashpad-binding-pill" });
|
||||
pill.setAttribute("role", "switch");
|
||||
pill.setAttribute("tabindex", "0");
|
||||
const knob = pill.createDiv({ cls: "stashpad-binding-pill-knob" });
|
||||
|
||||
// 0.59.1: "Use both" checkbox — when checked, both bindings fire and
|
||||
// the L/R pill becomes a no-op (visually greyed). Only meaningful
|
||||
// when both slots are filled.
|
||||
const bothWrap = row.controlEl.createDiv({ cls: "stashpad-binding-useboth" });
|
||||
const bothCb = bothWrap.createEl("input", { type: "checkbox" });
|
||||
bothCb.title = "Use both bindings simultaneously (overrides the L/R toggle)";
|
||||
bothWrap.createSpan({ text: "Use both" });
|
||||
bothCb.onchange = async () => {
|
||||
this.plugin.settings.bindings[meta.id].useBoth = bothCb.checked;
|
||||
// 0.121.11: tri-state behaviour. Left = primary only, Right = secondary
|
||||
// only, Both = both fire. Only meaningful once BOTH slots are bound (until
|
||||
// then the lone bound slot just fires and the control is disabled).
|
||||
setState = async (state: "left" | "both" | "right"): Promise<void> => {
|
||||
const b = this.plugin.settings.bindings[meta.id];
|
||||
if (!(b.primary && b.secondary)) return;
|
||||
if (state === "both") { b.useBoth = true; }
|
||||
else { b.useBoth = false; b.preferRight = state === "right"; }
|
||||
await this.plugin.saveSettings();
|
||||
refreshToggle();
|
||||
};
|
||||
|
|
@ -2916,36 +2957,15 @@ export class StashpadSettingTab extends PluginSettingTab {
|
|||
refreshToggle = (): void => {
|
||||
const b = get();
|
||||
const both = !!(b.primary && b.secondary);
|
||||
bothCb.checked = !!b.useBoth;
|
||||
bothCb.disabled = !both;
|
||||
bothWrap.toggleClass("is-disabled", !both);
|
||||
const useBoth = !!b.useBoth && both;
|
||||
// L/R pill: disabled when fewer than two slots OR when useBoth wins.
|
||||
pill.toggleClass("is-disabled", !both || useBoth);
|
||||
pill.toggleClass("is-right", b.preferRight);
|
||||
pill.setAttribute("aria-checked", String(b.preferRight));
|
||||
pill.setAttribute("aria-disabled", String(!both || useBoth));
|
||||
knob.setText(b.preferRight ? "R" : "L");
|
||||
pill.title = !both
|
||||
? "Set both slots to enable the toggle"
|
||||
: useBoth
|
||||
? "Overridden by \"Use both\""
|
||||
: (b.preferRight ? "Right slot active — click for left" : "Left slot active — click for right");
|
||||
};
|
||||
|
||||
const flip = async () => {
|
||||
const b = get();
|
||||
if (!b.primary || !b.secondary) return;
|
||||
this.plugin.settings.bindings[meta.id].preferRight = !b.preferRight;
|
||||
refreshToggle();
|
||||
await this.plugin.saveSettings();
|
||||
};
|
||||
pill.onclick = () => void flip();
|
||||
pill.onkeydown = (e) => {
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
void flip();
|
||||
}
|
||||
const active: "left" | "both" | "right" = b.useBoth ? "both" : (b.preferRight ? "right" : "left");
|
||||
(["left", "both", "right"] as const).forEach((k) => {
|
||||
segBtns[k].toggleClass("is-active", both && k === active);
|
||||
segBtns[k].disabled = !both;
|
||||
});
|
||||
seg.toggleClass("is-disabled", !both);
|
||||
seg.title = both
|
||||
? "Which binding is active — Left, Both, or Right"
|
||||
: "Bind both slots to choose Left / Both / Right";
|
||||
};
|
||||
|
||||
refreshToggle();
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from "./types";
|
||||
import { TreeIndex } from "./tree-index";
|
||||
import { perf } from "./perf";
|
||||
import { formatDateTime } from "./format";
|
||||
import { formatDateTime, formatDateOnly, formatTimeOnly } from "./format";
|
||||
import { OrderStore } from "./order-store";
|
||||
import { SortStore, SORT_MODE_LABELS, SORT_MODES_ORDER } from "./sort-store";
|
||||
import { FrontmatterSyncQueue, rebootstrapFolderFrontmatter } from "./frontmatter-sync";
|
||||
|
|
@ -11021,7 +11021,12 @@ export class StashpadView extends ItemView {
|
|||
const fmt = getTemplatesFormats(this.app);
|
||||
if (fmt) return `${d.format(fmt.dateFormat)}\n${d.format(fmt.timeFormat)}`;
|
||||
}
|
||||
return `${d.format("YYYY.MM.DD")}\n${d.format("HH:mm A")}`;
|
||||
// 0.121.7: when NOT using the Templates plugin format, honour the user's
|
||||
// "Date display format" dropdown (+ timezone) for the two-line list
|
||||
// timestamp — previously this ignored the dropdown and was hardcoded to
|
||||
// YYYY.MM.DD / HH:mm A, so the dropdown only affected the detail/tasks panels.
|
||||
const ms = d.valueOf();
|
||||
return `${formatDateOnly(ms, settings)}\n${formatTimeOnly(ms, settings)}`;
|
||||
}
|
||||
/** public: read by extracted command modules (commands/*.ts). */
|
||||
formatTimeInline(iso: string): string {
|
||||
|
|
|
|||
193
styles.css
193
styles.css
|
|
@ -893,6 +893,27 @@
|
|||
flex: 0 0 auto;
|
||||
}
|
||||
/* 0.118.0: live preview swatch next to the per-folder icon input in settings. */
|
||||
/* 0.121.10: icon-search suggestion rows (IconSuggest) — preview + id label. */
|
||||
.stashpad-icon-suggest-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.stashpad-icon-suggest-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.stashpad-icon-suggest-icon .svg-icon { width: 18px; height: 18px; }
|
||||
.stashpad-icon-suggest-label {
|
||||
font-family: var(--font-monospace);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.stashpad-folder-icon-preview {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -1411,7 +1432,10 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 0 12px;
|
||||
/* 0.121.5: inset to align with the section heading + Setting rows.
|
||||
0.121.7: bumped 14→20px per feedback that 14 read under-indented vs the
|
||||
rest. (Approximate — verify against the live settings.) */
|
||||
padding: 4px 20px 12px;
|
||||
}
|
||||
.stashpad-authored-folder-row {
|
||||
display: flex;
|
||||
|
|
@ -1997,11 +2021,17 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
.stashpad-folder-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 0 12px;
|
||||
/* 0.121.2: 14px left/right inset so these custom rows align with the Setting
|
||||
rows + section text (PROVISION insets text element types, not custom divs,
|
||||
so the list was sitting flush against the modal edge). */
|
||||
padding: 4px 14px 12px;
|
||||
}
|
||||
/* 0.95.2: placement overview group heading in settings */
|
||||
.stashpad-folder-placement-group {
|
||||
margin-top: 10px;
|
||||
/* 0.121.2: match the 14px inset of its rows + the section text above. */
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-muted);
|
||||
|
|
@ -2269,7 +2299,10 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
/* 0.121.5: 14px left/right inset so the author cards align with the rest. */
|
||||
margin: 8px 0 4px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
.stashpad-known-author-row {
|
||||
padding: 6px 10px;
|
||||
|
|
@ -2497,18 +2530,12 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
text-align: center;
|
||||
font-family: var(--font-monospace);
|
||||
cursor: pointer;
|
||||
}
|
||||
.stashpad-binding-useboth {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-right: 8px;
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.stashpad-binding-useboth.is-disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
/* 0.121.12 (#4): compact rounded key-chip look (desktop + mobile). */
|
||||
height: 26px;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 5px;
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
.stashpad-binding-input.is-recording {
|
||||
background: var(--background-modifier-active-hover);
|
||||
|
|
@ -2559,49 +2586,77 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
/* Pill toggle: a rounded track with a knob that slides L↔R. The knob
|
||||
shows "L" when on the left (primary active) or "R" when on the right
|
||||
(secondary active). Disabled state dims the whole pill. */
|
||||
.stashpad-binding-pill {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 22px;
|
||||
margin-left: 8px;
|
||||
border-radius: 11px;
|
||||
background: var(--background-modifier-border);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
vertical-align: middle;
|
||||
outline: none;
|
||||
}
|
||||
.stashpad-binding-pill:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--interactive-accent);
|
||||
}
|
||||
.stashpad-binding-pill.is-right {
|
||||
background: var(--interactive-accent);
|
||||
}
|
||||
.stashpad-binding-pill.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.stashpad-binding-pill-knob {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
/* 0.121.11 (#8): hotkey row — tri-state control on the LEFT, the two key slots
|
||||
STACKED on the right so the binding controls sit at the right end. */
|
||||
.stashpad-binding-row .setting-item-control {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: left 0.15s ease, transform 0.15s ease;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
gap: 12px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.stashpad-binding-pill.is-right .stashpad-binding-pill-knob {
|
||||
left: calc(100% - 20px);
|
||||
.stashpad-binding-slots {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.stashpad-binding-slots .stashpad-binding-slot { margin-right: 0; }
|
||||
/* 0.121.11 (#9): Left | Both | Right segmented control — replaces the old L/R
|
||||
pill + separate "Use both" checkbox. */
|
||||
.stashpad-binding-seg {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stashpad-binding-seg-btn {
|
||||
padding: 3px 10px;
|
||||
font-size: var(--font-ui-smaller);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.stashpad-binding-seg-btn:not(:last-child) {
|
||||
border-right: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
.stashpad-binding-seg-btn:hover:not(:disabled) {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.stashpad-binding-seg-btn.is-active {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
.stashpad-binding-seg.is-disabled { opacity: 0.45; }
|
||||
.stashpad-binding-seg-btn:disabled { cursor: not-allowed; }
|
||||
/* 0.121.12 (#4): the clear (×) + revert (↺) buttons sit side-by-side on desktop,
|
||||
stacked vertically on mobile to keep the (stacked) slots narrow. */
|
||||
.stashpad-binding-slot-btns { display: inline-flex; gap: 4px; align-items: center; }
|
||||
body.is-mobile .stashpad-binding-slot-btns { flex-direction: column; gap: 3px; }
|
||||
|
||||
/* 0.121.12 (#1/#3): give each folded-tab sub-heading top breathing room — the
|
||||
first one (e.g. "List & Display", "JD Index") was cramped right under the
|
||||
page title with no separation. */
|
||||
.stashpad-settings-section .setting-item-heading {
|
||||
margin-top: 16px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
/* 0.121.13: the FIRST sub-heading of a folded tab (top of Behavior / Org
|
||||
Systems) gets extra top room so it doesn't crowd the page title. */
|
||||
.stashpad-settings-section:first-child .setting-item-heading {
|
||||
margin-top: 28px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
/* 0.121.12 (#2): the JD "Actions" buttons spilled off the right on mobile —
|
||||
let them wrap instead. */
|
||||
body.is-mobile .stashpad-jd-actions .setting-item-control {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
/* Color picker modal */
|
||||
.stashpad-color-modal { width: min(360px, 90vw); }
|
||||
|
|
@ -2743,7 +2798,16 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
|
||||
/* Per-folder encryption: filename sub-toggles sit indented under their content
|
||||
toggle, dimmed when disabled. */
|
||||
.stashpad-subsetting { padding-left: 22px; opacity: 0.85; }
|
||||
/* 0.121.2: sub-settings (e.g. the encryption filename sub-toggle under its
|
||||
content toggle) get a left guide-line so the nesting reads structurally, not
|
||||
just by indent + dim. Additive + stashpad-scoped (never touches global
|
||||
.setting-item). */
|
||||
.stashpad-subsetting {
|
||||
padding-left: 18px;
|
||||
margin-left: 6px;
|
||||
border-left: 2px solid var(--background-modifier-border);
|
||||
opacity: 0.85;
|
||||
}
|
||||
.stashpad-subsetting .setting-item-name { font-size: var(--font-ui-smaller); color: var(--text-muted); }
|
||||
.stashpad-trash-iconbtn { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; padding: 4px 6px; cursor: pointer; --icon-size: 15px; background: transparent; border: none; box-shadow: none; color: var(--text-muted); }
|
||||
.stashpad-trash-iconbtn:hover { color: var(--text-normal); }
|
||||
|
|
@ -2810,6 +2874,17 @@ body.is-phone .modal.stashpad-compact-modal.stashpad-breadcrumb-modal {
|
|||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
}
|
||||
/* 0.121.7 (#2): the live date sample read under-indented vs the rest — bump it
|
||||
a touch past the standard 14px. (Approximate — verify against live settings.) */
|
||||
.stashpad-settings-section .stashpad-date-sample {
|
||||
padding-left: 20px;
|
||||
}
|
||||
/* 0.121.7 (#1): give each settings page some breathing room at the very bottom
|
||||
so the last section isn't flush against the modal edge (e.g. the encryption
|
||||
page's trailing advice text). Only the last section in a tab is affected. */
|
||||
.stashpad-settings-section:last-child {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.stashpad-ai-disclaimer {
|
||||
margin: 4px 14px 12px; padding: 10px 12px; border-radius: var(--radius-m, 6px);
|
||||
border: 1px solid var(--color-red, #e24b4a);
|
||||
|
|
@ -5072,7 +5147,9 @@ body.stashpad-folderpanel-resizing { cursor: row-resize; user-select: none; }
|
|||
color: var(--text-normal);
|
||||
background: rgba(var(--color-red-rgb, 224, 49, 49), 0.12);
|
||||
border-left: 3px solid var(--color-red, #e03131);
|
||||
padding: 8px 12px;
|
||||
/* 0.121.5: a touch more vertical padding + more bottom breathing room so the
|
||||
advice banner doesn't feel cramped (L/R stays at the section's 14px inset). */
|
||||
padding: 11px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 4px 0 12px;
|
||||
margin: 4px 0 18px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue