@signal9/era-ui 4.15.0 → 4.15.2

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.
@@ -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
- <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}
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
- {#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}
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
- // Only elements that can hold text directly; a <div> wrapper with no text of
62
- // its own has nothing to measure and is skipped by ownTextLines anyway.
63
- selector: '*',
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
- if (!el.textContent?.trim())
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, deliberately last: only measure once the leading is
80
- // already known to be too tight.
81
- if (ownTextLines(el) < 2)
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
- const issue = {
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: ownTextLines(el) }
101
+ details: { ratio: +ratio.toFixed(2), lineHeight, fontSize, lines }
88
102
  };
89
- return issue;
90
103
  }
91
104
  };