@signal9/era-ui 3.10.0 → 3.12.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/apps/index.d.ts +19 -0
- package/dist/apps/index.js +19 -0
- package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +11 -0
- package/dist/apps/notes/editor/bubble-menu.svelte.js +106 -0
- package/dist/apps/notes/editor/bubble-toolbar.svelte +42 -0
- package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +22 -0
- package/dist/apps/notes/editor/extensions.d.ts +18 -0
- package/dist/apps/notes/editor/extensions.js +88 -0
- package/dist/apps/notes/editor/floating.d.ts +21 -0
- package/dist/apps/notes/editor/floating.js +44 -0
- package/dist/apps/notes/editor/link.d.ts +9 -0
- package/dist/apps/notes/editor/link.js +22 -0
- package/dist/apps/notes/editor/list-cleanup-rule.d.ts +14 -0
- package/dist/apps/notes/editor/list-cleanup-rule.js +47 -0
- package/dist/apps/notes/editor/slash-command.svelte.d.ts +22 -0
- package/dist/apps/notes/editor/slash-command.svelte.js +215 -0
- package/dist/apps/notes/editor/slash-menu.svelte +63 -0
- package/dist/apps/notes/editor/slash-menu.svelte.d.ts +23 -0
- package/dist/apps/notes/index.d.ts +11 -0
- package/dist/apps/notes/index.js +13 -0
- package/dist/apps/notes/markdown.d.ts +12 -0
- package/dist/apps/notes/markdown.js +165 -0
- package/dist/apps/notes/note-editor.svelte +456 -0
- package/dist/apps/notes/note-editor.svelte.d.ts +35 -0
- package/dist/apps/notes/notes-store.svelte.d.ts +64 -0
- package/dist/apps/notes/notes-store.svelte.js +234 -0
- package/dist/apps/notes/notes.svelte +377 -0
- package/dist/apps/notes/notes.svelte.d.ts +22 -0
- package/dist/apps/notes/tree.d.ts +70 -0
- package/dist/apps/notes/tree.js +185 -0
- package/dist/apps/notes/types.d.ts +49 -0
- package/dist/apps/notes/types.js +11 -0
- package/dist/docs/notes.md +82 -0
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/llms-full.txt +91 -0
- package/dist/generated-docs/llms.txt +1 -1
- package/dist/generated-docs/manifest.json +15 -0
- package/dist/generated-docs/notes.md +86 -0
- package/dist/os/window.svelte +6 -1
- package/dist/os/wm.svelte.d.ts +8 -1
- package/dist/os/wm.svelte.js +2 -1
- package/dist/ui/badge/badge.svelte.d.ts +17 -17
- package/dist/ui/bar/bar.svelte.d.ts +11 -11
- package/dist/ui/button/variants.d.ts +44 -44
- package/dist/ui/card/card.svelte.d.ts +20 -20
- package/dist/ui/chip/chip.svelte.d.ts +14 -14
- package/dist/ui/pane/pane-root.svelte.d.ts +1 -1
- package/dist/ui/pane/pane.svelte.d.ts +1 -1
- package/dist/ui/sheet/sheet-content.svelte.d.ts +14 -14
- package/dist/ui/skeleton/skeleton.svelte.d.ts +11 -11
- package/dist/utils/index.js +11 -0
- package/package.json +31 -5
|
@@ -0,0 +1,70 @@
|
|
|
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;
|
|
@@ -0,0 +1,185 @@
|
|
|
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 { textContent, tiptapToMarkdown } from './markdown.js';
|
|
15
|
+
function countWords(text) {
|
|
16
|
+
return text.trim().split(/\s+/).filter(Boolean).length;
|
|
17
|
+
}
|
|
18
|
+
/** Build a tree for one note's document. */
|
|
19
|
+
export function buildNoteTree(noteId, title, doc) {
|
|
20
|
+
const root = { title, noteId, level: 1, wordCount: 0, children: [] };
|
|
21
|
+
if (!doc.content || doc.content.length === 0)
|
|
22
|
+
return root;
|
|
23
|
+
const stack = [root];
|
|
24
|
+
let currentText = '';
|
|
25
|
+
for (const node of doc.content) {
|
|
26
|
+
if (node.type === 'heading' && node.attrs?.level) {
|
|
27
|
+
// Text seen since the last heading belongs to the section we're leaving.
|
|
28
|
+
if (currentText.trim()) {
|
|
29
|
+
stack[stack.length - 1].wordCount += countWords(currentText);
|
|
30
|
+
currentText = '';
|
|
31
|
+
}
|
|
32
|
+
const headingLevel = node.attrs.level;
|
|
33
|
+
const treeLevel = headingLevel + 1; // note=1, H1=2, H2=3, H3=4
|
|
34
|
+
const headingNode = {
|
|
35
|
+
title: textContent(node),
|
|
36
|
+
headingId: node.attrs?.id ?? undefined,
|
|
37
|
+
level: treeLevel,
|
|
38
|
+
wordCount: 0,
|
|
39
|
+
children: []
|
|
40
|
+
};
|
|
41
|
+
// The parent is the deepest node still shallower than this heading.
|
|
42
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= treeLevel) {
|
|
43
|
+
stack.pop();
|
|
44
|
+
}
|
|
45
|
+
stack[stack.length - 1].children.push(headingNode);
|
|
46
|
+
stack.push(headingNode);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
currentText += textContent(node) + ' ';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (currentText.trim()) {
|
|
53
|
+
stack[stack.length - 1].wordCount += countWords(currentText);
|
|
54
|
+
}
|
|
55
|
+
// A note with no headings at all is one flat section.
|
|
56
|
+
if (root.children.length === 0 && root.wordCount === 0) {
|
|
57
|
+
root.wordCount = countWords(doc.content.map(textContent).join(' '));
|
|
58
|
+
}
|
|
59
|
+
return root;
|
|
60
|
+
}
|
|
61
|
+
/** Combine every note into one index tree. */
|
|
62
|
+
export function buildGlobalTree(notes) {
|
|
63
|
+
return {
|
|
64
|
+
title: 'Notes Index',
|
|
65
|
+
level: 0,
|
|
66
|
+
wordCount: 0,
|
|
67
|
+
children: notes.map((n) => buildNoteTree(n.id, n.title, n.content))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Node count below (and including) this node. */
|
|
71
|
+
export function countNodes(node) {
|
|
72
|
+
return 1 + node.children.reduce((sum, c) => sum + countNodes(c), 0);
|
|
73
|
+
}
|
|
74
|
+
/** Total words in this subtree. */
|
|
75
|
+
export function totalWords(node) {
|
|
76
|
+
return node.wordCount + node.children.reduce((sum, c) => sum + totalWords(c), 0);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Render the tree as YAML-shaped key/value text for a model to read.
|
|
80
|
+
*
|
|
81
|
+
* YAML-style indented pairs beat JSON on nested data in the 2025 retrieval
|
|
82
|
+
* benchmarks, and the labelled fields (`id`, `hid`) are copy-pasteable straight
|
|
83
|
+
* back into a `get(id)` / `extractSection(noteId, headingId)` call, which is
|
|
84
|
+
* the whole point of handing over a TOC.
|
|
85
|
+
*/
|
|
86
|
+
export function treeToText(tree) {
|
|
87
|
+
if (tree.children.length === 0)
|
|
88
|
+
return '(no notes)';
|
|
89
|
+
const blocks = [];
|
|
90
|
+
for (const note of tree.children) {
|
|
91
|
+
const noteWords = totalWords(note);
|
|
92
|
+
const lines = [
|
|
93
|
+
`- title: ${note.title || 'Untitled'}`,
|
|
94
|
+
` id: ${note.noteId ?? '?'}`,
|
|
95
|
+
` words: ${noteWords}`
|
|
96
|
+
];
|
|
97
|
+
if (note.children.length > 0) {
|
|
98
|
+
lines.push(' sections:');
|
|
99
|
+
renderSections(note.children, lines, 2);
|
|
100
|
+
}
|
|
101
|
+
blocks.push(lines.join('\n'));
|
|
102
|
+
}
|
|
103
|
+
return blocks.join('\n');
|
|
104
|
+
}
|
|
105
|
+
function renderSections(nodes, lines, depth) {
|
|
106
|
+
const indent = ' '.repeat(depth);
|
|
107
|
+
for (const node of nodes) {
|
|
108
|
+
const shortId = node.headingId ? node.headingId.slice(0, 8) : '';
|
|
109
|
+
lines.push(`${indent}- title: ${node.title || 'Untitled'}`);
|
|
110
|
+
if (shortId)
|
|
111
|
+
lines.push(`${indent} hid: ${shortId}`);
|
|
112
|
+
if (node.wordCount > 0)
|
|
113
|
+
lines.push(`${indent} words: ${node.wordCount}`);
|
|
114
|
+
if (node.children.length > 0) {
|
|
115
|
+
lines.push(`${indent} sections:`);
|
|
116
|
+
renderSections(node.children, lines, depth + 1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function isHeading(node) {
|
|
121
|
+
return node.type === 'heading' && typeof node.attrs?.level === 'number';
|
|
122
|
+
}
|
|
123
|
+
function updateBreadcrumb(stack, title, level) {
|
|
124
|
+
while (stack.length > 1 && stack[stack.length - 1].level >= level) {
|
|
125
|
+
stack.pop();
|
|
126
|
+
}
|
|
127
|
+
stack.push({ title, level });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The raw nodes of one section: everything between the target heading and the
|
|
131
|
+
* next heading of the same or a higher level, plus the breadcrumb that led to
|
|
132
|
+
* it. Callers pick their own serialisation.
|
|
133
|
+
*/
|
|
134
|
+
export function extractSectionNodes(noteTitle, doc, targetHeadingId) {
|
|
135
|
+
if (!doc.content)
|
|
136
|
+
return null;
|
|
137
|
+
const headingStack = [{ title: noteTitle, level: 0 }];
|
|
138
|
+
let startIdx = -1;
|
|
139
|
+
let targetLevel = 0;
|
|
140
|
+
for (let i = 0; i < doc.content.length; i++) {
|
|
141
|
+
const node = doc.content[i];
|
|
142
|
+
if (!isHeading(node))
|
|
143
|
+
continue;
|
|
144
|
+
updateBreadcrumb(headingStack, textContent(node), node.attrs.level);
|
|
145
|
+
const headingId = node.attrs?.id;
|
|
146
|
+
if (!headingId)
|
|
147
|
+
continue;
|
|
148
|
+
// `treeToText` emits 8-char prefixes, so accept a prefix as well as the
|
|
149
|
+
// full id — otherwise a model copying from the TOC never matches.
|
|
150
|
+
if (headingId === targetHeadingId || headingId.startsWith(targetHeadingId)) {
|
|
151
|
+
startIdx = i + 1;
|
|
152
|
+
targetLevel = node.attrs.level;
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (startIdx < 0)
|
|
157
|
+
return null;
|
|
158
|
+
const headingPath = headingStack.map((h) => h.title).join(' > ');
|
|
159
|
+
const nodes = [];
|
|
160
|
+
for (let i = startIdx; i < doc.content.length; i++) {
|
|
161
|
+
const node = doc.content[i];
|
|
162
|
+
if (isHeading(node) && node.attrs.level <= targetLevel)
|
|
163
|
+
break;
|
|
164
|
+
nodes.push(node);
|
|
165
|
+
}
|
|
166
|
+
return { headingPath, nodes };
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* One section, serialised to markdown.
|
|
170
|
+
*
|
|
171
|
+
* Markdown rather than flattened text on purpose: plain-text extraction garbles
|
|
172
|
+
* lists, tables, and code blocks, which is exactly the structure a model needs
|
|
173
|
+
* to answer from a retrieved section.
|
|
174
|
+
*/
|
|
175
|
+
export function extractSection(noteTitle, doc, targetHeadingId) {
|
|
176
|
+
const section = extractSectionNodes(noteTitle, doc, targetHeadingId);
|
|
177
|
+
if (!section)
|
|
178
|
+
return null;
|
|
179
|
+
const content = tiptapToMarkdown({ type: 'doc', content: section.nodes }).trim();
|
|
180
|
+
return {
|
|
181
|
+
headingPath: section.headingPath,
|
|
182
|
+
content,
|
|
183
|
+
wordCount: countWords(content)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Notes document model.
|
|
3
|
+
*
|
|
4
|
+
* TipTap's own `JSONContent` is deliberately loose (every field optional), so
|
|
5
|
+
* the app carries its own narrowed shapes: a `doc` always has `content`, and a
|
|
6
|
+
* `Note` always has the fields the list and the editor read. Nothing here
|
|
7
|
+
* imports from `@tiptap/*` — the model is the persistence contract, and a
|
|
8
|
+
* consumer storing notes server-side should be able to type its API against it
|
|
9
|
+
* without pulling the editor in.
|
|
10
|
+
*/
|
|
11
|
+
export interface TiptapMark {
|
|
12
|
+
type: string;
|
|
13
|
+
attrs?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
export interface TiptapNode {
|
|
16
|
+
type: string;
|
|
17
|
+
attrs?: Record<string, unknown>;
|
|
18
|
+
marks?: TiptapMark[];
|
|
19
|
+
content?: TiptapNode[];
|
|
20
|
+
text?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface TiptapDocument {
|
|
23
|
+
type: 'doc';
|
|
24
|
+
content: TiptapNode[];
|
|
25
|
+
}
|
|
26
|
+
export interface Note {
|
|
27
|
+
id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
/** A single emoji/character shown in the list, or `null` for the default glyph. */
|
|
30
|
+
icon: string | null;
|
|
31
|
+
content: TiptapDocument;
|
|
32
|
+
/**
|
|
33
|
+
* Flattened text of `content`, refreshed on every write. Kept denormalised
|
|
34
|
+
* so filtering the list never has to walk every document's AST.
|
|
35
|
+
*/
|
|
36
|
+
plainText: string;
|
|
37
|
+
pinned: boolean;
|
|
38
|
+
/** Epoch milliseconds — JSON-safe, so a note survives a `structuredClone` or a
|
|
39
|
+
* `JSON.stringify` round-trip through storage without a revive step. */
|
|
40
|
+
createdAt: number;
|
|
41
|
+
updatedAt: number;
|
|
42
|
+
}
|
|
43
|
+
/** The projection the sidebar list renders — no document body. */
|
|
44
|
+
export type NoteListItem = Pick<Note, 'id' | 'title' | 'icon' | 'pinned' | 'updatedAt'>;
|
|
45
|
+
/** Fields a caller may hand to `NotesStore.create()`. */
|
|
46
|
+
export type NoteInit = Partial<Pick<Note, 'title' | 'icon' | 'content' | 'pinned'>>;
|
|
47
|
+
/** Fields `NotesStore.update()` accepts. */
|
|
48
|
+
export type NotePatch = Partial<Pick<Note, 'title' | 'icon' | 'content' | 'pinned'>>;
|
|
49
|
+
export declare const EMPTY_DOC: TiptapDocument;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Notes document model.
|
|
3
|
+
*
|
|
4
|
+
* TipTap's own `JSONContent` is deliberately loose (every field optional), so
|
|
5
|
+
* the app carries its own narrowed shapes: a `doc` always has `content`, and a
|
|
6
|
+
* `Note` always has the fields the list and the editor read. Nothing here
|
|
7
|
+
* imports from `@tiptap/*` — the model is the persistence contract, and a
|
|
8
|
+
* consumer storing notes server-side should be able to type its API against it
|
|
9
|
+
* without pulling the editor in.
|
|
10
|
+
*/
|
|
11
|
+
export const EMPTY_DOC = { type: 'doc', content: [{ type: 'paragraph' }] };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
## Usage
|
|
2
|
+
|
|
3
|
+
Apps ship on their own subpath and are **not** re-exported from `@sig-nine/era-ui`.
|
|
4
|
+
Notes carries TipTap, and a consumer who never imports an app should never pay
|
|
5
|
+
for it.
|
|
6
|
+
|
|
7
|
+
```svelte
|
|
8
|
+
<script>
|
|
9
|
+
import { Notes } from '@sig-nine/era-ui/apps';
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<Notes />
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That is the whole integration: a sidebar with filter, pin, and delete; a TipTap
|
|
16
|
+
document with a `/` command menu and a selection toolbar; and persistence to
|
|
17
|
+
`localStorage` under `era-ui:notes`.
|
|
18
|
+
|
|
19
|
+
## Peer dependencies
|
|
20
|
+
|
|
21
|
+
Install alongside the library (they are not bundled):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
@tiptap/core @tiptap/pm @tiptap/starter-kit @tiptap/suggestion
|
|
25
|
+
@tiptap/extension-code-block-lowlight @tiptap/extension-details
|
|
26
|
+
@tiptap/extension-highlight @tiptap/extension-image
|
|
27
|
+
@tiptap/extension-placeholder @tiptap/extension-table
|
|
28
|
+
@tiptap/extension-task-item @tiptap/extension-task-list
|
|
29
|
+
@tiptap/extension-typography @tiptap/extension-unique-id
|
|
30
|
+
lowlight tiptap-markdown
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
| Prop | Type | Default | Notes |
|
|
36
|
+
| ------------ | ---------------- | ------------------ | ----------------------------------------------------------------- |
|
|
37
|
+
| `store` | `NotesStore` | created internally | Bring your own; when set, the host owns its lifecycle. |
|
|
38
|
+
| `adapter` | `NotesAdapter` | `localStorage` | Persistence for the internally-created store. |
|
|
39
|
+
| `storageKey` | `string` | `era-ui:notes` | Key for the default adapter. |
|
|
40
|
+
| `noteId` | `string \| null` | `null` | `$bindable` — the open note, for deep-linking or session restore. |
|
|
41
|
+
| `seed` | `NoteInit[]` | — | Created only when storage comes back empty (first-run sample). |
|
|
42
|
+
|
|
43
|
+
## Persistence
|
|
44
|
+
|
|
45
|
+
Persistence is one small interface, so server-backed hosting is a drop-in swap —
|
|
46
|
+
`load`/`save` may be sync or async:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { Notes, NotesStore } from '@sig-nine/era-ui/apps';
|
|
50
|
+
|
|
51
|
+
const store = new NotesStore({
|
|
52
|
+
adapter: {
|
|
53
|
+
load: () => fetch('/api/notes').then((r) => r.json()),
|
|
54
|
+
save: (notes) => fetch('/api/notes', { method: 'PUT', body: JSON.stringify(notes) })
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`NotesStore` is the reactive source of truth (`notes`, `ordered`, `loading`,
|
|
60
|
+
`status`, `lastError`) with `create` / `update` / `remove` / `togglePin` /
|
|
61
|
+
`setIcon` / `search` / `toMarkdown` / `flush`. Writes are debounced and
|
|
62
|
+
coalesced, so a burst of keystrokes costs one `save`.
|
|
63
|
+
|
|
64
|
+
## The heading index
|
|
65
|
+
|
|
66
|
+
Every document is also a tree. `buildGlobalTree()` walks heading structure,
|
|
67
|
+
`treeToText()` renders it as a table of contents a model can read, and
|
|
68
|
+
`extractSection(title, doc, headingId)` returns just the requested slice as
|
|
69
|
+
structure-preserving markdown — reasoning-based retrieval rather than
|
|
70
|
+
embeddings. Heading ids are stable (`@tiptap/extension-unique-id`), so a section
|
|
71
|
+
address survives edits above it.
|
|
72
|
+
|
|
73
|
+
## Notes
|
|
74
|
+
|
|
75
|
+
- Keyboard: `/` opens the command menu, `Mod-K` links a selection, `Mod-S`
|
|
76
|
+
flushes pending writes immediately.
|
|
77
|
+
- The slash menu and selection toolbar are real Svelte components mounted into
|
|
78
|
+
the document root, so they follow every axis — density tiers, surface chrome,
|
|
79
|
+
corners, motion, font — instead of carrying their own hardcoded styling.
|
|
80
|
+
- `@tiptap/extension-mathematics` is deliberately absent: it needs KaTeX's
|
|
81
|
+
stylesheet and fonts to render. Spread `noteExtensions` and add it yourself if
|
|
82
|
+
you want math.
|