@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,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
|
+
}
|