@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,47 @@
1
+ import type { PropertyValues, TemplateResult } from "lit";
2
+ import { XmField } from "../field/index.js";
3
+ import type { SelectOption } from "../select/index.js";
4
+ import type { XmOverlay } from "../overlay/index.js";
5
+ export declare class XmAutocomplete extends XmField {
6
+ /** Shared option model — `{ label, value, disabled? }` (AD-8a). */
7
+ options: SelectOption[];
8
+ /** Native placeholder shown when the query is empty. */
9
+ placeholder: string;
10
+ protected _open: boolean;
11
+ protected _query: string;
12
+ protected _activeIndex: number;
13
+ protected _selectedValue: string | number | null;
14
+ protected _input: HTMLInputElement | null;
15
+ protected _overlay: XmOverlay | null;
16
+ connectedCallback(): void;
17
+ protected willUpdate(changed: PropertyValues<this>): void;
18
+ private _resolveSeed;
19
+ /** Typed primitive read for consumers; the inherited string `value` stays in
20
+ sync for xm-form / ElementInternals. */
21
+ get selectedValue(): string | number | null;
22
+ private get _filtered();
23
+ private get _enabledIndexes();
24
+ private _onDocPointerDown;
25
+ private _openList;
26
+ private _closeList;
27
+ private _onOverlayClose;
28
+ disconnectedCallback(): void;
29
+ protected updated(changed: PropertyValues<this>): void;
30
+ private _onInput;
31
+ private _commitSelection;
32
+ private _selectActive;
33
+ private _onKeydown;
34
+ private _nextEnabled;
35
+ protected renderControl(): TemplateResult;
36
+ private _renderOption;
37
+ /** Wrap the matched substring in a highlight mark. The mark carries BOTH a
38
+ coral ink AND an underline so the cue survives grayscale (NFR-15 — color
39
+ is never the sole carrier). */
40
+ private _highlight;
41
+ private _renderEmpty;
42
+ }
43
+ declare global {
44
+ interface HTMLElementTagNameMap {
45
+ "xm-autocomplete": XmAutocomplete;
46
+ }
47
+ }
@@ -0,0 +1,404 @@
1
+ /*
2
+ autocomplete/index.ts — <xm-autocomplete>, a filtering select (Story 2.4).
3
+
4
+ Extends the SAME pattern as xm-select: it subclasses XmField (chrome) and
5
+ composes xm-overlay (anchored, non-modal, menu-tier listbox). The only deltas
6
+ from xm-select are:
7
+ • the closed control is a TEXT INPUT that filters `options` as you type,
8
+ • each shown option highlights the matched substring (coral ink + underline,
9
+ so the cue survives grayscale — color is never the sole carrier, NFR-15),
10
+ • two typed events: `input` carries the query string, `change` carries the
11
+ selected option's PRIMITIVE value (AD-8a) — never overloaded onto one key,
12
+ • a no-match query renders an inline empty-state (sentence-case headline +
13
+ secondary copy + line icon, no imagery) instead of a blank/closed list.
14
+
15
+ NOTE (dependency): xm-empty-state (Epic 4 / FR-149) is not built yet, so the
16
+ no-match panel renders the empty-state markup INLINE here, using the same
17
+ line-icon + sentence-case headline + secondary copy pattern. Flagged for
18
+ consolidation once xm-empty-state ships.
19
+
20
+ It does NOT fork the overlay/positioning/focus contract and does NOT reach
21
+ into the overlay's shadow root (AD-12). Option model is the shared
22
+ `{ label, value, disabled? }`.
23
+
24
+ Keyboard (combobox + listbox APG, AD-9a): typing filters; ↑↓ move the active
25
+ match (skipping disabled), Home/End jump, Enter selects + closes, Esc closes
26
+ (routed through the overlay's innermost-Esc, which stopPropagations); focus
27
+ returns to the input on close.
28
+
29
+ Shadow DOM. Lit is a bare `import` (peer dep).
30
+ */
31
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
32
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
33
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
34
+ 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;
35
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
36
+ };
37
+ import { html, nothing } from "lit";
38
+ import { customElement, property, state, query } from "lit/decorators.js";
39
+ import { XmField } from "../field/index.js";
40
+ const AUTOCOMPLETE_CSS = new URL("../autocomplete/index.css", import.meta.url).href;
41
+ let XmAutocomplete = class XmAutocomplete extends XmField {
42
+ constructor() {
43
+ super(...arguments);
44
+ /** Shared option model — `{ label, value, disabled? }` (AD-8a). */
45
+ this.options = [];
46
+ /** Native placeholder shown when the query is empty. */
47
+ this.placeholder = "Search…";
48
+ this._open = false;
49
+ this._query = "";
50
+ this._activeIndex = -1;
51
+ this._selectedValue = null;
52
+ // ── Open / close ────────────────────────────────────────────────────
53
+ // Close the open list when a pointer goes down outside this element (the
54
+ // non-modal overlay provides no light-dismiss of its own).
55
+ this._onDocPointerDown = (e) => {
56
+ if (!this._open)
57
+ return;
58
+ if (!e.composedPath().includes(this))
59
+ this._closeList(false);
60
+ };
61
+ this._onOverlayClose = (e) => {
62
+ const detail = e.detail;
63
+ if (this._open) {
64
+ this._open = false;
65
+ this._activeIndex = -1;
66
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
67
+ if (detail?.reason !== "escape")
68
+ this._input?.focus();
69
+ }
70
+ };
71
+ // ── Input / select ──────────────────────────────────────────────────
72
+ this._onInput = (e) => {
73
+ const q = e.target.value;
74
+ this._query = q;
75
+ if (!this._open)
76
+ this._openList();
77
+ this._activeIndex = this._enabledIndexes[0] ?? -1;
78
+ this.dispatchEvent(new CustomEvent("input", {
79
+ bubbles: true,
80
+ composed: true,
81
+ detail: { value: q },
82
+ }));
83
+ };
84
+ // ── Keyboard ────────────────────────────────────────────────────────
85
+ this._onKeydown = (e) => {
86
+ if (this.nonInteractive)
87
+ return;
88
+ if (!this._open) {
89
+ if (e.key === "ArrowDown" || e.key === "Enter") {
90
+ e.preventDefault();
91
+ this._openList();
92
+ }
93
+ return;
94
+ }
95
+ const enabled = this._enabledIndexes;
96
+ switch (e.key) {
97
+ case "ArrowDown":
98
+ e.preventDefault();
99
+ this._activeIndex = this._nextEnabled(this._activeIndex, 1);
100
+ break;
101
+ case "ArrowUp":
102
+ e.preventDefault();
103
+ this._activeIndex = this._nextEnabled(this._activeIndex, -1);
104
+ break;
105
+ case "Home":
106
+ e.preventDefault();
107
+ this._activeIndex = enabled[0] ?? -1;
108
+ break;
109
+ case "End":
110
+ e.preventDefault();
111
+ this._activeIndex = enabled[enabled.length - 1] ?? -1;
112
+ break;
113
+ case "Enter":
114
+ e.preventDefault();
115
+ // Choosing an option must not bubble to an ancestor xm-form as submit.
116
+ e.stopPropagation();
117
+ this._selectActive();
118
+ break;
119
+ case "Tab":
120
+ this._closeList(false);
121
+ break;
122
+ // Esc handled by the overlay (innermost-only, stopPropagation).
123
+ }
124
+ };
125
+ }
126
+ connectedCallback() {
127
+ super.connectedCallback();
128
+ if (this.initialValue !== "" && this._selectedValue === null) {
129
+ this._resolveSeed();
130
+ }
131
+ }
132
+ willUpdate(changed) {
133
+ super.willUpdate(changed);
134
+ if (changed.has("options") &&
135
+ this._selectedValue === null &&
136
+ this.initialValue !== "") {
137
+ this._resolveSeed();
138
+ }
139
+ }
140
+ _resolveSeed() {
141
+ const match = this.options.find((o) => String(o.value) === this.initialValue);
142
+ if (match) {
143
+ this._selectedValue = match.value;
144
+ this._query = match.label;
145
+ this._value = String(match.value);
146
+ this.internals.setFormValue(this._value);
147
+ }
148
+ }
149
+ /** Typed primitive read for consumers; the inherited string `value` stays in
150
+ sync for xm-form / ElementInternals. */
151
+ get selectedValue() {
152
+ return this._selectedValue;
153
+ }
154
+ // ── Filtering ───────────────────────────────────────────────────────
155
+ get _filtered() {
156
+ const q = this._query.trim().toLowerCase();
157
+ if (q === "")
158
+ return this.options;
159
+ return this.options.filter((o) => o.label.toLowerCase().includes(q));
160
+ }
161
+ get _enabledIndexes() {
162
+ const f = this._filtered;
163
+ return f.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i !== -1);
164
+ }
165
+ _openList() {
166
+ if (this.nonInteractive || this._open)
167
+ return;
168
+ this._open = true;
169
+ this._activeIndex = this._enabledIndexes[0] ?? -1;
170
+ document.addEventListener("pointerdown", this._onDocPointerDown, true);
171
+ this.updateComplete.then(() => {
172
+ const ov = this._overlay;
173
+ const inp = this._input;
174
+ if (ov && inp) {
175
+ ov.anchor = inp;
176
+ ov.opener = inp;
177
+ ov.show();
178
+ }
179
+ });
180
+ }
181
+ _closeList(focusInput = true) {
182
+ if (!this._open)
183
+ return;
184
+ this._open = false;
185
+ this._activeIndex = -1;
186
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
187
+ const ov = this._overlay;
188
+ if (ov?.open)
189
+ ov.hide("api");
190
+ if (focusInput)
191
+ this._input?.focus();
192
+ }
193
+ disconnectedCallback() {
194
+ super.disconnectedCallback();
195
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
196
+ }
197
+ updated(changed) {
198
+ super.updated?.(changed);
199
+ if (this._open &&
200
+ changed.has("_activeIndex") &&
201
+ this._activeIndex >= 0) {
202
+ this.renderRoot
203
+ .querySelector(".autocomplete__option--active")
204
+ ?.scrollIntoView({ block: "nearest" });
205
+ }
206
+ }
207
+ _commitSelection(opt) {
208
+ this._selectedValue = opt.value;
209
+ this._query = opt.label;
210
+ // Mark touched so the base's uncontrolled-first re-seed can't clobber the
211
+ // committed value/label back to `initialValue` (AD-6).
212
+ this._dirty = true;
213
+ this._value = String(opt.value);
214
+ this.internals.setFormValue(this._value);
215
+ this.dispatchEvent(new CustomEvent("change", {
216
+ bubbles: true,
217
+ composed: true,
218
+ detail: { value: opt.value },
219
+ }));
220
+ this._closeList(true);
221
+ }
222
+ _selectActive() {
223
+ const opt = this._filtered[this._activeIndex];
224
+ if (opt && !opt.disabled)
225
+ this._commitSelection(opt);
226
+ }
227
+ _nextEnabled(from, dir) {
228
+ const f = this._filtered;
229
+ const n = f.length;
230
+ if (n === 0)
231
+ return -1;
232
+ let i = from;
233
+ for (let step = 0; step < n; step++) {
234
+ i = (i + dir + n) % n;
235
+ if (!f[i]?.disabled)
236
+ return i;
237
+ }
238
+ return from;
239
+ }
240
+ // ── Render ──────────────────────────────────────────────────────────
241
+ renderControl() {
242
+ const a = this.controlAria;
243
+ const filtered = this._filtered;
244
+ const activeId = this._open && this._activeIndex >= 0
245
+ ? `${a.id}-opt-${this._activeIndex}`
246
+ : nothing;
247
+ return html `
248
+ <link rel="stylesheet" href="${AUTOCOMPLETE_CSS}" />
249
+ <div class="autocomplete__control">
250
+ <input
251
+ class="autocomplete__input"
252
+ id=${a.id}
253
+ type="text"
254
+ role="combobox"
255
+ autocomplete="off"
256
+ aria-autocomplete="list"
257
+ aria-haspopup="listbox"
258
+ aria-expanded=${this._open ? "true" : "false"}
259
+ aria-controls="${a.id}-listbox"
260
+ aria-activedescendant=${activeId}
261
+ aria-describedby=${a.describedBy}
262
+ aria-invalid=${a.invalid ?? "false"}
263
+ aria-required=${a.required ?? nothing}
264
+ placeholder=${this.placeholder || nothing}
265
+ name=${this.name || nothing}
266
+ .value=${this._query}
267
+ ?disabled=${this.effectiveDisabled}
268
+ ?readonly=${this.readonly}
269
+ @input=${this._onInput}
270
+ @keydown=${this._onKeydown}
271
+ @focus=${() => {
272
+ if (!this._open && !this.nonInteractive)
273
+ this._openList();
274
+ }}
275
+ />
276
+ <span class="autocomplete__chevron ${this._open ? "autocomplete__chevron--open" : ""}">
277
+ <xm-chevron-down-icon size="16"></xm-chevron-down-icon>
278
+ </span>
279
+ </div>
280
+
281
+ <xm-overlay
282
+ mode="non-modal"
283
+ tier="menu"
284
+ placement="bottom-start"
285
+ label=${this.label || "Suggestions"}
286
+ @xm-overlay-close=${this._onOverlayClose}
287
+ >
288
+ ${filtered.length > 0
289
+ ? html `<ul
290
+ class="autocomplete__listbox"
291
+ id="${a.id}-listbox"
292
+ role="listbox"
293
+ aria-label=${this.label || "Suggestions"}
294
+ >
295
+ ${filtered.map((opt, i) => this._renderOption(opt, i, a.id))}
296
+ </ul>`
297
+ : this._renderEmpty(a.id)}
298
+ </xm-overlay>
299
+ `;
300
+ }
301
+ _renderOption(opt, index, baseId) {
302
+ const selected = opt.value === this._selectedValue;
303
+ const active = index === this._activeIndex;
304
+ const cls = [
305
+ "autocomplete__option",
306
+ selected ? "autocomplete__option--selected" : "",
307
+ active ? "autocomplete__option--active" : "",
308
+ opt.disabled ? "autocomplete__option--disabled" : "",
309
+ ]
310
+ .filter(Boolean)
311
+ .join(" ");
312
+ return html `
313
+ <li
314
+ class="${cls}"
315
+ id="${baseId}-opt-${index}"
316
+ role="option"
317
+ aria-selected=${selected ? "true" : "false"}
318
+ aria-disabled=${opt.disabled ? "true" : nothing}
319
+ @click=${() => {
320
+ if (!opt.disabled)
321
+ this._commitSelection(opt);
322
+ }}
323
+ @mousemove=${() => {
324
+ if (!opt.disabled)
325
+ this._activeIndex = index;
326
+ }}
327
+ >
328
+ <span class="autocomplete__option-label"
329
+ >${this._highlight(opt.label)}</span
330
+ >
331
+ ${selected
332
+ ? html `<span class="autocomplete__option-check" aria-hidden="true">
333
+ <xm-check-icon size="16"></xm-check-icon>
334
+ </span>`
335
+ : nothing}
336
+ </li>
337
+ `;
338
+ }
339
+ /** Wrap the matched substring in a highlight mark. The mark carries BOTH a
340
+ coral ink AND an underline so the cue survives grayscale (NFR-15 — color
341
+ is never the sole carrier). */
342
+ _highlight(label) {
343
+ const q = this._query.trim();
344
+ if (q === "")
345
+ return label;
346
+ const lower = label.toLowerCase();
347
+ const idx = lower.indexOf(q.toLowerCase());
348
+ if (idx === -1)
349
+ return label;
350
+ const before = label.slice(0, idx);
351
+ const matched = label.slice(idx, idx + q.length);
352
+ const after = label.slice(idx + q.length);
353
+ return html `${before}<mark class="autocomplete__mark">${matched}</mark
354
+ >${after}`;
355
+ }
356
+ _renderEmpty(baseId) {
357
+ // Inline empty-state (sentence-case headline + secondary copy + line icon).
358
+ // Replace with <xm-empty-state> once Epic 4 ships it.
359
+ return html `
360
+ <div
361
+ class="autocomplete__empty"
362
+ id="${baseId}-listbox"
363
+ role="listbox"
364
+ aria-label="No matches"
365
+ >
366
+ <span class="autocomplete__empty-icon" aria-hidden="true">
367
+ <xm-search-icon size="20"></xm-search-icon>
368
+ </span>
369
+ <p class="autocomplete__empty-title">No matches</p>
370
+ <p class="autocomplete__empty-copy">
371
+ Nothing matches “${this._query}”. Try a different search.
372
+ </p>
373
+ </div>
374
+ `;
375
+ }
376
+ };
377
+ __decorate([
378
+ property({ attribute: false })
379
+ ], XmAutocomplete.prototype, "options", void 0);
380
+ __decorate([
381
+ property({ type: String })
382
+ ], XmAutocomplete.prototype, "placeholder", void 0);
383
+ __decorate([
384
+ state()
385
+ ], XmAutocomplete.prototype, "_open", void 0);
386
+ __decorate([
387
+ state()
388
+ ], XmAutocomplete.prototype, "_query", void 0);
389
+ __decorate([
390
+ state()
391
+ ], XmAutocomplete.prototype, "_activeIndex", void 0);
392
+ __decorate([
393
+ state()
394
+ ], XmAutocomplete.prototype, "_selectedValue", void 0);
395
+ __decorate([
396
+ query(".autocomplete__input")
397
+ ], XmAutocomplete.prototype, "_input", void 0);
398
+ __decorate([
399
+ query("xm-overlay")
400
+ ], XmAutocomplete.prototype, "_overlay", void 0);
401
+ XmAutocomplete = __decorate([
402
+ customElement("xm-autocomplete")
403
+ ], XmAutocomplete);
404
+ export { XmAutocomplete };
@@ -0,0 +1,62 @@
1
+ /* ============================================
2
+ Avatar — circular identity token.
3
+
4
+ <xm-avatar> renders a circular identity chip: initials first,
5
+ then a Lucide-style person line-icon fallback, with an optional
6
+ image. No imagery is ever required (UX-DR8). The chip is an
7
+ opaque raise of the desk — it sits on a surface-container-* tier
8
+ (NOT a third surface family) with ink --md-sys-color-on-surface
9
+ matched to that backplate (AD-13). No gradient, no status hue.
10
+
11
+ Sizes share the canonical xs|sm|md|lg axis so an avatar lines up
12
+ with a same-size button / list row (AD-9).
13
+ ============================================ */
14
+
15
+ .avatar {
16
+ --avatar-size: 32px;
17
+ --avatar-font: var(--md-sys-typescale-label-large-size);
18
+
19
+ position: relative;
20
+ display: inline-flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ box-sizing: border-box;
24
+ width: var(--avatar-size);
25
+ height: var(--avatar-size);
26
+ flex-shrink: 0;
27
+ border-radius: var(--md-sys-shape-corner-full);
28
+ border: 1px solid var(--md-sys-color-outline-variant);
29
+ background: var(--md-sys-color-surface-container-high);
30
+ color: var(--md-sys-color-on-surface);
31
+ overflow: hidden;
32
+ user-select: none;
33
+ }
34
+
35
+ .avatar__initials {
36
+ font-family: var(--md-sys-typescale-label-large-font);
37
+ font-size: var(--avatar-font);
38
+ font-weight: 600;
39
+ line-height: 1;
40
+ letter-spacing: 0;
41
+ text-transform: uppercase;
42
+ }
43
+
44
+ .avatar__icon {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ color: var(--md-sys-color-on-surface-variant);
49
+ }
50
+
51
+ .avatar__img {
52
+ width: 100%;
53
+ height: 100%;
54
+ object-fit: cover;
55
+ display: block;
56
+ }
57
+
58
+ /* ---------- Sizes — one diameter per shared size axis ---------- */
59
+ .avatar--xs { --avatar-size: 22px; --avatar-font: var(--md-sys-typescale-label-small-size); }
60
+ .avatar--sm { --avatar-size: 28px; --avatar-font: var(--md-sys-typescale-label-medium-size); }
61
+ .avatar--md { --avatar-size: 32px; --avatar-font: var(--md-sys-typescale-label-large-size); }
62
+ .avatar--lg { --avatar-size: 40px; --avatar-font: var(--md-sys-typescale-title-small-size); }
@@ -0,0 +1,19 @@
1
+ import { LitElement } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ type AvatarSize = "xs" | "sm" | "md" | "lg";
4
+ declare class XmAvatar extends LitElement {
5
+ title: string;
6
+ name: string;
7
+ initials: string;
8
+ src: string;
9
+ size: AvatarSize;
10
+ private get _initials();
11
+ private get _label();
12
+ render(): TemplateResult;
13
+ }
14
+ declare global {
15
+ interface HTMLElementTagNameMap {
16
+ "xm-avatar": XmAvatar;
17
+ }
18
+ }
19
+ export {};
@@ -0,0 +1,112 @@
1
+ /*
2
+ avatar/index.ts — <xm-avatar>.
3
+
4
+ Circular identity token. Resolution order: image (`src`, optional) →
5
+ initials (`initials`, or derived from `name`) → person line-icon
6
+ fallback. No imagery is ever required (UX-DR8).
7
+
8
+ Authoring:
9
+ <xm-avatar name="Ada Lovelace"></xm-avatar>
10
+ <xm-avatar initials="JT" size="lg"></xm-avatar>
11
+ <xm-avatar size="sm"></xm-avatar> <!-- icon fallback -->
12
+
13
+ Properties:
14
+ name string — full name; initials derived from it, exposed as label
15
+ initials string — explicit initials (overrides derivation)
16
+ src string — optional image URL
17
+ size xs|sm|md|lg (default md) — shared control-height axis
18
+
19
+ Sits on a surface-container-* tier with on-surface ink (AD-13);
20
+ no gradient, no status hue (AD-11). Shadow DOM; Lit is a bare `import` (peer dep).
21
+ */
22
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
23
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
24
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
25
+ 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;
26
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
27
+ };
28
+ import { LitElement, html, nothing } from "lit";
29
+ import { customElement, property } from "lit/decorators.js";
30
+ const AVATAR_CSS = new URL("../avatar/index.css", import.meta.url).href;
31
+ const deriveInitials = (name) => {
32
+ const parts = name.trim().split(/\s+/).filter(Boolean);
33
+ if (parts.length === 0)
34
+ return "";
35
+ if (parts.length === 1)
36
+ return (parts[0] ?? "").slice(0, 2);
37
+ const first = parts[0] ?? "";
38
+ const last = parts[parts.length - 1] ?? "";
39
+ return (first.charAt(0) + last.charAt(0));
40
+ };
41
+ let XmAvatar = class XmAvatar extends LitElement {
42
+ constructor() {
43
+ super(...arguments);
44
+ this.title = "";
45
+ this.name = "";
46
+ this.initials = "";
47
+ this.src = "";
48
+ this.size = "md";
49
+ }
50
+ get _initials() {
51
+ return (this.initials || deriveInitials(this.name)).toUpperCase();
52
+ }
53
+ get _label() {
54
+ return this.title || this.name || (this._initials ? this._initials : "User");
55
+ }
56
+ render() {
57
+ const cls = `avatar avatar--${this.size}`;
58
+ let body;
59
+ if (this.src) {
60
+ body = html `<img class="avatar__img" src="${this.src}" alt="${this._label}" />`;
61
+ }
62
+ else if (this._initials) {
63
+ body = html `<span class="avatar__initials" aria-hidden="true">${this._initials}</span>`;
64
+ }
65
+ else {
66
+ body = html `
67
+ <span class="avatar__icon" aria-hidden="true">
68
+ <svg viewBox="0 0 24 24" width="60%" height="60%" fill="none"
69
+ stroke="currentColor" stroke-width="1.8"
70
+ stroke-linecap="round" stroke-linejoin="round">
71
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
72
+ <circle cx="12" cy="7" r="4" />
73
+ </svg>
74
+ </span>
75
+ `;
76
+ }
77
+ return html `
78
+ <link rel="stylesheet" href="${AVATAR_CSS}" />
79
+ <style>
80
+ :host {
81
+ display: inline-flex;
82
+ }
83
+ :host([hidden]) {
84
+ display: none;
85
+ }
86
+ </style>
87
+ <span
88
+ class="${cls}"
89
+ role="img"
90
+ aria-label=${this._label || nothing}
91
+ >${body}</span>
92
+ `;
93
+ }
94
+ };
95
+ __decorate([
96
+ property({ type: String })
97
+ ], XmAvatar.prototype, "title", void 0);
98
+ __decorate([
99
+ property({ type: String })
100
+ ], XmAvatar.prototype, "name", void 0);
101
+ __decorate([
102
+ property({ type: String })
103
+ ], XmAvatar.prototype, "initials", void 0);
104
+ __decorate([
105
+ property({ type: String })
106
+ ], XmAvatar.prototype, "src", void 0);
107
+ __decorate([
108
+ property({ type: String })
109
+ ], XmAvatar.prototype, "size", void 0);
110
+ XmAvatar = __decorate([
111
+ customElement("xm-avatar")
112
+ ], XmAvatar);