@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,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 {};
|