@adia-ai/web-components 0.7.5 → 0.7.6

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 (66) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/components/action-list/action-list.a2ui.json +2 -2
  3. package/components/action-list/action-list.yaml +2 -2
  4. package/components/badge/badge.yaml +1 -0
  5. package/components/block/block.class.js +20 -4
  6. package/components/block/block.css +18 -13
  7. package/components/button/button.a2ui.json +1 -2
  8. package/components/button/button.css +17 -6
  9. package/components/button/button.yaml +1 -0
  10. package/components/card/card.yaml +1 -0
  11. package/components/chart/chart.a2ui.json +1 -2
  12. package/components/chart/chart.d.ts +1 -1
  13. package/components/chart/chart.yaml +1 -0
  14. package/components/check/check.a2ui.json +0 -3
  15. package/components/check/check.yaml +0 -2
  16. package/components/combobox/combobox.a2ui.json +1 -2
  17. package/components/combobox/combobox.yaml +1 -0
  18. package/components/command/command.a2ui.json +0 -3
  19. package/components/command/command.class.js +19 -1
  20. package/components/command/command.yaml +0 -2
  21. package/components/context-menu/context-menu.class.js +1 -0
  22. package/components/description-list/description-list.a2ui.json +1 -2
  23. package/components/description-list/description-list.d.ts +1 -1
  24. package/components/description-list/description-list.yaml +1 -0
  25. package/components/field/field.class.js +1 -0
  26. package/components/fields/fields.class.js +1 -0
  27. package/components/grid/grid.yaml +2 -0
  28. package/components/input/input.a2ui.json +2 -14
  29. package/components/input/input.yaml +2 -0
  30. package/components/integration-card/integration-card.class.js +1 -0
  31. package/components/list/list.class.js +1 -0
  32. package/components/list/list.yaml +1 -0
  33. package/components/menu/menu.class.js +1 -0
  34. package/components/menu/menu.css +14 -2
  35. package/components/pipeline-status/pipeline-status.yaml +1 -0
  36. package/components/radio/radio.a2ui.json +0 -3
  37. package/components/radio/radio.yaml +0 -2
  38. package/components/rating/rating.yaml +1 -0
  39. package/components/search/search.a2ui.json +1 -8
  40. package/components/search/search.yaml +1 -5
  41. package/components/select/select.a2ui.json +1 -2
  42. package/components/select/select.class.js +46 -1
  43. package/components/select/select.test.js +33 -0
  44. package/components/select/select.yaml +2 -0
  45. package/components/slider/slider.a2ui.json +0 -3
  46. package/components/slider/slider.yaml +0 -2
  47. package/components/swatch/swatch.class.js +1 -0
  48. package/components/table/table.a2ui.json +2 -4
  49. package/components/table/table.d.ts +2 -2
  50. package/components/table/table.yaml +2 -0
  51. package/components/tabs/tabs.yaml +1 -0
  52. package/components/tag/tag.yaml +1 -0
  53. package/components/toggle-group/toggle-group.yaml +1 -0
  54. package/components/toolbar/toolbar.class.js +1 -0
  55. package/components/tree/tree.a2ui.json +2 -2
  56. package/components/tree/tree.yaml +2 -2
  57. package/dist/host.min.css +1 -1
  58. package/dist/web-components.min.css +1 -1
  59. package/dist/web-components.min.js +28 -28
  60. package/index.css +11 -17
  61. package/package.json +1 -1
  62. package/styles/api/sizing.css +18 -6
  63. package/styles/foundation/space.css +33 -0
  64. package/styles/host.css +20 -22
  65. package/styles/index.css +14 -17
  66. package/styles/verse.css +14 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.7.6] — 2026-06-02
