@adia-ai/web-components 0.7.23 → 0.7.25

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 (46) hide show
  1. package/CHANGELOG.md +56 -25
  2. package/README.md +2 -2
  3. package/USAGE.md +60 -15
  4. package/components/accordion/accordion-item.a2ui.json +30 -1
  5. package/components/accordion/accordion-item.yaml +33 -0
  6. package/components/accordion/accordion.a2ui.json +9 -0
  7. package/components/accordion/accordion.class.js +53 -3
  8. package/components/accordion/accordion.css +61 -3
  9. package/components/accordion/accordion.d.ts +12 -0
  10. package/components/accordion/accordion.yaml +17 -0
  11. package/components/code/code.class.js +1 -1
  12. package/components/display-field/display-field.a2ui.json +185 -0
  13. package/components/display-field/display-field.css +91 -0
  14. package/components/display-field/display-field.d.ts +26 -0
  15. package/components/display-field/display-field.examples.md +44 -0
  16. package/components/display-field/display-field.js +141 -0
  17. package/components/display-field/display-field.test.js +115 -0
  18. package/components/display-field/display-field.yaml +191 -0
  19. package/components/drawer/drawer.class.js +2 -2
  20. package/components/feed/feed.a2ui.json +1 -1
  21. package/components/feed/feed.class.js +1 -1
  22. package/components/feed/feed.d.ts +1 -1
  23. package/components/feed/feed.yaml +2 -2
  24. package/components/field/field.class.js +1 -1
  25. package/components/icon/icon.class.js +34 -1
  26. package/components/index.js +1 -0
  27. package/components/loading-overlay/loading-overlay.class.js +1 -1
  28. package/components/loading-overlay/loading-overlay.yaml +1 -1
  29. package/components/modal/modal.class.js +1 -1
  30. package/components/nav/nav.class.js +1 -1
  31. package/components/nav/nav.examples.md +4 -4
  32. package/components/popover/popover.class.js +1 -1
  33. package/components/spinner/spinner.class.js +1 -1
  34. package/components/spinner/spinner.yaml +1 -1
  35. package/components/table/table.class.js +42 -11
  36. package/components/table/table.test.js +53 -0
  37. package/components/toast/toast.class.js +1 -1
  38. package/core/icons.js +14 -0
  39. package/custom-elements.json +5723 -2865
  40. package/dist/theme-provider.min.js +1 -1
  41. package/dist/web-components.min.css +1 -1
  42. package/dist/web-components.min.js +87 -79
  43. package/dist/web-components.sheet.js +1 -1
  44. package/package.json +3 -2
  45. package/styles/components.css +1 -0
  46. package/traits/CATEGORIES.md +1 -1
