@adia-ai/web-components 0.8.9 → 0.8.11

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 (89) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/MIGRATION.md +58 -0
  3. package/USAGE.md +5 -7
  4. package/components/adia-mark/adia-mark.a2ui.json +6 -6
  5. package/components/adia-mark/adia-mark.class.js +1 -1
  6. package/components/adia-mark/adia-mark.css +3 -4
  7. package/components/adia-mark/adia-mark.d.ts +1 -1
  8. package/components/adia-mark/adia-mark.examples.md +0 -2
  9. package/components/adia-mark/adia-mark.test.js +3 -3
  10. package/components/adia-mark/adia-mark.yaml +5 -4
  11. package/components/adia-wordmark/adia-wordmark.a2ui.json +108 -0
  12. package/components/adia-wordmark/adia-wordmark.class.js +60 -0
  13. package/components/adia-wordmark/adia-wordmark.css +45 -0
  14. package/components/adia-wordmark/adia-wordmark.d.ts +20 -0
  15. package/components/adia-wordmark/adia-wordmark.examples.md +23 -0
  16. package/components/adia-wordmark/adia-wordmark.js +17 -0
  17. package/components/adia-wordmark/adia-wordmark.test.js +69 -0
  18. package/components/adia-wordmark/adia-wordmark.yaml +102 -0
  19. package/components/avatar/avatar-group.a2ui.json +1 -1
  20. package/components/avatar/avatar-group.yaml +1 -1
  21. package/components/avatar/avatar.a2ui.json +2 -4
  22. package/components/avatar/avatar.css +16 -12
  23. package/components/avatar/avatar.d.ts +3 -3
  24. package/components/avatar/avatar.examples.md +1 -1
  25. package/components/avatar/avatar.yaml +1 -3
  26. package/components/badge/badge.a2ui.json +2 -4
  27. package/components/badge/badge.d.ts +2 -2
  28. package/components/badge/badge.yaml +1 -3
  29. package/components/button/button.a2ui.json +2 -4
  30. package/components/button/button.d.ts +2 -2
  31. package/components/button/button.yaml +1 -3
  32. package/components/calendar-grid/calendar-grid.class.js +44 -27
  33. package/components/calendar-grid/calendar-grid.css +1 -1
  34. package/components/calendar-picker/calendar-picker.class.js +2 -2
  35. package/components/calendar-picker/calendar-picker.css +1 -1
  36. package/components/card/card.css +1 -1
  37. package/components/chat-thread/chat-input.css +1 -1
  38. package/components/combobox/combobox.class.js +1 -1
  39. package/components/combobox/combobox.css +1 -1
  40. package/components/icon/icon.a2ui.json +1 -1
  41. package/components/icon/icon.class.js +5 -5
  42. package/components/icon/icon.css +7 -15
  43. package/components/icon/icon.d.ts +1 -1
  44. package/components/icon/icon.examples.md +1 -1
  45. package/components/icon/icon.yaml +10 -10
  46. package/components/index.js +1 -0
  47. package/components/input/input.class.js +2 -2
  48. package/components/modal/modal.a2ui.json +2 -3
  49. package/components/modal/modal.d.ts +2 -2
  50. package/components/modal/modal.yaml +1 -2
  51. package/components/select/select.class.js +4 -4
  52. package/components/select/select.css +12 -3
  53. package/components/swatch/swatch.css +1 -1
  54. package/components/switch/switch.a2ui.json +2 -4
  55. package/components/switch/switch.yaml +1 -3
  56. package/components/table/table.yaml +9 -3
  57. package/components/tag/tag.a2ui.json +2 -1
  58. package/components/tag/tag.class.js +1 -1
  59. package/components/tag/tag.css +31 -5
  60. package/components/tag/tag.d.ts +2 -2
  61. package/components/tag/tag.yaml +5 -1
  62. package/components/theme-provider/theme-provider.a2ui.json +11 -1
  63. package/components/theme-provider/theme-provider.class.js +27 -7
  64. package/components/theme-provider/theme-provider.d.ts +2 -2
  65. package/components/theme-provider/theme-provider.yaml +10 -1
  66. package/components/timeline/timeline.css +1 -1
  67. package/components/toggle-scheme/toggle-scheme.a2ui.json +2 -4
  68. package/components/toggle-scheme/toggle-scheme.d.ts +2 -2
  69. package/components/toggle-scheme/toggle-scheme.yaml +1 -3
  70. package/dist/host.min.css +1 -1
  71. package/dist/host.sheet.js +1 -1
  72. package/dist/scale.min.css +1 -0
  73. package/dist/scale.sheet.js +11 -0
  74. package/dist/theme-provider.min.js +8 -8
  75. package/dist/web-components.min.css +1 -1
  76. package/dist/web-components.min.js +94 -86
  77. package/dist/web-components.sheet.js +1 -1
  78. package/package.json +1 -1
  79. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +4 -4
  80. package/patterns/kanban-board/kanban-board.examples.html +8 -8
  81. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +2 -2
  82. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +1 -1
  83. package/patterns/search-discovery/search-discovery.examples.html +4 -4
  84. package/styles/colors/semantics/aliases.css +16 -3
  85. package/styles/colors/semantics/features.css +10 -0
  86. package/styles/components.css +1 -0
  87. package/styles/prose.css +4 -0
  88. package/styles/scale.css +683 -0
  89. package/styles/verse.css +4 -0
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import '../../core/element.js';
3
+ import './adia-wordmark.js';
4
+
5
+ const tick = () => new Promise((r) => queueMicrotask(r));
6
+
7
+ function mount(html) {
8
+ const wrap = document.createElement('div');
9
+ wrap.innerHTML = html;
10
+ document.body.appendChild(wrap);
11
+ return wrap.firstElementChild;
12
+ }
13
+
14
+ describe('adia-wordmark-ui', () => {
15
+ beforeEach(() => { document.body.innerHTML = ''; });
16
+
17
+ it('registers adia-wordmark-ui as a custom element', () => {
18
+ expect(customElements.get('adia-wordmark-ui')).toBeDefined();
19
+ });
20
+
21
+ it('defaults to size="md"', async () => {
22
+ const el = mount('<adia-wordmark-ui></adia-wordmark-ui>');
23
+ await tick();
24
+ expect(el.size).toBe('md');
25
+ });
26
+
27
+ it('reflects an explicit [size]', async () => {
28
+ const el = mount('<adia-wordmark-ui size="lg"></adia-wordmark-ui>');
29
+ await tick();
30
+ expect(el.size).toBe('lg');
31
+ });
32
+
33
+ it('defaults to inline=false and reflects an explicit [inline]', async () => {
34
+ const a = mount('<adia-wordmark-ui></adia-wordmark-ui>');
35
+ const b = mount('<adia-wordmark-ui inline></adia-wordmark-ui>');
36
+ await tick();
37
+ expect(a.inline).toBe(false);
38
+ expect(b.inline).toBe(true);
39
+ expect(b.getAttribute('inline')).toBe('');
40
+ });
41
+
42
+ it('sets role="img" and a default aria-label on connect', async () => {
43
+ const el = mount('<adia-wordmark-ui></adia-wordmark-ui>');
44
+ await tick();
45
+ expect(el.getAttribute('role')).toBe('img');
46
+ expect(el.getAttribute('aria-label')).toBe('Adia');
47
+ });
48
+
49
+ it('does not overwrite a consumer-authored aria-label', async () => {
50
+ const el = mount('<adia-wordmark-ui aria-label="Adia — home"></adia-wordmark-ui>');
51
+ await tick();
52
+ expect(el.getAttribute('aria-label')).toBe('Adia — home');
53
+ });
54
+
55
+ it('stamps the wordmark SVG with five glyph paths, no consumer-content conflict', async () => {
56
+ const el = mount('<adia-wordmark-ui></adia-wordmark-ui>');
57
+ await tick();
58
+ const svg = el.querySelector('svg');
59
+ expect(svg).toBeTruthy();
60
+ expect(el.querySelectorAll('[data-wordmark-glyph]').length).toBe(5);
61
+ });
62
+
63
+ it('renders identically regardless of size — static template, no interpolation', async () => {
64
+ const a = mount('<adia-wordmark-ui size="sm"></adia-wordmark-ui>');
65
+ const b = mount('<adia-wordmark-ui size="lg"></adia-wordmark-ui>');
66
+ await tick();
67
+ expect(a.innerHTML).toBe(b.innerHTML);
68
+ });
69
+ });
@@ -0,0 +1,102 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UIAdiaWordmark
3
+ tag: adia-wordmark-ui
4
+ status: stable
5
+ component: AdiaWordmark
6
+ category: display
7
+ version: 1
8
+ description: >-
9
+ The Adia wordmark — the "Adia" text logotype rendered as inline SVG.
10
+ Unlike `adia-mark-ui` (gh#283's fixed inverse-on-inverse square tile),
11
+ this is a standalone mark meant to sit directly on a surface: its fill
12
+ defaults to the current scheme's on-surface foreground token
13
+ (`--a-fg-strong`) rather than a fixed inverse pairing, so it reads
14
+ correctly wherever it's placed — a login header, a footer, a splash
15
+ screen — without needing a background chip (gh#390).
16
+ props:
17
+ size:
18
+ description: Height of the wordmark; width follows via the fixed aspect ratio. Matches adia-mark-ui's size ladder for visual consistency when the two sit side by side.
19
+ type: string
20
+ default: md
21
+ enum:
22
+ - sm
23
+ - md
24
+ - lg
25
+ reflect: true
26
+ inline:
27
+ description: Renders as `display:inline-block` instead of the block-level default (ADR-0037) — set this when the wordmark sits beside a text run rather than standing alone.
28
+ type: boolean
29
+ default: false
30
+ reflect: true
31
+ events: {}
32
+ slots: {}
33
+ states:
34
+ - name: default
35
+ description: The only state — a static, non-interactive wordmark.
36
+ traits: []
37
+ tokens:
38
+ --adia-wordmark-height:
39
+ description: Height of the wordmark; width follows via aspect-ratio.
40
+ default: 1.5rem
41
+ --adia-wordmark-fg:
42
+ description: Wordmark fill — defaults to the on-surface foreground token. Themeable (unlike adia-mark-ui's fixed inverse pairing), since this mark already sits directly on a surface rather than owning its own background chip.
43
+ default: var(--a-fg-strong)
44
+ a2ui:
45
+ rules:
46
+ - Use <AdiaWordmark> for the Adia text logotype standing alone directly on a surface — a login/marketing header, a footer, an about panel, a splash screen with no background chip. Use <AdiaMark> instead for the square icon/tile use (app-switcher slot, favicon-style avatar).
47
+ - Do not wrap <AdiaWordmark> in <Icon> or pass it a color/tone override tied to an inverse pairing — it has no [name] prop, always renders the same logotype path data, and its fill is meant to track the current surface's foreground token.
48
+ - Prefer <AdiaWordmark size="sm"> in a nav/topbar brand slot; reserve larger sizes (lg, xl) for splash or login screens. Set [inline] only when the wordmark sits directly inside a run of prose text, not inside a Row/flex layout (block-level children compose fine as flex items already).
49
+ anti_patterns:
50
+ - wrong: |
51
+ <icon-ui name="adia"></icon-ui>
52
+ why: |
53
+ icon-ui only resolves Phosphor glyph names from the registry; there
54
+ is no "adia" Phosphor icon, and registering one would misuse a
55
+ third-party glyph set for a proprietary wordmark.
56
+ fix: |
57
+ <adia-wordmark-ui size="sm"></adia-wordmark-ui>
58
+ - wrong: |
59
+ <adia-mark-ui size="sm"></adia-mark-ui>
60
+ why: |
61
+ adia-mark-ui is the fixed inverse-on-inverse SQUARE TILE (gh#283) —
62
+ always a background chip with the wordmark cut out, sized for an
63
+ app-switcher/favicon slot. It is not interchangeable with the
64
+ standalone text logotype meant to sit directly on a surface.
65
+ fix: |
66
+ <adia-wordmark-ui size="sm"></adia-wordmark-ui>
67
+ examples:
68
+ - name: login-header-wordmark
69
+ description: Standalone wordmark in a login/marketing header, no background chip needed.
70
+ a2ui: >-
71
+ [
72
+ {
73
+ "id": "brand-row",
74
+ "component": "Row",
75
+ "gap": "2",
76
+ "align": "center",
77
+ "children": ["wordmark"]
78
+ },
79
+ {
80
+ "id": "wordmark",
81
+ "component": "AdiaWordmark",
82
+ "size": "sm"
83
+ }
84
+ ]
85
+ keywords:
86
+ - adia
87
+ - wordmark
88
+ - logotype
89
+ - text logo
90
+ - identity
91
+ synonyms:
92
+ wordmark:
93
+ - adia
94
+ - logotype
95
+ - text logo
96
+ logotype:
97
+ - adia
98
+ - wordmark
99
+ related:
100
+ - AdiaMark
101
+ - Icon
102
+ - Image
@@ -22,7 +22,7 @@
22
22
  "default": 5
23
23
  },
24
24
  "size": {
25
- "description": "Forwards to each child <avatar-ui> (xs|sm|md|lg|xl). Empty uses the per-avatar size.",
25
+ "description": "Forwards to each child <avatar-ui> (sm|md|lg). Empty uses the per-avatar size. For denser or larger sets, set [scale] on an ancestor.",
26
26
  "type": "string"
27
27
  }
28
28
  },
@@ -23,7 +23,7 @@ props:
23
23
  type: number
24
24
  default: 5
25
25
  size:
26
- description: Forwards to each child <avatar-ui> (xs|sm|md|lg|xl). Empty uses the per-avatar size.
26
+ description: Forwards to each child <avatar-ui> (sm|md|lg). Empty uses the per-avatar size. For denser or larger sets, set [scale] on an ancestor.
27
27
  type: string
28
28
 
29
29
  keywords:
@@ -31,14 +31,12 @@
31
31
  "default": "circle"
32
32
  },
33
33
  "size": {
34
- "description": "Avatar size canonical scale.",
34
+ "description": "Avatar size (sm/md/lg). The former xs/xl diameters are the ancestor-[scale] grid cells (scale=\"ui-sm\", size=\"sm\") / (scale=\"ui-lg\", size=\"lg\").",
35
35
  "type": "string",
36
36
  "enum": [
37
- "xs",
38
37
  "sm",
39
38
  "md",
40
- "lg",
41
- "xl"
39
+ "lg"
42
40
  ],
43
41
  "default": "md"
44
42
  },
@@ -11,12 +11,12 @@
11
11
  --avatar-font-size: var(--a-ui-sm);
12
12
  --avatar-font-weight: var(--a-weight-medium);
13
13
 
14
- /* Sizes */
15
- --avatar-size-xs: 1.25rem;
14
+ /* Sizes — sm/md/lg only; the removed xs/xl rows live on as the
15
+ ancestor-[scale] grid cells (ui-sm, sm) / (ui-lg, lg), which
16
+ re-table this family (styles/scale.css). */
16
17
  --avatar-size-sm: 1.75rem;
17
18
  --avatar-size-md: 2.5rem;
18
19
  --avatar-size-lg: 3.5rem;
19
- --avatar-size-xl: 4.5rem;
20
20
  }
