@adia-ai/web-components 0.7.22 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.7.24] — 2026-07-01
4
+
5
+ Upstream consumer findings from `adia-pay` against installed `0.7.23` (F1–F6).
6
+
7
+ ### Added
8
+ - **`display-field-ui` (`components/display-field/`) — new display-only labeled-value primitive.** Fills a capability gap: a field-shaped read-out for a static or masked value (a card on file, `•••• 4242`, an account id) or a labeled slot that hosts a third-party element (a Stripe Element iframe) under a proper caption. Unlike `field-ui` it wraps **no** interactive control — no focus, no contenteditable, no form participation, no `[for]`/id-minting — so a readonly `input-ui` (which keeps input semantics) is no longer the only option. Props: `label` (required; names the group via `role="group"` + `aria-labelledby`), `value` (static text), `variant` (`default` | `masked` — mono/tabular treatment so `•••• 4242` aligns), `hint` (caption + `aria-describedby`), `icon` (leading glyph). The value comes from the `value` attr **or** an unnamed default-slot child (the foreign-element path) — slot content wins when both are present. Ships yaml + css + js + d.ts + a2ui sidecar + demo (`display-field.examples.html`) + 6 unit tests; registered in the component barrel + `styles/components.css`; `dist/` + `custom-elements.json` (120 → 121 modules) regenerated. Additive (PATCH). (upstream finding F5)
9
+ - **`table-ui` (`components/table/`) — `rowExpandable` per-row expand gate.** `expandable` was host-level: every row got the same `caret-right` toggle and one host-level `expandRenderer` drove all detail panes, so heterogeneous rows (a *succeeded* payment needing no detail vs. a *failed* one that expands to a reason) could not vary the affordance. New optional `rowExpandable(row, index) => boolean` callback gates the caret per row — non-expandable rows keep the reserved expand-column cell (grid alignment never shifts) but show no caret and can't open a detail pane; `toggleExpand()` is guarded to match (it still collapses an already-open row if the predicate later flips, so a row can't get stuck open). `null` (default) preserves the legacy "every row expandable" behavior. Demo: the `rowExpandable` section in `table.examples.html` (only Failed payments expand). Regression: 3 tests in `table.test.js`. Additive — no API removed (PATCH). (upstream finding F6)
10
+
11
+ ### Fixed
12
+ - **`icon-ui` (`components/icon/`) — fails loud when the icon registry was never wired.** Under Vite 8 / rolldown the zero-config `import.meta.glob('/node_modules/@phosphor-icons/…')` path in `icons-phosphor.js` does not transform from inside a published dependency, leaving the registry empty so every `<icon-ui>` renders blank — previously **silently** (a single missing icon warned, but a wholly-unwired registry did not). New `iconRegistryUnwired()` predicate (`core/icons.js`) distinguishes "never wired" (`registry.size === 0 && !registryReady`) from "one bad name"; `icon.class.js` schedules a one-shot, macrotask-deferred `console.error` naming the exact fix (install `@phosphor-icons/core` + call `installIconLoaders` from the app entry) only when a named icon resolves to nothing AND the registry is unwired. The deferral means a correctly-wired app entry is never false-flagged. Documented as the primary bundler path in `USAGE.md` § Icons. (upstream finding F1)
13
+ - **`custom-elements.json` — manifest no longer fabricates nonexistent modules.** `scripts/build/generate-custom-elements-json.mjs` emitted a `javascript-module` + class + `custom-element-definition` for every component yaml by assuming a `components/<dir>/<base>.js` file existed — fabricating e.g. `UIHeader` → `header.js` (the CSS-only `header-ui` has no class file) and pointing co-located children (menu-item-ui, menu-divider-ui, …) at nonexistent per-child `.js` files. The generator now scans the actual `.js` sources for `customElements.define` / `defineIfFree` / `defineIfUndefined` calls, maps each tag to the file that really defines it, groups declarations by real module path, and lists genuinely CSS-only tags (aside/footer/header/section-ui) as skipped. Regenerated manifest: 120 modules / 134 tags, 0 dangling file paths (was 110 modules carrying fabricated paths). (upstream finding F4)
14
+
15
+ ### Changed
16
+ - **`@phosphor-icons/core` is now a direct dependency (`^2.1.1`).** It was referenced by the icon-loader glob but absent from every dependency field, so the documented zero-config icon path could not resolve its SVG assets from a clean install. Added to `dependencies` so the assets resolve; the app still wires the loader glob from its own source graph (see `USAGE.md` § Icons / finding F1). (upstream finding F2)
17
+
18
+ ## [0.7.23] — 2026-06-30
19
+
20
+ ### Fixed
21
+ - **`button-ui` (`components/button/`) — child text now labels the button, same as `text=`.** A label written as child text (`<button-ui>Save</button-ui>`, no `text=` attr) was treated as icon-only and collapsed to a square icon slot with the text clipped — the collapse rule keyed off `:scope:not([text]), :scope[text=""]`, which can't see text-node children. `render()` now scans child nodes for non-whitespace text and reflects a JS-derived `[data-icon-only]` state **only** when there is neither a `text=` value nor child text; `button.css` keys its collapse + inline-level rules off `[data-icon-only]` instead of the `[text]` selectors. A child-text button now renders at full content width, identical to `text="Save"`, while a genuine `<button-ui icon="…">` still collapses; the icon-only accessible-name warning is re-gated to respect child text. Adds an `icon-only` state to `button.yaml`; `button.a2ui.json` + `dist/web-components.min.{js,css}` + `.sheet.js` (and the `theme-provider` / `everything` bundles) regenerated. (journal 2026-06-30 §1)
22
+
23
+ ### Changed
24
+ - **`accordion-ui` (`components/accordion/`) — normal block-flow gap, borderless items.** `--accordion-gap` changed from `0` to `var(--a-gap)`, so stacked items separate with the same canonical block-flow stack gap `col-ui` / `row-ui` / `grid-ui` use instead of butting together. The per-item `border-bottom` separator — with its `--accordion-item-border` token and the `:last-child` border reset — was removed for a clean borderless stack. Visible re-spacing, no public-API change (PATCH). (journal 2026-06-30 §2)
25
+
3
26
  ## [0.7.22] — 2026-06-16
