@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,50 @@
1
+ import type { PropertyValues, TemplateResult } from "lit";
2
+ import { XmField } from "../field/index.js";
3
+ import type { XmOverlay } from "../overlay/index.js";
4
+ export interface SelectOption {
5
+ label: string;
6
+ value: string | number;
7
+ disabled?: boolean;
8
+ }
9
+ export declare class XmSelect extends XmField {
10
+ /** The option model — `{ label, value, disabled? }` shared across
11
+ select / autocomplete / radio-group so one host handler is safe. */
12
+ options: SelectOption[];
13
+ /** Shown on the closed control when nothing is selected. */
14
+ placeholder: string;
15
+ protected _open: boolean;
16
+ protected _activeIndex: number;
17
+ /** The selected option's PRIMITIVE value (string | number), or null. */
18
+ protected _selectedValue: string | number | null;
19
+ protected _control: HTMLElement | null;
20
+ protected _overlay: XmOverlay | null;
21
+ private _typeahead;
22
+ private _typeaheadTimer;
23
+ connectedCallback(): void;
24
+ protected willUpdate(changed: PropertyValues<this>): void;
25
+ /** Live read for the closed control + xm-form. The inherited string `value`
26
+ stays in sync; this returns the typed primitive for consumers that want it. */
27
+ get selectedValue(): string | number | null;
28
+ private get _enabledIndexes();
29
+ private get _selectedLabel();
30
+ disconnectedCallback(): void;
31
+ protected updated(changed: PropertyValues<this>): void;
32
+ private _onDocPointerDown;
33
+ private _openList;
34
+ private _closeList;
35
+ private _onOverlayClose;
36
+ private _commitSelection;
37
+ private _selectActive;
38
+ private _selectOption;
39
+ private _onControlKeydown;
40
+ private _onListKeydown;
41
+ private _nextEnabled;
42
+ private _typeAhead;
43
+ protected renderControl(): TemplateResult;
44
+ private _renderOption;
45
+ }
46
+ declare global {
47
+ interface HTMLElementTagNameMap {
48
+ "xm-select": XmSelect;
49
+ }
50
+ }
@@ -0,0 +1,390 @@
1
+ /*
2
+ select/index.ts — <xm-select>, a single-select dropdown (Story 2.3).
3
+
4
+ Composes two foundations and re-implements NEITHER:
5
+ • XmField (Story 1.3) owns the chrome — label / helper / error / required /
6
+ disabled / focus ring / form association / ARIA association (AD-7).
7
+ • xm-overlay (Story 1.4) owns the anchored, non-modal listbox — positioning,
8
+ stacking tier, focus restore, scroll handling (AD-5/AD-12). We drive it
9
+ through its PUBLIC API only (open / mode / tier / placement / .anchor /
10
+ .opener / xm-overlay-close) and NEVER reach into its shadow root.
11
+
12
+ The closed control shows the selected option's label (or `placeholder`) and a
13
+ chevron that rotates on open. Activating it opens the listbox; selecting an
14
+ option commits the option's PRIMITIVE value (string | number), closes the
15
+ overlay, and restores focus to the control.
16
+
17
+ Value contract (AD-6 / AD-8a): uncontrolled-first. The selected primitive is
18
+ held internally and mirrored into the inherited string `value` (so xm-form and
19
+ ElementInternals see a stable serialization), while the `change` event carries
20
+ the real primitive in detail.value — never the option object.
21
+
22
+ Keyboard (WAI-ARIA listbox APG, AD-9a): closed → Enter/Space/↓ opens; open →
23
+ ↑/↓ move the active option (skipping disabled), Home/End jump to first/last
24
+ enabled, type-ahead matches labels, Enter selects + closes, Esc closes without
25
+ committing (routed through the overlay's innermost-Esc, which stopPropagations).
26
+
27
+ Shadow DOM. Lit is a bare `import` (peer dep).
28
+ */
29
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
30
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
31
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
32
+ 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;
33
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
34
+ };
35
+ import { html, nothing } from "lit";
36
+ import { customElement, property, state, query } from "lit/decorators.js";
37
+ import { XmField } from "../field/index.js";
38
+ const SELECT_CSS = new URL("../select/index.css", import.meta.url).href;
39
+ let XmSelect = class XmSelect extends XmField {
40
+ constructor() {
41
+ super(...arguments);
42
+ /** The option model — `{ label, value, disabled? }` shared across
43
+ select / autocomplete / radio-group so one host handler is safe. */
44
+ this.options = [];
45
+ /** Shown on the closed control when nothing is selected. */
46
+ this.placeholder = "Select…";
47
+ this._open = false;
48
+ this._activeIndex = -1;
49
+ /** The selected option's PRIMITIVE value (string | number), or null. */
50
+ this._selectedValue = null;
51
+ this._typeahead = "";
52
+ this._typeaheadTimer = 0;
53
+ // ── Open / close ────────────────────────────────────────────────────
54
+ // Close the open listbox when a pointer goes down anywhere outside this
55
+ // element (the non-modal overlay has no light-dismiss of its own).
56
+ this._onDocPointerDown = (e) => {
57
+ if (!this._open)
58
+ return;
59
+ const path = e.composedPath();
60
+ if (!path.includes(this))
61
+ this._closeList(false);
62
+ };
63
+ this._onOverlayClose = (e) => {
64
+ const detail = e.detail;
65
+ // Sync our open state when the overlay self-dismisses (Esc / backdrop).
66
+ if (this._open) {
67
+ this._open = false;
68
+ this._activeIndex = -1;
69
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
70
+ // The overlay already restored focus to its opener (the control) for
71
+ // Esc; only steer focus ourselves for non-escape dismissals.
72
+ if (detail?.reason !== "escape")
73
+ this._control?.focus();
74
+ }
75
+ };
76
+ // ── Keyboard (listbox APG) ──────────────────────────────────────────
77
+ this._onControlKeydown = (e) => {
78
+ if (this.nonInteractive)
79
+ return;
80
+ if (!this._open) {
81
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
82
+ e.preventDefault();
83
+ // Stop Enter/Space from reaching an ancestor xm-form (Enter-to-submit):
84
+ // opening the listbox must not also submit the surrounding form.
85
+ e.stopPropagation();
86
+ this._openList();
87
+ }
88
+ return;
89
+ }
90
+ this._onListKeydown(e);
91
+ };
92
+ }
93
+ connectedCallback() {
94
+ super.connectedCallback();
95
+ // Seed the selection from the uncontrolled `value` attribute (AD-6).
96
+ if (this.initialValue !== "" && this._selectedValue === null) {
97
+ const match = this.options.find((o) => String(o.value) === this.initialValue);
98
+ if (match)
99
+ this._commitSelection(match.value, false);
100
+ }
101
+ }
102
+ willUpdate(changed) {
103
+ super.willUpdate(changed);
104
+ // When options arrive after the value seed, resolve the seed once.
105
+ if (changed.has("options") &&
106
+ this._selectedValue === null &&
107
+ this.initialValue !== "") {
108
+ const match = this.options.find((o) => String(o.value) === this.initialValue);
109
+ if (match)
110
+ this._commitSelection(match.value, false);
111
+ }
112
+ }
113
+ /** Live read for the closed control + xm-form. The inherited string `value`
114
+ stays in sync; this returns the typed primitive for consumers that want it. */
115
+ get selectedValue() {
116
+ return this._selectedValue;
117
+ }
118
+ get _enabledIndexes() {
119
+ return this.options
120
+ .map((o, i) => (o.disabled ? -1 : i))
121
+ .filter((i) => i !== -1);
122
+ }
123
+ get _selectedLabel() {
124
+ const opt = this.options.find((o) => o.value === this._selectedValue);
125
+ return opt ? opt.label : "";
126
+ }
127
+ disconnectedCallback() {
128
+ super.disconnectedCallback();
129
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
130
+ }
131
+ updated(changed) {
132
+ super.updated?.(changed);
133
+ // Keep the active option visible when keyboard navigation moves it past the
134
+ // scrollable listbox fold.
135
+ if (this._open &&
136
+ changed.has("_activeIndex") &&
137
+ this._activeIndex >= 0) {
138
+ const active = this.renderRoot.querySelector(".select__option--active");
139
+ active?.scrollIntoView({ block: "nearest" });
140
+ }
141
+ }
142
+ _openList() {
143
+ if (this.nonInteractive || this._open)
144
+ return;
145
+ this._open = true;
146
+ document.addEventListener("pointerdown", this._onDocPointerDown, true);
147
+ // Land the active option on the current selection, else first enabled.
148
+ const selIdx = this.options.findIndex((o) => o.value === this._selectedValue);
149
+ this._activeIndex =
150
+ selIdx !== -1 && !this.options[selIdx]?.disabled
151
+ ? selIdx
152
+ : this._enabledIndexes[0] ?? -1;
153
+ this.updateComplete.then(() => {
154
+ const ov = this._overlay;
155
+ const ctrl = this._control;
156
+ if (ov && ctrl) {
157
+ ov.anchor = ctrl;
158
+ ov.opener = ctrl;
159
+ ov.show();
160
+ }
161
+ });
162
+ }
163
+ _closeList(focusControl = true) {
164
+ if (!this._open)
165
+ return;
166
+ this._open = false;
167
+ this._activeIndex = -1;
168
+ document.removeEventListener("pointerdown", this._onDocPointerDown, true);
169
+ const ov = this._overlay;
170
+ if (ov?.open)
171
+ ov.hide("api");
172
+ if (focusControl)
173
+ this._control?.focus();
174
+ }
175
+ // ── Selection commit ────────────────────────────────────────────────
176
+ _commitSelection(value, emit = true) {
177
+ this._selectedValue = value;
178
+ // Mark touched so the base's uncontrolled-first re-seed can't later clobber
179
+ // `_value` back to `initialValue` while `_selectedValue`/the label stay put.
180
+ this._dirty = true;
181
+ // Mirror into the inherited string value + form value (AD-6a).
182
+ this._value = String(value);
183
+ this.internals.setFormValue(this._value);
184
+ if (emit) {
185
+ this.dispatchEvent(new CustomEvent("change", {
186
+ bubbles: true,
187
+ composed: true,
188
+ detail: { value },
189
+ }));
190
+ }
191
+ }
192
+ _selectActive() {
193
+ const opt = this.options[this._activeIndex];
194
+ if (!opt || opt.disabled)
195
+ return;
196
+ this._commitSelection(opt.value);
197
+ this._closeList(true);
198
+ }
199
+ _selectOption(index) {
200
+ const opt = this.options[index];
201
+ if (!opt || opt.disabled)
202
+ return;
203
+ this._activeIndex = index;
204
+ this._commitSelection(opt.value);
205
+ this._closeList(true);
206
+ }
207
+ _onListKeydown(e) {
208
+ const enabled = this._enabledIndexes;
209
+ if (enabled.length === 0 && e.key !== "Escape")
210
+ return;
211
+ switch (e.key) {
212
+ case "ArrowDown": {
213
+ e.preventDefault();
214
+ this._activeIndex = this._nextEnabled(this._activeIndex, 1);
215
+ break;
216
+ }
217
+ case "ArrowUp": {
218
+ e.preventDefault();
219
+ this._activeIndex = this._nextEnabled(this._activeIndex, -1);
220
+ break;
221
+ }
222
+ case "Home": {
223
+ e.preventDefault();
224
+ this._activeIndex = enabled[0] ?? -1;
225
+ break;
226
+ }
227
+ case "End": {
228
+ e.preventDefault();
229
+ this._activeIndex = enabled[enabled.length - 1] ?? -1;
230
+ break;
231
+ }
232
+ case "Enter": {
233
+ e.preventDefault();
234
+ // Choosing an option must not bubble to an ancestor xm-form as submit.
235
+ e.stopPropagation();
236
+ this._selectActive();
237
+ break;
238
+ }
239
+ case "Tab": {
240
+ // Commit nothing; let focus leave — close the list.
241
+ this._closeList(false);
242
+ break;
243
+ }
244
+ // Esc is handled by the overlay (innermost-only, stopPropagation) →
245
+ // _onOverlayClose syncs our state. No local handler needed.
246
+ default: {
247
+ if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) {
248
+ this._typeAhead(e.key);
249
+ }
250
+ }
251
+ }
252
+ }
253
+ _nextEnabled(from, dir) {
254
+ const n = this.options.length;
255
+ if (n === 0)
256
+ return -1;
257
+ let i = from;
258
+ for (let step = 0; step < n; step++) {
259
+ i = (i + dir + n) % n;
260
+ if (!this.options[i]?.disabled)
261
+ return i;
262
+ }
263
+ return from;
264
+ }
265
+ _typeAhead(char) {
266
+ window.clearTimeout(this._typeaheadTimer);
267
+ this._typeahead += char.toLowerCase();
268
+ this._typeaheadTimer = window.setTimeout(() => {
269
+ this._typeahead = "";
270
+ }, 500);
271
+ const match = this.options.findIndex((o) => !o.disabled && o.label.toLowerCase().startsWith(this._typeahead));
272
+ if (match !== -1)
273
+ this._activeIndex = match;
274
+ }
275
+ // ── Render ──────────────────────────────────────────────────────────
276
+ renderControl() {
277
+ const a = this.controlAria;
278
+ const hasSelection = this._selectedValue !== null;
279
+ const display = hasSelection ? this._selectedLabel : this.placeholder;
280
+ const activeId = this._open && this._activeIndex >= 0
281
+ ? `${a.id}-opt-${this._activeIndex}`
282
+ : nothing;
283
+ return html `
284
+ <link rel="stylesheet" href="${SELECT_CSS}" />
285
+ <button
286
+ type="button"
287
+ class="select__control"
288
+ id=${a.id}
289
+ role="combobox"
290
+ aria-haspopup="listbox"
291
+ aria-expanded=${this._open ? "true" : "false"}
292
+ aria-controls="${a.id}-listbox"
293
+ aria-activedescendant=${activeId}
294
+ aria-describedby=${a.describedBy}
295
+ aria-invalid=${a.invalid ?? "false"}
296
+ aria-required=${a.required ?? nothing}
297
+ ?disabled=${this.effectiveDisabled}
298
+ @click=${() => (this._open ? this._closeList(true) : this._openList())}
299
+ @keydown=${this._onControlKeydown}
300
+ >
301
+ <span
302
+ class="select__value ${hasSelection
303
+ ? ""
304
+ : "select__value--placeholder"}"
305
+ >
306
+ ${display}
307
+ </span>
308
+ <span class="select__chevron ${this._open ? "select__chevron--open" : ""}">
309
+ <xm-chevron-down-icon size="16"></xm-chevron-down-icon>
310
+ </span>
311
+ </button>
312
+
313
+ <xm-overlay
314
+ mode="non-modal"
315
+ tier="menu"
316
+ placement="bottom-start"
317
+ label=${this.label || "Options"}
318
+ @xm-overlay-close=${this._onOverlayClose}
319
+ >
320
+ <ul
321
+ class="select__listbox"
322
+ id="${a.id}-listbox"
323
+ role="listbox"
324
+ aria-label=${this.label || "Options"}
325
+ @keydown=${(e) => this._onListKeydown(e)}
326
+ >
327
+ ${this.options.map((opt, i) => this._renderOption(opt, i, a.id))}
328
+ </ul>
329
+ </xm-overlay>
330
+ `;
331
+ }
332
+ _renderOption(opt, index, baseId) {
333
+ const selected = opt.value === this._selectedValue;
334
+ const active = index === this._activeIndex;
335
+ const cls = [
336
+ "select__option",
337
+ selected ? "select__option--selected" : "",
338
+ active ? "select__option--active" : "",
339
+ opt.disabled ? "select__option--disabled" : "",
340
+ ]
341
+ .filter(Boolean)
342
+ .join(" ");
343
+ return html `
344
+ <li
345
+ class="${cls}"
346
+ id="${baseId}-opt-${index}"
347
+ role="option"
348
+ aria-selected=${selected ? "true" : "false"}
349
+ aria-disabled=${opt.disabled ? "true" : nothing}
350
+ @click=${() => this._selectOption(index)}
351
+ @mousemove=${() => {
352
+ if (!opt.disabled)
353
+ this._activeIndex = index;
354
+ }}
355
+ >
356
+ <span class="select__option-label">${opt.label}</span>
357
+ ${selected
358
+ ? html `<span class="select__option-check" aria-hidden="true">
359
+ <xm-check-icon size="16"></xm-check-icon>
360
+ </span>`
361
+ : nothing}
362
+ </li>
363
+ `;
364
+ }
365
+ };
366
+ __decorate([
367
+ property({ attribute: false })
368
+ ], XmSelect.prototype, "options", void 0);
369
+ __decorate([
370
+ property({ type: String })
371
+ ], XmSelect.prototype, "placeholder", void 0);
372
+ __decorate([
373
+ state()
374
+ ], XmSelect.prototype, "_open", void 0);
375
+ __decorate([
376
+ state()
377
+ ], XmSelect.prototype, "_activeIndex", void 0);
378
+ __decorate([
379
+ state()
380
+ ], XmSelect.prototype, "_selectedValue", void 0);
381
+ __decorate([
382
+ query(".select__control")
383
+ ], XmSelect.prototype, "_control", void 0);
384
+ __decorate([
385
+ query("xm-overlay")
386
+ ], XmSelect.prototype, "_overlay", void 0);
387
+ XmSelect = __decorate([
388
+ customElement("xm-select")
389
+ ], XmSelect);
390
+ export { XmSelect };
@@ -0,0 +1,133 @@
1
+ /* ============================================================
2
+ <SidebarItem /> — chat-list row in the sidebar.
3
+
4
+ Two layouts share the same hover/active backgrounds:
5
+ row — single-line: [dot] [title] (sidebar-item.html)
6
+ stacked — single-line title in chat.html
7
+
8
+ Plus a collapsed variant that renders as a 4px stripe rail when the
9
+ sidebar is in icon-only mode.
10
+
11
+ Pairs with components/sidebar-item/index.jsx. The CSS lives on the
12
+ chat-shell `surface` (the warm dark "desk" / cream in light) and uses
13
+ `on-surface*` tokens accordingly.
14
+ ============================================================ */
15
+
16
+ .sidebar-item {
17
+ cursor: pointer;
18
+ border-radius: 8px;
19
+ transition: background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
20
+ }
21
+
22
+ /* ---------- Layout: row ---------- */
23
+ .sidebar-item--row {
24
+ display: flex;
25
+ align-items: center;
26
+ gap: var(--s-2-5);
27
+ padding: 9px var(--s-2-5);
28
+ }
29
+ .sidebar-item--row .sidebar-item__title {
30
+ font: 500 13px/1.2 var(--md-sys-typescale-body-large-font);
31
+ color: var(--md-sys-color-on-surface);
32
+ flex: 1;
33
+ min-width: 0;
34
+ overflow: hidden;
35
+ text-overflow: ellipsis;
36
+ white-space: nowrap;
37
+ }
38
+ /* The dot in the row layout is a leading 8px circle; consumers
39
+ pass showDot to render it filled. The slot is always present so
40
+ titles align across rows. */
41
+ .sidebar-item--row .sidebar-item__dot {
42
+ width: 8px; height: 8px;
43
+ border-radius: 999px;
44
+ background: transparent;
45
+ flex-shrink: 0;
46
+ }
47
+ .sidebar-item--row.has-dot .sidebar-item__dot {
48
+ background: var(--md-sys-color-primary);
49
+ }
50
+
51
+ /* ---------- Layout: stacked ---------- */
52
+ .sidebar-item--stacked {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: var(--s-0-5);
56
+ padding: var(--s-2) var(--s-2-5);
57
+ }
58
+ .sidebar-item--stacked .sidebar-item__title {
59
+ font: 500 13px/1.25 var(--md-sys-typescale-body-large-font);
60
+ color: var(--md-sys-color-on-surface);
61
+ overflow: hidden;
62
+ text-overflow: ellipsis;
63
+ white-space: nowrap;
64
+ }
65
+
66
+ /* ---------- Hover & active (shared across layouts) ----------
67
+ Hover and active were the same near-black surface-container-lowest, so
68
+ the two states read identically and reused the vanishing-bubble color.
69
+ The sidebar rides `surface` (the desk), so both states must be a step ABOVE
70
+ it to read — surface-container-low equals `surface` in dark and would vanish.
71
+ Ramp upward: desk → hover (surface-container) → active (surface-container-high,
72
+ the same raised family the user bubble / composer ride). */
73
+ .sidebar-item:hover,
74
+ .sidebar-item.is-hover {
75
+ background: var(--md-sys-color-surface-container);
76
+ }
77
+ .sidebar-item.is-active {
78
+ background: var(--md-sys-color-surface-container-high);
79
+ }
80
+ .sidebar-item:hover .sidebar-item__title,
81
+ .sidebar-item.is-hover .sidebar-item__title,
82
+ .sidebar-item.is-active .sidebar-item__title {
83
+ color: var(--md-sys-color-on-surface);
84
+ }
85
+
86
+ /* Dark theme — push the resting title color further into gray so the
87
+ active item's strong-ink title pops as the brighter, more-white state.
88
+ Light theme keeps the standard on-surface ramp (already reads as a clear
89
+ default → strong contrast there). */
90
+ [data-theme="dark"] .sidebar-item:not(.is-active):not(.is-hover):not(:hover) .sidebar-item__title {
91
+ color: var(--md-sys-color-on-surface-variant);
92
+ }
93
+
94
+ /* ---------- Collapsed: 4px stripe rail ---------- */
95
+ .sidebar-item--collapsed {
96
+ width: 24px;
97
+ height: 4px;
98
+ padding: 0;
99
+ border-radius: 2px;
100
+ background: var(--xm-color-on-surface-soft);
101
+ opacity: 0.35;
102
+ transition:
103
+ opacity var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
104
+ background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
105
+ }
106
+ .sidebar-item--collapsed:hover {
107
+ background: var(--md-sys-color-on-surface);
108
+ opacity: 0.6;
109
+ }
110
+ .sidebar-item--collapsed.is-active {
111
+ background: var(--md-sys-color-on-surface);
112
+ opacity: 0.85;
113
+ box-shadow: none;
114
+ }
115
+ .sidebar-item--collapsed > * { display: none; }
116
+
117
+ /* ---------- Eyebrow: section header above a group of items ----------
118
+ Renders as an uppercase mono caption on the surface. No hover, no
119
+ active — it's a label, not a target. */
120
+ .sidebar-item--eyebrow {
121
+ cursor: default;
122
+ padding: var(--s-3-5) var(--s-2-5) var(--s-1-5);
123
+ font:
124
+ var(--md-sys-typescale-label-small-weight)
125
+ var(--md-sys-typescale-label-small-size) /
126
+ var(--md-sys-typescale-label-small-line-height)
127
+ var(--md-sys-typescale-label-small-font);
128
+ color: var(--xm-color-on-surface-soft);
129
+ text-transform: uppercase;
130
+ letter-spacing: 0.04em;
131
+ font-family: var(--xm-typescale-mono-font);
132
+ }
133
+ .sidebar-item--eyebrow:hover { background: transparent; }
@@ -0,0 +1,20 @@
1
+ import { LitElement } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ type SidebarLayout = "row" | "stacked";
4
+ declare class XmSidebarItem extends LitElement {
5
+ title: string;
6
+ layout: SidebarLayout;
7
+ active: boolean;
8
+ hover: boolean;
9
+ showDot: boolean;
10
+ collapsed: boolean;
11
+ createRenderRoot(): HTMLElement | DocumentFragment;
12
+ connectedCallback(): void;
13
+ render(): TemplateResult;
14
+ }
15
+ declare global {
16
+ interface HTMLElementTagNameMap {
17
+ "xm-sidebar-item": XmSidebarItem;
18
+ }
19
+ }
20
+ export {};