@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,320 @@
|
|
|
1
|
+
/*
|
|
2
|
+
tabs/index.ts — <xm-tabs> / <xm-tab> / <xm-tab-panel>.
|
|
3
|
+
|
|
4
|
+
A tab strip that switches panels. Three elements registered in one file
|
|
5
|
+
(the multi-element carve-out, xm-<name>-<part> naming):
|
|
6
|
+
|
|
7
|
+
<xm-tabs value="overview">
|
|
8
|
+
<xm-tab value="overview">Overview</xm-tab>
|
|
9
|
+
<xm-tab value="activity">Activity</xm-tab>
|
|
10
|
+
<xm-tab value="settings" disabled>Settings</xm-tab>
|
|
11
|
+
|
|
12
|
+
<xm-tab-panel value="overview">…</xm-tab-panel>
|
|
13
|
+
<xm-tab-panel value="activity">…</xm-tab-panel>
|
|
14
|
+
<xm-tab-panel value="settings">…</xm-tab-panel>
|
|
15
|
+
</xm-tabs>
|
|
16
|
+
|
|
17
|
+
xm-tabs the tablist container + sliding coral indicator + panel host.
|
|
18
|
+
Owns the active value (uncontrolled-first), roving tabindex, the
|
|
19
|
+
WAI-ARIA horizontal tablist keymap, and the change event.
|
|
20
|
+
xm-tab a single tab. Light DOM so the HOST carries role="tab" /
|
|
21
|
+
aria-selected / aria-controls / roving tabindex and is the focus
|
|
22
|
+
target. Properties: value (String), disabled (Boolean).
|
|
23
|
+
xm-tab-panel a single panel. Light DOM so the HOST carries role="tabpanel"
|
|
24
|
+
/ aria-labelledby and is hidden when inactive. Property: value.
|
|
25
|
+
|
|
26
|
+
Keyboard (WAI-ARIA tablist, horizontal): ←/→ move active tab, Home/End jump
|
|
27
|
+
to first/last enabled tab, Enter/Space activate. Roving tabindex: only the
|
|
28
|
+
active tab is tabindex=0.
|
|
29
|
+
|
|
30
|
+
Event: change — { detail: { value }, bubbles, composed }; detail.value is the
|
|
31
|
+
activated tab's PRIMITIVE value (string), never the element.
|
|
32
|
+
|
|
33
|
+
Surface / ink (AD-13): a transparent strip tracing its host surface. On the
|
|
34
|
+
desk family active ink = on-surface, inactive = on-surface-variant; the coral
|
|
35
|
+
indicator is --md-sys-color-primary; hairline 1px baseline rule. Indicator
|
|
36
|
+
slide is short3 standard easing.
|
|
37
|
+
|
|
38
|
+
Shadow DOM (xm-tabs) / light DOM (xm-tab, xm-tab-panel); Lit from
|
|
39
|
+
lit; sibling CSS via the built-file-relative new URL(...).
|
|
40
|
+
BEM root block `tabs`.
|
|
41
|
+
*/
|
|
42
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
43
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
44
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
45
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
46
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
47
|
+
};
|
|
48
|
+
import { LitElement, html } from "lit";
|
|
49
|
+
import { customElement, property, query } from "lit/decorators.js";
|
|
50
|
+
// Resolve CSS relative to the *built* file:
|
|
51
|
+
// lit/build/components/tabs/index.js → ../tabs/index.css.
|
|
52
|
+
const TABS_CSS = new URL("../tabs/index.css", import.meta.url).href;
|
|
53
|
+
let tabsSeq = 0;
|
|
54
|
+
/* ─────────────────────────────────────────────────────────────
|
|
55
|
+
<xm-tab> — one tab. Light DOM: the host is the role="tab" target.
|
|
56
|
+
─────────────────────────────────────────────────────────────*/
|
|
57
|
+
let XmTab = class XmTab extends LitElement {
|
|
58
|
+
constructor() {
|
|
59
|
+
super(...arguments);
|
|
60
|
+
this.value = "";
|
|
61
|
+
this.disabled = false;
|
|
62
|
+
}
|
|
63
|
+
createRenderRoot() {
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
connectedCallback() {
|
|
67
|
+
super.connectedCallback();
|
|
68
|
+
if (!this.hasAttribute("role"))
|
|
69
|
+
this.setAttribute("role", "tab");
|
|
70
|
+
// Route into <xm-tabs>'s named "tab" slot so panels (default slot) and
|
|
71
|
+
// tabs are projected into separate regions.
|
|
72
|
+
if (this.getAttribute("slot") !== "tab")
|
|
73
|
+
this.setAttribute("slot", "tab");
|
|
74
|
+
}
|
|
75
|
+
render() {
|
|
76
|
+
return html `<span class="tabs__label"><slot></slot></span>`;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
__decorate([
|
|
80
|
+
property({ type: String })
|
|
81
|
+
], XmTab.prototype, "value", void 0);
|
|
82
|
+
__decorate([
|
|
83
|
+
property({ type: Boolean, reflect: true })
|
|
84
|
+
], XmTab.prototype, "disabled", void 0);
|
|
85
|
+
XmTab = __decorate([
|
|
86
|
+
customElement("xm-tab")
|
|
87
|
+
], XmTab);
|
|
88
|
+
/* ─────────────────────────────────────────────────────────────
|
|
89
|
+
<xm-tab-panel> — one panel. Light DOM: the host is role="tabpanel".
|
|
90
|
+
─────────────────────────────────────────────────────────────*/
|
|
91
|
+
let XmTabPanel = class XmTabPanel extends LitElement {
|
|
92
|
+
constructor() {
|
|
93
|
+
super(...arguments);
|
|
94
|
+
this.value = "";
|
|
95
|
+
}
|
|
96
|
+
createRenderRoot() {
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
connectedCallback() {
|
|
100
|
+
super.connectedCallback();
|
|
101
|
+
if (!this.hasAttribute("role"))
|
|
102
|
+
this.setAttribute("role", "tabpanel");
|
|
103
|
+
}
|
|
104
|
+
render() {
|
|
105
|
+
return html `<slot></slot>`;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
__decorate([
|
|
109
|
+
property({ type: String })
|
|
110
|
+
], XmTabPanel.prototype, "value", void 0);
|
|
111
|
+
XmTabPanel = __decorate([
|
|
112
|
+
customElement("xm-tab-panel")
|
|
113
|
+
], XmTabPanel);
|
|
114
|
+
/* ─────────────────────────────────────────────────────────────
|
|
115
|
+
<xm-tabs> — the tablist container.
|
|
116
|
+
─────────────────────────────────────────────────────────────*/
|
|
117
|
+
let XmTabs = class XmTabs extends LitElement {
|
|
118
|
+
constructor() {
|
|
119
|
+
super(...arguments);
|
|
120
|
+
this.value = "";
|
|
121
|
+
this.size = "md";
|
|
122
|
+
this._id = `xm-tabs-${++tabsSeq}`;
|
|
123
|
+
this._activeValue = "";
|
|
124
|
+
this._onResize = () => {
|
|
125
|
+
this._positionIndicator();
|
|
126
|
+
};
|
|
127
|
+
// Wire IDs / ARIA / roving tabindex / panel visibility, then place the
|
|
128
|
+
// coral indicator under the active tab.
|
|
129
|
+
this._sync = () => {
|
|
130
|
+
const tabs = this._tabs;
|
|
131
|
+
const panels = this._panels;
|
|
132
|
+
if (tabs.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const enabled = this._enabledTabs();
|
|
135
|
+
if (!this._activeValue ||
|
|
136
|
+
!enabled.some((t) => t.value === this._activeValue)) {
|
|
137
|
+
const first = enabled[0] ?? tabs[0];
|
|
138
|
+
this._activeValue = first ? first.value : "";
|
|
139
|
+
}
|
|
140
|
+
tabs.forEach((tab, i) => {
|
|
141
|
+
const active = tab.value === this._activeValue;
|
|
142
|
+
const tabId = `${this._id}-tab-${i}`;
|
|
143
|
+
const panelId = `${this._id}-panel-${i}`;
|
|
144
|
+
tab.id ||= tabId;
|
|
145
|
+
tab.classList.add("tabs__tab");
|
|
146
|
+
tab.classList.toggle("tabs__tab--active", active);
|
|
147
|
+
tab.classList.toggle("tabs__tab--disabled", tab.disabled);
|
|
148
|
+
tab.setAttribute("aria-selected", active ? "true" : "false");
|
|
149
|
+
tab.setAttribute("tabindex", active ? "0" : "-1");
|
|
150
|
+
if (tab.disabled)
|
|
151
|
+
tab.setAttribute("aria-disabled", "true");
|
|
152
|
+
else
|
|
153
|
+
tab.removeAttribute("aria-disabled");
|
|
154
|
+
const panel = panels.find((p) => p.value === tab.value);
|
|
155
|
+
if (panel) {
|
|
156
|
+
panel.id ||= panelId;
|
|
157
|
+
panel.classList.add("tabs__panel");
|
|
158
|
+
tab.setAttribute("aria-controls", panel.id);
|
|
159
|
+
panel.setAttribute("aria-labelledby", tab.id);
|
|
160
|
+
panel.toggleAttribute("hidden", !active);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
this._positionIndicator();
|
|
164
|
+
};
|
|
165
|
+
this._onClick = (event) => {
|
|
166
|
+
const path = event.composedPath();
|
|
167
|
+
const tab = path.find((n) => n instanceof HTMLElement && n.tagName === "XM-TAB");
|
|
168
|
+
if (tab && !tab.disabled)
|
|
169
|
+
this._activate(tab.value, false);
|
|
170
|
+
};
|
|
171
|
+
// WAI-ARIA horizontal tablist keymap on the roving-focus tablist.
|
|
172
|
+
this._onKeydown = (event) => {
|
|
173
|
+
const enabled = this._enabledTabs();
|
|
174
|
+
if (enabled.length === 0)
|
|
175
|
+
return;
|
|
176
|
+
const currentIndex = enabled.findIndex((t) => t.value === this._activeValue);
|
|
177
|
+
let nextIndex = currentIndex;
|
|
178
|
+
switch (event.key) {
|
|
179
|
+
case "ArrowRight":
|
|
180
|
+
nextIndex = (currentIndex + 1) % enabled.length;
|
|
181
|
+
break;
|
|
182
|
+
case "ArrowLeft":
|
|
183
|
+
nextIndex = (currentIndex - 1 + enabled.length) % enabled.length;
|
|
184
|
+
break;
|
|
185
|
+
case "Home":
|
|
186
|
+
nextIndex = 0;
|
|
187
|
+
break;
|
|
188
|
+
case "End":
|
|
189
|
+
nextIndex = enabled.length - 1;
|
|
190
|
+
break;
|
|
191
|
+
case "Enter":
|
|
192
|
+
case " ": {
|
|
193
|
+
const path = event.composedPath();
|
|
194
|
+
const tab = path.find((n) => n instanceof HTMLElement && n.tagName === "XM-TAB");
|
|
195
|
+
if (tab && !tab.disabled) {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
this._activate(tab.value);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
default:
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
const next = enabled[nextIndex];
|
|
206
|
+
if (next)
|
|
207
|
+
this._activate(next.value);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
render() {
|
|
211
|
+
const size = ["xs", "sm", "md", "lg"].includes(this.size)
|
|
212
|
+
? this.size
|
|
213
|
+
: "md";
|
|
214
|
+
return html `
|
|
215
|
+
<link rel="stylesheet" href="${TABS_CSS}" />
|
|
216
|
+
<style>
|
|
217
|
+
:host {
|
|
218
|
+
display: block;
|
|
219
|
+
}
|
|
220
|
+
:host([hidden]) {
|
|
221
|
+
display: none;
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
224
|
+
<div class="tabs tabs--${size}">
|
|
225
|
+
<div
|
|
226
|
+
class="tabs__list"
|
|
227
|
+
role="tablist"
|
|
228
|
+
aria-orientation="horizontal"
|
|
229
|
+
@slotchange=${this._sync}
|
|
230
|
+
@keydown=${this._onKeydown}
|
|
231
|
+
@click=${this._onClick}
|
|
232
|
+
>
|
|
233
|
+
<slot name="tab" @slotchange=${this._sync}></slot>
|
|
234
|
+
<span class="tabs__indicator" aria-hidden="true"></span>
|
|
235
|
+
</div>
|
|
236
|
+
<div class="tabs__panels">
|
|
237
|
+
<slot @slotchange=${this._sync}></slot>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
242
|
+
firstUpdated() {
|
|
243
|
+
this._activeValue = this.value;
|
|
244
|
+
this._sync();
|
|
245
|
+
}
|
|
246
|
+
updated(changed) {
|
|
247
|
+
if (changed.has("value") && this.value !== this._activeValue) {
|
|
248
|
+
this._activeValue = this.value;
|
|
249
|
+
this._sync();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
connectedCallback() {
|
|
253
|
+
super.connectedCallback();
|
|
254
|
+
window.addEventListener("resize", this._onResize);
|
|
255
|
+
}
|
|
256
|
+
disconnectedCallback() {
|
|
257
|
+
super.disconnectedCallback();
|
|
258
|
+
window.removeEventListener("resize", this._onResize);
|
|
259
|
+
}
|
|
260
|
+
get _tabs() {
|
|
261
|
+
return Array.from(this.querySelectorAll("xm-tab"));
|
|
262
|
+
}
|
|
263
|
+
get _panels() {
|
|
264
|
+
return Array.from(this.querySelectorAll("xm-tab-panel"));
|
|
265
|
+
}
|
|
266
|
+
_enabledTabs() {
|
|
267
|
+
return this._tabs.filter((t) => !t.disabled);
|
|
268
|
+
}
|
|
269
|
+
_positionIndicator() {
|
|
270
|
+
const indicator = this._indicator;
|
|
271
|
+
const list = this._list;
|
|
272
|
+
if (!indicator || !list)
|
|
273
|
+
return;
|
|
274
|
+
const active = this._tabs.find((t) => t.value === this._activeValue);
|
|
275
|
+
if (!active) {
|
|
276
|
+
indicator.style.opacity = "0";
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const listRect = list.getBoundingClientRect();
|
|
280
|
+
const tabRect = active.getBoundingClientRect();
|
|
281
|
+
const left = tabRect.left - listRect.left + list.scrollLeft;
|
|
282
|
+
indicator.style.opacity = "1";
|
|
283
|
+
indicator.style.width = `${Math.round(tabRect.width)}px`;
|
|
284
|
+
indicator.style.transform = `translateX(${Math.round(left)}px)`;
|
|
285
|
+
}
|
|
286
|
+
_activate(value, focus = true) {
|
|
287
|
+
const tab = this._tabs.find((t) => t.value === value);
|
|
288
|
+
if (!tab || tab.disabled)
|
|
289
|
+
return;
|
|
290
|
+
const changed = value !== this._activeValue;
|
|
291
|
+
this._activeValue = value;
|
|
292
|
+
this.value = value;
|
|
293
|
+
this._sync();
|
|
294
|
+
if (focus)
|
|
295
|
+
tab.focus();
|
|
296
|
+
if (changed) {
|
|
297
|
+
const detail = { value };
|
|
298
|
+
this.dispatchEvent(new CustomEvent("change", {
|
|
299
|
+
detail,
|
|
300
|
+
bubbles: true,
|
|
301
|
+
composed: true,
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
__decorate([
|
|
307
|
+
property({ type: String })
|
|
308
|
+
], XmTabs.prototype, "value", void 0);
|
|
309
|
+
__decorate([
|
|
310
|
+
property({ type: String })
|
|
311
|
+
], XmTabs.prototype, "size", void 0);
|
|
312
|
+
__decorate([
|
|
313
|
+
query(".tabs__list")
|
|
314
|
+
], XmTabs.prototype, "_list", void 0);
|
|
315
|
+
__decorate([
|
|
316
|
+
query(".tabs__indicator")
|
|
317
|
+
], XmTabs.prototype, "_indicator", void 0);
|
|
318
|
+
XmTabs = __decorate([
|
|
319
|
+
customElement("xm-tabs")
|
|
320
|
+
], XmTabs);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
xm-text-field — concrete control for XmField (Story 2.1).
|
|
3
|
+
|
|
4
|
+
The chrome (surface, border, radius, height, focus ring, label, helper/
|
|
5
|
+
error row, disabled/readonly) is owned by the XmField base — see
|
|
6
|
+
field/index.css. The native <input> rendered by this subclass is already
|
|
7
|
+
styled by the base's `.field__control input` rule (transparent, borderless,
|
|
8
|
+
full-bleed, inherits ink + type). So THIS file owns only the leading/trailing
|
|
9
|
+
affordance rails: the icon-left / icon-right slots and the prefix / suffix
|
|
10
|
+
inline units that flank the input inside the shared control wrapper.
|
|
11
|
+
|
|
12
|
+
Surface/ink: the field sits on the inverse-surface card tier, so all ink here
|
|
13
|
+
is inverse-on-surface (icons) or inverse-on-surface-muted (prefix/suffix
|
|
14
|
+
units) — matching the host surface (AD-13). No error color: severity is the base's
|
|
15
|
+
icon + copy in the message row (AD-11). Tokens only (AD-1).
|
|
16
|
+
|
|
17
|
+
BEM block: `text-field`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
|
|
18
|
+
============================================ */
|
|
19
|
+
|
|
20
|
+
/* ---------- Leading / trailing rails ----------
|
|
21
|
+
Flex children of the base's .field__control row, sitting on either side of
|
|
22
|
+
the input. Each collapses to zero width when its slots are empty, so the
|
|
23
|
+
input keeps its native edge padding when no affordance is authored. The
|
|
24
|
+
icon↔text gap rides the --s-N spacing scale to match the shared chrome. */
|
|
25
|
+
.text-field__lead,
|
|
26
|
+
.text-field__trail {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--s-2);
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
color: var(--md-sys-color-inverse-on-surface);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Empty rails contribute no inset — the inner pad-collapse below removes the
|
|
35
|
+
matching input padding so a field with no affordance looks untouched. */
|
|
36
|
+
.text-field__lead:not(:has(*)),
|
|
37
|
+
.text-field__trail:not(:has(*)) {
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* When a leading rail is present it owns the leading inset; the input drops its
|
|
42
|
+
own left pad so the icon/prefix sits at the wrapper edge inset, and text
|
|
43
|
+
follows the rail's gap. Mirror on the trailing side. */
|
|
44
|
+
.text-field__lead {
|
|
45
|
+
padding-left: var(--s-3);
|
|
46
|
+
}
|
|
47
|
+
.text-field__trail {
|
|
48
|
+
padding-right: var(--s-3);
|
|
49
|
+
}
|
|
50
|
+
.text-field__lead:not(:has(*)) + .text-field__input {
|
|
51
|
+
padding-left: var(--s-3);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ---------- Prefix / suffix inline units ----------
|
|
55
|
+
Static affordances like `$` or `KB` — quieter than the live value, so they
|
|
56
|
+
take the muted inverse ink. Slotted content inherits this via the rail color +
|
|
57
|
+
::slotted override. */
|
|
58
|
+
.text-field__lead ::slotted([slot="prefix"]),
|
|
59
|
+
.text-field__trail ::slotted([slot="suffix"]) {
|
|
60
|
+
color: var(--xm-color-inverse-on-surface-muted);
|
|
61
|
+
font:
|
|
62
|
+
var(--md-sys-typescale-body-large-weight)
|
|
63
|
+
var(--md-sys-typescale-body-large-size) /
|
|
64
|
+
var(--md-sys-typescale-body-large-line-height)
|
|
65
|
+
var(--md-sys-typescale-body-large-font);
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Icons inherit currentColor from the rail (inverse-on-surface), matching the
|
|
70
|
+
host card surface — never an accent or error hue. */
|
|
71
|
+
.text-field__lead ::slotted([slot="icon-left"]),
|
|
72
|
+
.text-field__trail ::slotted([slot="icon-right"]) {
|
|
73
|
+
display: inline-flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
color: inherit;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The input flexes to fill the gap between the rails. Its transparent /
|
|
79
|
+
borderless / type styling comes from the base; the rails set the inset, so
|
|
80
|
+
when a rail is present the input loses the matching native edge pad. */
|
|
81
|
+
.text-field__input {
|
|
82
|
+
flex: 1;
|
|
83
|
+
min-width: 0;
|
|
84
|
+
}
|
|
85
|
+
.text-field__lead:has(*) + .text-field__input {
|
|
86
|
+
padding-left: var(--s-2);
|
|
87
|
+
}
|
|
88
|
+
.text-field__input:has(+ .text-field__trail:has(*)) {
|
|
89
|
+
padding-right: var(--s-2);
|
|
90
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { TemplateResult } from "lit";
|
|
2
|
+
import { XmField } from "../field/index.js";
|
|
3
|
+
export type TextFieldType = "text" | "email" | "password" | "number" | "search" | "url" | "tel";
|
|
4
|
+
export declare class XmTextField extends XmField {
|
|
5
|
+
/** Native input type — single-line text variants only. */
|
|
6
|
+
type: TextFieldType;
|
|
7
|
+
/** Native placeholder — shown when the live value is empty. */
|
|
8
|
+
placeholder: string;
|
|
9
|
+
private _onInput;
|
|
10
|
+
private _onChange;
|
|
11
|
+
protected renderControl(): TemplateResult;
|
|
12
|
+
}
|
|
13
|
+
declare global {
|
|
14
|
+
interface HTMLElementTagNameMap {
|
|
15
|
+
"xm-text-field": XmTextField;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/*
|
|
2
|
+
text-field/index.ts — the first concrete XmField subclass (Epic 2 Story 2.1).
|
|
3
|
+
|
|
4
|
+
<xm-text-field> — single-line text input. It extends the abstract XmField
|
|
5
|
+
base (lit/components/field), so the chrome — label row, control wrapper at
|
|
6
|
+
the shared size height, helper/error row, required marker, focus ring,
|
|
7
|
+
disabled/readonly/loading rendering, ARIA wiring, form association, and the
|
|
8
|
+
uncontrolled-first value lifecycle — is NOT re-implemented here (AD-7). The
|
|
9
|
+
subclass supplies ONLY the concrete control: a native <input> projected into
|
|
10
|
+
the base's control wrapper via renderControl(), plus the type/prefix/suffix
|
|
11
|
+
and icon-left/icon-right affordances.
|
|
12
|
+
|
|
13
|
+
Authoring:
|
|
14
|
+
<xm-text-field
|
|
15
|
+
label="Email"
|
|
16
|
+
type="email"
|
|
17
|
+
placeholder="you@example.com"
|
|
18
|
+
helper="We never share it."
|
|
19
|
+
required
|
|
20
|
+
>
|
|
21
|
+
<xm-search-icon slot="icon-left"></xm-search-icon>
|
|
22
|
+
<span slot="suffix">KB</span>
|
|
23
|
+
</xm-text-field>
|
|
24
|
+
|
|
25
|
+
Value ownership is uncontrolled-first (AD-6): the `value` attribute seeds the
|
|
26
|
+
initial value once; the field then owns its live state. The base's emitInput /
|
|
27
|
+
emitChange fire bubbling + composed `input` (per keystroke) and `change`
|
|
28
|
+
(on commit: blur / Enter / native change) with detail.value, and mirror the
|
|
29
|
+
value into ElementInternals for xm-form (AD-6a/AD-8a).
|
|
30
|
+
|
|
31
|
+
Shadow DOM. The native <input> rendered here is ALREADY styled by the base's
|
|
32
|
+
`.field__control input` rule (transparent / borderless / full-bleed / inherits
|
|
33
|
+
ink + type) — so this component's CSS owns only the prefix/suffix/icon slots,
|
|
34
|
+
never the input box. Lit is a bare `import` (peer dep).
|
|
35
|
+
*/
|
|
36
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
37
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
38
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
39
|
+
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;
|
|
40
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
41
|
+
};
|
|
42
|
+
import { html, nothing } from "lit";
|
|
43
|
+
import { customElement, property } from "lit/decorators.js";
|
|
44
|
+
import { XmField } from "../field/index.js";
|
|
45
|
+
// Resolve CSS relative to the *built* file:
|
|
46
|
+
// lit/build/components/text-field/index.js → ../text-field/index.css.
|
|
47
|
+
const TEXT_FIELD_CSS = new URL("../text-field/index.css", import.meta.url).href;
|
|
48
|
+
let XmTextField = class XmTextField extends XmField {
|
|
49
|
+
constructor() {
|
|
50
|
+
super(...arguments);
|
|
51
|
+
/** Native input type — single-line text variants only. */
|
|
52
|
+
this.type = "text";
|
|
53
|
+
/** Native placeholder — shown when the live value is empty. */
|
|
54
|
+
this.placeholder = "";
|
|
55
|
+
this._onInput = (e) => {
|
|
56
|
+
this.emitInput(e.target.value);
|
|
57
|
+
};
|
|
58
|
+
this._onChange = (e) => {
|
|
59
|
+
this.emitChange(e.target.value);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
renderControl() {
|
|
63
|
+
const a = this.controlAria;
|
|
64
|
+
return html `
|
|
65
|
+
<link rel="stylesheet" href="${TEXT_FIELD_CSS}" />
|
|
66
|
+
<span class="text-field__lead">
|
|
67
|
+
<slot name="icon-left"></slot>
|
|
68
|
+
<slot name="prefix"></slot>
|
|
69
|
+
</span>
|
|
70
|
+
<input
|
|
71
|
+
class="text-field__input"
|
|
72
|
+
id=${a.id}
|
|
73
|
+
type=${this.type}
|
|
74
|
+
.value=${this._value}
|
|
75
|
+
placeholder=${this.placeholder || nothing}
|
|
76
|
+
name=${this.name || nothing}
|
|
77
|
+
aria-describedby=${a.describedBy ?? nothing}
|
|
78
|
+
aria-invalid=${a.invalid ?? "false"}
|
|
79
|
+
?required=${this.required}
|
|
80
|
+
?disabled=${this.effectiveDisabled}
|
|
81
|
+
?readonly=${this.readonly}
|
|
82
|
+
@input=${this._onInput}
|
|
83
|
+
@change=${this._onChange}
|
|
84
|
+
/>
|
|
85
|
+
<span class="text-field__trail">
|
|
86
|
+
<slot name="suffix"></slot>
|
|
87
|
+
<slot name="icon-right"></slot>
|
|
88
|
+
</span>
|
|
89
|
+
`;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
__decorate([
|
|
93
|
+
property({ type: String })
|
|
94
|
+
], XmTextField.prototype, "type", void 0);
|
|
95
|
+
__decorate([
|
|
96
|
+
property({ type: String })
|
|
97
|
+
], XmTextField.prototype, "placeholder", void 0);
|
|
98
|
+
XmTextField = __decorate([
|
|
99
|
+
customElement("xm-text-field")
|
|
100
|
+
], XmTextField);
|
|
101
|
+
export { XmTextField };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
xm-textarea — multi-line concrete control for XmField (Story 2.2).
|
|
3
|
+
|
|
4
|
+
The chrome (surface, border, radius, focus ring, label, helper/error row,
|
|
5
|
+
disabled/readonly) is owned by the XmField base — see field/index.css. The
|
|
6
|
+
only delta from xm-text-field is the multi-line <textarea>: it is taller
|
|
7
|
+
than one control row, so the control wrapper must stop vertically centering
|
|
8
|
+
its child and let the textarea own its own block size.
|
|
9
|
+
|
|
10
|
+
The native <textarea> rendered here is already styled transparent /
|
|
11
|
+
borderless / full-bleed / inherits-ink by the base's `.field__control
|
|
12
|
+
textarea` rule. So THIS file only:
|
|
13
|
+
• un-centers the control wrapper for a multi-row box,
|
|
14
|
+
• sets the textarea's own vertical padding + resize behavior,
|
|
15
|
+
• caps the auto-grow height.
|
|
16
|
+
|
|
17
|
+
Surface/ink: rides the inverse-surface card tier — ink is inverse-on-surface,
|
|
18
|
+
placeholder is on-surface-variant (matching the host surface, AD-13). No
|
|
19
|
+
error color — severity is the base's icon + copy (AD-11). Tokens only (AD-1).
|
|
20
|
+
|
|
21
|
+
BEM block: `textarea`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
|
|
22
|
+
============================================ */
|
|
23
|
+
|
|
24
|
+
/* The control wrapper centers a single-line input; a textarea needs the box to
|
|
25
|
+
start at the top and stretch, so the field-level control wrapper is realigned
|
|
26
|
+
when it hosts this block. Scoped under the block so it never leaks to other
|
|
27
|
+
fields. */
|
|
28
|
+
.textarea {
|
|
29
|
+
display: block;
|
|
30
|
+
width: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* The native <textarea> — already transparent/borderless/inheriting via the
|
|
34
|
+
base rule. Here we own the multi-line geometry: top-aligned text, vertical
|
|
35
|
+
pad on the --s-N scale, and the resize affordance. */
|
|
36
|
+
.textarea__control {
|
|
37
|
+
display: block;
|
|
38
|
+
width: 100%;
|
|
39
|
+
min-width: 0;
|
|
40
|
+
/* Override the base `.field__control textarea { height: 100% }` rule — a
|
|
41
|
+
textarea owns its own block size from `rows` / scrollHeight, not the
|
|
42
|
+
single-line wrapper height. */
|
|
43
|
+
height: auto;
|
|
44
|
+
align-self: stretch;
|
|
45
|
+
padding: var(--s-2) var(--s-3);
|
|
46
|
+
resize: vertical;
|
|
47
|
+
line-height: var(--md-sys-typescale-body-large-line-height);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Auto-grow disables the manual resize handle — the box tracks its content up
|
|
51
|
+
to the capped max-height instead. */
|
|
52
|
+
.textarea__control--auto-grow {
|
|
53
|
+
resize: none;
|
|
54
|
+
overflow-y: auto;
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PropertyValues, TemplateResult } from "lit";
|
|
2
|
+
import { XmField } from "../field/index.js";
|
|
3
|
+
export declare class XmTextarea extends XmField {
|
|
4
|
+
/** Initial visible height in text rows. */
|
|
5
|
+
rows: number;
|
|
6
|
+
/** Native placeholder — shown when the live value is empty. */
|
|
7
|
+
placeholder: string;
|
|
8
|
+
/** Grow the textarea height with content up to `maxRows` (no scroll jitter). */
|
|
9
|
+
autoGrow: boolean;
|
|
10
|
+
/** Cap for auto-grow, in rows. Beyond it the textarea scrolls. */
|
|
11
|
+
maxRows: number;
|
|
12
|
+
private _textarea;
|
|
13
|
+
protected updated(changed: PropertyValues<this>): void;
|
|
14
|
+
private _onInput;
|
|
15
|
+
private _onChange;
|
|
16
|
+
/** Reset then snap to scrollHeight, clamped to maxRows. Reading scrollHeight
|
|
17
|
+
after a height reset is the only reliable shrink-and-grow measurement;
|
|
18
|
+
it's a single synchronous layout read per input, not an animation. */
|
|
19
|
+
private _resize;
|
|
20
|
+
protected renderControl(): TemplateResult;
|
|
21
|
+
}
|
|
22
|
+
declare global {
|
|
23
|
+
interface HTMLElementTagNameMap {
|
|
24
|
+
"xm-textarea": XmTextarea;
|
|
25
|
+
}
|
|
26
|
+
}
|