@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,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
|
+
}
|