@adia-ai/web-components 0.8.22 → 0.8.24

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 (106) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/MIGRATION.md +9 -0
  3. package/components/agent-questions/agent-questions.yaml +0 -2
  4. package/components/agent-trace/agent-trace.js +4 -1
  5. package/components/alert/alert.a2ui.json +1 -1
  6. package/components/alert/alert.class.js +5 -0
  7. package/components/alert/alert.css +44 -5
  8. package/components/alert/alert.yaml +10 -4
  9. package/components/calendar-grid/calendar-grid.yaml +3 -0
  10. package/components/canvas/canvas.js +4 -1
  11. package/components/card/card.css +12 -0
  12. package/components/chat-thread/chat-input.js +4 -1
  13. package/components/code/code.css +29 -26
  14. package/components/combobox/combobox.class.js +1 -1
  15. package/components/combobox/combobox.yaml +0 -1
  16. package/components/date-range-picker/date-range-picker.class.js +1 -1
  17. package/components/date-range-picker/date-range-picker.yaml +0 -1
  18. package/components/datetime-picker/datetime-picker.class.js +1 -1
  19. package/components/datetime-picker/datetime-picker.yaml +0 -1
  20. package/components/description-list/description-list.css +9 -0
  21. package/components/drawer/drawer.class.js +5 -0
  22. package/components/drawer/drawer.css +16 -0
  23. package/components/drawer/drawer.yaml +2 -0
  24. package/components/feed/feed.class.js +5 -0
  25. package/components/feed/feed.yaml +2 -0
  26. package/components/field/field.css +4 -1
  27. package/components/input/input.class.js +5 -0
  28. package/components/input/input.yaml +3 -0
  29. package/components/list/list.css +25 -5
  30. package/components/modal/modal.class.js +5 -0
  31. package/components/modal/modal.yaml +2 -0
  32. package/components/nav-group/nav-group.class.js +50 -2
  33. package/components/nav-group/nav-group.css +11 -2
  34. package/components/nav-group/nav-group.yaml +2 -0
  35. package/components/pagination/pagination.yaml +3 -0
  36. package/components/progress-row/progress-row.class.js +10 -2
  37. package/components/richtext/richtext.css +7 -0
  38. package/components/select/select.a2ui.json +5 -0
  39. package/components/select/select.class.js +27 -8
  40. package/components/select/select.yaml +13 -1
  41. package/components/stat/stat.js +4 -1
  42. package/components/stepper/stepper.css +25 -20
  43. package/components/swiper/swiper.class.js +5 -0
  44. package/components/swiper/swiper.yaml +3 -0
  45. package/components/table/table.a2ui.json +15 -1
  46. package/components/table/table.class.js +14 -1
  47. package/components/table/table.css +68 -3
  48. package/components/table/table.d.ts +5 -1
  49. package/components/table/table.yaml +34 -1
  50. package/components/tabs/tab.js +4 -1
  51. package/components/tag/tag.css +15 -9
  52. package/components/toolbar/toolbar.class.js +20 -0
  53. package/components/toolbar/toolbar.yaml +2 -0
  54. package/core/provider.js +5 -1
  55. package/dist/host.min.css +1 -1
  56. package/dist/host.sheet.js +1 -1
  57. package/dist/scale.min.css +1 -1
  58. package/dist/scale.sheet.js +1 -1
  59. package/dist/theme-provider.min.js +2 -2
  60. package/dist/web-components.min.css +1 -1
  61. package/dist/web-components.min.js +93 -93
  62. package/dist/web-components.sheet.js +1 -1
  63. package/package.json +1 -1
  64. package/patterns/access-requests/access-requests.examples.html +12 -3
  65. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +23 -8
  66. package/patterns/agent-cost/agent-cost.examples.html +12 -3
  67. package/patterns/agent-memory/agent-memory.examples.html +12 -3
  68. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +12 -3
  69. package/patterns/agent-tool-call/agent-tool-call.examples.html +16 -4
  70. package/patterns/alert-dialog/alert-dialog.examples.html +12 -3
  71. package/patterns/app-nav/app-nav.examples.html +36 -9
  72. package/patterns/approvals/approvals.examples.html +14 -10
  73. package/patterns/audit-log/audit-log.examples.html +5 -2
  74. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +210 -32
  75. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +83 -4
  76. package/patterns/bulk-action-toolbar/bulk-action-toolbar.html +68 -0
  77. package/patterns/changelog-feed/changelog-feed.examples.html +7 -4
  78. package/patterns/chart-in-card/chart-in-card.examples.html +28 -7
  79. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +24 -9
  80. package/patterns/conversion-funnel/conversion-funnel.examples.html +24 -15
  81. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +17 -8
  82. package/patterns/diff-review/diff-review.examples.html +12 -3
  83. package/patterns/export-flow/export-flow.examples.html +12 -3
  84. package/patterns/filter-bar/filter-bar.examples.html +13 -4
  85. package/patterns/form-drawer/form-drawer.examples.html +15 -6
  86. package/patterns/form-system/form-system.examples.html +4 -1
  87. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +12 -3
  88. package/patterns/inline-dialog/inline-dialog.examples.html +12 -3
  89. package/patterns/kanban-board/kanban-board.examples.html +4 -1
  90. package/patterns/marketing-engagement/marketing-engagement.examples.html +32 -14
  91. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +13 -4
  92. package/patterns/permissions-matrix/permissions-matrix.examples.html +9 -3
  93. package/patterns/permissions-role-picker/permissions-role-picker.examples.html +8 -2
  94. package/patterns/permissions-sharing/permissions-sharing.examples.html +12 -3
  95. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +18 -6
  96. package/patterns/record-detail-drawer/record-detail-drawer.examples.html +10 -4
  97. package/patterns/retention-cohort/retention-cohort.examples.html +15 -6
  98. package/patterns/search-discovery/search-discovery.examples.html +16 -4
  99. package/patterns/system-banners/system-banners.examples.html +54 -48
  100. package/patterns/timezone-selector/timezone-selector.examples.html +8 -2
  101. package/patterns/timezone-selector/timezone-selector.examples.js +3 -1
  102. package/patterns/wizard-drawer/wizard-drawer.examples.html +25 -23
  103. package/patterns/wizard-drawer/wizard-drawer.examples.js +12 -1
  104. package/styles/foundation/radius.css +2 -2
  105. package/styles/scale.css +2 -2
  106. package/styles/type/scale.css +6 -6