4
+
5
+ ### Added
6
+
7
+ - **`[padding]` / `[margin]` named scale (`xs`–`xl`) — complete + parametric.** Previously the named padding/margin vocabulary was only `sm`/`md`/`lg` (literal `--a-space-4`/`6`/`8`). Now the full `xs`–`xl` set, each `calc(--a-space-N × --a-{padding,margin}-k)` — mirroring the `[gap]` scale's `value × --a-*-k` form. New `@property --a-padding-k` / `--a-margin-k` knobs (`<number>`, init 1, inherits — a root/provider-level control, subtree-inert like `--a-gap-k`/`--a-density`) retune all named padding/margin at once, per-dimension. `sm`/`md`/`lg` keep their prior values (16/24/32px @k=1) — **behavior-neutral**; `xs` (8px) + `xl` (40px) are new. Integer rungs (`[padding="4"]`) stay literal space-rungs (k-independent). Composes on `--a-density`. Verified: 8/16/24/32/40 @k=1; root `--a-padding-k:2` → md 48 / xl 80; `--a-margin-k` independent of padding. Files: `styles/foundation/space.css`, `styles/api/sizing.css`.
8
+
9
+ ### Changed
10
+
11
+ - **`styles/host.css` is now foundation-only (tokens + resets + page-frame); `styles/index.css` becomes the primitives barrel.** Previously `host.css` bundled the full library (tokens + all 120 component styles + resets, via `index.css`) plus the page-frame. Now: `host.css` = `tokens.css` + `resets.css` + `:where(html)` page-frame only; `styles/index.css` = `components.css` wrapped in `layer(components)` (the primitives barrel). Pages link both in order. The package-root `index.css` (`@adia-ai/web-components/css`) imports both and preserves the existing full-stack published entry — no change for consumers using the package entry. All 119 component dev shells and `site/index.html` updated. **Migration for `host.css`-only consumers:** add `<link rel="stylesheet" href="@adia-ai/web-components/styles/index.css" />` after the `host.css` link.
12
+ - **`<block-ui>` padding/margin unified with the global `[padding]`/`[margin]` scale — now parametric (behavior change).** `block-ui` carried its own literal padding/margin scale (`none`/`xs`/`sm`/`md`/`lg`/`xl` = `0`/`--a-space-1/2/4/6/10` = 0/4/8/16/24/40px, k-independent), duplicated across `block.css` (static `:scope[padding=…]` rules) and `block.class.js` `_SPACE` (the `@bp` responsive path), and diverging from the global `[padding]`/`[margin]` attribute API — `<block-ui padding="md">` rendered 16px while `[padding="md"]` is 24px (same vocabulary, different sizes). Both paths now reference the parametric `--a-{padding,margin}-{xs..xl}` tokens (`foundation/space.css`), so the two grammars AGREE (the same "one named scale" principle as the `[gap]` unification), `block-ui` tracks the `--a-padding-k`/`--a-margin-k` knobs, and the scale is no longer duplicated. **Behavior change** — `block-ui` spacing grows at `xs`/`sm`/`md`/`lg`: `xs` 4→8, `sm` 8→16, **`md` (the default) 16→24**, `lg` 24→32px; `xl` (40) + `none` (0) unchanged. Verified by computed-style probe (8/16/24/32/40 @k=1, default 24, `--a-padding-k:2` → md 48, `@bp` responsive + margins all correct, 0 console errors) + dev-shell visual QA. Files: `components/block/block.css`, `components/block/block.class.js`. **Migration:** a `<block-ui>` that relied on the prior tighter spacing should step the named value one rung down (`md`→`sm`, `lg`→`md`, …) or use the integer rungs (`padding="4"` = literal 16px, k-independent).
13
+ - **CDN bundles rebuilt** — `dist/host.min.css`, `dist/web-components.min.css`, and `dist/web-components.min.js` regenerated so the barrel split + `<block-ui>` + parametric `[padding]`/`[margin]` + `[verse]` re-scaling reach `@adia-ai/web-components@0.7` CDN consumers.
14
+
15
+ ### Fixed
16
+
17
+ - **`[verse]` register now scales icon + caret sizes down with its type + controls.** The compact `[verse]` register (`styles/verse.css`) shifted inset, gap, radius, control-size (`--a-size`), and every type role down — but left `--a-icon-size` (16px @md) and `--a-caret-size` (14px @md) at their regular-register values. So in a verse context a `<select-ui>` caret (an `<icon-ui>` reading `--a-icon-size`) stayed 16×16 against verse's 12px text in a 43px-tall control, and form-element chrome icons read full size — visually oversized. Added a one-tier-down icon/caret scale to `[verse]`, mirroring how it already re-implements `--a-size`: base `[verse]` = 14/12px (= regular-`sm`), `[verse][size="lg"]` = 16/14px (= regular-`md`), `[verse][size="sm"]` = 12/10px. The explicit `[size]` tier rules are **required** because the global `[size]` icon/caret rules live in the `utilities` layer, which verse's `context` layer shadows (same reason `--a-size` needs them). Covers both `<icon-ui>`-based carets (select / combobox, via `--a-icon-size`) and CSS-drawn carets (calendar-grid / calendar-picker / nav-group / pane, via `--a-caret-size`). Verified by computed-style + render-box probe: verse(md) caret 16→14px, verse-`sm` 14→12px, verse-`lg` 20→16px; regular register unchanged. File: `styles/verse.css`.
18
+ - **`<command-ui>` now parses `.map()`/`repeat()`-rendered `<option>`/`<optgroup>` palette items (5th sighting of the `display:contents` wrapper trap).** `#parseOptions()` walked `this.children` directly, so options rendered via `.map()` arrived nested in the template engine's `display:contents`/`role="presentation"` wrapper span and were missed — leaving an empty palette ("No results found.") when the consumer interpolated its option list. New `#logicalOptionChildren()` walks direct children and pierces wrapper spans, mirroring `select-ui#logicalOptionChildren` (FB-98). Eight other `for (const x of this.children)` loops across 8 components (context-menu, field, fields, integration-card, list, menu, swatch, toolbar) were audited and marked `// wrapper-trap-ok` — all read component-stamped or single-literal children that are never consumer-interpolated. `audit:wrapper-trap:strict` added to `check:dogfood-audits:strict`; the audit now reports 0 un-allowlisted loops. File: `components/command/command.class.js`.
19
+ - **`<select-ui>` now parses `.map()`/`repeat()`-rendered `<option>` children (FB-98).** `#parseOptions()` walked `this.children` and matched only `tagName === 'OPTION'`/`'OPTGROUP'`, so interpolated options — each nested in the template engine's `display:contents`/`role="presentation"` wrapper span (`core/template.js` Array branch) — were invisible, leaving the popover empty ("No options"); static-literal options worked (direct children, no wrapper). New `#logicalOptionChildren()` collects `<option>`/`<optgroup>` direct OR nested inside the wrapper spans (skipping component-stamped `[slot]` children), and a re-entrancy-guarded `MutationObserver` re-parses options that arrive after `connected()` (the template's `update()` pass) so the empty-state clears. The same `display:contents` wrapper trap fixed for `menu-ui#show()` (FB-92) and `tree-item-ui#stamp()` (FB-96), now in the third component; mirrors tree's `#logicalSlotChildren`. Static-literal selects + the FB-36 `<option selected>` initial-value path are unchanged. +2 regression tests. File: `components/select/select.class.js`. (Tokens Studio / color-app FB-98)
20
+ - **`<menu-ui>` — a closed declarative menu no longer takes layout space (FB-97).** The closed-state hide rule (`menu.css`) used a child combinator (`:scope > menu-item-ui`), so `.map()`/`repeat()`-rendered items — nested in the template engine's `display:contents`/`role="presentation"` wrapper span — escaped it on the initial (never-opened) render and laid out in flow, ballooning a compact menu to its widest item's width (a 388px file menu displaced a centered toolbar tab-row until a tab became un-clickable). Added wrapper-piercing descendant selectors (`:scope > [role="presentation"] menu-item-ui`, + `menu-divider-ui`); safe vs. the open state since `#show()` appendChild's the bare items into `[data-menu-popover]` (never the wrappers). Verified: a closed `.map()` menu collapses 388px → trigger width (~41px), items `display:none`, neighbour un-displaced; open still renders all items. The CSS instance of the FB-92/96/98 `display:contents` wrapper trap; makes the FB-92/95 "render menu items declaratively" guidance safe for compact menus. File: `components/menu/menu.css`. (Tokens Studio / color-app FB-97)
21
+ - **`button.yaml` `aria-label` prop marked `dynamic: true` — clears dogfood drift.** `aria-label` is a native ARIA attribute managed via `setAttribute` in `connected()` and deliberately kept out of `static properties` (would clobber native `ariaLabel` reflection via `installProps`). The `static-properties-vs-yaml` dogfood audit flagged the yaml declaration as a drift; `dynamic: true` is the established opt-out (same pattern as the `text` prop). `button.a2ui.json` regenerated (`aria-label` → `DynamicString $ref`); dogfood advisory count: 43 → 42.
22
+ - **`<button-ui>` trailing slot kbd-pill now tracks the button's label color on every variant.** A `<kbd-ui slot="trailing">` pill rendered `kbd-ui`'s dark default even on a primary (white-label) button. `kbd-ui` styles its own `color`/`background`/`border` inside `@scope (kbd-ui)`, which wins over the button's `[slot="trailing"]` rule by **scope proximity** (closer scope root) — so setting `color` (even `inherit`) on the slot is inert. Fix: the button overrides `kbd-ui`'s own *tokens* — `[slot="trailing"] { --kbd-fg: currentColor; --kbd-bg / --kbd-border: color-mix(in oklab, currentColor …, transparent) }`. `kbd-ui` declares those tokens at `:where(:scope)` (specificity 0,0,0), so the button's `[slot]` decls (0,1,0) win on **specificity — which the cascade resolves before proximity** — and the pill (text + translucent fill + border) tracks the label: white on primary/colored fills, dark on outline/ghost. Verified on a fresh serve (kbd computed `color` == button label across variants). File: `components/button/button.css`.
23
+
3
24
  ## [0.7.5] — 2026-06-02
