@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.
- package/dist/dev/audit-overlay.svelte +1 -1
- package/dist/docs/css-utilities.js +6 -1
- package/dist/docs/notes.md +12 -11
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/badge.md +3 -0
- package/dist/generated-docs/button.md +4 -0
- package/dist/generated-docs/chip.md +3 -0
- package/dist/generated-docs/llms-full.txt +53 -31
- package/dist/generated-docs/llms.txt +1 -1
- package/dist/generated-docs/manifest.json +17 -17
- package/dist/generated-docs/notes.md +12 -11
- package/dist/generated-docs/utilities.json +10 -10
- package/dist/generated-docs/utilities.md +30 -19
- package/dist/os/index.d.ts +0 -1
- package/dist/os/index.js +0 -1
- package/dist/os/notification-center.svelte +1 -2
- package/dist/os/taskbar.svelte +2 -3
- package/dist/os/toaster.svelte +2 -3
- package/dist/styles/index.css +77 -12
- package/dist/styles/layers.css +48 -0
- package/dist/styles/surfaces/base.css +4 -2
- package/dist/styles/surfaces/glass.css +0 -13
- package/dist/theme/index.d.ts +87 -0
- package/dist/theme/index.js +86 -0
- package/dist/ui/alert-dialog/alert-dialog-content.svelte +2 -2
- package/dist/ui/alert-dialog/alert-dialog-overlay.svelte +1 -1
- package/dist/ui/badge/badge.svelte +14 -1
- package/dist/ui/badge/badge.svelte.d.ts +4 -0
- package/dist/ui/button/button.svelte +37 -7
- package/dist/ui/button/button.svelte.d.ts +6 -0
- package/dist/ui/chip/chip.svelte +14 -1
- package/dist/ui/chip/chip.svelte.d.ts +4 -0
- package/dist/ui/combobox/combobox-content.svelte +1 -1
- package/dist/ui/command-bar/command-bar.svelte +1 -1
- package/dist/ui/context-menu/context-menu-content.svelte +1 -1
- package/dist/ui/context-menu/context-menu-sub-content.svelte +1 -1
- package/dist/ui/date-picker/date-picker-content.svelte +1 -1
- package/dist/ui/date-range-picker/date-range-picker-content.svelte +1 -1
- package/dist/ui/dialog/dialog-content.svelte +1 -1
- package/dist/ui/dialog/dialog-overlay.svelte +5 -1
- package/dist/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
- package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
- package/dist/ui/link-preview/link-preview-content.svelte +1 -1
- package/dist/ui/menubar/menubar-content.svelte +1 -1
- package/dist/ui/menubar/menubar-sub-content.svelte +1 -1
- package/dist/ui/popover/popover-content.svelte +1 -1
- package/dist/ui/select/select-content.svelte +1 -1
- package/dist/ui/sheet/sheet-content.svelte +1 -1
- package/dist/ui/sheet/sheet-content.svelte.d.ts +1 -1
- package/dist/ui/tooltip/tooltip-content.svelte +1 -1
- package/package.json +9 -12
- package/dist/apps/index.d.ts +0 -20
- package/dist/apps/index.js +0 -20
- package/dist/apps/llm-shell/index.d.ts +0 -2
- package/dist/apps/llm-shell/index.js +0 -1
- package/dist/apps/llm-shell/llm-shell.svelte +0 -334
- package/dist/apps/llm-shell/llm-shell.svelte.d.ts +0 -57
- package/dist/apps/llm-shell/llm-swarm.svelte +0 -79
- package/dist/apps/llm-shell/llm-swarm.svelte.d.ts +0 -25
- package/dist/apps/llm-shell/types.d.ts +0 -103
- package/dist/apps/llm-shell/types.js +0 -1
- package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +0 -11
- package/dist/apps/notes/editor/bubble-menu.svelte.js +0 -165
- package/dist/apps/notes/editor/bubble-toolbar.svelte +0 -55
- package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +0 -22
- package/dist/apps/notes/editor/extensions.d.ts +0 -18
- package/dist/apps/notes/editor/extensions.js +0 -88
- package/dist/apps/notes/editor/floating.d.ts +0 -25
- package/dist/apps/notes/editor/floating.js +0 -54
- package/dist/apps/notes/editor/link.d.ts +0 -9
- package/dist/apps/notes/editor/link.js +0 -22
- package/dist/apps/notes/editor/list-cleanup-rule.d.ts +0 -14
- package/dist/apps/notes/editor/list-cleanup-rule.js +0 -47
- package/dist/apps/notes/editor/slash-command.svelte.d.ts +0 -22
- package/dist/apps/notes/editor/slash-command.svelte.js +0 -215
- package/dist/apps/notes/editor/slash-menu.svelte +0 -82
- package/dist/apps/notes/editor/slash-menu.svelte.d.ts +0 -23
- package/dist/apps/notes/index.d.ts +0 -11
- package/dist/apps/notes/index.js +0 -13
- package/dist/apps/notes/markdown.d.ts +0 -12
- package/dist/apps/notes/markdown.js +0 -165
- package/dist/apps/notes/note-editor.svelte +0 -176
- package/dist/apps/notes/note-editor.svelte.d.ts +0 -36
- package/dist/apps/notes/notes-store.svelte.d.ts +0 -64
- package/dist/apps/notes/notes-store.svelte.js +0 -234
- package/dist/apps/notes/notes.svelte +0 -501
- package/dist/apps/notes/notes.svelte.d.ts +0 -53
- package/dist/apps/notes/tree.d.ts +0 -70
- package/dist/apps/notes/tree.js +0 -185
- package/dist/apps/notes/types.d.ts +0 -49
- package/dist/apps/notes/types.js +0 -11
- package/dist/os/layers.d.ts +0 -23
- 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
|
-
}
|