@adia-ai/web-components 0.0.15 → 0.0.16
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/components/alert/alert.css +5 -0
- package/components/alert/alert.js +4 -2
- package/components/button/button.js +4 -1
- package/components/chat/chat-input.js +13 -2
- package/components/description-list/description-list.js +4 -3
- package/components/field/field.css +113 -63
- package/components/field/field.js +44 -142
- package/components/icon/icon.a2ui.json +1 -1
- package/components/icon/icon.css +16 -0
- package/components/icon/icon.js +18 -0
- package/components/icon/icon.yaml +6 -2
- package/components/index.js +1 -0
- package/components/input/input.a2ui.json +1 -1
- package/components/input/input.css +19 -23
- package/components/input/input.js +36 -9
- package/components/input/input.yaml +3 -1
- package/components/option-card/option-card.a2ui.json +262 -0
- package/components/option-card/option-card.css +215 -0
- package/components/option-card/option-card.js +158 -0
- package/components/option-card/option-card.yaml +234 -0
- package/components/rating/rating.a2ui.json +10 -0
- package/components/rating/rating.yaml +8 -0
- package/components/segment/segment.a2ui.json +5 -0
- package/components/segment/segment.css +2 -0
- package/components/segment/segment.js +21 -1
- package/components/segment/segment.yaml +5 -0
- package/components/textarea/textarea.css +1 -1
- package/components/textarea/textarea.js +2 -2
- package/core/data-stream.js +21 -0
- package/core/form.js +5 -0
- package/core/index.js +2 -0
- package/core/streams-bridge.js +96 -0
- package/package.json +1 -1
- package/styles/colors/semantics.css +8 -3
- package/styles/components.css +1 -0
- package/styles/prose.css +3 -7
- package/styles/tokens.css +7 -4
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
@scope (option-card-ui) {
|
|
2
|
+
:where(:scope) {
|
|
3
|
+
/* ── Container ── */
|
|
4
|
+
--option-card-padding-block: var(--a-space-3);
|
|
5
|
+
--option-card-padding-inline: var(--a-space-4);
|
|
6
|
+
--option-card-radius: var(--a-radius-md);
|
|
7
|
+
--option-card-bg: var(--a-bg);
|
|
8
|
+
--option-card-border: var(--a-border);
|
|
9
|
+
--option-card-gap-x: var(--a-space-3);
|
|
10
|
+
--option-card-gap-y: var(--a-space-1);
|
|
11
|
+
|
|
12
|
+
/* ── State: hover ── */
|
|
13
|
+
--option-card-bg-hover: var(--a-bg-muted);
|
|
14
|
+
--option-card-border-hover: var(--a-fg-subtle);
|
|
15
|
+
|
|
16
|
+
/* ── State: checked ── */
|
|
17
|
+
--option-card-bg-checked: var(--a-accent-muted);
|
|
18
|
+
--option-card-border-checked: var(--a-accent);
|
|
19
|
+
|
|
20
|
+
/* ── Indicator (CSS radio circle, same size + recipe as <radio-ui>). */
|
|
21
|
+
--option-card-radio-size: var(--a-toggle-size);
|
|
22
|
+
--option-card-radio-bg: var(--a-bg);
|
|
23
|
+
--option-card-radio-border: var(--a-border);
|
|
24
|
+
--option-card-radio-fill: var(--a-accent);
|
|
25
|
+
--option-card-radio-dot: var(--a-accent-fg);
|
|
26
|
+
|
|
27
|
+
/* ── Typography ── */
|
|
28
|
+
--option-card-heading-color: var(--a-fg);
|
|
29
|
+
--option-card-heading-color-checked: var(--a-fg-strong);
|
|
30
|
+
--option-card-heading-weight: var(--a-weight-medium);
|
|
31
|
+
--option-card-heading-size: var(--a-ui-size);
|
|
32
|
+
--option-card-desc-color: var(--a-fg-muted);
|
|
33
|
+
--option-card-desc-size: var(--a-ui-sm);
|
|
34
|
+
--option-card-desc-line-height: 1.4;
|
|
35
|
+
--option-card-icon-color: var(--a-fg-subtle);
|
|
36
|
+
--option-card-icon-color-checked: var(--a-fg-strong);
|
|
37
|
+
--option-card-icon-size: 1.5rem;
|
|
38
|
+
|
|
39
|
+
/* ── State: disabled ── */
|
|
40
|
+
--option-card-disabled-opacity: 0.6;
|
|
41
|
+
|
|
42
|
+
/* ── Focus ── */
|
|
43
|
+
--option-card-focus-ring: var(--a-focus-ring);
|
|
44
|
+
|
|
45
|
+
/* ── Transitions ── */
|
|
46
|
+
--option-card-duration: var(--a-duration-fast);
|
|
47
|
+
--option-card-easing: var(--a-easing);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Base — grid: indicator | (heading + description). Optional icon
|
|
51
|
+
sits between indicator and heading via :has() rules below. ── */
|
|
52
|
+
:scope {
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
display: grid;
|
|
55
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
56
|
+
grid-template-areas:
|
|
57
|
+
"indicator heading"
|
|
58
|
+
"indicator description";
|
|
59
|
+
column-gap: var(--option-card-gap-x);
|
|
60
|
+
row-gap: var(--option-card-gap-y);
|
|
61
|
+
padding: var(--option-card-padding-block) var(--option-card-padding-inline);
|
|
62
|
+
border: 1px solid var(--option-card-border);
|
|
63
|
+
border-radius: var(--option-card-radius);
|
|
64
|
+
background: var(--option-card-bg);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
user-select: none;
|
|
67
|
+
outline: none;
|
|
68
|
+
transition:
|
|
69
|
+
background var(--option-card-duration) var(--option-card-easing),
|
|
70
|
+
border-color var(--option-card-duration) var(--option-card-easing);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* When an icon slot is present, insert a third column for it
|
|
74
|
+
between indicator and heading. */
|
|
75
|
+
:scope:has(> [slot="icon"]) {
|
|
76
|
+
grid-template-columns: auto auto minmax(0, 1fr);
|
|
77
|
+
grid-template-areas:
|
|
78
|
+
"indicator icon heading"
|
|
79
|
+
"indicator icon description";
|
|
80
|
+
}
|
|
81
|
+
:scope > [slot="icon"] {
|
|
82
|
+
grid-area: icon;
|
|
83
|
+
align-self: start;
|
|
84
|
+
color: var(--option-card-icon-color);
|
|
85
|
+
--a-icon-size: var(--option-card-icon-size);
|
|
86
|
+
transition: color var(--option-card-duration) var(--option-card-easing);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ── Indicator — pure CSS radio circle, no separate <radio-ui>. */
|
|
90
|
+
:scope::before {
|
|
91
|
+
content: '';
|
|
92
|
+
grid-area: indicator;
|
|
93
|
+
width: var(--option-card-radio-size);
|
|
94
|
+
height: var(--option-card-radio-size);
|
|
95
|
+
border: 1.5px solid var(--option-card-radio-border);
|
|
96
|
+
border-radius: var(--a-radius-full);
|
|
97
|
+
background: var(--option-card-radio-bg);
|
|
98
|
+
align-self: start;
|
|
99
|
+
margin-block-start: 0.125rem;
|
|
100
|
+
flex-shrink: 0;
|
|
101
|
+
transition:
|
|
102
|
+
background var(--option-card-duration) var(--option-card-easing),
|
|
103
|
+
border-color var(--option-card-duration) var(--option-card-easing);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ── Slots — heading + description ── */
|
|
107
|
+
:scope > [slot="heading"] {
|
|
108
|
+
grid-area: heading;
|
|
109
|
+
color: var(--option-card-heading-color);
|
|
110
|
+
font-weight: var(--option-card-heading-weight);
|
|
111
|
+
font-size: var(--option-card-heading-size);
|
|
112
|
+
transition: color var(--option-card-duration) var(--option-card-easing);
|
|
113
|
+
}
|
|
114
|
+
:scope > [slot="description"] {
|
|
115
|
+
grid-area: description;
|
|
116
|
+
color: var(--option-card-desc-color);
|
|
117
|
+
font-size: var(--option-card-desc-size);
|
|
118
|
+
line-height: var(--option-card-desc-line-height);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ── Default slot — "spillover" content revealed when checked.
|
|
122
|
+
Aligns with the heading/description column (skips the indicator
|
|
123
|
+
gutter). Common pattern: an "Other" option with a free-text
|
|
124
|
+
textarea, conditional follow-up fields, etc. ── */
|
|
125
|
+
:scope > :not([slot]) {
|
|
126
|
+
grid-column: 2 / -1;
|
|
127
|
+
margin-block-start: var(--a-space-3);
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
:scope:has(> [slot="icon"]) > :not([slot]) {
|
|
131
|
+
grid-column: 3 / -1;
|
|
132
|
+
}
|
|
133
|
+
:scope[checked] > :not([slot]) {
|
|
134
|
+
display: block;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ── State: hover (not checked, not disabled) ── */
|
|
138
|
+
:scope:not([checked]):not([disabled]):hover {
|
|
139
|
+
background: var(--option-card-bg-hover);
|
|
140
|
+
border-color: var(--option-card-border-hover);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ── State: checked — accent border + tinted bg + filled radio.
|
|
144
|
+
The indicator becomes an accent disc with a centered dot of
|
|
145
|
+
--option-card-radio-dot at 60% of the size, mirroring
|
|
146
|
+
radio-ui's recipe (radio.css:75-78). Done with a radial
|
|
147
|
+
gradient so a single pseudo-element carries both layers. */
|
|
148
|
+
:scope[checked] {
|
|
149
|
+
background: var(--option-card-bg-checked);
|
|
150
|
+
border-color: var(--option-card-border-checked);
|
|
151
|
+
}
|
|
152
|
+
:scope[checked]::before {
|
|
153
|
+
border-color: var(--option-card-radio-fill);
|
|
154
|
+
background:
|
|
155
|
+
radial-gradient(
|
|
156
|
+
circle,
|
|
157
|
+
var(--option-card-radio-dot) 0 30%,
|
|
158
|
+
var(--option-card-radio-fill) 30% 100%
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
/* Heading + icon shift to a strong color when checked — gives the
|
|
162
|
+
selected card a clear text-level emphasis on top of the bg/border
|
|
163
|
+
state, so picking is unambiguous beyond the radio dot alone. */
|
|
164
|
+
:scope[checked] > [slot="heading"] {
|
|
165
|
+
color: var(--option-card-heading-color-checked);
|
|
166
|
+
}
|
|
167
|
+
:scope[checked] > [slot="icon"] {
|
|
168
|
+
color: var(--option-card-icon-color-checked);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ── Layout: tile — icon top-left, indicator top-right, heading +
|
|
172
|
+
description below, all left-aligned. Used for hero pickers
|
|
173
|
+
(data source, role, plan tiles) where the icon is a primary
|
|
174
|
+
brand cue rather than secondary chrome. ── */
|
|
175
|
+
:scope[layout="tile"] {
|
|
176
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
177
|
+
grid-template-areas:
|
|
178
|
+
"icon indicator"
|
|
179
|
+
"heading heading"
|
|
180
|
+
"description description";
|
|
181
|
+
column-gap: var(--option-card-gap-x);
|
|
182
|
+
row-gap: var(--option-card-gap-y);
|
|
183
|
+
padding: var(--a-space-4);
|
|
184
|
+
align-items: start;
|
|
185
|
+
}
|
|
186
|
+
:scope[layout="tile"] > [slot="icon"] {
|
|
187
|
+
grid-area: icon;
|
|
188
|
+
justify-self: start;
|
|
189
|
+
align-self: start;
|
|
190
|
+
--option-card-icon-size: 1.75rem;
|
|
191
|
+
}
|
|
192
|
+
:scope[layout="tile"]::before {
|
|
193
|
+
grid-area: indicator;
|
|
194
|
+
align-self: start;
|
|
195
|
+
justify-self: end;
|
|
196
|
+
margin-block-start: 0;
|
|
197
|
+
}
|
|
198
|
+
:scope[layout="tile"] > [slot="heading"] {
|
|
199
|
+
margin-block-start: var(--a-space-2);
|
|
200
|
+
}
|
|
201
|
+
:scope[layout="tile"] > :not([slot]) {
|
|
202
|
+
grid-column: 1 / -1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ── State: disabled ── */
|
|
206
|
+
:scope[disabled] {
|
|
207
|
+
cursor: not-allowed;
|
|
208
|
+
opacity: var(--option-card-disabled-opacity);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* ── Focus ── */
|
|
212
|
+
:scope:focus-visible {
|
|
213
|
+
box-shadow: var(--option-card-focus-ring);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <option-card-ui> — Selectable card with radio semantics.
|
|
3
|
+
*
|
|
4
|
+
* A "rich radio" — single-select-of-N where each option carries a
|
|
5
|
+
* heading, optional description, and optional leading icon. Siblings
|
|
6
|
+
* with the same `name` form a radiogroup. The whole card is the click
|
|
7
|
+
* target; a CSS-rendered radio circle in the top-left signals state.
|
|
8
|
+
*
|
|
9
|
+
* <option-card-ui name="use-case" value="build" checked
|
|
10
|
+
* heading="I'm building a product"
|
|
11
|
+
* description="Spinning up a new project — design, ship, iterate.">
|
|
12
|
+
* </option-card-ui>
|
|
13
|
+
*
|
|
14
|
+
* Rich content via slots:
|
|
15
|
+
*
|
|
16
|
+
* <option-card-ui name="plan" value="pro">
|
|
17
|
+
* <span slot="heading">Pro <badge-ui text="14-day trial"></badge-ui></span>
|
|
18
|
+
* <span slot="description">Unlimited members · advanced charts</span>
|
|
19
|
+
* </option-card-ui>
|
|
20
|
+
*
|
|
21
|
+
* Spillover content via the default slot — any unslotted child shows
|
|
22
|
+
* only when the card is `[checked]`. Used for "Other"-style fields:
|
|
23
|
+
*
|
|
24
|
+
* <option-card-ui name="reason" value="other"
|
|
25
|
+
* heading="Something else">
|
|
26
|
+
* <textarea-ui name="reason-detail" rows="3"></textarea-ui>
|
|
27
|
+
* </option-card-ui>
|
|
28
|
+
*
|
|
29
|
+
* Sibling navigation: arrow keys move focus and selection; Space/Enter
|
|
30
|
+
* select. Form-associated via AdiaFormElement, so `name=value` submits
|
|
31
|
+
* with the parent form when the card is checked.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { AdiaFormElement } from '../../core/form.js';
|
|
35
|
+
|
|
36
|
+
class AdiaOptionCard extends AdiaFormElement {
|
|
37
|
+
static properties = {
|
|
38
|
+
...AdiaFormElement.properties,
|
|
39
|
+
checked: { type: Boolean, default: false, reflect: true },
|
|
40
|
+
heading: { type: String, default: '', reflect: true },
|
|
41
|
+
description: { type: String, default: '', reflect: true },
|
|
42
|
+
icon: { type: String, default: '', reflect: true },
|
|
43
|
+
layout: { type: String, default: 'default', reflect: true },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
static template = () => null;
|
|
47
|
+
|
|
48
|
+
connected() {
|
|
49
|
+
super.connected();
|
|
50
|
+
this.setAttribute('role', 'radio');
|
|
51
|
+
this.setAttribute('tabindex', '0');
|
|
52
|
+
this.#ensureLayout();
|
|
53
|
+
this.addEventListener('click', this.#select);
|
|
54
|
+
this.addEventListener('keydown', this.#onKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disconnected() {
|
|
58
|
+
super.disconnected();
|
|
59
|
+
this.removeEventListener('click', this.#select);
|
|
60
|
+
this.removeEventListener('keydown', this.#onKey);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render() {
|
|
64
|
+
this.setAttribute('aria-checked', String(this.checked));
|
|
65
|
+
if (this.disabled) this.setAttribute('aria-disabled', 'true');
|
|
66
|
+
else this.removeAttribute('aria-disabled');
|
|
67
|
+
|
|
68
|
+
// Keep slotted content in sync with attrs (only when slot was
|
|
69
|
+
// auto-stamped from the attr — don't clobber consumer-authored
|
|
70
|
+
// rich content).
|
|
71
|
+
const h = this.querySelector(':scope > [slot="heading"]');
|
|
72
|
+
if (h && h.dataset.fromAttr === 'true') h.textContent = this.heading || '';
|
|
73
|
+
const d = this.querySelector(':scope > [slot="description"]');
|
|
74
|
+
if (d && d.dataset.fromAttr === 'true') d.textContent = this.description || '';
|
|
75
|
+
const i = this.querySelector(':scope > [slot="icon"]');
|
|
76
|
+
if (i && i.dataset.fromAttr === 'true') i.setAttribute('name', this.icon || '');
|
|
77
|
+
|
|
78
|
+
if (this.checked) this.syncValue(this.value || 'on');
|
|
79
|
+
else this.syncValue('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Private ───────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/** Stamp heading / description / icon from attributes when the
|
|
85
|
+
* consumer hasn't already provided slotted content. */
|
|
86
|
+
#ensureLayout() {
|
|
87
|
+
if (this.heading && !this.querySelector(':scope > [slot="heading"]')) {
|
|
88
|
+
const el = document.createElement('span');
|
|
89
|
+
el.setAttribute('slot', 'heading');
|
|
90
|
+
el.dataset.fromAttr = 'true';
|
|
91
|
+
el.textContent = this.heading;
|
|
92
|
+
this.appendChild(el);
|
|
93
|
+
}
|
|
94
|
+
if (this.description && !this.querySelector(':scope > [slot="description"]')) {
|
|
95
|
+
const el = document.createElement('span');
|
|
96
|
+
el.setAttribute('slot', 'description');
|
|
97
|
+
el.dataset.fromAttr = 'true';
|
|
98
|
+
el.textContent = this.description;
|
|
99
|
+
this.appendChild(el);
|
|
100
|
+
}
|
|
101
|
+
if (this.icon && !this.querySelector(':scope > [slot="icon"]')) {
|
|
102
|
+
const el = document.createElement('icon-ui');
|
|
103
|
+
el.setAttribute('slot', 'icon');
|
|
104
|
+
el.dataset.fromAttr = 'true';
|
|
105
|
+
el.setAttribute('name', this.icon);
|
|
106
|
+
this.appendChild(el);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#select = () => {
|
|
111
|
+
if (this.disabled || this.readonly || this.checked) return;
|
|
112
|
+
const group = this.#group();
|
|
113
|
+
if (group) {
|
|
114
|
+
for (const el of group.querySelectorAll(`option-card-ui[name="${this.name}"]`)) {
|
|
115
|
+
if (el !== this && el.checked) el.checked = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.checked = true;
|
|
119
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
#onKey = (e) => {
|
|
123
|
+
// Don't intercept keys when focus is in a nested form control
|
|
124
|
+
// (textarea, input, contenteditable). Otherwise Space/Enter would
|
|
125
|
+
// block typing and arrow keys would navigate siblings instead of
|
|
126
|
+
// moving the cursor.
|
|
127
|
+
if (e.target !== this) return;
|
|
128
|
+
if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); this.#select(); return; }
|
|
129
|
+
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
const next = this.#sibling(1);
|
|
132
|
+
if (next) { next.focus(); next.click(); }
|
|
133
|
+
}
|
|
134
|
+
if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
const prev = this.#sibling(-1);
|
|
137
|
+
if (prev) { prev.focus(); prev.click(); }
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/** The radiogroup root — explicit `<fieldset>` / `[role="radiogroup"]`
|
|
142
|
+
* if present, else the immediate parent. */
|
|
143
|
+
#group() {
|
|
144
|
+
return this.closest('fieldset, [role="radiogroup"]') || this.parentElement;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#sibling(dir) {
|
|
148
|
+
const group = this.#group();
|
|
149
|
+
const items = [...(group?.querySelectorAll(`option-card-ui[name="${this.name}"]`) || [])];
|
|
150
|
+
const i = items.indexOf(this);
|
|
151
|
+
if (i < 0) return null;
|
|
152
|
+
return items[(i + dir + items.length) % items.length] || null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
customElements.define('option-card-ui', AdiaOptionCard);
|
|
157
|
+
|
|
158
|
+
export { AdiaOptionCard };
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Edit this file; run `npm run build:components` to regenerate a2ui.json.
|
|
2
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
3
|
+
name: AdiaOptionCard
|
|
4
|
+
tag: option-card-ui
|
|
5
|
+
component: OptionCard
|
|
6
|
+
category: input
|
|
7
|
+
version: 1
|
|
8
|
+
description: >-
|
|
9
|
+
Selectable card with radio semantics. A "rich radio" — single-select
|
|
10
|
+
one of N where each option carries a heading, optional description,
|
|
11
|
+
and optional leading icon. Siblings sharing a `name` form a
|
|
12
|
+
radiogroup. The whole card is the click target; a CSS-rendered radio
|
|
13
|
+
circle in the top-left signals state. Form-associated, so `name=value`
|
|
14
|
+
submits with the parent form when checked.
|
|
15
|
+
props:
|
|
16
|
+
name:
|
|
17
|
+
description: Form control name. Siblings sharing a name form a radiogroup.
|
|
18
|
+
type: string
|
|
19
|
+
default: ""
|
|
20
|
+
value:
|
|
21
|
+
description: Form value submitted when checked (defaults to `on` if empty).
|
|
22
|
+
type: string
|
|
23
|
+
default: ""
|
|
24
|
+
checked:
|
|
25
|
+
description: Whether this card is currently selected.
|
|
26
|
+
type: boolean
|
|
27
|
+
default: false
|
|
28
|
+
reflect: true
|
|
29
|
+
disabled:
|
|
30
|
+
description: Disables interaction and dims the card.
|
|
31
|
+
type: boolean
|
|
32
|
+
default: false
|
|
33
|
+
reflect: true
|
|
34
|
+
required:
|
|
35
|
+
description: Marks the radiogroup as requiring a selection for form validation.
|
|
36
|
+
type: boolean
|
|
37
|
+
default: false
|
|
38
|
+
reflect: true
|
|
39
|
+
heading:
|
|
40
|
+
description: Heading text. Stamped into a [slot="heading"] span when no slotted heading is provided.
|
|
41
|
+
type: string
|
|
42
|
+
default: ""
|
|
43
|
+
reflect: true
|
|
44
|
+
description:
|
|
45
|
+
description: Description text. Stamped into a [slot="description"] span when no slotted description is provided.
|
|
46
|
+
type: string
|
|
47
|
+
default: ""
|
|
48
|
+
reflect: true
|
|
49
|
+
icon:
|
|
50
|
+
description: Optional Phosphor icon name. Stamped as a leading <icon-ui slot="icon"> when set.
|
|
51
|
+
type: string
|
|
52
|
+
default: ""
|
|
53
|
+
reflect: true
|
|
54
|
+
layout:
|
|
55
|
+
description: >-
|
|
56
|
+
Internal layout. `default` puts the indicator on the left and the icon
|
|
57
|
+
adjacent. `tile` stacks vertically — icon top-left, indicator top-right,
|
|
58
|
+
heading + description below — for hero pickers (source / role / plan
|
|
59
|
+
tiles) where the icon is a primary brand cue.
|
|
60
|
+
type: string
|
|
61
|
+
default: default
|
|
62
|
+
enum: [default, tile]
|
|
63
|
+
reflect: true
|
|
64
|
+
events:
|
|
65
|
+
change:
|
|
66
|
+
description: Fired when this card becomes selected (bubbles).
|
|
67
|
+
slots:
|
|
68
|
+
heading:
|
|
69
|
+
description: Rich heading content. Overrides the `heading` attribute when present.
|
|
70
|
+
description:
|
|
71
|
+
description: Rich description content. Overrides the `description` attribute when present.
|
|
72
|
+
icon:
|
|
73
|
+
description: Custom icon element. Overrides the `icon` attribute when present.
|
|
74
|
+
default:
|
|
75
|
+
description: >-
|
|
76
|
+
Spillover content revealed only when the card is checked — typically a
|
|
77
|
+
follow-up form field (e.g. a textarea on an "Other" option, conditional
|
|
78
|
+
inputs that depend on the selection). Aligns with the heading/description
|
|
79
|
+
column; hidden via `display: none` when not checked.
|
|
80
|
+
states:
|
|
81
|
+
- name: idle
|
|
82
|
+
description: Default, ready for interaction.
|
|
83
|
+
- name: hover
|
|
84
|
+
description: Pointer over a non-checked card.
|
|
85
|
+
selector: ":not([checked]):not([disabled]):hover"
|
|
86
|
+
- name: checked
|
|
87
|
+
description: Selected — accent border, tinted background, filled radio circle.
|
|
88
|
+
attribute: checked
|
|
89
|
+
- name: disabled
|
|
90
|
+
description: Non-interactive; dimmed.
|
|
91
|
+
attribute: disabled
|
|
92
|
+
- name: focused
|
|
93
|
+
description: Keyboard focus ring.
|
|
94
|
+
selector: ":focus-visible"
|
|
95
|
+
traits:
|
|
96
|
+
- focusable
|
|
97
|
+
tokens:
|
|
98
|
+
--option-card-padding-block:
|
|
99
|
+
description: Vertical padding inside the card.
|
|
100
|
+
--option-card-padding-inline:
|
|
101
|
+
description: Horizontal padding inside the card.
|
|
102
|
+
--option-card-radius:
|
|
103
|
+
description: Card corner radius.
|
|
104
|
+
--option-card-bg:
|
|
105
|
+
description: Default background.
|
|
106
|
+
--option-card-border:
|
|
107
|
+
description: Default border color.
|
|
108
|
+
--option-card-gap-x:
|
|
109
|
+
description: Horizontal gap between indicator (and icon) and content.
|
|
110
|
+
--option-card-gap-y:
|
|
111
|
+
description: Vertical gap between heading and description.
|
|
112
|
+
--option-card-bg-hover:
|
|
113
|
+
description: Hover background (non-checked).
|
|
114
|
+
--option-card-border-hover:
|
|
115
|
+
description: Hover border color (non-checked).
|
|
116
|
+
--option-card-bg-checked:
|
|
117
|
+
description: Background when checked.
|
|
118
|
+
--option-card-border-checked:
|
|
119
|
+
description: Border color when checked.
|
|
120
|
+
--option-card-radio-size:
|
|
121
|
+
description: Diameter of the indicator circle.
|
|
122
|
+
--option-card-radio-bg:
|
|
123
|
+
description: Indicator background when not checked.
|
|
124
|
+
--option-card-radio-border:
|
|
125
|
+
description: Indicator border when not checked.
|
|
126
|
+
--option-card-radio-fill:
|
|
127
|
+
description: Indicator fill color when checked.
|
|
128
|
+
--option-card-radio-dot:
|
|
129
|
+
description: Inner dot color when checked.
|
|
130
|
+
--option-card-heading-color:
|
|
131
|
+
description: Heading text color.
|
|
132
|
+
--option-card-heading-color-checked:
|
|
133
|
+
description: Heading text color when the card is checked (defaults to `--a-fg-strong` so the selected card reads with extra emphasis on top of the bg/border state).
|
|
134
|
+
--option-card-heading-weight:
|
|
135
|
+
description: Heading font weight.
|
|
136
|
+
--option-card-heading-size:
|
|
137
|
+
description: Heading font size.
|
|
138
|
+
--option-card-desc-color:
|
|
139
|
+
description: Description text color.
|
|
140
|
+
--option-card-desc-size:
|
|
141
|
+
description: Description font size.
|
|
142
|
+
--option-card-desc-line-height:
|
|
143
|
+
description: Description line height.
|
|
144
|
+
--option-card-icon-color:
|
|
145
|
+
description: Leading icon color when the card is not checked.
|
|
146
|
+
--option-card-icon-color-checked:
|
|
147
|
+
description: Leading icon color when the card is checked.
|
|
148
|
+
--option-card-icon-size:
|
|
149
|
+
description: Leading icon size (sets `--a-icon-size` on the slotted icon-ui).
|
|
150
|
+
--option-card-disabled-opacity:
|
|
151
|
+
description: Opacity multiplier when disabled.
|
|
152
|
+
--option-card-focus-ring:
|
|
153
|
+
description: Focus ring (box-shadow value).
|
|
154
|
+
--option-card-duration:
|
|
155
|
+
description: Transition duration for hover / checked state changes.
|
|
156
|
+
--option-card-easing:
|
|
157
|
+
description: Transition easing.
|
|
158
|
+
a2ui:
|
|
159
|
+
rules: []
|
|
160
|
+
anti_patterns: []
|
|
161
|
+
examples:
|
|
162
|
+
- name: use-case-picker
|
|
163
|
+
description: A four-option pick-one for "what brings you here" — radio-card behavior with heading + description per option.
|
|
164
|
+
a2ui: >-
|
|
165
|
+
[
|
|
166
|
+
{
|
|
167
|
+
"id": "root",
|
|
168
|
+
"component": "Column",
|
|
169
|
+
"gap": "2",
|
|
170
|
+
"children": ["build", "explore", "migrate", "evaluate"]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "build",
|
|
174
|
+
"component": "OptionCard",
|
|
175
|
+
"name": "use-case",
|
|
176
|
+
"value": "build",
|
|
177
|
+
"checked": true,
|
|
178
|
+
"heading": "I'm building a product",
|
|
179
|
+
"description": "Spinning up a new project — design, ship, iterate."
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
"id": "explore",
|
|
183
|
+
"component": "OptionCard",
|
|
184
|
+
"name": "use-case",
|
|
185
|
+
"value": "explore",
|
|
186
|
+
"heading": "I'm exploring the product",
|
|
187
|
+
"description": "Kicking the tires before bringing a team along."
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"id": "migrate",
|
|
191
|
+
"component": "OptionCard",
|
|
192
|
+
"name": "use-case",
|
|
193
|
+
"value": "migrate",
|
|
194
|
+
"heading": "I'm migrating from another tool",
|
|
195
|
+
"description": "Moving an existing workspace and want a smooth port."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"id": "evaluate",
|
|
199
|
+
"component": "OptionCard",
|
|
200
|
+
"name": "use-case",
|
|
201
|
+
"value": "evaluate",
|
|
202
|
+
"heading": "I'm evaluating for my team",
|
|
203
|
+
"description": "Comparing options and want to dig into specifics."
|
|
204
|
+
}
|
|
205
|
+
]
|
|
206
|
+
keywords:
|
|
207
|
+
- option
|
|
208
|
+
- card
|
|
209
|
+
- radio
|
|
210
|
+
- select
|
|
211
|
+
- choice
|
|
212
|
+
- picker
|
|
213
|
+
- tier
|
|
214
|
+
- plan
|
|
215
|
+
- onboarding
|
|
216
|
+
- registration
|
|
217
|
+
synonyms:
|
|
218
|
+
radio:
|
|
219
|
+
- option-card
|
|
220
|
+
- radio
|
|
221
|
+
- select
|
|
222
|
+
picker:
|
|
223
|
+
- option-card
|
|
224
|
+
- radio
|
|
225
|
+
- select
|
|
226
|
+
tier:
|
|
227
|
+
- option-card
|
|
228
|
+
- card
|
|
229
|
+
- plan
|
|
230
|
+
related:
|
|
231
|
+
- radio
|
|
232
|
+
- card
|
|
233
|
+
- check
|
|
234
|
+
- segmented
|
|
@@ -55,6 +55,16 @@
|
|
|
55
55
|
"description": "Current rating value (0..max, supports 0.5 steps when allowHalf)",
|
|
56
56
|
"type": "number",
|
|
57
57
|
"default": 0
|
|
58
|
+
},
|
|
59
|
+
"variant": {
|
|
60
|
+
"description": "Color variant — applied via `:scope[variant=...]` selectors in CSS, no JS reflection needed",
|
|
61
|
+
"type": "string",
|
|
62
|
+
"enum": [
|
|
63
|
+
"",
|
|
64
|
+
"accent",
|
|
65
|
+
"warning"
|
|
66
|
+
],
|
|
67
|
+
"default": ""
|
|
58
68
|
}
|
|
59
69
|
},
|
|
60
70
|
"required": [
|
|
@@ -43,6 +43,14 @@ props:
|
|
|
43
43
|
description: Current rating value (0..max, supports 0.5 steps when allowHalf)
|
|
44
44
|
type: number
|
|
45
45
|
default: 0
|
|
46
|
+
variant:
|
|
47
|
+
description: Color variant — applied via `:scope[variant=...]` selectors in CSS, no JS reflection needed
|
|
48
|
+
type: string
|
|
49
|
+
default: ""
|
|
50
|
+
enum:
|
|
51
|
+
- ""
|
|
52
|
+
- accent
|
|
53
|
+
- warning
|
|
46
54
|
events:
|
|
47
55
|
"[object Object]":
|
|
48
56
|
description: "Fired on [object Object]."
|
|
@@ -21,6 +21,11 @@
|
|
|
21
21
|
"type": "boolean",
|
|
22
22
|
"default": false
|
|
23
23
|
},
|
|
24
|
+
"icon": {
|
|
25
|
+
"description": "Phosphor icon name. Rendered as a leading <icon-ui> child stamped on render.",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"default": ""
|
|
28
|
+
},
|
|
24
29
|
"selected": {
|
|
25
30
|
"description": "Whether this segment is currently selected. Managed by the parent Segmented container; don't set multiple siblings selected.",
|
|
26
31
|
"type": "boolean",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
:where(:scope) {
|
|
3
3
|
/* ── Layout ── */
|
|
4
4
|
--segment-px: var(--a-ui-px);
|
|
5
|
+
--segment-gap: var(--a-space-1);
|
|
5
6
|
--segment-radius: calc(var(--a-radius) - 2px);
|
|
6
7
|
|
|
7
8
|
/* ── Typography ── */
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
justify-content: center;
|
|
31
32
|
align-self: stretch;
|
|
32
33
|
min-width: 0;
|
|
34
|
+
gap: var(--segment-gap);
|
|
33
35
|
|
|
34
36
|
padding-inline: var(--segment-px);
|
|
35
37
|
|