@signal9/era-ui 9.0.0 → 10.1.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.
Files changed (78) hide show
  1. package/dist/ai/artifact/artifact-close.svelte +2 -2
  2. package/dist/ai/checkpoint.svelte +1 -1
  3. package/dist/ai/context/context.svelte +1 -1
  4. package/dist/ai/conversation/conversation-scroll-button.svelte +1 -1
  5. package/dist/ai/conversations/conversations-item.svelte +3 -3
  6. package/dist/ai/index.js +1 -1
  7. package/dist/ai/message/message-branch-selector.svelte +2 -2
  8. package/dist/ai/message/message-usage.svelte +1 -1
  9. package/dist/ai/prompt-input/prompt-input-add-attachments.svelte +1 -1
  10. package/dist/ai/prompt-input/prompt-input-attachments.svelte +1 -1
  11. package/dist/ai/prompt-input/prompt-input-submit.svelte +1 -1
  12. package/dist/ai/run-status/run-status.svelte +4 -3
  13. package/dist/ai/suggestion/suggestion.svelte +1 -1
  14. package/dist/ai/swarm/swarm-actions.svelte +2 -2
  15. package/dist/ai/swarm/swarm-agent.svelte +2 -2
  16. package/dist/ai/swarm/swarm-artifact.svelte +1 -1
  17. package/dist/ai/swarm/swarm-barrier.svelte +1 -1
  18. package/dist/ai/swarm/swarm-footer.svelte +3 -2
  19. package/dist/ai/swarm/swarm-header.svelte +1 -1
  20. package/dist/ai/swarm/swarm-node.svelte +1 -1
  21. package/dist/ai/swarm/swarm-role.svelte +5 -2
  22. package/dist/ai/tool/tool-chip.svelte +1 -1
  23. package/dist/ai/web-preview/web-preview.svelte +2 -2
  24. package/dist/apps/index.d.ts +3 -3
  25. package/dist/apps/index.js +3 -3
  26. package/dist/apps/notes/editor/bubble-toolbar.svelte +3 -3
  27. package/dist/apps/notes/editor/slash-menu.svelte +2 -4
  28. package/dist/apps/notes/notes.svelte +12 -12
  29. package/dist/dev/audit/audits/anonymous-text.d.ts +2 -0
  30. package/dist/dev/audit/audits/anonymous-text.js +73 -0
  31. package/dist/dev/audit/audits/index.d.ts +2 -1
  32. package/dist/dev/audit/audits/index.js +3 -1
  33. package/dist/dev/audit/types.d.ts +1 -1
  34. package/dist/docs/css-utilities.js +1 -1
  35. package/dist/docs/llms-txt.js +2 -2
  36. package/dist/docs/notes.md +3 -3
  37. package/dist/era-ui.css +1 -1
  38. package/dist/generated-docs/bar.md +2 -2
  39. package/dist/generated-docs/button.md +1 -1
  40. package/dist/generated-docs/chip.md +1 -1
  41. package/dist/generated-docs/input.md +1 -1
  42. package/dist/generated-docs/llms-full.txt +101 -21
  43. package/dist/generated-docs/llms.txt +11 -7
  44. package/dist/generated-docs/manifest.json +32 -11
  45. package/dist/generated-docs/notes.md +3 -3
  46. package/dist/generated-docs/utilities.json +23 -4
  47. package/dist/generated-docs/utilities.md +82 -6
  48. package/dist/os/notification-center.svelte +1 -1
  49. package/dist/os/pane.svelte +2 -2
  50. package/dist/os/taskbar.svelte +4 -4
  51. package/dist/os/toast.svelte +2 -2
  52. package/dist/styles/index.css +43 -5
  53. package/dist/ui/bar/bar.svelte +13 -13
  54. package/dist/ui/bar/bar.svelte.d.ts +15 -15
  55. package/dist/ui/button/button.svelte +15 -4
  56. package/dist/ui/button/button.svelte.d.ts +1 -1
  57. package/dist/ui/button/index.d.ts +1 -1
  58. package/dist/ui/button/variants.d.ts +10 -13
  59. package/dist/ui/button/variants.js +13 -15
  60. package/dist/ui/chip/chip.svelte +25 -15
  61. package/dist/ui/chip/chip.svelte.d.ts +8 -8
  62. package/dist/ui/command/command-item.svelte +1 -1
  63. package/dist/ui/command/command-link-item.svelte +1 -1
  64. package/dist/ui/command-bar/command-bar.svelte +1 -1
  65. package/dist/ui/copy-button/copy-button.svelte +1 -1
  66. package/dist/ui/dialog/dialog-header.svelte +7 -2
  67. package/dist/ui/index.js +1 -1
  68. package/dist/ui/input/input.svelte +3 -3
  69. package/dist/ui/input/input.svelte.d.ts +1 -1
  70. package/dist/ui/input/variants.d.ts +6 -6
  71. package/dist/ui/input/variants.js +3 -3
  72. package/dist/ui/kv/kv.svelte +1 -1
  73. package/dist/ui/pane/pane-close.svelte +2 -2
  74. package/dist/ui/pane/pane-handle.svelte +3 -3
  75. package/dist/ui/select/select-trigger.svelte +1 -1
  76. package/dist/ui/select/select-value.svelte +1 -1
  77. package/dist/ui/step/step-summary.svelte +1 -1
  78. package/package.json +1 -1
