@adia-ai/web-components 0.8.43 → 0.8.44

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 (93) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/components/avatar/avatar.class.js +12 -1
  3. package/components/button/button.d.ts +4 -0
  4. package/components/chart/chart.a2ui.json +26 -0
  5. package/components/chart/chart.class.js +1304 -222
  6. package/components/chart/chart.css +151 -1
  7. package/components/chart/chart.d.ts +6 -0
  8. package/components/chart/chart.yaml +57 -0
  9. package/components/chart-legend/chart-legend.a2ui.json +17 -2
  10. package/components/chart-legend/chart-legend.class.js +69 -4
  11. package/components/chart-legend/chart-legend.css +65 -1
  12. package/components/chart-legend/chart-legend.d.ts +4 -2
  13. package/components/chart-legend/chart-legend.examples.md +4 -4
  14. package/components/chart-legend/chart-legend.yaml +43 -3
  15. package/components/combobox/combobox.a2ui.json +3 -0
  16. package/components/combobox/combobox.class.js +9 -8
  17. package/components/combobox/combobox.css +25 -4
  18. package/components/combobox/combobox.yaml +6 -0
  19. package/components/command/command.css +17 -1
  20. package/components/context-menu/context-menu.d.ts +2 -0
  21. package/components/heatmap/heatmap.a2ui.json +5 -0
  22. package/components/heatmap/heatmap.d.ts +2 -0
  23. package/components/heatmap/heatmap.yaml +8 -0
  24. package/components/input/input.a2ui.json +2 -2
  25. package/components/input/input.class.js +12 -7
  26. package/components/input/input.yaml +6 -5
  27. package/components/integration-card/integration-card.d.ts +2 -0
  28. package/components/noodles/noodles.a2ui.json +0 -15
  29. package/components/noodles/noodles.yaml +0 -12
  30. package/components/number-format/number-format.class.js +10 -0
  31. package/components/pagination/pagination.class.js +88 -4
  32. package/components/popover/popover.d.ts +2 -0
  33. package/components/progress/progress.class.js +11 -6
  34. package/components/range/range.class.js +9 -1
  35. package/components/select/select.a2ui.json +5 -1
  36. package/components/select/select.class.js +14 -14
  37. package/components/select/select.css +14 -1
  38. package/components/select/select.yaml +7 -1
  39. package/components/stream/stream.d.ts +2 -0
  40. package/components/table/table.a2ui.json +6 -1
  41. package/components/table/table.class.js +18 -1
  42. package/components/table/table.css +31 -6
  43. package/components/table/table.d.ts +3 -1
  44. package/components/table/table.examples.md +2 -2
  45. package/components/table/table.yaml +9 -1
  46. package/components/table-toolbar/table-toolbar.a2ui.json +28 -4
  47. package/components/table-toolbar/table-toolbar.class.js +346 -11
  48. package/components/table-toolbar/table-toolbar.css +265 -0
  49. package/components/table-toolbar/table-toolbar.d.ts +5 -1
  50. package/components/table-toolbar/table-toolbar.yaml +130 -15
  51. package/components/tag/tag.class.js +13 -1
  52. package/components/text/text.a2ui.json +3 -3
  53. package/components/text/text.d.ts +5 -3
  54. package/components/text/text.yaml +3 -3
  55. package/components/textarea/textarea.class.js +9 -1
  56. package/components/tour/tour.d.ts +10 -0
  57. package/core/a11y.d.ts +22 -0
  58. package/core/a11y.js +64 -0
  59. package/core/anchor.js +9 -5
  60. package/core/element.js +1 -1
  61. package/custom-elements.json +73 -29
  62. package/dist/host.min.css +1 -1
  63. package/dist/host.sheet.js +1 -1
  64. package/dist/theme-provider.min.js +1 -1
  65. package/dist/web-components.min.css +1 -1
  66. package/dist/web-components.min.js +119 -119
  67. package/dist/web-components.sheet.js +1 -1
  68. package/package.json +1 -1
  69. package/patterns/access-requests/access-requests.examples.html +1 -1
  70. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
  71. package/patterns/agent-cost/agent-cost.examples.html +6 -6
  72. package/patterns/agent-memory/agent-memory.examples.html +3 -3
  73. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
  74. package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
  75. package/patterns/approvals/approvals.examples.html +6 -6
  76. package/patterns/audit-log/audit-log.examples.html +1 -1
  77. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
  78. package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
  79. package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
  80. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
  81. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
  82. package/patterns/diff-review/diff-review.examples.html +6 -6
  83. package/patterns/export-flow/export-flow.examples.html +2 -2
  84. package/patterns/filter-bar/filter-bar.examples.html +2 -2
  85. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
  86. package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
  87. package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
  88. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  89. package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
  90. package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
  91. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
  92. package/patterns/search-discovery/search-discovery.examples.html +2 -2
  93. package/styles/type/roles.css +29 -9
