From 04a97ea27f0d065514330b5b2b9546ee325dfd8b Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:06:48 -0700
Subject: [PATCH 01/10] spec: add palette long-title fix proposal
---
.../fix-palette-long-title/proposal.md | 19 +++++++++++++++++++
.../changes/fix-palette-long-title/tasks.md | 6 ++++++
2 files changed, 25 insertions(+)
create mode 100644 openspec/changes/fix-palette-long-title/proposal.md
create mode 100644 openspec/changes/fix-palette-long-title/tasks.md
diff --git a/openspec/changes/fix-palette-long-title/proposal.md b/openspec/changes/fix-palette-long-title/proposal.md
new file mode 100644
index 0000000..db7264d
--- /dev/null
+++ b/openspec/changes/fix-palette-long-title/proposal.md
@@ -0,0 +1,19 @@
+# Change: Keep the action label readable for long note titles in the palette
+
+## Why
+Note-scoped commands in the command palette render the note title as a breadcrumb
+prefix before the action label, for example "My very long note title > Pin note".
+The title and the action shared a single ellipsized line, so a long title consumed
+the whole row and pushed the action label out of view. The user could no longer tell
+which action a row would run.
+
+## What Changes
+- Render the palette title region as a flex row so the action label keeps its width.
+- Give the note-title and folder prefix its own max-width and ellipsis, so a long
+ title truncates on its own while the separator and action stay fully visible.
+- Keep the breadcrumb separator, the leading icon, and the action label from shrinking.
+
+## Impact
+A presentational change confined to `src/lib/components/CommandPalette.svelte` (one
+markup wrap plus CSS). No command logic, store, or Rust changes. The folder breadcrumb
+shown on searched leaves benefits from the same prefix cap.
diff --git a/openspec/changes/fix-palette-long-title/tasks.md b/openspec/changes/fix-palette-long-title/tasks.md
new file mode 100644
index 0000000..196f2af
--- /dev/null
+++ b/openspec/changes/fix-palette-long-title/tasks.md
@@ -0,0 +1,6 @@
+# Tasks
+
+- [x] Wrap the action title in its own element so it can stay non-shrinking
+- [x] Make the title region a flex row and cap the prefix with its own ellipsis
+- [x] Keep icon, separator, and action label from shrinking
+- [x] Confirm svelte-check and Vitest stay green
From b6ce6eb5a7e9e3e93c4e4eb465bca0b47fbafa6d Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:06:48 -0700
Subject: [PATCH 02/10] fix(palette): keep the action label readable for long
note titles
Note-scoped commands show the note title as a breadcrumb prefix before the
action. A long title used to consume the row and hide the action. The title
region is now a flex row, the prefix takes its own max-width and ellipsis, and
the separator, icon, and action label no longer shrink.
---
src/lib/components/CommandPalette.svelte | 22 +++++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/src/lib/components/CommandPalette.svelte b/src/lib/components/CommandPalette.svelte
index 090545f..82d797c 100644
--- a/src/lib/components/CommandPalette.svelte
+++ b/src/lib/components/CommandPalette.svelte
@@ -151,7 +151,7 @@
onmousemove={() => (active = i)}
>
- {#if cmd.icon}{cmd.icon()}{/if}{#if cmd.prefix}{cmd.prefix}›{/if}{#if cmd.parent && cmd.parent !== currentParent}{findCommand(commands, cmd.parent)?.title}›{/if}{cmd.title}
+ {#if cmd.icon}{cmd.icon()}{/if}{#if cmd.prefix}{cmd.prefix}›{/if}{#if cmd.parent && cmd.parent !== currentParent}{findCommand(commands, cmd.parent)?.title}›{/if}{cmd.title}
{#if cmd.isActive?.()}
@@ -255,18 +255,34 @@
.cmd-icon {
margin-right: 8px;
color: var(--accent-text);
+ flex-shrink: 0;
}
+ /* A flex row so the action label keeps its width while a long note-title
+ prefix takes its own ellipsis instead of clipping the whole line. */
.cmd-title {
+ display: flex;
+ align-items: baseline;
+ min-width: 0;
overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
}
.cmd-parent {
color: var(--text-tertiary);
+ max-width: 16ch;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex-shrink: 1;
}
.cmd-crumb {
margin: 0 6px;
color: var(--text-tertiary);
+ flex-shrink: 0;
+ }
+ .cmd-action {
+ flex-shrink: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.cmd-meta {
display: flex;
From 114663e50f343ce11ecd7c192c567c29e7134edb Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:11:43 -0700
Subject: [PATCH 03/10] spec: add native titlebar theming proposal
---
.../feat-titlebar-theme-sync/proposal.md | 20 +++++++++++++++++++
.../changes/feat-titlebar-theme-sync/tasks.md | 6 ++++++
2 files changed, 26 insertions(+)
create mode 100644 openspec/changes/feat-titlebar-theme-sync/proposal.md
create mode 100644 openspec/changes/feat-titlebar-theme-sync/tasks.md
diff --git a/openspec/changes/feat-titlebar-theme-sync/proposal.md b/openspec/changes/feat-titlebar-theme-sync/proposal.md
new file mode 100644
index 0000000..ff17fea
--- /dev/null
+++ b/openspec/changes/feat-titlebar-theme-sync/proposal.md
@@ -0,0 +1,20 @@
+# Change: Native titlebar follows the in-app dark/light theme
+
+## Why
+The library window keeps the native macOS titlebar, but the theme store never tells
+macOS when the resolved light/dark variant changes. The titlebar and traffic lights
+stay locked to whatever the system appearance was at launch, so switching to a dark
+theme under a light system (or the reverse) leaves a mismatched bright titlebar above
+a dark app.
+
+## What Changes
+- Add a thin `set_window_theme` command that sets the library window's native theme
+ (Light or Dark) through Tauri's window API.
+- Expose it as `setWindowTheme` in the IPC client.
+- Sync it from the theme store whenever the resolved variant changes, alongside the
+ existing vibrancy sync, so every theme or mode change repaints the chrome.
+
+## Impact
+Three small touch points: one Tauri command, one IPC wrapper, and one private sync
+method on the theme store. The core crate is untouched. A no-op off macOS and in browser
+dev. The borderless capture window has no native chrome and is left alone.
diff --git a/openspec/changes/feat-titlebar-theme-sync/tasks.md b/openspec/changes/feat-titlebar-theme-sync/tasks.md
new file mode 100644
index 0000000..d795922
--- /dev/null
+++ b/openspec/changes/feat-titlebar-theme-sync/tasks.md
@@ -0,0 +1,6 @@
+# Tasks
+
+- [x] Add the set_window_theme Tauri command and register it in the handler
+- [x] Add the setWindowTheme IPC client wrapper
+- [x] Sync the window theme from the theme store on every resolved-variant change
+- [ ] Confirm the native titlebar flips live when toggling light/dark in the running app
From af9708ba55a24dde841f7211b24a0ac5f7a26141 Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:11:44 -0700
Subject: [PATCH 04/10] feat(window): sync the native titlebar to the in-app
theme
The native macOS titlebar stayed on the launch-time system appearance even
after switching to a dark or light theme in the app. A thin set_window_theme
command sets the library window's native theme, and the theme store calls it
whenever the resolved variant changes, next to the existing vibrancy sync.
---
src-tauri/src/lib.rs | 19 +++++++++++++++++++
src/lib/api/client.ts | 4 ++++
src/lib/stores/theme.svelte.ts | 16 +++++++++++++++-
3 files changed, 38 insertions(+), 1 deletion(-)
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index f715b5c..bfd1be4 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -335,6 +335,24 @@ fn set_window_vibrancy(app: AppHandle, material: Option) {
}
}
+/// Match the native library window's theme (titlebar and traffic-light treatment)
+/// to the in-app light/dark variant. Tauri maps this to the window's OS appearance,
+/// so the chrome follows the active theme instead of the launch-time system setting.
+/// An unknown variant is a no-op; the borderless capture window has no native chrome
+/// and is left alone.
+#[tauri::command]
+fn set_window_theme(app: AppHandle, variant: String) {
+ use tauri::Theme;
+ let theme = match variant.as_str() {
+ "light" => Theme::Light,
+ "dark" => Theme::Dark,
+ _ => return,
+ };
+ if let Some(win) = app.get_webview_window("library") {
+ let _ = win.set_theme(Some(theme));
+ }
+}
+
// ---- theme file sharing ----
// Thin byte I/O for portable `.intheme.json` theme files. The open/save dialog
// runs in JS via the dialog plugin; Rust only reads/writes the chosen path, so
@@ -796,6 +814,7 @@ pub fn run() {
hide_capture,
open_library,
set_window_vibrancy,
+ set_window_theme,
export_theme_file,
import_theme_file,
export_note_file,
diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts
index 444e03c..8abc1cd 100644
--- a/src/lib/api/client.ts
+++ b/src/lib/api/client.ts
@@ -97,6 +97,10 @@ export const openUrl = (url: string) => call("open_url", { url });
// window. A no-op off macOS; the material string is one of theme MATERIAL_KEYS.
export const setWindowVibrancy = (material: string | null) =>
call("set_window_vibrancy", { material });
+// Match the native library window theme (titlebar / traffic-light treatment) to
+// the in-app variant. A no-op off macOS; the capture window is left alone.
+export const setWindowTheme = (variant: "light" | "dark") =>
+ call("set_window_theme", { variant });
// ---- theme files ----
// Byte I/O for portable .intheme.json files; the open/save dialog runs in JS.
diff --git a/src/lib/stores/theme.svelte.ts b/src/lib/stores/theme.svelte.ts
index a7cfd08..0885350 100644
--- a/src/lib/stores/theme.svelte.ts
+++ b/src/lib/stores/theme.svelte.ts
@@ -3,7 +3,7 @@
// writes tokens to :root via apply. Persists to the existing settings KV so
// the choice survives restarts and is shared with the capture window.
-import { getSetting, setSetting, setWindowVibrancy } from "$lib/api/client";
+import { getSetting, setSetting, setWindowVibrancy, setWindowTheme } from "$lib/api/client";
import { applyTheme } from "$lib/themes/apply";
import { BUILTIN_THEMES, DEFAULT_THEME_ID } from "$lib/themes/builtin";
import { validateTheme } from "$lib/themes/validate";
@@ -90,6 +90,7 @@ class ThemeStore {
if (font) document.documentElement.style.setProperty("--font-body", font.value);
}
void this.#syncVibrancy();
+ void this.#syncWindowTheme();
}
/** Ask the OS to render (or clear) the active theme's window material, and
@@ -110,6 +111,19 @@ class ThemeStore {
else delete document.documentElement.dataset.vibrancy;
}
+ /** Match the native window chrome (titlebar, traffic lights) to the resolved
+ * light/dark variant, so the macOS decorations follow the in-app theme rather
+ * than the launch-time system appearance. Library window only; a no-op in
+ * browser dev or off macOS, where the native call is unavailable. */
+ async #syncWindowTheme(): Promise {
+ if (location.pathname.startsWith("/capture")) return;
+ try {
+ await setWindowTheme(this.resolvedVariant);
+ } catch {
+ // Best-effort; nothing to clean up if the native call is unavailable.
+ }
+ }
+
setTheme(id: string): void {
if (!this.allThemes.some((t) => t.id === id)) return;
this.activeId = id;
From 792cdef0c9c1135d78c8f834830bb739b72d3914 Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:15:11 -0700
Subject: [PATCH 05/10] spec: add list Tab indentation proposal
---
.../changes/feat-list-tab-indent/proposal.md | 19 +++++++++++++++++++
.../changes/feat-list-tab-indent/tasks.md | 6 ++++++
2 files changed, 25 insertions(+)
create mode 100644 openspec/changes/feat-list-tab-indent/proposal.md
create mode 100644 openspec/changes/feat-list-tab-indent/tasks.md
diff --git a/openspec/changes/feat-list-tab-indent/proposal.md b/openspec/changes/feat-list-tab-indent/proposal.md
new file mode 100644
index 0000000..a519411
--- /dev/null
+++ b/openspec/changes/feat-list-tab-indent/proposal.md
@@ -0,0 +1,19 @@
+# Change: Tab and Shift+Tab nest list items in the editor
+
+## Why
+The editor had no Tab handling for lists, so there was no way to create a sublist or
+un-nest one with the keyboard. Nesting is a basic outlining need: pressing Tab on a list
+item should indent it into a sublist, and Shift+Tab should outdent it back.
+
+## What Changes
+- Add a pure `listIndentChanges` helper that computes the indent/outdent change set for
+ every list line a selection touches.
+- Bind Tab and Shift+Tab in the editor (before the default keymap) to apply it: Tab nests
+ by two spaces, Shift+Tab un-nests by up to two. Bullet (-, *, +) and ordered (1.)
+ markers are recognized.
+- Leave non-list lines to the editor's existing default Tab behavior.
+
+## Impact
+One new pure module with unit tests and one keymap in the editor. Ordered lists are not
+renumbered (markdown renderers handle nesting). Always on, no new setting. CodeMirror
+remaps the selection through the line-anchored changes so the caret stays with its text.
diff --git a/openspec/changes/feat-list-tab-indent/tasks.md b/openspec/changes/feat-list-tab-indent/tasks.md
new file mode 100644
index 0000000..0d624f8
--- /dev/null
+++ b/openspec/changes/feat-list-tab-indent/tasks.md
@@ -0,0 +1,6 @@
+# Tasks
+
+- [x] Add a pure listIndentChanges helper (bullet + ordered, multi-line selection)
+- [x] Unit-test indent, outdent, non-list, mixed, and multi-line cases
+- [x] Bind Tab / Shift+Tab in the editor before the default keymap
+- [ ] Confirm nesting and un-nesting work in the running editor
From f091c8dcd44147ccbb9cf0974f1f66b3b92106f2 Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:15:11 -0700
Subject: [PATCH 06/10] feat(editor): nest list items with Tab and Shift+Tab
Pressing Tab on a bullet or ordered list line now indents it into a sublist,
and Shift+Tab outdents it. A pure listIndentChanges helper computes the
line-anchored change set (unit-tested) and the editor binds Tab / Shift+Tab to
it before the default keymap. Non-list lines keep the default behaviour.
---
src/lib/components/Editor.svelte | 19 ++++++++++++
src/lib/list-indent.test.ts | 49 +++++++++++++++++++++++++++++++
src/lib/list-indent.ts | 50 ++++++++++++++++++++++++++++++++
3 files changed, 118 insertions(+)
create mode 100644 src/lib/list-indent.test.ts
create mode 100644 src/lib/list-indent.ts
diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte
index 8930d81..f86f959 100644
--- a/src/lib/components/Editor.svelte
+++ b/src/lib/components/Editor.svelte
@@ -20,6 +20,7 @@
import { formatEdit, type FormatKind, type Sel } from "$lib/markdown-format";
import { activeMarks, type ActiveMarks } from "$lib/markdown-active";
import { wysiwygExtension, wysiwygTheme, setPreviewMode } from "$lib/wysiwyg";
+ import { listIndentChanges } from "$lib/list-indent";
let {
value = "",
@@ -85,6 +86,12 @@
{ key: "Mod-e", run: () => { applyFormat("code"); return true; } },
{ key: "Mod-Shift-k", run: () => { applyFormat("link"); return true; } },
]),
+ // Tab nests a list item (and Shift-Tab un-nests it). On a non-list line
+ // both return false so the default Tab handling is untouched.
+ keymap.of([
+ { key: "Tab", run: (v) => applyListIndent(v, false) },
+ { key: "Shift-Tab", run: (v) => applyListIndent(v, true) },
+ ]),
keymap.of([...defaultKeymap, ...historyKeymap]),
// GFM base so ~~strikethrough~~ parses (the highlight + active-state
// detection both rely on Strikethrough nodes existing).
@@ -151,6 +158,18 @@
});
view.focus();
}
+
+ // Nest (outdent = false) or un-nest (outdent = true) the list lines the
+ // selection touches. Returns false when nothing is an indentable list line so
+ // CodeMirror falls back to its default Tab handling. CM remaps the selection
+ // through the line-anchored changes, so the caret stays with its text.
+ function applyListIndent(v: EditorView, outdent: boolean): boolean {
+ const { from, to } = v.state.selection.main;
+ const changes = listIndentChanges(v.state.doc.toString(), from, to, outdent);
+ if (changes.length === 0) return false;
+ v.dispatch({ changes, scrollIntoView: true });
+ return true;
+ }
diff --git a/src/lib/list-indent.test.ts b/src/lib/list-indent.test.ts
new file mode 100644
index 0000000..9678235
--- /dev/null
+++ b/src/lib/list-indent.test.ts
@@ -0,0 +1,49 @@
+import { describe, it, expect } from "vitest";
+import { listIndentChanges } from "./list-indent";
+
+describe("listIndentChanges", () => {
+ it("indents a single bullet line", () => {
+ expect(listIndentChanges("- item", 2, 2, false)).toEqual([{ from: 0, insert: " " }]);
+ });
+
+ it("indents ordered list items", () => {
+ expect(listIndentChanges("1. first", 0, 0, false)).toEqual([{ from: 0, insert: " " }]);
+ });
+
+ it("returns no changes for a non-list line", () => {
+ expect(listIndentChanges("just a paragraph", 3, 3, false)).toEqual([]);
+ });
+
+ it("indents a line that is already nested, deeper", () => {
+ expect(listIndentChanges(" - nested", 5, 5, false)).toEqual([{ from: 0, insert: " " }]);
+ });
+
+ it("outdents an indented bullet by two spaces", () => {
+ expect(listIndentChanges(" - item", 4, 4, true)).toEqual([{ from: 0, to: 2 }]);
+ });
+
+ it("does not outdent a top-level bullet", () => {
+ expect(listIndentChanges("- item", 2, 2, true)).toEqual([]);
+ });
+
+ it("removes only the available leading space when outdenting one space", () => {
+ expect(listIndentChanges(" - item", 3, 3, true)).toEqual([{ from: 0, to: 1 }]);
+ });
+
+ it("indents every list line a multi-line selection touches", () => {
+ const doc = "- a\n- b\n- c";
+ expect(listIndentChanges(doc, 0, doc.length, false)).toEqual([
+ { from: 0, insert: " " },
+ { from: 4, insert: " " },
+ { from: 8, insert: " " },
+ ]);
+ });
+
+ it("skips non-list lines inside a mixed selection", () => {
+ const doc = "- a\nplain\n- c";
+ expect(listIndentChanges(doc, 0, doc.length, false)).toEqual([
+ { from: 0, insert: " " },
+ { from: 10, insert: " " },
+ ]);
+ });
+});
diff --git a/src/lib/list-indent.ts b/src/lib/list-indent.ts
new file mode 100644
index 0000000..b7f118b
--- /dev/null
+++ b/src/lib/list-indent.ts
@@ -0,0 +1,50 @@
+// Pure list-indentation helper for the editor's Tab / Shift+Tab keys. Computes
+// the change set that nests (Tab) or un-nests (Shift+Tab) every list line a
+// selection touches, anchored at line starts so CodeMirror remaps the selection
+// for us. Kept free of CodeMirror types so it is testable against a plain string.
+
+/** One CodeMirror-style change: insert at `from`, or delete the range [from, to). */
+export interface IndentChange {
+ from: number;
+ to?: number;
+ insert?: string;
+}
+
+/** Two spaces per indent level, matching the editor's existing list markers. */
+const INDENT = " ";
+
+/** A bullet (-, *, +) or ordered (1.) marker with its leading whitespace. */
+const LIST_RE = /^(\s*)([-*+]|\d+\.)\s+/;
+
+/**
+ * Indent (`outdent` false) or outdent (`outdent` true) every list line the range
+ * [from, to] touches, by one level. Non-list lines in the range are left alone.
+ * Returns an empty array when no touched line is an indentable list item, so the
+ * caller can fall back to the default Tab behaviour.
+ */
+export function listIndentChanges(
+ doc: string,
+ from: number,
+ to: number,
+ outdent: boolean,
+): IndentChange[] {
+ const blockStart = doc.lastIndexOf("\n", from - 1) + 1;
+ const nextNl = doc.indexOf("\n", to);
+ const blockEnd = nextNl === -1 ? doc.length : nextNl;
+
+ const changes: IndentChange[] = [];
+ let lineStart = blockStart;
+ for (const line of doc.slice(blockStart, blockEnd).split("\n")) {
+ const m = LIST_RE.exec(line);
+ if (m) {
+ if (!outdent) {
+ changes.push({ from: lineStart, insert: INDENT });
+ } else {
+ const remove = Math.min(INDENT.length, m[1].length);
+ if (remove > 0) changes.push({ from: lineStart, to: lineStart + remove });
+ }
+ }
+ lineStart += line.length + 1; // + 1 for the newline that split() dropped
+ }
+ return changes;
+}
From e0c9ec9bf925de1b8ac89b22bec597ab6791bbe9 Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:20:19 -0700
Subject: [PATCH 07/10] spec: add settings wiki and Contexting copy proposal
---
.../feat-settings-contexting/proposal.md | 24 +++++++++++++++++++
.../changes/feat-settings-contexting/tasks.md | 8 +++++++
2 files changed, 32 insertions(+)
create mode 100644 openspec/changes/feat-settings-contexting/proposal.md
create mode 100644 openspec/changes/feat-settings-contexting/tasks.md
diff --git a/openspec/changes/feat-settings-contexting/proposal.md b/openspec/changes/feat-settings-contexting/proposal.md
new file mode 100644
index 0000000..6a7ba9e
--- /dev/null
+++ b/openspec/changes/feat-settings-contexting/proposal.md
@@ -0,0 +1,24 @@
+# Change: Settings wiki navigation and the Contexting copy format
+
+## Why
+The settings view shipped as a sidebar with a single About tab and an empty placeholder.
+Two needs push it forward: settings should grow without becoming a long scroll, and the
+app needs a foundation for AI features. The answer is a wiki-style settings shell, a
+landing grid of categories that open focused sub-pages, plus a first new category,
+Contexting, that controls what copying a note hands to other tools.
+
+## What Changes
+- Replace the settings sidebar with a landing grid of category cards; each card opens a
+ focused sub-page with a breadcrumb back to the grid. Escape steps back to the grid
+ before closing the view.
+- Keep the existing About page; drop the empty Default New Tab placeholder.
+- Add a Contexting category: a user-editable copy template with {title}, {tags}, {date},
+ and {content} placeholders, a live preview, and the list of available variables.
+- Add a "Copy note as context" command to the palette that renders the template for the
+ selected note and writes it to the clipboard.
+- Persist the template to the settings KV, and render it from a pure, tested module.
+
+## Impact
+A new pure module (contexting-format) with tests, a small rune store, one palette command,
+and a rewritten SettingsView. Clipboard uses the WebView's navigator.clipboard, so no new
+native dependency or capability is added. The core crate is untouched.
diff --git a/openspec/changes/feat-settings-contexting/tasks.md b/openspec/changes/feat-settings-contexting/tasks.md
new file mode 100644
index 0000000..0842fd7
--- /dev/null
+++ b/openspec/changes/feat-settings-contexting/tasks.md
@@ -0,0 +1,8 @@
+# Tasks
+
+- [x] Add a pure renderTemplate module with {title}/{tags}/{date}/{content} and tests
+- [x] Add a contexting rune store persisting the template to the settings KV
+- [x] Add the "Copy note as context" palette command (clipboard write)
+- [x] Rewrite SettingsView as a wiki grid with About and Contexting sub-pages
+- [x] Init the contexting store on app start
+- [ ] Confirm in the running app: edit the template, the preview updates, and the copy fills variables
From ac2ea71b79014ac4dcdb8bb61c2e3dfd50948f95 Mon Sep 17 00:00:00 2001
From: jamubc <150970140+jamubc@users.noreply.github.com>
Date: Thu, 18 Jun 2026 18:20:19 -0700
Subject: [PATCH 08/10] feat(settings): add wiki navigation and the Contexting
copy format
Replace the settings sidebar with a landing grid of category cards that open
focused sub-pages with a breadcrumb back. Add a Contexting category whose
editable template wraps a note with {title}, {tags}, {date}, and {content}, with
a live preview. A new 'Copy note as context' palette command renders the
template for the selected note and writes it to the clipboard, seeding the app's
AI features. Rendering lives in a pure, tested module.
---
src/lib/commands.ts | 13 +
src/lib/components/SettingsView.svelte | 345 +++++++++++++++++--------
src/lib/contexting-format.test.ts | 40 +++
src/lib/contexting-format.ts | 32 +++
src/lib/stores/contexting.svelte.ts | 38 +++
src/routes/+page.svelte | 2 +
6 files changed, 364 insertions(+), 106 deletions(-)
create mode 100644 src/lib/contexting-format.test.ts
create mode 100644 src/lib/contexting-format.ts
create mode 100644 src/lib/stores/contexting.svelte.ts
diff --git a/src/lib/commands.ts b/src/lib/commands.ts
index e99bd6e..1c18be9 100644
--- a/src/lib/commands.ts
+++ b/src/lib/commands.ts
@@ -5,6 +5,7 @@
import { library } from "$lib/stores/library.svelte";
import { theme } from "$lib/stores/theme.svelte";
+import { contexting } from "$lib/stores/contexting.svelte";
import { exportTheme, importTheme } from "$lib/themes/share";
import { BODY_FONTS } from "$lib/themes/fonts";
import type { Command } from "$lib/command-filter";
@@ -88,6 +89,18 @@ export function buildCommands(): Command[] {
prefix: notePrefix,
run: () => (n.isDeleted ? library.restoreSelected() : library.deleteSelected()),
},
+ {
+ id: "note.copyContext",
+ title: "Copy note as context",
+ group: "Notes",
+ prefix: notePrefix,
+ run: async () => {
+ library.flushPendingEdits();
+ const note = library.selected;
+ if (!note) return;
+ await navigator.clipboard.writeText(contexting.render(note, library.selectedTags));
+ },
+ },
);
}
diff --git a/src/lib/components/SettingsView.svelte b/src/lib/components/SettingsView.svelte
index d67f967..3fc7024 100644
--- a/src/lib/components/SettingsView.svelte
+++ b/src/lib/components/SettingsView.svelte
@@ -1,6 +1,13 @@
-
-