@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,388 @@
1
+ /*
2
+ field/index.ts — the abstract XmField form-field base.
3
+
4
+ XmField is NOT a registered custom element. It is the shared base every
5
+ form component in Epic 2 subclasses:
6
+ xm-text-field · xm-textarea · xm-select · xm-checkbox · xm-radio ·
7
+ xm-switch · xm-slider · xm-file-input
8
+
9
+ What the base owns (so ten fields don't hand-roll it ten ways, AD-7/AD-9a):
10
+ • The field chrome — label row, control wrapper with a shared control
11
+ height per `size`, helper/error row, required marker.
12
+ • disabled / readonly / loading visual + ARIA treatment, rendered
13
+ identically on every subclass.
14
+ • The focus ring (var(--xm-state-focus-ring) on the control wrapper).
15
+ • ARIA wiring — label association, aria-invalid, aria-describedby.
16
+ • The uncontrolled-first value lifecycle (AD-6): the `value` / `checked`
17
+ attribute is INITIAL only; the base holds live state and exposes emit
18
+ helpers that fire input/change (bubbles + composed) with typed detail.
19
+ • Form association (AD-6a): static formAssociated, ElementInternals,
20
+ `name` attribute, public value/checked getters, and an OR-propagation
21
+ disabled hook xm-form (Epic 2 Story 2.10) sets later.
22
+
23
+ What a subclass supplies — ONLY the concrete control, projected into the
24
+ fixed `slot="control"`. The subclass authors its control as a light-DOM
25
+ child:
26
+
27
+ <xm-text-field label="Name" size="md">
28
+ <input slot="control" type="text" />
29
+ </xm-text-field>
30
+
31
+ …or a subclass may override `renderControl()` to render the control into
32
+ shadow DOM instead. Either way the chrome stays on the base.
33
+
34
+ Shadow DOM. Loads its sibling index.css via <link> resolved from the built
35
+ file location (lit/build/components/field/index.js → ../…).
36
+ Lit is a bare `import` (peer dep).
37
+
38
+ Naming caveat: the base class is XmField; it is *not* an element. The first
39
+ concrete element is xm-text-field (Epic 2 Story 2.1). The folder is
40
+ lit/components/field/ per the structural seed.
41
+ */
42
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
43
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
44
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
45
+ 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;
46
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
47
+ };
48
+ import { LitElement, html, nothing } from "lit";
49
+ import { property, state } from "lit/decorators.js";
50
+ // Resolve CSS relative to the *built* file:
51
+ // lit/build/components/field/index.js → ../field/index.css.
52
+ const FIELD_CSS = new URL("../field/index.css", import.meta.url).href;
53
+ const PRIMITIVES_CSS = new URL("../primitives/index.css", import.meta.url).href;
54
+ let fieldIdSeq = 0;
55
+ /**
56
+ * Abstract chrome + lifecycle base for every xmesh form field.
57
+ *
58
+ * Subclasses register with `@customElement("xm-<name>")`; XmField itself is
59
+ * never registered (it is not a usable element on its own).
60
+ */
61
+ export class XmField extends LitElement {
62
+ // The contract xm-form (AD-6a / AD-12) reads against: a real form-associated
63
+ // custom element. Subclasses inherit this automatically.
64
+ static { this.formAssociated = true; }
65
+ // Forward focus to the inner control so the host participating in a label or
66
+ // tabindex chain delegates to the real control.
67
+ static { this.shadowRootOptions = {
68
+ ...LitElement.shadowRootOptions,
69
+ delegatesFocus: true,
70
+ }; }
71
+ constructor() {
72
+ super();
73
+ this.label = "";
74
+ this.helper = "";
75
+ /** Severity copy. Non-empty ⇒ the field is in error (icon + copy, never color). */
76
+ this.error = "";
77
+ this.size = "md";
78
+ this.required = false;
79
+ this.disabled = false;
80
+ this.readonly = false;
81
+ this.loading = false;
82
+ /** Form-control name — mirrors native <input name>. */
83
+ this.name = "";
84
+ /** INITIAL value (uncontrolled-first, AD-6): the `value` attribute seeds the
85
+ live state once, then never overrides user input. Mapped from attribute
86
+ `value` so authors write `<xm-text-field value="…">`. */
87
+ this.initialValue = "";
88
+ /** INITIAL checked for toggle subclasses; mapped from attribute `checked`. */
89
+ this.initialChecked = false;
90
+ /** Live value state — seeded from `initialValue`, then owned by the field. */
91
+ this._value = "";
92
+ /** Live checked state (toggle subclasses) — seeded from `initialChecked`. */
93
+ this._checked = false;
94
+ /** True once a toggle subclass has declared itself, so the form value is
95
+ submitted as checked-state rather than text. Set eagerly by overriding
96
+ `isToggle`, or lazily on the first `emitToggle`. */
97
+ this._toggle = false;
98
+ /** OR-propagated disabled flag a future xm-form sets — never re-enables a
99
+ self-disabled field (AD-6a / AD-9a). */
100
+ this._formDisabled = false;
101
+ this._seq = ++fieldIdSeq;
102
+ this._controlId = `xm-field-control-${this._seq}`;
103
+ this._describedById = `xm-field-desc-${this._seq}`;
104
+ this._valueSeeded = false;
105
+ // Flips the first time the user (or a programmatic value/checked set) touches
106
+ // the field. Once dirty, the initial attribute never re-seeds — so a later
107
+ // `value`/`checked` attribute change can't clobber a deliberately-cleared
108
+ // value or a user-unchecked toggle (uncontrolled-first, AD-6). Protected so a
109
+ // subclass with a bespoke commit path (e.g. xm-select) can mark touched.
110
+ this._dirty = false;
111
+ this.internals = this.attachInternals();
112
+ }
113
+ connectedCallback() {
114
+ super.connectedCallback();
115
+ if (!this._valueSeeded) {
116
+ this._value = this.initialValue;
117
+ this._checked = this.initialChecked;
118
+ this._valueSeeded = true;
119
+ this._syncFormValue();
120
+ }
121
+ }
122
+ willUpdate(changed) {
123
+ // Before first paint the attribute seed may arrive after connectedCallback
124
+ // (declarative attributes parse first, but guard anyway): only re-seed
125
+ // while still in the initial, UNTOUCHED state. `_dirty` (not `_value === ""`)
126
+ // is the touched signal — an empty value can be a deliberate user clear, and
127
+ // re-seeding over that would break uncontrolled-first (AD-6).
128
+ if (!this._valueSeeded || this._dirty)
129
+ return;
130
+ if (changed.has("initialValue")) {
131
+ this._value = this.initialValue;
132
+ }
133
+ if (changed.has("initialChecked")) {
134
+ this._checked = this.initialChecked;
135
+ }
136
+ }
137
+ /** Whether this field submits checked-state (toggle) rather than text. Toggle
138
+ subclasses (checkbox / radio / switch) override this to return `true` so
139
+ their form value is correct from first paint — before any interaction —
140
+ and is never inferred from `initialChecked` (a declaratively-checked text
141
+ field must NOT be treated as a toggle, and an untouched unchecked toggle
142
+ must submit `null`, not `""`). */
143
+ get isToggle() {
144
+ return this._toggle;
145
+ }
146
+ /** Effective disabled = own disabled OR a future xm-form down-propagation.
147
+ OR semantics: neither source can re-enable what the other disabled. */
148
+ get effectiveDisabled() {
149
+ return this.disabled || this._formDisabled;
150
+ }
151
+ /** Non-interactive whenever disabled (either source), readonly, or loading. */
152
+ get nonInteractive() {
153
+ return this.effectiveDisabled || this.readonly || this.loading;
154
+ }
155
+ get isError() {
156
+ return this.error.trim().length > 0;
157
+ }
158
+ /** True when the message row has helper or error copy to describe. */
159
+ get hasMessage() {
160
+ return (this.isError ? this.error : this.helper).length > 0;
161
+ }
162
+ /** Public read contract for xm-form (AD-6a/AD-12): live value, no shadow reach. */
163
+ get value() {
164
+ return this._value;
165
+ }
166
+ /** Programmatic reset support — setting `value` after first paint updates live state. */
167
+ set value(next) {
168
+ if (this._valueSeeded) {
169
+ this._dirty = true;
170
+ this._value = next;
171
+ this._syncFormValue();
172
+ }
173
+ else {
174
+ this.initialValue = next;
175
+ }
176
+ }
177
+ /** Public read contract for toggle subclasses (AD-6a). */
178
+ get checked() {
179
+ return this._checked;
180
+ }
181
+ set checked(next) {
182
+ if (this._valueSeeded) {
183
+ this._dirty = true;
184
+ this._checked = next;
185
+ this._syncFormValue();
186
+ }
187
+ else {
188
+ this.initialChecked = next;
189
+ }
190
+ }
191
+ /** Down-propagation hook: xm-form (Story 2.10) sets this; it OR's with the
192
+ field's own disabled and can never re-enable a self-disabled field. */
193
+ setFormDisabled(disabled) {
194
+ this._formDisabled = disabled;
195
+ }
196
+ // ── Input lifecycle (subclasses call these from their control handlers) ──
197
+ /** Per-keystroke / per-drag live update. Updates live state, syncs the form
198
+ value, and emits a composed, bubbling `input` with a typed primitive detail. */
199
+ emitInput(value) {
200
+ this._dirty = true;
201
+ this._value = value;
202
+ this._syncFormValue();
203
+ this.dispatchEvent(new CustomEvent("input", {
204
+ bubbles: true,
205
+ composed: true,
206
+ detail: { value },
207
+ }));
208
+ }
209
+ /** Commit. Same payload shape as input; fire on blur / Enter / native change. */
210
+ emitChange(value) {
211
+ this._dirty = true;
212
+ this._value = value;
213
+ this._syncFormValue();
214
+ this.dispatchEvent(new CustomEvent("change", {
215
+ bubbles: true,
216
+ composed: true,
217
+ detail: { value },
218
+ }));
219
+ }
220
+ /** Toggle commit — marks the field a toggle and emits `detail.checked` (AD-8a). */
221
+ emitToggle(checked) {
222
+ this._dirty = true;
223
+ this._toggle = true;
224
+ this._checked = checked;
225
+ this._syncFormValue();
226
+ this.dispatchEvent(new CustomEvent("change", {
227
+ bubbles: true,
228
+ composed: true,
229
+ detail: { checked },
230
+ }));
231
+ }
232
+ _syncFormValue() {
233
+ if (this.isToggle) {
234
+ // Match native checkbox/radio: checked submits "on" (or a subclass value),
235
+ // unchecked submits NOTHING (null) — never an empty string.
236
+ this.internals.setFormValue(this._checked ? "on" : null);
237
+ }
238
+ else {
239
+ this.internals.setFormValue(this._value);
240
+ }
241
+ }
242
+ /**
243
+ * The ARIA hooks the chrome wires. Subclasses set `id`, `aria-describedby`,
244
+ * `aria-invalid`, and `aria-required` on their control element from these so
245
+ * the rendered label associates with the control and the error string is
246
+ * announced (NFR-13 / UX-DR7).
247
+ */
248
+ get controlAria() {
249
+ return {
250
+ id: this._controlId,
251
+ describedBy: this.hasMessage ? this._describedById : undefined,
252
+ invalid: this.isError ? "true" : undefined,
253
+ required: this.required ? "true" : undefined,
254
+ };
255
+ }
256
+ /**
257
+ * Subclasses override to render their concrete control directly into shadow
258
+ * DOM. The default projects whatever the author slots as `slot="control"`,
259
+ * so a subclass can also author the control as a light-DOM child. Either
260
+ * path keeps the chrome on the base (AD-7).
261
+ */
262
+ renderControl() {
263
+ return html `<slot name="control"></slot>`;
264
+ }
265
+ render() {
266
+ const sizeClass = `field--${this.size}`;
267
+ const stateClass = [
268
+ this.effectiveDisabled ? "field--disabled" : "",
269
+ this.readonly ? "field--readonly" : "",
270
+ this.loading ? "field--loading" : "",
271
+ this.isError ? "field--error" : "",
272
+ ]
273
+ .filter(Boolean)
274
+ .join(" ");
275
+ const cls = `field ${sizeClass} ${stateClass}`.replace(/\s+/g, " ").trim();
276
+ const helperText = this.isError ? this.error : this.helper;
277
+ const ctrl = this.controlAria;
278
+ return html `
279
+ <link rel="stylesheet" href="${PRIMITIVES_CSS}" />
280
+ <link rel="stylesheet" href="${FIELD_CSS}" />
281
+ <style>
282
+ :host {
283
+ display: block;
284
+ }
285
+ :host([hidden]) {
286
+ display: none;
287
+ }
288
+ </style>
289
+ <div class="${cls}" aria-busy=${this.loading ? "true" : nothing}>
290
+ ${this.label
291
+ ? html `<label class="field__label" for="${ctrl.id}">
292
+ <span class="field__label-text">${this.label}</span>
293
+ ${this.required
294
+ ? html `<span class="field__required" aria-hidden="true">*</span>`
295
+ : nothing}
296
+ </label>`
297
+ : nothing}
298
+
299
+ <div class="field__control">
300
+ ${this.loading
301
+ ? html `<span
302
+ class="field__loading"
303
+ role="status"
304
+ aria-label="Loading"
305
+ >
306
+ <xm-spinner size="16"></xm-spinner>
307
+ </span>`
308
+ : this.renderControl()}
309
+ </div>
310
+
311
+ ${helperText
312
+ ? html `<div
313
+ class="field__message ${this.isError
314
+ ? "field__message--error"
315
+ : "field__message--helper"}"
316
+ id="${this._describedById}"
317
+ role=${this.isError ? "alert" : nothing}
318
+ >
319
+ ${this.isError
320
+ ? html `<span class="field__error-icon" aria-hidden="true">
321
+ <svg
322
+ width="14"
323
+ height="14"
324
+ viewBox="0 0 24 24"
325
+ fill="none"
326
+ stroke="currentColor"
327
+ stroke-width="1.8"
328
+ stroke-linecap="round"
329
+ stroke-linejoin="round"
330
+ class="ds-icon"
331
+ >
332
+ <circle cx="12" cy="12" r="10" />
333
+ <path d="M12 8v5M12 16h.01" />
334
+ </svg>
335
+ </span>`
336
+ : nothing}
337
+ <span class="field__message-text">${helperText}</span>
338
+ </div>`
339
+ : html `<div class="field__message field__message--empty"></div>`}
340
+ </div>
341
+ `;
342
+ }
343
+ }
344
+ __decorate([
345
+ property({ type: String })
346
+ ], XmField.prototype, "label", void 0);
347
+ __decorate([
348
+ property({ type: String })
349
+ ], XmField.prototype, "helper", void 0);
350
+ __decorate([
351
+ property({ type: String })
352
+ ], XmField.prototype, "error", void 0);
353
+ __decorate([
354
+ property({ type: String })
355
+ ], XmField.prototype, "size", void 0);
356
+ __decorate([
357
+ property({ type: Boolean })
358
+ ], XmField.prototype, "required", void 0);
359
+ __decorate([
360
+ property({ type: Boolean, reflect: true })
361
+ ], XmField.prototype, "disabled", void 0);
362
+ __decorate([
363
+ property({ type: Boolean, reflect: true })
364
+ ], XmField.prototype, "readonly", void 0);
365
+ __decorate([
366
+ property({ type: Boolean, reflect: true })
367
+ ], XmField.prototype, "loading", void 0);
368
+ __decorate([
369
+ property({ type: String, reflect: true })
370
+ ], XmField.prototype, "name", void 0);
371
+ __decorate([
372
+ property({ type: String, attribute: "value" })
373
+ ], XmField.prototype, "initialValue", void 0);
374
+ __decorate([
375
+ property({ type: Boolean, attribute: "checked" })
376
+ ], XmField.prototype, "initialChecked", void 0);
377
+ __decorate([
378
+ state()
379
+ ], XmField.prototype, "_value", void 0);
380
+ __decorate([
381
+ state()
382
+ ], XmField.prototype, "_checked", void 0);
383
+ __decorate([
384
+ state()
385
+ ], XmField.prototype, "_toggle", void 0);
386
+ __decorate([
387
+ state()
388
+ ], XmField.prototype, "_formDisabled", void 0);
@@ -0,0 +1,257 @@
1
+ /* ============================================
2
+ xm-file-input — generic file picker / dropzone on XmField (Story 2.9).
3
+
4
+ Extends XmField for chrome but renders its own layout: a label row, a dropzone
5
+ surface (the focusable button), the selected-file rows, and the helper/error
6
+ row. State machinery (disabled / form-association / focus / ARIA) is inherited;
7
+ this file styles the dropzone, file rows, and the byte-size text.
8
+
9
+ Surface & ink (AD-13): the dropzone rides the inverse-surface card tier —
10
+ primary copy is inverse-on-surface, the secondary hint + size are the muted
11
+ tier. The icon takes the card ink.
12
+
13
+ Borders: the dropzone is a hairline 1px (outline-variant → outline on hover).
14
+ The DRAG-ACTIVE state is the ONE place a 2px border is allowed — 2px solid the
15
+ coral --md-sys-color-primary, with the focus-ring halo. Coral here signals the
16
+ live drop target, never severity (AD-11). Errors are the warn icon + copy.
17
+
18
+ BEM block: `file-input`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
19
+ ============================================ */
20
+
21
+ .file-input {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--s-2);
25
+ width: 100%;
26
+ }
27
+
28
+ /* ---------- Label ---------- */
29
+ .file-input__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
+ .file-input__label-text {
41
+ letter-spacing: 0;
42
+ }
43
+ .file-input__required {
44
+ color: var(--md-sys-color-primary);
45
+ font-weight: 700;
46
+ line-height: 1;
47
+ }
48
+
49
+ /* ---------- Hidden native input — kept out of layout + tab order ---------- */
50
+ .file-input__native {
51
+ position: absolute;
52
+ width: 1px;
53
+ height: 1px;
54
+ padding: 0;
55
+ margin: -1px;
56
+ overflow: hidden;
57
+ clip: rect(0, 0, 0, 0);
58
+ white-space: nowrap;
59
+ border: 0;
60
+ }
61
+
62
+ /* ---------- Dropzone — the focusable button surface ---------- */
63
+ .file-input__dropzone {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--s-3);
67
+ box-sizing: border-box;
68
+ padding: var(--s-4) var(--s-4);
69
+ /* Hairline + a 1px transparent inset so the box does not jump when the
70
+ drag-active 2px border lands (the inset absorbs the extra 1px). */
71
+ border: 1px solid var(--md-sys-color-outline-variant);
72
+ outline: 1px solid transparent;
73
+ outline-offset: -2px;
74
+ border-radius: var(--md-sys-shape-corner-button);
75
+ background: var(--md-sys-color-inverse-surface);
76
+ color: var(--md-sys-color-inverse-on-surface);
77
+ cursor: pointer;
78
+ transition:
79
+ border-color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
80
+ box-shadow var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
81
+ }
82
+ .file-input__dropzone:hover {
83
+ border-color: var(--md-sys-color-outline);
84
+ }
85
+ .file-input__dropzone:focus-visible {
86
+ outline: none;
87
+ border-color: var(--md-sys-color-primary);
88
+ box-shadow: var(--xm-state-focus-ring);
89
+ }
90
+
91
+ .file-input__icon {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ flex-shrink: 0;
95
+ color: var(--md-sys-color-inverse-on-surface);
96
+ }
97
+ .file-input__copy {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 1px;
101
+ min-width: 0;
102
+ }
103
+ .file-input__primary {
104
+ color: var(--md-sys-color-inverse-on-surface);
105
+ font:
106
+ var(--md-sys-typescale-body-medium-weight)
107
+ var(--md-sys-typescale-body-medium-size) /
108
+ var(--md-sys-typescale-body-medium-line-height)
109
+ var(--md-sys-typescale-body-medium-font);
110
+ }
111
+ .file-input__secondary {
112
+ color: var(--xm-color-inverse-on-surface-muted);
113
+ font:
114
+ var(--md-sys-typescale-body-small-weight)
115
+ var(--md-sys-typescale-body-small-size) /
116
+ var(--md-sys-typescale-body-small-line-height)
117
+ var(--md-sys-typescale-body-small-font);
118
+ }
119
+
120
+ /* ---------- Drag-active — the ONLY 2px border in the system ---------- */
121
+ .file-input--drag-active .file-input__dropzone {
122
+ border: 2px solid var(--md-sys-color-primary);
123
+ box-shadow: var(--xm-state-focus-ring);
124
+ }
125
+
126
+ /* ---------- Selected-file rows — raised level1 cards ---------- */
127
+ .file-input__files {
128
+ list-style: none;
129
+ margin: 0;
130
+ padding: 0;
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: var(--s-2);
134
+ }
135
+ .file-input__file {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: var(--s-2);
139
+ box-sizing: border-box;
140
+ padding: var(--s-2) var(--s-3);
141
+ border: 1px solid var(--md-sys-color-outline-variant);
142
+ border-radius: var(--md-sys-shape-corner-small);
143
+ background: var(--md-sys-color-inverse-surface);
144
+ box-shadow: var(--md-sys-elevation-level1);
145
+ }
146
+ .file-input__file-icon {
147
+ display: inline-flex;
148
+ align-items: center;
149
+ flex-shrink: 0;
150
+ color: var(--md-sys-color-inverse-on-surface);
151
+ }
152
+ .file-input__file-name {
153
+ flex: 1;
154
+ min-width: 0;
155
+ overflow: hidden;
156
+ white-space: nowrap;
157
+ text-overflow: ellipsis;
158
+ color: var(--md-sys-color-inverse-on-surface);
159
+ font:
160
+ var(--md-sys-typescale-body-medium-weight)
161
+ var(--md-sys-typescale-body-medium-size) /
162
+ var(--md-sys-typescale-body-medium-line-height)
163
+ var(--md-sys-typescale-body-medium-font);
164
+ }
165
+ .file-input__file-size {
166
+ flex-shrink: 0;
167
+ color: var(--xm-color-inverse-on-surface-muted);
168
+ font:
169
+ var(--xm-typescale-mono-weight)
170
+ var(--xm-typescale-mono-size) /
171
+ var(--xm-typescale-mono-line-height)
172
+ var(--xm-typescale-mono-font);
173
+ }
174
+ .file-input__remove {
175
+ display: inline-flex;
176
+ align-items: center;
177
+ justify-content: center;
178
+ flex-shrink: 0;
179
+ width: 20px;
180
+ height: 20px;
181
+ padding: 0;
182
+ border: none;
183
+ border-radius: var(--md-sys-shape-corner-full);
184
+ background: transparent;
185
+ color: var(--xm-color-inverse-on-surface-muted);
186
+ cursor: pointer;
187
+ transition: background var(--md-sys-motion-duration-short3)
188
+ var(--md-sys-motion-easing-standard);
189
+ }
190
+ .file-input__remove:hover {
191
+ background: color-mix(
192
+ in oklab,
193
+ var(--md-sys-color-inverse-on-surface)
194
+ var(--md-sys-state-hover-state-layer-opacity),
195
+ transparent
196
+ );
197
+ color: var(--md-sys-color-inverse-on-surface);
198
+ }
199
+ .file-input__remove:focus-visible {
200
+ outline: none;
201
+ box-shadow: var(--xm-state-focus-ring);
202
+ }
203
+
204
+ /* ---------- Disabled — reduced emphasis ----------
205
+ No opacity (it would dim the copy below AA). The dropzone children
206
+ (__primary / __secondary / __icon) each set their own color, so the swap
207
+ must target them directly — they do NOT inherit a color set on __dropzone.
208
+ A muted container fill restores the visual "disabled" cue the opacity used
209
+ to give. */
210
+ .file-input--disabled .file-input__dropzone {
211
+ cursor: not-allowed;
212
+ background: color-mix(in oklab, var(--md-sys-color-inverse-on-surface) 5%, var(--md-sys-color-inverse-surface));
213
+ border-color: var(--md-sys-color-outline-variant);
214
+ }
215
+ .file-input--disabled .file-input__dropzone:hover {
216
+ border-color: var(--md-sys-color-outline-variant);
217
+ }
218
+ .file-input--disabled .file-input__primary,
219
+ .file-input--disabled .file-input__secondary,
220
+ .file-input--disabled .file-input__icon {
221
+ color: var(--xm-color-inverse-on-surface-disabled);
222
+ }
223
+ .file-input--disabled .file-input__label {
224
+ color: var(--xm-color-inverse-on-surface-disabled);
225
+ }
226
+
227
+ /* ---------- Helper / error message row ----------
228
+ Severity is the warn icon + copy, not a color (rule 3a). */
229
+ .file-input__message {
230
+ display: flex;
231
+ align-items: flex-start;
232
+ gap: var(--s-1);
233
+ min-height: 1em;
234
+ font:
235
+ var(--md-sys-typescale-body-small-weight)
236
+ var(--md-sys-typescale-body-small-size) /
237
+ var(--md-sys-typescale-body-small-line-height)
238
+ var(--md-sys-typescale-body-small-font);
239
+ }
240
+ .file-input__message--helper {
241
+ color: var(--xm-color-inverse-on-surface-muted);
242
+ }
243
+ .file-input__message--error {
244
+ color: var(--md-sys-color-inverse-on-surface);
245
+ }
246
+ .file-input__error-icon {
247
+ display: inline-flex;
248
+ align-items: center;
249
+ flex-shrink: 0;
250
+ margin-top: 1px;
251
+ color: var(--md-sys-color-inverse-on-surface);
252
+ }
253
+ .file-input__message-text {
254
+ flex: 1;
255
+ min-width: 0;
256
+ text-wrap: pretty;
257
+ }
@@ -0,0 +1,30 @@
1
+ import type { TemplateResult } from "lit";
2
+ import { XmField } from "../field/index.js";
3
+ export declare class XmFileInput extends XmField {
4
+ /** Native accept filter (e.g. ".yml,.json,image/*"). */
5
+ accept: string;
6
+ /** Allow selecting more than one file. */
7
+ multiple: boolean;
8
+ private _files;
9
+ private _dragActive;
10
+ private _native;
11
+ /** Public read contract (AD-6a) — the selected files. */
12
+ get files(): File[];
13
+ private _setFiles;
14
+ private _openPicker;
15
+ private _onNativeChange;
16
+ private _onKeydown;
17
+ private _onDragOver;
18
+ private _onDragLeave;
19
+ /** Does a dropped file satisfy the `accept` list? Mirrors the native picker so
20
+ drag-drop and click-to-pick enforce the same filter. */
21
+ private _accepts;
22
+ private _onDrop;
23
+ private _removeFile;
24
+ render(): TemplateResult;
25
+ }
26
+ declare global {
27
+ interface HTMLElementTagNameMap {
28
+ "xm-file-input": XmFileInput;
29
+ }
30
+ }