@@ -23,8 +23,12 @@ props:
23
23
  reflect: true
24
24
  items:
25
25
  description: >-
26
- JSON array of {key, label, slot?, pct?} legend items. Takes precedence
27
- over [for] when both are provided.
26
+ JSON array of {key, label, slot?, pct?, deemphasized?} legend items.
27
+ Takes precedence over [for] when both are provided. `deemphasized`
28
+ (REQ-R-007) mutes the row's swatch/label to the same muted-neutral
29
+ tier chart-ui's own internal legend uses for a de-emphasized series
30
+ (e.g. `composed@1:1`'s secondary line) — mirrored automatically from
31
+ a `[for]`-bound chart's own `.legendData`, or settable explicitly here.
28
32
  type: string
29
33
  default: ''
30
34
  shape:
@@ -38,7 +42,11 @@ props:
38
42
  - dashed
39
43
  reflect: true
40
44
  position:
41
- description: Layout hint — drives flex-direction. Actual placement follows DOM order.
45
+ description: >-
46
+ Layout hint — drives flex-direction. Actual placement follows DOM
47
+ order. Superseded by a resolved `ratio` bucket (below) whenever one
48
+ is in effect; this is the pre-ADR-0074 fallback for a legend with
49
+ neither an explicit `ratio` nor a `[for]`-bound chart.
42
50
  type: string
43
51
  default: bottom
44
52
  enum:
@@ -47,6 +55,26 @@ props:
47
55
  - left
48
56
  - right
49
57
  reflect: true
58
+ ratio:
59
+ description: >-
60
+ Chart 2.0 legend layout bucket (ADR-0074, gh#1624 plan step 5,
61
+ REQ-R-005): 3:2 renders a horizontal row, 1:1 a wrapped 2-column
62
+ grid, 2:3 a vertical stacked list. An explicit value PINS the
63
+ bucket regardless of any bound chart. Unset (default) auto-mirrors
64
+ the `[for]`-bound chart's own resolved `data-ratio-resolved` bucket
65
+ — the legend never runs its own container-query classification,
66
+ since its own box is typically a thin strip with no studied aspect
67
+ of its own. With neither a pin nor a bound chart, no bucket
68
+ resolves and `position`'s flex-direction rules apply unchanged. The
69
+ resolved bucket reflects read-only onto the host as
70
+ `data-ratio-resolved`, mirroring chart-ui's own convention.
71
+ type: string
72
+ default: ""
73
+ enum:
74
+ - "3:2"
75
+ - "1:1"
76
+ - "2:3"
77
+ reflect: true
50
78
  interactive:
51
79
  description: >-
52
80
  Canonical opt-in spelling (ADR-0063, gh#1563) — the deprecated
@@ -107,6 +135,8 @@ a2ui:
107
135
  reason: 'Composition contract.'
108
136
  - rule: 'position attribute (top|bottom|left|right) places legend relative to its chart; interactive="false" disables interactivity.'
109
137
  reason: 'Layout + interaction knobs.'
138
+ - rule: 'ratio attribute (3:2|1:1|2:3, ADR-0074) selects the legend layout shape (row/grid/list); left unset it auto-mirrors a [for]-bound chart''s own resolved ratio.'
139
+ reason: 'Ratio-responsive layout contract, gh#1624 plan step 5.'
110
140
  anti_patterns: []
111
141
  examples:
112
142
  - name: standalone-with-items
@@ -119,6 +149,16 @@ examples:
119
149
  "items": "[{\"key\":\"revenue\",\"label\":\"Revenue\",\"slot\":0},{\"key\":\"users\",\"label\":\"Users\",\"slot\":1}]"
120
150
  }
121
151
  ]
152
+ - name: ratio-pinned-grid
153
+ description: Standalone legend pinned to the 1:1 (2-col grid) layout bucket.
154
+ a2ui: >-
155
+ [
156
+ {
157
+ "id": "legend", "component": "ChartLegend",
158
+ "ratio": "1:1",
159
+ "items": "[{\"key\":\"revenue\",\"label\":\"Revenue\",\"slot\":0},{\"key\":\"users\",\"label\":\"Users\",\"slot\":1},{\"key\":\"churn\",\"label\":\"Churn\",\"slot\":2},{\"key\":\"nps\",\"label\":\"NPS\",\"slot\":3}]"
160
+ }
161
+ ]
122
162
  - name: mirror-chart-by-id
123
163
  description: Legend mirrors a named chart's series data.
124
164
  a2ui: >-
@@ -339,6 +339,9 @@
339
339
  "--combobox-match-bg": {
340
340
  "description": "Background for `<mark>` spans that highlight matched substrings"
341
341
  },