21
21
 
22
22
  :scope {
@@ -59,11 +59,9 @@
59
59
  }
60
60
 
61
61
  /* ── Sizes ── */
62
- :scope[size="xs"] { --avatar-size: var(--avatar-size-xs); --avatar-font-size: var(--a-ui-sm); }
63
62
  :scope[size="sm"] { --avatar-size: var(--avatar-size-sm); --avatar-font-size: var(--a-ui-sm); }
64
63
  :scope[size="md"] { --avatar-size: var(--avatar-size-md); --avatar-font-size: var(--a-ui-sm); }
65
64
  :scope[size="lg"] { --avatar-size: var(--avatar-size-lg); --avatar-font-size: var(--a-ui-md); }
66
- :scope[size="xl"] { --avatar-size: var(--avatar-size-xl); --avatar-font-size: var(--a-ui-lg); }
67
65
 
68
66
  /* ── Shape ── */
69
67
  :scope[shape="circle"] { --avatar-radius: var(--a-radius-full); }
@@ -84,11 +82,19 @@ avatar-group-ui:hover > [slot="overflow"] {
84
82
 
85
83
  @scope (avatar-group-ui) {
86
84
  :where(:scope) {
87
- /* ── Layout ── */
88
- --avatar-group-overlap: -0.5rem;
85
+ /* ── Layout counter/overlap are 3-row families so the ancestor
86
+ [scale] tiers can re-table them (styles/scale.css), mirroring
87
+ the --avatar-size-* family. ── */
88
+ --avatar-group-counter-size-sm: 1.75rem;
89
+ --avatar-group-counter-size-md: 2rem;
90
+ --avatar-group-counter-size-lg: 3.5rem;
91
+ --avatar-group-overlap-sm: -0.4rem;
92
+ --avatar-group-overlap-md: -0.5rem;
93
+ --avatar-group-overlap-lg: -0.625rem;
94
+ --avatar-group-overlap: var(--avatar-group-overlap-md);
89
95
  --avatar-group-spread: var(--a-space-0-5);
90
96
  --avatar-group-overflow-px: var(--a-space-1);
91
- --avatar-group-counter-size: 2rem;
97
+ --avatar-group-counter-size: var(--avatar-group-counter-size-md);
92
98
 
93
99
  /* ── Colors ── */
94
100
  --avatar-group-border: 2px solid var(--md-sys-color-neutral-background);
@@ -151,8 +157,6 @@ avatar-group-ui:hover > [slot="overflow"] {
151
157
  }
152
158
 
153
159
  /* Size variants — adjust counter + overlap */
154
- :scope[size="xs"] { --avatar-group-counter-size: 1.25rem; --avatar-group-overlap: -0.3rem; }
155
- :scope[size="sm"] { --avatar-group-counter-size: 1.75rem; --avatar-group-overlap: -0.4rem; }
156
- :scope[size="lg"] { --avatar-group-counter-size: 3.5rem; --avatar-group-overlap: -0.625rem; }
157
- :scope[size="xl"] { --avatar-group-counter-size: 4.5rem; --avatar-group-overlap: -0.75rem; }
160
+ :scope[size="sm"] { --avatar-group-counter-size: var(--avatar-group-counter-size-sm); --avatar-group-overlap: var(--avatar-group-overlap-sm); }
161
+ :scope[size="lg"] { --avatar-group-counter-size: var(--avatar-group-counter-size-lg); --avatar-group-overlap: var(--avatar-group-overlap-lg); }
158
162
  }
@@ -23,8 +23,8 @@ export class UIAvatar extends UIElement {
23
23
  icon: string;
24
24
  /** Avatar shape */
25
25
  shape: 'circle' | 'square';
26
- /** Avatar size canonical scale. */
27
- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
26
+ /** Avatar size (sm/md/lg). The former xs/xl diameters are the ancestor-[scale] grid cells (scale="ui-sm", size="sm") / (scale="ui-lg", size="lg"). */
27
+ size: 'sm' | 'md' | 'lg';
28
28
  /** Image source URL. Falls back to initials on error. */
29
29
  src: string;
30
30
  /** Display string (full name or initials). Initials derive from spaces — first letter of each word, max 2. */
@@ -34,6 +34,6 @@ export class UIAvatar extends UIElement {
34
34
  export class UIAvatarGroup extends UIElement {
35
35
  /** Maximum avatars to show before the +N overflow indicator. */
36
36
  max: number;
37
- /** Forwards to each child <avatar-ui> (xs|sm|md|lg|xl). Empty uses the per-avatar size. */
37
+ /** Forwards to each child <avatar-ui> (sm|md|lg). Empty uses the per-avatar size. For denser or larger sets, set [scale] on an ancestor. */
38
38
  size: string;
39
39
  }
@@ -9,7 +9,7 @@
9
9
  ## size
10
10
 
11
11
  ```html
12
- <avatar-ui text="XS" size="xs"></avatar-ui>
12
+ <avatar-ui text="SM" size="sm"></avatar-ui>
13
13
  ```
14
14
 
15
15
  ## shape
@@ -35,15 +35,13 @@ props:
35
35
  - circle
36
36
  - square
37
37
  size:
38
- description: Avatar size canonical scale.
38
+ description: Avatar size (sm/md/lg). The former xs/xl diameters are the ancestor-[scale] grid cells (scale="ui-sm", size="sm") / (scale="ui-lg", size="lg").
39
39
  type: string
40
40
  default: md
41
41
  enum:
42
- - xs
43
42
  - sm
44
43
  - md
45
44
  - lg
46
- - xl
47
45
  src:
48
46
  description: Image source URL. Falls back to initials on error.
49
47
  type: string
@@ -22,14 +22,12 @@
22
22
  "default": ""
23
23
  },
24
24
  "size": {
25
- "description": "Badge size",
25
+ "description": "Badge size (sm/md/lg). For a denser or larger register, set [scale] on an ancestor.",
26
26
  "type": "string",
27
27
  "enum": [
28
- "xs",
29
28
  "sm",
30
29
  "md",
31
- "lg",
32
- "xl"
30
+ "lg"
33
31
  ],
34
32
  "default": "md"
35
33
  },
@@ -21,8 +21,8 @@ import { UIElement } from '../../core/element.js';
21
21
  export class UIBadge extends UIElement {
22
22
  /** Optional leading icon (any registered name). Use icon="dot" for chart-legend / status-pip chips. Inherits the variant's foreground color so the dot matches the label. */
23
23
  icon: string;
24
- /** Badge size */
25
- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
24
+ /** Badge size (sm/md/lg). For a denser or larger register, set [scale] on an ancestor. */
25
+ size: 'sm' | 'md' | 'lg';
26
26
  /** Semantic status shorthand. Auto-maps to variant and sets text. `new` → success "New"; `updated` → info "Updated"; `deprecated` → warning "Deprecated"; `removed` → danger "Removed"; `beta` → accent "Beta". Use `variant` + `text` directly for custom chips. */
27
27
  status: 'new' | 'updated' | 'deprecated' | 'removed' | 'beta';
28
28
  /** Badge text content. Falls back to existing textContent. */
@@ -25,15 +25,13 @@ props:
25
25
  type: string
26
26
  default: ""
27
27
  size:
28
- description: Badge size
28
+ description: Badge size (sm/md/lg). For a denser or larger register, set [scale] on an ancestor.
29
29
  type: string
30
30
  default: md
31
31
  enum:
32
- - xs
33
32
  - sm
34
33
  - md
35
34
  - lg
36
- - xl
37
35
  status:
38
36
  description: Semantic status shorthand. Auto-maps to variant and sets text.
39
37
  `new` → success "New"; `updated` → info "Updated"; `deprecated` → warning
@@ -54,14 +54,12 @@
54
54
  "default": ""
55
55
  },
56
56
  "size": {
57
- "description": "Sizing scale (xs/sm/md/lg/xl).",
57
+ "description": "Sizing scale (sm/md/lg). For a denser or larger register, set [scale] on an ancestor (scale=\"ui-sm\" … \"content-lg\") — scale picks the table, size picks the row.",
58
58
  "type": "string",
59
59
  "enum": [
60
- "xs",
61
60
  "sm",
62
61
  "md",
63
- "lg",
64
- "xl"
62
+ "lg"
65
63
  ],
66
64
  "default": "md"
67
65
  },
@@ -23,8 +23,8 @@ export class UIButton extends UIElement {
23
23
  disabled: boolean;
24
24
  /** Leading Phosphor icon name (rendered before text). */
25
25
  icon: string;
26
- /** Sizing scale (xs/sm/md/lg/xl). */
27
- size: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
26
+ /** Sizing scale (sm/md/lg). For a denser or larger register, set [scale] on an ancestor (scale="ui-sm" … "content-lg") — scale picks the table, size picks the row. */
27
+ size: 'sm' | 'md' | 'lg';
28
28
  /** Makes the button stretch to fill the parent's width */
29
29
  stretch: boolean;
30
30
  /** Button text, rendered via CSS attr(text) on ::after */
@@ -42,15 +42,13 @@ props:
42
42
  type: string
43
43
  default: ""
44
44
  size:
45
- description: "Sizing scale (xs/sm/md/lg/xl)."
45
+ description: "Sizing scale (sm/md/lg). For a denser or larger register, set [scale] on an ancestor (scale=\"ui-sm\" … \"content-lg\") — scale picks the table, size picks the row."
46
46
  type: string
47
47
  default: md
48
48
  enum:
49
- - xs
50
49
  - sm
51
50
  - md
52
51
  - lg
53
- - xl
54
52
  text:
55
53
  description: Button text, rendered via CSS attr(text) on ::after
56
54
  type: string
@@ -168,9 +168,9 @@ export class UICalendarGrid extends UIElement {
168
168
  // respects the consumer-authored tabindex="-1", so the nav carets stay
169
169
  // out of the roving-tabindex day-grid's tab sequence exactly as before.
170
170
  let h = `<div data-cal-header>
171
- <button-ui variant="ghost" size="xs" icon="caret-left" data-cal-prev aria-label="Previous month" tabindex="-1"></button-ui>
171
+ <button-ui variant="ghost" size="sm" icon="caret-left" data-cal-prev aria-label="Previous month" tabindex="-1"></button-ui>
172
172
  <span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
173
- <button-ui variant="ghost" size="xs" icon="caret-right" data-cal-next aria-label="Next month" tabindex="-1"></button-ui>
173
+ <button-ui variant="ghost" size="sm" icon="caret-right" data-cal-next aria-label="Next month" tabindex="-1"></button-ui>
174
174
  </div>`;
175
175
 
176
176
  // Day-of-week header
@@ -181,36 +181,39 @@ export class UICalendarGrid extends UIElement {
181
181
  const daysInMonth = new Date(year, month + 1, 0).getDate();
182
182
  const daysInPrev = new Date(year, month, 0).getDate();
183
183
 
184
- h += '<div data-cal-grid>';
185
-
186
- // Previous-month trailing days (outside, disabled)
187
- for (let i = firstDay - 1; i >= 0; i--) {
188
- const day = daysInPrev - i;
189
- h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${day}"></button-ui>`;
190
- }
191
-
192
- // Current-month days
193
- for (let d = 1; d <= daysInMonth; d++) {
194
- const date = new Date(year, month, d);
195
- const iso = toISO(year, month, d);
196
- const isToday = sameDay(date, today);
197
- const isSelected = selected && sameDay(date, selected);
198
- const isDisabled = (minDate && date < minDate) || (maxDate && date > maxDate);
199
- const isFocused = this.#focusedDay === d;
184
+ // <button-ui variant="ghost" text="…"> (gh issue 276 wave 4) — the day
185
+ // number moves from element text content to the `text` attribute, which
186
+ // button-ui renders via CSS attr(text) on its ::after.
187
+ //
188
+ // Shared by all three day-loops (prev-month filler / current-month /
189
+ // next-month filler) so today/selected/in-range predicates stay
190
+ // consistent across a month boundary (gh#368) — a range spanning two
191
+ // months must show a continuous in-range band through the filler days
192
+ // representing the adjacent month, not just within each month's own
193
+ // cells. Filler days pass `outside: true`, which forces them
194
+ // disabled/unfocusable/unclickable regardless of the computed
195
+ // predicates this only affects the highlight band's visual
196
+ // continuity, never selectability.
197
+ const dayCell = (date, day, outside) => {
198
+ const iso = toISO(date.getFullYear(), date.getMonth(), date.getDate());
199
+ const isToday = sameDay(date, today);
200
+ const isSelected = !!(selected && sameDay(date, selected));
200
201
  // In-range: strictly between rangeStart and rangeEnd (endpoints
201
202
  // themselves render as `[data-selected]` via the `value` prop;
202
203
  // marking them in-range too would double-up the styling).
203
- const isInRange = !!(rangeFrom && rangeTo
204
+ const isInRange = !!(rangeFrom && rangeTo
204
205
  && date > rangeFrom && date < rangeTo);
205
206
  // Range endpoint flags — let CSS render the start/end cell with
206
- // half-pill caps so the in-range strip reads as a continuous
207
- // band when displayed next to the in-range cells.
207
+ // half-pill caps so the in-range strip reads as a continuous band
208
+ // when displayed next to the in-range cells.
208
209
  const isRangeStart = !!(rangeFrom && sameDay(date, rangeFrom));
209
210
  const isRangeEnd = !!(rangeTo && sameDay(date, rangeTo));
211
+ const isDisabled = outside || (minDate && date < minDate) || (maxDate && date > maxDate);
212
+ const isFocused = !outside && this.#focusedDay === day;
210
213
 
211
214
  const attrs = [
212
215
  'data-cal-day',
213
- `data-date="${iso}"`,
216
+ outside ? 'data-outside' : `data-date="${iso}"`,
214
217
  isToday ? 'data-today' : '',
215
218
  isSelected ? 'data-selected' : '',
216
219
  isInRange ? 'data-in-range' : '',
@@ -221,17 +224,31 @@ export class UICalendarGrid extends UIElement {
221
224
  `tabindex="${isFocused ? '0' : '-1'}"`,
222
225
  ].filter(Boolean).join(' ');
223
226
 
224
- // <button-ui variant="ghost" text=""> (gh issue 276 wave 4) — the day
225
- // number moves from element text content to the `text` attribute,
226
- // which button-ui renders via CSS attr(text) on its ::after.
227
- h += `<button-ui variant="ghost" ${attrs} text="${d}"></button-ui>`;
227
+ return `<button-ui variant="ghost" ${attrs} text="${day}"></button-ui>`;
228
+ };
229
+
230
+ h += '<div data-cal-grid>';
231
+
232
+ // Previous-month trailing days (outside, disabled)
233
+ const prevMonth = month === 0 ? 11 : month - 1;
234
+ const prevYear = month === 0 ? year - 1 : year;
235
+ for (let i = firstDay - 1; i >= 0; i--) {
236
+ const day = daysInPrev - i;
237
+ h += dayCell(new Date(prevYear, prevMonth, day), day, true);
238
+ }
239
+
240
+ // Current-month days
241
+ for (let d = 1; d <= daysInMonth; d++) {
242
+ h += dayCell(new Date(year, month, d), d, false);
228
243
  }
229
244
 
230
245
  // Next-month leading days
231
246
  const totalCells = firstDay + daysInMonth;
232
247
  const remaining = (7 - (totalCells % 7)) % 7;
248
+ const nextMonth = month === 11 ? 0 : month + 1;
249
+ const nextYear = month === 11 ? year + 1 : year;
233
250
  for (let d = 1; d <= remaining; d++) {
234
- h += `<button-ui variant="ghost" data-cal-day data-outside disabled tabindex="-1" text="${d}"></button-ui>`;
251
+ h += dayCell(new Date(nextYear, nextMonth, d), d, true);
235
252
  }
236
253
 
237
254
  h += '</div>';
@@ -115,7 +115,7 @@
115
115
  color: var(--calendar-grid-title-fg);
116
116
  }
117
117
 
118
- /* Stamped <button-ui variant="ghost" size="xs" icon="caret-…"> (gh issue
118
+ /* Stamped <button-ui variant="ghost" size="sm" icon="caret-…"> (gh issue
119
119
  276 wave 4) — icon-only (no [text], no child text) so button-ui's own
120
120
  [data-icon-only] rule already squares it to --button-height; pointing
121
121
  --button-height at the nav size (not just literal width/height) avoids
@@ -196,9 +196,9 @@ export class UICalendarPicker extends UIFormElement {
196
196
  // the default tab order (unlike calendar-grid-ui's, which are pulled out
197
197
  // via tabindex="-1" for its inline roving-tabindex day grid).
198
198
  let h = `<div data-cal-header>
199
- <button-ui variant="ghost" size="xs" icon="caret-left" data-cal-prev aria-label="Previous month"></button-ui>
199
+ <button-ui variant="ghost" size="sm" icon="caret-left" data-cal-prev aria-label="Previous month"></button-ui>
200
200
  <span data-cal-title>${MONTHS_LONG[month]} ${year}</span>
201
- <button-ui variant="ghost" size="xs" icon="caret-right" data-cal-next aria-label="Next month"></button-ui>
201
+ <button-ui variant="ghost" size="sm" icon="caret-right" data-cal-next aria-label="Next month"></button-ui>
202
202
  </div>`;
203
203
 
204
204
  // Day-of-week header
@@ -232,7 +232,7 @@ calendar-picker-ui [data-cal-title] {
232
232
  color: var(--calendar-picker-title-fg);
233
233
  }
234
234
 
235
- /* Stamped <button-ui variant="ghost" size="xs" icon="caret-…"> (gh issue 276
235
+ /* Stamped <button-ui variant="ghost" size="sm" icon="caret-…"> (gh issue 276
236
236
  wave 4) — icon-only (no [text], no child text) so button-ui's own
237
237
  [data-icon-only] rule already squares it to --button-height; pointing
238
238
  --button-height at the nav size (not just literal height below) avoids
@@ -61,7 +61,7 @@
61
61
  box-shadow: var(--card-shadow);
62
62
  overflow: hidden;
63
63
  corner-shape: superellipse(1.1);
64
- width: stretch;
64
+ flex: 1 1 auto;
65
65
  }
66
66
 
67
67
  /* ═══════ Variants — token-only overrides ═══════ */
@@ -241,7 +241,7 @@ chat-input-ui textarea-ui:not([disabled]) [slot="text"]:hover {
241
241
  border: 1px solid var(--chat-input-image-border);
242
242
  }
243
243
 
244
- /* Attachment-remove — a stamped <button-ui variant="ghost" size="xs"
244
+ /* Attachment-remove — a stamped <button-ui variant="ghost" size="sm"
245
245
  icon="x"> (gh issue 276: no native <button> in stamped DOM); the
246
246
  floating filled-circle look rides button token re-points. */
247
247
  [slot="preview"] [data-preview-remove] {
@@ -241,7 +241,7 @@ export class UICombobox extends UIFormElement {
241
241
  aria-controls="${listboxId}"
242
242
  aria-labelledby="${labelId}"
243
243
  data-placeholder="${escapeHTML(this.placeholder || '')}"></span>
244
- <button-ui variant="ghost" size="xs" data-clear aria-label="Clear" tabindex="-1" style="display:none"><icon-ui slot="icon" name="x-circle"></icon-ui></button-ui>
244
+ <button-ui variant="ghost" size="sm" data-clear aria-label="Clear" tabindex="-1" style="display:none"><icon-ui slot="icon" name="x-circle"></icon-ui></button-ui>
245
245
  <span data-suffix>
246
246
  <icon-ui name="caret-down"></icon-ui>
247
247
  </span>
@@ -161,7 +161,7 @@
161
161
  align-items: center;
162
162
  }
163
163
 
164
- /* Clear x — a stamped <button-ui variant="ghost" size="xs"> kept
164
+ /* Clear x — a stamped <button-ui variant="ghost" size="sm"> kept
165
165
  mouse-only via tabindex="-1" (gh issue 276: no native <button> in
166
166
  stamped DOM); colors/geometry ride button token re-points. */
167
167
  [data-clear] {
@@ -27,7 +27,7 @@
27
27
  "default": ""
28
28
  },
29
29
  "size": {
30
- "description": "Icon size. Accepts the named scale (`xs` 12px / `sm` 14px / `md` 16px / `lg` 20px / `xl` 32px / `2xl` 48px / `3xl` 64px / `4xl` 96px / `fill` 100% of parent) or a free-form pixel / rem / em value as a string (\"48\", \"3rem\", \"1.25em\"). Overrides the inherited `--a-icon-size` from the universal `[size]` system on ancestors.",
30
+ "description": "Icon size. Named tiers `sm` 14px / `md` 16px / `lg` 20px ride the universal `[size]` system (`--a-icon-size`), which the ancestor `[scale]` tiers re-table. Anything beyond the row is a free-form pixel / rem / em / percent string (\"48\", \"3rem\", \"1.25em\", \"100%\" fills the parent box).",
31
31
  "type": "string",
32
32
  "default": ""
33
33
  },
@@ -100,10 +100,10 @@ export class UIIcon extends UIElement {
100
100
  // and is correctly NOT flagged here.
101
101
  else if (this.name && !svg) warnIfIconRegistryUnwired();
102
102
 
103
- // Pixel / rem / em passthrough — `size="48"` → 48px,
104
- // `size="3.5rem"` → 3.5rem. Named-scale values (xs/sm/md/lg/xl/
105
- // 2xl/3xl/4xl/fill) are handled by `:scope[size="…"]` rules in
106
- // icon.css; we only set --icon-size inline for free-form values.
103
+ // Pixel / rem / em / % passthrough — `size="48"` → 48px,
104
+ // `size="3.5rem"` → 3.5rem, `size="100%"` fill the parent box.
105
+ // Named sm/md/lg ride the universal [size] system (--a-icon-size);
106
+ // we only set --icon-size inline for free-form values.
107
107
  if (this.size && this.#isFreeFormSize(this.size)) {
108
108
  this.style.setProperty('--icon-size', this.#normalizeSize(this.size));
109
109
  } else if (this.style.getPropertyValue('--icon-size')) {
@@ -112,7 +112,7 @@ export class UIIcon extends UIElement {
112
112
  }
113
113
 
114
114
  #isFreeFormSize(s) {
115
- return /^\d+(\.\d+)?(px|rem|em)?$/.test(s);
115
+ return /^\d+(\.\d+)?(px|rem|em|%)?$/.test(s);
116
116
  }
117
117
 
118
118
  #normalizeSize(s) {