@@ -107,18 +107,21 @@
107
107
  }
108
108
 
109
109
  /* Anatomy (header-row model):
110
- row 1 = icon (col 1) · text/title (col 2) · action (col 3), all aligned
111
- on the title row. row 2 = description, spanning col 2 / -1 (under the
110
+ row 1 = text/title (col 2) · action (col 3), aligned on the title row.
111
+ icon (col 1) spans both rows, centered on the whole item (gh#558).
112
+ row 2 = description, spanning col 2 / -1 (under the
112
113
  action) so multi-line supporting text uses the full content width rather
113
- than being boxed into col 2 beside the action. For icon+text-only items
114
- there is a single row, so `grid-row: 1` is identical to the old `1 / -1`.
114
+ than being boxed into col 2 beside the action.
115
115
  `[slot="content"]` (used when consumer provides custom rendering)
116
116
  spans all columns. */
117
117
  :scope {
118
118
  box-sizing: border-box;
119
119
  display: grid;
120
120
  grid-template-columns: auto 1fr;
121
- column-gap: var(--list-item-gap-column);
121
+ /* NO column-gap (gh#524): an iconless item's empty auto track still
122
+ charges the gap, indenting content behind a ghost cell. The
123
+ icon-to-text spacing rides the icon's own margin-inline-end
124
+ instead, so a zero-width track costs zero. */
122
125
  row-gap: var(--list-item-gap-row);
123
126
  font-size: var(--list-item-font-size);
124
127
  color: var(--list-item-text-color);
@@ -141,6 +144,21 @@
141
144
  grid-row: 1;
142
145
  align-self: center;
143
146
  color: var(--list-item-icon-color);
147
+ /* carries the old column-gap (gh#524) */
148
+ margin-inline-end: var(--list-item-gap-column);
149
+ }
150
+ /* Two-line items center the glyph on the WHOLE item (gh#558) — Material's
151
+ two-line convention; pinned to row 1 it hung above the item center.
152
+ The span must be explicit (`span 2`): with no grid-template-rows the
153
+ explicit grid has zero rows, so `1 / -1` collapses back to one row.
154
+ Scoped by :has() so single-line items don't mint an empty second row
155
+ (which would charge row-gap below the content). */
156
+ :scope:has([slot="description"]) [slot="icon"] {
157
+ grid-row: 1 / span 2;
158
+ }
159
+ /* An empty stamped icon span must not resurrect the ghost track. */
160
+ :scope [slot="icon"]:empty {
161
+ display: none;
144
162
  }
145
163
 
146
164
  :scope [slot="text"] {
@@ -177,6 +195,8 @@
177
195
  grid-template-columns: auto 1fr auto;
178
196
  }
