@signal9/era-ui 3.9.0 → 3.11.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/dev/audit/audits/index.d.ts +2 -1
- package/dist/dev/audit/audits/index.js +3 -1
- package/dist/dev/audit/audits/resting-gap.d.ts +2 -0
- package/dist/dev/audit/audits/resting-gap.js +78 -0
- package/dist/docs/notes.md +82 -0
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/llms-full.txt +92 -1
- package/dist/generated-docs/llms.txt +1 -1
- package/dist/generated-docs/manifest.json +16 -1
- package/dist/generated-docs/notes.md +86 -0
- package/dist/generated-docs/utilities.json +1 -1
- package/dist/generated-docs/utilities.md +1 -1
- package/dist/styles/index.css +2 -1
- package/dist/styles/themes.css +70 -52
- 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,456 @@
|
|
|
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
|
+
|
|
22
|
+
interface Props {
|
|
23
|
+
/** Initial document. Later changes are ignored — re-key to reseed. */
|
|
24
|
+
content: JSONContent;
|
|
25
|
+
onUpdate?: (json: JSONContent) => void;
|
|
26
|
+
/** Placeholder shown on an empty first paragraph. */
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let { content, onUpdate, class: className }: Props = $props();
|
|
31
|
+
|
|
32
|
+
let editor: Editor | undefined;
|
|
33
|
+
|
|
34
|
+
type MarkdownStorage = { markdown?: { getMarkdown?: () => string } };
|
|
35
|
+
|
|
36
|
+
let activeFormats = $state<Record<string, boolean>>({});
|
|
37
|
+
|
|
38
|
+
function updateActiveFormats(ed: Editor) {
|
|
39
|
+
activeFormats = {
|
|
40
|
+
bold: ed.isActive('bold'),
|
|
41
|
+
italic: ed.isActive('italic'),
|
|
42
|
+
strike: ed.isActive('strike'),
|
|
43
|
+
code: ed.isActive('code'),
|
|
44
|
+
highlight: ed.isActive('highlight'),
|
|
45
|
+
heading: ed.isActive('heading'),
|
|
46
|
+
bulletList: ed.isActive('bulletList'),
|
|
47
|
+
orderedList: ed.isActive('orderedList'),
|
|
48
|
+
taskList: ed.isActive('taskList'),
|
|
49
|
+
blockquote: ed.isActive('blockquote'),
|
|
50
|
+
codeBlock: ed.isActive('codeBlock'),
|
|
51
|
+
details: ed.isActive('details'),
|
|
52
|
+
table: ed.isActive('table'),
|
|
53
|
+
link: ed.isActive('link')
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function initEditor(element: HTMLDivElement) {
|
|
58
|
+
// untrack: the attachment must not re-run when `content` or `onUpdate`
|
|
59
|
+
// change — tearing down and rebuilding the editor mid-typing would lose
|
|
60
|
+
// the selection and the undo history.
|
|
61
|
+
return untrack(() => {
|
|
62
|
+
editor = new Editor({
|
|
63
|
+
element,
|
|
64
|
+
extensions: noteExtensions,
|
|
65
|
+
content,
|
|
66
|
+
onUpdate: ({ editor: ed }) => {
|
|
67
|
+
onUpdate?.(ed.getJSON());
|
|
68
|
+
updateActiveFormats(ed);
|
|
69
|
+
},
|
|
70
|
+
onSelectionUpdate: ({ editor: ed }) => {
|
|
71
|
+
updateActiveFormats(ed);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return () => {
|
|
75
|
+
editor?.destroy();
|
|
76
|
+
editor = undefined;
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* Imperative API. The bubble toolbar and slash menu already cover these for a
|
|
83
|
+
* pointer/keyboard user; these exports exist so a HOST can drive the document
|
|
84
|
+
* from its own chrome — a window toolbar, a command palette, an agent tool —
|
|
85
|
+
* without reaching into TipTap itself. Bind the component and call them.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
export function isActive(name: string) {
|
|
89
|
+
return activeFormats[name] ?? false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function focus() {
|
|
93
|
+
editor?.chain().focus().run();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatBold() {
|
|
97
|
+
editor?.chain().focus().toggleBold().run();
|
|
98
|
+
}
|
|
99
|
+
export function formatItalic() {
|
|
100
|
+
editor?.chain().focus().toggleItalic().run();
|
|
101
|
+
}
|
|
102
|
+
export function formatStrike() {
|
|
103
|
+
editor?.chain().focus().toggleStrike().run();
|
|
104
|
+
}
|
|
105
|
+
export function formatCode() {
|
|
106
|
+
editor?.chain().focus().toggleCode().run();
|
|
107
|
+
}
|
|
108
|
+
export function formatHighlight() {
|
|
109
|
+
editor?.chain().focus().toggleHighlight().run();
|
|
110
|
+
}
|
|
111
|
+
export function formatLink() {
|
|
112
|
+
if (editor) void import('./editor/link.js').then((m) => m.toggleLink(editor!));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatHeading(level: 1 | 2 | 3 = 2) {
|
|
116
|
+
editor?.chain().focus().toggleHeading({ level }).run();
|
|
117
|
+
}
|
|
118
|
+
export function formatBlockquote() {
|
|
119
|
+
editor?.chain().focus().toggleBlockquote().run();
|
|
120
|
+
}
|
|
121
|
+
export function formatList() {
|
|
122
|
+
editor?.chain().focus().toggleBulletList().run();
|
|
123
|
+
}
|
|
124
|
+
export function formatOrderedList() {
|
|
125
|
+
editor?.chain().focus().toggleOrderedList().run();
|
|
126
|
+
}
|
|
127
|
+
export function formatTaskList() {
|
|
128
|
+
editor?.chain().focus().toggleTaskList().run();
|
|
129
|
+
}
|
|
130
|
+
export function formatCodeBlock() {
|
|
131
|
+
editor?.chain().focus().toggleCodeBlock().run();
|
|
132
|
+
}
|
|
133
|
+
export function formatDetails() {
|
|
134
|
+
editor?.chain().focus().setDetails().run();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function insertTable() {
|
|
138
|
+
editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
139
|
+
}
|
|
140
|
+
export function deleteTable() {
|
|
141
|
+
editor?.chain().focus().deleteTable().run();
|
|
142
|
+
}
|
|
143
|
+
export function addTableRow() {
|
|
144
|
+
editor?.chain().focus().addRowAfter().run();
|
|
145
|
+
}
|
|
146
|
+
export function addTableCol() {
|
|
147
|
+
editor?.chain().focus().addColumnAfter().run();
|
|
148
|
+
}
|
|
149
|
+
export function deleteTableRow() {
|
|
150
|
+
editor?.chain().focus().deleteRow().run();
|
|
151
|
+
}
|
|
152
|
+
export function deleteTableCol() {
|
|
153
|
+
editor?.chain().focus().deleteColumn().run();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function insertImage() {
|
|
157
|
+
const url = globalThis.prompt?.('Image URL:');
|
|
158
|
+
if (url) editor?.chain().focus().setImage({ src: url }).run();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The document as markdown, via tiptap-markdown's serializer. */
|
|
162
|
+
export function getMarkdown(): string {
|
|
163
|
+
return (editor?.storage as MarkdownStorage | undefined)?.markdown?.getMarkdown?.() ?? '';
|
|
164
|
+
}
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<!-- eslint-disable better-tailwindcss/no-unknown-classes -- era-notes-prose is defined in this component's scoped stylesheet -->
|
|
168
|
+
<div class="era-notes-prose min-w-0 {className ?? ''}" {@attach initEditor}></div>
|
|
169
|
+
|
|
170
|
+
<style>
|
|
171
|
+
/*
|
|
172
|
+
* TipTap renders headless: the editable element it creates carries `.tiptap`,
|
|
173
|
+
* so every rule here is scoped to that inside this component's wrapper.
|
|
174
|
+
*
|
|
175
|
+
* Vertical rhythm is ONE value — --era-gap, the library's inter-element gap —
|
|
176
|
+
* on every block's bottom margin. A document then breathes at exactly the
|
|
177
|
+
* pace the surrounding chrome does, and loosens with density along with it.
|
|
178
|
+
*
|
|
179
|
+
* Font family is deliberately absent: the [data-font] axis sets it globally
|
|
180
|
+
* (with !important, by design), so anything declared here would either lose
|
|
181
|
+
* or break the axis promise.
|
|
182
|
+
*/
|
|
183
|
+
.era-notes-prose :global(.tiptap) {
|
|
184
|
+
outline: none;
|
|
185
|
+
min-height: 100%;
|
|
186
|
+
font-size: var(--era-text);
|
|
187
|
+
line-height: 1.6;
|
|
188
|
+
color: var(--color-fg);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* Placeholder, via @tiptap/extension-placeholder */
|
|
192
|
+
.era-notes-prose :global(.tiptap p.is-editor-empty:first-child::before) {
|
|
193
|
+
content: attr(data-placeholder);
|
|
194
|
+
float: left;
|
|
195
|
+
height: 0;
|
|
196
|
+
color: var(--color-muted);
|
|
197
|
+
pointer-events: none;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* Typography */
|
|
201
|
+
.era-notes-prose :global(.tiptap p) {
|
|
202
|
+
margin: 0 0 var(--era-gap);
|
|
203
|
+
}
|
|
204
|
+
.era-notes-prose :global(.tiptap p:last-child) {
|
|
205
|
+
margin-bottom: 0;
|
|
206
|
+
}
|
|
207
|
+
.era-notes-prose :global(.tiptap h1) {
|
|
208
|
+
font-size: 1.5em;
|
|
209
|
+
}
|
|
210
|
+
.era-notes-prose :global(.tiptap h2) {
|
|
211
|
+
font-size: 1.25em;
|
|
212
|
+
}
|
|
213
|
+
.era-notes-prose :global(.tiptap h3) {
|
|
214
|
+
font-size: 1.1em;
|
|
215
|
+
}
|
|
216
|
+
.era-notes-prose :global(.tiptap h1),
|
|
217
|
+
.era-notes-prose :global(.tiptap h2),
|
|
218
|
+
.era-notes-prose :global(.tiptap h3) {
|
|
219
|
+
margin: 0 0 var(--era-gap);
|
|
220
|
+
font-weight: 600;
|
|
221
|
+
line-height: 1.6;
|
|
222
|
+
color: var(--color-bright);
|
|
223
|
+
}
|
|
224
|
+
.era-notes-prose :global(.tiptap strong) {
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
color: var(--color-bright);
|
|
227
|
+
}
|
|
228
|
+
.era-notes-prose :global(.tiptap em) {
|
|
229
|
+
font-style: italic;
|
|
230
|
+
}
|
|
231
|
+
.era-notes-prose :global(.tiptap s) {
|
|
232
|
+
text-decoration: line-through;
|
|
233
|
+
text-decoration-thickness: 0.5px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* Code — inline chips take the xxs radius, blocks the lg one a Card uses. */
|
|
237
|
+
.era-notes-prose :global(.tiptap code) {
|
|
238
|
+
padding: 1px var(--era-inset-xxs);
|
|
239
|
+
border-radius: var(--era-rd-xxs);
|
|
240
|
+
background: var(--era-surface-bg-elevated);
|
|
241
|
+
}
|
|
242
|
+
.era-notes-prose :global(.tiptap pre) {
|
|
243
|
+
overflow-x: auto;
|
|
244
|
+
margin: 0 0 var(--era-gap);
|
|
245
|
+
padding: var(--era-inset-md);
|
|
246
|
+
border-radius: var(--era-rd-lg);
|
|
247
|
+
background: var(--color-surface);
|
|
248
|
+
box-shadow: var(--era-shadow);
|
|
249
|
+
}
|
|
250
|
+
.era-notes-prose :global(.tiptap pre code) {
|
|
251
|
+
padding: 0;
|
|
252
|
+
border-radius: 0;
|
|
253
|
+
background: transparent;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Syntax highlighting — the neutral scale, so it themes with everything else. */
|
|
257
|
+
.era-notes-prose :global(.tiptap pre .hljs-keyword),
|
|
258
|
+
.era-notes-prose :global(.tiptap pre .hljs-built_in) {
|
|
259
|
+
color: var(--color-9);
|
|
260
|
+
}
|
|
261
|
+
.era-notes-prose :global(.tiptap pre .hljs-string),
|
|
262
|
+
.era-notes-prose :global(.tiptap pre .hljs-regexp) {
|
|
263
|
+
color: var(--color-10);
|
|
264
|
+
}
|
|
265
|
+
.era-notes-prose :global(.tiptap pre .hljs-number),
|
|
266
|
+
.era-notes-prose :global(.tiptap pre .hljs-literal),
|
|
267
|
+
.era-notes-prose :global(.tiptap pre .hljs-type),
|
|
268
|
+
.era-notes-prose :global(.tiptap pre .hljs-class) {
|
|
269
|
+
color: var(--color-11);
|
|
270
|
+
}
|
|
271
|
+
.era-notes-prose :global(.tiptap pre .hljs-comment) {
|
|
272
|
+
color: var(--color-8);
|
|
273
|
+
}
|
|
274
|
+
.era-notes-prose :global(.tiptap pre .hljs-function),
|
|
275
|
+
.era-notes-prose :global(.tiptap pre .hljs-title) {
|
|
276
|
+
font-weight: 600;
|
|
277
|
+
color: var(--color-9);
|
|
278
|
+
}
|
|
279
|
+
.era-notes-prose :global(.tiptap pre .hljs-variable),
|
|
280
|
+
.era-notes-prose :global(.tiptap pre .hljs-attr) {
|
|
281
|
+
color: var(--color-12);
|
|
282
|
+
}
|
|
283
|
+
.era-notes-prose :global(.tiptap pre .hljs-params) {
|
|
284
|
+
color: var(--color-11);
|
|
285
|
+
}
|
|
286
|
+
.era-notes-prose :global(.tiptap pre .hljs-meta),
|
|
287
|
+
.era-notes-prose :global(.tiptap pre .hljs-tag) {
|
|
288
|
+
color: var(--color-10);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* Blockquote */
|
|
292
|
+
.era-notes-prose :global(.tiptap blockquote) {
|
|
293
|
+
margin: 0 0 var(--era-gap);
|
|
294
|
+
padding-left: var(--era-inset-md);
|
|
295
|
+
border-left: 2px solid var(--era-divider-color);
|
|
296
|
+
color: var(--color-muted);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Lists */
|
|
300
|
+
.era-notes-prose :global(.tiptap li),
|
|
301
|
+
.era-notes-prose :global(.tiptap li p),
|
|
302
|
+
.era-notes-prose :global(.tiptap ul),
|
|
303
|
+
.era-notes-prose :global(.tiptap ol) {
|
|
304
|
+
margin: 0;
|
|
305
|
+
}
|
|
306
|
+
.era-notes-prose :global(.tiptap ul) {
|
|
307
|
+
padding-left: 1.4em;
|
|
308
|
+
list-style-type: disc;
|
|
309
|
+
}
|
|
310
|
+
.era-notes-prose :global(.tiptap ol) {
|
|
311
|
+
padding-left: 1.9em;
|
|
312
|
+
list-style-type: decimal;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Task list — a checkbox styled like era's Checkbox, at the icon tier. */
|
|
316
|
+
.era-notes-prose :global(.tiptap .task-list) {
|
|
317
|
+
padding-left: 0;
|
|
318
|
+
list-style: none;
|
|
319
|
+
}
|
|
320
|
+
.era-notes-prose :global(.tiptap .task-item) {
|
|
321
|
+
display: flex;
|
|
322
|
+
gap: var(--era-gap);
|
|
323
|
+
}
|
|
324
|
+
.era-notes-prose :global(.tiptap .task-item label) {
|
|
325
|
+
/* Match the line box so the box centres against the first line of text. */
|
|
326
|
+
display: flex;
|
|
327
|
+
align-items: center;
|
|
328
|
+
height: calc(var(--era-text) * 1.6);
|
|
329
|
+
}
|
|
330
|
+
.era-notes-prose :global(.tiptap .task-item input[type='checkbox']) {
|
|
331
|
+
flex-shrink: 0;
|
|
332
|
+
width: calc(var(--era-h-xs) - 4px);
|
|
333
|
+
height: calc(var(--era-h-xs) - 4px);
|
|
334
|
+
border-radius: var(--era-rd-xs);
|
|
335
|
+
background: var(--era-surface-bg-elevated);
|
|
336
|
+
box-shadow: var(--era-shadow-well);
|
|
337
|
+
appearance: none;
|
|
338
|
+
cursor: pointer;
|
|
339
|
+
transition:
|
|
340
|
+
background var(--era-duration) var(--era-ease),
|
|
341
|
+
box-shadow var(--era-duration) var(--era-ease);
|
|
342
|
+
}
|
|
343
|
+
.era-notes-prose :global(.tiptap .task-item input[type='checkbox']:hover) {
|
|
344
|
+
background: var(--era-highlight);
|
|
345
|
+
}
|
|
346
|
+
.era-notes-prose :global(.tiptap .task-item input[type='checkbox']:checked) {
|
|
347
|
+
background: var(--color-primary);
|
|
348
|
+
box-shadow: var(--era-shadow-pressed);
|
|
349
|
+
}
|
|
350
|
+
.era-notes-prose :global(.tiptap .task-item[data-checked='true'] > div > p) {
|
|
351
|
+
color: var(--color-muted);
|
|
352
|
+
text-decoration: line-through;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/* Horizontal rule */
|
|
356
|
+
.era-notes-prose :global(.tiptap hr) {
|
|
357
|
+
margin: var(--era-gap) 0;
|
|
358
|
+
border: none;
|
|
359
|
+
border-top: 1px solid var(--era-divider-color);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/* Links — the era-link recipe, inlined because ProseMirror owns this DOM. */
|
|
363
|
+
.era-notes-prose :global(.tiptap a) {
|
|
364
|
+
color: var(--color-link);
|
|
365
|
+
text-decoration-line: underline;
|
|
366
|
+
text-decoration-color: color-mix(in oklch, var(--color-muted) 30%, transparent);
|
|
367
|
+
text-underline-offset: 0.2em;
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
transition: text-decoration-color var(--era-duration) var(--era-ease);
|
|
370
|
+
}
|
|
371
|
+
.era-notes-prose :global(.tiptap a:hover) {
|
|
372
|
+
text-decoration-color: var(--color-fg);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/* Highlight — translucent warning so it reads on every theme and surface. */
|
|
376
|
+
.era-notes-prose :global(.tiptap mark) {
|
|
377
|
+
padding: 1px 0;
|
|
378
|
+
background: oklch(from var(--color-warning) l c h / 0.25);
|
|
379
|
+
color: inherit;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* Table */
|
|
383
|
+
.era-notes-prose :global(.tiptap .note-table) {
|
|
384
|
+
width: 100%;
|
|
385
|
+
margin: 0 0 var(--era-gap);
|
|
386
|
+
border-collapse: collapse;
|
|
387
|
+
table-layout: fixed;
|
|
388
|
+
}
|
|
389
|
+
.era-notes-prose :global(.tiptap .note-table td),
|
|
390
|
+
.era-notes-prose :global(.tiptap .note-table th) {
|
|
391
|
+
padding: var(--era-inset-sm) var(--era-inset-md);
|
|
392
|
+
border: 1px solid var(--era-divider-color);
|
|
393
|
+
text-align: left;
|
|
394
|
+
vertical-align: top;
|
|
395
|
+
}
|
|
396
|
+
.era-notes-prose :global(.tiptap .note-table th) {
|
|
397
|
+
background: var(--color-surface);
|
|
398
|
+
font-weight: 600;
|
|
399
|
+
color: var(--color-bright);
|
|
400
|
+
}
|
|
401
|
+
.era-notes-prose :global(.tiptap .note-table td > p),
|
|
402
|
+
.era-notes-prose :global(.tiptap .note-table th > p) {
|
|
403
|
+
margin: 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/* Details (collapsible) */
|
|
407
|
+
.era-notes-prose :global(.tiptap details) {
|
|
408
|
+
margin: 0 0 var(--era-gap);
|
|
409
|
+
padding: var(--era-inset-sm) var(--era-inset-md);
|
|
410
|
+
border-radius: var(--era-rd-md);
|
|
411
|
+
background: var(--era-surface-bg-elevated);
|
|
412
|
+
box-shadow: var(--era-shadow);
|
|
413
|
+
}
|
|
414
|
+
.era-notes-prose :global(.tiptap details summary) {
|
|
415
|
+
font-weight: 600;
|
|
416
|
+
color: var(--color-bright);
|
|
417
|
+
cursor: pointer;
|
|
418
|
+
}
|
|
419
|
+
.era-notes-prose :global(.tiptap details > div) {
|
|
420
|
+
margin-top: var(--era-gap);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* Image */
|
|
424
|
+
.era-notes-prose :global(.tiptap img) {
|
|
425
|
+
max-width: 100%;
|
|
426
|
+
height: auto;
|
|
427
|
+
border-radius: var(--era-rd-md);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/*
|
|
431
|
+
* ProseMirror ships hardcoded selection colours (#8cf outlines, a black gap
|
|
432
|
+
* cursor, rgba(200,200,255,.4) cell selection, #adf resize handles). Each is
|
|
433
|
+
* overridden here or the editor shows chrome from a different design system.
|
|
434
|
+
*/
|
|
435
|
+
.era-notes-prose :global(.tiptap .ProseMirror-selectednode) {
|
|
436
|
+
outline: 2px solid var(--color-primary);
|
|
437
|
+
}
|
|
438
|
+
.era-notes-prose :global(.tiptap li.ProseMirror-selectednode) {
|
|
439
|
+
outline: none;
|
|
440
|
+
}
|
|
441
|
+
.era-notes-prose :global(.tiptap li.ProseMirror-selectednode::after) {
|
|
442
|
+
border-color: var(--color-primary);
|
|
443
|
+
}
|
|
444
|
+
.era-notes-prose :global(.ProseMirror-gapcursor::after) {
|
|
445
|
+
border-top-color: var(--color-primary);
|
|
446
|
+
}
|
|
447
|
+
.era-notes-prose :global(.tiptap .selectedCell::after) {
|
|
448
|
+
background: var(--era-highlight);
|
|
449
|
+
}
|
|
450
|
+
.era-notes-prose :global(.tiptap .column-resize-handle) {
|
|
451
|
+
background-color: var(--color-primary);
|
|
452
|
+
}
|
|
453
|
+
.era-notes-prose :global(.tiptap ::selection) {
|
|
454
|
+
background: var(--color-4);
|
|
455
|
+
}
|
|
456
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type JSONContent } from '@tiptap/core';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Initial document. Later changes are ignored — re-key to reseed. */
|
|
4
|
+
content: JSONContent;
|
|
5
|
+
onUpdate?: (json: JSONContent) => void;
|
|
6
|
+
/** Placeholder shown on an empty first paragraph. */
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const NoteEditor: import("svelte").Component<Props, {
|
|
10
|
+
isActive: (name: string) => boolean;
|
|
11
|
+
focus: () => void;
|
|
12
|
+
formatBold: () => void;
|
|
13
|
+
formatItalic: () => void;
|
|
14
|
+
formatStrike: () => void;
|
|
15
|
+
formatCode: () => void;
|
|
16
|
+
formatHighlight: () => void;
|
|
17
|
+
formatLink: () => void;
|
|
18
|
+
formatHeading: (level?: 1 | 2 | 3) => void;
|
|
19
|
+
formatBlockquote: () => void;
|
|
20
|
+
formatList: () => void;
|
|
21
|
+
formatOrderedList: () => void;
|
|
22
|
+
formatTaskList: () => void;
|
|
23
|
+
formatCodeBlock: () => void;
|
|
24
|
+
formatDetails: () => void;
|
|
25
|
+
insertTable: () => void;
|
|
26
|
+
deleteTable: () => void;
|
|
27
|
+
addTableRow: () => void;
|
|
28
|
+
addTableCol: () => void;
|
|
29
|
+
deleteTableRow: () => void;
|
|
30
|
+
deleteTableCol: () => void;
|
|
31
|
+
insertImage: () => void;
|
|
32
|
+
getMarkdown: () => string;
|
|
33
|
+
}, "">;
|
|
34
|
+
type NoteEditor = ReturnType<typeof NoteEditor>;
|
|
35
|
+
export default NoteEditor;
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
}
|