@@ -38,6 +38,12 @@ export class UIAccordion extends UIElement {
38
38
 
39
39
  static properties = {
40
40
  multiple: { type: Boolean, default: false, reflect: true },
41
+ // `reflect: true` is load-bearing: the CSS ancestor selector
42
+ // (accordion.css `@scope (accordion-item-ui)`) matches
43
+ // `accordion-ui[variant="contained"]`. Default 'flat' ⇒ absent-attribute
44
+ // = current behavior. No render/JS logic keys off it — inheritance of
45
+ // the contained posture into child items is pure CSS.
46
+ variant: { type: String, default: 'flat', reflect: true }, // 'flat' | 'contained'
41
47
  };
42
48
 
43
49
  static template = () => null;
@@ -70,6 +76,10 @@ export class UIAccordionItem extends UIElement {
70
76
  static properties = {
71
77
  text: { type: String, default: '', reflect: true },
72
78
  open: { type: Boolean, default: false, reflect: true },
79
+ // Per-item posture override against the host accordion-ui's [variant]
80
+ // (see UIAccordion.variant above). Default 'flat'; CSS-only, pure
81
+ // token re-point in contained (accordion.css).
82
+ variant: { type: String, default: 'flat', reflect: true }, // 'flat' | 'contained'
73
83
  };
74
84
 
75
85
  static template = () => null;
@@ -77,13 +87,24 @@ export class UIAccordionItem extends UIElement {
77
87
  #bound = false;
78
88
 
79
89
  connected() {
80
- // Stamp header if not present
81
- if (!this.querySelector('[slot="header"]')) {
90
+ // Stamp header if not present; otherwise restore the disclosure caret
91
+ // (FN-1) — a consumer's custom [slot="header"] previously suppressed it
92
+ // entirely because the caret only ever got stamped alongside the header.
93
+ const existingHeader = this.querySelector('[slot="header"]');
94
+ if (!existingHeader) {
82
95
  const header = document.createElement('div');
83
96
  header.setAttribute('slot', 'header');
97
+ // SPEC-R8: the auto-stamped header was mouse-only (no tabindex/keydown).
98
+ // Make it keyboard-operable; a consumer-authored header keeps owning
99
+ // its own semantics (data-autofocusable marks the header WE own).
100
+ header.setAttribute('tabindex', '0');
101
+ header.setAttribute('role', 'button');
102
+ header.setAttribute('aria-expanded', 'false');
103
+ header.dataset.autofocusable = '';
84
104
 
85
105
  const label = document.createElement('span');
86
106
  label.setAttribute('slot', 'header-text');
107
+ label.dataset.autolabel = ''; // marks THIS label as prop-driven (FN-2)
87
108
  header.appendChild(label);
88
109
 
89
110
  const caret = document.createElement('icon-ui');
@@ -92,6 +113,13 @@ export class UIAccordionItem extends UIElement {
92
113
  header.appendChild(caret);
93
114
 
94
115
  this.prepend(header);
116
+ } else if (!existingHeader.querySelector('[slot="caret"]')) {
117
+ // Scope to the HEADER, not the whole item — a caret placed in
118
+ // [slot="body"] must not suppress this (Finding 10).
119
+ const caret = document.createElement('icon-ui');
120
+ caret.setAttribute('name', 'caret-down');
121
+ caret.setAttribute('slot', 'caret');
122
+ existingHeader.appendChild(caret); // restore the disclosure signifier
95
123
  }
96
124
 
97
125
  // Stamp content wrapper if not present
@@ -112,17 +140,26 @@ export class UIAccordionItem extends UIElement {
112
140
  if (!this.#bound) {
113
141
  this.#bound = true;
114
142
  this.addEventListener('click', this.#onClick);
143
+ this.addEventListener('keydown', this.#onKeydown);
115
144
  }
116
145
  }
117
146
 
118
147
  disconnected() {
119
148
  this.removeEventListener('click', this.#onClick);
149
+ this.removeEventListener('keydown', this.#onKeydown);
120
150
  this.#bound = false;
121
151
  }
122
152
 
123
153
  render() {
124
- const label = this.querySelector('[slot="header-text"]');
154
+ // Only the auto-stamped label follows [text] — an authored
155
+ // [slot="header-text"] (no data-autolabel marker) is never touched (FN-2).
156
+ const label = this.querySelector('[slot="header-text"][data-autolabel]');
125
157
  if (label) label.textContent = this.text;
158
+
159
+ // Keep aria-expanded in sync on the auto-stamped header — `open` is a
160
+ // reactive prop, so render() re-runs on every toggle (SPEC-R8).
161
+ const hdr = this.querySelector('[slot="header"][data-autofocusable]');
162
+ if (hdr) hdr.setAttribute('aria-expanded', String(this.open));
126
163
  }
127
164
 
128
165
  #onClick = (e) => {
@@ -140,4 +177,17 @@ export class UIAccordionItem extends UIElement {
140
177
  detail: { open: this.open },
141
178
  }));
142
179
  };
180
+
181
+ // SPEC-R8: Enter/Space on the auto-stamped header toggles it. Scoped to
182
+ // OUR stamped header only (data-autofocusable) — never a consumer control
183
+ // inside a custom header. Routes through header.click() → #onClick, so
184
+ // there is exactly one toggle code path (DRY).
185
+ #onKeydown = (e) => {
186
+ const header = this.querySelector('[slot="header"][data-autofocusable]');
187
+ if (!header || e.target !== header) return;
188
+ if (e.key === 'Enter' || e.key === ' ') {
189
+ e.preventDefault(); // Space: no page scroll
190
+ header.click();
191
+ }
192
+ };
143
193
  }
@@ -27,6 +27,16 @@
27
27
  /* ── Item ── */
28
28
  @scope (accordion-item-ui) {
29
29
  :where(:scope) {
30
+ /* ── Surface (variant) ──
31
+ Flat-pixel-exact defaults (SPEC-R2/AC-2): transparent bg, no border,
32
+ no header/body divider, zero body top-padding — byte-identical to the
33
+ pre-variant item. Re-pointed by :scope[variant="contained"] below
34
+ (SPEC-R1/AC-1); consumer-overridable in BOTH postures (SPEC-R3/AC-3). */
35
+ --accordion-item-bg: transparent;
36
+ --accordion-item-border: none;
37
+ --accordion-item-divider: none;
38
+ --accordion-item-body-pt: 0;
39
+
30
40
  /* ── Colors ── */
31
41
  --accordion-item-header-fg: var(--a-fg-subtle);
32
42
  --accordion-item-header-fg-hover: var(--a-fg-strong);
@@ -51,11 +61,58 @@
51
61
 
52
62
  /* ── State ── */
53
63
  --accordion-item-focus-ring: var(--a-focus-ring);
64
+
65
+ /* Header-scoped surface tokens — flat defaults keep today's look; contained
66
+ re-points them (pill flush + inner ring off, ring relocates to the item).
67
+ Kept as tokens (not [slot="header"] rules under an accordion-ui-led selector)
68
+ so slot-vocab-vs-css attributes the header slot to accordion-item.yaml. */
69
+ --accordion-item-header-radius: var(--accordion-item-radius);
70
+ --accordion-item-header-focus-shadow: var(--accordion-item-focus-ring);
54
71
  }
55
72
 
56
73
  :scope {
57
74
  box-sizing: border-box;
58
75
  display: block;
76
+ background: var(--accordion-item-bg);
77
+ border: var(--accordion-item-border);
78
+ border-radius: var(--accordion-item-radius); /* invisible in flat: no border/bg */
79
+ }
80
+
81
+ /* ═══════ Variant: contained (token-only re-point + sanctioned layout exception) ═══════
82
+ Own variant OR inherited from a contained host. CHILD combinator (`>`) so a FLAT
83
+ accordion nested inside a contained one does NOT leak contained styling to the inner
84
+ items (Finding 4); items are canonically direct children. Per-item opt-out: [variant="flat"].
85
+ Two layout touches (overflow clip + surface focus-ring relocation) are documented
86
+ exceptions in component-token-contract.md's sanctioned-exception table; bg/border/divider/
87
+ body-pt AND the header pill-radius / inner-ring (via --accordion-item-header-*) stay pure
88
+ token re-points.
89
+ NB: the inherited form MUST use `:scope` (not a literal `accordion-item-ui`) — under
90
+ @scope, only `:scope` may reference the `accordion-ui` ancestor to its LEFT; a literal
91
+ tag is confined to the scope donut and would not match the outside ancestor. */
92
+ :scope[variant="contained"],
93
+ :where(accordion-ui[variant="contained"]) > :scope:not([variant="flat"]) {
94
+ --accordion-item-bg: var(--a-bg-subtle);
95
+ --accordion-item-border: 1px solid var(--a-border-subtle);
96
+ --accordion-item-divider: 1px solid var(--a-border-subtle);
97
+ --accordion-item-body-pt: var(--a-space-2);
98
+ --accordion-item-header-radius: 0; /* pill corners flush to the surface (Finding 8) */
99
+ --accordion-item-header-focus-shadow: none; /* inner ring off; surface ring below (Finding 2) */
100
+ }
101
+
102
+ :scope[variant="contained"],
103
+ :where(accordion-ui[variant="contained"]) > :scope:not([variant="flat"]) {
104
+ overflow: hidden; /* pill + body clip to --accordion-item-radius */
105
+ }
106
+
107
+ /* Focus ring follows the SURFACE radius (AC-4); the item's own box-shadow is NOT clipped by
108
+ its own overflow:hidden. `:has(> :focus-visible)` fires when the header — the item's only
109
+ focusable DIRECT child (a body-nested control is a grandchild) — is focused, WITHOUT naming
110
+ [slot="header"] in an accordion-ui-led selector. Covers BOTH own-variant and host-inherited
111
+ forms; the inner ring is suppressed via --accordion-item-header-focus-shadow → none, so no
112
+ double ring in the attribute-less-item case (Finding 2). */
113
+ :scope[variant="contained"]:has(> :focus-visible),
114
+ :where(accordion-ui[variant="contained"]) > :scope:not([variant="flat"]):has(> :focus-visible) {
115
+ box-shadow: var(--accordion-item-focus-ring);
59
116
  }
60
117
 
61
118
  /* ── Header ── */
@@ -67,7 +124,7 @@
67
124
  justify-content: space-between;
68
125
  gap: var(--accordion-item-header-gap);
69
126
  padding: var(--accordion-item-py) var(--accordion-item-px);
70
- border-radius: var(--accordion-item-radius);
127
+ border-radius: var(--accordion-item-header-radius); /* flat: = --accordion-item-radius; contained: 0 */
71
128
  cursor: pointer;
72
129
  user-select: none;
73
130
  font-size: var(--accordion-item-header-font);
@@ -90,7 +147,7 @@
90
147
  }
91
148
 
92
149
  :scope [slot="header"]:focus-visible {
93
- box-shadow: var(--accordion-item-focus-ring);
150
+ box-shadow: var(--accordion-item-header-focus-shadow); /* flat: = --accordion-item-focus-ring; contained: none (surface ring instead) */
94
151
  }
95
152
 
96
153
  :scope[open] [slot="header"] {
@@ -125,6 +182,7 @@
125
182
 
126
183
  :scope[open] [slot="body"] {
127
184
  display: block;
128
- padding: 0 var(--accordion-item-px) var(--accordion-item-py);
185
+ padding: var(--accordion-item-body-pt) var(--accordion-item-px) var(--accordion-item-py);
186
+ border-top: var(--accordion-item-divider); /* default `none` in flat → invisible, zero layout (Finding 3) */
129
187
  }
130
188
  }
@@ -29,6 +29,14 @@ export type AccordionToggleEvent = CustomEvent<AccordionToggleEventDetail>;
29
29
  export class UIAccordion extends UIElement {
30
30
  /** Allow multiple panels to be open simultaneously */
31
31
  multiple: boolean;
32
+ /** Visual posture, inherited by child accordion-item-ui elements unless a
33
+ child sets its own [variant]. `flat` (default) renders items with no
34
+ chrome — settings pages, FAQ blocks, anywhere the accordion sits
35
+ directly on the page canvas. `contained` gives every item a bounded
36
+ surface (background + border + radius spanning header and open body,
37
+ plus a divider between them when open) — the composition to reach for
38
+ when nesting the accordion inside <card-ui>. */
39
+ variant: 'flat' | 'contained';
32
40
 
33
41
  addEventListener<K extends keyof HTMLElementEventMap>(
34
42
  type: K,
@@ -50,6 +58,10 @@ export class UIAccordionItem extends UIElement {
50
58
  open: boolean;
51
59
  /** Header text — the clickable label that toggles the section. */
52
60
  text: string;
61
+ /** Per-item posture override. Items normally inherit their host
62
+ accordion-ui's [variant] — set this directly on ONE item only to
63
+ opt it out of (`flat`) or into (`contained`) the host's posture. */
64
+ variant: 'flat' | 'contained';
53
65
 
54
66
  addEventListener<K extends keyof HTMLElementEventMap>(
55
67
  type: K,
@@ -24,6 +24,21 @@ props:
24
24
  description: Allow multiple panels to be open simultaneously
25
25
  type: boolean
26
26
  default: false
27
+ variant:
28
+ description: |-
29
+ Visual posture, inherited by child accordion-item-ui elements unless a
30
+ child sets its own [variant]. `flat` (default) renders items with no
31
+ chrome — settings pages, FAQ blocks, anywhere the accordion sits
32
+ directly on the page canvas. `contained` gives every item a bounded
33
+ surface (background + border + radius spanning header and open body,
34
+ plus a divider between them when open) — the composition to reach for
35
+ when nesting the accordion inside <card-ui>.
36
+ type: string
37
+ default: flat
38
+ enum:
39
+ - flat
40
+ - contained
41
+ reflect: true
27
42
  events:
28
43
  toggle:
29
44
  description: Fired when the accordion panel opens or closes (single-pane accordion-ui dispatch — multi-pane group emits the same event per child).
@@ -49,6 +64,8 @@ a2ui:
49
64
  reason: 'Decision rule vs tabs.'
50
65
  - rule: 'Item ordering is DOM-order; no auto-sort.'
51
66
  reason: 'Author-controlled order.'
67
+ - rule: 'Set variant="contained" for a bounded per-item surface (background, border, radius, and an open-state divider) — the composition to reach for when nesting inside <card-ui>. Default variant="flat" (no chrome) suits settings pages and FAQ blocks sitting on the page canvas.'
68
+ reason: 'Posture selection guidance (flat vs contained).'
52
69
  anti_patterns: []
53
70
  examples:
54
71
  - name: accordion-settings
@@ -26,7 +26,7 @@
26
26
  * CodeMirror 6 is lazy-loaded and mounted in place of `<pre><code>` for
27
27
  * syntax-highlighted rendering. Phase 1 shipped read-only; Phase 2 added
28
28
  * `[editable]`; Phase 3 added lint + form-association (see
29
- * `docs/specs/code-editor.md §12`).
29
+ * `.claude/docs/specs/code-editor.md §12`).
30
30
  *
31
31
  * Form participation — editable instances are Form-Associated Custom
32
32
  * Elements (FACE). Set `name`, `required`, `disabled`, `readonly` to
@@ -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
+ ```