@signal9/era-ui 2.22.0 → 2.23.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 (50) hide show
  1. package/dist/ai/artifact/artifact-actions.svelte +3 -1
  2. package/dist/ai/artifact/artifact-content.svelte +3 -1
  3. package/dist/ai/artifact/artifact-header.svelte +5 -1
  4. package/dist/ai/artifact/artifact.svelte +5 -1
  5. package/dist/ai/chain-of-thought/chain-of-thought-step.svelte +5 -1
  6. package/dist/ai/chain-of-thought/chain-of-thought.svelte +1 -1
  7. package/dist/ai/confirmation/confirmation-args.svelte +7 -7
  8. package/dist/ai/conversation/conversation-content.svelte +4 -1
  9. package/dist/ai/conversations/conversations-group.svelte +1 -1
  10. package/dist/ai/conversations/conversations.svelte.js +5 -1
  11. package/dist/ai/error-panel/error-panel.svelte +1 -1
  12. package/dist/ai/inline-citation/inline-citation.svelte +22 -5
  13. package/dist/ai/loader.svelte +4 -1
  14. package/dist/ai/message/message-usage.svelte +4 -2
  15. package/dist/ai/open-in-chat/open-in-chat.svelte +16 -4
  16. package/dist/ai/plan/plan-header.svelte +1 -1
  17. package/dist/ai/project.js +10 -2
  18. package/dist/ai/prompt-input/prompt-input-attachments.svelte +1 -1
  19. package/dist/ai/prompt-input/prompt-input.svelte +2 -6
  20. package/dist/ai/run-status/run-status.svelte +5 -2
  21. package/dist/ai/runtime-feed/runtime-feed-item.svelte +1 -1
  22. package/dist/ai/runtime-feed/runtime-feed-root.svelte +1 -3
  23. package/dist/ai/structured-value/structured-value.svelte +8 -7
  24. package/dist/ai/suggestion/suggestions.svelte +4 -1
  25. package/dist/ai/task/task-content.svelte +4 -1
  26. package/dist/ai/tool/tool-chip.svelte +1 -1
  27. package/dist/ai/tool/tool-exec.svelte +1 -3
  28. package/dist/ai/tool/tool.svelte +1 -4
  29. package/dist/ai/web-preview/web-preview.svelte +1 -1
  30. package/dist/ai/workflow/node.svelte +3 -1
  31. package/dist/dev/axis-signature.d.ts +17 -0
  32. package/dist/dev/axis-signature.js +87 -0
  33. package/dist/dev/index.d.ts +1 -0
  34. package/dist/dev/index.js +1 -0
  35. package/dist/os/window.svelte +1 -1
  36. package/dist/styles/typography.css +9 -1
  37. package/dist/ui/button/button.svelte +10 -0
  38. package/dist/ui/card/card.svelte +3 -1
  39. package/dist/ui/command/command-input.svelte +1 -1
  40. package/dist/ui/command/command-item.svelte +1 -1
  41. package/dist/ui/command/command-link-item.svelte +1 -1
  42. package/dist/ui/command-bar/command-bar.svelte +2 -2
  43. package/dist/ui/copy-button/copy-button.svelte +3 -1
  44. package/dist/ui/input/input.svelte +1 -1
  45. package/dist/ui/kv/kv.svelte +1 -1
  46. package/dist/ui/pane/pane-close.svelte +9 -1
  47. package/dist/ui/select/select-trigger.svelte +1 -1
  48. package/dist/ui/step/step-summary.svelte +1 -1
  49. package/package.json +1 -1
  50. package/skill/SKILL.md +9 -9
@@ -4,4 +4,6 @@
4
4
  let { children, class: className }: { children?: Snippet; class?: string } = $props();
5
5
  </script>
6
6
 
7
- <div class={cn('flex shrink-0 items-center gap-(--era-gap)', className)}>{@render children?.()}</div>
7
+ <div class={cn('flex shrink-0 items-center gap-(--era-gap)', className)}>
8
+ {@render children?.()}
9
+ </div>
@@ -4,4 +4,6 @@
4
4
  let { children, class: className }: { children?: Snippet; class?: string } = $props();
5
5
  </script>
6
6
 
7
- <div class={cn('min-h-0 flex-1 overflow-auto p-(--era-pad-md)', className)}>{@render children?.()}</div>
7
+ <div class={cn('min-h-0 flex-1 overflow-auto p-(--era-pad-md)', className)}>
8
+ {@render children?.()}
9
+ </div>
@@ -2,7 +2,11 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { cn } from '../../utils/index.js';
5
- let { children, class: className, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
5
+ let {
6
+ children,
7
+ class: className,
8
+ ...restProps
9
+ }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
6
10
  </script>
7
11
 
8
12
  <div
@@ -2,7 +2,11 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { cn } from '../../utils/index.js';
5
- let { children, class: className, ...restProps }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
5
+ let {
6
+ children,
7
+ class: className,
8
+ ...restProps
9
+ }: HTMLAttributes<HTMLDivElement> & { children?: Snippet } = $props();
6
10
  </script>
7
11
 
8
12
  <div
@@ -26,7 +26,11 @@
26
26
  <Icon
27
27
  class={cn(
28
28
  'size-(--era-h-xs) shrink-0',
29
- status === 'active' ? 'text-bright' : status === 'pending' ? 'text-muted opacity-50' : 'text-muted'
29
+ status === 'active'
30
+ ? 'text-bright'
31
+ : status === 'pending'
32
+ ? 'text-muted opacity-50'
33
+ : 'text-muted'
30
34
  )}
31
35
  aria-hidden="true"
32
36
  />
@@ -15,7 +15,7 @@
15
15
  streaming?: boolean;
16
16
  label?: string;
17
17
  children?: Snippet;
18
- class?: string;
18
+ class?: string;
19
19
  } = $props();