@@ -4,10 +4,10 @@
4
4
  let { onclick }: { onclick?: (e: MouseEvent) => void } = $props();
5
5
  </script>
6
6
 
7
- <!-- size="xxs", the close-affordance tier (Pane.Close, the chip dismiss): an
7
+ <!-- size="pill", the close-affordance tier (Pane.Close, the chip dismiss): an
8
8
  18px hit target whose tier draws the 12px dismiss ink by itself. The <X>
9
9
  used to carry size-(--era-h-icon) — a class the Button variant's [&_svg]
10
10
  selector outranks, so it claimed 14px while 10 shipped. -->
11
- <Button icon size="xxs" aria-label="Close" {onclick}>
11
+ <Button icon size="pill" aria-label="Close" {onclick}>
12
12
  <X />
13
13
  </Button>
@@ -20,7 +20,7 @@
20
20
 
21
21
  <div class={cn('flex w-full items-center gap-(--era-gap) text-muted', className)}>
22
22
  <Bookmark class="size-(--era-h-icon) shrink-0" aria-hidden="true" />
23
- <Button variant="link" size="sm" onclick={onRestore}>
23
+ <Button variant="link" size="chip" onclick={onRestore}>
24
24
  {#if children}{@render children()}{:else}{label}{/if}
25
25
  </Button>
26
26
  <Separator class="flex-1" />
@@ -23,7 +23,7 @@
23
23
  </script>
24
24
 
25
25
  <Popover.Root>
26
- <Popover.Trigger size="sm" class={cn('gap-(--era-gap) tabular-nums', className)}>
26
+ <Popover.Trigger size="chip" class={cn('gap-(--era-gap) tabular-nums', className)}>
27
27
  {Math.round(percent * 100)}%
28
28
  <svg viewBox="0 0 24 24" class="size-(--era-h-icon) -rotate-90" aria-hidden="true">
29
29
  <circle cx="12" cy="12" r="10" fill="none" stroke="var(--color-3)" stroke-width="3" />
@@ -16,7 +16,7 @@
16
16
  >
17
17
  <Button
18
18
  icon
19
- size="default"
19
+ size="control"
20
20
  aria-label="Scroll to bottom"
21
21
  class="rounded-full shadow-(--era-shadow-lg)"
22
22
  onclick={() => stick.repin()}
@@ -203,7 +203,7 @@
203
203
  >
204
204
  <Button
205
205
  icon
206
- size="xxs"
206
+ size="pill"
207
207
  aria-label={starred ? 'Unstar conversation' : 'Star conversation'}
208
208
  aria-pressed={starred}
209
209
  onclick={toggleStar}
@@ -212,14 +212,14 @@
212
212
  </Button>
213
213
 
214
214
  {#if showCancel}
215
- <Button icon size="xxs" aria-label="Cancel generation" onclick={() => onCancel?.()}>
215
+ <Button icon size="pill" aria-label="Cancel generation" onclick={() => onCancel?.()}>
216
216
  <X class="size-(--era-h-icon)" />
217
217
  </Button>
218
218
  {/if}
219
219
 
220
220
  <Button
221
221
  icon
222
- size="xxs"
222
+ size="pill"
223
223
  tone="destructive"
224
224
  active={armed}
225
225
  aria-label={armed ? 'Confirm delete conversation' : 'Delete conversation'}
package/dist/ai/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // era-ui take on Vercel's AI Elements (via the Svelte port). Namespaced parts
3
3
  // for the families, flat exports for the singles. `workflow/` is deliberately
4
4
  // NOT exported here: it depends on @xyflow/svelte and lives on its own
5
- // subpath (`@sig-nine/era-ui/ai/workflow`) so the dependency stays opt-in.
5
+ // subpath (`@signal9/era-ui/ai/workflow`) so the dependency stays opt-in.
6
6
  export * as Conversation from './conversation/index.js';
7
7
  export * as Message from './message/index.js';
8
8
  export * as PromptInput from './prompt-input/index.js';
@@ -12,13 +12,13 @@
12
12
 
13
13
  {#if ctrl.total > 1}
14
14
  <ButtonGroup class={cn(className)}>
15
- <Button icon size="sm" aria-label="Previous version" onclick={() => ctrl.previous()}>
15
+ <Button icon size="chip" aria-label="Previous version" onclick={() => ctrl.previous()}>
16
16
  <ChevronLeft class="size-(--era-h-icon)" />
17
17
  </Button>
18
18
  <span class="flex items-center px-(--era-inset-chip) text-body text-muted tabular-nums">
19
19
  {ctrl.current + 1} of {ctrl.total}
20
20
  </span>
21
- <Button icon size="sm" aria-label="Next version" onclick={() => ctrl.next()}>
21
+ <Button icon size="chip" aria-label="Next version" onclick={() => ctrl.next()}>
22
22
  <ChevronRight class="size-(--era-h-icon)" />
23
23
  </Button>
24
24
  </ButtonGroup>
@@ -84,7 +84,7 @@
84
84
 
85
85
  <div
86
86
  class={cn(
87
- 'flex items-center gap-(--era-inset-chip) font-mono text-body era-text-trim text-muted tabular-nums',
87
+ 'flex items-center gap-(--era-inset-chip) font-mono text-body leading-none text-muted tabular-nums',
88
88
  className
89
89
  )}
90
90
  {...restProps}
@@ -7,6 +7,6 @@
7
7
  const attachments = getAttachmentsContext();
8
8
  </script>
9
9
 
10
- <Button icon size="default" aria-label={label} onclick={() => attachments.openFileDialog()}>
10
+ <Button icon size="control" aria-label={label} onclick={() => attachments.openFileDialog()}>
11
11
  <Plus class="size-(--era-h-icon)" />
12
12
  </Button>
@@ -22,7 +22,7 @@
22
22
  <!-- md control geometry, like a Button with an icon: wall inset and
23
23
  icon gap both --era-inset-control, ink centered by era-text-trim. -->
24
24
  <div
25
- class="flex h-(--era-h-control) items-center gap-(--era-inset-control) rounded-(--era-rd-control) bg-(--era-surface-bg) px-(--era-inset-control) text-body era-text-trim text-muted shadow-(--era-shadow)"
25
+ class="flex h-(--era-h-control) items-center gap-(--era-inset-control) rounded-(--era-rd-control) bg-(--era-surface-bg) px-(--era-inset-control) text-body leading-none text-muted shadow-(--era-shadow)"
26
26
  >
27
27
  <Paperclip class="size-(--era-h-icon) shrink-0" />
28
28
  <span class="max-w-32 truncate">{a.filename}</span>
@@ -26,7 +26,7 @@
26
26
  resubmitting. -->
27
27
  <Button
28
28
  icon
29
- size="default"
29
+ size="control"
30
30
  tone={generating ? 'default' : 'accent'}
31
31
  type={generating ? 'button' : 'submit'}
32
32
  aria-label={generating ? 'Stop' : 'Send'}
@@ -91,11 +91,12 @@
91
91
  <div class="flex h-(--era-h-control) items-center gap-(--era-gap) px-(--era-inset-control)">
92
92
  <!-- leading status dot + label -->
93
93
  <span
94
- class="flex items-center gap-(--era-inset-pill) font-mono text-body {style.text} era-text-trim"
94
+ class="flex items-center gap-(--era-inset-pill) font-mono text-body {style.text} leading-none"
95
95
  >
96
96
  <span class={cn('size-1.5 shrink-0 rounded-full', style.dot, style.pulse && 'animate-pulse')}
97
97
  ></span>
98
- {style.label}
98
+ <!-- Own box, so the trim applies — see Role for the anonymous-item rule. -->
99
+ <span class="era-text-trim">{style.label}</span>
99
100
  </span>
100
101
 
101
102
  {#if tools}
@@ -115,7 +116,7 @@
115
116
  <div class="flex-1"></div>
116
117
 
117
118
  {#if style.active && oncancel}
118
- <Button size="xs" tone="destructive" onclick={() => oncancel?.()}>
119
+ <Button size="icon" tone="destructive" onclick={() => oncancel?.()}>
119
120
  <X class="size-(--era-h-icon)" aria-hidden="true" />
120
121
  Cancel
121
122
  </Button>
@@ -21,7 +21,7 @@
21
21
  vertical inset) so the words breathe. Era pills are rd-sm, not
22
22
  full-round — touch mode alone pills the ladder. -->
23
23
  <Button
24
- size="sm"
24
+ size="chip"
25
25
  class={cn('shrink-0 px-(--era-chip-px)', className)}
26
26
  onclick={() => onSelect?.(suggestion)}
27
27
  >
@@ -60,7 +60,7 @@
60
60
  {#if cancel}
61
61
  <Button
62
62
  icon
63
- size="xxs"
63
+ size="pill"
64
64
  tone="destructive"
65
65
  aria-label="Cancel {agent.task}"
66
66
  onclick={(event: MouseEvent) => {
@@ -74,7 +74,7 @@
74
74
  {#if open}
75
75
  <Button
76
76
  icon
77
- size="xxs"
77
+ size="pill"
78
78
  aria-label="Open {agent.task}"
79
79
  onclick={(event: MouseEvent) => {
80
80
  event.stopPropagation();
@@ -61,7 +61,7 @@
61
61
  )}
62
62
  {...restProps}
63
63
  >
64
- <div class="flex min-w-0 items-center gap-(--era-gap) text-body era-text-trim">
64
+ <div class="flex min-w-0 items-center gap-(--era-gap) text-body leading-none">
65
65
  <Dot status={agent.status} />
66
66
  <Role role={agent.role ?? 'worker'} label={agent.label} class="min-w-0" />
67
67
  {#if agent.key}
@@ -85,7 +85,7 @@
85
85
  {@render children?.(agent)}
86
86
 
87
87
  {#if agent.sandbox || elapsed != null}
88
- <div class="flex min-w-0 items-center gap-(--era-gap) text-body era-text-trim text-muted">
88
+ <div class="flex min-w-0 items-center gap-(--era-gap) text-body leading-none text-muted">
89
89
  {#if agent.sandbox}
90
90
  <Sandbox sandbox={agent.sandbox} label />
91
91
  {/if}
@@ -43,7 +43,7 @@
43
43
  class={cn('flex min-w-0 flex-col gap-(--era-gap) p-(--era-pad-panel)', className)}
44
44
  {...restProps}
45
45
  >
46
- <div class="flex min-w-0 items-center gap-(--era-gap) text-body era-text-trim">
46
+ <div class="flex min-w-0 items-center gap-(--era-gap) text-body leading-none">
47
47
  <span class="shrink-0 font-mono text-[0.625rem] tracking-[0.15em] text-muted uppercase">
48
48
  {artifact.kind}
49
49
  </span>
@@ -40,7 +40,7 @@
40
40
  class={cn('flex min-w-0 flex-col gap-(--era-gap)', className)}
41
41
  {...restProps}
42
42
  >
43
- <div class="flex min-w-0 items-center gap-(--era-gap) text-body era-text-trim">
43
+ <div class="flex min-w-0 items-center gap-(--era-gap) text-body leading-none">
44
44
  <GitMerge class="size-(--era-h-icon) shrink-0 text-muted" aria-hidden="true" />
45
45
  <span class="min-w-0 flex-1 truncate text-fg">{barrier.title}</span>
46
46
  {#if barrier.key}
@@ -50,14 +50,15 @@
50
50
 
51
51
  <div
52
52
  class={cn(
53
- 'flex h-(--era-h-control) shrink-0 items-center gap-(--era-gap) border-t border-divider-faded px-(--era-inset-control) font-mono text-body era-text-trim',
53
+ 'flex h-(--era-h-control) shrink-0 items-center gap-(--era-gap) border-t border-divider-faded px-(--era-inset-control) font-mono text-body leading-none',
54
54
  className
55
55
  )}
56
56
  {...restProps}
57
57
  >
58
58
  <span class={cn('flex shrink-0 items-center gap-(--era-gap)', tone.class)}>
59
59
  <Dot status={tone.status} label={tone.label} class={tone.pulse ? 'animate-pulse' : undefined} />
60
- {tone.label}
60
+ <!-- Own box, so the trim applies — see Role for the anonymous-item rule. -->
61
+ <span class="era-text-trim">{tone.label}</span>
61
62
  </span>
62
63
  <!-- Always rendered, empty when there is nothing to say: the span is also the
63
64
  spacer that pushes the watermark to the trailing edge. -->
@@ -43,7 +43,7 @@
43
43
  the end of a sentence you can hover for, never the count of what failed. -->
44
44
  <div
45
45
  class={cn(
46
- 'flex h-(--era-h-control) shrink-0 items-center gap-(--era-gap) px-(--era-inset-control) text-body era-text-trim',
46
+ 'flex h-(--era-h-control) shrink-0 items-center gap-(--era-gap) px-(--era-inset-control) text-body leading-none',
47
47
  className
48
48
  )}
49
49
  {...restProps}
@@ -160,7 +160,7 @@
160
160
  highlight instantly so a fast pass down the tree doesn't smear. -->
161
161
  <div
162
162
  class={cn(
163
- 'flex h-(--era-h-control) items-center gap-(--era-gap) rounded-(--era-rd-control) pl-(--era-inset-control) text-body era-text-trim',
163
+ 'flex h-(--era-h-control) items-center gap-(--era-gap) rounded-(--era-rd-control) pl-(--era-inset-control) text-body leading-none',
164
164
  hasActions ? 'pr-(--era-pill-inset-control)' : 'pr-(--era-inset-control)',
165
165
  (hasChildren || hasDetail) && 'cursor-pointer',
166
166
  active
@@ -53,12 +53,15 @@
53
53
  data-role={role}
54
54
  title={name}
55
55
  class={cn(
56
- 'inline-flex shrink-0 items-center gap-(--era-gap) font-mono text-[0.625rem] era-text-trim-caps tracking-[0.15em] uppercase',
56
+ 'inline-flex shrink-0 items-center gap-(--era-gap) font-mono text-[0.625rem] leading-none tracking-[0.15em] uppercase',
57
57
  role === 'orchestrator' ? 'text-fg' : 'text-muted',
58
58
  className
59
59
  )}
60
60
  {...restProps}
61
61
  >
62
62
  <Icon class="size-(--era-h-icon) shrink-0" aria-hidden="true" />
63
- {name}
63
+ <!-- The name gets its OWN box so the caps trim has something to trim. Left on
64
+ the flex row it was inert: bare text in a flex container is an anonymous
65
+ item, which no non-inherited property reaches. -->
66
+ <span class="era-text-trim-caps">{name}</span>
64
67
  </span>
@@ -47,7 +47,7 @@
47
47
  data-status={status}
48
48
  {onclick}
49
49
  class={cn(
50
- 'inline-flex h-(--era-h-pill) max-w-full min-w-0 shrink-0 era-interactive cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-pill) px-(--era-pill-px) text-body era-text-trim text-muted transition-colors duration-(--era-duration) ease-(--era-ease) hover:bg-(--era-highlight) hover:text-fg',
50
+ 'inline-flex h-(--era-h-pill) max-w-full min-w-0 shrink-0 era-interactive cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-pill) px-(--era-pill-px) text-body leading-none text-muted transition-colors duration-(--era-duration) ease-(--era-ease) hover:bg-(--era-highlight) hover:text-fg',
51
51
  active && 'bg-fill text-fg',
52
52
  className
53
53
  )}
@@ -21,8 +21,8 @@
21
21
  className
22
22
  )}
23
23
  >
24
- <Bar size="md" class="shrink-0 gap-(--era-gap) rounded-none">
25
- <Button icon size="sm" aria-label="Reload" onclick={() => (nonce += 1)}>
24
+ <Bar size="control" class="shrink-0 gap-(--era-gap) rounded-none">
25
+ <Button icon size="chip" aria-label="Reload" onclick={() => (nonce += 1)}>
26
26
  <RotateCw class="size-(--era-h-icon)" />
27
27
  </Button>
28
28
  <Input
@@ -7,11 +7,11 @@
7
7
  * makes product decisions a primitive never would, which is exactly why these
8
8
  * live behind their own subpath instead of the root barrel.
9
9
  *
10
- * Apps are NOT re-exported from `@sig-nine/era-ui`. Each carries heavy,
10
+ * Apps are NOT re-exported from `@signal9/era-ui`. Each carries heavy,
11
11
  * app-specific dependencies (Notes brings TipTap), and the same reasoning that
12
12
  * isolates `ai/workflow` on its own subpath applies here: a consumer who never
13
- * imports an app never pays for it. Import from `@sig-nine/era-ui/apps` (or a
14
- * single app, e.g. `@sig-nine/era-ui/apps/notes`) and install that app's peers.
13
+ * imports an app never pays for it. Import from `@signal9/era-ui/apps` (or a
14
+ * single app, e.g. `@signal9/era-ui/apps/notes`) and install that app's peers.
15
15
  *
16
16
  * Exports are flat and prefixed by app (`Notes`, `NotesStore`, `NoteEditor`) so
17
17
  * a second app can land without a namespace churn.
@@ -7,11 +7,11 @@
7
7
  * makes product decisions a primitive never would, which is exactly why these
8
8
  * live behind their own subpath instead of the root barrel.
9
9
  *
10
- * Apps are NOT re-exported from `@sig-nine/era-ui`. Each carries heavy,
10
+ * Apps are NOT re-exported from `@signal9/era-ui`. Each carries heavy,
11
11
  * app-specific dependencies (Notes brings TipTap), and the same reasoning that
12
12
  * isolates `ai/workflow` on its own subpath applies here: a consumer who never
13
- * imports an app never pays for it. Import from `@sig-nine/era-ui/apps` (or a
14
- * single app, e.g. `@sig-nine/era-ui/apps/notes`) and install that app's peers.
13
+ * imports an app never pays for it. Import from `@signal9/era-ui/apps` (or a
14
+ * single app, e.g. `@signal9/era-ui/apps/notes`) and install that app's peers.
15
15
  *
16
16
  * Exports are flat and prefixed by app (`Notes`, `NotesStore`, `NoteEditor`) so
17
17
  * a second app can land without a namespace churn.
@@ -31,15 +31,15 @@
31
31
  leaves no discernible edge. border-divider-faded is the token'd hairline, so
32
32
  it still tracks the surface rather than being a drawn-on line. -->
33
33
  <Bar
34
- size="md"
35
- content="xxs"
34
+ size="control"
35
+ content="pill"
36
36
  class="w-max border border-divider-faded bg-elevated shadow-lg glass-blur"
37
37
  >
38
38
  {#each items as item (item.mark)}
39
39
  {@const Icon = item.icon}
40
40
  <Button
41
41
  icon
42
- size="xxs"
42
+ size="pill"
43
43
  active={active[item.mark] ?? false}
44
44
  aria-label={item.label}
45
45
  title={item.label}
@@ -53,9 +53,7 @@
53
53
  class="scrollbar-none flex max-h-[calc(var(--era-h-control)*8)] w-max min-w-48 flex-col overflow-y-auto rounded-control bg-elevated p-menu shadow-lg glass-blur"
54
54
  >
55
55
  {#if items.length === 0}
56
- <div
57
- class="flex h-control shrink-0 items-center px-inset-control text-body era-text-trim text-muted"
58
- >
56
+ <div class="era-label-ink h-control shrink-0 px-inset-control text-body text-muted">
59
57
  No commands
60
58
  </div>
61
59
  {:else}
@@ -63,7 +61,7 @@
63
61
  {@const Icon = item.icon}
64
62
  <button
65
63
  type="button"
66
- class="flex h-control w-full shrink-0 cursor-pointer items-center gap-gutter rounded-menu-item px-inset-control text-left text-body era-text-trim text-fg {i ===
64
+ class="flex h-control w-full shrink-0 cursor-pointer items-center gap-gutter rounded-menu-item px-inset-control text-left text-body leading-none text-fg {i ===
67
65
  selected
68
66
  ? 'bg-highlight shadow-highlight'
69
67
  : ''}"
@@ -165,7 +165,7 @@
165
165
 
166
166
  {#snippet iconPicker(note: Note)}
167
167
  <Popover.Root bind:open={iconPickerOpen}>
168
- <Popover.Trigger icon size="xxs" aria-label="Change icon" title="Change icon">
168
+ <Popover.Trigger icon size="pill" aria-label="Change icon" title="Change icon">
169
169
  {#if note.icon}
170
170
  <span aria-hidden="true">{note.icon}</span>
171
171
  {:else}
@@ -179,7 +179,7 @@
179
179
  {#each row as glyph (glyph)}
180
180
  <Button
181
181
  icon
182
- size="default"
182
+ size="control"
183
183
  active={note.icon === glyph}
184
184
  aria-label={glyph}
185
185
  onclick={() => {
@@ -194,7 +194,7 @@
194
194
  {/each}
195
195
  {#if note.icon}
196
196
  <Button
197
- size="sm"
197
+ size="chip"
198
198
  class="w-full"
199
199
  onclick={() => {
200
200
  store.setIcon(note.id, null);
@@ -219,7 +219,7 @@
219
219
  {#snippet titleField(note: Note, cls: string)}
220
220
  <Input
221
221
  reveal
222
- size="xxs"
222
+ size="pill"
223
223
  class={cls}
224
224
  aria-label="Note title"
225
225
  placeholder="Untitled"
@@ -265,7 +265,7 @@
265
265
  >
266
266
  <!-- Sidebar: filter + note list -->
267
267
  <div class="flex w-56 shrink-0 flex-col border-r border-divider-faded">
268
- <!-- content="xxs": 24/30/36/48 of chrome around an 18/20/22/26 field and
268
+ <!-- content="pill": 24/30/36/48 of chrome around an 18/20/22/26 field and
269
269
  button, with Bar deriving the two-tier concentric inset. A notes sidebar
270
270
  is chrome around a list; at the lg/md pairing it was a 32px bar over
271
271
  24px rows and the chrome outweighed the content. This is the tier pair
@@ -277,17 +277,17 @@
277
277
  centred in 23px and sat 2.5px from the top against a 3px wall. Outset
278
278
  costs no layout, so the bar keeps its full tier height and every child
279
279
  sits the wall's distance from all four edges. -->
280
- <Bar size="md" content="xxs" divider class="rounded-none">
280
+ <Bar size="control" content="pill" divider class="rounded-none">
281
281
  <Input
282
282
  icon={Search}
283
- size="xxs"
283
+ size="pill"
284
284
  class="min-w-0 flex-1"
285
285
  type="text"
286
286
  placeholder="Filter…"
287
287
  aria-label="Filter notes"
288
288
  bind:value={query}
289
289
  />
290
- <Button icon size="xxs" aria-label="New note" title="New note" onclick={createNote}>
290
+ <Button icon size="pill" aria-label="New note" title="New note" onclick={createNote}>
291
291
  <Plus />
292
292
  </Button>
293
293
  </Bar>
@@ -331,7 +331,7 @@
331
331
  {#snippet child({ props })}
332
332
  <div
333
333
  {...props}
334
- class="group/row flex h-control cursor-pointer items-center gap-(--era-pill-inset-control) rounded-control px-(--era-pill-inset-control) era-text-trim {current
334
+ class="group/row flex h-control cursor-pointer items-center gap-(--era-pill-inset-control) rounded-control px-(--era-pill-inset-control) leading-none {current
335
335
  ? 'bg-hover text-bright shadow-(--era-shadow-pressed)'
336
336
  : 'hover:bg-highlight hover:shadow-highlight'}"
337
337
  role="option"
@@ -374,7 +374,7 @@
374
374
  >
375
375
  <Button
376
376
  icon
377
- size="xxs"
377
+ size="pill"
378
378
  aria-label={note.pinned ? 'Unpin note' : 'Pin note'}
379
379
  title={note.pinned ? 'Unpin note' : 'Pin note'}
380
380
  active={note.pinned}
@@ -387,7 +387,7 @@
387
387
  </Button>
388
388
  <Button
389
389
  icon
390
- size="xxs"
390
+ size="pill"
391
391
  tone="destructive"
392
392
  aria-label="Delete note"
393
393
  title="Delete note"
@@ -444,7 +444,7 @@
444
444
  header (see the snippets below) lives in the pane bar's centre and
445
445
  drawing it twice would title the document twice. -->
446
446
  {#if !pane}
447
- <Bar size="md" content="xxs" divider class="rounded-none">
447
+ <Bar size="control" content="pill" divider class="rounded-none">
448
448
  {@render iconPicker(selected)}
449
449
  {@render titleField(selected, 'min-w-0 flex-1')}
450
450
  <!-- Fixed-width so the badge appearing and clearing never nudges the
@@ -0,0 +1,2 @@
1
+ import type { Audit } from '../types.js';
2
+ export declare const anonymousText: Audit;
@@ -0,0 +1,73 @@
1
+ /*
2
+ * A flex row that centres its OWN bare text — the one place ink centring
3
+ * cannot be applied, and so the one place it silently isn't.
4
+ *
5
+ * `text-box-trim` is era's mechanism for centring INK rather than the font box:
6
+ * it collapses the line box to a typographic band so what you see is centred
7
+ * rather than what the metrics measure. It applies to a BLOCK box. It cannot
8
+ * apply to bare text inside a flex container, because that text is wrapped in
9
+ * an ANONYMOUS flex item — a box no selector reaches and no non-inherited
10
+ * property touches. `text-box-trim` is not inherited, so it never arrives.
11
+ *
12
+ * Proven rather than reasoned: the same pill rendered `flex + trim` and `flex,
13
+ * no trim` produces IDENTICAL ink, to the device pixel. Centring then falls
14
+ * back to `align-items` on the LINE BOX — precisely the font-metric-dependent
15
+ * centring the trim exists to eliminate. On the shipped mono stack that put a
16
+ * badge's caps 2px high in an 18px pill.
17
+ *
18
+ * This audit deliberately does NOT look for the `era-text-trim` class. That was
19
+ * the first version, and it was the wrong instrument: deleting an inert class
20
+ * silences it while leaving the text exactly as mis-centred as before. What
21
+ * matters is the SHAPE — a centring flex row with a text node of its own — so
22
+ * that is what it measures. It stays true after the cleanup.
23
+ *
24
+ * Two kinds of hit, and only one of them is the author's to fix:
25
+ *
26
+ * FIXABLE — the row holds nothing but text. It has no reason to be flex; as a
27
+ * block with `era-label-ink` (or `era-token-ink` for a centred token) the trim
28
+ * works and the centring stops depending on the font.
29
+ *
30
+ * STRUCTURAL — the row holds an icon AND text. The box must stay flex to lay
31
+ * the two out, which re-creates the anonymous item, and no CSS reaches it.
32
+ * Components take the `not-has-[svg]:era-label-ink` guard so the text-only
33
+ * case is centred properly and only the icon case falls back. Closing it for
34
+ * good means the label arriving in its own slot instead of inside an opaque
35
+ * `children` snippet — an API change, tracked in KNOWN_ISSUES.
36
+ */
37
+ /** Cheap pre-filter: only a row that CENTRES is relying on ink centring. */
38
+ const CENTRING = new Set(['center', 'safe center', 'anchor-center']);
39
+ export const anonymousText = {
40
+ id: 'typography/anonymous-text',
41
+ name: 'A flex row centres its own bare text',
42
+ description: 'Bare text in a flex container is an anonymous flex item, which text-box-trim cannot reach — the ink is centred by font metrics instead. Make a text-only row a block with era-label-ink; an icon+text row needs the label in its own element.',
43
+ category: 'typography',
44
+ severity: 'warn',
45
+ // Every flex box is a candidate; the checks below are the real gate and are
46
+ // far cheaper than reading styles for the whole tree.
47
+ selector: 'div,span,button,a,label,li,header,footer,summary',
48
+ check(el) {
49
+ const s = getComputedStyle(el);
50
+ if (s.display !== 'flex' && s.display !== 'inline-flex')
51
+ return null;
52
+ if (!CENTRING.has(s.alignItems))
53
+ return null;
54
+ // The anonymous item itself: a text node directly under the flex box.
55
+ const ownText = [...el.childNodes].some((n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim());
56
+ if (!ownText)
57
+ return null;
58
+ // Multi-line text is not centred by ink in the first place — this law is
59
+ // about single-line control chrome, which is what `nowrap` marks.
60
+ if (s.whiteSpace !== 'nowrap' && s.flexWrap !== 'nowrap')
61
+ return null;
62
+ const hasElementChild = el.children.length > 0;
63
+ const issue = {
64
+ auditId: 'typography/anonymous-text',
65
+ element: el,
66
+ message: hasElementChild
67
+ ? 'flex row holding an icon AND its own bare text — the text is an anonymous flex item, so its ink is centred by font metrics. The label needs its own element (or its own slot) before a trim can reach it.'
68
+ : 'flex row holding nothing but text — it has no reason to be flex. Make it a block with era-label-ink (or era-token-ink for a centred token) and the trim applies.',
69
+ details: { display: s.display, hasElementChild }
70
+ };
71
+ return issue;
72
+ }
73
+ };
@@ -4,6 +4,7 @@ import { concentricInset } from './concentric-inset.js';
4
4
  import { cornersAxis } from './corners-axis.js';
5
5
  import { evenContainerPadding } from './even-container-padding.js';
6
6
  import { flatChrome } from './flat-chrome.js';
7
+ import { anonymousText } from './anonymous-text.js';
7
8
  import { iconFlush } from './icon-flush.js';
8
9
  import { offAxisMotion } from './off-axis-motion.js';
9
10
  import { offTierHeight } from './off-tier-height.js';
@@ -14,4 +15,4 @@ import { restingGap } from './resting-gap.js';
14
15
  import { singleGlyphSquare } from './single-glyph-square.js';
15
16
  import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
16
17
  import { wallMatchesGap } from './wall-matches-gap.js';
17
- export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, iconFlush, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
18
+ export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, iconFlush, anonymousText, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
@@ -5,6 +5,7 @@ import { concentricInset } from './concentric-inset.js';
5
5
  import { cornersAxis } from './corners-axis.js';
6
6
  import { evenContainerPadding } from './even-container-padding.js';
7
7
  import { flatChrome } from './flat-chrome.js';
8
+ import { anonymousText } from './anonymous-text.js';
8
9
  import { iconFlush } from './icon-flush.js';
9
10
  import { offAxisMotion } from './off-axis-motion.js';
10
11
  import { offTierHeight } from './off-tier-height.js';
@@ -15,7 +16,7 @@ import { restingGap } from './resting-gap.js';
15
16
  import { singleGlyphSquare } from './single-glyph-square.js';
16
17
  import { uniformSiblingGaps } from './uniform-sibling-gaps.js';
17
18
  import { wallMatchesGap } from './wall-matches-gap.js';
18
- export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, iconFlush, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
19
+ export { clippedGlyphs, collapsedTextTrim, concentricInset, cornersAxis, evenContainerPadding, flatChrome, iconFlush, anonymousText, offAxisMotion, offTierHeight, pillHoldsItsInk, proseLeading, radiusConcentricity, restingGap, singleGlyphSquare, uniformSiblingGaps, wallMatchesGap };
19
20
  // Auto-register built-ins. Consumers can still unregister or add their own.
20
21
  //
21
22
  // Two of these only tell the truth on a particular axis setting, because the bug
@@ -31,6 +32,7 @@ registry.register(cornersAxis);
31
32
  registry.register(evenContainerPadding);
32
33
  registry.register(flatChrome);
33
34
  registry.register(iconFlush);
35
+ registry.register(anonymousText);
34
36
  registry.register(offAxisMotion);
35
37
  registry.register(offTierHeight);
36
38
  registry.register(pillHoldsItsInk);
@@ -1,5 +1,5 @@
1
1
  export type AuditSeverity = 'info' | 'warn' | 'error';
2
- export type AuditCategory = 'layout' | 'tokens' | 'a11y' | 'semantics';
2
+ export type AuditCategory = 'layout' | 'tokens' | 'typography' | 'a11y' | 'semantics';
3
3
  export interface Issue {
4
4
  readonly auditId: string;
5
5
  readonly element: HTMLElement;
@@ -105,7 +105,7 @@ const INTRO = [
105
105
  'with the stylesheet and needs no import:',
106
106
  '',
107
107
  '```ts',
108
- 'import "@sig-nine/era-ui/css";',
108
+ 'import "@signal9/era-ui/css";',
109
109
  '```',
110
110
  '',
111
111
  '```svelte',
@@ -10,8 +10,8 @@ function orientation(origin) {
10
10
  '## Using the library',
11
11
  '',
12
12
  '```ts',
13
- 'import { Button, Table, Select } from "@sig-nine/era-ui";',
14
- 'import "@sig-nine/era-ui/css";',
13
+ 'import { Button, Table, Select } from "@signal9/era-ui";',
14
+ 'import "@signal9/era-ui/css";',
15
15
  '```',
16
16
  '',
17
17
  'Named exports wrap a single primitive (`Button`, `Badge`, `Chip`, `Input`, …). Namespace exports compose primitives via sub-components (`Table.Root`, `Table.Row`, `Select.Trigger`, `Dialog.Content`, …). Every wrapper forwards unknown props to its underlying element or bits-ui primitive, so standard HTML attributes (`class`, `style`, event handlers, `aria-*`, `data-*`) work without ceremony.',