@xmesh/system-design 0.0.1
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/README.md +472 -0
- package/assets/brand-lockup-dark.svg +9 -0
- package/assets/brand-lockup-light.svg +9 -0
- package/assets/brand-mark.svg +9 -0
- package/colors_and_type.css +11 -0
- package/dist/lit/components/alert/index.css +201 -0
- package/dist/lit/components/alert/index.d.ts +25 -0
- package/dist/lit/components/alert/index.js +191 -0
- package/dist/lit/components/app-bar/index.css +80 -0
- package/dist/lit/components/app-bar/index.d.ts +19 -0
- package/dist/lit/components/app-bar/index.js +120 -0
- package/dist/lit/components/artifact/index.css +166 -0
- package/dist/lit/components/artifact/index.d.ts +37 -0
- package/dist/lit/components/artifact/index.js +294 -0
- package/dist/lit/components/autocomplete/index.css +171 -0
- package/dist/lit/components/autocomplete/index.d.ts +47 -0
- package/dist/lit/components/autocomplete/index.js +404 -0
- package/dist/lit/components/avatar/index.css +62 -0
- package/dist/lit/components/avatar/index.d.ts +19 -0
- package/dist/lit/components/avatar/index.js +112 -0
- package/dist/lit/components/avatar-group/index.css +60 -0
- package/dist/lit/components/avatar-group/index.d.ts +19 -0
- package/dist/lit/components/avatar-group/index.js +97 -0
- package/dist/lit/components/badge/index.css +72 -0
- package/dist/lit/components/badge/index.d.ts +18 -0
- package/dist/lit/components/badge/index.js +115 -0
- package/dist/lit/components/brand-mark/index.css +109 -0
- package/dist/lit/components/brand-mark/index.d.ts +24 -0
- package/dist/lit/components/brand-mark/index.js +116 -0
- package/dist/lit/components/breadcrumbs/index.css +91 -0
- package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
- package/dist/lit/components/breadcrumbs/index.js +104 -0
- package/dist/lit/components/bubble/index.css +182 -0
- package/dist/lit/components/bubble/index.d.ts +72 -0
- package/dist/lit/components/bubble/index.js +617 -0
- package/dist/lit/components/button/index.css +342 -0
- package/dist/lit/components/button/index.d.ts +32 -0
- package/dist/lit/components/button/index.js +202 -0
- package/dist/lit/components/card/index.css +99 -0
- package/dist/lit/components/card/index.d.ts +20 -0
- package/dist/lit/components/card/index.js +133 -0
- package/dist/lit/components/chat/index.css +292 -0
- package/dist/lit/components/chat/index.d.ts +74 -0
- package/dist/lit/components/chat/index.js +589 -0
- package/dist/lit/components/checkbox/index.css +126 -0
- package/dist/lit/components/checkbox/index.d.ts +21 -0
- package/dist/lit/components/checkbox/index.js +138 -0
- package/dist/lit/components/chip/index.css +145 -0
- package/dist/lit/components/chip/index.d.ts +30 -0
- package/dist/lit/components/chip/index.js +230 -0
- package/dist/lit/components/chip-group/index.css +19 -0
- package/dist/lit/components/chip-group/index.d.ts +24 -0
- package/dist/lit/components/chip-group/index.js +171 -0
- package/dist/lit/components/code/index.css +42 -0
- package/dist/lit/components/code/index.d.ts +12 -0
- package/dist/lit/components/code/index.js +68 -0
- package/dist/lit/components/composer/index.css +548 -0
- package/dist/lit/components/composer/index.d.ts +67 -0
- package/dist/lit/components/composer/index.js +713 -0
- package/dist/lit/components/data-table/index.css +166 -0
- package/dist/lit/components/data-table/index.d.ts +55 -0
- package/dist/lit/components/data-table/index.js +390 -0
- package/dist/lit/components/dialog/index.css +124 -0
- package/dist/lit/components/dialog/index.d.ts +24 -0
- package/dist/lit/components/dialog/index.js +199 -0
- package/dist/lit/components/divider/index.css +27 -0
- package/dist/lit/components/divider/index.d.ts +13 -0
- package/dist/lit/components/divider/index.js +67 -0
- package/dist/lit/components/empty-state/index.css +69 -0
- package/dist/lit/components/empty-state/index.d.ts +21 -0
- package/dist/lit/components/empty-state/index.js +123 -0
- package/dist/lit/components/expansion-panel/index.css +120 -0
- package/dist/lit/components/expansion-panel/index.d.ts +22 -0
- package/dist/lit/components/expansion-panel/index.js +174 -0
- package/dist/lit/components/field/index.css +223 -0
- package/dist/lit/components/field/index.d.ts +106 -0
- package/dist/lit/components/field/index.js +388 -0
- package/dist/lit/components/file-input/index.css +257 -0
- package/dist/lit/components/file-input/index.d.ts +30 -0
- package/dist/lit/components/file-input/index.js +298 -0
- package/dist/lit/components/form/index.css +29 -0
- package/dist/lit/components/form/index.d.ts +38 -0
- package/dist/lit/components/form/index.js +192 -0
- package/dist/lit/components/grid/index.css +53 -0
- package/dist/lit/components/grid/index.d.ts +14 -0
- package/dist/lit/components/grid/index.js +82 -0
- package/dist/lit/components/kbd/index.css +35 -0
- package/dist/lit/components/kbd/index.d.ts +11 -0
- package/dist/lit/components/kbd/index.js +43 -0
- package/dist/lit/components/list/index.css +15 -0
- package/dist/lit/components/list/index.d.ts +28 -0
- package/dist/lit/components/list/index.js +188 -0
- package/dist/lit/components/list-item/index.css +119 -0
- package/dist/lit/components/list-item/index.d.ts +20 -0
- package/dist/lit/components/list-item/index.js +127 -0
- package/dist/lit/components/menu/index.css +94 -0
- package/dist/lit/components/menu/index.d.ts +47 -0
- package/dist/lit/components/menu/index.js +386 -0
- package/dist/lit/components/navigation-drawer/index.css +114 -0
- package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
- package/dist/lit/components/navigation-drawer/index.js +218 -0
- package/dist/lit/components/overlay/index.css +171 -0
- package/dist/lit/components/overlay/index.d.ts +65 -0
- package/dist/lit/components/overlay/index.js +566 -0
- package/dist/lit/components/pagination/index.css +102 -0
- package/dist/lit/components/pagination/index.d.ts +22 -0
- package/dist/lit/components/pagination/index.js +184 -0
- package/dist/lit/components/primitives/index.css +504 -0
- package/dist/lit/components/primitives/index.d.ts +25 -0
- package/dist/lit/components/primitives/index.js +283 -0
- package/dist/lit/components/progress/index.css +143 -0
- package/dist/lit/components/progress/index.d.ts +23 -0
- package/dist/lit/components/progress/index.js +180 -0
- package/dist/lit/components/radio-group/index.css +178 -0
- package/dist/lit/components/radio-group/index.d.ts +35 -0
- package/dist/lit/components/radio-group/index.js +292 -0
- package/dist/lit/components/select/index.css +151 -0
- package/dist/lit/components/select/index.d.ts +50 -0
- package/dist/lit/components/select/index.js +390 -0
- package/dist/lit/components/sidebar-item/index.css +133 -0
- package/dist/lit/components/sidebar-item/index.d.ts +20 -0
- package/dist/lit/components/sidebar-item/index.js +105 -0
- package/dist/lit/components/skeleton/index.css +81 -0
- package/dist/lit/components/skeleton/index.d.ts +19 -0
- package/dist/lit/components/skeleton/index.js +119 -0
- package/dist/lit/components/slider/index.css +171 -0
- package/dist/lit/components/slider/index.d.ts +36 -0
- package/dist/lit/components/slider/index.js +302 -0
- package/dist/lit/components/snackbar/index.css +279 -0
- package/dist/lit/components/snackbar/index.d.ts +33 -0
- package/dist/lit/components/snackbar/index.js +195 -0
- package/dist/lit/components/stack/index.css +41 -0
- package/dist/lit/components/stack/index.d.ts +20 -0
- package/dist/lit/components/stack/index.js +103 -0
- package/dist/lit/components/switch/index.css +126 -0
- package/dist/lit/components/switch/index.d.ts +17 -0
- package/dist/lit/components/switch/index.js +116 -0
- package/dist/lit/components/table/index.css +85 -0
- package/dist/lit/components/table/index.d.ts +25 -0
- package/dist/lit/components/table/index.js +139 -0
- package/dist/lit/components/tabs/index.css +116 -0
- package/dist/lit/components/tabs/index.d.ts +49 -0
- package/dist/lit/components/tabs/index.js +320 -0
- package/dist/lit/components/text-field/index.css +90 -0
- package/dist/lit/components/text-field/index.d.ts +17 -0
- package/dist/lit/components/text-field/index.js +101 -0
- package/dist/lit/components/textarea/index.css +55 -0
- package/dist/lit/components/textarea/index.d.ts +26 -0
- package/dist/lit/components/textarea/index.js +124 -0
- package/dist/lit/components/tooltip/index.css +37 -0
- package/dist/lit/components/tooltip/index.d.ts +31 -0
- package/dist/lit/components/tooltip/index.js +196 -0
- package/dist/lit/components/validation/index.css +386 -0
- package/dist/lit/components/validation/index.d.ts +45 -0
- package/dist/lit/components/validation/index.js +318 -0
- package/dist/lit/index.d.ts +50 -0
- package/dist/lit/index.js +59 -0
- package/package.json +81 -0
- package/styles/README.md +346 -0
- package/styles/_elevation.css +24 -0
- package/styles/_fonts.css +6 -0
- package/styles/_layout.css +37 -0
- package/styles/_primitives.css +154 -0
- package/styles/_scroll.css +75 -0
- package/styles/_semantic.css +146 -0
- package/styles/_space.css +61 -0
- package/styles/_type.css +139 -0
- package/styles/_xmesh-extensions.css +232 -0
- package/styles/index.css +44 -0
- package/styles/md3/_color.css +102 -0
- package/styles/md3/_elevation.css +26 -0
- package/styles/md3/_motion.css +35 -0
- package/styles/md3/_shape.css +22 -0
- package/styles/md3/_state.css +22 -0
- package/styles/md3/_type.css +111 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
xm-radio-group + xm-radio — single-choice group (Story 2.6).
|
|
3
|
+
|
|
4
|
+
Two BEM blocks in one file (multi-element component, AD-4):
|
|
5
|
+
• `radio-group` — the form-associated value owner (extends XmField). Lays
|
|
6
|
+
out the group label, the radiogroup options column, and the helper/error
|
|
7
|
+
row. Chrome semantics (disabled / error) mirror the field family.
|
|
8
|
+
• `radio` — a selectable, presentational child: the hairline circle + coral
|
|
9
|
+
dot + label. Value/selection/disabled are driven by the group.
|
|
10
|
+
|
|
11
|
+
Surface & ink (AD-13): both ride the inverse-surface card tier — label + radio
|
|
12
|
+
text are inverse-on-surface ink; the circle hairline is outline-variant. The
|
|
13
|
+
SELECTED radio fills its dot + ring with the single coral accent
|
|
14
|
+
(--md-sys-color-primary). Coral = selection only, never severity (AD-11). The
|
|
15
|
+
error string in the message row carries severity via the warn icon + copy.
|
|
16
|
+
|
|
17
|
+
BEM blocks: `radio-group` and `radio`. Both registered in
|
|
18
|
+
scripts/check-bem.sh STRICT_BLOCKS.
|
|
19
|
+
============================================ */
|
|
20
|
+
|
|
21
|
+
/* ──────────────── radio-group block ──────────────── */
|
|
22
|
+
.radio-group {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: var(--s-2);
|
|
26
|
+
width: 100%;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.radio-group__label {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: var(--s-1);
|
|
33
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
34
|
+
font:
|
|
35
|
+
var(--md-sys-typescale-label-large-weight)
|
|
36
|
+
var(--md-sys-typescale-label-large-size) /
|
|
37
|
+
var(--md-sys-typescale-label-large-line-height)
|
|
38
|
+
var(--md-sys-typescale-label-large-font);
|
|
39
|
+
}
|
|
40
|
+
.radio-group__label-text {
|
|
41
|
+
letter-spacing: 0;
|
|
42
|
+
}
|
|
43
|
+
.radio-group__required {
|
|
44
|
+
color: var(--md-sys-color-primary);
|
|
45
|
+
font-weight: 700;
|
|
46
|
+
line-height: 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.radio-group__options {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
gap: var(--s-2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.radio-group__message {
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: flex-start;
|
|
58
|
+
gap: var(--s-1);
|
|
59
|
+
min-height: 1em;
|
|
60
|
+
font:
|
|
61
|
+
var(--md-sys-typescale-body-small-weight)
|
|
62
|
+
var(--md-sys-typescale-body-small-size) /
|
|
63
|
+
var(--md-sys-typescale-body-small-line-height)
|
|
64
|
+
var(--md-sys-typescale-body-small-font);
|
|
65
|
+
}
|
|
66
|
+
.radio-group__message--helper {
|
|
67
|
+
color: var(--xm-color-inverse-on-surface-muted);
|
|
68
|
+
}
|
|
69
|
+
.radio-group__message--error {
|
|
70
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
71
|
+
}
|
|
72
|
+
.radio-group__error-icon {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
flex-shrink: 0;
|
|
76
|
+
margin-top: 1px;
|
|
77
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
78
|
+
}
|
|
79
|
+
.radio-group__message-text {
|
|
80
|
+
flex: 1;
|
|
81
|
+
min-width: 0;
|
|
82
|
+
text-wrap: pretty;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ──────────────── radio block ──────────────── */
|
|
86
|
+
/* Focus ring lives on the host (the focusable element) — the inner circle gets
|
|
87
|
+
the coral halo when the host is focus-visible. */
|
|
88
|
+
:host(:focus-visible) {
|
|
89
|
+
outline: none;
|
|
90
|
+
}
|
|
91
|
+
:host(:focus-visible) .radio__circle {
|
|
92
|
+
border-color: var(--md-sys-color-primary);
|
|
93
|
+
box-shadow: var(--xm-state-focus-ring);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.radio {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: var(--s-2);
|
|
100
|
+
cursor: pointer;
|
|
101
|
+
user-select: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.radio__circle {
|
|
105
|
+
position: relative;
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
width: 18px;
|
|
110
|
+
height: 18px;
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
box-sizing: border-box;
|
|
113
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
114
|
+
border-radius: var(--md-sys-shape-corner-full);
|
|
115
|
+
background: transparent;
|
|
116
|
+
transition:
|
|
117
|
+
border-color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
|
|
118
|
+
box-shadow var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
|
|
119
|
+
}
|
|
120
|
+
.radio:hover .radio__circle {
|
|
121
|
+
border-color: var(--md-sys-color-outline);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* The inner dot — hidden until selected, then coral. Fades in (opacity), not a
|
|
125
|
+
scale-in reveal (POLICIES.md rule 8: no scale-in). */
|
|
126
|
+
.radio__dot {
|
|
127
|
+
width: 10px;
|
|
128
|
+
height: 10px;
|
|
129
|
+
border-radius: var(--md-sys-shape-corner-full);
|
|
130
|
+
background: var(--md-sys-color-primary);
|
|
131
|
+
opacity: 0;
|
|
132
|
+
transition: opacity var(--md-sys-motion-duration-short3)
|
|
133
|
+
var(--md-sys-motion-easing-standard);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.radio__label {
|
|
137
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
138
|
+
font:
|
|
139
|
+
var(--md-sys-typescale-body-medium-weight)
|
|
140
|
+
var(--md-sys-typescale-body-medium-size) /
|
|
141
|
+
var(--md-sys-typescale-body-medium-line-height)
|
|
142
|
+
var(--md-sys-typescale-body-medium-font);
|
|
143
|
+
letter-spacing: 0;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Selected — coral ring + visible dot. */
|
|
147
|
+
.radio--checked .radio__circle {
|
|
148
|
+
border-color: var(--md-sys-color-primary);
|
|
149
|
+
}
|
|
150
|
+
.radio--checked .radio__dot {
|
|
151
|
+
opacity: 1;
|
|
152
|
+
}
|
|
153
|
+
.radio--checked:hover .radio__circle {
|
|
154
|
+
border-color: var(--md-sys-color-primary);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Disabled — reduced emphasis, hover reverted, no pointer. */
|
|
158
|
+
.radio--disabled {
|
|
159
|
+
cursor: not-allowed;
|
|
160
|
+
}
|
|
161
|
+
.radio--disabled .radio__circle {
|
|
162
|
+
opacity: 0.45;
|
|
163
|
+
box-shadow: none;
|
|
164
|
+
}
|
|
165
|
+
.radio--disabled .radio__label {
|
|
166
|
+
color: var(--xm-color-inverse-on-surface-disabled);
|
|
167
|
+
}
|
|
168
|
+
.radio--disabled:hover .radio__circle {
|
|
169
|
+
border-color: var(--md-sys-color-outline-variant);
|
|
170
|
+
}
|
|
171
|
+
.radio--disabled.radio--checked:hover .radio__circle {
|
|
172
|
+
border-color: var(--md-sys-color-primary);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* Group disabled cascades to the option text + label. */
|
|
176
|
+
.radio-group--disabled .radio-group__label {
|
|
177
|
+
color: var(--xm-color-inverse-on-surface-disabled);
|
|
178
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { LitElement } from "lit";
|
|
2
|
+
import type { PropertyValues, TemplateResult } from "lit";
|
|
3
|
+
import { XmField } from "../field/index.js";
|
|
4
|
+
export declare class XmRadio extends LitElement {
|
|
5
|
+
/** The primitive value this radio contributes to the group when selected. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** Own disabled — OR'd with the group's disabled by the group. */
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
/** Driven by the group; never set directly by authors. */
|
|
10
|
+
checked: boolean;
|
|
11
|
+
/** Effective disabled the group computes (group OR own). */
|
|
12
|
+
groupDisabled: boolean;
|
|
13
|
+
get effectiveDisabled(): boolean;
|
|
14
|
+
render(): TemplateResult;
|
|
15
|
+
}
|
|
16
|
+
export declare class XmRadioGroup extends XmField {
|
|
17
|
+
private get _radios();
|
|
18
|
+
private get _enabledRadios();
|
|
19
|
+
connectedCallback(): void;
|
|
20
|
+
disconnectedCallback(): void;
|
|
21
|
+
protected updated(changed: PropertyValues<this>): void;
|
|
22
|
+
/** Reflect the group's selected value + disabled onto each child, and set the
|
|
23
|
+
roving tabindex (selected, else first enabled, is the only tab stop). */
|
|
24
|
+
private _syncRadios;
|
|
25
|
+
private _select;
|
|
26
|
+
private _onClick;
|
|
27
|
+
private _onKeydown;
|
|
28
|
+
render(): TemplateResult;
|
|
29
|
+
}
|
|
30
|
+
declare global {
|
|
31
|
+
interface HTMLElementTagNameMap {
|
|
32
|
+
"xm-radio": XmRadio;
|
|
33
|
+
"xm-radio-group": XmRadioGroup;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/*
|
|
2
|
+
radio-group/index.ts — <xm-radio-group> + <xm-radio> (Story 2.6).
|
|
3
|
+
|
|
4
|
+
A multi-element component (AD-4 / NFR-4): one folder, two custom elements.
|
|
5
|
+
|
|
6
|
+
<xm-radio-group label="Plan" value="pro" name="plan">
|
|
7
|
+
<xm-radio value="free">Free</xm-radio>
|
|
8
|
+
<xm-radio value="pro">Pro</xm-radio>
|
|
9
|
+
<xm-radio value="team" disabled>Team</xm-radio>
|
|
10
|
+
</xm-radio-group>
|
|
11
|
+
|
|
12
|
+
Value ownership lives on the GROUP, never the children (AD-6a / AD-12): the
|
|
13
|
+
group extends XmField, so it owns the chrome (label / helper / error / disabled)
|
|
14
|
+
AND the form association — xm-form reads the GROUP's public `value` getter, not
|
|
15
|
+
any radio's shadow root. An <xm-radio> is a presentational, selectable child:
|
|
16
|
+
it renders the coral ring/dot + label and reports `role="radio"` + aria-checked,
|
|
17
|
+
but it does not register itself with the form.
|
|
18
|
+
|
|
19
|
+
Keyboard (radiogroup APG, AD-9a): roving tabindex — only the selected (else the
|
|
20
|
+
first enabled) radio is in the tab order. Inside the group ↑/↓/←/→ move AND
|
|
21
|
+
select the next/previous enabled radio (wrapping), Home/End jump to first/last
|
|
22
|
+
enabled, Space selects the focused radio. Selection commits emit `change` with
|
|
23
|
+
detail.value = the selected radio's PRIMITIVE value (AD-8a).
|
|
24
|
+
|
|
25
|
+
Disabled is OR-propagated (AD-6a): a radio is disabled if the GROUP is disabled
|
|
26
|
+
OR the radio sets its own `disabled` — neither can re-enable the other.
|
|
27
|
+
|
|
28
|
+
Shadow DOM. Lit is a bare `import` (peer dep).
|
|
29
|
+
*/
|
|
30
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
31
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
33
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
34
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
35
|
+
};
|
|
36
|
+
import { LitElement, html, nothing } from "lit";
|
|
37
|
+
import { customElement, property } from "lit/decorators.js";
|
|
38
|
+
import { XmField } from "../field/index.js";
|
|
39
|
+
const RADIO_CSS = new URL("../radio-group/index.css", import.meta.url).href;
|
|
40
|
+
const PRIMITIVES_CSS = new URL("../primitives/index.css", import.meta.url).href;
|
|
41
|
+
/* ── <xm-radio> — selectable, presentational child ──────────────────── */
|
|
42
|
+
let XmRadio = class XmRadio extends LitElement {
|
|
43
|
+
constructor() {
|
|
44
|
+
super(...arguments);
|
|
45
|
+
/** The primitive value this radio contributes to the group when selected. */
|
|
46
|
+
this.value = "";
|
|
47
|
+
/** Own disabled — OR'd with the group's disabled by the group. */
|
|
48
|
+
this.disabled = false;
|
|
49
|
+
/** Driven by the group; never set directly by authors. */
|
|
50
|
+
this.checked = false;
|
|
51
|
+
/** Effective disabled the group computes (group OR own). */
|
|
52
|
+
this.groupDisabled = false;
|
|
53
|
+
}
|
|
54
|
+
get effectiveDisabled() {
|
|
55
|
+
return this.disabled || this.groupDisabled;
|
|
56
|
+
}
|
|
57
|
+
render() {
|
|
58
|
+
const cls = [
|
|
59
|
+
"radio",
|
|
60
|
+
this.checked ? "radio--checked" : "",
|
|
61
|
+
this.effectiveDisabled ? "radio--disabled" : "",
|
|
62
|
+
]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(" ");
|
|
65
|
+
return html `
|
|
66
|
+
<link rel="stylesheet" href="${PRIMITIVES_CSS}" />
|
|
67
|
+
<link rel="stylesheet" href="${RADIO_CSS}" />
|
|
68
|
+
<style>
|
|
69
|
+
:host {
|
|
70
|
+
display: inline-flex;
|
|
71
|
+
}
|
|
72
|
+
:host([hidden]) {
|
|
73
|
+
display: none;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
<span class="${cls}">
|
|
77
|
+
<span class="radio__circle" aria-hidden="true">
|
|
78
|
+
<span class="radio__dot"></span>
|
|
79
|
+
</span>
|
|
80
|
+
<span class="radio__label"><slot></slot></span>
|
|
81
|
+
</span>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
__decorate([
|
|
86
|
+
property({ type: String })
|
|
87
|
+
], XmRadio.prototype, "value", void 0);
|
|
88
|
+
__decorate([
|
|
89
|
+
property({ type: Boolean, reflect: true })
|
|
90
|
+
], XmRadio.prototype, "disabled", void 0);
|
|
91
|
+
__decorate([
|
|
92
|
+
property({ type: Boolean, reflect: true })
|
|
93
|
+
], XmRadio.prototype, "checked", void 0);
|
|
94
|
+
__decorate([
|
|
95
|
+
property({ type: Boolean, reflect: true, attribute: "group-disabled" })
|
|
96
|
+
], XmRadio.prototype, "groupDisabled", void 0);
|
|
97
|
+
XmRadio = __decorate([
|
|
98
|
+
customElement("xm-radio")
|
|
99
|
+
], XmRadio);
|
|
100
|
+
export { XmRadio };
|
|
101
|
+
/* ── <xm-radio-group> — the form-associated value owner ─────────────── */
|
|
102
|
+
let XmRadioGroup = class XmRadioGroup extends XmField {
|
|
103
|
+
constructor() {
|
|
104
|
+
super(...arguments);
|
|
105
|
+
this._onClick = (e) => {
|
|
106
|
+
if (this.effectiveDisabled)
|
|
107
|
+
return;
|
|
108
|
+
const radio = e.target.closest("xm-radio");
|
|
109
|
+
if (radio && this._radios.includes(radio))
|
|
110
|
+
this._select(radio);
|
|
111
|
+
};
|
|
112
|
+
this._onKeydown = (e) => {
|
|
113
|
+
if (this.effectiveDisabled)
|
|
114
|
+
return;
|
|
115
|
+
const enabled = this._enabledRadios;
|
|
116
|
+
if (enabled.length === 0)
|
|
117
|
+
return;
|
|
118
|
+
const current = e.target.closest("xm-radio");
|
|
119
|
+
const curIdx = current ? enabled.indexOf(current) : -1;
|
|
120
|
+
switch (e.key) {
|
|
121
|
+
case "ArrowDown":
|
|
122
|
+
case "ArrowRight": {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
const next = enabled[(curIdx + 1 + enabled.length) % enabled.length];
|
|
125
|
+
if (next)
|
|
126
|
+
this._select(next);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "ArrowUp":
|
|
130
|
+
case "ArrowLeft": {
|
|
131
|
+
e.preventDefault();
|
|
132
|
+
const prev = enabled[(curIdx - 1 + enabled.length) % enabled.length];
|
|
133
|
+
if (prev)
|
|
134
|
+
this._select(prev);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "Home": {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
const first = enabled[0];
|
|
140
|
+
if (first)
|
|
141
|
+
this._select(first);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "End": {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
const last = enabled[enabled.length - 1];
|
|
147
|
+
if (last)
|
|
148
|
+
this._select(last);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case " ":
|
|
152
|
+
case "Spacebar": {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
if (current)
|
|
155
|
+
this._select(current);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
get _radios() {
|
|
162
|
+
return Array.from(this.querySelectorAll("xm-radio"));
|
|
163
|
+
}
|
|
164
|
+
get _enabledRadios() {
|
|
165
|
+
return this._radios.filter((r) => !r.disabled && !this.effectiveDisabled);
|
|
166
|
+
}
|
|
167
|
+
connectedCallback() {
|
|
168
|
+
super.connectedCallback();
|
|
169
|
+
this.addEventListener("keydown", this._onKeydown);
|
|
170
|
+
this.addEventListener("click", this._onClick);
|
|
171
|
+
// Radios may upgrade after the group connects; sync on the next frame.
|
|
172
|
+
this.updateComplete.then(() => this._syncRadios());
|
|
173
|
+
}
|
|
174
|
+
disconnectedCallback() {
|
|
175
|
+
super.disconnectedCallback();
|
|
176
|
+
this.removeEventListener("keydown", this._onKeydown);
|
|
177
|
+
this.removeEventListener("click", this._onClick);
|
|
178
|
+
}
|
|
179
|
+
updated(changed) {
|
|
180
|
+
super.updated?.(changed);
|
|
181
|
+
this._syncRadios();
|
|
182
|
+
}
|
|
183
|
+
/** Reflect the group's selected value + disabled onto each child, and set the
|
|
184
|
+
roving tabindex (selected, else first enabled, is the only tab stop). */
|
|
185
|
+
_syncRadios() {
|
|
186
|
+
const radios = this._radios;
|
|
187
|
+
if (radios.length === 0)
|
|
188
|
+
return;
|
|
189
|
+
const selected = radios.find((r) => r.value === this._value) ?? null;
|
|
190
|
+
const firstEnabled = radios.find((r) => !r.disabled && !this.effectiveDisabled);
|
|
191
|
+
const tabTarget = selected && !selected.disabled ? selected : firstEnabled;
|
|
192
|
+
for (const r of radios) {
|
|
193
|
+
r.groupDisabled = this.effectiveDisabled;
|
|
194
|
+
r.checked = r === selected;
|
|
195
|
+
r.setAttribute("role", "radio");
|
|
196
|
+
r.setAttribute("aria-checked", r.checked ? "true" : "false");
|
|
197
|
+
if (r.effectiveDisabled)
|
|
198
|
+
r.setAttribute("aria-disabled", "true");
|
|
199
|
+
else
|
|
200
|
+
r.removeAttribute("aria-disabled");
|
|
201
|
+
const focusable = r === tabTarget && !r.effectiveDisabled;
|
|
202
|
+
r.tabIndex = focusable ? 0 : -1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
_select(radio, focus = true) {
|
|
206
|
+
if (radio.effectiveDisabled)
|
|
207
|
+
return;
|
|
208
|
+
if (radio.value === this._value) {
|
|
209
|
+
if (focus)
|
|
210
|
+
radio.focus();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this._value = radio.value;
|
|
214
|
+
this.internals.setFormValue(this._value);
|
|
215
|
+
this._syncRadios();
|
|
216
|
+
if (focus)
|
|
217
|
+
radio.focus();
|
|
218
|
+
this.dispatchEvent(new CustomEvent("change", {
|
|
219
|
+
bubbles: true,
|
|
220
|
+
composed: true,
|
|
221
|
+
detail: { value: radio.value },
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
render() {
|
|
225
|
+
const cls = [
|
|
226
|
+
"radio-group",
|
|
227
|
+
this.effectiveDisabled ? "radio-group--disabled" : "",
|
|
228
|
+
this.isError ? "radio-group--error" : "",
|
|
229
|
+
]
|
|
230
|
+
.filter(Boolean)
|
|
231
|
+
.join(" ");
|
|
232
|
+
const helperText = this.isError ? this.error : this.helper;
|
|
233
|
+
const ctrl = this.controlAria;
|
|
234
|
+
return html `
|
|
235
|
+
<link rel="stylesheet" href="${PRIMITIVES_CSS}" />
|
|
236
|
+
<link rel="stylesheet" href="${RADIO_CSS}" />
|
|
237
|
+
<style>
|
|
238
|
+
:host {
|
|
239
|
+
display: block;
|
|
240
|
+
}
|
|
241
|
+
:host([hidden]) {
|
|
242
|
+
display: none;
|
|
243
|
+
}
|
|
244
|
+
</style>
|
|
245
|
+
<div class="${cls}">
|
|
246
|
+
${this.label
|
|
247
|
+
? html `<span class="radio-group__label" id="${ctrl.id}-label">
|
|
248
|
+
<span class="radio-group__label-text">${this.label}</span>
|
|
249
|
+
${this.required
|
|
250
|
+
? html `<span class="radio-group__required" aria-hidden="true"
|
|
251
|
+
>*</span
|
|
252
|
+
>`
|
|
253
|
+
: nothing}
|
|
254
|
+
</span>`
|
|
255
|
+
: nothing}
|
|
256
|
+
|
|
257
|
+
<div
|
|
258
|
+
class="radio-group__options"
|
|
259
|
+
role="radiogroup"
|
|
260
|
+
aria-labelledby="${this.label ? `${ctrl.id}-label` : nothing}"
|
|
261
|
+
aria-label="${this.label ? nothing : "Options"}"
|
|
262
|
+
aria-describedby="${helperText ? ctrl.describedBy : nothing}"
|
|
263
|
+
aria-invalid="${ctrl.invalid ?? nothing}"
|
|
264
|
+
aria-required="${ctrl.required ?? nothing}"
|
|
265
|
+
>
|
|
266
|
+
<slot @slotchange=${() => this._syncRadios()}></slot>
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
${helperText
|
|
270
|
+
? html `<div
|
|
271
|
+
class="radio-group__message ${this.isError
|
|
272
|
+
? "radio-group__message--error"
|
|
273
|
+
: "radio-group__message--helper"}"
|
|
274
|
+
id="${ctrl.describedBy}"
|
|
275
|
+
role=${this.isError ? "alert" : nothing}
|
|
276
|
+
>
|
|
277
|
+
${this.isError
|
|
278
|
+
? html `<span class="radio-group__error-icon" aria-hidden="true">
|
|
279
|
+
<xm-warn-icon size="14"></xm-warn-icon>
|
|
280
|
+
</span>`
|
|
281
|
+
: nothing}
|
|
282
|
+
<span class="radio-group__message-text">${helperText}</span>
|
|
283
|
+
</div>`
|
|
284
|
+
: nothing}
|
|
285
|
+
</div>
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
XmRadioGroup = __decorate([
|
|
290
|
+
customElement("xm-radio-group")
|
|
291
|
+
], XmRadioGroup);
|
|
292
|
+
export { XmRadioGroup };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
xm-select — single-select dropdown for XmField (Story 2.3).
|
|
3
|
+
|
|
4
|
+
Composes XmField (chrome) + xm-overlay (anchored listbox). This file styles
|
|
5
|
+
ONLY the bespoke parts: the closed control (a full-bleed <button> inside the
|
|
6
|
+
base's .field__control wrapper), the chevron, and the listbox panel content
|
|
7
|
+
projected into the overlay. Positioning / elevation of the panel surface is
|
|
8
|
+
owned by xm-overlay; here we style the option rows inside it.
|
|
9
|
+
|
|
10
|
+
Surface & ink (AD-13):
|
|
11
|
+
• Closed control rides the inverse-surface card tier → inverse-on-surface
|
|
12
|
+
ink, inverse-on-surface-muted for the placeholder.
|
|
13
|
+
• The listbox rows sit on the overlay's inverse-surface panel → ink stays
|
|
14
|
+
inverse-on-surface; hover uses the MD3 state layer.
|
|
15
|
+
• The SELECTED option tints with the coral primary-container and pairs
|
|
16
|
+
on-primary-container ink (matching that surface). Coral = selection only,
|
|
17
|
+
never severity (AD-11).
|
|
18
|
+
|
|
19
|
+
BEM block: `select`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
|
|
20
|
+
============================================ */
|
|
21
|
+
|
|
22
|
+
/* ---------- Closed control — full-bleed button in the field wrapper ---------- */
|
|
23
|
+
.select__control {
|
|
24
|
+
display: flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
gap: var(--s-2);
|
|
27
|
+
flex: 1;
|
|
28
|
+
min-width: 0;
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
appearance: none;
|
|
33
|
+
border: none;
|
|
34
|
+
outline: none;
|
|
35
|
+
background: transparent;
|
|
36
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
37
|
+
font:
|
|
38
|
+
var(--md-sys-typescale-body-large-weight)
|
|
39
|
+
var(--md-sys-typescale-body-large-size) /
|
|
40
|
+
var(--md-sys-typescale-body-large-line-height)
|
|
41
|
+
var(--md-sys-typescale-body-large-font);
|
|
42
|
+
padding: 0 var(--s-3);
|
|
43
|
+
text-align: left;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
.select__control:disabled {
|
|
47
|
+
cursor: not-allowed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ---------- Value / placeholder ---------- */
|
|
51
|
+
.select__value {
|
|
52
|
+
flex: 1;
|
|
53
|
+
min-width: 0;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
text-overflow: ellipsis;
|
|
57
|
+
}
|
|
58
|
+
.select__value--placeholder {
|
|
59
|
+
color: var(--xm-color-inverse-on-surface-muted);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ---------- Chevron — rotates on open (short3, standard) ---------- */
|
|
63
|
+
.select__chevron {
|
|
64
|
+
display: inline-flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
68
|
+
transition: transform var(--md-sys-motion-duration-short3)
|
|
69
|
+
var(--md-sys-motion-easing-standard);
|
|
70
|
+
}
|
|
71
|
+
.select__chevron--open {
|
|
72
|
+
transform: rotate(180deg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* ---------- Listbox panel content (inside the overlay) ---------- */
|
|
76
|
+
.select__listbox {
|
|
77
|
+
list-style: none;
|
|
78
|
+
margin: 0;
|
|
79
|
+
padding: 0;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: var(--s-0-5);
|
|
83
|
+
min-width: 180px;
|
|
84
|
+
max-height: 320px;
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* ---------- Option rows ---------- */
|
|
89
|
+
.select__option {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: var(--s-2);
|
|
93
|
+
padding: var(--s-2) var(--s-3);
|
|
94
|
+
border-radius: var(--md-sys-shape-corner-small);
|
|
95
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
96
|
+
font:
|
|
97
|
+
var(--md-sys-typescale-body-medium-weight)
|
|
98
|
+
var(--md-sys-typescale-body-medium-size) /
|
|
99
|
+
var(--md-sys-typescale-body-medium-line-height)
|
|
100
|
+
var(--md-sys-typescale-body-medium-font);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
user-select: none;
|
|
103
|
+
}
|
|
104
|
+
.select__option-label {
|
|
105
|
+
flex: 1;
|
|
106
|
+
min-width: 0;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
text-overflow: ellipsis;
|
|
110
|
+
}
|
|
111
|
+
.select__option-check {
|
|
112
|
+
display: inline-flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
flex-shrink: 0;
|
|
115
|
+
color: var(--md-sys-color-primary);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* Active (keyboard / pointer hover) — the MD3 state layer over the panel ink. */
|
|
119
|
+
.select__option--active {
|
|
120
|
+
background: color-mix(
|
|
121
|
+
in oklab,
|
|
122
|
+
var(--md-sys-color-inverse-on-surface)
|
|
123
|
+
var(--md-sys-state-hover-state-layer-opacity),
|
|
124
|
+
transparent
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Selected — coral primary-container tint + on-primary-container ink (AD-13). */
|
|
129
|
+
.select__option--selected {
|
|
130
|
+
background: var(--md-sys-color-primary-container);
|
|
131
|
+
color: var(--md-sys-color-on-primary-container);
|
|
132
|
+
}
|
|
133
|
+
.select__option--selected .select__option-check {
|
|
134
|
+
color: var(--md-sys-color-on-primary-container);
|
|
135
|
+
}
|
|
136
|
+
/* Selected AND active — keep the coral identity, lift slightly via the state
|
|
137
|
+
layer so the keyboard focus is still legible on the tint. */
|
|
138
|
+
.select__option--selected.select__option--active {
|
|
139
|
+
background: color-mix(
|
|
140
|
+
in oklab,
|
|
141
|
+
var(--md-sys-color-on-primary-container)
|
|
142
|
+
var(--md-sys-state-hover-state-layer-opacity),
|
|
143
|
+
var(--md-sys-color-primary-container)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Disabled option — reduced emphasis, no pointer. */
|
|
148
|
+
.select__option--disabled {
|
|
149
|
+
opacity: 0.4;
|
|
150
|
+
cursor: not-allowed;
|
|
151
|
+
}
|