@signal9/era-ui 4.14.1 → 4.15.1
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/note-editor.svelte +20 -0
- package/dist/apps/notes/notes.svelte +132 -78
- package/dist/apps/notes/notes.svelte.d.ts +31 -0
- package/dist/dev/audit/audits/prose-leading.js +24 -11
- package/dist/era-ui.css +1 -1
- package/dist/generated-docs/bar.md +1 -0
- package/dist/generated-docs/llms-full.txt +2 -0
- package/dist/generated-docs/manifest.json +2 -2
- package/dist/generated-docs/pane.md +1 -0
- package/dist/os/index.d.ts +1 -1
- package/dist/os/index.js +1 -1
- package/dist/os/pane-manager.svelte.d.ts +16 -0
- package/dist/os/pane-manager.svelte.js +10 -0
- package/dist/os/pane.svelte +16 -9
- package/dist/styles/themes.css +88 -100
- package/dist/ui/bar/bar.svelte +27 -2
- package/dist/ui/bar/bar.svelte.d.ts +68 -0
- package/dist/ui/dialog/dialog-content.svelte +2 -1
- package/dist/ui/dialog/dialog-header.svelte +10 -25
- package/dist/ui/pane/pane-handle.svelte +42 -14
- 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/dist/ui/sheet/sheet-content.svelte +14 -16
- package/dist/ui/sheet/sheet-content.svelte.d.ts +18 -18
- package/package.json +19 -19
|
@@ -209,6 +209,26 @@
|
|
|
209
209
|
text-box-edge: cap alphabetic;
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/* FALLBACK for engines without text-box (Firefox): the trim above silently
|
|
213
|
+
* disappears there, and the first line opened ~0.47em too low (measured:
|
|
214
|
+
* cap 16.4px from the top against 8px from the left, dense/mono) — the
|
|
215
|
+
* exact asymmetry the trim exists to remove. Approximate the same cut with
|
|
216
|
+
* a negative first-block margin: -0.45em is the first line's half-leading
|
|
217
|
+
* (0.3em at line-height 1.6) plus the ascent-above-cap of the ui stacks
|
|
218
|
+
* (~0.15-0.17em). In em on purpose — it scales with the FIRST BLOCK's own
|
|
219
|
+
* size, so a note opening on an h1 and one opening on a paragraph both
|
|
220
|
+
* land their cap on the container inset, which one padding value could
|
|
221
|
+
* never do for both (the reference terminal app's pt-1 is this same idea,
|
|
222
|
+
* tuned only for paragraphs). The margin collapses up through .tiptap and
|
|
223
|
+
* stops at the scroll container's padding, which is where it should act.
|
|
224
|
+
* Within ~1px of the trimmed geometry; engines with text-box never match
|
|
225
|
+
* this block and keep the exact version. */
|
|
226
|
+
@supports not ((text-box-trim: trim-start) and (text-box-edge: cap alphabetic)) {
|
|
227
|
+
.era-notes-prose :global(.tiptap > :first-child) {
|
|
228
|
+
margin-top: -0.45em;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
212
232
|
/* Placeholder, via @tiptap/extension-placeholder */
|
|
213
233
|
.era-notes-prose :global(.tiptap p.is-editor-empty:first-child::before) {
|
|
214
234
|
content: attr(data-placeholder);
|
|
@@ -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,95 @@
|
|
|
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 titleField(note: Note, cls: string)}
|
|
220
|
+
<Input
|
|
221
|
+
reveal
|
|
222
|
+
size="xxs"
|
|
223
|
+
class={cls}
|
|
224
|
+
aria-label="Note title"
|
|
225
|
+
placeholder="Untitled"
|
|
226
|
+
value={note.title}
|
|
227
|
+
oninput={(e: Event & { currentTarget: HTMLInputElement }) =>
|
|
228
|
+
store.update(note.id, { title: e.currentTarget.value })}
|
|
229
|
+
/>
|
|
230
|
+
{/snippet}
|
|
231
|
+
|
|
232
|
+
{#snippet statusBadge()}
|
|
233
|
+
{#if store.status === 'saved'}
|
|
234
|
+
<Badge tone="success">saved</Badge>
|
|
235
|
+
{:else if store.status === 'error'}
|
|
236
|
+
<Badge tone="destructive" title={store.lastError ?? undefined}>error</Badge>
|
|
237
|
+
{/if}
|
|
238
|
+
{/snippet}
|
|
239
|
+
|
|
240
|
+
{#snippet paneHeader()}
|
|
241
|
+
{#if selected}
|
|
242
|
+
<div class="relative flex min-w-0 items-center gap-(--era-gap)" data-pane-control>
|
|
243
|
+
{@render iconPicker(selected)}
|
|
244
|
+
{@render titleField(selected, 'field-sizing-content max-w-56 min-w-0 text-center')}
|
|
245
|
+
<!-- Absolutely positioned, so the badge appearing never nudges the
|
|
246
|
+
centred title; empty when the status is quiet, which costs
|
|
247
|
+
nothing off-flow. -->
|
|
248
|
+
<span class="absolute top-1/2 left-full ml-(--era-gap) -translate-y-1/2">
|
|
249
|
+
{@render statusBadge()}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
{/if}
|
|
253
|
+
{/snippet}
|
|
254
|
+
|
|
141
255
|
<div
|
|
142
256
|
class={cn('flex h-full min-h-0 text-body text-fg', className)}
|
|
143
257
|
use:keys={{
|
|
@@ -163,11 +277,7 @@
|
|
|
163
277
|
centred in 23px and sat 2.5px from the top against a 3px wall. Outset
|
|
164
278
|
costs no layout, so the bar keeps its full tier height and every child
|
|
165
279
|
sits the wall's distance from all four edges. -->
|
|
166
|
-
<Bar
|
|
167
|
-
size="md"
|
|
168
|
-
content="xxs"
|
|
169
|
-
class="shrink-0 rounded-none [box-shadow:0_1px_0_var(--color-divider-faded)]"
|
|
170
|
-
>
|
|
280
|
+
<Bar size="md" content="xxs" divider class="rounded-none">
|
|
171
281
|
<Input
|
|
172
282
|
icon={Search}
|
|
173
283
|
size="xxs"
|
|
@@ -326,77 +436,21 @@
|
|
|
326
436
|
<!-- Matches the sidebar's filter bar exactly — the two headers sit side by
|
|
327
437
|
side across the top of the app, so they must be the same tier. -->
|
|
328
438
|
<!-- Same outset divider as the sidebar's filter bar above. -->
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
{
|
|
341
|
-
</
|
|
342
|
-
|
|
343
|
-
|
|
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>
|
|
377
|
-
|
|
378
|
-
<Input
|
|
379
|
-
reveal
|
|
380
|
-
size="xxs"
|
|
381
|
-
class="min-w-0 flex-1"
|
|
382
|
-
aria-label="Note title"
|
|
383
|
-
placeholder="Untitled"
|
|
384
|
-
value={selected.title}
|
|
385
|
-
oninput={(e: Event & { currentTarget: HTMLInputElement }) =>
|
|
386
|
-
store.update(selected.id, { title: e.currentTarget.value })}
|
|
387
|
-
/>
|
|
388
|
-
|
|
389
|
-
<!-- Fixed-width so the badge appearing and clearing never nudges the
|
|
390
|
-
title field; `saving` is deliberately silent — the badge only
|
|
391
|
-
reports a settled outcome. -->
|
|
392
|
-
<span class="flex w-16 shrink-0 justify-end">
|
|
393
|
-
{#if store.status === 'saved'}
|
|
394
|
-
<Badge tone="success">saved</Badge>
|
|
395
|
-
{:else if store.status === 'error'}
|
|
396
|
-
<Badge tone="destructive" title={store.lastError ?? undefined}>error</Badge>
|
|
397
|
-
{/if}
|
|
398
|
-
</span>
|
|
399
|
-
</Bar>
|
|
439
|
+
<!-- Rendered only when no pane hosts the app — with a pane, the same
|
|
440
|
+
header (see the snippets below) lives in the pane bar's centre and
|
|
441
|
+
drawing it twice would title the document twice. -->
|
|
442
|
+
{#if !pane}
|
|
443
|
+
<Bar size="md" content="xxs" divider class="rounded-none">
|
|
444
|
+
{@render iconPicker(selected)}
|
|
445
|
+
{@render titleField(selected, 'min-w-0 flex-1')}
|
|
446
|
+
<!-- Fixed-width so the badge appearing and clearing never nudges the
|
|
447
|
+
title field; `saving` is deliberately silent — the badge only
|
|
448
|
+
reports a settled outcome. -->
|
|
449
|
+
<span class="flex w-16 shrink-0 justify-end">
|
|
450
|
+
{@render statusBadge()}
|
|
451
|
+
</span>
|
|
452
|
+
</Bar>
|
|
453
|
+
{/if}
|
|
400
454
|
|
|
401
455
|
<div class="min-h-0 flex-1 overflow-x-hidden overflow-y-auto p-card">
|
|
402
456
|
<!-- 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">;
|
|
@@ -49,7 +49,6 @@ function ownTextLines(el) {
|
|
|
49
49
|
tops.add(Math.round(rect.top * 2) / 2);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
range.detach();
|
|
53
52
|
return tops.size;
|
|
54
53
|
}
|
|
55
54
|
export const proseLeading = {
|
|
@@ -58,11 +57,24 @@ export const proseLeading = {
|
|
|
58
57
|
description: 'Text that wraps to a second line must set its own leading (leading-body) — otherwise it inherits a fixed-height row’s control leading and the lines collide.',
|
|
59
58
|
category: 'layout',
|
|
60
59
|
severity: 'error',
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
60
|
+
// Text-bearing elements only. The audit is about PROSE, and the wrapping the
|
|
61
|
+
// check measures happens on the element that owns the text nodes — chrome
|
|
62
|
+
// tags (svg, input, button…) can't fire it, so they never pay for a style
|
|
63
|
+
// read. div/span stay in: half the wrapping copy in this library lives in
|
|
64
|
+
// them.
|
|
65
|
+
selector: 'p, span, div, li, dd, dt, td, th, label, a, blockquote, figcaption, h1, h2, h3, h4, h5, h6',
|
|
64
66
|
check(el) {
|
|
65
|
-
|
|
67
|
+
// Cheapest gate first: any own text at all? A scan over direct children
|
|
68
|
+
// short-circuits; textContent would serialize the whole subtree on every
|
|
69
|
+
// wrapper in the tree.
|
|
70
|
+
let hasText = false;
|
|
71
|
+
for (const n of el.childNodes) {
|
|
72
|
+
if (n.nodeType === Node.TEXT_NODE && n.textContent.trim()) {
|
|
73
|
+
hasText = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!hasText)
|
|
66
78
|
return null;
|
|
67
79
|
const s = getComputedStyle(el);
|
|
68
80
|
// Text that cannot wrap cannot have this bug — the whole defect is the
|
|
@@ -76,16 +88,17 @@ export const proseLeading = {
|
|
|
76
88
|
const ratio = lineHeight / fontSize;
|
|
77
89
|
if (ratio >= PROSE_RATIO)
|
|
78
90
|
return null;
|
|
79
|
-
// The expensive half
|
|
80
|
-
//
|
|
81
|
-
|
|
91
|
+
// The expensive half (a Range + getClientRects is a forced layout read),
|
|
92
|
+
// deliberately last and measured once: only elements whose leading is
|
|
93
|
+
// already known to be too tight pay for it.
|
|
94
|
+
const lines = ownTextLines(el);
|
|
95
|
+
if (lines < 2)
|
|
82
96
|
return null;
|
|
83
|
-
|
|
97
|
+
return {
|
|
84
98
|
auditId: 'typography/prose-leading',
|
|
85
99
|
element: el,
|
|
86
100
|
message: `wrapped text at line-height ${ratio.toFixed(2)}× (${lineHeight.toFixed(1)}px on ${fontSize}px type) — this is control leading, inherited from a fixed-height row. Add leading-body.`,
|
|
87
|
-
details: { ratio: +ratio.toFixed(2), lineHeight, fontSize, lines
|
|
101
|
+
details: { ratio: +ratio.toFixed(2), lineHeight, fontSize, lines }
|
|
88
102
|
};
|
|
89
|
-
return issue;
|
|
90
103
|
}
|
|
91
104
|
};
|