20
20
  </script>
21
21
 
@@ -4,10 +4,8 @@
4
4
  import { Switch } from '../../ui/switch/index.js';
5
5
  import { ApprovalController, getApprovalContext } from './approval.svelte.js';
6
6
 
7
- let {
8
- controller,
9
- class: className
10
- }: { controller?: ApprovalController; class?: string } = $props();
7
+ let { controller, class: className }: { controller?: ApprovalController; class?: string } =
8
+ $props();
11
9
 
12
10
  const ctrl = $derived(controller ?? getApprovalContext());
13
11
 
@@ -27,10 +25,12 @@
27
25
  <div
28
26
  class={cn(
29
27
  'flex min-w-0 flex-col gap-(--era-gap)',
30
- ctrl.isDirty(key) && 'border-s-2 border-warning ps-(--era-pad-sm)'
28
+ ctrl.isDirty(key) && 'border-warning border-s-2 ps-(--era-pad-sm)'
31
29
  )}
32
30
  >
33
- <span class="text-[0.625rem] leading-none tracking-[0.15em] text-muted uppercase">{key}</span>
31
+ <span class="text-[0.625rem] leading-none tracking-[0.15em] text-muted uppercase"
32
+ >{key}</span
33
+ >
34
34
 
35
35
  {#if kind === 'boolean'}
36
36
  <Switch
@@ -51,7 +51,7 @@
51
51
  )}
52
52
  />
