@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 +23 -0
- package/USAGE.md +56 -11
- package/components/accordion/accordion.css +5 -7
- package/components/button/button.a2ui.json +5 -0
- package/components/button/button.class.js +21 -1
- package/components/button/button.css +12 -6
- package/components/button/button.yaml +8 -0
- package/components/display-field/display-field.a2ui.json +185 -0
- package/components/display-field/display-field.css +91 -0
- package/components/display-field/display-field.d.ts +26 -0
- package/components/display-field/display-field.examples.md +44 -0
- package/components/display-field/display-field.js +141 -0
- package/components/display-field/display-field.test.js +115 -0
- package/components/display-field/display-field.yaml +191 -0
- package/components/icon/icon.class.js +34 -1
- package/components/index.js +1 -0
- package/components/table/table.class.js +42 -11
- package/components/table/table.test.js +53 -0
- package/core/icons.js +14 -0
- package/custom-elements.json +5722 -2864
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +87 -79
- package/dist/web-components.sheet.js +1 -1
- package/package.json +3 -2
- package/styles/components.css +1 -0
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. [
|
|
13
|
-
3. [
|
|
14
|
-
4. [Property
|
|
15
|
-
5. [
|
|
16
|
-
6. [
|
|
17
|
-
7. [
|
|
18
|
-
8. [
|
|
19
|
-
9. [
|
|
20
|
-
10. [
|
|
21
|
-
11. [
|
|
22
|
-
12. [
|
|
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
|
-
|
|
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
|
|
152
|
-
stay inline-level, and [inline] forces
|
|
153
|
-
row-ui/col-ui/grid-ui this is moot (flex/grid
|
|
154
|
-
|
|
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
|
|
288
|
-
|
|
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
|
+
```
|