@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.
Files changed (175) hide show
  1. package/README.md +472 -0
  2. package/assets/brand-lockup-dark.svg +9 -0
  3. package/assets/brand-lockup-light.svg +9 -0
  4. package/assets/brand-mark.svg +9 -0
  5. package/colors_and_type.css +11 -0
  6. package/dist/lit/components/alert/index.css +201 -0
  7. package/dist/lit/components/alert/index.d.ts +25 -0
  8. package/dist/lit/components/alert/index.js +191 -0
  9. package/dist/lit/components/app-bar/index.css +80 -0
  10. package/dist/lit/components/app-bar/index.d.ts +19 -0
  11. package/dist/lit/components/app-bar/index.js +120 -0
  12. package/dist/lit/components/artifact/index.css +166 -0
  13. package/dist/lit/components/artifact/index.d.ts +37 -0
  14. package/dist/lit/components/artifact/index.js +294 -0
  15. package/dist/lit/components/autocomplete/index.css +171 -0
  16. package/dist/lit/components/autocomplete/index.d.ts +47 -0
  17. package/dist/lit/components/autocomplete/index.js +404 -0
  18. package/dist/lit/components/avatar/index.css +62 -0
  19. package/dist/lit/components/avatar/index.d.ts +19 -0
  20. package/dist/lit/components/avatar/index.js +112 -0
  21. package/dist/lit/components/avatar-group/index.css +60 -0
  22. package/dist/lit/components/avatar-group/index.d.ts +19 -0
  23. package/dist/lit/components/avatar-group/index.js +97 -0
  24. package/dist/lit/components/badge/index.css +72 -0
  25. package/dist/lit/components/badge/index.d.ts +18 -0
  26. package/dist/lit/components/badge/index.js +115 -0
  27. package/dist/lit/components/brand-mark/index.css +109 -0
  28. package/dist/lit/components/brand-mark/index.d.ts +24 -0
  29. package/dist/lit/components/brand-mark/index.js +116 -0
  30. package/dist/lit/components/breadcrumbs/index.css +91 -0
  31. package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
  32. package/dist/lit/components/breadcrumbs/index.js +104 -0
  33. package/dist/lit/components/bubble/index.css +182 -0
  34. package/dist/lit/components/bubble/index.d.ts +72 -0
  35. package/dist/lit/components/bubble/index.js +617 -0
  36. package/dist/lit/components/button/index.css +342 -0
  37. package/dist/lit/components/button/index.d.ts +32 -0
  38. package/dist/lit/components/button/index.js +202 -0
  39. package/dist/lit/components/card/index.css +99 -0
  40. package/dist/lit/components/card/index.d.ts +20 -0
  41. package/dist/lit/components/card/index.js +133 -0
  42. package/dist/lit/components/chat/index.css +292 -0
  43. package/dist/lit/components/chat/index.d.ts +74 -0
  44. package/dist/lit/components/chat/index.js +589 -0
  45. package/dist/lit/components/checkbox/index.css +126 -0
  46. package/dist/lit/components/checkbox/index.d.ts +21 -0
  47. package/dist/lit/components/checkbox/index.js +138 -0
  48. package/dist/lit/components/chip/index.css +145 -0
  49. package/dist/lit/components/chip/index.d.ts +30 -0
  50. package/dist/lit/components/chip/index.js +230 -0
  51. package/dist/lit/components/chip-group/index.css +19 -0
  52. package/dist/lit/components/chip-group/index.d.ts +24 -0
  53. package/dist/lit/components/chip-group/index.js +171 -0
  54. package/dist/lit/components/code/index.css +42 -0
  55. package/dist/lit/components/code/index.d.ts +12 -0
  56. package/dist/lit/components/code/index.js +68 -0
  57. package/dist/lit/components/composer/index.css +548 -0
  58. package/dist/lit/components/composer/index.d.ts +67 -0
  59. package/dist/lit/components/composer/index.js +713 -0
  60. package/dist/lit/components/data-table/index.css +166 -0
  61. package/dist/lit/components/data-table/index.d.ts +55 -0
  62. package/dist/lit/components/data-table/index.js +390 -0
  63. package/dist/lit/components/dialog/index.css +124 -0
  64. package/dist/lit/components/dialog/index.d.ts +24 -0
  65. package/dist/lit/components/dialog/index.js +199 -0
  66. package/dist/lit/components/divider/index.css +27 -0
  67. package/dist/lit/components/divider/index.d.ts +13 -0
  68. package/dist/lit/components/divider/index.js +67 -0
  69. package/dist/lit/components/empty-state/index.css +69 -0
  70. package/dist/lit/components/empty-state/index.d.ts +21 -0
  71. package/dist/lit/components/empty-state/index.js +123 -0
  72. package/dist/lit/components/expansion-panel/index.css +120 -0
  73. package/dist/lit/components/expansion-panel/index.d.ts +22 -0
  74. package/dist/lit/components/expansion-panel/index.js +174 -0
  75. package/dist/lit/components/field/index.css +223 -0
  76. package/dist/lit/components/field/index.d.ts +106 -0
  77. package/dist/lit/components/field/index.js +388 -0
  78. package/dist/lit/components/file-input/index.css +257 -0
  79. package/dist/lit/components/file-input/index.d.ts +30 -0
  80. package/dist/lit/components/file-input/index.js +298 -0
  81. package/dist/lit/components/form/index.css +29 -0
  82. package/dist/lit/components/form/index.d.ts +38 -0
  83. package/dist/lit/components/form/index.js +192 -0
  84. package/dist/lit/components/grid/index.css +53 -0
  85. package/dist/lit/components/grid/index.d.ts +14 -0
  86. package/dist/lit/components/grid/index.js +82 -0
  87. package/dist/lit/components/kbd/index.css +35 -0
  88. package/dist/lit/components/kbd/index.d.ts +11 -0
  89. package/dist/lit/components/kbd/index.js +43 -0
  90. package/dist/lit/components/list/index.css +15 -0
  91. package/dist/lit/components/list/index.d.ts +28 -0
  92. package/dist/lit/components/list/index.js +188 -0
  93. package/dist/lit/components/list-item/index.css +119 -0
  94. package/dist/lit/components/list-item/index.d.ts +20 -0
  95. package/dist/lit/components/list-item/index.js +127 -0
  96. package/dist/lit/components/menu/index.css +94 -0
  97. package/dist/lit/components/menu/index.d.ts +47 -0
  98. package/dist/lit/components/menu/index.js +386 -0
  99. package/dist/lit/components/navigation-drawer/index.css +114 -0
  100. package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
  101. package/dist/lit/components/navigation-drawer/index.js +218 -0
  102. package/dist/lit/components/overlay/index.css +171 -0
  103. package/dist/lit/components/overlay/index.d.ts +65 -0
  104. package/dist/lit/components/overlay/index.js +566 -0
  105. package/dist/lit/components/pagination/index.css +102 -0
  106. package/dist/lit/components/pagination/index.d.ts +22 -0
  107. package/dist/lit/components/pagination/index.js +184 -0
  108. package/dist/lit/components/primitives/index.css +504 -0
  109. package/dist/lit/components/primitives/index.d.ts +25 -0
  110. package/dist/lit/components/primitives/index.js +283 -0
  111. package/dist/lit/components/progress/index.css +143 -0
  112. package/dist/lit/components/progress/index.d.ts +23 -0
  113. package/dist/lit/components/progress/index.js +180 -0
  114. package/dist/lit/components/radio-group/index.css +178 -0
  115. package/dist/lit/components/radio-group/index.d.ts +35 -0
  116. package/dist/lit/components/radio-group/index.js +292 -0
  117. package/dist/lit/components/select/index.css +151 -0
  118. package/dist/lit/components/select/index.d.ts +50 -0
  119. package/dist/lit/components/select/index.js +390 -0
  120. package/dist/lit/components/sidebar-item/index.css +133 -0
  121. package/dist/lit/components/sidebar-item/index.d.ts +20 -0
  122. package/dist/lit/components/sidebar-item/index.js +105 -0
  123. package/dist/lit/components/skeleton/index.css +81 -0
  124. package/dist/lit/components/skeleton/index.d.ts +19 -0
  125. package/dist/lit/components/skeleton/index.js +119 -0
  126. package/dist/lit/components/slider/index.css +171 -0
  127. package/dist/lit/components/slider/index.d.ts +36 -0
  128. package/dist/lit/components/slider/index.js +302 -0
  129. package/dist/lit/components/snackbar/index.css +279 -0
  130. package/dist/lit/components/snackbar/index.d.ts +33 -0
  131. package/dist/lit/components/snackbar/index.js +195 -0
  132. package/dist/lit/components/stack/index.css +41 -0
  133. package/dist/lit/components/stack/index.d.ts +20 -0
  134. package/dist/lit/components/stack/index.js +103 -0
  135. package/dist/lit/components/switch/index.css +126 -0
  136. package/dist/lit/components/switch/index.d.ts +17 -0
  137. package/dist/lit/components/switch/index.js +116 -0
  138. package/dist/lit/components/table/index.css +85 -0
  139. package/dist/lit/components/table/index.d.ts +25 -0
  140. package/dist/lit/components/table/index.js +139 -0
  141. package/dist/lit/components/tabs/index.css +116 -0
  142. package/dist/lit/components/tabs/index.d.ts +49 -0
  143. package/dist/lit/components/tabs/index.js +320 -0
  144. package/dist/lit/components/text-field/index.css +90 -0
  145. package/dist/lit/components/text-field/index.d.ts +17 -0
  146. package/dist/lit/components/text-field/index.js +101 -0
  147. package/dist/lit/components/textarea/index.css +55 -0
  148. package/dist/lit/components/textarea/index.d.ts +26 -0
  149. package/dist/lit/components/textarea/index.js +124 -0
  150. package/dist/lit/components/tooltip/index.css +37 -0
  151. package/dist/lit/components/tooltip/index.d.ts +31 -0
  152. package/dist/lit/components/tooltip/index.js +196 -0
  153. package/dist/lit/components/validation/index.css +386 -0
  154. package/dist/lit/components/validation/index.d.ts +45 -0
  155. package/dist/lit/components/validation/index.js +318 -0
  156. package/dist/lit/index.d.ts +50 -0
  157. package/dist/lit/index.js +59 -0
  158. package/package.json +81 -0
  159. package/styles/README.md +346 -0
  160. package/styles/_elevation.css +24 -0
  161. package/styles/_fonts.css +6 -0
  162. package/styles/_layout.css +37 -0
  163. package/styles/_primitives.css +154 -0
  164. package/styles/_scroll.css +75 -0
  165. package/styles/_semantic.css +146 -0
  166. package/styles/_space.css +61 -0
  167. package/styles/_type.css +139 -0
  168. package/styles/_xmesh-extensions.css +232 -0
  169. package/styles/index.css +44 -0
  170. package/styles/md3/_color.css +102 -0
  171. package/styles/md3/_elevation.css +26 -0
  172. package/styles/md3/_motion.css +35 -0
  173. package/styles/md3/_shape.css +22 -0
  174. package/styles/md3/_state.css +22 -0
  175. 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
+ }