179
197
  :scope [slot="action"] {
198
+ /* carries the old column-gap on the trailing side (gh#524) */
199
+ margin-inline-start: var(--list-item-gap-column);
180
200
  grid-column: 3;
181
201
  grid-row: 1;
182
202
  align-self: center;
@@ -51,6 +51,11 @@ export class UIModal extends UIElement {
51
51
  #closeTimer = null;
52
52
  #dialogRef = null;
53
53
 
54
+ // Phosphor icons this primitive auto-stamps (without consumer markup).
55
+ // Aggregated by installIconLoadersForRegistered() across all defined
56
+ // elements. Audited by check-required-icons.mjs.
57
+ static requiredIcons = ['x'];
58
+
54
59
  static properties = {
55
60
  text: { type: String, default: '', reflect: true },
56
61
  size: { type: String, default: 'md', reflect: true },
@@ -79,6 +79,8 @@ tokens:
79
79
  description: Override surface border radius
80
80
  --modal-shadow:
81
81
  description: Override surface box shadow
82
+ requiredIcons:
83
+ - x
82
84
  a2ui:
83
85
  rules:
84
86
  - >-
@@ -30,6 +30,11 @@ import { anchorPopover } from '../../core/anchor.js';
30
30
  import { logicalSlotted } from '../../core/logical-children.js';
31
31
 
32
32
  export class UINavGroup extends UIElement {
33
+ // Phosphor icons this primitive auto-stamps (without consumer markup).
34
+ // Aggregated by installIconLoadersForRegistered() across all defined
35
+ // elements. Audited by check-required-icons.mjs.
36
+ static requiredIcons = ['caret-right'];
37
+
33
38
  static properties = {
34
39
  text: { type: String, default: '', reflect: true },
35
40
  icon: { type: String, default: '', reflect: true },
@@ -48,6 +53,14 @@ export class UINavGroup extends UIElement {
48
53
  #popover = null;
49
54
  #anchorCleanup = null;
50
55
  #headerEl = null;
56
+ // gh#501 — the gh#459 ruling ("the group CONTAINING the selected route
57
+ // fills") self-enforces: watch descendant nav-item [selected] flips and
58
+ // membership changes so the header glyph fills without consumer wiring.
59
+ // Targets ONLY the weight sync — a full render() from the observer loops:
60
+ // render's textContent writes are themselves subtree childList mutations.
61
+ // Stable ref for symmetric disconnect.
62
+ #onChildSelection = () => this.#syncHeaderWeight();
63
+ #selectionObserver = null;
51
64
 
52
65
  #onHeaderKey = (e) => {
53
66
  if (!this.collapsible) return;
@@ -80,6 +93,21 @@ export class UINavGroup extends UIElement {
80
93
  }
81
94
 
82
95
  this.#headerEl?.addEventListener('keydown', this.#onHeaderKey);
96
+
97
+ // gh#501 — fill-on-child-selection needs no consumer wiring: observe
98
+ // [selected] flips on descendants + child list churn. Watching the
99
+ // whole subtree includes this host's own [selected], which render()
100
+ // reads anyway; render() writes only the icon's [weight], which the
101
+ // attributeFilter excludes, so the observer can't feed back.
102
+ if (!this.#selectionObserver) {
103
+ this.#selectionObserver = new MutationObserver(this.#onChildSelection);
104
+ this.#selectionObserver.observe(this, {
105
+ subtree: true,
106
+ childList: true,
107
+ attributes: true,
108
+ attributeFilter: ['selected'],
109
+ });
110
+ }
83
111
  }
84
112
 
85
113
  render() {
@@ -99,9 +127,27 @@ export class UINavGroup extends UIElement {
99
127
  // path; [open] keeps driving aria-expanded only. A state-driven weight
100
128
  // swap, not a CSS color change, since Phosphor ships filled/regular as
101
129
  // distinct SVGs.
130
+ this.#syncHeaderWeight();
131
+ }
132
+
133
+ // Own [selected] OR any selected descendant item (gh#501) — the ruling's
134
+ // "group CONTAINING the selected route", self-enforcing. Skips the write
135
+ // when the weight is already right so the observer path stays quiet.
136
+ #syncHeaderWeight() {
137
+ const containsSelection =
138
+ this.hasAttribute('selected') || !!this.querySelector('nav-item-ui[selected]');
139
+ // Reflect contains-selection for CSS (gh#522): the icon COLOR half of
140
+ // the selection language keys on this host attribute — weight alone
141
+ // read as an unselected white glyph beside nav-item's primary one.
142
+ // The observer's attributeFilter is ['selected'], so this write on
143
+ // the host cannot feed back.
144
+ if (containsSelection !== this.hasAttribute('data-selected-within')) {
145
+ this.toggleAttribute('data-selected-within', containsSelection);
146
+ }
102
147
  const iconEl = this.querySelector('[slot="header"] [slot="icon"] icon-ui');
103
- if (iconEl)
104
- iconEl.setAttribute('weight', this.hasAttribute('selected') ? 'fill' : 'regular');
148
+ if (!iconEl) return;
149
+ const weight = containsSelection ? 'fill' : 'regular';
150
+ if (iconEl.getAttribute('weight') !== weight) iconEl.setAttribute('weight', weight);
105
151
  }
106
152
 
107
153
  showPopover() {
@@ -165,5 +211,7 @@ export class UINavGroup extends UIElement {
165
211
  this.#anchorCleanup?.();
166
212
  this.#anchorCleanup = null;
167
213
  this.#popover?.hidePopover?.();
214
+ this.#selectionObserver?.disconnect();
215
+ this.#selectionObserver = null;
168
216
  }
169
217
  }
@@ -14,6 +14,7 @@
14
14
  --nav-group-fg-hover: var(--a-fg-strong);
15
15
  --nav-group-fg-muted: var(--a-fg-muted);
16
16
  --nav-group-fg-selected: var(--a-fg-strong);
17
+ --nav-group-icon-fg-selected: var(--md-sys-color-primary); /* one selection color with --nav-item-accent (gh#522) */
17
18
  --nav-group-bg-hover: var(--a-bg-muted);
18
19
  --nav-group-icon-size: calc(var(--nav-group-row-height) - var(--a-space-2));
19
20
  --nav-group-badge-size: var(--a-ui-sm);
@@ -52,10 +53,18 @@
52
53
  /* Selected-look header treatment keys on [selected] — the CSS half of
53
54
  gh issue 459 (PR 460 corrected the icon weight): the group CONTAINING
54
55
  the selected route reads strong; a merely-expanded group does not. */
55
- :scope[selected] > [slot="header"],
56
- :scope[selected] > [slot="header"] [slot="icon"] {
56
+ :scope[selected] > [slot="header"] {
57
57
  color: var(--nav-group-fg-selected);
58
58
  }
59
+ /* Icon color = the SELECTION color, one language with nav-item's accent
60
+ glyph (gh#522, operator ruling 2026-07-30): filled + primary whenever
61
+ this group is selected OR contains the selected route.
62
+ [data-selected-within] is reflected by #syncHeaderWeight (gh#501) so
63
+ the JS-detected contains-selection state is CSS-addressable. */
64
+ :scope[selected] > [slot="header"] [slot="icon"],
65
+ :scope[data-selected-within] > [slot="header"] [slot="icon"] {
66
+ color: var(--nav-group-icon-fg-selected);
67
+ }
59
68
 
60
69
  /* Header row */
61
70
  [slot="header"] {
@@ -81,6 +81,8 @@ states:
81
81
 
82
82
  traits: []
83
83
  tokens: {}
84
+ requiredIcons:
85
+ - caret-right
84
86
  a2ui:
85
87
  rules:
86
88
  - >-
@@ -64,6 +64,9 @@ states:
64
64
  description: Default, ready for interaction.
65
65
  traits: []
66
66
  tokens: {}
67
+ requiredIcons:
68
+ - caret-left
69
+ - caret-right
67
70
  a2ui:
68
71
  rules:
69
72
  - rule: 'Renders below tables, card grids, or list views for paginated data. Emits page-change events.'
@@ -91,8 +91,16 @@ export class UIProgressRow extends UIElement {
91
91
  this.#labelEl.textContent = this.label;
92
92
  this.#labelEl.hidden = !this.label;
93
93
 
94
- this.#metaEl.textContent = this.meta;
95
- this.#metaEl.hidden = !this.meta;
94
+ // The [meta] ATTR owns the text when set. When it's absent, authored
95
+ // [slot="meta"] content — e.g. meta text with a trailing delta badge
96
+ // (gh#502) — is left intact instead of being wiped + hidden; only a
97
+ // genuinely empty meta element hides.
98
+ if (this.meta) {
99
+ this.#metaEl.textContent = this.meta;
100
+ this.#metaEl.hidden = false;
101
+ } else {
102
+ this.#metaEl.hidden = !this.#metaEl.hasChildNodes();
103
+ }
96
104
 
97
105
  if (this.value != null && this.value >= 0) {
98
106
  this.#progressEl.setAttribute('value', String(this.value));
@@ -96,6 +96,13 @@
96
96
  width: 100%;
97
97
  max-width: var(--richtext-max-width);
98
98
  padding: var(--richtext-py) var(--richtext-px);
99
+ /* Body spec set ONCE at the container (gh#546): p was pinned to
100
+ --richtext-body-size while li/ul had no font rule and inherited the
101
+ HOST page's larger base — chat-rendered lists read bigger than the
102
+ paragraph beside them. Everything now inherits this; headings/code
103
+ still override explicitly. */
104
+ font-size: var(--richtext-body-size);
105
+ line-height: var(--richtext-body-leading);
99
106
  }
100
107
 
101
108
  /* [flush] — paragraph-grade embedding (yaml `flush` prop): drop the
@@ -106,6 +106,11 @@
106
106
  "type": "string",
107
107
  "default": ""
108
108
  },
109
+ "no-radio": {
110
+ "description": "Suppress the single-select radio indicator on option rows (gh#498). For menu-shaped switchers — workspace or user identity menus — where the trigger already shows the committed value and a per-row radio reads as redundant selection language. Rows keep their layout and aria-selected state; the trigger and selected-row weight carry the signal. No effect under [multiple]. IMPLIED by [mark] — a mark declares an identity switcher, which is always menu-shaped (gh#498 reopen: per-instance opt-outs proved forgettable).",
111
+ "type": "boolean",
112
+ "default": false
113
+ },
109
114
  "open": {
110
115
  "description": "Whether the listbox is open",
111
116
  "type": "boolean",
@@ -26,8 +26,8 @@ export class UISelect extends UIFormElement {
26
26
  // across all defined elements. Audited by check-required-icons.mjs
27
27
  // (slot 11). Per FEEDBACK-06 §4 + FEEDBACK-07 §4.
28
28
  // SPEC-040: [multiple] mode stamps <tag-ui> chips (own `x`), checkbox
29
- // indicator (`check`), and search-input prefix (`magnifying-glass`).
30
- static requiredIcons = ['caret-down', 'check', 'x', 'magnifying-glass'];
29
+ // indicator (`check`).
30
+ static requiredIcons = ['caret-down', 'check', 'x'];
31
31
 
32
32
  // §225 (v0.5.9, FEEDBACK-10 §3): once-per-element console.warn dedup when
33
33
  // consumer authors children that aren't native <option>/<optgroup>. The
@@ -53,6 +53,10 @@ export class UISelect extends UIFormElement {
53
53
  searchable: { type: Boolean, default: false, reflect: true },
54
54
  freeText: { type: Boolean, default: false, reflect: true, attribute: 'free-text' },
55
55
  divider: { type: Boolean, default: false, reflect: true },
56
+ // gh#498 — suppress the single-select radio indicator for menu-shaped
57
+ // switchers (workspace/user identity menus) where the trigger already
58
+ // carries the committed value; rows keep layout + aria-selected.
59
+ noRadio: { type: Boolean, default: false, reflect: true, attribute: 'no-radio' },
56
60
  // §184 (v0.5.5, FEEDBACK-08 §7): optional caption beneath the
57
61
  // trigger, wired to aria-describedby on the host.
58
62
  hint: { type: String, default: '', reflect: true },
@@ -817,11 +821,22 @@ export class UISelect extends UIFormElement {
817
821
  // Single-select mirrors the multi structure with a RADIO indicator
818
822
  // (operator ruling 2026-07-27) — selection is carried by the
819
823
  // indicator, not a row tint; CSS keys off [data-single-option].
824
+ // [no-radio] (gh#498) drops only the dot: identity-switcher menus
825
+ // read the committed value from the trigger, so the per-row radio
826
+ // is redundant there; row layout + aria-selected are unchanged.
827
+ // [mark] IMPLIES it (gh#498 reopen, 2026-07-30): mark declares an
828
+ // identity switcher, and the first fix proved per-instance
829
+ // attributes get forgotten — the playground got [no-radio], the
830
+ // site chrome switcher shipped radios anyway. The semantic
831
+ // attribute is the reliable signal; [no-radio] remains for
832
+ // markless menus.
820
833
  el.setAttribute('data-single-option', '');
821
- const dot = document.createElement('span');
822
- dot.setAttribute('data-radio', '');
823
- dot.setAttribute('aria-hidden', 'true');
824
- el.appendChild(dot);
834
+ if (!(this.noRadio || this.mark)) {
835
+ const dot = document.createElement('span');
836
+ dot.setAttribute('data-radio', '');
837
+ dot.setAttribute('aria-hidden', 'true');
838
+ el.appendChild(dot);
839
+ }
825
840
  const label = document.createElement('span');
826
841
  label.setAttribute('data-option-label', '');
827
842
  if (lead) label.innerHTML = `${lead}${escapeHTML(opt.label)}`;
@@ -1053,11 +1068,15 @@ export class UISelect extends UIFormElement {
1053
1068
 
1054
1069
  #applyFilter() {
1055
1070
  if (!this.#listbox) return;
1056
- const q = this.#query.toLowerCase();
1071
+ // Underscores normalize to spaces on BOTH sides (gh#538): IANA-style
1072
+ // labels ("America/Los_Angeles") must match the query a human types
1073
+ // ("los angeles") — and vice versa.
1074
+ const norm = (t) => t.toLowerCase().replace(/_/g, ' ');
1075
+ const q = norm(this.#query);
1057
1076
  const opts = this.#listbox.querySelectorAll('[role="option"]');
1058
1077
  let anyVisible = false;
1059
1078
  for (const opt of opts) {
1060
- const label = (opt.textContent || '').toLowerCase();
1079
+ const label = norm(opt.textContent || '');
1061
1080
  const match = !q || label.includes(q);
1062
1081
  if (match) {
1063
1082
  opt.removeAttribute('data-filtered-out');
@@ -74,6 +74,19 @@ props:
74
74
  type: boolean
75
75
  default: false
76
76
  reflect: true
77
+ no-radio:
78
+ description: >-
79
+ Suppress the single-select radio indicator on option rows (gh#498).
80
+ For menu-shaped switchers — workspace or user identity menus — where
81
+ the trigger already shows the committed value and a per-row radio
82
+ reads as redundant selection language. Rows keep their layout and
83
+ aria-selected state; the trigger and selected-row weight carry the
84
+ signal. No effect under [multiple]. IMPLIED by [mark] — a mark
85
+ declares an identity switcher, which is always menu-shaped
86
+ (gh#498 reopen: per-instance opt-outs proved forgettable).
87
+ type: boolean
88
+ default: false
89
+ reflect: true
77
90
  error:
78
91
  description: Validation error message
79
92
  type: string
@@ -261,7 +274,6 @@ requiredIcons:
261
274
  - caret-down
262
275
  - check # checkbox indicator on each option in [multiple] mode
263
276
  - x # chip dismiss + clear-all affordance ([multiple] + [clearable])
264
- - magnifying-glass # search-input prefix ([searchable])
265
277
  a2ui:
266
278
  rules:
267
279
  - >-
@@ -8,6 +8,7 @@
8
8
  * Trend: up, down, neutral (colors the change badge accordingly)
9
9
  */
10
10
 
11
+ import { defineIfFree } from '../../core/register.js';
11
12
  import { UIElement } from '../../core/element.js';
12
13
 
13
14
  class UIStat extends UIElement {
@@ -119,6 +120,8 @@ class UIStat extends UIElement {
119
120
  this.#iconEl = null;
120
121
  }
121
122
  }
122
- customElements.define('stat-ui', UIStat);
123
+ // defineIfFree (gh#534): the prod bundle + module graph load in parallel;
124
+ // second registration must no-op, not throw.
125
+ defineIfFree('stat-ui', UIStat);
123
126
 
124
127
  export { UIStat };
@@ -13,20 +13,24 @@
13
13
  :where(:scope) {
14
14
  /* ── Circle ── */
15
15
  --stepper-size: 2rem;
16
- --stepper-bg: var(--md-sys-color-neutral-background);
17
- --stepper-border: var(--md-sys-color-neutral-outline);
18
- --stepper-fg: var(--a-fg-muted);
16
+ /* Operator stepper system (2026-07-30): next = transparent circle on a
17
+ low-emphasis outline; current = filled primary; completed = tonal
18
+ primary-container with a primary glyph. Borders on the two emphasized
19
+ states are transparent — the fill IS the affordance. */
20
+ --stepper-bg: transparent;
21
+ --stepper-border: var(--md-sys-color-neutral-outline-variant);
22
+ --stepper-fg: var(--md-sys-color-neutral-on-surface-variant);
19
23
  --stepper-font: var(--a-ui-sm);
20
24
  --stepper-weight: var(--a-weight-medium);
21
25
  --stepper-border-size: 2px;
22
26
 
23
- --stepper-bg-active: var(--md-sys-color-neutral-background);
24
- --stepper-border-active: var(--md-sys-color-primary);
25
- --stepper-fg-active: var(--md-sys-color-primary);
27
+ --stepper-bg-active: var(--md-sys-color-primary);
28
+ --stepper-border-active: transparent;
29
+ --stepper-fg-active: var(--md-sys-color-primary-on-primary);
26
30
 
27
- --stepper-bg-done: var(--md-sys-color-primary);
28
- --stepper-border-done: var(--md-sys-color-primary);
29
- --stepper-fg-done: var(--md-sys-color-primary-on-primary);
31
+ --stepper-bg-done: var(--md-sys-color-primary-container);
32
+ --stepper-border-done: transparent;
33
+ --stepper-fg-done: var(--md-sys-color-primary);
30
34
 
31
35
  --stepper-line: var(--md-sys-color-neutral-outline-variant);
32
36
  --stepper-line-done: var(--md-sys-color-primary);
@@ -69,20 +73,20 @@
69
73
  co-located item legitimately reads parent tokens (the same
70
74
  scope pattern as menu-item-ui inside menu-ui's popover). */
71
75
  --stepper-item-size: var(--stepper-size, 2rem);
72
- --stepper-item-bg: var(--stepper-bg, var(--md-sys-color-neutral-background));
73
- --stepper-item-border: var(--stepper-border, var(--md-sys-color-neutral-outline));
74
- --stepper-item-fg: var(--stepper-fg, var(--a-fg-muted));
76
+ --stepper-item-bg: var(--stepper-bg, transparent);
77
+ --stepper-item-border: var(--stepper-border, var(--md-sys-color-neutral-outline-variant));
78
+ --stepper-item-fg: var(--stepper-fg, var(--md-sys-color-neutral-on-surface-variant));
75
79
  --stepper-item-font: var(--stepper-font, var(--a-ui-sm));
76
80
  --stepper-item-weight: var(--stepper-weight, var(--a-weight-medium));
77
81
  --stepper-item-border-size: var(--stepper-border-size, 2px);
78
82
 
79
- --stepper-item-bg-active: var(--stepper-bg-active, var(--md-sys-color-neutral-background));
80
- --stepper-item-border-active: var(--stepper-border-active, var(--md-sys-color-primary));
81
- --stepper-item-fg-active: var(--stepper-fg-active, var(--md-sys-color-primary));
83
+ --stepper-item-bg-active: var(--stepper-bg-active, var(--md-sys-color-primary));
84
+ --stepper-item-border-active: var(--stepper-border-active, transparent);
85
+ --stepper-item-fg-active: var(--stepper-fg-active, var(--md-sys-color-primary-on-primary));
82
86
 
83
- --stepper-item-bg-done: var(--stepper-bg-done, var(--md-sys-color-primary));
84
- --stepper-item-border-done: var(--stepper-border-done, var(--md-sys-color-primary));
85
- --stepper-item-fg-done: var(--stepper-fg-done, var(--md-sys-color-primary-on-primary));
87
+ --stepper-item-bg-done: var(--stepper-bg-done, var(--md-sys-color-primary-container));
88
+ --stepper-item-border-done: var(--stepper-border-done, transparent);
89
+ --stepper-item-fg-done: var(--stepper-fg-done, var(--md-sys-color-primary));
86
90
 
87
91
  --stepper-item-line: var(--stepper-line, var(--md-sys-color-neutral-outline-variant));
88
92
  --stepper-item-line-done: var(--stepper-line-done, var(--md-sys-color-primary));
@@ -99,7 +103,8 @@
99
103
  --stepper-item-label-weight: var(--a-weight-medium);
100
104
  --stepper-item-label-size: var(--a-ui-size);
101
105
  --stepper-item-label-fg: var(--md-sys-color-neutral-on-surface);
102
- --stepper-item-pending-fg: var(--a-fg-muted);
106
+ --stepper-item-label-fg-done: var(--md-sys-color-neutral-on-surface-variant);
107
+ --stepper-item-pending-fg: var(--md-sys-color-neutral-on-surface-variant);
103
108
  --stepper-item-desc-fg: var(--md-sys-color-neutral-on-surface-variant);
104
109
  --stepper-item-desc-size: var(--a-ui-sm);
105
110
 
@@ -181,7 +186,7 @@
181
186
  }
182
187
 
183
188
  :scope[status="completed"] [slot="label"] {
184
- color: var(--stepper-item-label-fg);
189
+ color: var(--stepper-item-label-fg-done);
185
190
  }
186
191
 
187
192
  /* Pending label */
@@ -37,6 +37,11 @@
37
37
  import { UIElement } from '../../core/element.js';
38
38
 
39
39
  export class UISwiper extends UIElement {
40
+ // Phosphor icons this primitive auto-stamps (without consumer markup).
41
+ // Aggregated by installIconLoadersForRegistered() across all defined
42
+ // elements. Audited by check-required-icons.mjs.
43
+ static requiredIcons = ['caret-left', 'caret-right'];
44
+
40
45
  static properties = {
41
46
  autoplay: { type: Boolean, default: false, reflect: true },
42
47
  interval: { type: Number, default: 5000, reflect: true },
@@ -95,6 +95,9 @@ states:
95
95
  description: Default, ready for interaction.
96
96
  traits: []
97
97
  tokens: {}
98
+ requiredIcons:
99
+ - caret-left
100
+ - caret-right
98
101
  a2ui:
99
102
  rules:
100
103
  - rule: 'Horizontal slide carousel with touch + arrow nav. Hosts arbitrary children as slides.'
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "properties": {
16
16
  "columns": {
17
- "description": "Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, accessor?, format?, render?, sortFn?, filterType?, meta?}. Alternative to declarative <col-def> children.",
17
+ "description": "Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign=\"top\">).",
18
18
  "$ref": "common_types.json#/$defs/DynamicStringList"
19
19
  },
20
20
  "component": {
@@ -39,6 +39,11 @@
39
39
  "type": "boolean",
40
40
  "default": false
41
41
  },
42
+ "frameless": {
43
+ "description": "Drop the table's outer 1px perimeter and nothing else (gh#511) — row dividers are untouched (the perimeter is split onto its own --table-perimeter token, defaulting to --table-border). For card-composed tables: the card owns the frame, so the table's square perimeter otherwise renders a doubled edge (flush bleed) or a stray inner box (with content above). Standalone tables keep their chrome by default. Theming path without the attribute: set --table-perimeter to transparent per instance.",
44
+ "type": "boolean",
45
+ "default": false
46
+ },
42
47
  "loading": {
43
48
  "description": "Renders N ghost skeleton rows in place of the body data (count derived from `paginate` if set, else 5). Header + columns stay intact so the table layout is preserved while data fetches. Sets aria-busy=\"true\" on the host. Data updates are deferred until loading is set back to false.",
44
49
  "type": "boolean",
@@ -74,6 +79,15 @@
74
79
  "type": "boolean",
75
80
  "default": false
76
81
  },
82
+ "valign": {
83
+ "description": "Vertical alignment of body-cell content. 'center' (default) keeps today's behavior — every cell centers on the row's cross-axis, which reads well for single-line rows. 'top' aligns each cell's content to the top of its padding box instead, so tall/wrapped rows (a [wrap] title column, a stacked identity cell) share one first-line scan anchor across the row rather than each cell floating at a different vertical center (gh#505). Per-column `valign` in the columns[] array (or <col-def valign=\"…\">) overrides the host per-column — e.g. keep a checkbox column centered while data cells top-align. Header cells always stay centered regardless of this prop (single-line by contract) — the checkbox/expand structural columns also default to centered under [valign=\"top\"] since they're not part of columns[].",
84
+ "type": "string",
85
+ "enum": [
86
+ "top",
87
+ "center"
88
+ ],
89
+ "default": "center"
90
+ },
77
91
  "wrap": {
78
92
  "description": "Allow body-cell content to wrap onto multiple lines. Default is single-line with ellipsis truncation (matches the row convention shared by <select-ui> + <nav-item-ui>); long unbreakable strings clip gracefully rather than rewrapping the row. With [wrap], row height auto-grows to fit wrapped content. For surgical opt-in on a single column, set [data-wrap] on the cell / col-def instead.",
79
93
  "type": "boolean",
@@ -22,6 +22,7 @@
22
22
  * selectable — enable row selection via checkboxes
23
23
  * striped — alternate row background
24
24
  * density — 'compact' | 'standard' | 'comfortable'
25
+ * valign — 'center' (default) | 'top' — body-cell vertical alignment (gh#505)
25
26
  * paginate — rows per page (0 = no pagination)
26
27
  * loading — show loading overlay
27
28
  * search — global search filter string
@@ -33,7 +34,7 @@
33
34
  *
34
35
  * JS API:
35
36
  * .columns = [{key, label, type, width, minWidth, maxWidth, flex,
36
- * sortable, filterable, resizable, pinned, hidden,
37
+ * sortable, filterable, resizable, pinned, hidden, valign,
37
38
  * accessor, format, render, sortFn, sortDescFirst,
38
39
  * filterType, filterFn, aggregate, meta}]
39
40
  * .data = [{...}, ...]
@@ -111,6 +112,10 @@ export class UITable extends UIElement {
111
112
  selectable: { type: Boolean, default: false, reflect: true },
112
113
  expandable: { type: Boolean, default: false, reflect: true },
113
114
  striped: { type: Boolean, default: false, reflect: true },
115
+ valign: { type: String, default: 'center', reflect: true },
116
+ // gh#511 — drop the outer perimeter only (row dividers stay); the
117
+ // card+bleed composition's "let the container own the frame" knob.
118
+ frameless: { type: Boolean, default: false, reflect: true },
114
119
  wrap: { type: Boolean, default: false, reflect: true },
115
120
  raw: { type: Boolean, default: false, reflect: true },
116
121
  density: { type: String, default: 'standard', reflect: true },
@@ -412,6 +417,7 @@ export class UITable extends UIElement {
412
417
  resizable: el.hasAttribute('resizable'),
413
418
  filterable: el.hasAttribute('filterable'),
414
419
  filterType: el.getAttribute('filter-type') || null,
420
+ valign: el.getAttribute('valign') || null, // 'top' | 'center' | null (inherit host)
415
421
  };
416
422
 
417
423
  const w = el.getAttribute('width');
@@ -896,6 +902,13 @@ export class UITable extends UIElement {
896
902
  cell.dataset.align = typeDef.align;
897
903
  }
898
904
 
905
+ // Vertical alignment — per-column override of the host [valign] (gh#505).
906
+ // Only stamped when the column explicitly opts in; unset falls through
907
+ // to the host's --table-valign token.
908
+ if (col.valign) {
909
+ cell.dataset.valign = col.valign;
910
+ }
911
+
899
912
  // Pinned
900
913
  if (col.pinned) {
901
914
  cell.setAttribute('data-pinned', col.pinned);