@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,176 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * The TipTap document surface.
4
- *
5
- * The editor instance is created inside an attachment so its lifetime is tied
6
- * to the element, and torn down with it. `content` is a SEED, not a bound
7
- * value: ProseMirror owns the document once it is running, so re-feeding it
8
- * every keystroke would fight the editor. The host re-seeds by re-keying this
9
- * component (`{#key noteId}`) when it switches notes.
10
- *
11
- * Prose styling lives in the scoped stylesheet below and is written entirely
12
- * against era tokens, so a document follows the density, surface, corners,
13
- * and theme axes the same way a control does.
14
- *
15
- * (Do not spell a literal style tag in this comment — svelte2tsx 0.7.58 scans
16
- * for it lexically and decides the script never closed.)
17
- */
18
- import { untrack } from 'svelte';
19
- import { Editor, type JSONContent } from '@tiptap/core';
20
- import { noteExtensions } from './editor/extensions.js';
21
- // Side-effect import: mounting the editor should not require the consumer to
22
- // remember a stylesheet. Also published as `@signal9/era-ui/styles/prose`.
23
- import '../../styles/prose.css';
24
-
25
- interface Props {
26
- /** Initial document. Later changes are ignored — re-key to reseed. */
27
- content: JSONContent;
28
- onUpdate?: (json: JSONContent) => void;
29
- /** Placeholder shown on an empty first paragraph. */
30
- class?: string;
31
- }
32
-
33
- let { content, onUpdate, class: className }: Props = $props();
34
-
35
- let editor: Editor | undefined;
36
-
37
- type MarkdownStorage = { markdown?: { getMarkdown?: () => string } };
38
-
39
- let activeFormats = $state<Record<string, boolean>>({});
40
-
41
- function updateActiveFormats(ed: Editor) {
42
- activeFormats = {
43
- bold: ed.isActive('bold'),
44
- italic: ed.isActive('italic'),
45
- strike: ed.isActive('strike'),
46
- code: ed.isActive('code'),
47
- highlight: ed.isActive('highlight'),
48
- heading: ed.isActive('heading'),
49
- bulletList: ed.isActive('bulletList'),
50
- orderedList: ed.isActive('orderedList'),
51
- taskList: ed.isActive('taskList'),
52
- blockquote: ed.isActive('blockquote'),
53
- codeBlock: ed.isActive('codeBlock'),
54
- details: ed.isActive('details'),
55
- table: ed.isActive('table'),
56
- link: ed.isActive('link')
57
- };
58
- }
59
-
60
- function initEditor(element: HTMLDivElement) {
61
- // untrack: the attachment must not re-run when `content` or `onUpdate`
62
- // change — tearing down and rebuilding the editor mid-typing would lose
63
- // the selection and the undo history.
64
- return untrack(() => {
65
- editor = new Editor({
66
- element,
67
- extensions: noteExtensions,
68
- content,
69
- onUpdate: ({ editor: ed }) => {
70
- onUpdate?.(ed.getJSON());
71
- updateActiveFormats(ed);
72
- },
73
- onSelectionUpdate: ({ editor: ed }) => {
74
- updateActiveFormats(ed);
75
- }
76
- });
77
- return () => {
78
- editor?.destroy();
79
- editor = undefined;
80
- };
81
- });
82
- }
83
-
84
- /*
85
- * Imperative API. The bubble toolbar and slash menu already cover these for a
86
- * pointer/keyboard user; these exports exist so a HOST can drive the document
87
- * from its own chrome — a window toolbar, a command palette, an agent tool —
88
- * without reaching into TipTap itself. Bind the component and call them.
89
- */
90
-
91
- export function isActive(name: string) {
92
- return activeFormats[name] ?? false;
93
- }
94
-
95
- export function focus() {
96
- editor?.chain().focus().run();
97
- }
98
-
99
- export function formatBold() {
100
- editor?.chain().focus().toggleBold().run();
101
- }
102
- export function formatItalic() {
103
- editor?.chain().focus().toggleItalic().run();
104
- }
105
- export function formatStrike() {
106
- editor?.chain().focus().toggleStrike().run();
107
- }
108
- export function formatCode() {
109
- editor?.chain().focus().toggleCode().run();
110
- }
111
- export function formatHighlight() {
112
- editor?.chain().focus().toggleHighlight().run();
113
- }
114
- export function formatLink() {
115
- if (editor) void import('./editor/link.js').then((m) => m.toggleLink(editor!));
116
- }
117
-
118
- export function formatHeading(level: 1 | 2 | 3 = 2) {
119
- editor?.chain().focus().toggleHeading({ level }).run();
120
- }
121
- export function formatBlockquote() {
122
- editor?.chain().focus().toggleBlockquote().run();
123
- }
124
- export function formatList() {
125
- editor?.chain().focus().toggleBulletList().run();
126
- }
127
- export function formatOrderedList() {
128
- editor?.chain().focus().toggleOrderedList().run();
129
- }
130
- export function formatTaskList() {
131
- editor?.chain().focus().toggleTaskList().run();
132
- }
133
- export function formatCodeBlock() {
134
- editor?.chain().focus().toggleCodeBlock().run();
135
- }
136
- export function formatDetails() {
137
- editor?.chain().focus().setDetails().run();
138
- }
139
-
140
- export function insertTable() {
141
- editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
142
- }
143
- export function deleteTable() {
144
- editor?.chain().focus().deleteTable().run();
145
- }
146
- export function addTableRow() {
147
- editor?.chain().focus().addRowAfter().run();
148
- }
149
- export function addTableCol() {
150
- editor?.chain().focus().addColumnAfter().run();
151
- }
152
- export function deleteTableRow() {
153
- editor?.chain().focus().deleteRow().run();
154
- }
155
- export function deleteTableCol() {
156
- editor?.chain().focus().deleteColumn().run();
157
- }
158
-
159
- export function insertImage() {
160
- const url = globalThis.prompt?.('Image URL:');
161
- if (url) editor?.chain().focus().setImage({ src: url }).run();
162
- }
163
-
164
- /** The document as markdown, via tiptap-markdown's serializer. */
165
- export function getMarkdown(): string {
166
- return (editor?.storage as MarkdownStorage | undefined)?.markdown?.getMarkdown?.() ?? '';
167
- }
168
- </script>
169
-
170
- <!-- eslint-disable better-tailwindcss/no-unknown-classes -- era-notes-prose is defined in this component's scoped stylesheet -->
171
- <div class="era-notes-prose min-w-0 {className ?? ''}" {@attach initEditor}></div>
172
-
173
- <!-- The document styling lives in ../../styles/prose.css, imported above:
174
- Svelte would scope a <style> block to this component, which is what made
175
- era's prose typography unreachable for anyone building their own TipTap
176
- instance. Same rules, addressable by anyone who sets .era-notes-prose. -->
@@ -1,36 +0,0 @@
1
- import { type JSONContent } from '@tiptap/core';
2
- import '../../styles/prose.css';
3
- interface Props {
4
- /** Initial document. Later changes are ignored — re-key to reseed. */
5
- content: JSONContent;
6
- onUpdate?: (json: JSONContent) => void;
7
- /** Placeholder shown on an empty first paragraph. */
8
- class?: string;
9
- }
10
- declare const NoteEditor: import("svelte").Component<Props, {
11
- isActive: (name: string) => boolean;
12
- focus: () => void;
13
- formatBold: () => void;
14
- formatItalic: () => void;
15
- formatStrike: () => void;
16
- formatCode: () => void;
17
- formatHighlight: () => void;
18
- formatLink: () => void;
19
- formatHeading: (level?: 1 | 2 | 3) => void;
20
- formatBlockquote: () => void;
21
- formatList: () => void;
22
- formatOrderedList: () => void;
23
- formatTaskList: () => void;
24
- formatCodeBlock: () => void;
25
- formatDetails: () => void;
26
- insertTable: () => void;
27
- deleteTable: () => void;
28
- addTableRow: () => void;
29
- addTableCol: () => void;
30
- deleteTableRow: () => void;
31
- deleteTableCol: () => void;
32
- insertImage: () => void;
33
- getMarkdown: () => string;
34
- }, "">;
35
- type NoteEditor = ReturnType<typeof NoteEditor>;
36
- export default NoteEditor;
@@ -1,64 +0,0 @@
1
- /**
2
- * The Notes data layer — reactive state plus a pluggable persistence adapter.
3
- *
4
- * The store owns the notes; the adapter owns where they live. That split is
5
- * deliberate: era ships a browser-only app (localStorage), but the same
6
- * component is meant to drop into a host that persists server-side. Such a host
7
- * supplies its own adapter and changes nothing else — which is why `load`/`save`
8
- * are allowed to return promises even though the bundled adapters are sync.
9
- *
10
- * Writes are debounced and coalesced: every mutation marks the store dirty and
11
- * re-arms one timer, so a burst of keystrokes costs a single `save`.
12
- */
13
- import { type Note, type NoteInit, type NotePatch } from './types.js';
14
- export type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
15
- export interface NotesAdapter {
16
- load(): Note[] | Promise<Note[]>;
17
- save(notes: Note[]): void | Promise<void>;
18
- }
19
- export declare const DEFAULT_STORAGE_KEY = "era-ui:notes";
20
- /**
21
- * Persist to `localStorage`. Safe to construct during SSR — it only touches
22
- * storage inside `load`/`save`, and returns empty / no-ops when there is none.
23
- */
24
- export declare function localStorageAdapter(key?: string): NotesAdapter;
25
- /** In-memory only — for demos, tests, and SSR snapshots. */
26
- export declare function memoryAdapter(seed?: Note[]): NotesAdapter;
27
- export interface NotesStoreOptions {
28
- /** Where notes live. Defaults to `localStorage` under `era-ui:notes`. */
29
- adapter?: NotesAdapter;
30
- /** Quiet period before a burst of edits is flushed, in ms. Default 400. */
31
- debounce?: number;
32
- /** How long the `saved` badge lingers before falling back to `idle`, in ms. */
33
- statusLinger?: number;
34
- }
35
- export declare class NotesStore {
36
- #private;
37
- notes: Note[];
38
- /** True until the first `load()` settles — the list renders a skeleton on it. */
39
- loading: boolean;
40
- status: SaveStatus;
41
- lastError: string | null;
42
- /** Pinned first, then most-recently-touched. The order the sidebar renders. */
43
- readonly ordered: Note[];
44
- constructor(options?: NotesStoreOptions);
45
- load(): Promise<void>;
46
- get(id: string | null | undefined): Note | undefined;
47
- /**
48
- * Case-insensitive match over title AND body. Matching the body is why
49
- * `plainText` is denormalised — the alternative is walking every document's
50
- * AST on every keystroke.
51
- */
52
- search(query: string): Note[];
53
- create(init?: NoteInit): Note;
54
- update(id: string, patch: NotePatch): void;
55
- remove(id: string): void;
56
- togglePin(id: string): void;
57
- setIcon(id: string, icon: string | null): void;
58
- /** The note as markdown — for copy-out, export, or handing to a model. */
59
- toMarkdown(id: string): string;
60
- /** Persist immediately, cancelling any pending debounce. Safe to call spuriously. */
61
- flush(): Promise<void>;
62
- /** Clear timers and write out whatever is pending. Call from `onDestroy`. */
63
- destroy(): void;
64
- }
@@ -1,234 +0,0 @@
1
- /**
2
- * The Notes data layer — reactive state plus a pluggable persistence adapter.
3
- *
4
- * The store owns the notes; the adapter owns where they live. That split is
5
- * deliberate: era ships a browser-only app (localStorage), but the same
6
- * component is meant to drop into a host that persists server-side. Such a host
7
- * supplies its own adapter and changes nothing else — which is why `load`/`save`
8
- * are allowed to return promises even though the bundled adapters are sync.
9
- *
10
- * Writes are debounced and coalesced: every mutation marks the store dirty and
11
- * re-arms one timer, so a burst of keystrokes costs a single `save`.
12
- */
13
- import { tiptapToMarkdown, textContent } from './markdown.js';
14
- import { EMPTY_DOC } from './types.js';
15
- export const DEFAULT_STORAGE_KEY = 'era-ui:notes';
16
- /** Loose structural check — storage is untrusted (stale schema, another tab, a
17
- * user editing devtools), so a malformed entry is dropped rather than crashing
18
- * the list on first render. */
19
- function isNote(value) {
20
- if (typeof value !== 'object' || value === null)
21
- return false;
22
- const n = value;
23
- return (typeof n.id === 'string' &&
24
- typeof n.title === 'string' &&
25
- typeof n.content === 'object' &&
26
- n.content !== null &&
27
- Array.isArray(n.content.content));
28
- }
29
- /**
30
- * Persist to `localStorage`. Safe to construct during SSR — it only touches
31
- * storage inside `load`/`save`, and returns empty / no-ops when there is none.
32
- */
33
- export function localStorageAdapter(key = DEFAULT_STORAGE_KEY) {
34
- const available = () => typeof localStorage !== 'undefined';
35
- return {
36
- load() {
37
- if (!available())
38
- return [];
39
- try {
40
- const raw = localStorage.getItem(key);
41
- if (!raw)
42
- return [];
43
- const parsed = JSON.parse(raw);
44
- return Array.isArray(parsed) ? parsed.filter(isNote) : [];
45
- }
46
- catch {
47
- return [];
48
- }
49
- },
50
- save(notes) {
51
- if (!available())
52
- return;
53
- // Let a quota/private-mode failure reach the store so the UI can say so,
54
- // rather than silently dropping the user's writing.
55
- localStorage.setItem(key, JSON.stringify(notes));
56
- }
57
- };
58
- }
59
- /** In-memory only — for demos, tests, and SSR snapshots. */
60
- export function memoryAdapter(seed = []) {
61
- let notes = seed;
62
- return {
63
- load: () => notes,
64
- save: (next) => {
65
- notes = next;
66
- }
67
- };
68
- }
69
- function newId() {
70
- if (typeof crypto !== 'undefined' && 'randomUUID' in crypto)
71
- return crypto.randomUUID();
72
- return `note-${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
73
- }
74
- export class NotesStore {
75
- #adapter;
76
- #debounce;
77
- #statusLinger;
78
- #saveTimer = null;
79
- #statusTimer = null;
80
- notes = $state([]);
81
- /** True until the first `load()` settles — the list renders a skeleton on it. */
82
- loading = $state(true);
83
- status = $state('idle');
84
- lastError = $state(null);
85
- /** Pinned first, then most-recently-touched. The order the sidebar renders. */
86
- ordered = $derived([...this.notes].sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.updatedAt - a.updatedAt));
87
- constructor(options = {}) {
88
- this.#adapter = options.adapter ?? localStorageAdapter();
89
- this.#debounce = options.debounce ?? 400;
90
- this.#statusLinger = options.statusLinger ?? 1600;
91
- }
92
- async load() {
93
- this.loading = true;
94
- try {
95
- const loaded = await this.#adapter.load();
96
- this.notes = loaded;
97
- this.lastError = null;
98
- }
99
- catch (error) {
100
- this.notes = [];
101
- this.#fail(error);
102
- }
103
- finally {
104
- this.loading = false;
105
- }
106
- }
107
- get(id) {
108
- if (!id)
109
- return undefined;
110
- return this.notes.find((n) => n.id === id);
111
- }
112
- /**
113
- * Case-insensitive match over title AND body. Matching the body is why
114
- * `plainText` is denormalised — the alternative is walking every document's
115
- * AST on every keystroke.
116
- */
117
- search(query) {
118
- const q = query.trim().toLowerCase();
119
- if (!q)
120
- return this.ordered;
121
- return this.ordered.filter((n) => n.title.toLowerCase().includes(q) || n.plainText.toLowerCase().includes(q));
122
- }
123
- create(init = {}) {
124
- const now = Date.now();
125
- const content = init.content ?? structuredClone(EMPTY_DOC);
126
- const note = {
127
- id: newId(),
128
- title: init.title ?? 'Untitled',
129
- icon: init.icon ?? null,
130
- content,
131
- plainText: content.content.map(textContent).join(' ').trim(),
132
- pinned: init.pinned ?? false,
133
- createdAt: now,
134
- updatedAt: now
135
- };
136
- this.notes.push(note);
137
- this.#schedule();
138
- return note;
139
- }
140
- update(id, patch) {
141
- const note = this.get(id);
142
- if (!note)
143
- return;
144
- if (patch.title !== undefined)
145
- note.title = patch.title;
146
- if (patch.icon !== undefined)
147
- note.icon = patch.icon;
148
- if (patch.pinned !== undefined)
149
- note.pinned = patch.pinned;
150
- if (patch.content !== undefined) {
151
- note.content = patch.content;
152
- note.plainText = patch.content.content.map(textContent).join(' ').trim();
153
- }
154
- note.updatedAt = Date.now();
155
- this.#schedule();
156
- }
157
- remove(id) {
158
- const index = this.notes.findIndex((n) => n.id === id);
159
- if (index < 0)
160
- return;
161
- this.notes.splice(index, 1);
162
- this.#schedule();
163
- }
164
- togglePin(id) {
165
- const note = this.get(id);
166
- if (!note)
167
- return;
168
- // Pinning is an ordering change, not an edit — leave `updatedAt` alone so a
169
- // pin doesn't shuffle the note to the top of the recency list as well.
170
- note.pinned = !note.pinned;
171
- this.#schedule();
172
- }
173
- setIcon(id, icon) {
174
- this.update(id, { icon });
175
- }
176
- /** The note as markdown — for copy-out, export, or handing to a model. */
177
- toMarkdown(id) {
178
- const note = this.get(id);
179
- if (!note)
180
- return '';
181
- const heading = note.title ? `# ${note.title}\n\n` : '';
182
- return heading + tiptapToMarkdown(note.content);
183
- }
184
- /** Persist immediately, cancelling any pending debounce. Safe to call spuriously. */
185
- async flush() {
186
- if (this.#saveTimer) {
187
- clearTimeout(this.#saveTimer);
188
- this.#saveTimer = null;
189
- }
190
- await this.#persist();
191
- }
192
- /** Clear timers and write out whatever is pending. Call from `onDestroy`. */
193
- destroy() {
194
- if (this.#statusTimer)
195
- clearTimeout(this.#statusTimer);
196
- this.#statusTimer = null;
197
- void this.flush();
198
- }
199
- #schedule() {
200
- this.status = 'saving';
201
- if (this.#saveTimer)
202
- clearTimeout(this.#saveTimer);
203
- this.#saveTimer = setTimeout(() => {
204
- this.#saveTimer = null;
205
- void this.#persist();
206
- }, this.#debounce);
207
- }
208
- async #persist() {
209
- try {
210
- await this.#adapter.save($state.snapshot(this.notes));
211
- this.lastError = null;
212
- this.#flash('saved');
213
- }
214
- catch (error) {
215
- this.#fail(error);
216
- }
217
- }
218
- #flash(status) {
219
- this.status = status;
220
- if (this.#statusTimer)
221
- clearTimeout(this.#statusTimer);
222
- this.#statusTimer = setTimeout(() => {
223
- this.#statusTimer = null;
224
- this.status = 'idle';
225
- }, this.#statusLinger);
226
- }
227
- #fail(error) {
228
- this.lastError = error instanceof Error ? error.message : String(error);
229
- this.status = 'error';
230
- if (this.#statusTimer)
231
- clearTimeout(this.#statusTimer);
232
- this.#statusTimer = null;
233
- }
234
- }