@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,120 @@
1
+ /* ============================================
2
+ Expansion panel — collapsible disclosure with a rotating chevron.
3
+
4
+ <xm-expansion-panel> is a framed disclosure: a header button +
5
+ a collapsible body. The chevron rotates short4 / standard easing
6
+ on toggle (no spring, bounce, or scale-in; NFR-19).
7
+
8
+ Sits on one surface family (AD-13): surface-container-low frame,
9
+ ink --md-sys-color-on-surface, hairline 1px outline-variant
10
+ separators. No severity hue (AD-11). The chevron inherits ink via
11
+ currentColor.
12
+ ============================================ */
13
+
14
+ .expansion-panel {
15
+ display: block;
16
+ border: 1px solid var(--md-sys-color-outline-variant);
17
+ border-radius: var(--md-sys-shape-corner-medium);
18
+ background: var(--md-sys-color-surface-container-low);
19
+ color: var(--md-sys-color-on-surface);
20
+ overflow: hidden;
21
+ }
22
+
23
+ .expansion-panel__header {
24
+ appearance: none;
25
+ width: 100%;
26
+ display: flex;
27
+ align-items: center;
28
+ gap: var(--s-3);
29
+ box-sizing: border-box;
30
+ padding: var(--s-3) var(--s-4);
31
+ border: none;
32
+ background: transparent;
33
+ color: var(--md-sys-color-on-surface);
34
+ text-align: start;
35
+ cursor: pointer;
36
+ font-family: var(--md-sys-typescale-title-small-font);
37
+ font-size: var(--md-sys-typescale-title-small-size);
38
+ font-weight: 500;
39
+ line-height: 1.3;
40
+ transition: background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
41
+ }
42
+ .expansion-panel__header:hover {
43
+ background: color-mix(in oklab, var(--md-sys-color-on-surface) var(--md-sys-state-hover-state-layer-opacity), transparent);
44
+ }
45
+ .expansion-panel__header:focus { outline: none; }
46
+ /* Inset focus ring so the accent halo stays inside the frame's rounded clip
47
+ rather than being cut off by overflow: hidden. Same 3px weight + coral
48
+ primary-container as --xm-state-focus-ring, just drawn inset. */
49
+ .expansion-panel__header:focus-visible {
50
+ outline: none;
51
+ box-shadow: inset 0 0 0 3px var(--md-sys-color-primary-container);
52
+ }
53
+
54
+ .expansion-panel__heading {
55
+ flex: 1;
56
+ min-width: 0;
57
+ }
58
+
59
+ .expansion-panel__chevron {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+ flex-shrink: 0;
64
+ color: var(--md-sys-color-on-surface-variant);
65
+ transition: transform var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
66
+ }
67
+ .expansion-panel--open .expansion-panel__chevron {
68
+ transform: rotate(180deg);
69
+ }
70
+
71
+ /* Body — hidden when collapsed. A hairline separates it from the header
72
+ only while open. */
73
+ /* Animated reveal: the body is a 1-row grid that transitions 1fr→0fr; the inner
74
+ wrapper has min-height:0 + overflow:hidden so the auto-height content can be
75
+ clipped to zero. This animates a content-sized body (which a plain
76
+ height/max-height can't) with short4 standard easing — no spring/scale. */
77
+ .expansion-panel__body {
78
+ display: grid;
79
+ grid-template-rows: 1fr;
80
+ border-top: 1px solid var(--md-sys-color-outline-variant);
81
+ color: var(--md-sys-color-on-surface-variant);
82
+ font-family: var(--md-sys-typescale-body-large-font);
83
+ font-size: var(--md-sys-typescale-body-large-size);
84
+ line-height: var(--md-sys-typescale-body-large-line-height);
85
+ transition:
86
+ grid-template-rows var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard),
87
+ border-color var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-standard);
88
+ }
89
+ .expansion-panel__body-inner {
90
+ min-height: 0;
91
+ overflow: hidden;
92
+ padding: var(--s-3) var(--s-4) var(--s-4);
93
+ }
94
+ .expansion-panel__body.is-collapsed {
95
+ grid-template-rows: 0fr;
96
+ border-top-color: transparent;
97
+ }
98
+ .expansion-panel__body.is-collapsed .expansion-panel__body-inner {
99
+ /* Drop the vertical padding while collapsed so it doesn't hold the row open. */
100
+ padding-block: 0;
101
+ }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .expansion-panel__body {
105
+ transition: none;
106
+ }
107
+ }
108
+
109
+ /* Disabled — shared reduced emphasis, non-interactive. The chevron carries its
110
+ own color, so mute it too or it stays at full emphasis on a disabled panel. */
111
+ .expansion-panel--disabled .expansion-panel__header {
112
+ cursor: not-allowed;
113
+ color: var(--xm-color-on-surface-disabled);
114
+ }
115
+ .expansion-panel--disabled .expansion-panel__chevron {
116
+ color: var(--xm-color-on-surface-disabled);
117
+ }
118
+ .expansion-panel--disabled .expansion-panel__header:hover {
119
+ background: transparent;
120
+ }
@@ -0,0 +1,22 @@
1
+ import { LitElement } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ declare class XmExpansionPanel extends LitElement {
4
+ open: boolean;
5
+ heading: string;
6
+ disabled: boolean;
7
+ value: string;
8
+ name: string;
9
+ private _uid;
10
+ connectedCallback(): void;
11
+ disconnectedCallback(): void;
12
+ private _onSiblingOpen;
13
+ private _toggle;
14
+ private _onHeaderClick;
15
+ render(): TemplateResult;
16
+ }
17
+ declare global {
18
+ interface HTMLElementTagNameMap {
19
+ "xm-expansion-panel": XmExpansionPanel;
20
+ }
21
+ }
22
+ export {};
@@ -0,0 +1,174 @@
1
+ /*
2
+ expansion-panel/index.ts — <xm-expansion-panel>.
3
+
4
+ Collapsible disclosure: a header button + a body that shows/hides on
5
+ toggle, with a chevron that rotates short4 / standard easing.
6
+
7
+ Authoring:
8
+ <xm-expansion-panel heading="Routing" open>
9
+ The planner reads routing.yaml and falls back to JSON.
10
+ </xm-expansion-panel>
11
+
12
+ <!-- Slotted header for richer content -->
13
+ <xm-expansion-panel>
14
+ <span slot="header">Advanced <xm-badge count="2">…</xm-badge></span>
15
+ …body…
16
+ </xm-expansion-panel>
17
+
18
+ Properties:
19
+ open boolean — INITIAL open state (uncontrolled-first, AD-6);
20
+ the panel owns it thereafter
21
+ heading string — header text (or use slot="header")
22
+ disabled boolean — non-interactive, reduced emphasis
23
+ value string|number — identity for host bookkeeping
24
+ name string — single-open group key: panels sharing a `name`
25
+ auto-collapse siblings on open (the documented
26
+ grouping). Omit the name (or give unique names)
27
+ to allow several open at once (multiple mode).
28
+
29
+ Events:
30
+ change bubbles+composed. detail.open: boolean. (Bare native-mirroring
31
+ name per AD-8; the panel owns its state per AD-6.)
32
+
33
+ Header is a real <button> with aria-expanded + aria-controls; the body is
34
+ aria-labelledby the header. Shadow DOM; 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 { LitElement, html } from "lit";
43
+ import { customElement, property } from "lit/decorators.js";
44
+ const PANEL_CSS = new URL("../expansion-panel/index.css", import.meta.url).href;
45
+ const COORDINATION_EVENT = "xm-expansion-panel-open";
46
+ let panelInstance = 0;
47
+ let XmExpansionPanel = class XmExpansionPanel extends LitElement {
48
+ constructor() {
49
+ super(...arguments);
50
+ this.open = false;
51
+ this.heading = "";
52
+ this.disabled = false;
53
+ this.value = "";
54
+ this.name = "";
55
+ this._uid = `expansion-panel-${++panelInstance}`;
56
+ this._onSiblingOpen = (e) => {
57
+ if (!this.name)
58
+ return;
59
+ if (e.detail.source === this)
60
+ return;
61
+ if (e.detail.name === this.name && this.open) {
62
+ this.open = false;
63
+ }
64
+ };
65
+ this._onHeaderClick = () => {
66
+ this._toggle();
67
+ };
68
+ }
69
+ connectedCallback() {
70
+ super.connectedCallback();
71
+ // Single-open coordination: listen on the document for sibling opens of
72
+ // the same `name` and collapse when another panel in the group opens.
73
+ document.addEventListener(COORDINATION_EVENT, this._onSiblingOpen);
74
+ }
75
+ disconnectedCallback() {
76
+ super.disconnectedCallback();
77
+ document.removeEventListener(COORDINATION_EVENT, this._onSiblingOpen);
78
+ }
79
+ _toggle() {
80
+ if (this.disabled)
81
+ return;
82
+ this.open = !this.open;
83
+ if (this.open && this.name) {
84
+ document.dispatchEvent(new CustomEvent(COORDINATION_EVENT, {
85
+ detail: { name: this.name, source: this },
86
+ }));
87
+ }
88
+ this.dispatchEvent(new CustomEvent("change", {
89
+ detail: { open: this.open },
90
+ bubbles: true,
91
+ composed: true,
92
+ }));
93
+ }
94
+ render() {
95
+ const cls = [
96
+ "expansion-panel",
97
+ this.open && "expansion-panel--open",
98
+ this.disabled && "expansion-panel--disabled",
99
+ ]
100
+ .filter(Boolean)
101
+ .join(" ");
102
+ const headerId = `${this._uid}-header`;
103
+ const bodyId = `${this._uid}-body`;
104
+ return html `
105
+ <link rel="stylesheet" href="${PANEL_CSS}" />
106
+ <style>
107
+ :host {
108
+ display: block;
109
+ }
110
+ :host([hidden]) {
111
+ display: none;
112
+ }
113
+ </style>
114
+ <div class="${cls}">
115
+ <button
116
+ id="${headerId}"
117
+ class="expansion-panel__header"
118
+ type="button"
119
+ aria-expanded=${this.open ? "true" : "false"}
120
+ aria-controls="${bodyId}"
121
+ ?disabled=${this.disabled}
122
+ @click=${this._onHeaderClick}
123
+ >
124
+ <span class="expansion-panel__heading">
125
+ ${this.heading ? this.heading : html `<slot name="header"></slot>`}
126
+ </span>
127
+ <span class="expansion-panel__chevron" aria-hidden="true">
128
+ <svg
129
+ viewBox="0 0 24 24"
130
+ width="18"
131
+ height="18"
132
+ fill="none"
133
+ stroke="currentColor"
134
+ stroke-width="2"
135
+ stroke-linecap="round"
136
+ stroke-linejoin="round"
137
+ >
138
+ <polyline points="6 9 12 15 18 9" />
139
+ </svg>
140
+ </span>
141
+ </button>
142
+ <div
143
+ id="${bodyId}"
144
+ role="region"
145
+ aria-labelledby="${headerId}"
146
+ class="expansion-panel__body ${this.open ? "" : "is-collapsed"}"
147
+ ?inert=${!this.open}
148
+ >
149
+ <div class="expansion-panel__body-inner">
150
+ <slot></slot>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ `;
155
+ }
156
+ };
157
+ __decorate([
158
+ property({ type: Boolean, reflect: true })
159
+ ], XmExpansionPanel.prototype, "open", void 0);
160
+ __decorate([
161
+ property({ type: String })
162
+ ], XmExpansionPanel.prototype, "heading", void 0);
163
+ __decorate([
164
+ property({ type: Boolean, reflect: true })
165
+ ], XmExpansionPanel.prototype, "disabled", void 0);
166
+ __decorate([
167
+ property({ type: String })
168
+ ], XmExpansionPanel.prototype, "value", void 0);
169
+ __decorate([
170
+ property({ type: String })
171
+ ], XmExpansionPanel.prototype, "name", void 0);
172
+ XmExpansionPanel = __decorate([
173
+ customElement("xm-expansion-panel")
174
+ ], XmExpansionPanel);
@@ -0,0 +1,223 @@
1
+ /* ============================================
2
+ XmField — abstract form-field chrome.
3
+
4
+ The base renders label / control wrapper / helper-or-error row; the
5
+ subclass drops only its concrete control into the control wrapper
6
+ (AD-7). One shared control height per `size` so a field lines up beside
7
+ a button of the same size (NFR-22). Surface is the card tier
8
+ (inverse-surface) with inverse-on-surface ink (AD-13). Severity is the
9
+ error icon + copy in the message row — never an error color, never a red
10
+ field (AD-11 / rule 3a).
11
+
12
+ Sizes mirror the button height ramp so fields snap to buttons:
13
+ .field--xs 22px control
14
+ .field--sm 26px control
15
+ .field--md 32px control (default)
16
+ .field--lg 42px control
17
+
18
+ BEM block: `field`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
19
+ ============================================ */
20
+
21
+ .field {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--s-2);
25
+ width: 100%;
26
+ }
27
+
28
+ /* ---------- Label row ---------- */
29
+ .field__label {
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: var(--s-1);
33
+ color: var(--md-sys-color-inverse-on-surface);
34
+ font:
35
+ var(--md-sys-typescale-label-large-weight)
36
+ var(--md-sys-typescale-label-large-size) /
37
+ var(--md-sys-typescale-label-large-line-height)
38
+ var(--md-sys-typescale-label-large-font);
39
+ cursor: default;
40
+ }
41
+ .field__label-text {
42
+ letter-spacing: 0;
43
+ }
44
+ .field__required {
45
+ color: var(--md-sys-color-primary);
46
+ font-weight: 700;
47
+ line-height: 1;
48
+ }
49
+
50
+ /* ---------- Control wrapper ----------
51
+ The wrapper carries the shared chrome: surface, border, radius, height,
52
+ focus ring. The concrete control (slotted or shadow-rendered by the
53
+ subclass) sits inside and inherits ink + type. */
54
+ .field__control {
55
+ position: relative;
56
+ display: flex;
57
+ align-items: center;
58
+ width: 100%;
59
+ box-sizing: border-box;
60
+ border: 1px solid var(--md-sys-color-outline-variant);
61
+ border-radius: var(--md-sys-shape-corner-button);
62
+ background: var(--md-sys-color-inverse-surface);
63
+ color: var(--md-sys-color-inverse-on-surface);
64
+ font:
65
+ var(--md-sys-typescale-body-large-weight)
66
+ var(--md-sys-typescale-body-large-size) /
67
+ var(--md-sys-typescale-body-large-line-height)
68
+ var(--md-sys-typescale-body-large-font);
69
+ transition:
70
+ border-color var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
71
+ box-shadow var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard),
72
+ background var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
73
+ }
74
+ .field__control:hover {
75
+ border-color: var(--md-sys-color-outline);
76
+ }
77
+
78
+ /* Focus — ring is the canonical visualization; outline suppressed. The wrapper
79
+ reacts to KEYBOARD focus of the inner control via :has(:focus-visible) so the
80
+ coral ring shows on tab-in but not on a plain pointer click (matches the
81
+ :focus-visible intent in the spec while still reacting to the slotted/native
82
+ control nested inside the wrapper). */
83
+ .field__control:has(:focus-visible) {
84
+ outline: none;
85
+ border-color: var(--md-sys-color-primary);
86
+ box-shadow: var(--xm-state-focus-ring);
87
+ }
88
+
89
+ /* Style whatever native control a subclass slots OR shadow-renders so the
90
+ chrome owns the box and the control is a transparent, borderless,
91
+ full-bleed inner. Two selector sets are required: ::slotted() matches a
92
+ light-DOM `slot="control"` child; the scoped descendant set matches a
93
+ control a subclass renders into shadow DOM via renderControl(). Without
94
+ the descendant set a UA-default input fill shows through (a dark box on
95
+ the light card surface) — the surface/ink foot-gun. */
96
+ .field__control ::slotted(input),
97
+ .field__control ::slotted(textarea),
98
+ .field__control ::slotted(select),
99
+ .field__control input,
100
+ .field__control textarea,
101
+ .field__control select {
102
+ flex: 1;
103
+ min-width: 0;
104
+ width: 100%;
105
+ appearance: none;
106
+ border: none;
107
+ outline: none;
108
+ background: transparent;
109
+ color: inherit;
110
+ font: inherit;
111
+ padding: 0 var(--s-3);
112
+ height: 100%;
113
+ box-sizing: border-box;
114
+ }
115
+ .field__control ::slotted(input)::placeholder,
116
+ .field__control ::slotted(textarea)::placeholder,
117
+ .field__control input::placeholder,
118
+ .field__control textarea::placeholder {
119
+ color: var(--xm-color-inverse-on-surface-muted);
120
+ }
121
+
122
+ /* ---------- Sizes — shared control height ---------- */
123
+ .field--xs .field__control { min-height: 22px; }
124
+ .field--sm .field__control { min-height: 26px; }
125
+ .field--md .field__control { min-height: 32px; }
126
+ .field--lg .field__control { min-height: 42px; }
127
+
128
+ .field--xs .field__control ::slotted(input),
129
+ .field--xs .field__control ::slotted(select) { padding: 0 var(--s-2); }
130
+
131
+ /* ---------- Loading ----------
132
+ The control region is swapped for the spinner; the wrapper keeps its box
133
+ so the field height does not jump (AD-9a). */
134
+ .field__loading {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ justify-content: flex-start;
138
+ width: 100%;
139
+ height: 100%;
140
+ padding: 0 var(--s-3);
141
+ color: var(--xm-color-inverse-on-surface-muted);
142
+ }
143
+ .field--loading .field__control {
144
+ cursor: progress;
145
+ }
146
+
147
+ /* ---------- Disabled ----------
148
+ Shared reduced emphasis, cursor, reverted hover; never an error color. */
149
+ .field--disabled {
150
+ cursor: not-allowed;
151
+ }
152
+ /* No opacity on the control: it would dim the VALUE text (which inherits
153
+ .field__control's color) below AA. Swap the ink to the disabled token so
154
+ the value stays legible; the box is muted by its border + container. */
155
+ .field--disabled .field__control {
156
+ cursor: not-allowed;
157
+ color: var(--xm-color-inverse-on-surface-disabled);
158
+ box-shadow: none;
159
+ }
160
+ .field--disabled .field__control:hover {
161
+ border-color: var(--md-sys-color-outline-variant);
162
+ }
163
+ .field--disabled .field__control ::slotted(input)::placeholder,
164
+ .field--disabled .field__control ::slotted(textarea)::placeholder,
165
+ .field--disabled .field__control input::placeholder,
166
+ .field--disabled .field__control textarea::placeholder {
167
+ color: var(--xm-color-inverse-on-surface-disabled);
168
+ }
169
+ .field--disabled .field__label {
170
+ color: var(--xm-color-inverse-on-surface-disabled);
171
+ }
172
+
173
+ /* ---------- Readonly ----------
174
+ Value visible but not editable; still focusable. Stays on the card
175
+ (inverse-surface) family — a faint inverse-on-surface wash over the same
176
+ surface reads as inert without swapping in the desk surface family. */
177
+ .field--readonly .field__control {
178
+ background: color-mix(
179
+ in oklab,
180
+ var(--md-sys-color-inverse-on-surface) 5%,
181
+ var(--md-sys-color-inverse-surface)
182
+ );
183
+ border-color: var(--md-sys-color-outline-variant);
184
+ }
185
+ .field--readonly .field__control:hover {
186
+ border-color: var(--md-sys-color-outline-variant);
187
+ }
188
+
189
+ /* ---------- Helper / error message row ----------
190
+ Severity is icon + copy. The error keeps the same ink as the helper —
191
+ the icon and string carry the severity, not a color (AD-11 / rule 3a). */
192
+ .field__message {
193
+ display: flex;
194
+ align-items: flex-start;
195
+ gap: var(--s-1);
196
+ min-height: 1em;
197
+ font:
198
+ var(--md-sys-typescale-body-small-weight)
199
+ var(--md-sys-typescale-body-small-size) /
200
+ var(--md-sys-typescale-body-small-line-height)
201
+ var(--md-sys-typescale-body-small-font);
202
+ }
203
+ .field__message--helper {
204
+ color: var(--xm-color-inverse-on-surface-muted);
205
+ }
206
+ .field__message--error {
207
+ color: var(--md-sys-color-inverse-on-surface);
208
+ }
209
+ .field__message--empty {
210
+ min-height: 0;
211
+ }
212
+ .field__error-icon {
213
+ display: inline-flex;
214
+ align-items: center;
215
+ flex-shrink: 0;
216
+ margin-top: 1px;
217
+ color: var(--md-sys-color-inverse-on-surface);
218
+ }
219
+ .field__message-text {
220
+ flex: 1;
221
+ min-width: 0;
222
+ text-wrap: pretty;
223
+ }
@@ -0,0 +1,106 @@
1
+ import { LitElement } from "lit";
2
+ import type { PropertyValues, TemplateResult } from "lit";
3
+ export type FieldSize = "xs" | "sm" | "md" | "lg";
4
+ export interface FieldControlAria {
5
+ id: string;
6
+ /** undefined when there is no helper/error text — so a control's
7
+ aria-describedby never points at an empty message node. */
8
+ describedBy: string | undefined;
9
+ invalid: "true" | undefined;
10
+ required: "true" | undefined;
11
+ }
12
+ /**
13
+ * Abstract chrome + lifecycle base for every xmesh form field.
14
+ *
15
+ * Subclasses register with `@customElement("xm-<name>")`; XmField itself is
16
+ * never registered (it is not a usable element on its own).
17
+ */
18
+ export declare abstract class XmField extends LitElement {
19
+ static formAssociated: boolean;
20
+ static shadowRootOptions: ShadowRootInit;
21
+ label: string;
22
+ helper: string;
23
+ /** Severity copy. Non-empty ⇒ the field is in error (icon + copy, never color). */
24
+ error: string;
25
+ size: FieldSize;
26
+ required: boolean;
27
+ disabled: boolean;
28
+ readonly: boolean;
29
+ loading: boolean;
30
+ /** Form-control name — mirrors native <input name>. */
31
+ name: string;
32
+ /** INITIAL value (uncontrolled-first, AD-6): the `value` attribute seeds the
33
+ live state once, then never overrides user input. Mapped from attribute
34
+ `value` so authors write `<xm-text-field value="…">`. */
35
+ initialValue: string;
36
+ /** INITIAL checked for toggle subclasses; mapped from attribute `checked`. */
37
+ initialChecked: boolean;
38
+ /** Live value state — seeded from `initialValue`, then owned by the field. */
39
+ protected _value: string;
40
+ /** Live checked state (toggle subclasses) — seeded from `initialChecked`. */
41
+ protected _checked: boolean;
42
+ /** True once a toggle subclass has declared itself, so the form value is
43
+ submitted as checked-state rather than text. Set eagerly by overriding
44
+ `isToggle`, or lazily on the first `emitToggle`. */
45
+ protected _toggle: boolean;
46
+ /** OR-propagated disabled flag a future xm-form sets — never re-enables a
47
+ self-disabled field (AD-6a / AD-9a). */
48
+ private _formDisabled;
49
+ protected readonly internals: ElementInternals;
50
+ private readonly _seq;
51
+ private readonly _controlId;
52
+ private readonly _describedById;
53
+ private _valueSeeded;
54
+ protected _dirty: boolean;
55
+ constructor();
56
+ connectedCallback(): void;
57
+ protected willUpdate(changed: PropertyValues<this>): void;
58
+ /** Whether this field submits checked-state (toggle) rather than text. Toggle
59
+ subclasses (checkbox / radio / switch) override this to return `true` so
60
+ their form value is correct from first paint — before any interaction —
61
+ and is never inferred from `initialChecked` (a declaratively-checked text
62
+ field must NOT be treated as a toggle, and an untouched unchecked toggle
63
+ must submit `null`, not `""`). */
64
+ protected get isToggle(): boolean;
65
+ /** Effective disabled = own disabled OR a future xm-form down-propagation.
66
+ OR semantics: neither source can re-enable what the other disabled. */
67
+ protected get effectiveDisabled(): boolean;
68
+ /** Non-interactive whenever disabled (either source), readonly, or loading. */
69
+ protected get nonInteractive(): boolean;
70
+ protected get isError(): boolean;
71
+ /** True when the message row has helper or error copy to describe. */
72
+ protected get hasMessage(): boolean;
73
+ /** Public read contract for xm-form (AD-6a/AD-12): live value, no shadow reach. */
74
+ get value(): string;
75
+ /** Programmatic reset support — setting `value` after first paint updates live state. */
76
+ set value(next: string);
77
+ /** Public read contract for toggle subclasses (AD-6a). */
78
+ get checked(): boolean;
79
+ set checked(next: boolean);
80
+ /** Down-propagation hook: xm-form (Story 2.10) sets this; it OR's with the
81
+ field's own disabled and can never re-enable a self-disabled field. */
82
+ setFormDisabled(disabled: boolean): void;
83
+ /** Per-keystroke / per-drag live update. Updates live state, syncs the form
84
+ value, and emits a composed, bubbling `input` with a typed primitive detail. */
85
+ protected emitInput(value: string): void;
86
+ /** Commit. Same payload shape as input; fire on blur / Enter / native change. */
87
+ protected emitChange(value: string): void;
88
+ /** Toggle commit — marks the field a toggle and emits `detail.checked` (AD-8a). */
89
+ protected emitToggle(checked: boolean): void;
90
+ private _syncFormValue;
91
+ /**
92
+ * The ARIA hooks the chrome wires. Subclasses set `id`, `aria-describedby`,
93
+ * `aria-invalid`, and `aria-required` on their control element from these so
94
+ * the rendered label associates with the control and the error string is
95
+ * announced (NFR-13 / UX-DR7).
96
+ */
97
+ protected get controlAria(): FieldControlAria;
98
+ /**
99
+ * Subclasses override to render their concrete control directly into shadow
100
+ * DOM. The default projects whatever the author slots as `slot="control"`,
101
+ * so a subclass can also author the control as a light-DOM child. Either
102
+ * path keeps the chrome on the base (AD-7).
103
+ */
104
+ protected renderControl(): TemplateResult;
105
+ render(): TemplateResult;
106
+ }