@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.
Files changed (52) hide show
  1. package/dist/apps/index.d.ts +19 -0
  2. package/dist/apps/index.js +19 -0
  3. package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +11 -0
  4. package/dist/apps/notes/editor/bubble-menu.svelte.js +106 -0
  5. package/dist/apps/notes/editor/bubble-toolbar.svelte +42 -0
  6. package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +22 -0
  7. package/dist/apps/notes/editor/extensions.d.ts +18 -0
  8. package/dist/apps/notes/editor/extensions.js +88 -0
  9. package/dist/apps/notes/editor/floating.d.ts +21 -0
  10. package/dist/apps/notes/editor/floating.js +44 -0
  11. package/dist/apps/notes/editor/link.d.ts +9 -0
  12. package/dist/apps/notes/editor/link.js +22 -0
  13. package/dist/apps/notes/editor/list-cleanup-rule.d.ts +14 -0
  14. package/dist/apps/notes/editor/list-cleanup-rule.js +47 -0
  15. package/dist/apps/notes/editor/slash-command.svelte.d.ts +22 -0
  16. package/dist/apps/notes/editor/slash-command.svelte.js +215 -0
  17. package/dist/apps/notes/editor/slash-menu.svelte +63 -0
  18. package/dist/apps/notes/editor/slash-menu.svelte.d.ts +23 -0
  19. package/dist/apps/notes/index.d.ts +11 -0
  20. package/dist/apps/notes/index.js +13 -0
  21. package/dist/apps/notes/markdown.d.ts +12 -0
  22. package/dist/apps/notes/markdown.js +165 -0
  23. package/dist/apps/notes/note-editor.svelte +456 -0
  24. package/dist/apps/notes/note-editor.svelte.d.ts +35 -0
  25. package/dist/apps/notes/notes-store.svelte.d.ts +64 -0
  26. package/dist/apps/notes/notes-store.svelte.js +234 -0
  27. package/dist/apps/notes/notes.svelte +377 -0
  28. package/dist/apps/notes/notes.svelte.d.ts +22 -0
  29. package/dist/apps/notes/tree.d.ts +70 -0
  30. package/dist/apps/notes/tree.js +185 -0
  31. package/dist/apps/notes/types.d.ts +49 -0
  32. package/dist/apps/notes/types.js +11 -0
  33. package/dist/docs/notes.md +82 -0
  34. package/dist/era-ui.css +1 -1
  35. package/dist/generated-docs/llms-full.txt +91 -0
  36. package/dist/generated-docs/llms.txt +1 -1
  37. package/dist/generated-docs/manifest.json +15 -0
  38. package/dist/generated-docs/notes.md +86 -0
  39. package/dist/os/window.svelte +6 -1
  40. package/dist/os/wm.svelte.d.ts +8 -1
  41. package/dist/os/wm.svelte.js +2 -1
  42. package/dist/ui/badge/badge.svelte.d.ts +17 -17
  43. package/dist/ui/bar/bar.svelte.d.ts +11 -11
  44. package/dist/ui/button/variants.d.ts +44 -44
  45. package/dist/ui/card/card.svelte.d.ts +20 -20
  46. package/dist/ui/chip/chip.svelte.d.ts +14 -14
  47. package/dist/ui/pane/pane-root.svelte.d.ts +1 -1
  48. package/dist/ui/pane/pane.svelte.d.ts +1 -1
  49. package/dist/ui/sheet/sheet-content.svelte.d.ts +14 -14
  50. package/dist/ui/skeleton/skeleton.svelte.d.ts +11 -11
  51. package/dist/utils/index.js +11 -0
  52. 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.