@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,94 @@
1
+ /* ============================================
2
+ Menu — an anchored dropdown of menu items.
3
+
4
+ The popover surface chrome (inverse-surface fill, inverse-on-surface ink,
5
+ hairline border, level3 elevation, edge-flip, open/close motion) is owned by
6
+ the composed <xm-overlay> in the `menu` tier. This file styles the list, the
7
+ trigger wrapper, and each <xm-menu-item> (a separate block, rendered in its
8
+ own shadow root).
9
+
10
+ All ink is inverse-on-surface (AD-13) — the popover stack ink. Item
11
+ hover/active uses a state layer mixed from the ink, not opacity tricks on the
12
+ real content. Disabled items take the shared reduced emphasis — never an
13
+ error hue (AD-11). Destructive actions are icon + copy.
14
+
15
+ BEM blocks: `menu` (root) + `menu-item` (the item, its own shadow root). Both
16
+ registered in scripts/check-bem.sh STRICT_BLOCKS.
17
+ Elements: menu__trigger, menu__list; menu-item__icon, menu-item__label.
18
+ Modifiers: menu-item--active, menu-item--disabled.
19
+ ============================================ */
20
+
21
+ .menu__trigger {
22
+ display: inline-flex;
23
+ align-items: center;
24
+ }
25
+
26
+ .menu__list {
27
+ list-style: none;
28
+ margin: 0;
29
+ padding: var(--s-1);
30
+ display: flex;
31
+ flex-direction: column;
32
+ gap: var(--s-0-5);
33
+ min-width: 180px;
34
+ outline: none;
35
+ color: var(--md-sys-color-inverse-on-surface);
36
+ }
37
+
38
+ /* ---------- Menu item (separate block / shadow root) ---------- */
39
+ .menu-item {
40
+ display: flex;
41
+ align-items: center;
42
+ gap: var(--s-3);
43
+ padding: var(--s-2) var(--s-3);
44
+ border-radius: var(--md-sys-shape-corner-small);
45
+ color: var(--md-sys-color-inverse-on-surface);
46
+ font-family: var(--md-sys-typescale-body-medium-font);
47
+ font-size: var(--md-sys-typescale-body-medium-size);
48
+ line-height: var(--md-sys-typescale-body-medium-line-height);
49
+ cursor: pointer;
50
+ user-select: none;
51
+ transition: background var(--md-sys-motion-duration-short3)
52
+ var(--md-sys-motion-easing-standard);
53
+ }
54
+
55
+ /* Active (keyboard / hover) — a state layer mixed from the popover ink. */
56
+ .menu-item--active {
57
+ background: color-mix(
58
+ in oklch,
59
+ var(--md-sys-color-inverse-on-surface) 10%,
60
+ transparent
61
+ );
62
+ }
63
+
64
+ .menu-item--disabled {
65
+ color: color-mix(
66
+ in oklch,
67
+ var(--md-sys-color-inverse-on-surface) 40%,
68
+ transparent
69
+ );
70
+ cursor: default;
71
+ pointer-events: none;
72
+ }
73
+
74
+ .menu-item__icon {
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ flex-shrink: 0;
79
+ color: color-mix(
80
+ in oklch,
81
+ var(--md-sys-color-inverse-on-surface) 70%,
82
+ transparent
83
+ );
84
+ }
85
+ /* Collapse the icon column entirely when no icon is slotted so labels align. */
86
+ .menu-item__icon.is-empty {
87
+ display: none;
88
+ }
89
+
90
+ .menu-item__label {
91
+ flex: 1;
92
+ min-width: 0;
93
+ text-wrap: pretty;
94
+ }
@@ -0,0 +1,47 @@
1
+ import { LitElement } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ import type { OverlayPlacement } from "../overlay/index.js";
4
+ export declare class XmMenu extends LitElement {
5
+ open: boolean;
6
+ placement: OverlayPlacement;
7
+ label: string;
8
+ private _activeIndex;
9
+ private _overlay;
10
+ private _triggerWrap;
11
+ private _list;
12
+ private readonly _listId;
13
+ private get _items();
14
+ private get _enabledIndexes();
15
+ render(): TemplateResult;
16
+ private _onTriggerClick;
17
+ private _onTriggerKeydown;
18
+ private _openMenu;
19
+ private _close;
20
+ private _onOverlayClose;
21
+ private _onListKeydown;
22
+ private _nextEnabled;
23
+ private _onItemsSlot;
24
+ private _onItemActivate;
25
+ private _onItemHover;
26
+ private _activateIndex;
27
+ private _syncItems;
28
+ private get _triggerEl();
29
+ private _wireTriggerAria;
30
+ protected updated(): void;
31
+ }
32
+ export declare class XmMenuItem extends LitElement {
33
+ value: string | number;
34
+ disabled: boolean;
35
+ active: boolean;
36
+ private _hasIcon;
37
+ render(): TemplateResult;
38
+ private _onIconSlot;
39
+ private _onClick;
40
+ private _onMouseMove;
41
+ }
42
+ declare global {
43
+ interface HTMLElementTagNameMap {
44
+ "xm-menu": XmMenu;
45
+ "xm-menu-item": XmMenuItem;
46
+ }
47
+ }
@@ -0,0 +1,386 @@
1
+ /*
2
+ menu/index.ts — <xm-menu> + <xm-menu-item>, an anchored dropdown menu.
3
+
4
+ Composes the xm-overlay foundation (Story 1.4) for positioning, top-layer
5
+ stacking, outside-click + Esc dismiss, and focus-restore — it does NOT
6
+ hand-roll z-index, anchoring math, or focus handling. The overlay runs in the
7
+ `menu` tier (tooltip < menu < dialog). We drive it through its PUBLIC API only
8
+ (mode / tier / placement / .anchor / .opener / show / hide / xm-overlay-close)
9
+ and never reach into its shadow root (AD-12).
10
+
11
+ Behaviour (FR-144):
12
+ • clicking the trigger opens an anchored popover of <xm-menu-item> children
13
+ • clicking the trigger again, clicking outside, or selecting an item closes
14
+ • keyboard follows the WAI-ARIA `menu` pattern (APG): ↑/↓ move the active
15
+ item, Home/End jump to first/last, Enter/Space activate, Esc closes;
16
+ disabled items are skipped (AD-9a)
17
+
18
+ Events (AD-8 / AD-8a):
19
+ xm-menu-select detail.value = the activated item's primitive value
20
+ close bare native-style name, fired on every close path
21
+ both bubbles:true, composed:true.
22
+
23
+ Esc is owned by the overlay's innermost-only handler (stopPropagation), so a
24
+ menu nested in a dialog closes only the menu (AD-5a). Focus restores to the
25
+ trigger on close (delegated to the overlay via .opener).
26
+
27
+ Severity is never expressed by hue here (AD-11): a destructive item uses icon +
28
+ copy, never a status color.
29
+
30
+ Authoring:
31
+ <xm-menu>
32
+ <xm-button slot="trigger">Actions</xm-button>
33
+ <xm-menu-item value="rename">Rename</xm-menu-item>
34
+ <xm-menu-item value="duplicate">
35
+ <xm-copy-icon slot="icon-left" size="16"></xm-copy-icon>
36
+ Duplicate
37
+ </xm-menu-item>
38
+ <xm-menu-item value="archive" disabled>Archive</xm-menu-item>
39
+ </xm-menu>
40
+
41
+ Shadow DOM. Depends on xm-overlay + tokens (AD-12). Lit is a bare `import` (peer dep).
42
+ */
43
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
44
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
45
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
46
+ 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;
47
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
48
+ };
49
+ import { LitElement, html } from "lit";
50
+ import { customElement, property, query, state } from "lit/decorators.js";
51
+ const MENU_CSS = new URL("../menu/index.css", import.meta.url).href;
52
+ let menuSeq = 0;
53
+ let XmMenu = class XmMenu extends LitElement {
54
+ constructor() {
55
+ super(...arguments);
56
+ this.open = false;
57
+ this.placement = "bottom-start";
58
+ this.label = "";
59
+ this._activeIndex = -1;
60
+ this._listId = `xm-menu-${++menuSeq}`;
61
+ // ── Open / close ──────────────────────────────────────────────────────
62
+ this._onTriggerClick = () => {
63
+ if (this.open)
64
+ this._close("api");
65
+ else
66
+ this._openMenu();
67
+ };
68
+ this._onTriggerKeydown = (e) => {
69
+ if (this.open)
70
+ return;
71
+ if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
72
+ e.preventDefault();
73
+ this._openMenu(0);
74
+ }
75
+ else if (e.key === "ArrowUp") {
76
+ e.preventDefault();
77
+ this._openMenu(this._enabledIndexes.length - 1);
78
+ }
79
+ };
80
+ this._onOverlayClose = () => {
81
+ // Overlay self-dismissed (Esc / backdrop). Sync + emit our close once.
82
+ if (this.open) {
83
+ this.open = false;
84
+ this._activeIndex = -1;
85
+ this._syncItems();
86
+ this.dispatchEvent(new CustomEvent("close", {
87
+ bubbles: true,
88
+ composed: true,
89
+ detail: { reason: "overlay" },
90
+ }));
91
+ }
92
+ };
93
+ // ── Keyboard (APG menu) ───────────────────────────────────────────────
94
+ this._onListKeydown = (e) => {
95
+ const enabled = this._enabledIndexes;
96
+ switch (e.key) {
97
+ case "ArrowDown":
98
+ e.preventDefault();
99
+ this._activeIndex = this._nextEnabled(this._activeIndex, 1);
100
+ this._syncItems();
101
+ break;
102
+ case "ArrowUp":
103
+ e.preventDefault();
104
+ this._activeIndex = this._nextEnabled(this._activeIndex, -1);
105
+ this._syncItems();
106
+ break;
107
+ case "Home":
108
+ e.preventDefault();
109
+ this._activeIndex = enabled[0] ?? -1;
110
+ this._syncItems();
111
+ break;
112
+ case "End":
113
+ e.preventDefault();
114
+ this._activeIndex = enabled[enabled.length - 1] ?? -1;
115
+ this._syncItems();
116
+ break;
117
+ case "Enter":
118
+ case " ":
119
+ e.preventDefault();
120
+ this._activateIndex(this._activeIndex);
121
+ break;
122
+ case "Tab":
123
+ // Let focus leave; close without committing.
124
+ this._close("tab");
125
+ break;
126
+ // Esc handled by the overlay (innermost-only, stopPropagation).
127
+ }
128
+ };
129
+ // ── Selection ─────────────────────────────────────────────────────────
130
+ this._onItemsSlot = () => {
131
+ // Re-stamp ids + index so active-descendant + selection stay aligned.
132
+ this._syncItems();
133
+ };
134
+ this._onItemActivate = (e) => {
135
+ const item = e.target;
136
+ const index = this._items.indexOf(item);
137
+ if (index !== -1)
138
+ this._activateIndex(index);
139
+ };
140
+ this._onItemHover = (e) => {
141
+ const item = e.target;
142
+ const index = this._items.indexOf(item);
143
+ if (index !== -1 && !item.disabled) {
144
+ this._activeIndex = index;
145
+ this._syncItems();
146
+ }
147
+ };
148
+ this._wireTriggerAria = (e) => {
149
+ const slot = e.target;
150
+ const el = slot.assignedElements()[0];
151
+ if (el) {
152
+ el.setAttribute("aria-haspopup", "menu");
153
+ el.setAttribute("aria-expanded", this.open ? "true" : "false");
154
+ el.setAttribute("aria-controls", this._listId);
155
+ }
156
+ };
157
+ }
158
+ get _items() {
159
+ const slot = this.renderRoot.querySelector(".menu__list slot:not([name])");
160
+ if (!slot)
161
+ return [];
162
+ return slot
163
+ .assignedElements({ flatten: true })
164
+ .filter((el) => el instanceof XmMenuItem);
165
+ }
166
+ get _enabledIndexes() {
167
+ return this._items
168
+ .map((it, i) => (it.disabled ? -1 : i))
169
+ .filter((i) => i !== -1);
170
+ }
171
+ render() {
172
+ const activeId = this._activeIndex >= 0 ? `${this._listId}-item-${this._activeIndex}` : "";
173
+ return html `
174
+ <link rel="stylesheet" href="${MENU_CSS}" />
175
+ <style>
176
+ :host { display: inline-flex; }
177
+ </style>
178
+ <span
179
+ class="menu__trigger"
180
+ @click=${this._onTriggerClick}
181
+ @keydown=${this._onTriggerKeydown}
182
+ >
183
+ <slot name="trigger" @slotchange=${this._wireTriggerAria}></slot>
184
+ </span>
185
+
186
+ <xm-overlay
187
+ mode="non-modal"
188
+ tier="menu"
189
+ placement=${this.placement}
190
+ label=${this.label || "Menu"}
191
+ @xm-overlay-close=${this._onOverlayClose}
192
+ >
193
+ <ul
194
+ class="menu__list"
195
+ id=${this._listId}
196
+ role="menu"
197
+ aria-label=${this.label || "Menu"}
198
+ aria-activedescendant=${activeId}
199
+ tabindex="-1"
200
+ @keydown=${this._onListKeydown}
201
+ @xm-menu-item-activate=${this._onItemActivate}
202
+ @xm-menu-item-hover=${this._onItemHover}
203
+ >
204
+ <slot @slotchange=${this._onItemsSlot}></slot>
205
+ </ul>
206
+ </xm-overlay>
207
+ `;
208
+ }
209
+ _openMenu(landOn) {
210
+ if (this.open)
211
+ return;
212
+ this.open = true;
213
+ const enabled = this._enabledIndexes;
214
+ this._activeIndex =
215
+ landOn !== undefined ? enabled[landOn] ?? enabled[0] ?? -1 : -1;
216
+ this.updateComplete.then(() => {
217
+ // Guard the deferred open: a click-to-close (or Esc) before this microtask
218
+ // would otherwise re-open an orphaned popover.
219
+ if (!this.open)
220
+ return;
221
+ const ov = this._overlay;
222
+ const trigger = this._triggerEl;
223
+ if (ov && trigger) {
224
+ ov.anchor = trigger;
225
+ ov.opener = trigger;
226
+ ov.show();
227
+ }
228
+ this._syncItems();
229
+ // Move DOM focus into the list so arrow keys are captured and the APG
230
+ // roving model works. Wait a frame so the popover is shown + laid out
231
+ // (showPopover runs in the overlay's own update, after ours); focusing a
232
+ // not-yet-shown popover element would no-op.
233
+ requestAnimationFrame(() => {
234
+ if (this.open)
235
+ this._list?.focus();
236
+ });
237
+ });
238
+ }
239
+ _close(reason) {
240
+ if (!this.open)
241
+ return;
242
+ this.open = false;
243
+ this._activeIndex = -1;
244
+ const ov = this._overlay;
245
+ if (ov?.open)
246
+ ov.hide("api");
247
+ this._syncItems();
248
+ this.dispatchEvent(new CustomEvent("close", {
249
+ bubbles: true,
250
+ composed: true,
251
+ detail: { reason },
252
+ }));
253
+ }
254
+ _nextEnabled(from, dir) {
255
+ const n = this._items.length;
256
+ if (n === 0)
257
+ return -1;
258
+ let i = from;
259
+ for (let step = 0; step < n; step++) {
260
+ i = (i + dir + n) % n;
261
+ if (!this._items[i]?.disabled)
262
+ return i;
263
+ }
264
+ return from;
265
+ }
266
+ _activateIndex(index) {
267
+ const item = this._items[index];
268
+ if (!item || item.disabled)
269
+ return;
270
+ this.dispatchEvent(new CustomEvent("xm-menu-select", {
271
+ bubbles: true,
272
+ composed: true,
273
+ detail: { value: item.value },
274
+ }));
275
+ this._close("select");
276
+ }
277
+ // Push the menu's active index + a stable id down onto each item so the
278
+ // active item paints its state layer and the list's active-descendant points
279
+ // at a real element id.
280
+ _syncItems() {
281
+ this._items.forEach((it, i) => {
282
+ it.id = `${this._listId}-item-${i}`;
283
+ it.active = this.open && i === this._activeIndex;
284
+ });
285
+ }
286
+ get _triggerEl() {
287
+ const slot = this.renderRoot.querySelector('slot[name="trigger"]');
288
+ return slot?.assignedElements()[0] ?? this._triggerWrap;
289
+ }
290
+ updated() {
291
+ const trigger = this._triggerEl;
292
+ if (trigger)
293
+ trigger.setAttribute("aria-expanded", this.open ? "true" : "false");
294
+ }
295
+ };
296
+ __decorate([
297
+ property({ type: Boolean, reflect: true })
298
+ ], XmMenu.prototype, "open", void 0);
299
+ __decorate([
300
+ property({ type: String })
301
+ ], XmMenu.prototype, "placement", void 0);
302
+ __decorate([
303
+ property({ type: String })
304
+ ], XmMenu.prototype, "label", void 0);
305
+ __decorate([
306
+ state()
307
+ ], XmMenu.prototype, "_activeIndex", void 0);
308
+ __decorate([
309
+ query("xm-overlay")
310
+ ], XmMenu.prototype, "_overlay", void 0);
311
+ __decorate([
312
+ query(".menu__trigger")
313
+ ], XmMenu.prototype, "_triggerWrap", void 0);
314
+ __decorate([
315
+ query(".menu__list")
316
+ ], XmMenu.prototype, "_list", void 0);
317
+ XmMenu = __decorate([
318
+ customElement("xm-menu")
319
+ ], XmMenu);
320
+ export { XmMenu };
321
+ let XmMenuItem = class XmMenuItem extends LitElement {
322
+ constructor() {
323
+ super(...arguments);
324
+ this.value = "";
325
+ this.disabled = false;
326
+ this.active = false;
327
+ this._hasIcon = false;
328
+ this._onIconSlot = (e) => {
329
+ const slot = e.target;
330
+ this._hasIcon = slot.assignedElements().length > 0;
331
+ };
332
+ this._onClick = () => {
333
+ if (this.disabled)
334
+ return;
335
+ this.dispatchEvent(new CustomEvent("xm-menu-item-activate", { bubbles: true, composed: true }));
336
+ };
337
+ this._onMouseMove = () => {
338
+ if (this.disabled)
339
+ return;
340
+ this.dispatchEvent(new CustomEvent("xm-menu-item-hover", { bubbles: true, composed: true }));
341
+ };
342
+ }
343
+ render() {
344
+ const cls = [
345
+ "menu-item",
346
+ this.active ? "menu-item--active" : "",
347
+ this.disabled ? "menu-item--disabled" : "",
348
+ ]
349
+ .filter(Boolean)
350
+ .join(" ");
351
+ return html `
352
+ <link rel="stylesheet" href="${MENU_CSS}" />
353
+ <style>
354
+ :host { display: block; }
355
+ </style>
356
+ <div
357
+ class="${cls}"
358
+ role="menuitem"
359
+ aria-disabled=${this.disabled ? "true" : "false"}
360
+ @click=${this._onClick}
361
+ @mousemove=${this._onMouseMove}
362
+ >
363
+ <span class="menu-item__icon ${this._hasIcon ? "" : "is-empty"}"
364
+ ><slot name="icon-left" @slotchange=${this._onIconSlot}></slot
365
+ ></span>
366
+ <span class="menu-item__label"><slot></slot></span>
367
+ </div>
368
+ `;
369
+ }
370
+ };
371
+ __decorate([
372
+ property({ type: String })
373
+ ], XmMenuItem.prototype, "value", void 0);
374
+ __decorate([
375
+ property({ type: Boolean, reflect: true })
376
+ ], XmMenuItem.prototype, "disabled", void 0);
377
+ __decorate([
378
+ property({ type: Boolean, reflect: true })
379
+ ], XmMenuItem.prototype, "active", void 0);
380
+ __decorate([
381
+ state()
382
+ ], XmMenuItem.prototype, "_hasIcon", void 0);
383
+ XmMenuItem = __decorate([
384
+ customElement("xm-menu-item")
385
+ ], XmMenuItem);
386
+ export { XmMenuItem };
@@ -0,0 +1,114 @@
1
+ /* ============================================
2
+ <xm-navigation-drawer> — side navigation panel.
3
+
4
+ Surface / ink (AD-13): the INVERSE-SURFACE family (the card stack) —
5
+ panel background var(--md-sys-color-inverse-surface), ink
6
+ var(--md-sys-color-inverse-on-surface), directional drawer shadow
7
+ var(--xm-elevation-drawer). Hairline 1px borders only.
8
+
9
+ Hosts light-DOM xm-sidebar-item children. Those items reference the desk
10
+ surface roles (--md-sys-color-on-surface / -variant / -container-lowest); to
11
+ keep AD-12 (no reaching into their shadow root) while making them read on
12
+ the inverse-surface panel, the panel REMAPS those role custom properties to
13
+ their inverse equivalents at the panel scope. Custom properties inherit, so
14
+ the items pick up the right ink/hover automatically.
15
+
16
+ Modal mode is a native <dialog> in EDGE layout (showModal gives focus-trap +
17
+ inert for free; ::backdrop is the scrim). Persistent mode is a static panel;
18
+ collapsed mode is an icon rail. Slide-in is short4 emphasized-decelerate.
19
+
20
+ BEM block `nav-drawer`; elements `__dialog` `__panel`; modifiers `--open`
21
+ `--collapsed` `--modal` `--persistent`.
22
+ ============================================ */
23
+
24
+ .nav-drawer__panel {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--s-1);
28
+ box-sizing: border-box;
29
+ width: 280px;
30
+ height: 100%;
31
+ padding: var(--s-3);
32
+ background: var(--md-sys-color-inverse-surface);
33
+ color: var(--md-sys-color-inverse-on-surface);
34
+ border-inline-end: 1px solid var(--md-sys-color-outline-variant);
35
+ box-shadow: var(--xm-elevation-drawer);
36
+ overflow-y: auto;
37
+
38
+ /* Remap the desk surface roles to the card-stack equivalents so slotted
39
+ xm-sidebar-item children (which use these roles) read on inverse-surface.
40
+ This is token-level composition, not reaching into the item's shadow. */
41
+ --md-sys-color-on-surface: var(--md-sys-color-inverse-on-surface);
42
+ --md-sys-color-on-surface-variant: var(--xm-color-inverse-on-surface-muted);
43
+ --md-sys-color-surface-container-lowest: color-mix(
44
+ in oklab,
45
+ var(--md-sys-color-inverse-on-surface) 8%,
46
+ var(--md-sys-color-inverse-surface)
47
+ );
48
+ }
49
+
50
+ /* ---------- Modal: native <dialog> in edge layout ---------- */
51
+ .nav-drawer__dialog {
52
+ margin: 0;
53
+ inset-block: 0;
54
+ inset-inline-start: 0;
55
+ height: 100dvh;
56
+ max-height: 100dvh;
57
+ max-width: 92vw;
58
+ padding: 0;
59
+ border: 0;
60
+ background: transparent;
61
+ overflow: hidden;
62
+ }
63
+ .nav-drawer__dialog::backdrop {
64
+ background: var(--md-sys-color-scrim);
65
+ opacity: 0.5;
66
+ }
67
+ .nav-drawer__dialog .nav-drawer__panel {
68
+ border-radius: 0;
69
+ /* Slide-in: panel translates from the leading edge on open. */
70
+ transform: translateX(-100%);
71
+ transition: transform var(--md-sys-motion-duration-short4)
72
+ var(--md-sys-motion-easing-emphasized-decelerate);
73
+ }
74
+ .nav-drawer__dialog[open] .nav-drawer__panel {
75
+ transform: translateX(0);
76
+ }
77
+
78
+ /* ---------- Persistent panel ---------- */
79
+ .nav-drawer--persistent .nav-drawer__panel {
80
+ border-radius: var(--md-sys-shape-corner-medium);
81
+ height: auto;
82
+ /* Slide + collapse on open/close. Width animates to 0 so the closed drawer
83
+ yields its layout space (not just translated off-screen). */
84
+ transition:
85
+ width var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-emphasized-decelerate),
86
+ transform var(--md-sys-motion-duration-short4) var(--md-sys-motion-easing-emphasized-decelerate),
87
+ opacity var(--md-sys-motion-duration-short3) var(--md-sys-motion-easing-standard);
88
+ }
89
+
90
+ /* Closed persistent drawer: collapse to zero width, slide out, and hide so its
91
+ slotted (focusable) children leave the tab order — `aria-hidden` alone would
92
+ leave focusable descendants inside a hidden region (an a11y anti-pattern). */
93
+ .nav-drawer--persistent:not(.nav-drawer--open) .nav-drawer__panel {
94
+ width: 0;
95
+ padding-inline: 0;
96
+ transform: translateX(-100%);
97
+ opacity: 0;
98
+ overflow: hidden;
99
+ visibility: hidden;
100
+ border-inline-end-color: transparent;
101
+ }
102
+
103
+ @media (prefers-reduced-motion: reduce) {
104
+ .nav-drawer--persistent .nav-drawer__panel {
105
+ transition: none;
106
+ }
107
+ }
108
+
109
+ /* ---------- Collapsed icon rail ---------- */
110
+ .nav-drawer--collapsed .nav-drawer__panel {
111
+ width: 64px;
112
+ align-items: center;
113
+ padding-inline: var(--s-2);
114
+ }
@@ -0,0 +1,29 @@
1
+ import { LitElement } from "lit";
2
+ import type { PropertyValues, TemplateResult } from "lit";
3
+ declare class XmNavigationDrawer extends LitElement {
4
+ static shadowRootOptions: ShadowRootInit;
5
+ open: boolean;
6
+ collapsed: boolean;
7
+ modal: boolean;
8
+ label: string;
9
+ private _dialog;
10
+ private _restoreFocusTo;
11
+ private _wasOpen;
12
+ render(): TemplateResult;
13
+ protected updated(changed: PropertyValues<this>): void;
14
+ disconnectedCallback(): void;
15
+ private _activateModal;
16
+ private _deactivateModal;
17
+ private _onDialogCancel;
18
+ private _onDialogClose;
19
+ private _onDialogClick;
20
+ private _emitClose;
21
+ private _restoreFocus;
22
+ private _deepActiveElement;
23
+ }
24
+ declare global {
25
+ interface HTMLElementTagNameMap {
26
+ "xm-navigation-drawer": XmNavigationDrawer;
27
+ }
28
+ }
29
+ export {};