4
25
 
5
26
  ### Fixed
@@ -66,8 +66,8 @@
66
66
  "Button"
67
67
  ],
68
68
  "slots": {
69
- "default (action-item-ui children)": {
70
- "description": "Child content region for the `default (action-item-ui children)` slot."
69
+ "default": {
70
+ "description": "Holds the list's `<action-item-ui>` children (the action items)."
71
71
  }
72
72
  },
73
73
  "states": [
@@ -28,8 +28,8 @@ events:
28
28
  type: object
29
29
  description: The triggering list-item element.
30
30
  slots:
31
- default (action-item-ui children):
32
- description: "Child content region for the `default (action-item-ui children)` slot."
31
+ default:
32
+ description: "Holds the list's `<action-item-ui>` children (the action items)."
33
33
  states:
34
34
  - name: idle
35
35
  description: Default, ready for interaction.
@@ -90,6 +90,7 @@ props:
90
90
  - muted
91
91
  - solid
92
92
  - outline
93
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
93
94
  events: {}
94
95
  slots: {}
95
96
  states:
@@ -19,13 +19,29 @@
19
19
  * Properties:
20
20
  * padding — none | xs | sm | md | lg | xl (default 'md')
21
21
  * margin — none | xs | sm | md | lg | xl (default 'none')
22
+ *
23
+ * Values resolve to the parametric --a-{padding,margin}-{xs..xl} scale
24
+ * (foundation/space.css), unified with the global [padding]/[margin] attribute
25
+ * API: md = 24px @k=1, tracks --a-{padding,margin}-k. The `@bp` responsive
26
+ * path below emits the same tokens as the static block.css rules.
22
27
  */
23
28
 
24
29
  import { UIElement } from '../../core/element.js';
25
30
  import { parseResponsive, breakpoint } from '../../core/responsive.js';
26
31
 
27
- const _SPACE = { none: '0', xs: 'var(--a-space-1)', sm: 'var(--a-space-2)', md: 'var(--a-space-4)', lg: 'var(--a-space-6)', xl: 'var(--a-space-10)' };
28
- function _spaceToCss(v) { return _SPACE[v] ?? (/^\d+$/.test(v ?? '') ? `var(--a-space-${v})` : v ?? ''); }
32
+ const _NAMED = new Set(['xs', 'sm', 'md', 'lg', 'xl']);
33
+ /** Resolve a (responsive-parsed) padding/margin value to CSS for dimension
34
+ * `dim` ('padding' | 'margin'). Named rungs map to the parametric
35
+ * --a-{dim}-{xs..xl} scale (foundation/space.css) so this responsive JS path
36
+ * AGREES with the static block.css rules + the global [padding]/[margin]
37
+ * attribute API; bare integers stay literal --a-space-N rungs; 'none' → 0. */
38
+ function _spaceToCss(v, dim) {
39
+ if (v == null || v === '') return '';
40
+ if (v === 'none') return '0';
41
+ if (_NAMED.has(v)) return `var(--a-${dim}-${v})`;
42
+ if (/^\d+$/.test(v)) return `var(--a-space-${v})`;
43
+ return v;
44
+ }
29
45
 
30
46
  export class UIBlock extends UIElement {
31
47
  static properties = {
@@ -42,13 +58,13 @@ export class UIBlock extends UIElement {
42
58
  const bp = anyR ? breakpoint.value : '';
43
59
 
44
60
  if (padding?.includes('@')) {
45
- this.style.setProperty('--block-padding', _spaceToCss(parseResponsive(padding, bp)));
61
+ this.style.setProperty('--block-padding', _spaceToCss(parseResponsive(padding, bp), 'padding'));
46
62
  } else {
47
63
  this.style.removeProperty('--block-padding');
48
64
  }
49
65
 
50
66
  if (margin?.includes('@')) {
51
- this.style.setProperty('--block-margin', _spaceToCss(parseResponsive(margin, bp)));
67
+ this.style.setProperty('--block-margin', _spaceToCss(parseResponsive(margin, bp), 'margin'));
52
68
  } else {
53
69
  this.style.removeProperty('--block-margin');
54
70
  }
@@ -1,6 +1,6 @@
1
1
  @scope (block-ui) {
2
2
  :where(:scope) {
3
- --block-padding: var(--a-space-4);
3
+ --block-padding: var(--a-padding-md);
4
4
  --block-margin: 0;
5
5
  }
6
6
 
@@ -11,19 +11,24 @@
11
11
  margin: var(--block-margin);
12
12
  }
13
13
 
14
- /* ── Padding variants ── */
14
+ /* ── Padding variants the parametric --a-padding-{xs..xl} scale
15
+ (foundation/space.css), unified with the global [padding] attribute API
16
+ (api/sizing.css) so <block-ui padding="md"> == [padding="md"] == 24px @k=1,
17
+ and block-ui now tracks the --a-padding-k knob. Was a literal --a-space-N
18
+ scale (md=16px, k-independent) — see CHANGELOG 0.7.7 behavior change. ── */
15
19
  :scope[padding="none"] { --block-padding: 0; }
16
- :scope[padding="xs"] { --block-padding: var(--a-space-1); }
17
- :scope[padding="sm"] { --block-padding: var(--a-space-2); }
18
- :scope[padding="md"] { --block-padding: var(--a-space-4); }
19
- :scope[padding="lg"] { --block-padding: var(--a-space-6); }
20
- :scope[padding="xl"] { --block-padding: var(--a-space-10); }
20
+ :scope[padding="xs"] { --block-padding: var(--a-padding-xs); }
21
+ :scope[padding="sm"] { --block-padding: var(--a-padding-sm); }
22
+ :scope[padding="md"] { --block-padding: var(--a-padding-md); }
23
+ :scope[padding="lg"] { --block-padding: var(--a-padding-lg); }
24
+ :scope[padding="xl"] { --block-padding: var(--a-padding-xl); }
21
25
 
22
- /* ── Margin variants ── */
26
+ /* ── Margin variants the parametric --a-margin-{xs..xl} scale, unified
27
+ with the global [margin] attribute API (tracks --a-margin-k). ── */
23
28
  :scope[margin="none"] { --block-margin: 0; }
24
- :scope[margin="xs"] { --block-margin: var(--a-space-1); }
25
- :scope[margin="sm"] { --block-margin: var(--a-space-2); }
26
- :scope[margin="md"] { --block-margin: var(--a-space-4); }
27
- :scope[margin="lg"] { --block-margin: var(--a-space-6); }
28
- :scope[margin="xl"] { --block-margin: var(--a-space-10); }
29
+ :scope[margin="xs"] { --block-margin: var(--a-margin-xs); }
30
+ :scope[margin="sm"] { --block-margin: var(--a-margin-sm); }
31
+ :scope[margin="md"] { --block-margin: var(--a-margin-md); }
32
+ :scope[margin="lg"] { --block-margin: var(--a-margin-lg); }
33
+ :scope[margin="xl"] { --block-margin: var(--a-margin-xl); }
29
34
  }
@@ -20,8 +20,7 @@
20
20
  },
21
21
  "aria-label": {
22
22
  "description": "Accessible label for screen readers. Auto-set from `text` when text is non-empty; meaningful override for icon-only buttons.",
23
- "type": "string",
24
- "default": ""
23
+ "$ref": "common_types.json#/$defs/DynamicString"
25
24
  },
26
25
  "color": {
27
26
  "description": "Semantic intent — composes with [variant]. `<button-ui variant=\"solid\" color=\"danger\">` = filled destructive action; `<button-ui variant=\"outline\" color=\"success\">` = outlined success affordance.",
@@ -71,11 +71,12 @@ button-ui[color="danger"]:not([disabled]):hover {
71
71
 
72
72
  /* ── Trailing slot ── */
73
73
  --button-trailing-font-size: var(--a-ui-sm);
74
- /* kbd-pill tracks the button's OWN text color (currentColor) so it reads
75
- on any variant fillon a primary button the fixed --a-fg-muted gray
76
- was nearly invisible against the white label. Border is a translucent
77
- slice of the same color so the pill outline adapts too. */
78
- --button-trailing-fg: currentColor;
74
+ /* kbd-ui styles its color/bg/border inside `@scope (kbd-ui)`, which beats
75
+ this scope by PROXIMITYso setting `color`/`border` on [slot="trailing"]
76
+ here is inert for a kbd pill. The pill is recolored through kbd's OWN
77
+ tokens (--kbd-fg/-bg/-border) in the rule below instead. The
78
+ --button-trailing-* tokens remain the fallback for non-kbd trailing
79
+ content (badges, plain spans). */
79
80
  --button-trailing-border: color-mix(in oklab, currentColor 28%, transparent);
80
81
  --button-trailing-radius: var(--a-radius-sm);
81
82
  --button-trailing-px: var(--a-space-0-5);
@@ -142,12 +143,22 @@ button-ui[color="danger"]:not([disabled]):hover {
142
143
  order: 99;
143
144
  margin-inline-start: auto;
144
145
  font-size: var(--button-trailing-font-size);
145
- color: var(--button-trailing-fg);
146
+ color: inherit;
146
147
  font-family: inherit;
147
148
  border: 1px solid var(--button-trailing-border);
148
149
  border-radius: var(--button-trailing-radius);
149
150
  padding: 0 var(--button-trailing-px);
150
151
  line-height: 1;
152
+ /* Recolor a <kbd-ui slot="trailing"> pill via kbd's OWN tokens so it tracks
153
+ the button label color on every variant (primary white, ghost subtle,
154
+ colored fills). kbd-ui's `:scope { color: var(--kbd-fg) }` wins over the
155
+ `color: inherit` above by scope proximity, but its tokens are declared at
156
+ :where(:scope) (0,0,0) — these (0,1,0) decls win on specificity, which the
157
+ cascade resolves BEFORE proximity. currentColor here is the inherited
158
+ button label color. */
159
+ --kbd-fg: currentColor;
160
+ --kbd-bg: color-mix(in oklab, currentColor 16%, transparent);
161
+ --kbd-border: color-mix(in oklab, currentColor 30%, transparent);
151
162
  }
152
163
 
153
164
  /* :scope:active moved outside @scope — see Safari 17.x bug note at top. */
@@ -18,6 +18,7 @@ props:
18
18
  description: Accessible label for screen readers. Auto-set from `text` when text is non-empty; meaningful override for icon-only buttons.
19
19
  type: string
20
20
  default: ""
21
+ dynamic: true # native ARIA attr — managed in connected() (setAttribute), deliberately NOT in `static properties` (would clobber native ariaLabel reflection per the installProps memory)
21
22
  type:
22
23
  description: HTML button type (button, submit, reset)
23
24
  type: string
@@ -16,6 +16,7 @@ props:
16
16
  rung ("1"…"16").
17
17
  type: string
18
18
  default: md
19
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
19
20
  draggable:
20
21
  description: Enables drag handle + cursor:grab. Wires the draggable trait; dispatches drag-end.
21
22
  type: boolean
@@ -55,8 +55,7 @@
55
55
  },
56
56
  "data": {
57
57
  "description": "JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x=\"month\" y=\"revenue\">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data=\"[…]\"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute.",
58
- "type": "array",
59
- "default": []
58
+ "$ref": "common_types.json#/$defs/DynamicStringList"
60
59
  },
61
60
  "format": {
62
61
  "description": "Number-format mode applied to axis labels + value overlays + donut total + gauge value + treemap value + funnel value + internal tooltip. `abbr` is the legacy 1.2K / 3M format; `decimal` fixes 2 decimals; `currency` prefixes via `--chart-currency-prefix` token (default \"$\"); `percent` multiplies × 100 and adds a % suffix.",
@@ -23,7 +23,7 @@ export class UIChart extends UIElement {
23
23
  /** Color scheme */
24
24
  color: 'accent' | 'success' | 'warning' | 'danger' | 'info';
25
25
  /** JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x="month" y="revenue">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data="[…]"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute. */
26
- data: unknown[];
26
+ data: string;
27
27
  /** Number-format mode applied to axis labels + value overlays + donut total + gauge value + treemap value + funnel value + internal tooltip. `abbr` is the legacy 1.2K / 3M format; `decimal` fixes 2 decimals; `currency` prefixes via `--chart-currency-prefix` token (default "$"); `percent` multiplies × 100 and adds a % suffix. */
28
28
  format: 'abbr' | 'decimal' | 'currency' | 'percent';
29
29
  /** When true, suppress the overlaid average line */
@@ -123,6 +123,7 @@ props:
123
123
  reflected attribute.
124
124
  type: array
125
125
  default: []
126
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
126
127
  events:
127
128
  chart-hover:
128
129
  description: >-
@@ -98,9 +98,6 @@
98
98
  "box": {
99
99
  "description": "Visual checkbox indicator"
100
100
  },
101
- "hint": {
102
- "description": "Help text below the label"
103
- },
104
101
  "label": {
105
102
  "description": "Label text beside the checkbox"
106
103
  }
@@ -58,8 +58,6 @@ events:
58
58
  slots:
59
59
  box:
60
60
  description: Visual checkbox indicator
61
- hint:
62
- description: Help text below the label
63
61
  label:
64
62
  description: Label text beside the checkbox
65
63
  states:
@@ -93,8 +93,7 @@
93
93
  },
94
94
  "options": {
95
95
  "description": "Programmatic option list. Array of {value, label, disabled?, group?} or grouped {label, options:[…]}. Alternative to declarative <option> / <optgroup> children.",
96
- "type": "array",
97
- "default": []
96
+ "$ref": "common_types.json#/$defs/DynamicStringList"
98
97
  },
99
98
  "placeholder": {
100
99
  "description": "Placeholder text in the input when value is empty",
@@ -84,6 +84,7 @@ props:
84
84
  <option> / <optgroup> children.
85
85
  type: array
86
86
  default: []
87
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
87
88
  open:
88
89
  description: Reflects popover open / closed state
89
90
  type: boolean
@@ -103,9 +103,6 @@
103
103
  },
104
104
  "list": {
105
105
  "description": "Container for command items and groups"
106
- },
107
- "search": {
108
- "description": "Search input with combobox role"
109
106
  }
110
107
  },
111
108
  "states": [
@@ -140,9 +140,27 @@ export class UICommand extends UIElement {
140
140
 
141
141
  // ── Parse declarative <option>/<optgroup> ──
142
142
 
143
+ // Wrapper-piercing walk for consumer-authored <option>/<optgroup> children.
144
+ // Mirrors select-ui's #logicalOptionChildren (FB-98): .map()/repeat()-rendered
145
+ // options arrive in display:contents/role="presentation" wrapper spans; a plain
146
+ // this.children iteration misses them (FB-98 class, 5th sighting in command-ui).
147
+ #logicalOptionChildren() {
148
+ const out = [];
149
+ const walk = (parent) => {
150
+ for (const ch of parent.children) {
151
+ if (ch.hasAttribute('slot')) continue; // component-stamped — skip
152
+ if (ch.tagName === 'OPTION' || ch.tagName === 'OPTGROUP') { out.push(ch); continue; }
153
+ if (ch.matches('[role="presentation"]') || ch.style?.display === 'contents') { walk(ch); continue; }
154
+ // unknown authored child — ignore (command-ui only uses option/optgroup)
155
+ }
156
+ };
157
+ walk(this);
158
+ return out;
159
+ }
160
+
143
161
  #parseOptions() {
144
162
  const items = [];
145
- for (const child of this.children) {
163
+ for (const child of this.#logicalOptionChildren()) {
146
164
  if (child.tagName === 'OPTGROUP') {
147
165
  const groupLabel = child.label;
148
166
  const group = { label: groupLabel, items: [] };
@@ -51,8 +51,6 @@ slots:
51
51
  description: Keyboard hint bar
52
52
  list:
53
53
  description: Container for command items and groups
54
- search:
55
- description: Search input with combobox role
56
54
  states:
57
55
  - name: idle
58
56
  description: Default, ready for interaction.
@@ -105,6 +105,7 @@ export class UIContextMenu extends UIElement {
105
105
  return [];
106
106
  }
107
107
  }
108
+ // wrapper-trap-ok: detects non-menu-item children as custom content; context-menu items are always static declarative children (right-click actions are not .map()'d)
108
109
  for (const child of this.children) {
109
110
  const tag = child.tagName.toLowerCase();
110
111
  if (tag !== 'menu-item-ui' && tag !== 'menu-divider-ui') return [child];
@@ -15,8 +15,7 @@
15
15
  "properties": {
16
16
  "items": {
17
17
  "description": "Optional JSON array of {term, description} — alternative to declarative <dt>/<dd> children",
18
- "type": "array",
19
- "default": []
18
+ "$ref": "common_types.json#/$defs/DynamicStringList"
20
19
  },
21
20
  "align": {
22
21
  "description": "Alignment for inline layout. `start` (default): term and value pack to the term column edge. `between`: term left, value right-aligned with `text-align: end`. `stretch`: value fills the remaining track width (e.g. for a `<slider-ui>` / `<select-ui>` / `<color-picker-ui>` as `<dd>` that should span the full row). Sets `justify-self: stretch` and `width: 100%` on `dd` so block-level form controls inside reach the column edge rather than shrink-wrapping to content.\n",
@@ -14,7 +14,7 @@ import { UIElement } from '../../core/element.js';
14
14
 
15
15
  export class UIDescriptionList extends UIElement {
16
16
  /** Optional JSON array of {term, description} — alternative to declarative <dt>/<dd> children */
17
- items: unknown[];
17
+ items: string;
18
18
  /** Alignment for inline layout. `start` (default): term and value pack to the term column edge. `between`: term left, value right-aligned with `text-align: end`. `stretch`: value fills the remaining track width (e.g. for a `<slider-ui>` / `<select-ui>` / `<color-picker-ui>` as `<dd>` that should span the full row). Sets `justify-self: stretch` and `width: 100%` on `dd` so block-level form controls inside reach the column edge rather than shrink-wrapping to content.
19
19
  */
20
20
  align: 'start' | 'between' | 'stretch';
@@ -30,6 +30,7 @@ props:
30
30
  description: Optional JSON array of {term, description} — alternative to declarative <dt>/<dd> children
31
31
  type: array
32
32
  default: []
33
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
33
34
  layout:
34
35
  description: "stacked: term above description. inline: term and description on one row."
35
36
  type: string
@@ -245,6 +245,7 @@ export class UIField extends UIElement {
245
245
 
246
246
  /** The default-slot control — first child without a `slot` attribute. */
247
247
  #findControl() {
248
+ // wrapper-trap-ok: field control is always a single literal child (one <input>/<textarea>/<select>); interpolating the control via .map() is not a documented field pattern
248
249
  for (const ch of this.children) {
249
250
  if (!ch.hasAttribute('slot')) return ch;
250
251
  }
@@ -81,6 +81,7 @@ export class UIFields extends UIElement {
81
81
 
82
82
  #syncInline() {
83
83
  const inline = this.hasAttribute('inline');
84
+ // wrapper-trap-ok: fields-ui children are authored as static field-ui literals within a form group; dynamic .map() field collections use the programmatic API
84
85
  for (const child of this.children) {
85
86
  if (child.localName !== 'field-ui') continue;
86
87
  if (inline) {
@@ -23,6 +23,7 @@ props:
23
23
  in a 2-column hero. Sets grid-item alignment, NOT text alignment.
24
24
  type: string
25
25
  default: stretch
26
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
26
27
  columnGap:
27
28
  description: Column gap override
28
29
  type: string
@@ -50,6 +51,7 @@ props:
50
51
  justify-items. Default stretch (items fill their column track).
51
52
  type: string
52
53
  default: stretch
54
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
53
55
  minColumnWidth:
54
56
  description: >-
55
57
  Minimum track width for columns="auto-fit"/"auto-fill" (any CSS length,
@@ -40,8 +40,7 @@
40
40
  },
41
41
  "autocomplete": {
42
42
  "description": "Browser autofill behavior per HTML autocomplete spec. Routed via setAttribute to the host element. Common values: off, on, cc-number, cc-exp, cc-csc, cc-name, email, username, current-password, new-password, one-time-code, given-name, family-name, street-address, postal-code.",
43
- "type": "string",
44
- "default": ""
43
+ "$ref": "common_types.json#/$defs/DynamicString"
45
44
  },
46
45
  "component": {
47
46
  "const": "Input"
@@ -58,18 +57,7 @@
58
57
  },
59
58
  "inputmode": {
60
59
  "description": "Mobile keyboard hint per HTML inputmode spec. Routed via setAttribute to the host element. Values: text, decimal, numeric, tel, search, email, url.",
61
- "type": "string",
62
- "enum": [
63
- "text",
64
- "decimal",
65
- "numeric",
66
- "tel",
67
- "search",
68
- "email",
69
- "url",
70
- "none"
71
- ],
72
- "default": null
60
+ "$ref": "common_types.json#/$defs/DynamicString"
73
61
  },
74
62
  "label": {
75
63
  "description": "Inline label rendered as a leading caption inside the input chrome, between any prefix and the value. Wires aria-labelledby on the editable surface. For stacked label / hint / error compositions, wrap with field-ui.",
@@ -80,12 +80,14 @@ props:
80
80
  - email
81
81
  - url
82
82
  - none
83
+ dynamic: true # native attr — routed via setAttribute to the host, deliberately NOT in static properties (would clobber native reflection)
83
84
  autocomplete:
84
85
  description: 'Browser autofill behavior per HTML autocomplete spec. Routed via setAttribute to the host element. Common
85
86
  values: off, on, cc-number, cc-exp, cc-csc, cc-name, email, username, current-password, new-password, one-time-code,
86
87
  given-name, family-name, street-address, postal-code.'
87
88
  type: string
88
89
  default: ''
90
+ dynamic: true # native attr — routed via setAttribute to the host, deliberately NOT in static properties (would clobber native reflection)
89
91
  min:
90
92
  description: Minimum numeric value. Applies when `type="number"`. Clamps + drives aria-valuemin + the [-] button's disabled
91
93
  state.
@@ -226,6 +226,7 @@ export class UIIntegrationCard extends UIElement {
226
226
  #hasConsumerDescription() {
227
227
  // Author description = direct child element that is neither one of our
228
228
  // stamped data- markers nor an [slot="actions"] / [slot="action"] node.
229
+ // wrapper-trap-ok: detects presence of consumer-provided description; wrapper spans from interpolated content also qualify as "consumer description present" (correct — the content IS there)
229
230
  for (const child of this.children) {
230
231
  if (child === this.#bodyEl) continue;
231
232
  if (child === this.#descEl) continue;
@@ -174,6 +174,7 @@ export class UIListItem extends UIElement {
174
174
  // otherwise we'd accidentally remove e.g. a <span slot="icon"> inside a
175
175
  // child <card-ui> when list-item-ui has no `icon` prop.
176
176
  #ownChild(selector) {
177
+ // wrapper-trap-ok: finds component-stamped children (identified by [data-list-stamped]) — consumer list-item-ui children are never queried here
177
178
  for (const ch of this.children) {
178
179
  if (ch.matches(selector)) return ch;
179
180
  }
@@ -34,6 +34,7 @@ props:
34
34
  so content sits at the list-ui edges (canonical edge-to-edge style).
35
35
  type: boolean
36
36
  default: false
37
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
37
38
  selectable:
38
39
  description: Enable selection on child listitems (roving tabindex + aria-selected).
39
40
  type: boolean
@@ -290,6 +290,7 @@ export class UIMenuItem extends UIElement {
290
290
  #wasStamped(el) { return el?.dataset?.menuItemStamped === '1'; }
291
291
 
292
292
  #ownChild(selector) {
293
+ // wrapper-trap-ok: finds component-stamped children (data-menuItemStamped); consumer menu-item-ui children are adopted by #show() using logical children
293
294
  for (const ch of this.children) {
294
295
  if (ch.matches(selector)) return ch;
295
296
  }
@@ -21,9 +21,21 @@
21
21
 
22
22
  /* Items/dividers in Light DOM are hidden unless they've been adopted
23
23
  into the popover on open. Popover API also hides the popover itself
24
- when closed. */
24
+ when closed.
25
+
26
+ FB-97: interpolated items (`.map()` / `repeat()`) arrive nested in the
27
+ template engine's `display:contents` / `role="presentation"` wrapper
28
+ span (core/template.js), so the direct-child combinator alone misses
29
+ them on the initial (never-opened) render — a CLOSED declarative menu
30
+ would lay them out in flow and balloon to its widest item's width,
31
+ displacing siblings. The wrapper-piercing descendant selectors hide
32
+ those too. Safe vs. the open state: #show() appendChild's the real
33
+ items (not the wrappers) into [data-menu-popover], so open-state items
34
+ are neither `:scope >` children nor under a `:scope > [role]` wrapper. */
25
35
  :scope > menu-item-ui,
26
- :scope > menu-divider-ui {
36
+ :scope > menu-divider-ui,
37
+ :scope > [role="presentation"] menu-item-ui,
38
+ :scope > [role="presentation"] menu-divider-ui {
27
39
  display: none;
28
40
  }
29
41
  }
@@ -15,6 +15,7 @@ props:
15
15
  description: 'Component property: complete.'
16
16
  type: boolean
17
17
  default: false
18
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
18
19
  message:
19
20
  description: 'Component property: message.'
20
21
  type: string
@@ -108,9 +108,6 @@
108
108
  "dot": {
109
109
  "description": "Radio dot indicator, sized via --dot-size"
110
110
  },
111
- "hint": {
112
- "description": "Hint text container, rendered via CSS attr(hint) on ::after"
113
- },
114
111
  "label": {
115
112
  "description": "Label text container, rendered via CSS attr(label) on ::after"
116
113
  }
@@ -59,8 +59,6 @@ events:
59
59
  slots:
60
60
  dot:
61
61
  description: Radio dot indicator, sized via --dot-size
62
- hint:
63
- description: Hint text container, rendered via CSS attr(hint) on ::after
64
62
  label:
65
63
  description: Label text container, rendered via CSS attr(label) on ::after
66
64
  states:
@@ -53,6 +53,7 @@ props:
53
53
  - ""
54
54
  - accent
55
55
  - warning
56
+ reflect: false # declarative presentational attribute — CSS/author-time read, non-reactive by design
56
57
  events:
57
58
  "[object Object]":
58
59
  description: "Fired on [object Object]."
@@ -99,14 +99,7 @@
99
99
  "Command",
100
100
  "Select"
101
101
  ],
102
- "slots": {
103
- "clear": {
104
- "description": "Clear button (shown when value is non-empty)"
105
- },
106
- "input": {
107
- "description": "Native search input element"
108
- }
109
- },
102
+ "slots": {},
110
103
  "states": [
111
104
  {
112
105
  "description": "Default, ready for interaction.",