342
+ "--combobox-min-width": {
343
+ "description": "Intrinsic min-width floor on the host (default `20ch`, matching input-ui's own `--input-min-width` UA-baseline derivation) so a combobox in a shrinking flex/grid cell doesn't crush to unusable width. Set to `0` to opt back into full collapse (gh#1633)."
344
+ },
342
345
  "--combobox-option-bg-active": {
343
346
  "description": "Background for the currently active (arrow-navigated) option"
344
347
  },
@@ -41,6 +41,7 @@ import { UIFormElement } from '../../core/form.js';
41
41
  import { anchorPopover } from '../../core/anchor.js';
42
42
  import { untracked } from '../../core/signals.js';
43
43
  import { logicalSlotted } from '../../core/logical-children.js';
44
+ import { syncAutoAriaLabel } from '../../core/a11y.js';
44
45
 
45
46
  function escapeHTML(s) {
46
47
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -103,6 +104,10 @@ export class UICombobox extends UIFormElement {
103
104
  #anchorCleanup = null;
104
105
  #rafId = null;
105
106
  #instanceId = `combobox-${++cbInstanceSeq}`;
107
+ // gh#1647: tracks the aria-label VALUE this element itself last derived
108
+ // from `placeholder` (undefined when none) — value-tracking guard
109
+ // ported from tag.class.js/select.class.js (gh#1644/#1646).
110
+ #lastAutoAriaLabel = undefined;
106
111
 
107
112
  // ── Stable handler refs (so removeEventListener finds them) ──
108
113
  #onInputEvent = () => this.#handleInput();
@@ -176,14 +181,10 @@ export class UICombobox extends UIFormElement {
176
181
  this.#labelEl.style.display = this.label ? '' : 'none';
177
182
  }
178
183
 
179
- // aria-label fallback when no [label] is set.
180
- if (this.label) {
181
- this.removeAttribute('aria-label');
182
- } else if (this.placeholder) {
183
- this.setAttribute('aria-label', this.placeholder);
184
- } else {
185
- this.removeAttribute('aria-label');
186
- }
184
+ // aria-label fallback when no [label] is set (gh#1647) — never clobber
185
+ // a consumer's own aria-label (value-tracking guard, gh#1644/#1646).
186
+ const ariaSource = this.label ? '' : (this.placeholder || '');
187
+ this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, ariaSource);
187
188
 
188
189
  // Toggle clear-button visibility when [clearable].
189
190
  if (this.#clearBtn) {
@@ -6,6 +6,15 @@
6
6
  --combobox-height: var(--a-size);
7
7
  --combobox-px: var(--a-ui-px);
8
8
  --combobox-radius: var(--a-radius);
9
+ /* gh#1633 / ADIA2-9136 (adiav2#12791) — the host's own `min-width: 0`
10
+ below (the ellipsis-reset trick, kept for [data-input] inside this
11
+ floor) had no intrinsic lower bound, so a combobox in a shrinking
12
+ flex/grid cell crushed to unusable width — the same gap already
13
+ fixed on --input-min-width (this family's reference; see input.css,
14
+ gh#781) and on --select-min-width (same fix, select.css). Same
15
+ ch-based UA-baseline derivation. Override to 0 to opt back into
16
+ full collapse. */
17
+ --combobox-min-width: 20ch;
9
18
 
10
19
  /* ── Typography ── */
11
20
  --combobox-font-size: var(--a-ui-size);
@@ -62,7 +71,11 @@
62
71
  flex-direction: column;
63
72
  gap: var(--combobox-gap);
64
73
  position: relative;
65
- min-width: 0;
74
+ /* Floored, not zeroed — see --combobox-min-width above (gh#1633).
75
+ [data-input]'s own `min-width: 0` further down stays 0: that one
76
+ exists so its value text can ellipsis-truncate INSIDE this floor,
77
+ not to guard the host's own collapse. */
78
+ min-width: var(--combobox-min-width);
66
79
  /* font-family floor on the host — internal input/options inherit it. */
67
80
  font-family: var(--combobox-font-family, var(--a-font-family-ui));
68
81
  font-size: var(--combobox-font-size);
@@ -326,9 +339,17 @@ combobox-ui [data-listbox] [role="group"] [data-group-label] {
326
339
  white-space: nowrap;
327
340
  }
328
341
 
329
- combobox-ui [data-empty],
330
- combobox-ui [data-loading],
331
- combobox-ui [data-footer] {
342
+ /* Scoped to `[data-listbox]`'s own markers — gh#1700, same shape as
343
+ gh#1697/#1698. The input span (`[data-input]`) toggles the SAME
344
+ `data-empty` attribute for an unrelated reason (gating its own
345
+ `::before` placeholder pseudo) — a bare, unscoped `combobox-ui
346
+ [data-empty]` selector matched BOTH uses, and (being (0,1,1)) outranked
347
+ `[data-input]`'s own (0,1,0) padding/color/font-size, one property away
348
+ from repeating the command-ui leak. Scoping to the listbox container
349
+ removes the collision at its root. */
350
+ combobox-ui [data-listbox] [data-empty],
351
+ combobox-ui [data-listbox] [data-loading],
352
+ combobox-ui [data-listbox] [data-footer] {
332
353
  padding: var(--a-space-2) var(--a-ui-px);
333
354
  color: var(--a-ui-text-muted);
334
355
  font-size: var(--a-ui-size);
@@ -257,6 +257,12 @@ tokens:
257
257
  tinted selected row.
258
258
  --combobox-match-bg:
259
259
  description: Background for `<mark>` spans that highlight matched substrings
260
+ --combobox-min-width:
261
+ description: >-
262
+ Intrinsic min-width floor on the host (default `20ch`, matching
263
+ input-ui's own `--input-min-width` UA-baseline derivation) so a
264
+ combobox in a shrinking flex/grid cell doesn't crush to unusable
265
+ width. Set to `0` to opt back into full collapse (gh#1633).
260
266
  requiredIcons:
261
267
  - caret-down
262
268
  - x-circle
@@ -221,7 +221,23 @@
221
221
  }
222
222
 
223
223
  /* ── Empty state ── */
224
- [data-empty] {
224
+ /* Scoped to `[slot="list"]`'s own `[data-empty]` — gh#1697. The search
225
+ input (`& > header [slot="input"]`) toggles the SAME `data-empty`
226
+ attribute for an unrelated reason (gating its own `::before`
227
+ placeholder pseudo, per REQ above) — a bare, unscoped `[data-empty]`
228
+ selector matched BOTH uses, so this rule's `text-align: center` leaked
229
+ onto the input span whenever it was empty, centering the placeholder
230
+ (and, structurally, any text a consumer's own script inserted while
231
+ the attribute was still present). `padding`/`color` never leaked in
232
+ practice — `& > header [slot="input"]`'s own compound selector
233
+ (header-descendant + attribute, (0,1,1)) already outranks the bare
234
+ `[data-empty]` (0,1,0) for those two properties — but `text-align` had
235
+ no competing declaration anywhere else in this file, so the leaked
236
+ value won by default with nothing to lose against. Scoping the
237
+ selector to the list container removes the collision at its root
238
+ instead of only patching the one property a screenshot happened to
239
+ catch. */
240
+ [slot="list"] [data-empty] {
225
241
  padding: var(--command-py) var(--command-px);
226
242
  color: var(--command-empty-fg);
227
243
  text-align: center;
@@ -56,6 +56,8 @@ export type ContextMenuSelectEvent = CustomEvent<ContextMenuSelectEventDetail>;
56
56
  export class UIContextMenu extends UIElement {
57
57
  /** CSS selector(s) for target element(s). Empty = use default-slot child. */
58
58
  for: string;
59
+ /** Long-press duration (ms) on touch devices to open the menu. */
60
+ longPressMs: number;
59
61
  /** Programmatic open state. Set true to open at target center. */
60
62
  open: boolean;
61
63
 
@@ -53,6 +53,11 @@
53
53
  "component": {
54
54
  "const": "Heatmap"
55
55
  },
56
+ "data": {
57
+ "description": "Cell data as a JSON array of `{ r, c, v, label? }` objects, or set the `.data` property directly with an already-parsed array. Parsed on connect if the attribute is present and `.data` hasn't already been set programmatically.",
58
+ "type": "string",
59
+ "default": ""
60
+ },
56
61
  "noLegend": {
57
62
  "description": "Hide the Less/More legend strip",
58
63
  "type": "boolean",
@@ -27,6 +27,8 @@ export class UIHeatmap extends UIElement {
27
27
  colorScheme: 'accent' | 'success' | 'warning' | 'danger' | 'info' | 'data-ramp';
28
28
  /** Column count */
29
29
  cols: number;
30
+ /** Cell data as a JSON array of `{ r, c, v, label? }` objects, or set the `.data` property directly with an already-parsed array. Parsed on connect if the attribute is present and `.data` hasn't already been set programmatically. */
31
+ data: string;
30
32
  /** Hide the Less/More legend strip */
31
33
  noLegend: boolean;
32
34
  /** Row count */
@@ -25,6 +25,14 @@ props:
25
25
  enum:
26
26
  - square
27
27
  - wide
28
+ data:
29
+ description: >-
30
+ Cell data as a JSON array of `{ r, c, v, label? }` objects, or set the
31
+ `.data` property directly with an already-parsed array. Parsed on
32
+ connect if the attribute is present and `.data` hasn't already been
33
+ set programmatically.
34
+ type: string
35
+ default: ""
28
36
  colorScheme:
29
37
  description: Color ramp
30
38
  type: string
@@ -166,7 +166,7 @@
166
166
  },
167
167
  {
168
168
  "fix": "{\"component\": \"CalendarPicker\", \"precision\": \"minute\"}\n",
169
- "why": "Same gap as \"date\" — no native datetime picker rendered. Use\nCalendarPicker with precision=\"minute\" for combined date + time\ninput (DatetimePicker was a deprecated thin alias, cut in 0.8.43,\ngh#1617 — CalendarPicker is the sole tag now).\n",
169
+ "why": "Same gap as \"date\" — no native datetime picker rendered. Use\nCalendarPicker with precision=\"minute\" for combined date + time\ninput (DatetimePicker was a deprecated thin alias, cut in 0.8.43,\ngh#1617 — CalendarPicker is the sole A2UI component now).\n",
170
170
  "wrong": "{\"component\": \"Input\", \"type\": \"datetime-local\"}\n"
171
171
  },
172
172
  {
@@ -186,7 +186,7 @@
186
186
  },
187
187
  {
188
188
  "fix": "{\"component\": \"ColorInput\"}\n",
189
- "why": "No native color swatch/picker rendered — just a plain text\nsurface. Use ColorInput for a compact form-bearing swatch +\npopover, or ColorArea directly for the full OKLCH picker surface\n(ColorPicker was a deprecated thin alias, cut in 0.8.43, gh#1617 —\nColorArea is the sole tag now).\n",
189
+ "why": "No native color swatch/picker rendered — just a plain text\nsurface. Use ColorInput for a compact form-bearing swatch +\npopover, or ColorArea directly for the full OKLCH picker surface\n(ColorPicker was a deprecated thin alias, cut in 0.8.43, gh#1617 —\nColorArea is the sole A2UI component now).\n",
190
190
  "wrong": "{\"component\": \"Input\", \"type\": \"color\"}\n"
191
191
  },
192
192
  {
@@ -53,6 +53,7 @@
53
53
  import { UIFormElement } from '../../core/form.js';
54
54
  import { isIconName, whenIconRegistryReady } from '../../core/icons.js';
55
55
  import { logicalSlotted } from '../../core/logical-children.js';
56
+ import { syncAutoAriaLabel } from '../../core/a11y.js';
56
57
 
57
58
  const renderAffix = (v) => isIconName(v)
58
59
  ? `<icon-ui name="${v}"></icon-ui>`
@@ -106,6 +107,12 @@ export class UIInput extends UIFormElement {
106
107
  #cachedGroup = '';
107
108
  #cachedSepFor = null;
108
109
  static #labelSeq = 0;
110
+ // gh#1647: tracks the aria-label VALUE this element itself last derived
111
+ // from `placeholder` (undefined when none), so re-renders refresh/clear
112
+ // OUR name without ever clobbering a consumer's own aria-label —
113
+ // value-tracking guard ported from tag.class.js/select.class.js
114
+ // (gh#1644/#1646).
115
+ #lastAutoAriaLabel = undefined;
109
116
 
110
117
  // Hold-to-repeat tuning. Initial delay before autorepeat begins, and the
111
118
  // interval between repeats. Values match the cadence of the native
@@ -471,13 +478,11 @@ export class UIInput extends UIFormElement {
471
478
  // Re-resolve prefix/suffix from the live property each render (FEEDBACK-93).
472
479
  this.#syncAffixSlots();
473
480
 
474
- if (this.label) {
475
- this.removeAttribute('aria-label');
476
- } else if (this.placeholder) {
477
- this.setAttribute('aria-label', this.placeholder);
478
- } else {
479
- this.removeAttribute('aria-label');
480
- }
481
+ // A11y (gh#1647): a visible `label` already names the field; otherwise
482
+ // fall back to `placeholder` — but never clobber a consumer's own
483
+ // aria-label (value-tracking guard, gh#1644/#1646).
484
+ const ariaSource = this.label ? '' : (this.placeholder || '');
485
+ this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, ariaSource);
481
486
 
482
487
  if (this.#isNumberMode) {
483
488
  const n = this.valueAsNumber;
@@ -264,9 +264,10 @@ a2ui:
264
264
  <input type="password"> for masking (per ADR-0025).
265
265
  - >-
266
266
  Wrap <input-ui> in <field-ui label="…" hint="…" error="…"> for
267
- the canonical stacked label / hint / error chrome. The inline
268
- [label] / [hint] / [error] props are also supported on the
269
- primitive for compact use.
267
+ the canonical stacked label / hint / error chrome. input-ui itself
268
+ only supports inline [label] / [error] (gh#1669 [hint] is a
269
+ field-ui-only prop; input-ui has no equivalent) — use field-ui
270
+ when a hint line is needed.
270
271
  - >-
271
272
  Form participation is implicit via UIFormElement. Set [name] for
272
273
  FormData submission; [required] / [disabled] / [readonly]
@@ -309,7 +310,7 @@ anti_patterns:
309
310
  Same gap as "date" — no native datetime picker rendered. Use
310
311
  CalendarPicker with precision="minute" for combined date + time
311
312
  input (DatetimePicker was a deprecated thin alias, cut in 0.8.43,
312
- gh#1617 — CalendarPicker is the sole tag now).
313
+ gh#1617 — CalendarPicker is the sole A2UI component now).
313
314
  fix: |
314
315
  {"component": "CalendarPicker", "precision": "minute"}
315
316
  - wrong: |
@@ -342,7 +343,7 @@ anti_patterns:
342
343
  surface. Use ColorInput for a compact form-bearing swatch +
343
344
  popover, or ColorArea directly for the full OKLCH picker surface
344
345
  (ColorPicker was a deprecated thin alias, cut in 0.8.43, gh#1617 —
345
- ColorArea is the sole tag now).
346
+ ColorArea is the sole A2UI component now).
346
347
  fix: |
347
348
  {"component": "ColorInput"}
348
349
  - wrong: |
@@ -42,6 +42,8 @@ export class UIIntegrationCard extends UIElement {
42
42
  description: string;
43
43
  /** Disables the action button. */
44
44
  disabled: boolean;
45
+ /** When `status="error"`, the message to show below the description. Ignored for other statuses. */
46
+ errorMessage: string;
45
47
  /** Logo URL (preferred — renders as <img>) or icon name (renders as <icon-ui>). URLs are sniffed by presence of `/`. */
46
48
  logo: string;
47
49
  /** Display name shown as the card title. Required. */
@@ -41,11 +41,6 @@
41
41
  "type": "boolean",
42
42
  "default": false
43
43
  },
44
- "port-size": {
45
- "description": "Port indicator dot size in pixels",
46
- "type": "number",
47
- "default": 10
48
- },
49
44
  "portSize": {
50
45
  "description": "Port indicator size",
51
46
  "type": "number",
@@ -56,21 +51,11 @@
56
51
  "type": "boolean",
57
52
  "default": false
58
53
  },
59
- "show-ports": {
60
- "description": "Show port indicator dots. Defaults to true when editable",
61
- "type": "boolean",
62
- "default": false
63
- },
64
54
  "showPorts": {
65
55
  "description": "Show port indicators",
66
56
  "type": "boolean",
67
57
  "default": false
68
58
  },
69
- "stroke-width": {
70
- "description": "Stroke width in pixels",
71
- "type": "number",
72
- "default": 2
73
- },
74
59
  "strokeWidth": {
75
60
  "description": "Line stroke width",
76
61
  "type": "number",
@@ -30,10 +30,6 @@ props:
30
30
  description: Allow interactive creation/deletion of connections via drag
31
31
  type: boolean
32
32
  default: false
33
- port-size:
34
- description: Port indicator dot size in pixels
35
- type: number
36
- default: 10
37
33
  portSize:
38
34
  description: Port indicator size
39
35
  type: number
@@ -44,19 +40,11 @@ props:
44
40
  type: boolean
45
41
  default: false
46
42
  reflect: true
47
- show-ports:
48
- description: Show port indicator dots. Defaults to true when editable
49
- type: boolean
50
- default: false
51
43
  showPorts:
52
44
  description: Show port indicators
53
45
  type: boolean
54
46
  default: false
55
47
  attribute: show-ports
56
- stroke-width:
57
- description: Stroke width in pixels
58
- type: number
59
- default: 2
60
48
  strokeWidth:
61
49
  description: Line stroke width
62
50
  type: number
@@ -87,6 +87,16 @@ export class UINumberFormat extends UIElement {
87
87
  this.textContent = text;
88
88
  // Mirror raw value via aria-label so screen readers can read the
89
89
  // underlying number alongside the formatted glyphs.
90
+ //
91
+ // gh#1647 judgment call (triage-corrections comment, 2026-08-18):
92
+ // this aria-label is intentional component-owned chrome, not a
93
+ // consumer-overridable name — it mirrors the RAW underlying value
94
+ // ("42% (raw: 0.42)") specifically for a11y clarity, so a consumer
95
+ // override would likely defeat that purpose. Deliberately NOT ported
96
+ // to the value-tracking `#lastAutoAriaLabel` guard (core/a11y.js)
97
+ // that the other five gh#1647 primitives got — kept unconditional,
98
+ // auto-deriving on every render. Revisit only if a stated consumer
99
+ // need for overriding it surfaces.
90
100
  if (text && Number.isFinite(Number(this.value))) {
91
101
  this.setAttribute('aria-label', `${text} (raw: ${this.value})`);
92
102
  } else {
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import { UIElement } from '../../core/element.js';
25
+ import { KEY_MAP } from '../../core/template.js';
25
26
 
26
27
  export class UIPagination extends UIElement {
27
28
  static properties = {
@@ -48,10 +49,32 @@ export class UIPagination extends UIElement {
48
49
 
49
50
  connected() {
50
51
  if (!this.#nav) {
51
- this.#nav = document.createElement('nav');
52
- this.#nav.setAttribute('slot', 'nav');
53
- this.#nav.setAttribute('aria-label', 'Pagination');
54
- this.appendChild(this.#nav);
52
+ // Adopt a pre-existing server-rendered <nav slot="nav"> instead of
53
+ // unconditionally appending a fresh one — first-connect adoption
54
+ // (gh#1687). Steady-state reconcile is unaffected; this only guards
55
+ // the construction-time #nav field being null on upgrade.
56
+ this.#nav = this.querySelector(':scope > nav[slot="nav"]');
57
+ if (this.#nav) {
58
+ // Adopting an existing nav is only half the fix — reconcile()'s
59
+ // own keyed-diff (element.js) keys off a WeakMap-shaped
60
+ // `parent[KEY_MAP]` populated by THIS element's own prior render
61
+ // calls, which a freshly-parsed SSR fragment never has. Without
62
+ // seeding it here, the very first render treats every adopted
63
+ // child as unrecognized, stamps a full set of BRAND NEW nodes
64
+ // alongside them, and never removes the orphaned originals —
65
+ // silently doubling the button count inside the (correctly
66
+ // singular) adopted <nav>. Positionally zip the adopted
67
+ // children against the SAME key order #buildRange would
68
+ // produce right now; a shape mismatch (stale/hand-edited SSR
69
+ // markup) is left unseeded so reconcile falls back to its
70
+ // normal rebuild-from-scratch behavior instead of miskeying.
71
+ this.#seedKeyMapFromAdoptedNav();
72
+ } else {
73
+ this.#nav = document.createElement('nav');
74
+ this.#nav.setAttribute('slot', 'nav');
75
+ this.#nav.setAttribute('aria-label', 'Pagination');
76
+ this.appendChild(this.#nav);
77
+ }
55
78
  }
56
79
 
57
80
  if (!this.#bound) {
@@ -69,6 +92,67 @@ export class UIPagination extends UIElement {
69
92
  this.#bound = false;
70
93
  }
71
94
 
95
+ // Review finding (PR #1692) — the tag/marker a given item.type produces
96
+ // via #createItem(): ellipsis is a bare <span data-ellipsis>; prev/next/
97
+ // page are all <button-ui> distinguished by their own data-* marker
98
+ // (page additionally carries data-value). A same-LENGTH adopted child
99
+ // that doesn't match this shape (e.g. a plain <span> standing in for a
100
+ // page-cell <button-ui>) would still get SEEDED by a count-only check,
101
+ // then #updateItem() — which only sets attributes, never replaces the
102
+ // element — leaves it permanently the wrong tag. Checking shape here
103
+ // is what makes the fallback in #seedKeyMapFromAdoptedNav trustworthy.
104
+ #childMatchesItem(el, item) {
105
+ if (item.type === 'ellipsis') {
106
+ return el.tagName === 'SPAN' && el.hasAttribute('data-ellipsis');
107
+ }
108
+ if (el.tagName !== 'BUTTON-UI') return false;
109
+ if (item.type === 'prev') return el.hasAttribute('data-prev');
110
+ if (item.type === 'next') return el.hasAttribute('data-next');
111
+ return el.hasAttribute('data-page') && el.dataset.value === String(item.value);
112
+ }
113
+
114
+ // gh#1687 — see the call site's comment in connected(). Positionally
115
+ // zips the adopted nav's existing children against the key order the
116
+ // very next render() will compute, seeding reconcile()'s own
117
+ // `parent[KEY_MAP]` (element.js) so that render finds every item
118
+ // already present instead of stamping a duplicate full set.
119
+ #seedKeyMapFromAdoptedNav() {
120
+ const page = Math.max(1, Math.min(this.page, this.total));
121
+ const total = Math.max(1, this.total);
122
+ const siblings = Math.max(0, this.siblings);
123
+ const range = this.#buildRange(page, total, siblings);
124
+ const children = [...this.#nav.children];
125
+ const shapeMatches = children.length === range.length
126
+ && range.every((item, i) => this.#childMatchesItem(children[i], item));
127
+ if (!shapeMatches) {
128
+ // Review finding (PR #1692, two rounds) — a bare return here left
129
+ // the mismatch case worse off than doing nothing: reconcile()'s
130
+ // own cleanup loop only removes entries it finds IN
131
+ // `parent[KEY_MAP]`, so an unseeded map means every existing
132
+ // child is invisible to it — the next render() then stamps a
133
+ // full fresh set AND the stale adopted children survive alongside
134
+ // it (reproduced: 3 stale placeholder buttons + a correct fresh
135
+ // 9-item render = 12 nodes). Length alone isn't a reliable
136
+ // correlation signal either — a same-length fixture with the
137
+ // WRONG tag/marker per position (e.g. N bare <span>s standing in
138
+ // for N <button-ui> page-cells) would pass a count-only check and
139
+ // get seeded, then #updateItem() — attribute-only, never a tag
140
+ // swap — leaves it permanently wrong. Either kind of mismatch
141
+ // means the adopted markup can't be trusted to correlate with the
142
+ // next render's key order at all — abandon adoption for real:
143
+ // clear the stale children so the fallback is an actual clean
144
+ // rebuild, the same shape #nav would be in if it had just been
145
+ // freshly created.
146
+ this.#nav.replaceChildren();
147
+ return;
148
+ }
149
+ const map = new Map();
150
+ for (let i = 0; i < range.length; i++) {
151
+ map.set(range[i].key, children[i]);
152
+ }
153
+ this.#nav[KEY_MAP] = map;
154
+ }
155
+
72
156
  render() {
73
157
  if (!this.#nav) return;
74
158
 
@@ -13,6 +13,8 @@
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
15
  export class UIPopover extends UIElement {
16
+ /** Size the content panel to the trigger's width instead of max-content. Opt-in — the default (unset) keeps every existing consumer's content-sized panel unchanged. */
17
+ matchWidth: boolean;
16
18
  /** Pixel offset between the anchor and the popover. Renamed from `[gap]` (gh#1335) — a raw-px number can't share a name with the global Scale-grammar `[gap]` attribute (ADR-0053). */
17
19
  offset: number;
18
20
  /** Controls visibility via showPopover()/hidePopover() */
@@ -28,6 +28,7 @@
28
28
  */
29
29
 
30
30
  import { UIElement } from '../../core/element.js';
31
+ import { syncAutoAriaLabel } from '../../core/a11y.js';
31
32
 
32
33
  export class UIProgress extends UIElement {
33
34
  static properties = {
@@ -42,6 +43,10 @@ export class UIProgress extends UIElement {
42
43
  #fill = null;
43
44
  #labelEl = null;
44
45
  #metaEl = null;
46
+ // gh#1647: tracks the aria-label VALUE this element itself last derived
47
+ // from `label`/`meta` (undefined when none) — value-tracking guard
48
+ // ported from tag.class.js/select.class.js (gh#1644/#1646).
49
+ #lastAutoAriaLabel = undefined;
45
50
 
46
51
  connected() {
47
52
  this.setAttribute('role', 'progressbar');
@@ -111,12 +116,12 @@ export class UIProgress extends UIElement {
111
116
  }
112
117
  }
113
118
 
114
- // ARIA: label the control so screen readers announce "CPU, 42 percent".
115
- if (this.label) {
116
- this.setAttribute('aria-label', this.meta ? `${this.label}, ${this.meta}` : this.label);
117
- } else {
118
- this.removeAttribute('aria-label');
119
- }
119
+ // ARIA: label the control so screen readers announce "CPU, 42 percent"
120
+ // — never clobbering a consumer's own aria-label (gh#1647,
121
+ // value-tracking guard ported from tag.class.js/select.class.js,
122
+ // gh#1644/#1646).
123
+ const ariaSource = this.label ? (this.meta ? `${this.label}, ${this.meta}` : this.label) : '';
124
+ this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, ariaSource);
120
125
  }
121
126
 
122
127
  disconnected() {
@@ -25,6 +25,7 @@
25
25
  */
26
26
 
27
27
  import { UIFormElement } from '../../core/form.js';
28
+ import { syncAutoAriaLabel } from '../../core/a11y.js';
28
29
 
29
30
  export class UIRange extends UIFormElement {
30
31
  // Opt out of UIFormElement's per-control `label` deprecation warning.
@@ -52,6 +53,10 @@ export class UIRange extends UIFormElement {
52
53
  #dragging = false;
53
54
  #startX = 0;
54
55
  #startVal = 0;
56
+ // gh#1647: tracks the aria-label VALUE this element itself last derived
57
+ // from `label` (undefined when none) — value-tracking guard ported from
58
+ // tag.class.js/select.class.js (gh#1644/#1646).
59
+ #lastAutoAriaLabel = undefined;
55
60
 
56
61
  connected() {
57
62
  super.connected();
@@ -96,7 +101,10 @@ export class UIRange extends UIFormElement {
96
101
  this.setAttribute('aria-valuemax', this.max);
97
102
  this.setAttribute('aria-valuenow', this.value);
98
103
  this.setAttribute('aria-valuetext', `${this.value}${this.suffix ? ' ' + this.suffix : ''}`);
99
- this.setAttribute('aria-label', this.label || '');
104
+
105
+ // A11y (gh#1647): never clobber a consumer's own aria-label
106
+ // (value-tracking guard, gh#1644/#1646).
107
+ this.#lastAutoAriaLabel = syncAutoAriaLabel(this, this.#lastAutoAriaLabel, this.label || '');
100
108
 
101
109
  this.syncValue(String(this.value));
102
110
  }