@signal9/era-ui 12.1.2 → 13.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/dev/audit-overlay.svelte +1 -1
  2. package/dist/docs/css-utilities.js +6 -1
  3. package/dist/docs/notes.md +12 -11
  4. package/dist/era-ui.css +1 -1
  5. package/dist/generated-docs/badge.md +3 -0
  6. package/dist/generated-docs/button.md +4 -0
  7. package/dist/generated-docs/chip.md +3 -0
  8. package/dist/generated-docs/llms-full.txt +53 -31
  9. package/dist/generated-docs/llms.txt +1 -1
  10. package/dist/generated-docs/manifest.json +17 -17
  11. package/dist/generated-docs/notes.md +12 -11
  12. package/dist/generated-docs/utilities.json +10 -10
  13. package/dist/generated-docs/utilities.md +30 -19
  14. package/dist/os/index.d.ts +0 -1
  15. package/dist/os/index.js +0 -1
  16. package/dist/os/notification-center.svelte +1 -2
  17. package/dist/os/taskbar.svelte +2 -3
  18. package/dist/os/toaster.svelte +2 -3
  19. package/dist/styles/index.css +77 -12
  20. package/dist/styles/layers.css +48 -0
  21. package/dist/styles/surfaces/base.css +4 -2
  22. package/dist/styles/surfaces/glass.css +0 -13
  23. package/dist/theme/index.d.ts +87 -0
  24. package/dist/theme/index.js +86 -0
  25. package/dist/ui/alert-dialog/alert-dialog-content.svelte +2 -2
  26. package/dist/ui/alert-dialog/alert-dialog-overlay.svelte +1 -1
  27. package/dist/ui/badge/badge.svelte +14 -1
  28. package/dist/ui/badge/badge.svelte.d.ts +4 -0
  29. package/dist/ui/button/button.svelte +37 -7
  30. package/dist/ui/button/button.svelte.d.ts +6 -0
  31. package/dist/ui/chip/chip.svelte +14 -1
  32. package/dist/ui/chip/chip.svelte.d.ts +4 -0
  33. package/dist/ui/combobox/combobox-content.svelte +1 -1
  34. package/dist/ui/command-bar/command-bar.svelte +1 -1
  35. package/dist/ui/context-menu/context-menu-content.svelte +1 -1
  36. package/dist/ui/context-menu/context-menu-sub-content.svelte +1 -1
  37. package/dist/ui/date-picker/date-picker-content.svelte +1 -1
  38. package/dist/ui/date-range-picker/date-range-picker-content.svelte +1 -1
  39. package/dist/ui/dialog/dialog-content.svelte +1 -1
  40. package/dist/ui/dialog/dialog-overlay.svelte +5 -1
  41. package/dist/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
  42. package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  43. package/dist/ui/link-preview/link-preview-content.svelte +1 -1
  44. package/dist/ui/menubar/menubar-content.svelte +1 -1
  45. package/dist/ui/menubar/menubar-sub-content.svelte +1 -1
  46. package/dist/ui/popover/popover-content.svelte +1 -1
  47. package/dist/ui/select/select-content.svelte +1 -1
  48. package/dist/ui/sheet/sheet-content.svelte +1 -1
  49. package/dist/ui/sheet/sheet-content.svelte.d.ts +1 -1
  50. package/dist/ui/tooltip/tooltip-content.svelte +1 -1
  51. package/package.json +9 -12
  52. package/dist/apps/index.d.ts +0 -20
  53. package/dist/apps/index.js +0 -20
  54. package/dist/apps/llm-shell/index.d.ts +0 -2
  55. package/dist/apps/llm-shell/index.js +0 -1
  56. package/dist/apps/llm-shell/llm-shell.svelte +0 -334
  57. package/dist/apps/llm-shell/llm-shell.svelte.d.ts +0 -57
  58. package/dist/apps/llm-shell/llm-swarm.svelte +0 -79
  59. package/dist/apps/llm-shell/llm-swarm.svelte.d.ts +0 -25
  60. package/dist/apps/llm-shell/types.d.ts +0 -103
  61. package/dist/apps/llm-shell/types.js +0 -1
  62. package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +0 -11
  63. package/dist/apps/notes/editor/bubble-menu.svelte.js +0 -165
  64. package/dist/apps/notes/editor/bubble-toolbar.svelte +0 -55
  65. package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +0 -22
  66. package/dist/apps/notes/editor/extensions.d.ts +0 -18
  67. package/dist/apps/notes/editor/extensions.js +0 -88
  68. package/dist/apps/notes/editor/floating.d.ts +0 -25
  69. package/dist/apps/notes/editor/floating.js +0 -54
  70. package/dist/apps/notes/editor/link.d.ts +0 -9
  71. package/dist/apps/notes/editor/link.js +0 -22
  72. package/dist/apps/notes/editor/list-cleanup-rule.d.ts +0 -14
  73. package/dist/apps/notes/editor/list-cleanup-rule.js +0 -47
  74. package/dist/apps/notes/editor/slash-command.svelte.d.ts +0 -22
  75. package/dist/apps/notes/editor/slash-command.svelte.js +0 -215
  76. package/dist/apps/notes/editor/slash-menu.svelte +0 -82
  77. package/dist/apps/notes/editor/slash-menu.svelte.d.ts +0 -23
  78. package/dist/apps/notes/index.d.ts +0 -11
  79. package/dist/apps/notes/index.js +0 -13
  80. package/dist/apps/notes/markdown.d.ts +0 -12
  81. package/dist/apps/notes/markdown.js +0 -165
  82. package/dist/apps/notes/note-editor.svelte +0 -176
  83. package/dist/apps/notes/note-editor.svelte.d.ts +0 -36
  84. package/dist/apps/notes/notes-store.svelte.d.ts +0 -64
  85. package/dist/apps/notes/notes-store.svelte.js +0 -234
  86. package/dist/apps/notes/notes.svelte +0 -501
  87. package/dist/apps/notes/notes.svelte.d.ts +0 -53
  88. package/dist/apps/notes/tree.d.ts +0 -70
  89. package/dist/apps/notes/tree.js +0 -185
  90. package/dist/apps/notes/types.d.ts +0 -49
  91. package/dist/apps/notes/types.js +0 -11
  92. package/dist/os/layers.d.ts +0 -23
  93. package/dist/os/layers.js +0 -23
