@xmesh/system-design 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +472 -0
  2. package/assets/brand-lockup-dark.svg +9 -0
  3. package/assets/brand-lockup-light.svg +9 -0
  4. package/assets/brand-mark.svg +9 -0
  5. package/colors_and_type.css +11 -0
  6. package/dist/lit/components/alert/index.css +201 -0
  7. package/dist/lit/components/alert/index.d.ts +25 -0
  8. package/dist/lit/components/alert/index.js +191 -0
  9. package/dist/lit/components/app-bar/index.css +80 -0
  10. package/dist/lit/components/app-bar/index.d.ts +19 -0
  11. package/dist/lit/components/app-bar/index.js +120 -0
  12. package/dist/lit/components/artifact/index.css +166 -0
  13. package/dist/lit/components/artifact/index.d.ts +37 -0
  14. package/dist/lit/components/artifact/index.js +294 -0
  15. package/dist/lit/components/autocomplete/index.css +171 -0
  16. package/dist/lit/components/autocomplete/index.d.ts +47 -0
  17. package/dist/lit/components/autocomplete/index.js +404 -0
  18. package/dist/lit/components/avatar/index.css +62 -0
  19. package/dist/lit/components/avatar/index.d.ts +19 -0
  20. package/dist/lit/components/avatar/index.js +112 -0
  21. package/dist/lit/components/avatar-group/index.css +60 -0
  22. package/dist/lit/components/avatar-group/index.d.ts +19 -0
  23. package/dist/lit/components/avatar-group/index.js +97 -0
  24. package/dist/lit/components/badge/index.css +72 -0
  25. package/dist/lit/components/badge/index.d.ts +18 -0
  26. package/dist/lit/components/badge/index.js +115 -0
  27. package/dist/lit/components/brand-mark/index.css +109 -0
  28. package/dist/lit/components/brand-mark/index.d.ts +24 -0
  29. package/dist/lit/components/brand-mark/index.js +116 -0
  30. package/dist/lit/components/breadcrumbs/index.css +91 -0
  31. package/dist/lit/components/breadcrumbs/index.d.ts +19 -0
  32. package/dist/lit/components/breadcrumbs/index.js +104 -0
  33. package/dist/lit/components/bubble/index.css +182 -0
  34. package/dist/lit/components/bubble/index.d.ts +72 -0
  35. package/dist/lit/components/bubble/index.js +617 -0
  36. package/dist/lit/components/button/index.css +342 -0
  37. package/dist/lit/components/button/index.d.ts +32 -0
  38. package/dist/lit/components/button/index.js +202 -0
  39. package/dist/lit/components/card/index.css +99 -0
  40. package/dist/lit/components/card/index.d.ts +20 -0
  41. package/dist/lit/components/card/index.js +133 -0
  42. package/dist/lit/components/chat/index.css +292 -0
  43. package/dist/lit/components/chat/index.d.ts +74 -0
  44. package/dist/lit/components/chat/index.js +589 -0
  45. package/dist/lit/components/checkbox/index.css +126 -0
  46. package/dist/lit/components/checkbox/index.d.ts +21 -0
  47. package/dist/lit/components/checkbox/index.js +138 -0
  48. package/dist/lit/components/chip/index.css +145 -0
  49. package/dist/lit/components/chip/index.d.ts +30 -0
  50. package/dist/lit/components/chip/index.js +230 -0
  51. package/dist/lit/components/chip-group/index.css +19 -0
  52. package/dist/lit/components/chip-group/index.d.ts +24 -0
  53. package/dist/lit/components/chip-group/index.js +171 -0
  54. package/dist/lit/components/code/index.css +42 -0
  55. package/dist/lit/components/code/index.d.ts +12 -0
  56. package/dist/lit/components/code/index.js +68 -0
  57. package/dist/lit/components/composer/index.css +548 -0
  58. package/dist/lit/components/composer/index.d.ts +67 -0
  59. package/dist/lit/components/composer/index.js +713 -0
  60. package/dist/lit/components/data-table/index.css +166 -0
  61. package/dist/lit/components/data-table/index.d.ts +55 -0
  62. package/dist/lit/components/data-table/index.js +390 -0
  63. package/dist/lit/components/dialog/index.css +124 -0
  64. package/dist/lit/components/dialog/index.d.ts +24 -0
  65. package/dist/lit/components/dialog/index.js +199 -0
  66. package/dist/lit/components/divider/index.css +27 -0
  67. package/dist/lit/components/divider/index.d.ts +13 -0
  68. package/dist/lit/components/divider/index.js +67 -0
  69. package/dist/lit/components/empty-state/index.css +69 -0
  70. package/dist/lit/components/empty-state/index.d.ts +21 -0
  71. package/dist/lit/components/empty-state/index.js +123 -0
  72. package/dist/lit/components/expansion-panel/index.css +120 -0
  73. package/dist/lit/components/expansion-panel/index.d.ts +22 -0
  74. package/dist/lit/components/expansion-panel/index.js +174 -0
  75. package/dist/lit/components/field/index.css +223 -0
  76. package/dist/lit/components/field/index.d.ts +106 -0
  77. package/dist/lit/components/field/index.js +388 -0
  78. package/dist/lit/components/file-input/index.css +257 -0
  79. package/dist/lit/components/file-input/index.d.ts +30 -0
  80. package/dist/lit/components/file-input/index.js +298 -0
  81. package/dist/lit/components/form/index.css +29 -0
  82. package/dist/lit/components/form/index.d.ts +38 -0
  83. package/dist/lit/components/form/index.js +192 -0
  84. package/dist/lit/components/grid/index.css +53 -0
  85. package/dist/lit/components/grid/index.d.ts +14 -0
  86. package/dist/lit/components/grid/index.js +82 -0
  87. package/dist/lit/components/kbd/index.css +35 -0
  88. package/dist/lit/components/kbd/index.d.ts +11 -0
  89. package/dist/lit/components/kbd/index.js +43 -0
  90. package/dist/lit/components/list/index.css +15 -0
  91. package/dist/lit/components/list/index.d.ts +28 -0
  92. package/dist/lit/components/list/index.js +188 -0
  93. package/dist/lit/components/list-item/index.css +119 -0
  94. package/dist/lit/components/list-item/index.d.ts +20 -0
  95. package/dist/lit/components/list-item/index.js +127 -0
  96. package/dist/lit/components/menu/index.css +94 -0
  97. package/dist/lit/components/menu/index.d.ts +47 -0
  98. package/dist/lit/components/menu/index.js +386 -0
  99. package/dist/lit/components/navigation-drawer/index.css +114 -0
  100. package/dist/lit/components/navigation-drawer/index.d.ts +29 -0
  101. package/dist/lit/components/navigation-drawer/index.js +218 -0
  102. package/dist/lit/components/overlay/index.css +171 -0
  103. package/dist/lit/components/overlay/index.d.ts +65 -0
  104. package/dist/lit/components/overlay/index.js +566 -0
  105. package/dist/lit/components/pagination/index.css +102 -0
  106. package/dist/lit/components/pagination/index.d.ts +22 -0
  107. package/dist/lit/components/pagination/index.js +184 -0
  108. package/dist/lit/components/primitives/index.css +504 -0
  109. package/dist/lit/components/primitives/index.d.ts +25 -0
  110. package/dist/lit/components/primitives/index.js +283 -0
  111. package/dist/lit/components/progress/index.css +143 -0
  112. package/dist/lit/components/progress/index.d.ts +23 -0
  113. package/dist/lit/components/progress/index.js +180 -0
  114. package/dist/lit/components/radio-group/index.css +178 -0
  115. package/dist/lit/components/radio-group/index.d.ts +35 -0
  116. package/dist/lit/components/radio-group/index.js +292 -0
  117. package/dist/lit/components/select/index.css +151 -0
  118. package/dist/lit/components/select/index.d.ts +50 -0
  119. package/dist/lit/components/select/index.js +390 -0
  120. package/dist/lit/components/sidebar-item/index.css +133 -0
  121. package/dist/lit/components/sidebar-item/index.d.ts +20 -0
  122. package/dist/lit/components/sidebar-item/index.js +105 -0
  123. package/dist/lit/components/skeleton/index.css +81 -0
  124. package/dist/lit/components/skeleton/index.d.ts +19 -0
  125. package/dist/lit/components/skeleton/index.js +119 -0
  126. package/dist/lit/components/slider/index.css +171 -0
  127. package/dist/lit/components/slider/index.d.ts +36 -0
  128. package/dist/lit/components/slider/index.js +302 -0
  129. package/dist/lit/components/snackbar/index.css +279 -0
  130. package/dist/lit/components/snackbar/index.d.ts +33 -0
  131. package/dist/lit/components/snackbar/index.js +195 -0
  132. package/dist/lit/components/stack/index.css +41 -0
  133. package/dist/lit/components/stack/index.d.ts +20 -0
  134. package/dist/lit/components/stack/index.js +103 -0
  135. package/dist/lit/components/switch/index.css +126 -0
  136. package/dist/lit/components/switch/index.d.ts +17 -0
  137. package/dist/lit/components/switch/index.js +116 -0
  138. package/dist/lit/components/table/index.css +85 -0
  139. package/dist/lit/components/table/index.d.ts +25 -0
  140. package/dist/lit/components/table/index.js +139 -0
  141. package/dist/lit/components/tabs/index.css +116 -0
  142. package/dist/lit/components/tabs/index.d.ts +49 -0
  143. package/dist/lit/components/tabs/index.js +320 -0
  144. package/dist/lit/components/text-field/index.css +90 -0
  145. package/dist/lit/components/text-field/index.d.ts +17 -0
  146. package/dist/lit/components/text-field/index.js +101 -0
  147. package/dist/lit/components/textarea/index.css +55 -0
  148. package/dist/lit/components/textarea/index.d.ts +26 -0
  149. package/dist/lit/components/textarea/index.js +124 -0
  150. package/dist/lit/components/tooltip/index.css +37 -0
  151. package/dist/lit/components/tooltip/index.d.ts +31 -0
  152. package/dist/lit/components/tooltip/index.js +196 -0
  153. package/dist/lit/components/validation/index.css +386 -0
  154. package/dist/lit/components/validation/index.d.ts +45 -0
  155. package/dist/lit/components/validation/index.js +318 -0
  156. package/dist/lit/index.d.ts +50 -0
  157. package/dist/lit/index.js +59 -0
  158. package/package.json +81 -0
  159. package/styles/README.md +346 -0
  160. package/styles/_elevation.css +24 -0
  161. package/styles/_fonts.css +6 -0
  162. package/styles/_layout.css +37 -0
  163. package/styles/_primitives.css +154 -0
  164. package/styles/_scroll.css +75 -0
  165. package/styles/_semantic.css +146 -0
  166. package/styles/_space.css +61 -0
  167. package/styles/_type.css +139 -0
  168. package/styles/_xmesh-extensions.css +232 -0
  169. package/styles/index.css +44 -0
  170. package/styles/md3/_color.css +102 -0
  171. package/styles/md3/_elevation.css +26 -0
  172. package/styles/md3/_motion.css +35 -0
  173. package/styles/md3/_shape.css +22 -0
  174. package/styles/md3/_state.css +22 -0
  175. package/styles/md3/_type.css +111 -0
@@ -0,0 +1,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
+ }