@signal9/era-ui 4.14.1 → 4.15.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/notes/notes.svelte +139 -71
- package/dist/apps/notes/notes.svelte.d.ts +31 -0
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/llms-full.txt +1 -0
- package/dist/generated-docs/manifest.json +1 -1
- package/dist/generated-docs/pane.md +1 -0
- package/dist/os/pane-manager.svelte.d.ts +12 -0
- package/dist/os/pane-manager.svelte.js +4 -0
- package/dist/os/pane.svelte +16 -9
- package/dist/styles/themes.css +38 -33
- package/dist/ui/pane/pane-handle.svelte +36 -1
- package/dist/ui/pane/pane-handle.svelte.d.ts +7 -0
- package/dist/ui/pane/pane.svelte +12 -2
- package/dist/ui/pane/pane.svelte.d.ts +2 -0
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* follows the density, surface, corners, motion, and font axes like any other
|
|
18
18
|
* component in the library.
|
|
19
19
|
*/
|
|
20
|
-
import { onDestroy, onMount, untrack } from 'svelte';
|
|
20
|
+
import { onDestroy, onMount, untrack, type Snippet } from 'svelte';
|
|
21
21
|
import FileText from '@lucide/svelte/icons/file-text';
|
|
22
22
|
import Plus from '@lucide/svelte/icons/plus';
|
|
23
23
|
import Search from '@lucide/svelte/icons/search';
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
import { cn, keys } from '../../utils/index.js';
|
|
37
37
|
import NoteEditor from './note-editor.svelte';
|
|
38
38
|
import { NotesStore, localStorageAdapter, type NotesAdapter } from './notes-store.svelte.js';
|
|
39
|
-
import type { NoteInit, TiptapDocument } from './types.js';
|
|
39
|
+
import type { Note, NoteInit, TiptapDocument } from './types.js';
|
|
40
40
|
|
|
41
41
|
interface Props {
|
|
42
42
|
/**
|
|
@@ -53,6 +53,16 @@
|
|
|
53
53
|
noteId?: string | null;
|
|
54
54
|
/** Notes created only when storage comes back empty — a first-run sample. */
|
|
55
55
|
seed?: NoteInit[];
|
|
56
|
+
/**
|
|
57
|
+
* The pane hosting this app, if any. Structural on purpose — an os-layer
|
|
58
|
+
* `AppPane` satisfies it as-is, and Notes never has to import the os layer.
|
|
59
|
+
* When provided, the document header (icon picker, title, save status)
|
|
60
|
+
* moves out of the editor area and into the pane bar's CENTRE slot, the
|
|
61
|
+
* way the reference terminal app titles its notes window; it is cleared
|
|
62
|
+
* again on unmount. Omit it and the header renders above the editor as a
|
|
63
|
+
* normal in-app bar.
|
|
64
|
+
*/
|
|
65
|
+
pane?: { center?: Snippet } | null;
|
|
56
66
|
class?: string;
|
|
57
67
|
}
|
|
58
68
|
|
|
@@ -62,6 +72,7 @@
|
|
|
62
72
|
storageKey,
|
|
63
73
|
noteId = $bindable(null),
|
|
64
74
|
seed,
|
|
75
|
+
pane,
|
|
65
76
|
class: className
|
|
66
77
|
}: Props = $props();
|
|
67
78
|
|
|
@@ -106,6 +117,20 @@
|
|
|
106
117
|
if (owned) store.destroy();
|
|
107
118
|
});
|
|
108
119
|
|
|
120
|
+
// Claim the pane bar's centre for the document header. An effect rather than
|
|
121
|
+
// a one-shot: `pane` can arrive late (the docs shell finds its Reader pane
|
|
122
|
+
// after mount) or change identity, and each run releases the previous claim.
|
|
123
|
+
// The guard on the cleanup matters — if some later occupant already replaced
|
|
124
|
+
// the snippet, unmounting Notes must not blank THEIR chrome.
|
|
125
|
+
$effect(() => {
|
|
126
|
+
const host = pane;
|
|
127
|
+
if (!host) return;
|
|
128
|
+
host.center = paneHeader;
|
|
129
|
+
return () => {
|
|
130
|
+
if (host.center === paneHeader) host.center = undefined;
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
109
134
|
function createNote() {
|
|
110
135
|
const note = store.create();
|
|
111
136
|
noteId = note.id;
|
|
@@ -138,6 +163,86 @@
|
|
|
138
163
|
}
|
|
139
164
|
</script>
|
|
140
165
|
|
|
166
|
+
{#snippet iconPicker(note: Note)}
|
|
167
|
+
<Popover.Root bind:open={iconPickerOpen}>
|
|
168
|
+
<Popover.Trigger icon size="xxs" aria-label="Change icon" title="Change icon">
|
|
169
|
+
{#if note.icon}
|
|
170
|
+
<span aria-hidden="true">{note.icon}</span>
|
|
171
|
+
{:else}
|
|
172
|
+
<FileText />
|
|
173
|
+
{/if}
|
|
174
|
+
</Popover.Trigger>
|
|
175
|
+
<Popover.Content align="start" class="w-max">
|
|
176
|
+
<div class="flex flex-col gap-gutter">
|
|
177
|
+
{#each ICON_ROWS as row, r (r)}
|
|
178
|
+
<div class="flex gap-gutter">
|
|
179
|
+
{#each row as glyph (glyph)}
|
|
180
|
+
<Button
|
|
181
|
+
icon
|
|
182
|
+
size="default"
|
|
183
|
+
active={note.icon === glyph}
|
|
184
|
+
aria-label={glyph}
|
|
185
|
+
onclick={() => {
|
|
186
|
+
store.setIcon(note.id, glyph);
|
|
187
|
+
iconPickerOpen = false;
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{glyph}
|
|
191
|
+
</Button>
|
|
192
|
+
{/each}
|
|
193
|
+
</div>
|
|
194
|
+
{/each}
|
|
195
|
+
{#if note.icon}
|
|
196
|
+
<Button
|
|
197
|
+
size="sm"
|
|
198
|
+
class="w-full"
|
|
199
|
+
onclick={() => {
|
|
200
|
+
store.setIcon(note.id, null);
|
|
201
|
+
iconPickerOpen = false;
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
Clear icon
|
|
205
|
+
</Button>
|
|
206
|
+
{/if}
|
|
207
|
+
</div>
|
|
208
|
+
</Popover.Content>
|
|
209
|
+
</Popover.Root>
|
|
210
|
+
{/snippet}
|
|
211
|
+
|
|
212
|
+
<!-- The document header, pane-bar edition: the same icon picker and title,
|
|
213
|
+
compacted for the bar's centre track. data-pane-control is what makes an
|
|
214
|
+
EDITABLE title possible inside a drag zone — a press on the cluster means
|
|
215
|
+
caret/click, everywhere else on the bar still means drag. The input is
|
|
216
|
+
content-sized (field-sizing) so the cluster hugs the title and stays
|
|
217
|
+
optically centred; the status badge hangs OFF the flow (absolute, past the
|
|
218
|
+
right edge) so saved/error appearing never nudges the centred title. -->
|
|
219
|
+
{#snippet paneHeader()}
|
|
220
|
+
{#if selected}
|
|
221
|
+
<div class="relative flex min-w-0 items-center gap-(--era-gap)" data-pane-control>
|
|
222
|
+
{@render iconPicker(selected)}
|
|
223
|
+
<Input
|
|
224
|
+
reveal
|
|
225
|
+
size="xxs"
|
|
226
|
+
class="field-sizing-content max-w-56 min-w-0 text-center"
|
|
227
|
+
aria-label="Note title"
|
|
228
|
+
placeholder="Untitled"
|
|
229
|
+
value={selected.title}
|
|
230
|
+
oninput={(e: Event & { currentTarget: HTMLInputElement }) =>
|
|
231
|
+
store.update(selected.id, { title: e.currentTarget.value })}
|
|
232
|
+
/>
|
|
233
|
+
{#if store.status === 'saved' || store.status === 'error'}
|
|
234
|
+
<span class="absolute top-1/2 left-full ml-(--era-gap) -translate-y-1/2">
|
|
235
|
+
{#if store.status === 'saved'}
|
|
236
|
+
<Badge tone="success">saved</Badge>
|
|
237
|
+
{:else}
|
|
238
|
+
<Badge tone="destructive" title={store.lastError ?? undefined}>error</Badge>
|
|
239
|
+
{/if}
|
|
240
|
+
</span>
|
|
241
|
+
{/if}
|
|
242
|
+
</div>
|
|
243
|
+
{/if}
|
|
244
|
+
{/snippet}
|
|
245
|
+
|
|
141
246
|
<div
|
|
142
247
|
class={cn('flex h-full min-h-0 text-body text-fg', className)}
|
|
143
248
|
use:keys={{
|
|
@@ -326,77 +431,40 @@
|
|
|
326
431
|
<!-- Matches the sidebar's filter bar exactly — the two headers sit side by
|
|
327
432
|
side across the top of the app, so they must be the same tier. -->
|
|
328
433
|
<!-- Same outset divider as the sidebar's filter bar above. -->
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
<FileText />
|
|
340
|
-
{/if}
|
|
341
|
-
</Popover.Trigger>
|
|
342
|
-
<Popover.Content align="start" class="w-max">
|
|
343
|
-
<div class="flex flex-col gap-gutter">
|
|
344
|
-
{#each ICON_ROWS as row, r (r)}
|
|
345
|
-
<div class="flex gap-gutter">
|
|
346
|
-
{#each row as glyph (glyph)}
|
|
347
|
-
<Button
|
|
348
|
-
icon
|
|
349
|
-
size="default"
|
|
350
|
-
active={selected.icon === glyph}
|
|
351
|
-
aria-label={glyph}
|
|
352
|
-
onclick={() => {
|
|
353
|
-
store.setIcon(selected.id, glyph);
|
|
354
|
-
iconPickerOpen = false;
|
|
355
|
-
}}
|
|
356
|
-
>
|
|
357
|
-
{glyph}
|
|
358
|
-
</Button>
|
|
359
|
-
{/each}
|
|
360
|
-
</div>
|
|
361
|
-
{/each}
|
|
362
|
-
{#if selected.icon}
|
|
363
|
-
<Button
|
|
364
|
-
size="sm"
|
|
365
|
-
class="w-full"
|
|
366
|
-
onclick={() => {
|
|
367
|
-
store.setIcon(selected.id, null);
|
|
368
|
-
iconPickerOpen = false;
|
|
369
|
-
}}
|
|
370
|
-
>
|
|
371
|
-
Clear icon
|
|
372
|
-
</Button>
|
|
373
|
-
{/if}
|
|
374
|
-
</div>
|
|
375
|
-
</Popover.Content>
|
|
376
|
-
</Popover.Root>
|
|
434
|
+
<!-- Rendered only when no pane hosts the app — with a pane, the same
|
|
435
|
+
header (see the snippets below) lives in the pane bar's centre and
|
|
436
|
+
drawing it twice would title the document twice. -->
|
|
437
|
+
{#if !pane}
|
|
438
|
+
<Bar
|
|
439
|
+
size="md"
|
|
440
|
+
content="xxs"
|
|
441
|
+
class="shrink-0 rounded-none [box-shadow:0_1px_0_var(--color-divider-faded)]"
|
|
442
|
+
>
|
|
443
|
+
{@render iconPicker(selected)}
|
|
377
444
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
445
|
+
<Input
|
|
446
|
+
reveal
|
|
447
|
+
size="xxs"
|
|
448
|
+
class="min-w-0 flex-1"
|
|
449
|
+
aria-label="Note title"
|
|
450
|
+
placeholder="Untitled"
|
|
451
|
+
value={selected.title}
|
|
452
|
+
oninput={(e: Event & { currentTarget: HTMLInputElement }) =>
|
|
453
|
+
store.update(selected.id, { title: e.currentTarget.value })}
|
|
454
|
+
/>
|
|
388
455
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
456
|
+
<!-- Fixed-width so the badge appearing and clearing never nudges the
|
|
457
|
+
title field; `saving` is deliberately silent — the badge only
|
|
458
|
+
reports a settled outcome. -->
|
|
459
|
+
<span class="flex w-16 shrink-0 justify-end">
|
|
460
|
+
{#if store.status === 'saved'}
|
|
461
|
+
<Badge tone="success">saved</Badge>
|
|
462
|
+
{:else if store.status === 'error'}
|
|
463
|
+
<Badge tone="destructive" title={store.lastError ?? undefined}>error</Badge>
|
|
464
|
+
{/if}
|
|
465
|
+
</span>
|
|
466
|
+
</Bar>
|
|
467
|
+
{/if}
|
|
400
468
|
|
|
401
469
|
<div class="min-h-0 flex-1 overflow-x-hidden overflow-y-auto p-card">
|
|
402
470
|
<!-- ProseMirror owns the document once running, so switching notes
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notes — a complete, client-side notes app.
|
|
3
|
+
*
|
|
4
|
+
* An era port of the term/web notes app: same TipTap editor, same slash menu
|
|
5
|
+
* and selection toolbar, same heading-tree index. Two things changed.
|
|
6
|
+
*
|
|
7
|
+
* PERSISTENCE. The original round-tripped every edit through a server via
|
|
8
|
+
* TanStack Query, which forced a whole dirty-override layer — local edits had
|
|
9
|
+
* to out-rank refetched query data, or an in-flight save clobbered what you
|
|
10
|
+
* were typing. Here the store IS the source of truth and persistence is a
|
|
11
|
+
* debounced write behind it, so the editor writes straight through and there
|
|
12
|
+
* is nothing to reconcile.
|
|
13
|
+
*
|
|
14
|
+
* CHROME. Everything visible is an era primitive (Bar, Button, Input, Badge,
|
|
15
|
+
* Popover, ScrollArea, AlertDialog, Skeleton) on era tokens, so the app
|
|
16
|
+
* follows the density, surface, corners, motion, and font axes like any other
|
|
17
|
+
* component in the library.
|
|
18
|
+
*/
|
|
19
|
+
import { type Snippet } from 'svelte';
|
|
1
20
|
import { NotesStore, type NotesAdapter } from './notes-store.svelte.js';
|
|
2
21
|
import type { NoteInit } from './types.js';
|
|
3
22
|
interface Props {
|
|
@@ -15,6 +34,18 @@ interface Props {
|
|
|
15
34
|
noteId?: string | null;
|
|
16
35
|
/** Notes created only when storage comes back empty — a first-run sample. */
|
|
17
36
|
seed?: NoteInit[];
|
|
37
|
+
/**
|
|
38
|
+
* The pane hosting this app, if any. Structural on purpose — an os-layer
|
|
39
|
+
* `AppPane` satisfies it as-is, and Notes never has to import the os layer.
|
|
40
|
+
* When provided, the document header (icon picker, title, save status)
|
|
41
|
+
* moves out of the editor area and into the pane bar's CENTRE slot, the
|
|
42
|
+
* way the reference terminal app titles its notes window; it is cleared
|
|
43
|
+
* again on unmount. Omit it and the header renders above the editor as a
|
|
44
|
+
* normal in-app bar.
|
|
45
|
+
*/
|
|
46
|
+
pane?: {
|
|
47
|
+
center?: Snippet;
|
|
48
|
+
} | null;
|
|
18
49
|
class?: string;
|
|
19
50
|
}
|
|
20
51
|
declare const Notes: import("svelte").Component<Props, {}, "noteId">;
|