@@ -1,501 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * Notes — a complete, client-side notes app.
4
- *
5
- * An era port of the term/web notes app: same TipTap editor, same slash menu
6
- * and selection toolbar, same heading-tree index. Two things changed.
7
- *
8
- * PERSISTENCE. The original round-tripped every edit through a server via
9
- * TanStack Query, which forced a whole dirty-override layer — local edits had
10
- * to out-rank refetched query data, or an in-flight save clobbered what you
11
- * were typing. Here the store IS the source of truth and persistence is a
12
- * debounced write behind it, so the editor writes straight through and there
13
- * is nothing to reconcile.
14
- *
15
- * CHROME. Everything visible is an era primitive (Bar, Button, Input, Badge,
16
- * Popover, ScrollArea, AlertDialog, Skeleton) on era tokens, so the app
17
- * follows the density, surface, corners, motion, and font axes like any other
18
- * component in the library.
19
- */
20
- import { onDestroy, onMount, untrack, type Snippet } from 'svelte';
21
- import FileText from '@lucide/svelte/icons/file-text';
22
- import Plus from '@lucide/svelte/icons/plus';
23
- import Search from '@lucide/svelte/icons/search';
24
- import Pin from '@lucide/svelte/icons/pin';
25
- import PinOff from '@lucide/svelte/icons/pin-off';
26
- import Trash2 from '@lucide/svelte/icons/trash-2';
27
- import { Bar } from '../../ui/bar/index.js';
28
- import { Badge } from '../../ui/badge/index.js';
29
- import { Button } from '../../ui/button/index.js';
30
- import { Input } from '../../ui/input/index.js';
31
- import { Skeleton } from '../../ui/skeleton/index.js';
32
- import * as AlertDialog from '../../ui/alert-dialog/index.js';
33
- import * as ContextMenu from '../../ui/context-menu/index.js';
34
- import * as Popover from '../../ui/popover/index.js';
35
- import * as ScrollArea from '../../ui/scroll-area/index.js';
36
- import { cn, keys } from '../../utils/index.js';
37
- import NoteEditor from './note-editor.svelte';
38
- import { NotesStore, localStorageAdapter, type NotesAdapter } from './notes-store.svelte.js';
39
- import type { Note, NoteInit, TiptapDocument } from './types.js';
40
-
41
- interface Props {
42
- /**
43
- * Bring your own store — for a custom adapter, or to share one set of
44
- * notes between two mounted views. When supplied, the host owns its
45
- * lifecycle: this component will not destroy it on unmount.
46
- */
47
- store?: NotesStore;
48
- /** Persistence for the store this component creates. Ignored if `store` is set. */
49
- adapter?: NotesAdapter;
50
- /** `localStorage` key for the default adapter. Ignored if `store`/`adapter` is set. */
51
- storageKey?: string;
52
- /** The open note. Bindable, so a host can deep-link or restore a session. */
53
- noteId?: string | null;
54
- /** Notes created only when storage comes back empty — a first-run sample. */
55
- seed?: NoteInit[];
56
- /**
57
- * The pane hosting this app, if any. Structural on purpose — an os-layer
58
- * `AppPane` satisfies it as-is, and Notes never has to import the os layer.
59
- * When provided, the document header (icon picker, title, save status)
60
- * moves out of the editor area and into the pane bar's CENTRE slot, the
61
- * way the reference terminal app titles its notes window; it is cleared
62
- * again on unmount. Omit it and the header renders above the editor as a
63
- * normal in-app bar.
64
- */
65
- pane?: { center?: Snippet } | null;
66
- class?: string;
67
- }
68
-
69
- let {
70
- store: providedStore,
71
- adapter,
72
- storageKey,
73
- noteId = $bindable(null),
74
- seed,
75
- pane,
76
- class: className
77
- }: Props = $props();
78
-
79
- // Resolved once, on purpose — untrack() is the declaration of that intent, not
80
- // a workaround. A store swapped in mid-flight would strand the notes currently
81
- // on screen and the editor mounted over them.
82
- const owned = untrack(() => providedStore === undefined);
83
- const store = untrack(
84
- () => providedStore ?? new NotesStore({ adapter: adapter ?? localStorageAdapter(storageKey) })
85
- );
86
-
87
- let query = $state('');
88
- let pendingDelete = $state<string | null>(null);
89
- let iconPickerOpen = $state(false);
90
-
91
- const visible = $derived(store.search(query));
92
- const selected = $derived(store.get(noteId));
93
- const pendingDeleteNote = $derived(store.get(pendingDelete));
94
-
95
- /** A compact glyph set — enough to tell notes apart at a glance, no picker UI. */
96
- const ICON_ROWS = [
97
- ['📝', '📋', '📌', '💡', '🔥', '⭐'],
98
- ['🎯', '🚀', '🔧', '📦', '🔒', '🔑'],
99
- ['💻', '🤖', '🧠', '⚡', '🔍', '🌐']
100
- ];
101
-
102
- onMount(async () => {
103
- // `loading` is only true before a first successful load, so this is also the
104
- // flag for "this mount is the one that hydrated the store". Seeding keys off
105
- // it rather than off `owned`: a second view sharing an already-loaded store
106
- // must not seed again, but a HOST-supplied store that comes back empty
107
- // should still get its first-run notes.
108
- const hydrating = store.loading;
109
- if (hydrating) await store.load();
110
- if (hydrating && seed?.length && store.notes.length === 0) {
111
- for (const init of seed) store.create(init);
112
- }
113
- if (!store.get(noteId)) noteId = store.ordered[0]?.id ?? null;
114
- });
115
-
116
- onDestroy(() => {
117
- if (owned) store.destroy();
118
- });
119
-
120
- // Claim the pane bar's centre for the document header. An effect rather than
121
- // a one-shot: `pane` can arrive late (the docs shell finds its Reader pane
122
- // after mount) or change identity, and each run releases the previous claim.
123
- // The guard on the cleanup matters — if some later occupant already replaced
124
- // the snippet, unmounting Notes must not blank THEIR chrome.
125
- $effect(() => {
126
- const host = pane;
127
- if (!host) return;
128
- host.center = paneHeader;
129
- return () => {
130
- if (host.center === paneHeader) host.center = undefined;
131
- };
132
- });
133
-
134
- function createNote() {
135
- const note = store.create();
136
- noteId = note.id;
137
- query = '';
138
- }
139
-
140
- function confirmDelete() {
141
- const id = pendingDelete;
142
- pendingDelete = null;
143
- if (!id) return;
144
- store.remove(id);
145
- if (noteId === id) noteId = store.ordered[0]?.id ?? null;
146
- }
147
-
148
- /** Short relative age — the list has a single cell for it, so it must stay narrow. */
149
- function formatAge(ts: number) {
150
- const d = Date.now() - ts;
151
- if (d < 60_000) return 'now';
152
- if (d < 3_600_000) return `${Math.floor(d / 60_000)}m`;
153
- if (d < 86_400_000) return `${Math.floor(d / 3_600_000)}h`;
154
- return new Date(ts).toLocaleDateString();
155
- }
156
-
157
- function onRowKeydown(event: KeyboardEvent) {
158
- if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
159
- event.preventDefault();
160
- const row = event.currentTarget as HTMLElement;
161
- const next = event.key === 'ArrowDown' ? row.nextElementSibling : row.previousElementSibling;
162
- (next as HTMLElement | null)?.focus();
163
- }
164
- </script>
165
-
166
- {#snippet iconPicker(note: Note)}
167
- <Popover.Root bind:open={iconPickerOpen}>
168
- <Popover.Trigger icon size="pill" aria-label="Change icon" title="Change icon">
169
- {#if note.icon}
170
- <span aria-hidden="true">{note.icon}</span>
171
- {:else}
172
- <FileText />
173
- {/if}
174
- </Popover.Trigger>
175
- <Popover.Content align="start" class="w-max">
176
- <div class="flex flex-col gap-gutter">
177
- {#each ICON_ROWS as row, r (r)}
178
- <div class="flex gap-gutter">
179
- {#each row as glyph (glyph)}
180
- <Button
181
- icon
182
- size="control"
183
- active={note.icon === glyph}
184
- aria-label={glyph}
185
- onclick={() => {
186
- store.setIcon(note.id, glyph);
187
- iconPickerOpen = false;
188
- }}
189
- >
190
- {glyph}
191
- </Button>
192
- {/each}
193
- </div>
194
- {/each}
195
- {#if note.icon}
196
- <Button
197
- size="chip"
198
- class="w-full"
199
- onclick={() => {
200
- store.setIcon(note.id, null);
201
- iconPickerOpen = false;
202
- }}
203
- >
204
- Clear icon
205
- </Button>
206
- {/if}
207
- </div>
208
- </Popover.Content>
209
- </Popover.Root>
210
- {/snippet}
211
-
212
- <!-- The document header, pane-bar edition: the same icon picker and title,
213
- compacted for the bar's centre track. data-pane-control is what makes an
214
- EDITABLE title possible inside a drag zone — a press on the cluster means
215
- caret/click, everywhere else on the bar still means drag. The input is
216
- content-sized (field-sizing) so the cluster hugs the title and stays
217
- optically centred; the status badge hangs OFF the flow (absolute, past the
218
- right edge) so saved/error appearing never nudges the centred title. -->
219
- {#snippet titleField(note: Note, cls: string)}
220
- <Input
221
- reveal
222
- size="pill"
223
- class={cls}
224
- aria-label="Note title"
225
- placeholder="Untitled"
226
- value={note.title}
227
- oninput={(e: Event & { currentTarget: HTMLInputElement }) =>
228
- store.update(note.id, { title: e.currentTarget.value })}
229
- />
230
- {/snippet}
231
-
232
- {#snippet statusBadge()}
233
- {#if store.status === 'saved'}
234
- <Badge tone="success">saved</Badge>
235
- {:else if store.status === 'error'}
236
- <Badge tone="destructive" title={store.lastError ?? undefined}>error</Badge>
237
- {/if}
238
- {/snippet}
239
-
240
- {#snippet paneHeader()}
241
- {#if selected}
242
- <div class="relative flex min-w-0 items-center gap-gutter" data-pane-control>
243
- {@render iconPicker(selected)}
244
- {@render titleField(selected, 'field-sizing-content max-w-56 min-w-0 text-center')}
245
- <!-- Absolutely positioned, so the badge appearing never nudges the
246
- centred title; empty when the status is quiet, which costs
247
- nothing off-flow. -->
248
- <span class="absolute top-1/2 left-full ml-gutter -translate-y-1/2">
249
- {@render statusBadge()}
250
- </span>
251
- </div>
252
- {/if}
253
- {/snippet}
254
-
255
- <div
256
- class={cn('flex h-full min-h-0 text-body text-fg', className)}
257
- use:keys={{
258
- // Persistence is already debounced; Ctrl/Cmd-S is the reassurance gesture
259
- // people press anyway, wired to a real immediate flush.
260
- '$mod+s': (e) => {
261
- e.preventDefault();
262
- void store.flush();
263
- }
264
- }}
265
- >
266
- <!-- Sidebar: filter + note list -->
267
- <div class="flex w-56 shrink-0 flex-col border-r border-divider-faded">
268
- <!-- content="pill": 24/30/36/48 of chrome around an 18/20/22/26 field and
269
- button, with Bar deriving the two-tier concentric inset. A notes sidebar
270
- is chrome around a list; at the lg/md pairing it was a 32px bar over
271
- 24px rows and the chrome outweighed the content. This is the tier pair
272
- term's notes app uses — its hardcoded 3px is what Bar now derives at
273
- dense. The field keeps its fill: xxs is the smallest tier a real field
274
- fits on. -->
275
- <!-- The divider is drawn OUTSIDE the box, exactly as Pane.Handle draws its
276
- own: a border consumes a pixel of the bar's content box, so its children
277
- centred in 23px and sat 2.5px from the top against a 3px wall. Outset
278
- costs no layout, so the bar keeps its full tier height and every child
279
- sits the wall's distance from all four edges. -->
280
- <Bar size="control" content="pill" divider class="rounded-none">
281
- <Input
282
- icon={Search}
283
- size="pill"
284
- class="min-w-0 flex-1"
285
- type="text"
286
- placeholder="Filter…"
287
- aria-label="Filter notes"
288
- bind:value={query}
289
- />
290
- <Button icon size="pill" aria-label="New note" title="New note" onclick={createNote}>
291
- <Plus />
292
- </Button>
293
- </Bar>
294
-
295
- {#if store.loading}
296
- <div class="flex flex-col gap-gutter p-gutter">
297
- {#each Array.from({ length: 5 }, (_, i) => i) as i (i)}
298
- <Skeleton class="h-control rounded-control" />
299
- {/each}
300
- </div>
301
- {:else if visible.length === 0}
302
- <!-- No call to action here: the document pane already carries the primary
303
- one, and the bar's + is always within reach. Two competing CTAs for
304
- an empty list reads as indecision. -->
305
- <div
306
- class="flex flex-1 flex-col items-center justify-center gap-gutter p-section text-center"
307
- >
308
- <p class="text-muted">{query ? 'No matches' : 'No notes yet'}</p>
309
- </div>
310
- {:else}
311
- <ScrollArea.Root class="min-h-0 flex-1 rounded-none">
312
- <ScrollArea.Viewport>
313
- <div class="flex flex-col gap-gutter p-gutter" role="listbox" aria-label="Notes">
314
- {#each visible as note (note.id)}
315
- {@const current = note.id === noteId}
316
- <!-- The row is the selectable option; the pin/delete buttons inside it
317
- are separate actions, so they stop propagation rather than
318
- selecting the note out from under the click.
319
- Right-click opens the same two actions as a real ContextMenu.
320
- `child` delegation is load-bearing: the trigger's props land ON
321
- the row instead of wrapping it, so the row stays a direct
322
- role="option" child of the listbox. A wrapper element there
323
- would put a non-option between them and break the relationship
324
- screen readers rely on.
325
- rounded-control, not rounded-menu-item: item-rd is the radius for a row
326
- inside a PANEL (it subtracts the panel's frame inset). A list
327
- row on a plain surface takes its own tier radius — the
328
- conversations-rail pattern. -->
329
- <ContextMenu.Root>
330
- <ContextMenu.Trigger>
331
- {#snippet child({ props })}
332
- <div
333
- {...props}
334
- class="group/row flex h-control cursor-pointer items-center gap-pill-inset-control rounded-control px-pill-inset-control leading-none {current
335
- ? 'bg-hover text-bright shadow-pressed'
336
- : 'hover:bg-highlight hover:shadow-highlight'}"
337
- role="option"
338
- aria-selected={current}
339
- tabindex="0"
340
- onclick={() => (noteId = note.id)}
341
- onkeydown={(e: KeyboardEvent) => {
342
- if (e.key === 'Enter' || e.key === ' ') {
343
- e.preventDefault();
344
- noteId = note.id;
345
- return;
346
- }
347
- onRowKeydown(e);
348
- }}
349
- >
350
- <span class="flex size-pill shrink-0 items-center justify-center">
351
- {#if note.icon}
352
- {note.icon}
353
- {:else}
354
- <FileText class="size-glyph-pill text-muted" />
355
- {/if}
356
- </span>
357
-
358
- <span class="min-w-0 flex-1 truncate">{note.title || 'Untitled'}</span>
359
-
360
- <!-- One cell, two occupants: the age at rest, the actions on
361
- hover. Swapping them in place keeps the row from reflowing.
362
- At rest EVERY row shows its age and nothing else. A pinned
363
- row used to put a pin glyph here instead — the same glyph,
364
- in the same slot, as the pin BUTTON one state away — so a
365
- pinned row read as a row whose actions were stuck on. Being
366
- pinned is already visible: those rows sort to the top, and
367
- the button carries its latched state once you are on the
368
- row. -->
369
- <span class="shrink-0 text-muted group-hover/row:hidden">
370
- {formatAge(note.updatedAt)}
371
- </span>
372
- <span
373
- class="hidden shrink-0 items-center gap-pill-inset-control group-hover/row:flex"
374
- >
375
- <Button
376
- icon
377
- size="pill"
378
- aria-label={note.pinned ? 'Unpin note' : 'Pin note'}
379
- title={note.pinned ? 'Unpin note' : 'Pin note'}
380
- active={note.pinned}
381
- onclick={(e: MouseEvent) => {
382
- e.stopPropagation();
383
- store.togglePin(note.id);
384
- }}
385
- >
386
- {#if note.pinned}<PinOff />{:else}<Pin />{/if}
387
- </Button>
388
- <Button
389
- icon
390
- size="pill"
391
- tone="destructive"
392
- aria-label="Delete note"
393
- title="Delete note"
394
- onclick={(e: MouseEvent) => {
395
- e.stopPropagation();
396
- pendingDelete = note.id;
397
- }}
398
- >
399
- <Trash2 />
400
- </Button>
401
- </span>
402
- </div>
403
- {/snippet}
404
- </ContextMenu.Trigger>
405
- <ContextMenu.Content>
406
- <ContextMenu.Item onSelect={() => store.togglePin(note.id)}>
407
- {#if note.pinned}
408
- <PinOff class="size-icon text-muted" />
409
- Unpin note
410
- {:else}
411
- <Pin class="size-icon text-muted" />
412
- Pin note
413
- {/if}
414
- </ContextMenu.Item>
415
- <!-- No separator: a rule groups things, and two actions on one row
416
- are one group — it was drawing a boundary through the middle of
417
- a pair. The panel's gutter gives them their own breathing room. -->
418
- <!-- Routed through the same confirm dialog the toolbar button
419
- uses, not a direct delete: a destructive action should not
420
- get easier to reach by arriving from a different gesture. -->
421
- <ContextMenu.Item onSelect={() => (pendingDelete = note.id)}>
422
- <Trash2 class="size-icon text-destructive" />
423
- Delete note
424
- </ContextMenu.Item>
425
- </ContextMenu.Content>
426
- </ContextMenu.Root>
427
- {/each}
428
- </div>
429
- </ScrollArea.Viewport>
430
- <ScrollArea.Scrollbar orientation="vertical">
431
- <ScrollArea.Thumb />
432
- </ScrollArea.Scrollbar>
433
- </ScrollArea.Root>
434
- {/if}
435
- </div>
436
-
437
- <!-- Document pane -->
438
- <div class="flex min-w-0 flex-1 flex-col">
439
- {#if selected}
440
- <!-- Matches the sidebar's filter bar exactly — the two headers sit side by
441
- side across the top of the app, so they must be the same tier. -->
442
- <!-- Same outset divider as the sidebar's filter bar above. -->
443
- <!-- Rendered only when no pane hosts the app — with a pane, the same
444
- header (see the snippets below) lives in the pane bar's centre and
445
- drawing it twice would title the document twice. -->
446
- {#if !pane}
447
- <Bar size="control" content="pill" divider class="rounded-none">
448
- {@render iconPicker(selected)}
449
- {@render titleField(selected, 'min-w-0 flex-1')}
450
- <!-- Fixed-width so the badge appearing and clearing never nudges the
451
- title field; `saving` is deliberately silent — the badge only
452
- reports a settled outcome. -->
453
- <span class="flex w-16 shrink-0 justify-end">
454
- {@render statusBadge()}
455
- </span>
456
- </Bar>
457
- {/if}
458
-
459
- <div class="min-h-0 flex-1 overflow-x-hidden overflow-y-auto p-card">
460
- <!-- ProseMirror owns the document once running, so switching notes
461
- rebuilds the editor rather than pushing new content into it. -->
462
- {#key selected.id}
463
- <NoteEditor
464
- content={selected.content}
465
- onUpdate={(json) => store.update(selected.id, { content: json as TiptapDocument })}
466
- />
467
- {/key}
468
- </div>
469
- {:else}
470
- <div class="flex flex-1 flex-col items-center justify-center gap-gutter p-section">
471
- <p class="text-muted">
472
- {store.notes.length === 0 ? 'Create a note to get started' : 'Select a note'}
473
- </p>
474
- {#if store.notes.length === 0}
475
- <Button onclick={createNote}>
476
- <Plus />
477
- New note
478
- </Button>
479
- {/if}
480
- </div>
481
- {/if}
482
- </div>
483
- </div>
484
-
485
- <AlertDialog.Root
486
- open={pendingDelete !== null}
487
- onOpenChange={(open) => {
488
- if (!open) pendingDelete = null;
489
- }}
490
- >
491
- <AlertDialog.Content>
492
- <AlertDialog.Title>Delete note</AlertDialog.Title>
493
- <AlertDialog.Description>
494
- “{pendingDeleteNote?.title || 'Untitled'}” will be removed. This cannot be undone.
495
- </AlertDialog.Description>
496
- <div class="flex justify-end gap-gutter">
497
- <AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
498
- <AlertDialog.Action tone="destructive" onclick={confirmDelete}>Delete</AlertDialog.Action>
499
- </div>
500
- </AlertDialog.Content>
501
- </AlertDialog.Root>
@@ -1,53 +0,0 @@
1
- /**
2
- * Notes — a complete, client-side notes app.
3
- *
4
- * An era port of the term/web notes app: same TipTap editor, same slash menu
5
- * and selection toolbar, same heading-tree index. Two things changed.
6
- *
7
- * PERSISTENCE. The original round-tripped every edit through a server via
8
- * TanStack Query, which forced a whole dirty-override layer — local edits had
9
- * to out-rank refetched query data, or an in-flight save clobbered what you
10
- * were typing. Here the store IS the source of truth and persistence is a
11
- * debounced write behind it, so the editor writes straight through and there
12
- * is nothing to reconcile.
13
- *
14
- * CHROME. Everything visible is an era primitive (Bar, Button, Input, Badge,
15
- * Popover, ScrollArea, AlertDialog, Skeleton) on era tokens, so the app
16
- * follows the density, surface, corners, motion, and font axes like any other
17
- * component in the library.
18
- */
19
- import { type Snippet } from 'svelte';
20
- import { NotesStore, type NotesAdapter } from './notes-store.svelte.js';
21
- import type { NoteInit } from './types.js';
22
- interface Props {
23
- /**
24
- * Bring your own store — for a custom adapter, or to share one set of
25
- * notes between two mounted views. When supplied, the host owns its
26
- * lifecycle: this component will not destroy it on unmount.
27
- */
28
- store?: NotesStore;
29
- /** Persistence for the store this component creates. Ignored if `store` is set. */
30
- adapter?: NotesAdapter;
31
- /** `localStorage` key for the default adapter. Ignored if `store`/`adapter` is set. */
32
- storageKey?: string;
33
- /** The open note. Bindable, so a host can deep-link or restore a session. */
34
- noteId?: string | null;
35
- /** Notes created only when storage comes back empty — a first-run sample. */
36
- seed?: NoteInit[];
37
- /**
38
- * The pane hosting this app, if any. Structural on purpose — an os-layer
39
- * `AppPane` satisfies it as-is, and Notes never has to import the os layer.
40
- * When provided, the document header (icon picker, title, save status)
41
- * moves out of the editor area and into the pane bar's CENTRE slot, the
42
- * way the reference terminal app titles its notes window; it is cleared
43
- * again on unmount. Omit it and the header renders above the editor as a
44
- * normal in-app bar.
45
- */
46
- pane?: {
47
- center?: Snippet;
48
- } | null;
49
- class?: string;
50
- }
51
- declare const Notes: import("svelte").Component<Props, {}, "noteId">;
52
- type Notes = ReturnType<typeof Notes>;
53
- export default Notes;
@@ -1,70 +0,0 @@
1
- /**
2
- * Notes tree index — a hierarchical table of contents built by walking a
3
- * TipTap document's heading structure.
4
- *
5
- * This is the retrieval half of the app. Instead of embedding notes and
6
- * hoping vector similarity lands on the right paragraph, a model is handed
7
- * `treeToText(tree)` as a table of contents and reasons about which section to
8
- * open — then `extractSection()` returns just that slice, structure intact
9
- * (PageIndex-style reasoning-based retrieval).
10
- *
11
- * Nothing here touches the editor or the DOM: it operates on stored documents,
12
- * so it is equally usable from a tool handler, a worker, or a build step.
13
- */
14
- import type { TiptapDocument, TiptapNode } from './types.js';
15
- export interface NoteTreeNode {
16
- title: string;
17
- noteId?: string;
18
- headingId?: string;
19
- /** 0=root, 1=note, 2=H1, 3=H2, 4=H3 — heading level shifted by the note row. */
20
- level: number;
21
- /** Words in this section's own content, not its children's. */
22
- wordCount: number;
23
- children: NoteTreeNode[];
24
- }
25
- export interface SectionResult {
26
- /** `Note Title > H2 > H3` */
27
- headingPath: string;
28
- /** Markdown for this section — structure-preserving, not flattened text. */
29
- content: string;
30
- wordCount: number;
31
- }
32
- export interface SectionNodes {
33
- headingPath: string;
34
- nodes: TiptapNode[];
35
- }
36
- /** Build a tree for one note's document. */
37
- export declare function buildNoteTree(noteId: string, title: string, doc: TiptapDocument): NoteTreeNode;
38
- /** Combine every note into one index tree. */
39
- export declare function buildGlobalTree(notes: Array<{
40
- id: string;
41
- title: string;
42
- content: TiptapDocument;
43
- }>): NoteTreeNode;
44
- /** Node count below (and including) this node. */
45
- export declare function countNodes(node: NoteTreeNode): number;
46
- /** Total words in this subtree. */
47
- export declare function totalWords(node: NoteTreeNode): number;
48
- /**
49
- * Render the tree as YAML-shaped key/value text for a model to read.
50
- *
51
- * YAML-style indented pairs beat JSON on nested data in the 2025 retrieval
52
- * benchmarks, and the labelled fields (`id`, `hid`) are copy-pasteable straight
53
- * back into a `get(id)` / `extractSection(noteId, headingId)` call, which is
54
- * the whole point of handing over a TOC.
55
- */
56
- export declare function treeToText(tree: NoteTreeNode): string;
57
- /**
58
- * The raw nodes of one section: everything between the target heading and the
59
- * next heading of the same or a higher level, plus the breadcrumb that led to
60
- * it. Callers pick their own serialisation.
61
- */
62
- export declare function extractSectionNodes(noteTitle: string, doc: TiptapDocument, targetHeadingId: string): SectionNodes | null;
63
- /**
64
- * One section, serialised to markdown.
65
- *
66
- * Markdown rather than flattened text on purpose: plain-text extraction garbles
67
- * lists, tables, and code blocks, which is exactly the structure a model needs
68
- * to answer from a retrieved section.
69
- */
70
- export declare function extractSection(noteTitle: string, doc: TiptapDocument, targetHeadingId: string): SectionResult | null;