@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,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);
|