@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.
- package/CHANGELOG.md +56 -25
- package/README.md +2 -2
- package/USAGE.md +60 -15
- package/components/accordion/accordion-item.a2ui.json +30 -1
- package/components/accordion/accordion-item.yaml +33 -0
- package/components/accordion/accordion.a2ui.json +9 -0
- package/components/accordion/accordion.class.js +53 -3
- package/components/accordion/accordion.css +61 -3
- package/components/accordion/accordion.d.ts +12 -0
- package/components/accordion/accordion.yaml +17 -0
- package/components/code/code.class.js +1 -1
- 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/drawer/drawer.class.js +2 -2
- package/components/feed/feed.a2ui.json +1 -1
- package/components/feed/feed.class.js +1 -1
- package/components/feed/feed.d.ts +1 -1
- package/components/feed/feed.yaml +2 -2
- package/components/field/field.class.js +1 -1
- package/components/icon/icon.class.js +34 -1
- package/components/index.js +1 -0
- package/components/loading-overlay/loading-overlay.class.js +1 -1
- package/components/loading-overlay/loading-overlay.yaml +1 -1
- package/components/modal/modal.class.js +1 -1
- package/components/nav/nav.class.js +1 -1
- package/components/nav/nav.examples.md +4 -4
- package/components/popover/popover.class.js +1 -1
- package/components/spinner/spinner.class.js +1 -1
- package/components/spinner/spinner.yaml +1 -1
- package/components/table/table.class.js +42 -11
- package/components/table/table.test.js +53 -0
- package/components/toast/toast.class.js +1 -1
- package/core/icons.js +14 -0
- package/custom-elements.json +5723 -2865
- 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/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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
*
|
|
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
|
+
```
|