4
27
 
5
28
  ### Changed
package/USAGE.md CHANGED
@@ -9,17 +9,18 @@ The complete reference for engineers and agents **integrating** AdiaUI into an a
9
9
  ## Contents
10
10
 
11
11
  1. [Install](#install)
12
- 2. [The mental model](#the-mental-model)
13
- 3. [Property reactivity (signal-backed)](#property-reactivity-signal-backed)
14
- 4. [Property binding patterns (templates / framework integration)](#property-binding-patterns)
15
- 5. [Event contract `CustomEvent` with `detail`](#event-contract)
16
- 6. [Form participation — `UIFormElement` + `ElementInternals`](#form-participation)
17
- 7. [Data binding with `data-stream-*`](#data-binding-with-data-stream-)
18
- 8. [Lifecycle `connected` / `render` / `updated` / `disconnected`](#lifecycle)
19
- 9. [Theming, density, size](#theming-density-size)
20
- 10. [Registration auto vs explicit](#registration--auto-vs-explicit)
21
- 11. [TypeScript](#typescript)
22
- 12. [Anti-patterns](#anti-patterns)
12
+ 2. [Icons wiring the registry](#icons--wiring-the-registry)
13
+ 3. [The mental model](#the-mental-model)
14
+ 4. [Property reactivity (signal-backed)](#property-reactivity-signal-backed)
15
+ 5. [Property binding patterns (templates / framework integration)](#property-binding-patterns)
16
+ 6. [Event contract — `CustomEvent` with `detail`](#event-contract)
17
+ 7. [Form participation `UIFormElement` + `ElementInternals`](#form-participation)
18
+ 8. [Data binding with `data-stream-*`](#data-binding-with-data-stream-)
19
+ 9. [Lifecycle `connected` / `render` / `updated` / `disconnected`](#lifecycle)
20
+ 10. [Theming, density, size](#theming-density-size)
21
+ 11. [Registration — auto vs explicit](#registration--auto-vs-explicit)
22
+ 12. [TypeScript](#typescript)
23
+ 13. [Anti-patterns](#anti-patterns)
23
24
 
24
25
  ---
25
26
 
@@ -96,6 +97,50 @@ For the live demo of every component: <https://ui-kit.exe.xyz/site/components/>.
96
97
 
97
98
  ---
98
99
 
100
+ ## Icons — wiring the registry
101
+
102
+ `<icon-ui>` (and every component that renders an icon — `<button-ui icon>`,
103
+ `<badge-ui>`, menus, etc.) draws from an **icon registry** that your app
104
+ must wire **once, from your own entry module**. Out of the box the registry
105
+ is empty; an un-wired registry renders every icon **blank**.
106
+
107
+ **Why the app has to do it.** The zero-config glob in the package's own
108
+ `icons-phosphor.js` uses `import.meta.glob('/node_modules/@phosphor-icons/…')`,
109
+ which is a **build-time macro your bundler rewrites** — and under Vite 8 /
110
+ rolldown that rewrite **does not happen for code imported from inside a
111
+ published dependency**. The glob has to be evaluated in *your* source graph.
112
+ `@phosphor-icons/core` ships as a dependency of this package (so the SVG
113
+ assets resolve), but the glob that reads them must live in your app.
114
+
115
+ **The one-time wiring (do this in your app entry, before first paint):**
116
+
117
+ ```js
118
+ import { installIconLoaders } from '@adia-ai/web-components/core/icons';
119
+
120
+ installIconLoaders({
121
+ regular: import.meta.glob(
122
+ '/node_modules/@phosphor-icons/core/assets/regular/*.svg',
123
+ { query: '?raw', import: 'default', eager: true },
124
+ ),
125
+ // add other weights only if you use them:
126
+ // bold: import.meta.glob('/node_modules/@phosphor-icons/core/assets/bold/*.svg', { query: '?raw', import: 'default', eager: true }),
127
+ // fill: import.meta.glob('/node_modules/@phosphor-icons/core/assets/fill/*.svg', { query: '?raw', import: 'default', eager: true }),
128
+ });
129
+ ```
130
+
131
+ `installIconLoaders` is idempotent, may be called before or after the
132
+ elements connect (icons already on the page repaint when it runs), and
133
+ accepts either the per-weight map above or a flat `path → svg-string` map
134
+ if you source icons some other way (`registerIcon(name, svg)` /
135
+ `registerIcons(map)` are the lower-level primitives).
136
+
137
+ **Fail-loud, not fail-blank.** If any `<icon-ui name="…">` renders and the
138
+ registry was never wired, the component logs a one-shot `console.error`
139
+ naming this exact fix. A single *missing* icon (registry wired, that one
140
+ name absent) warns separately and does not trip the un-wired error.
141
+
142
+ ---
143
+
99
144
  ## The mental model
100
145
 
101
146
  Every AdiaUI primitive is a **standard light-DOM custom element**. There's no shadow DOM, no compiler, no framework — just `customElements.define()` extending a small `UIElement` base class. The base class wires a signal-backed property system on top.
@@ -9,7 +9,11 @@
9
9
  /* ── Container ── */
10
10
  @scope (accordion-ui) {
11
11
  :where(:scope) {
12
- --accordion-gap: 0;
12
+ /* Normal block-flow gap between items — the same ambient stack gap
13
+ col-ui/row-ui/grid-ui read (`--a-gap`, base `--a-gap-md`). Items now
14
+ separate by this gap rather than by flush hairline dividers, so the
15
+ item @scope below carries no inter-item border. */
16
+ --accordion-gap: var(--a-gap);
13
17
  }
14
18
 
15
19
  :scope {
@@ -24,7 +28,6 @@
24
28
  @scope (accordion-item-ui) {
25
29
  :where(:scope) {
26
30
  /* ── Colors ── */
27
- --accordion-item-border: var(--a-border-subtle);
28
31
  --accordion-item-header-fg: var(--a-fg-subtle);
29
32
  --accordion-item-header-fg-hover: var(--a-fg-strong);
30
33
  --accordion-item-header-bg-hover: var(--a-bg-muted);
@@ -53,11 +56,6 @@
53
56
  :scope {
54
57
  box-sizing: border-box;
55
58
  display: block;
56
- border-bottom: 1px solid var(--accordion-item-border);
57
- }
58
-
59
- :scope:last-child {
60
- border-bottom: none;
61
59
  }
62
60
 
63
61
  /* ── Header ── */
@@ -145,6 +145,11 @@
145
145
  "description": "Default, ready for interaction.",
146
146
  "name": "idle"
147
147
  },
148
+ {
149
+ "description": "No text label — neither the `text` attr nor non-whitespace child text. Reflected by render() as `[data-icon-only]`; the CSS collapses the button to a square icon slot (zero inline padding, width = height) and keeps it inline-level. A button labeled by child text (`<button-ui>Save</button-ui>`) is NOT icon-only and renders at full content width, same as `text=`.",
150
+ "attribute": "data-icon-only",
151
+ "name": "icon-only"
152
+ },
148
153
  {
149
154
  "description": "Non-interactive; dimmed.",
150
155
  "attribute": "disabled",
@@ -82,6 +82,24 @@ export class UIButton extends UIElement {
82
82
  }
83
83
  }
84
84
 
85
+ // A button is "labeled" by EITHER the [text] attribute (rendered via the
86
+ // ::after in button.css) OR direct light-DOM child text — `<button-ui>Save
87
+ // </button-ui>`, which renders as-is in the flex row. Both are valid labels;
88
+ // only a button with NEITHER is a square icon-only (or empty) button. Detect
89
+ // child text by scanning direct text nodes — the auto-stamped <icon-ui>
90
+ // children are element nodes (nodeType 1), so they're correctly ignored.
91
+ // Reflect the result as [data-icon-only]: button.css keys its collapse +
92
+ // inline-level rules off it (mirrors the derived-marker pattern in
93
+ // page.class.js's [data-header-stuck]). Child-text edits don't re-trigger
94
+ // render() (textContent isn't reactive — see the static-properties note
95
+ // above), but light-DOM children are parsed before the first render, so the
96
+ // initial classification is correct; toolbar spillover toggles the [text]
97
+ // attr (reactive), which re-runs this and keeps the marker in sync.
98
+ const hasChildText = Array.from(this.childNodes).some(
99
+ (n) => n.nodeType === 3 && n.textContent.trim() !== '',
100
+ );
101
+ this.toggleAttribute('data-icon-only', !this.text && !hasChildText);
102
+
85
103
  // FEEDBACK-15: warn when text= begins with a symbol that icon= already
86
104
  // renders (e.g. text="+ New Claim" icon="plus" → a doubled "+"). One-shot
87
105
  // per element via WeakSet; warning-only — the button still renders. The
@@ -113,7 +131,9 @@ export class UIButton extends UIElement {
113
131
  // so the bug class is diagnosable in dev without firing every
114
132
  // render tick. The warning is suppressed via a one-shot
115
133
  // WeakSet so subsequent re-renders stay quiet.
116
- if (this.icon && !this.text) {
134
+ if (this.icon && !this.text && !hasChildText) {
135
+ // (child text is itself the accessible name natively, so a button with
136
+ // light-DOM text is NOT icon-only and never warns here.)
117
137
  const ariaLabel = this.getAttribute('aria-label');
118
138
  const ariaLabelledBy = this.getAttribute('aria-labelledby');
119
139
  const titleAttr = this.getAttribute('title');
@@ -148,10 +148,11 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
148
148
  }
149
149
 
150
150
  /* Display convention (ADR-0037): block-level by default — a text button used
151
- directly in block flow fills its container. Icon-only buttons (no [text])
152
- stay inline-level, and [inline] forces inline for any button. Inside
153
- row-ui/col-ui/grid-ui this is moot (flex/grid items are blockified). */
154
- :scope:not([text]) { display: inline-flex; }
151
+ directly in block flow fills its container. Icon-only buttons (no text
152
+ label — see [data-icon-only] below) stay inline-level, and [inline] forces
153
+ inline for any button. Inside row-ui/col-ui/grid-ui this is moot (flex/grid
154
+ items are blockified). */
155
+ :scope[data-icon-only] { display: inline-flex; }
155
156
  :scope[inline] { display: inline-flex; }
156
157
 
157
158
  /* Trailing icon slot — for forward-affordance carets ("Next →"), distinct
@@ -284,8 +285,13 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
284
285
  cursor: not-allowed;
285
286
  }
286
287
 
287
- /* Icon-only (no text) */
288
- :scope:not([text]), :scope[text=""] {
288
+ /* Icon-only / empty — no text label from EITHER the [text] attr or direct
289
+ child text. The JS render() reflects [data-icon-only] (set only when
290
+ !text AND no non-whitespace child text), which subsumes the old
291
+ :not([text]) / [text=""] selectors AND correctly treats
292
+ `<button-ui>Save</button-ui>` as a labeled (full-width) button rather than
293
+ collapsing it into a square. */
294
+ :scope[data-icon-only] {
289
295
  --button-px: 0;
290
296
  width: var(--button-height);
291
297
  }
@@ -112,6 +112,14 @@ slots:
112
112
  states:
113
113
  - name: idle
114
114
  description: Default, ready for interaction.
115
+ - name: icon-only
116
+ description: >-
117
+ No text label — neither the `text` attr nor non-whitespace child text.
118
+ Reflected by render() as `[data-icon-only]`; the CSS collapses the button
119
+ to a square icon slot (zero inline padding, width = height) and keeps it
120
+ inline-level. A button labeled by child text (`<button-ui>Save</button-ui>`)
121
+ is NOT icon-only and renders at full content width, same as `text=`.
122
+ attribute: data-icon-only
115
123
  - name: disabled
116
124
  description: Non-interactive; dimmed.
117
125
  attribute: disabled
@@ -0,0 +1,185 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/DisplayField.json",
4
+ "title": "DisplayField",
5
+ "description": "Display-only labeled value — a field-shaped read-out for a static or masked value (e.g. a card on file \"•••• 4242\"), or a labeled slot that hosts a third-party element (a Stripe Element iframe) under a proper caption. Unlike field-ui it wraps NO interactive control: no focus, no contenteditable, no form participation, no [for]/id-minting. The label names the whole field for assistive tech via role=\"group\" + aria-labelledby, so a slotted foreign element (iframe) is announced with its caption. Reach for field-ui + input-ui when the value is EDITABLE; stat-ui for a prominent metric/KPI; description-list-ui for many read-only key/value pairs at once.",
6
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "component": {
17
+ "const": "DisplayField"
18
+ },
19
+ "hint": {
20
+ "description": "Optional caption rendered below the value in subtle style (e.g. \"Expires 12/26\"). Wired into the field's aria-describedby.",
21
+ "type": "string",
22
+ "default": ""
23
+ },
24
+ "icon": {
25
+ "description": "Optional leading icon name shown beside the value (e.g. a lock or card-brand glyph). Lazily creates a single icon-ui child.",
26
+ "type": "string",
27
+ "default": ""
28
+ },
29
+ "label": {
30
+ "description": "The field caption. Names the whole field for assistive tech.",
31
+ "type": "string",
32
+ "default": ""
33
+ },
34
+ "value": {
35
+ "description": "The static value text to display (e.g. \"•••• 4242\"). Ignored when the default slot already carries content — slot a foreign element OR set value=, not both.",
36
+ "type": "string",
37
+ "default": ""
38
+ },
39
+ "variant": {
40
+ "description": "Cosmetic value treatment. `default` renders the value in the UI font; `masked` switches to the code/mono family with tabular numerals and light letter-spacing so masked strings (\"•••• 4242\") align cleanly. Tokens only — no layout change.",
41
+ "type": "string",
42
+ "enum": [
43
+ "default",
44
+ "masked"
45
+ ],
46
+ "default": "default"
47
+ }
48
+ },
49
+ "required": [
50
+ "component",
51
+ "label"
52
+ ],
53
+ "unevaluatedProperties": false,
54
+ "x-adiaui": {
55
+ "anti_patterns": [
56
+ {
57
+ "description": "Using input-ui[readonly] to render a static masked value. A readonly input still carries contenteditable/focus semantics and tab-stops; a display read-out should not be a control.",
58
+ "right": "<display-field-ui label=\"Card on file\" value=\"•••• 4242\" variant=\"masked\"></display-field-ui>\n",
59
+ "rule": "A non-editable masked value is display-field-ui, not a readonly input.",
60
+ "wrong": "<input-ui readonly value=\"•••• 4242\"></input-ui>\n"
61
+ }
62
+ ],
63
+ "category": "form",
64
+ "composes": [
65
+ "icon-ui"
66
+ ],
67
+ "events": {},
68
+ "examples": [
69
+ {
70
+ "description": "A masked \"card on file\" read-out with a leading lock icon and an expiry hint.",
71
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"DisplayField\",\n \"label\": \"Card on file\",\n \"value\": \"•••• •••• •••• 4242\",\n \"variant\": \"masked\",\n \"icon\": \"lock\",\n \"hint\": \"Expires 12/26\"\n }\n]",
72
+ "name": "card-on-file"
73
+ },
74
+ {
75
+ "description": "A labeled slot hosting a third-party element (e.g. a Stripe Element mount).",
76
+ "a2ui": "[\n {\n \"id\": \"root\",\n \"component\": \"DisplayField\",\n \"label\": \"Card number\",\n \"children\": [\"mount\"]\n },\n { \"id\": \"mount\", \"component\": \"Text\", \"text\": \"[Stripe Element mounts here]\" }\n]",
77
+ "name": "foreign-element-slot"
78
+ }
79
+ ],
80
+ "keywords": [
81
+ "display field",
82
+ "readonly",
83
+ "read-only",
84
+ "masked",
85
+ "mask",
86
+ "static value",
87
+ "card on file",
88
+ "last four",
89
+ "value",
90
+ "readout",
91
+ "stripe element",
92
+ "iframe slot",
93
+ "non-editable"
94
+ ],
95
+ "name": "UIDisplayField",
96
+ "related": [
97
+ "field",
98
+ "input",
99
+ "stat",
100
+ "description-list"
101
+ ],
102
+ "slots": {
103
+ "default": {
104
+ "description": "The value content. Slot a third-party element here — a Stripe Element iframe mount, or rich markup — and it takes the value region under the label, replacing the [value] attribute. The host's role=\"group\" + aria-labelledby names it for assistive tech."
105
+ },
106
+ "hint": {
107
+ "description": "The hint caption below the value. Auto-stamped from the [hint] attribute; wired into aria-describedby."
108
+ },
109
+ "icon": {
110
+ "description": "The leading icon (a single icon-ui). Auto-stamped from the [icon] attribute."
111
+ },
112
+ "label": {
113
+ "description": "The caption element. Auto-stamped from the [label] attribute; slot your own [slot=\"label\"] child to override with richer markup. It is the group's accessible name (the aria-labelledby target)."
114
+ },
115
+ "value": {
116
+ "description": "The static value element. Auto-stamped from the [value] attribute; suppressed when the default slot carries foreign content."
117
+ }
118
+ },
119
+ "states": [
120
+ {
121
+ "description": "Default, non-interactive read-out.",
122
+ "name": "idle"
123
+ }
124
+ ],
125
+ "status": "stable",
126
+ "synonyms": {
127
+ "masked": [
128
+ "display field",
129
+ "field",
130
+ "value"
131
+ ],
132
+ "readout": [
133
+ "display field",
134
+ "stat",
135
+ "field"
136
+ ],
137
+ "value": [
138
+ "display field",
139
+ "field",
140
+ "stat"
141
+ ]
142
+ },
143
+ "tag": "display-field-ui",
144
+ "tokens": {
145
+ "--display-field-gap": {
146
+ "description": "Vertical gap between the label, value, and hint rows."
147
+ },
148
+ "--display-field-hint-color": {
149
+ "description": "Hint foreground color."
150
+ },
151
+ "--display-field-hint-size": {
152
+ "description": "Hint font size."
153
+ },
154
+ "--display-field-icon-gap": {
155
+ "description": "Horizontal gap between the leading icon and the value."
156
+ },
157
+ "--display-field-label-color": {
158
+ "description": "Label foreground color."
159
+ },
160
+ "--display-field-label-size": {
161
+ "description": "Label font size."
162
+ },
163
+ "--display-field-label-weight": {
164
+ "description": "Label font weight."
165
+ },
166
+ "--display-field-masked-font": {
167
+ "description": "Font family applied to a masked value."
168
+ },
169
+ "--display-field-masked-tracking": {
170
+ "description": "Letter-spacing applied to a masked value."
171
+ },
172
+ "--display-field-value-color": {
173
+ "description": "Value foreground color."
174
+ },
175
+ "--display-field-value-size": {
176
+ "description": "Value font size."
177
+ },
178
+ "--display-field-value-weight": {
179
+ "description": "Value font weight."
180
+ }
181
+ },
182
+ "traits": [],
183
+ "version": 1
184
+ }
185
+ }
@@ -0,0 +1,91 @@
1
+ @scope (display-field-ui) {
2
+ :where(:scope) {
3
+ /* ── Tokens ── (label chrome mirrors field-ui so a display value reads
4
+ as a sibling of an editable field) */
5
+ --display-field-gap: var(--a-space-1);
6
+ --display-field-icon-gap: var(--a-space-2);
7
+ --display-field-label-color: var(--a-fg);
8
+ --display-field-label-size: var(--a-ui-sm);
9
+ --display-field-label-weight: var(--a-weight-medium);
10
+ --display-field-value-color: var(--a-fg);
11
+ --display-field-value-size: var(--a-ui-md);
12
+ --display-field-value-weight: var(--a-weight-regular);
13
+ --display-field-hint-color: var(--a-fg-muted);
14
+ --display-field-hint-size: var(--a-ui-sm);
15
+ --display-field-masked-font: var(--a-font-family-code);
16
+ --display-field-masked-tracking: 0.06em;
17
+ }
18
+
19
+ /* ── Base — single grid, rows placed by [slot] named areas. No DOM
20
+ reparenting; the value area accepts the auto [slot="value"] element
21
+ (from the `value` attr) AND any unnamed foreign child (a slotted
22
+ iframe mount). ── */
23
+ :scope {
24
+ box-sizing: border-box;
25
+ display: grid;
26
+ grid-template-columns: minmax(0, 1fr);
27
+ grid-template-areas:
28
+ "label"
29
+ "value"
30
+ "hint";
31
+ row-gap: var(--display-field-gap);
32
+ min-width: 0;
33
+ }
34
+
35
+ /* With a leading icon → two columns; the icon spans the value row only,
36
+ label + hint stay full-width. The column is gated on a present icon so
37
+ no empty track leaks a column-gap when there's no icon. */
38
+ :scope:has(> [slot="icon"]) {
39
+ grid-template-columns: auto minmax(0, 1fr);
40
+ grid-template-areas:
41
+ "label label"
42
+ "icon value"
43
+ "hint hint";
44
+ column-gap: var(--display-field-icon-gap);
45
+ }
46
+
47
+ /* ── Slot styling ── */
48
+ :scope > [slot="label"] {
49
+ grid-area: label;
50
+ color: var(--display-field-label-color);
51
+ font-size: var(--display-field-label-size);
52
+ font-weight: var(--display-field-label-weight);
53
+ line-height: 1.3;
54
+ min-width: 0;
55
+ }
56
+
57
+ :scope > [slot="icon"] {
58
+ grid-area: icon;
59
+ align-self: center;
60
+ color: var(--a-fg-muted);
61
+ }
62
+
63
+ /* The value area: the auto attr element + any foreign unnamed child. */
64
+ :scope > [slot="value"],
65
+ :scope > :not([slot]) {
66
+ grid-area: value;
67
+ color: var(--display-field-value-color);
68
+ font-size: var(--display-field-value-size);
69
+ font-weight: var(--display-field-value-weight);
70
+ line-height: 1.3;
71
+ min-width: 0;
72
+ }
73
+
74
+ :scope > [slot="hint"] {
75
+ grid-area: hint;
76
+ color: var(--display-field-hint-color);
77
+ font-size: var(--display-field-hint-size);
78
+ line-height: 1.3;
79
+ min-width: 0;
80
+ }
81
+
82
+ /* ── Variant: masked — code/mono family + tabular numerals + tracking so
83
+ bullet-and-digit masked strings ("•••• 4242") align cleanly. Cosmetic
84
+ only; no layout change. ── */
85
+ :scope[variant="masked"] > [slot="value"],
86
+ :scope[variant="masked"] > :not([slot]) {
87
+ font-family: var(--display-field-masked-font);
88
+ font-variant-numeric: tabular-nums;
89
+ letter-spacing: var(--display-field-masked-tracking);
90
+ }
91
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `<display-field-ui>` — Display-only labeled value — a field-shaped read-out for a static or masked value (e.g. a card on file "•••• 4242"), or a labeled slot that hosts a third-party element (a Stripe Element iframe) under a proper caption. Unlike field-ui it wraps NO interactive control: no focus, no contenteditable, no form participation, no [for]/id-minting. The label names the whole field for assistive tech via role="group" + aria-labelledby, so a slotted foreign element (iframe) is announced with its caption. Reach for field-ui + input-ui when the value is EDITABLE; stat-ui for a prominent metric/KPI; description-list-ui for many read-only key/value pairs at once.
3
+ *
4
+ * @see https://ui-kit.exe.xyz/site/components/display-field
5
+ *
6
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
7
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
8
+ * run `npm run build:components`, then `npm run codegen:dts` to
9
+ * regenerate; or hand-author this file fully if rich event types are
10
+ * needed beyond what the yaml `events:` block can express.
11
+ */
12
+
13
+ import { UIElement } from '../../core/element.js';
14
+
15
+ export class UIDisplayField extends UIElement {
16
+ /** Optional caption rendered below the value in subtle style (e.g. "Expires 12/26"). Wired into the field's aria-describedby. */
17
+ hint: string;
18
+ /** Optional leading icon name shown beside the value (e.g. a lock or card-brand glyph). Lazily creates a single icon-ui child. */
19
+ icon: string;
20
+ /** The field caption. Names the whole field for assistive tech. */
21
+ label: string;
22
+ /** The static value text to display (e.g. "•••• 4242"). Ignored when the default slot already carries content — slot a foreign element OR set value=, not both. */
23
+ value: string;
24
+ /** Cosmetic value treatment. `default` renders the value in the UI font; `masked` switches to the code/mono family with tabular numerals and light letter-spacing so masked strings ("•••• 4242") align cleanly. Tokens only — no layout change. */
25
+ variant: 'default' | 'masked';
26
+ }
@@ -0,0 +1,44 @@
1
+ # display-field — Examples
2
+
3
+ A display-only labeled value. No input semantics (no focus, no
4
+ contenteditable, no form participation). For an editable field use
5
+ `field-ui` + `input-ui`; for a metric use `stat-ui`; for many read-only
6
+ pairs use `description-list-ui`.
7
+
8
+ ## Basic
9
+
10
+ ```html
11
+ <display-field-ui label="Account ID" value="acct_1Q9xK2"></display-field-ui>
12
+ ```
13
+
14
+ ## Masked value
15
+
16
+ ```html
17
+ <display-field-ui
18
+ label="Card on file"
19
+ value="•••• •••• •••• 4242"
20
+ variant="masked"></display-field-ui>
21
+ ```
22
+
23
+ ## With a leading icon and a hint
24
+
25
+ ```html
26
+ <display-field-ui
27
+ label="Card on file"
28
+ value="•••• 4242"
29
+ variant="masked"
30
+ icon="lock"
31
+ hint="Expires 12/26"></display-field-ui>
32
+ ```
33
+
34
+ ## Hosting a third-party element (Stripe Element)
35
+
36
+ Slot the foreign element as the default child; it takes the value region
37
+ under the label. The host's `role="group"` + `aria-labelledby` names it for
38
+ assistive tech.
39
+
40
+ ```html
41
+ <display-field-ui label="Card number">
42
+ <div id="card-element"><!-- stripe.elements().create('card').mount('#card-element') --></div>
43
+ </display-field-ui>
44
+ ```