53
53
  {#if !ctrl.isValid(key)}
54
- <span class="text-body text-destructive era-text-trim">Invalid JSON</span>
54
+ <span class="text-body era-text-trim text-destructive">Invalid JSON</span>
55
55
  {/if}
56
56
  {:else}
57
57
  <Input
@@ -24,7 +24,10 @@
24
24
  >
25
25
  <div
26
26
  {@attach stick.content}
27
- class={cn('mx-auto flex w-full max-w-2xl flex-col gap-(--era-pad-lg) p-(--era-pad-md)', className)}
27
+ class={cn(
28
+ 'mx-auto flex w-full max-w-2xl flex-col gap-(--era-pad-lg) p-(--era-pad-md)',
29
+ className
30
+ )}
28
31
  {...restProps}
29
32
  >
30
33
  {@render children?.()}
@@ -19,7 +19,7 @@
19
19
  <div class={cn('flex min-w-0 flex-col gap-(--era-gap)', className)} {...restProps}>
20
20
  {#if label}
21
21
  <span
22
- class="px-(--era-inset-md) text-[0.625rem] leading-none tracking-[0.15em] text-muted uppercase era-text-trim-caps"
22
+ class="px-(--era-inset-md) text-[0.625rem] era-text-trim-caps leading-none tracking-[0.15em] text-muted uppercase"
23
23
  >
24
24
  {label}
25
25
  </span>
@@ -46,7 +46,11 @@ export function bucketConversations(items, now) {
46
46
  const newestFirst = (a, b) => b.updatedAt - a.updatedAt;
47
47
  const groups = [];
48
48
  if (starred.length > 0) {
49
- groups.push({ bucket: 'starred', label: BUCKET_LABELS.starred, items: starred.sort(newestFirst) });
49
+ groups.push({
50
+ bucket: 'starred',
51
+ label: BUCKET_LABELS.starred,
52
+ items: starred.sort(newestFirst)
53
+ });
50
54
  }
51
55
  for (const bucket of AGE_ORDER) {
52
56
  if (byAge[bucket].length > 0) {
@@ -73,7 +73,7 @@
73
73
  {#if showDetail}
74
74
  <!-- The whole summary row toggles the raw detail; quiet until hovered. -->
75
75
  <Collapsible.Trigger
76
- class="flex w-full min-w-0 cursor-pointer items-center gap-(--era-gap) text-body text-muted era-interactive"
76
+ class="flex w-full min-w-0 era-interactive cursor-pointer items-center gap-(--era-gap) text-body text-muted"
77
77
  >
78
78
  {@render head()}
79
79
  <ChevronDown
@@ -36,28 +36,45 @@
36
36
  target="_blank"
37
37
  rel="noreferrer"
38
38
  >
39
- <Badge class="ml-1">{host(sources[0]?.href ?? '')}{sources.length > 1 ? ` +${sources.length - 1}` : ''}</Badge>
39
+ <Badge class="ml-1"
40
+ >{host(sources[0]?.href ?? '')}{sources.length > 1 ? ` +${sources.length - 1}` : ''}</Badge
41
+ >
40
42
  </LinkPreview.Trigger>
41
43
  <LinkPreview.Content class="flex w-72 flex-col gap-(--era-gap)">
42
44
  {#if sources.length > 1}
43
45
  <div class="flex items-center justify-between">
44
46
  <span class="text-body text-muted tabular-nums">{index + 1} of {sources.length}</span>
45
47
  <div class="flex gap-(--era-gap)">
46
- <Button icon size="icon" aria-label="Previous source" onclick={() => (index = (index - 1 + sources.length) % sources.length)}>
48
+ <Button
49
+ icon
50
+ size="icon"
51
+ aria-label="Previous source"
52
+ onclick={() => (index = (index - 1 + sources.length) % sources.length)}
53
+ >
47
54
  <ChevronLeft class="size-(--era-h-xs)" />
48
55
  </Button>
49
- <Button icon size="icon" aria-label="Next source" onclick={() => (index = (index + 1) % sources.length)}>
56
+ <Button
57
+ icon
58
+ size="icon"
59
+ aria-label="Next source"
60
+ onclick={() => (index = (index + 1) % sources.length)}
61
+ >
50
62
  <ChevronRight class="size-(--era-h-xs)" />
51
63
  </Button>
52
64
  </div>
53
65
  </div>
54
66
  {/if}
55
67
  {#if active}
56
- <a href={active.href} target="_blank" rel="noreferrer" class="truncate text-body text-primary hover:underline">
68
+ <a
69
+ href={active.href}
70
+ target="_blank"
71
+ rel="noreferrer"
72
+ class="truncate text-body text-primary hover:underline"
73
+ >
57
74
  {active.title ?? active.href}
58
75
  </a>
59
76
  {#if active.quote}
60
- <blockquote class="border-s-2 border-divider-faded ps-(--era-gap) text-body text-muted">
77
+ <blockquote class="border-divider-faded border-s-2 ps-(--era-gap) text-body text-muted">
61
78
  {active.quote}
62
79
  </blockquote>
63
80
  {/if}
@@ -5,4 +5,7 @@
5
5
  let { class: className }: { class?: string } = $props();
6
6
  </script>
7
7
 
8
- <LoaderCircle class={cn('size-(--era-h-xs) animate-spin text-muted', className)} aria-label="Loading" />
8
+ <LoaderCircle
9
+ class={cn('size-(--era-h-xs) animate-spin text-muted', className)}
10
+ aria-label="Loading"
11
+ />
@@ -84,7 +84,7 @@
84
84
 
85
85
  <div
86
86
  class={cn(
87
- 'flex items-center gap-(--era-inset-sm) font-mono text-body tabular-nums text-muted era-text-trim',
87
+ 'flex items-center gap-(--era-inset-sm) font-mono text-body era-text-trim text-muted tabular-nums',
88
88
  className
89
89
  )}
90
90
  {...restProps}
@@ -109,7 +109,9 @@
109
109
  class="h-auto flex-col items-start gap-(--era-inset-xxs) py-(--era-inset-xxs) whitespace-normal"
110
110
  >
111
111
  {#each rows as [label, value] (label)}
112
- <span class="flex w-full items-center justify-between gap-(--era-inset-md) tabular-nums">
112
+ <span
113
+ class="flex w-full items-center justify-between gap-(--era-inset-md) tabular-nums"
114
+ >
113
115
  <span class="text-muted">{label}</span>
114
116
  <span>{formatTokens(value)}</span>
115
117
  </span>
@@ -1,10 +1,22 @@
1
1
  <script lang="ts" module>
2
2
  /** Deep-link builders for "continue this prompt elsewhere". */
3
3
  export const providers = {
4
- chatgpt: { title: 'ChatGPT', url: (q: string) => `https://chatgpt.com/?${new URLSearchParams({ q })}` },
5
- claude: { title: 'Claude', url: (q: string) => `https://claude.ai/new?${new URLSearchParams({ q })}` },
6
- t3: { title: 'T3 Chat', url: (q: string) => `https://t3.chat/new?${new URLSearchParams({ q })}` },
7
- scira: { title: 'Scira', url: (q: string) => `https://scira.ai/?${new URLSearchParams({ q })}` },
4
+ chatgpt: {
5
+ title: 'ChatGPT',
6
+ url: (q: string) => `https://chatgpt.com/?${new URLSearchParams({ q })}`
7
+ },
8
+ claude: {
9
+ title: 'Claude',
10
+ url: (q: string) => `https://claude.ai/new?${new URLSearchParams({ q })}`
11
+ },
12
+ t3: {
13
+ title: 'T3 Chat',
14
+ url: (q: string) => `https://t3.chat/new?${new URLSearchParams({ q })}`
15
+ },
16
+ scira: {
17
+ title: 'Scira',
18
+ url: (q: string) => `https://scira.ai/?${new URLSearchParams({ q })}`
19
+ },
8
20
  v0: { title: 'v0', url: (q: string) => `https://v0.dev/chat?${new URLSearchParams({ q })}` }
9
21
  } as const;
10
22
  export type ProviderId = keyof typeof providers;
@@ -13,7 +13,7 @@
13
13
 
14
14
  <Collapsible.Trigger
15
15
  class={cn(
16
- 'flex h-(--era-h-md) w-full cursor-pointer era-interactive items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body text-bright hover:bg-(--era-highlight)',
16
+ 'flex h-(--era-h-md) w-full era-interactive cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body text-bright hover:bg-(--era-highlight)',
17
17
  className
18
18
  )}
19
19
  >
@@ -29,7 +29,11 @@ export function projectMessages(messages) {
29
29
  const flushRow = () => {
30
30
  if (row.length === 0)
31
31
  return;
32
- blocks.push({ type: 'tool-row', key: `${message.id}:tools:${row[0].toolCallId}`, tools: row });
32
+ blocks.push({
33
+ type: 'tool-row',
34
+ key: `${message.id}:tools:${row[0].toolCallId}`,
35
+ tools: row
36
+ });
33
37
  row = [];
34
38
  };
35
39
  for (const [i, part] of message.parts.entries()) {
@@ -42,7 +46,11 @@ export function projectMessages(messages) {
42
46
  case 'reasoning':
43
47
  flushRow();
44
48
  if (part.text)
45
- blocks.push({ type: 'reasoning', key: `${message.id}:reasoning:${i}`, text: part.text });
49
+ blocks.push({
50
+ type: 'reasoning',
51
+ key: `${message.id}:reasoning:${i}`,
52
+ text: part.text
53
+ });
46
54
  break;
47
55
  case 'tool-call': {
48
56
  if (seenTools.has(part.toolCallId))
@@ -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-md, ink centered by era-text-trim. -->
24
24
  <div
25
- class="flex h-(--era-h-md) items-center gap-(--era-inset-md) rounded-(--era-rd-md) bg-(--era-surface-bg) px-(--era-inset-md) text-body text-muted shadow-(--era-shadow) era-text-trim"
25
+ class="flex h-(--era-h-md) items-center gap-(--era-inset-md) rounded-(--era-rd-md) bg-(--era-surface-bg) px-(--era-inset-md) text-body era-text-trim text-muted shadow-(--era-shadow)"
26
26
  >
27
27
  <Paperclip class="size-(--era-h-xs) shrink-0" />
28
28
  <span class="max-w-32 truncate">{a.filename}</span>
@@ -2,11 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLFormAttributes } from 'svelte/elements';
4
4
  import { cn } from '../../utils/index.js';
5
- import {
6
- Attachments,
7
- setAttachmentsContext,
8
- type PromptMessage
9
- } from './attachments.svelte.js';
5
+ import { Attachments, setAttachmentsContext, type PromptMessage } from './attachments.svelte.js';
10
6
 
11
7
  let {
12
8
  accept = '',
@@ -88,7 +84,7 @@
88
84
  // base fill while a button inside is hovered, so the highlight lands on the
89
85
  // button, not the bar. (General rule for any highlighting container with
90
86
  // interactive children.)
91
- 'flex w-full flex-col era-interactive rounded-(--era-rd-lg) bg-(--era-surface-bg-elevated) p-(--era-gap) text-body text-fg shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) has-[button:hover]:bg-(--era-surface-bg-elevated)',
87
+ 'flex w-full era-interactive flex-col rounded-(--era-rd-lg) bg-(--era-surface-bg-elevated) p-(--era-gap) text-body text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight) has-[button:hover]:bg-(--era-surface-bg-elevated)',
92
88
  className
93
89
  )}
94
90
  {...restProps}
@@ -90,8 +90,11 @@
90
90
  >
91
91
  <div class="flex h-(--era-h-md) items-center gap-(--era-gap) px-(--era-inset-md)">
92
92
  <!-- leading status dot + label -->
93
- <span class="flex items-center gap-(--era-inset-xxs) font-mono text-body {style.text} era-text-trim">
94
- <span class={cn('size-1.5 shrink-0 rounded-full', style.dot, style.pulse && 'animate-pulse')}></span>
93
+ <span
94
+ class="flex items-center gap-(--era-inset-xxs) font-mono text-body {style.text} era-text-trim"
95
+ >
96
+ <span class={cn('size-1.5 shrink-0 rounded-full', style.dot, style.pulse && 'animate-pulse')}
97
+ ></span>
95
98
  {style.label}
96
99
  </span>
97
100
 
@@ -59,6 +59,6 @@
59
59
  {#if children}{@render children()}{:else}{text}{/if}
60
60
  </span>
61
61
  {#if label}
62
- <span class="shrink-0 text-muted tabular-nums era-text-trim">{label}</span>
62
+ <span class="shrink-0 era-text-trim text-muted tabular-nums">{label}</span>
63
63
  {/if}
64
64
  </div>
@@ -19,9 +19,7 @@
19
19
  } = $props();
20
20
 
21
21
  // The running-tail feel: keep only the last `limit` lines when capped.
22
- const visible = $derived(
23
- items && limit !== undefined ? items.slice(-limit) : (items ?? [])
24
- );
22
+ const visible = $derived(items && limit !== undefined ? items.slice(-limit) : (items ?? []));
25
23
  </script>
26
24
 
27
25
  <div class={cn('flex min-w-0 flex-col gap-(--era-gap)', className)} {...restProps}>
@@ -10,7 +10,9 @@
10
10
 
11
11
  function isPrimitive(input: unknown): input is Primitive {
12
12
  return (
13
- input === null || input === undefined || ['string', 'number', 'boolean'].includes(typeof input)
13
+ input === null ||
14
+ input === undefined ||
15
+ ['string', 'number', 'boolean'].includes(typeof input)
14
16
  );
15
17
  }
16
18
 
@@ -58,12 +60,11 @@
58
60
 
59
61
  {#snippet tag(kind: 'key' | 'index', label: string)}
60
62
  {#if kind === 'index'}
61
- <span
62
- class="shrink-0 text-[0.625rem] leading-none tracking-[0.15em] text-muted uppercase"
63
+ <span class="shrink-0 text-[0.625rem] leading-none tracking-[0.15em] text-muted uppercase"
63
64
  >{label}</span
64
65
  >
65
66
  {:else}
66
- <span class="shrink-0 text-muted era-text-trim">{label}:</span>
67
+ <span class="shrink-0 era-text-trim text-muted">{label}:</span>
67
68
  {/if}
68
69
  {/snippet}
69
70
 
@@ -105,10 +106,10 @@
105
106
 
106
107
  <div class={cn('flex min-w-0 flex-col gap-(--era-gap) text-body', className)} {...restProps}>
107
108
  {#if depth > MAX_DEPTH}
108
- <span class="text-muted era-text-trim">…</span>
109
+ <span class="era-text-trim text-muted">…</span>
109
110
  {:else if isRecord(value)}
110
111
  {#if Object.keys(value).length === 0}
111
- <span class="text-muted era-text-trim">empty</span>
112
+ <span class="era-text-trim text-muted">empty</span>
112
113
  {:else}
113
114
  {#each Object.entries(value) as [key, entry] (key)}
114
115
  {@render row('key', key, entry)}
@@ -116,7 +117,7 @@
116
117
  {/if}
117
118
  {:else if Array.isArray(value)}
118
119
  {#if value.length === 0}
119
- <span class="text-muted era-text-trim">empty</span>
120
+ <span class="era-text-trim text-muted">empty</span>
120
121
  {:else if isSimpleArray(value)}
121
122
  {@render leaf(value)}
122
123
  {:else}
@@ -11,7 +11,10 @@
11
11
  </script>
12
12
 
13
13
  <div
14
- class={cn('scrollbar-none flex w-full flex-nowrap items-center gap-(--era-gap) overflow-x-auto', className)}
14
+ class={cn(
15
+ 'scrollbar-none flex w-full flex-nowrap items-center gap-(--era-gap) overflow-x-auto',
16
+ className
17
+ )}
15
18
  {...restProps}
16
19
  >
17
20
  {@render children?.()}
@@ -8,7 +8,10 @@
8
8
 
9
9
  <!-- The task log rail: items hang off a left border, terminal-trace style. -->
10
10
  <Step.Content
11
- class={cn('border-s-2 border-divider-faded bg-transparent p-0 ps-(--era-pad-sm) shadow-none', className)}
11
+ class={cn(
12
+ 'border-divider-faded border-s-2 bg-transparent p-0 ps-(--era-pad-sm) shadow-none',
13
+ className
14
+ )}
12
15
  >
13
16
  {@render children?.()}
14
17
  </Step.Content>
@@ -47,7 +47,7 @@
47
47
  data-status={status}
48
48
  {onclick}
49
49
  class={cn(
50
- 'inline-flex h-(--era-h-xxs) min-w-0 max-w-full shrink-0 cursor-pointer era-interactive items-center gap-(--era-gap) rounded-(--era-rd-xxs) px-(--era-pill-xxs) text-body text-muted transition-colors duration-(--era-duration) ease-(--era-ease) hover:bg-(--era-highlight) hover:text-fg era-text-trim',
50
+ 'inline-flex h-(--era-h-xxs) max-w-full min-w-0 shrink-0 era-interactive cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-xxs) px-(--era-pill-xxs) text-body era-text-trim text-muted transition-colors duration-(--era-duration) ease-(--era-ease) hover:bg-(--era-highlight) hover:text-fg',
51
51
  active && 'bg-hover text-fg',
52
52
  className
53
53
  )}
@@ -23,9 +23,7 @@
23
23
  {#if command || failed}
24
24
  <div class="flex min-w-0 items-center gap-(--era-gap)">
25
25
  {#if command}
26
- <code class="min-w-0 flex-1 truncate font-mono text-body text-muted"
27
- >{command}</code
28
- >
26
+ <code class="min-w-0 flex-1 truncate font-mono text-body text-muted">{command}</code>
29
27
  {/if}
30
28
  {#if failed}
31
29
  <Badge tone="destructive" class="ml-auto shrink-0">exit {exitCode}</Badge>
@@ -1,10 +1,7 @@
1
1
  <script lang="ts" module>
2
2
  /** AI-SDK tool part lifecycle. */
3
3
  export type ToolState =
4
- | 'input-streaming'
5
- | 'input-available'
6
- | 'output-available'
7
- | 'output-error';
4
+ 'input-streaming' | 'input-available' | 'output-available' | 'output-error';
8
5
  </script>
9
6
 
10
7
  <script lang="ts">
@@ -21,7 +21,7 @@
21
21
  className
22
22
  )}
23
23
  >
24
- <Bar size="md" class="shrink-0 rounded-none gap-(--era-gap)">
24
+ <Bar size="md" class="shrink-0 gap-(--era-gap) rounded-none">
25
25
  <Button icon size="sm" aria-label="Reload" onclick={() => (nonce += 1)}>
26
26
  <RotateCw class="size-(--era-h-xs)" />
27
27
  </Button>
@@ -18,7 +18,9 @@
18
18
  >
19
19
  <Handle type="target" position={Position.Left} />
20
20
  {#if title}
21
- <div class="flex h-(--era-h-md) items-center border-b border-divider-faded px-(--era-inset-md) font-medium text-bright">
21
+ <div
22
+ class="flex h-(--era-h-md) items-center border-b border-divider-faded px-(--era-inset-md) font-medium text-bright"
23
+ >
22
24
  {title}
23
25
  </div>
24
26
  {/if}
@@ -0,0 +1,17 @@
1
+ /** What one component looks like, on the axes, at one setting. */
2
+ export interface AxisSignature {
3
+ slug: string;
4
+ /** Unique non-zero transition/animation durations in the subtree, in ms. */
5
+ durations: number[];
6
+ /** Names of running keyframe animations (a spinner is allowed to keep spinning). */
7
+ animations: string[];
8
+ /** Unique painted box-shadows — the surface system's whole output. */
9
+ shadows: string[];
10
+ /** The font actually resolved for the component's text. */
11
+ fontFamily: string;
12
+ /** Elements whose text is being cut off — the font-swap failure mode. */
13
+ clipped: string[];
14
+ /** Interactive controls, for the hover-state sweep the harness drives. */
15
+ controls: number;
16
+ }
17
+ export declare function captureSignatures(root?: ParentNode): AxisSignature[];
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Axis signatures — the fingerprint a component leaves at one axis setting.
3
+ *
4
+ * Every audit in ./audit answers a question about a SINGLE render: is this gap
5
+ * even, is this radius concentric, does this panel have a shadow. But the most
6
+ * important promises era makes are about the axes themselves, and they are
7
+ * comparative:
8
+ *
9
+ * MOTION does this component actually respond to instant / normal / extra —
10
+ * freezing at one, moving at the next, and moving MORE at the third?
11
+ * SURFACE does its chrome actually change between flat / glass / bevel /
12
+ * cartoon, or does it render the same rectangle on all four?
13
+ * FONT does it survive mono / sans / serif without clipping its own text?
14
+ *
15
+ * None of those can be answered by looking at one DOM. A component that hardcodes
16
+ * `duration-150` looks perfectly correct at data-motion="normal" — it is only
17
+ * wrong RELATIVE to the other two levels, where it fails to change. So the check
18
+ * has to be: render at each level, fingerprint what should have moved, and diff.
19
+ *
20
+ * This module captures the fingerprint. The comparison lives in the harness that
21
+ * drives the renders (tests/visual/axis-conformance.spec.ts), because only it can
22
+ * visit more than one axis at a time.
23
+ */
24
+ const num = (v) => v
25
+ .split(',')
26
+ .map((s) => s.trim())
27
+ .filter(Boolean)
28
+ .map((s) => (s.endsWith('ms') ? parseFloat(s) : parseFloat(s) * 1000))
29
+ .filter((n) => !Number.isNaN(n) && n > 0);
30
+ const uniq = (xs) => [...new Set(xs)];
31
+ /**
32
+ * Is this element's text being cut off by its own box?
33
+ *
34
+ * The font axis is the one that breaks silently: a serif or sans face is WIDER
35
+ * than the mono default at the same size, so a label that fits in mono can
36
+ * overflow its control in serif — and because the control clips, the text simply
37
+ * disappears rather than reflowing. Nothing in a single-font render can predict
38
+ * that; you have to swap the font and look.
39
+ */
40
+ function isClipped(el) {
41
+ const s = getComputedStyle(el);
42
+ if (!/hidden|clip/.test(s.overflowX))
43
+ return false;
44
+ if (!el.textContent?.trim())
45
+ return false;
46
+ // A visually-hidden element (sr-only) is a 1px box with its overflow clipped —
47
+ // that IS the technique, not a defect. It reports a wildly overflowing
48
+ // scrollWidth by construction (the calendar's screen-reader heading measured
49
+ // 144px of text in a 1px box), so exclude the degenerate boxes.
50
+ if (el.clientWidth <= 1 || el.clientHeight <= 1)
51
+ return false;
52
+ // 1px of slack: sub-pixel text metrics routinely round a hair over.
53
+ return el.scrollWidth > el.clientWidth + 1;
54
+ }
55
+ export function captureSignatures(root = document) {
56
+ const out = [];
57
+ for (const host of root.querySelectorAll('[data-specimen]')) {
58
+ const slug = host.getAttribute('data-specimen') ?? '';
59
+ const durations = [];
60
+ const animations = [];
61
+ const shadows = [];
62
+ const clipped = [];
63
+ let controls = 0;
64
+ for (const el of host.querySelectorAll('*')) {
65
+ const s = getComputedStyle(el);
66
+ durations.push(...num(s.transitionDuration), ...num(s.animationDuration));
67
+ if (s.animationName && s.animationName !== 'none')
68
+ animations.push(s.animationName);
69
+ if (s.boxShadow && s.boxShadow !== 'none')
70
+ shadows.push(s.boxShadow);
71
+ if (isClipped(el))
72
+ clipped.push(`${el.tagName.toLowerCase()}"${el.textContent?.trim().slice(0, 24)}"`);
73
+ if (el.matches('button, input, [role="switch"], [role="checkbox"], a[href]'))
74
+ controls++;
75
+ }
76
+ out.push({
77
+ slug,
78
+ durations: uniq(durations).sort((a, b) => a - b),
79
+ animations: uniq(animations).sort(),
80
+ shadows: uniq(shadows).sort(),
81
+ fontFamily: getComputedStyle(host).fontFamily,
82
+ clipped: uniq(clipped),
83
+ controls
84
+ });
85
+ }
86
+ return out;
87
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './audit/index.js';
2
2
  export * from './measure.js';
3
+ export * from './axis-signature.js';
3
4
  export { default as AuditOverlay } from './audit-overlay.svelte';
package/dist/dev/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from './audit/index.js';
2
2
  export * from './measure.js';
3
+ export * from './axis-signature.js';
3
4
  export { default as AuditOverlay } from './audit-overlay.svelte';
@@ -101,7 +101,7 @@
101
101
  // mouse events, so the browser's autoscroll never engages.
102
102
  if (e.button === 1) e.preventDefault();
103
103
  armed =
104
- e.button === 2 && win.minimizable ? 'minimize' : e.button === 1 ? 'maximize' : null;
104
+ e.button === 2 && win.minimizable ? 'minimize' : e.button === 1 ? 'maximize' : null;
105
105
  }}
106
106
  onpointerup={(e: PointerEvent) => {
107
107
  if (e.button === 2 && armed === 'minimize') wm.minimize(win.id);
@@ -14,7 +14,15 @@
14
14
 
15
15
  [data-font='mono'],
16
16
  [data-font='mono'] * {
17
- font-family: var(--font-mono, ui-monospace, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace) !important;
17
+ font-family: var(
18
+ --font-mono,
19
+ ui-monospace,
20
+ 'SF Mono',
21
+ Menlo,
22
+ Consolas,
23
+ 'Liberation Mono',
24
+ monospace
25
+ ) !important;
18
26
  }
19
27
 
20
28
  [data-font='sans'],
@@ -38,10 +38,20 @@
38
38
  const effectiveDisabled = $derived(disabled || loading);
39
39
  </script>
40
40
 
41
+ <!--
42
+ `active` is the latched state — a held-down key. Every OTHER latched control in
43
+ the library (Toggle, Tabs, ToggleGroup, Switch) announces that with
44
+ data-state="on"; Button expressed it through classes alone, so nothing could
45
+ SEE it: not a consumer styling off the state, not a test, not an audit. The
46
+ hover sweep is what surfaced this — a latched button correctly shows no hover
47
+ delta (it must keep its pressed face), but with no attribute to read, that is
48
+ indistinguishable from a control with no hover state at all.
49
+ -->
41
50
  <Button.Root
42
51
  bind:ref
43
52
  disabled={effectiveDisabled}
44
53
  aria-busy={loading || undefined}
54
+ data-state={active ? 'on' : undefined}
45
55
  class={cn(buttonVariants({ variant, tone, size, icon, active }), className)}
46
56
  {...restProps}
47
57
  >
@@ -93,7 +93,9 @@
93
93
  {#snippet body()}
94
94
  {#if sectioned}
95
95
  {#if header}
96
- <div class="border-b border-divider-faded px-(--era-pad-md) pt-(--era-gap) pb-(--era-pad-md) text-bright">
96
+ <div
97
+ class="border-b border-divider-faded px-(--era-pad-md) pt-(--era-gap) pb-(--era-pad-md) text-bright"
98
+ >
97
99
  {@render header()}
98
100
  </div>
99
101
  {/if}
@@ -20,7 +20,7 @@
20
20
  {@const Icon = icon}
21
21
  <div
22
22
  class={cn(
23
- 'flex h-(--era-h-md) w-full era-interactive items-center gap-(--era-gap) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-inset-md) text-body text-fg shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) era-text-trim',
23
+ 'flex h-(--era-h-md) w-full era-interactive items-center gap-(--era-gap) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-inset-md) text-body era-text-trim text-fg shadow-(--era-shadow-well) focus-within:bg-(--era-highlight) hover:bg-(--era-highlight)',
24
24
  className
25
25
  )}
26
26
  >
@@ -8,7 +8,7 @@
8
8
  <Command.Item
9
9
  bind:ref
10
10
  class={cn(
11
- 'flex h-(--era-h-md) cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body text-fg outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[selected]:bg-hover era-text-trim',
11
+ 'flex h-(--era-h-md) cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body era-text-trim text-fg outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[selected]:bg-hover',
12
12
  className
13
13
  )}
14
14
  {...restProps}
@@ -8,7 +8,7 @@
8
8
  <Command.LinkItem
9
9
  bind:ref
10
10
  class={cn(
11
- 'flex h-(--era-h-md) cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body text-fg no-underline outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[selected]:bg-hover era-text-trim',
11
+ 'flex h-(--era-h-md) cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body era-text-trim text-fg no-underline outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[selected]:bg-hover',
12
12
  className
13
13
  )}
14
14
  {...restProps}
@@ -214,7 +214,7 @@
214
214
  role="listbox"
215
215
  tabindex="-1"
216
216
  aria-label="Commands"
217
- class="glass-blur absolute top-full left-0 z-50 max-h-64 w-full overflow-x-hidden overflow-y-auto bg-(--era-surface-bg-elevated) shadow-(--era-shadow-lg)"
217
+ class="absolute top-full left-0 z-50 max-h-64 w-full overflow-x-hidden overflow-y-auto bg-(--era-surface-bg-elevated) shadow-(--era-shadow-lg) glass-blur"
218
218
  onmousedown={(e) => e.preventDefault()}
219
219
  >
220
220
  {#if !filtered.length}
@@ -234,7 +234,7 @@
234
234
  tabindex={-1}
235
235
  aria-selected={i === selectedIndex}
236
236
  class={cn(
237
- 'flex h-(--era-h-md) cursor-default era-text-trim items-center gap-(--era-gap) px-(--era-inset-md) text-body whitespace-nowrap',
237
+ 'flex h-(--era-h-md) cursor-default items-center gap-(--era-gap) px-(--era-inset-md) text-body era-text-trim whitespace-nowrap',
238
238
  i === selectedIndex ? 'bg-3 text-bright' : 'text-fg'
239
239
  )}
240
240
  onclick={() => choose(item)}
@@ -92,6 +92,8 @@
92
92
  <span class="relative block size-(--era-h-xs) shrink-0">
93
93
  <IdleIcon class={cn(iconBase, status === 'idle' ? iconIn : iconOut)} />
94
94
  <SuccessIcon class={cn(iconBase, 'text-success', status === 'success' ? iconIn : iconOut)} />
95
- <FailureIcon class={cn(iconBase, 'text-destructive', status === 'failure' ? iconIn : iconOut)} />
95
+ <FailureIcon
96
+ class={cn(iconBase, 'text-destructive', status === 'failure' ? iconIn : iconOut)}
97
+ />
96
98
  </span>
97
99
  </Button>
@@ -16,7 +16,7 @@
16
16
  bind:this={ref}
17
17
  bind:value
18
18
  class={cn(
19
- 'flex h-(--era-h-md) w-full era-interactive rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-inset-md) text-body text-fg shadow-(--era-shadow-well) placeholder:text-muted hover:bg-(--era-highlight) focus:bg-(--era-highlight) era-text-trim',
19
+ 'flex h-(--era-h-md) w-full era-interactive rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-inset-md) text-body era-text-trim text-fg shadow-(--era-shadow-well) placeholder:text-muted hover:bg-(--era-highlight) focus:bg-(--era-highlight)',
20
20
  className
21
21
  )}
22
22
  {...restProps}
@@ -21,7 +21,7 @@
21
21
  <span
22
22
  bind:this={ref}
23
23
  class={cn(
24
- 'inline-flex h-(--era-h-xxs) items-center overflow-hidden rounded-(--era-rd-xxs) bg-hover font-mono text-body leading-none shadow-(--era-shadow) era-text-trim',
24
+ 'inline-flex h-(--era-h-xxs) items-center overflow-hidden rounded-(--era-rd-xxs) bg-hover font-mono text-body era-text-trim leading-none shadow-(--era-shadow)',
25
25
  className
26
26
  )}
27
27
  {...restProps}
@@ -18,6 +18,14 @@
18
18
  movement and suppresses the trailing click after a real drag). Its natural
19
19
  rd-xs is what the PANE's corner radius conforms to (rd-sm — see Pane.Root),
20
20
  not the other way round. -->
21
- <Button bind:ref icon size="icon" tone="destructive" aria-label="Close" class={className} {...restProps}>
21
+ <Button
22
+ bind:ref
23
+ icon
24
+ size="icon"
25
+ tone="destructive"
26
+ aria-label="Close"
27
+ class={className}
28
+ {...restProps}
29
+ >
22
30
  <X class="size-(--era-h-xs)" />
23
31
  </Button>
@@ -14,7 +14,7 @@
14
14
  <Select.Trigger
15
15
  bind:ref
16
16
  class={cn(
17
- 'flex h-(--era-h-md) w-full items-center justify-between gap-(--era-xs-inset-sm) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-inset-md) text-body text-fg shadow-(--era-shadow) hover:bg-(--era-highlight) focus:bg-(--era-highlight) focus:outline-none disabled:opacity-50 data-[state=open]:rounded-b-none data-[state=open]:shadow-(--era-shadow-pressed) era-text-trim',
17
+ 'flex h-(--era-h-md) w-full items-center justify-between gap-(--era-xs-inset-sm) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-inset-md) text-body era-text-trim text-fg shadow-(--era-shadow) hover:bg-(--era-highlight) focus:bg-(--era-highlight) focus:outline-none disabled:opacity-50 data-[state=open]:rounded-b-none data-[state=open]:shadow-(--era-shadow-pressed)',
18
18
  className
19
19
  )}
20
20
  {...restProps}
@@ -26,7 +26,7 @@
26
26
  <Collapsible.Trigger
27
27
  bind:ref
28
28
  class={cn(
29
- 'flex h-(--era-h-md) w-full min-w-0 cursor-pointer era-interactive items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body text-muted transition-colors duration-(--era-duration) ease-(--era-ease) hover:text-fg era-text-trim',
29
+ 'flex h-(--era-h-md) w-full min-w-0 era-interactive cursor-pointer items-center gap-(--era-gap) rounded-(--era-rd-md) px-(--era-inset-md) text-body era-text-trim text-muted transition-colors duration-(--era-duration) ease-(--era-ease) hover:text-fg',
30
30
  className
31
31
  )}
32
32
  {...restProps}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signal9/era-ui",
3
- "version": "2.22.0",
3
+ "version": "2.23.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev --host",
6
6
  "build": "vite build && npm run prepack",
package/skill/SKILL.md CHANGED
@@ -34,13 +34,13 @@ to ~13px, silently shrinking every rem-derived token).
34
34
 
35
35
  ## The tier ladder
36
36
 
37
- | Tier | Height token | Radius token | Use for |
38
- | ----- | -------------- | --------------- | ------------------------------------ |
39
- | `xs` | `--era-h-xs` | `--era-rd-xs` | icons (THE icon size), switch thumbs |
40
- | `xxs` | `--era-h-xxs` | `--era-rd-xxs` | pills nested inside md (badge/chip in a button) |
41
- | `sm` | `--era-h-sm` | `--era-rd-sm` | standalone pills, switch tracks, KV |
42
- | `md` | `--era-h-md` | `--era-rd-md` | buttons, inputs, selects, triggers |
43
- | `lg` | `--era-h-lg` | `--era-rd-lg` | bars, containers, dialog headers |
37
+ | Tier | Height token | Radius token | Use for |
38
+ | ----- | ------------- | -------------- | ----------------------------------------------- |
39
+ | `xs` | `--era-h-xs` | `--era-rd-xs` | icons (THE icon size), switch thumbs |
40
+ | `xxs` | `--era-h-xxs` | `--era-rd-xxs` | pills nested inside md (badge/chip in a button) |
41
+ | `sm` | `--era-h-sm` | `--era-rd-sm` | standalone pills, switch tracks, KV |
42
+ | `md` | `--era-h-md` | `--era-rd-md` | buttons, inputs, selects, triggers |
43
+ | `lg` | `--era-h-lg` | `--era-rd-lg` | bars, containers, dialog headers |
44
44
 
45
45
  Usage: `h-(--era-h-md)`, `rounded-(--era-rd-md)`, icons `size-(--era-h-xs)`.
46
46
  Always the paren shorthand — never `h-[var(--era-h-md)]`.
@@ -119,8 +119,8 @@ motion-axis-aware for free.
119
119
 
120
120
  ```svelte
121
121
  <script lang="ts">
122
- import { Button, Badge, OS, AI } from '@sig-nine/era-ui';
123
- import * as Select from '@sig-nine/era-ui'; // namespaced parts: Select.Root…
122
+ import { Button, Badge, OS, AI } from '@sig-nine/era-ui';
123
+ import * as Select from '@sig-nine/era-ui'; // namespaced parts: Select.Root…
124
124
  </script>
125
125
  ```
126
126