@signal9/era-ui 4.15.0 → 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 +32 -46
- 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 +1 -0
- package/dist/generated-docs/manifest.json +1 -1
- package/dist/os/index.d.ts +1 -1
- package/dist/os/index.js +1 -1
- package/dist/os/pane-manager.svelte.d.ts +4 -0
- package/dist/os/pane-manager.svelte.js +6 -0
- package/dist/styles/themes.css +50 -67
- 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 +12 -19
- 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);
|
|
@@ -216,29 +216,38 @@
|
|
|
216
216
|
content-sized (field-sizing) so the cluster hugs the title and stays
|
|
217
217
|
optically centred; the status badge hangs OFF the flow (absolute, past the
|
|
218
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
|
+
|
|
219
240
|
{#snippet paneHeader()}
|
|
220
241
|
{#if selected}
|
|
221
242
|
<div class="relative flex min-w-0 items-center gap-(--era-gap)" data-pane-control>
|
|
222
243
|
{@render iconPicker(selected)}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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}
|
|
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>
|
|
242
251
|
</div>
|
|
243
252
|
{/if}
|
|
244
253
|
{/snippet}
|
|
@@ -268,11 +277,7 @@
|
|
|
268
277
|
centred in 23px and sat 2.5px from the top against a 3px wall. Outset
|
|
269
278
|
costs no layout, so the bar keeps its full tier height and every child
|
|
270
279
|
sits the wall's distance from all four edges. -->
|
|
271
|
-
<Bar
|
|
272
|
-
size="md"
|
|
273
|
-
content="xxs"
|
|
274
|
-
class="shrink-0 rounded-none [box-shadow:0_1px_0_var(--color-divider-faded)]"
|
|
275
|
-
>
|
|
280
|
+
<Bar size="md" content="xxs" divider class="rounded-none">
|
|
276
281
|
<Input
|
|
277
282
|
icon={Search}
|
|
278
283
|
size="xxs"
|
|
@@ -435,33 +440,14 @@
|
|
|
435
440
|
header (see the snippets below) lives in the pane bar's centre and
|
|
436
441
|
drawing it twice would title the document twice. -->
|
|
437
442
|
{#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
|
+
<Bar size="md" content="xxs" divider class="rounded-none">
|
|
443
444
|
{@render iconPicker(selected)}
|
|
444
|
-
|
|
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
|
-
/>
|
|
455
|
-
|
|
445
|
+
{@render titleField(selected, 'min-w-0 flex-1')}
|
|
456
446
|
<!-- Fixed-width so the badge appearing and clearing never nudges the
|
|
457
447
|
title field; `saving` is deliberately silent — the badge only
|
|
458
448
|
reports a settled outcome. -->
|
|
459
449
|
<span class="flex w-16 shrink-0 justify-end">
|
|
460
|
-
{
|
|
461
|
-
<Badge tone="success">saved</Badge>
|
|
462
|
-
{:else if store.status === 'error'}
|
|
463
|
-
<Badge tone="destructive" title={store.lastError ?? undefined}>error</Badge>
|
|
464
|
-
{/if}
|
|
450
|
+
{@render statusBadge()}
|
|
465
451
|
</span>
|
|
466
452
|
</Bar>
|
|
467
453
|
{/if}
|
|
@@ -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
|
};
|