@spfx-fast/sharepoint-ui 0.0.0-stage → 0.4.0

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 (158) hide show
  1. package/AGENTS.md +211 -0
  2. package/LICENSE +21 -0
  3. package/README.md +897 -2
  4. package/components.json +5810 -0
  5. package/dist/appearance.d.ts +6 -0
  6. package/dist/appearance.js +217 -0
  7. package/dist/elements/accordion.d.ts +9 -0
  8. package/dist/elements/accordion.js +338 -0
  9. package/dist/elements/alert.d.ts +2 -0
  10. package/dist/elements/alert.js +62 -0
  11. package/dist/elements/avatar-group-primitives.d.ts +3 -0
  12. package/dist/elements/avatar-group-primitives.js +189 -0
  13. package/dist/elements/avatar-group.d.ts +2 -0
  14. package/dist/elements/avatar-group.js +110 -0
  15. package/dist/elements/avatar.d.ts +4 -0
  16. package/dist/elements/avatar.js +251 -0
  17. package/dist/elements/badge.d.ts +8 -0
  18. package/dist/elements/badge.js +182 -0
  19. package/dist/elements/breadcrumb.d.ts +4 -0
  20. package/dist/elements/breadcrumb.js +233 -0
  21. package/dist/elements/button-group.d.ts +1 -0
  22. package/dist/elements/button-group.js +20 -0
  23. package/dist/elements/button-variants.d.ts +6 -0
  24. package/dist/elements/button-variants.js +339 -0
  25. package/dist/elements/button.d.ts +5 -0
  26. package/dist/elements/button.js +233 -0
  27. package/dist/elements/card-primitives.d.ts +3 -0
  28. package/dist/elements/card-primitives.js +83 -0
  29. package/dist/elements/card.d.ts +5 -0
  30. package/dist/elements/card.js +213 -0
  31. package/dist/elements/carousel.d.ts +92 -0
  32. package/dist/elements/carousel.js +1112 -0
  33. package/dist/elements/color-picker.d.ts +24 -0
  34. package/dist/elements/color-picker.js +1004 -0
  35. package/dist/elements/combobox.d.ts +1 -0
  36. package/dist/elements/combobox.js +1012 -0
  37. package/dist/elements/data-grid-model.d.ts +117 -0
  38. package/dist/elements/data-grid-model.js +294 -0
  39. package/dist/elements/data-grid.d.ts +29 -0
  40. package/dist/elements/data-grid.js +1567 -0
  41. package/dist/elements/date-picker.d.ts +1 -0
  42. package/dist/elements/date-picker.js +486 -0
  43. package/dist/elements/dialog.d.ts +23 -0
  44. package/dist/elements/dialog.js +409 -0
  45. package/dist/elements/divider.d.ts +2 -0
  46. package/dist/elements/divider.js +66 -0
  47. package/dist/elements/drawer.d.ts +3 -0
  48. package/dist/elements/drawer.js +308 -0
  49. package/dist/elements/field-composition.d.ts +26 -0
  50. package/dist/elements/field-composition.js +133 -0
  51. package/dist/elements/field.d.ts +1 -0
  52. package/dist/elements/field.js +67 -0
  53. package/dist/elements/file-upload.d.ts +6 -0
  54. package/dist/elements/file-upload.js +211 -0
  55. package/dist/elements/floating-position.d.ts +26 -0
  56. package/dist/elements/floating-position.js +39 -0
  57. package/dist/elements/image.d.ts +28 -0
  58. package/dist/elements/image.js +38 -0
  59. package/dist/elements/info.d.ts +9 -0
  60. package/dist/elements/info.js +102 -0
  61. package/dist/elements/input.d.ts +5 -0
  62. package/dist/elements/input.js +255 -0
  63. package/dist/elements/label.d.ts +15 -0
  64. package/dist/elements/label.js +56 -0
  65. package/dist/elements/link.d.ts +1 -0
  66. package/dist/elements/link.js +92 -0
  67. package/dist/elements/list.d.ts +19 -0
  68. package/dist/elements/list.js +297 -0
  69. package/dist/elements/menu.d.ts +33 -0
  70. package/dist/elements/menu.js +1103 -0
  71. package/dist/elements/message-bar.d.ts +10 -0
  72. package/dist/elements/message-bar.js +299 -0
  73. package/dist/elements/nav.d.ts +32 -0
  74. package/dist/elements/nav.js +791 -0
  75. package/dist/elements/overflow.d.ts +9 -0
  76. package/dist/elements/overflow.js +218 -0
  77. package/dist/elements/persona.d.ts +3 -0
  78. package/dist/elements/persona.js +124 -0
  79. package/dist/elements/popover.d.ts +77 -0
  80. package/dist/elements/popover.js +840 -0
  81. package/dist/elements/progress-bar.d.ts +4 -0
  82. package/dist/elements/progress-bar.js +82 -0
  83. package/dist/elements/radio-group.d.ts +1 -0
  84. package/dist/elements/radio-group.js +303 -0
  85. package/dist/elements/rating-display.d.ts +2 -0
  86. package/dist/elements/rating-display.js +125 -0
  87. package/dist/elements/rating.d.ts +2 -0
  88. package/dist/elements/rating.js +206 -0
  89. package/dist/elements/rich-text-editor.d.ts +18 -0
  90. package/dist/elements/rich-text-editor.js +508 -0
  91. package/dist/elements/search.d.ts +3 -0
  92. package/dist/elements/search.js +207 -0
  93. package/dist/elements/select.d.ts +6 -0
  94. package/dist/elements/select.js +291 -0
  95. package/dist/elements/shared.d.ts +19 -0
  96. package/dist/elements/shared.js +396 -0
  97. package/dist/elements/skeleton.d.ts +6 -0
  98. package/dist/elements/skeleton.js +91 -0
  99. package/dist/elements/slider.d.ts +1 -0
  100. package/dist/elements/slider.js +248 -0
  101. package/dist/elements/spin-button.d.ts +7 -0
  102. package/dist/elements/spin-button.js +499 -0
  103. package/dist/elements/spinner.d.ts +4 -0
  104. package/dist/elements/spinner.js +124 -0
  105. package/dist/elements/swatch-picker.d.ts +5 -0
  106. package/dist/elements/swatch-picker.js +363 -0
  107. package/dist/elements/table.d.ts +21 -0
  108. package/dist/elements/table.js +261 -0
  109. package/dist/elements/tabs.d.ts +1 -0
  110. package/dist/elements/tabs.js +276 -0
  111. package/dist/elements/tag-picker.d.ts +29 -0
  112. package/dist/elements/tag-picker.js +697 -0
  113. package/dist/elements/tags.d.ts +21 -0
  114. package/dist/elements/tags.js +421 -0
  115. package/dist/elements/teaching-popover.d.ts +32 -0
  116. package/dist/elements/teaching-popover.js +412 -0
  117. package/dist/elements/text.d.ts +5 -0
  118. package/dist/elements/text.js +69 -0
  119. package/dist/elements/textarea.d.ts +1 -0
  120. package/dist/elements/textarea.js +182 -0
  121. package/dist/elements/toast.d.ts +25 -0
  122. package/dist/elements/toast.js +337 -0
  123. package/dist/elements/toaster.d.ts +6 -0
  124. package/dist/elements/toaster.js +356 -0
  125. package/dist/elements/toggle.d.ts +2 -0
  126. package/dist/elements/toggle.js +246 -0
  127. package/dist/elements/toolbar.d.ts +12 -0
  128. package/dist/elements/toolbar.js +381 -0
  129. package/dist/elements/tooltip.d.ts +76 -0
  130. package/dist/elements/tooltip.js +510 -0
  131. package/dist/elements/tree-model.d.ts +19 -0
  132. package/dist/elements/tree-model.js +34 -0
  133. package/dist/elements/tree.d.ts +5 -0
  134. package/dist/elements/tree.js +273 -0
  135. package/dist/elements/view-card.d.ts +2 -0
  136. package/dist/elements/view-card.js +78 -0
  137. package/dist/elements/virtual-list.d.ts +41 -0
  138. package/dist/elements/virtual-list.js +369 -0
  139. package/dist/elements.d.ts +72 -0
  140. package/dist/elements.js +207 -0
  141. package/dist/index.d.ts +76 -0
  142. package/dist/index.js +66 -0
  143. package/dist/react-theme-styles.d.ts +2 -0
  144. package/dist/react-theme-styles.js +75 -0
  145. package/dist/react.d.ts +4540 -0
  146. package/dist/react.js +6799 -0
  147. package/dist/theme.d.ts +169 -0
  148. package/dist/theme.js +167 -0
  149. package/dist/virtualizer-hooks.d.ts +80 -0
  150. package/dist/virtualizer-hooks.js +253 -0
  151. package/package.json +87 -4
  152. package/scripts/generate-token-index.mjs +43 -0
  153. package/scripts/sync-component-catalog-signatures.mjs +55 -0
  154. package/scripts/validate-component-catalog.mjs +44 -0
  155. package/skills/sharepoint-ui/SKILL.md +12 -0
  156. package/skills/sharepoint-ui/references/choosing-components.md +20 -0
  157. package/skills/sharepoint-ui/references/sharepoint-patterns.md +17 -0
  158. package/tokens.json +3777 -0
@@ -0,0 +1,1103 @@
1
+ import { base, findFocusable } from "./shared.js";
2
+ const nextId = (() => { let value = 0; return () => `spui-menu-${++value}`; })();
3
+ export function defineMenuItem() {
4
+ if (customElements.get("spui-menu-item"))
5
+ return;
6
+ class SpuiMenuItem extends HTMLElement {
7
+ static observedAttributes = ["disabled", "kind", "checked", "value", "name", "aria-haspopup", "aria-controls", "aria-expanded", "persist-on-click", "has-submenu"];
8
+ connectedCallback() { if (!this.shadowRoot)
9
+ this.render();
10
+ else
11
+ this.sync(); }
12
+ attributeChangedCallback() { if (this.isConnected)
13
+ this.sync(); }
14
+ get checked() { return this.hasAttribute("checked"); }
15
+ set checked(value) { this.toggleAttribute("checked", Boolean(value)); }
16
+ focus(options) { this.shadowRoot?.querySelector("button")?.focus(options); }
17
+ refreshMenuListState() { this.sync(); }
18
+ render() {
19
+ const shadow = this.attachShadow({ mode: "open" });
20
+ const style = document.createElement("style");
21
+ style.textContent = `${base}:host{display:block;min-width:0}button{box-sizing:border-box;display:flex;align-items:center;gap:10px;width:100%;min-height:36px;padding:6px 10px;border:0;border-radius:var(--spui-menu-item-radius,4px);background:transparent;color:inherit;text-align:start;font:inherit;line-height:1.35;cursor:pointer}button:hover:not([aria-disabled=true]),button:focus-visible{background:var(--spui-menu-item-hover,var(--spui-ui-surface-hover,#f5f5f5))}button:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:-2px}button[aria-disabled=true]{opacity:.5;cursor:not-allowed}.check,.icon{display:inline-grid;place-items:center;flex:none;width:16px;height:16px;color:var(--spui-ui-accent,var(--spui-semantic-accent,#0078d4));font-size:14px}.label{display:grid;min-width:0;flex:1}.content{min-width:0}.subtext,.secondary{color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#605e5c));font-size:.85em}.secondary{margin-inline-start:auto;white-space:nowrap}.submenu-indicator{display:inline-grid;place-items:center;flex:none;margin-inline-start:auto;font-size:1.25em;line-height:1}.submenu-indicator[hidden]{display:none!important}slot[name=checkmark],slot[name=content],slot[name=submenuIndicator]{display:contents}:host([kind=switch]) .check{position:relative;width:30px;height:18px;border-radius:999px;background:var(--spui-ui-stroke,#8a8886);color:transparent}:host([kind=switch][checked]) .check{background:var(--spui-ui-accent,#0078d4)}:host([kind=switch]) .check::after{content:"";position:absolute;inset-block-start:2px;inset-inline-start:2px;width:14px;height:14px;border-radius:50%;background:var(--spui-ui-surface,#fff);transition:inset-inline-start var(--spui-motion-duration-fast,120ms) var(--spui-motion-easing-standard,ease)}:host([kind=switch][checked]) .check::after{inset-inline-start:14px}[hidden]{display:none!important}@media(prefers-reduced-motion:reduce){.check::after{transition:none}}@media(forced-colors:active){button:focus-visible{outline-color:Highlight}:host([kind=switch]) .check{border:1px solid ButtonText}}`;
22
+ style.textContent += ":host([kind=switch][has-switch-indicator]) .check::after{display:none}:host([kind=switch]) slot[name=switch-indicator]{display:contents}";
23
+ const button = document.createElement("button");
24
+ button.type = "button";
25
+ const marker = document.createElement("span");
26
+ marker.className = "check";
27
+ marker.setAttribute("aria-hidden", "true");
28
+ const markerText = document.createElement("span");
29
+ markerText.className = "checkmark";
30
+ const checkmarkSlot = document.createElement("slot");
31
+ checkmarkSlot.name = "checkmark";
32
+ checkmarkSlot.append(markerText);
33
+ checkmarkSlot.addEventListener("slotchange", () => this.sync());
34
+ const switchIndicator = document.createElement("slot");
35
+ switchIndicator.name = "switch-indicator";
36
+ switchIndicator.addEventListener("slotchange", () => this.toggleAttribute("has-switch-indicator", switchIndicator.assignedNodes({ flatten: true }).length > 0));
37
+ marker.append(checkmarkSlot, switchIndicator);
38
+ const icon = document.createElement("span");
39
+ icon.className = "icon";
40
+ icon.setAttribute("aria-hidden", "true");
41
+ const iconSlot = document.createElement("slot");
42
+ iconSlot.name = "icon";
43
+ icon.append(iconSlot);
44
+ const label = document.createElement("span");
45
+ label.className = "label";
46
+ const content = document.createElement("span");
47
+ content.className = "content";
48
+ const contentSlot = document.createElement("slot");
49
+ contentSlot.name = "content";
50
+ contentSlot.append(document.createElement("slot"));
51
+ contentSlot.addEventListener("slotchange", () => this.sync());
52
+ content.append(contentSlot);
53
+ const subtext = document.createElement("span");
54
+ subtext.className = "subtext";
55
+ const subtextSlot = document.createElement("slot");
56
+ subtextSlot.name = "subtext";
57
+ subtext.append(subtextSlot);
58
+ label.append(content, subtext);
59
+ const secondary = document.createElement("span");
60
+ secondary.className = "secondary";
61
+ const secondarySlot = document.createElement("slot");
62
+ secondarySlot.name = "secondary-content";
63
+ secondary.append(secondarySlot);
64
+ const submenuIndicator = document.createElement("span");
65
+ submenuIndicator.className = "submenu-indicator";
66
+ submenuIndicator.hidden = true;
67
+ submenuIndicator.setAttribute("aria-hidden", "true");
68
+ const submenuSlot = document.createElement("slot");
69
+ submenuSlot.name = "submenuIndicator";
70
+ const defaultSubmenuIndicator = document.createElement("span");
71
+ defaultSubmenuIndicator.textContent = "›";
72
+ submenuSlot.append(defaultSubmenuIndicator);
73
+ submenuIndicator.append(submenuSlot);
74
+ for (const slot of [iconSlot, subtextSlot, secondarySlot, submenuSlot])
75
+ slot.addEventListener("slotchange", () => this.sync());
76
+ button.append(marker, icon, label, secondary, submenuIndicator);
77
+ button.addEventListener("click", (event) => {
78
+ if (this.hasAttribute("disabled"))
79
+ return;
80
+ if (this.getAttribute("aria-haspopup") === "menu")
81
+ return;
82
+ const kind = this.getAttribute("kind");
83
+ if (kind === "checkbox" || kind === "switch")
84
+ this.checked = !this.checked;
85
+ else if (kind === "radio")
86
+ this.checked = true;
87
+ this.dispatchEvent(new CustomEvent("spui-select", { bubbles: true, composed: true, detail: { value: this.getAttribute("value") ?? "", name: this.getAttribute("name") ?? "", label: this.textContent?.trim() ?? "", checked: this.checked, kind, persistOnClick: this.hasAttribute("persist-on-click"), event } }));
88
+ });
89
+ shadow.append(style, button);
90
+ this.sync();
91
+ }
92
+ sync() {
93
+ const button = this.shadowRoot?.querySelector("button");
94
+ if (!button)
95
+ return;
96
+ const kind = this.getAttribute("kind");
97
+ button.disabled = false;
98
+ button.setAttribute("aria-disabled", String(this.hasAttribute("disabled")));
99
+ button.setAttribute("role", kind === "checkbox" || kind === "switch" ? "menuitemcheckbox" : kind === "radio" ? "menuitemradio" : "menuitem");
100
+ button.tabIndex = -1;
101
+ if (kind === "checkbox" || kind === "radio" || kind === "switch")
102
+ button.setAttribute("aria-checked", String(this.checked));
103
+ else
104
+ button.removeAttribute("aria-checked");
105
+ if (this.hasAttribute("has-submenu")) {
106
+ button.setAttribute("aria-haspopup", "menu");
107
+ button.setAttribute("aria-expanded", this.getAttribute("aria-expanded") ?? "false");
108
+ }
109
+ const submenuIndicator = this.shadowRoot?.querySelector(".submenu-indicator");
110
+ if (submenuIndicator)
111
+ submenuIndicator.hidden = !this.hasAttribute("has-submenu");
112
+ for (const name of ["aria-haspopup", "aria-controls", "aria-expanded"]) {
113
+ const value = this.getAttribute(name);
114
+ if (value === null) {
115
+ if (!(name === "aria-haspopup" && this.hasAttribute("has-submenu")) && !(name === "aria-expanded" && this.hasAttribute("has-submenu")))
116
+ button.removeAttribute(name);
117
+ }
118
+ else
119
+ button.setAttribute(name, value);
120
+ }
121
+ const marker = this.shadowRoot?.querySelector(".check");
122
+ if (marker)
123
+ marker.hidden = !(kind === "switch" || this.checked && (kind === "checkbox" || kind === "radio"));
124
+ const markerText = this.shadowRoot?.querySelector(".checkmark");
125
+ if (markerText)
126
+ markerText.textContent = kind === "radio" ? "●" : kind === "switch" ? "" : "✓";
127
+ const list = this.closest("[has-checkmarks], spui-menu-list[has-checkmarks]");
128
+ if (marker && list?.hasAttribute("has-checkmarks") && kind !== "switch")
129
+ marker.hidden = false;
130
+ const icon = this.shadowRoot?.querySelector(".icon");
131
+ const iconSlot = this.shadowRoot?.querySelector("slot[name=icon]");
132
+ if (icon && iconSlot)
133
+ icon.hidden = !this.closest("[has-icons], spui-menu-list[has-icons]")?.hasAttribute("has-icons") && iconSlot.assignedNodes().length === 0;
134
+ for (const slotName of ["subtext", "secondary-content"]) {
135
+ const slot = this.shadowRoot?.querySelector(`slot[name=${slotName}]`);
136
+ const host = slot?.parentElement;
137
+ if (host && slot)
138
+ host.hidden = slot.assignedNodes().length === 0;
139
+ }
140
+ }
141
+ }
142
+ customElements.define("spui-menu-item", SpuiMenuItem);
143
+ }
144
+ export function defineMenuDivider() {
145
+ if (customElements.get("spui-menu-divider"))
146
+ return;
147
+ class SpuiMenuDivider extends HTMLElement {
148
+ connectedCallback() {
149
+ if (this.shadowRoot)
150
+ return;
151
+ if (!this.hasAttribute("role"))
152
+ this.setAttribute("role", "presentation");
153
+ if (!this.hasAttribute("aria-hidden"))
154
+ this.setAttribute("aria-hidden", "true");
155
+ const shadow = this.attachShadow({ mode: "open" });
156
+ const style = document.createElement("style");
157
+ style.textContent = `${base}:host{display:block;height:1px;margin-block:5px;background:var(--spui-menu-divider,var(--spui-ui-stroke,var(--spui-semantic-body-divider,#e1dfdd)))}`;
158
+ const divider = document.createElement("div");
159
+ divider.setAttribute("aria-hidden", "true");
160
+ shadow.append(style, divider);
161
+ }
162
+ }
163
+ customElements.define("spui-menu-divider", SpuiMenuDivider);
164
+ }
165
+ export function defineMenuGroup() {
166
+ if (customElements.get("spui-menu-group"))
167
+ return;
168
+ class SpuiMenuGroup extends HTMLElement {
169
+ observer;
170
+ managedHeaderId;
171
+ connectedCallback() {
172
+ if (!this.shadowRoot) {
173
+ const shadow = this.attachShadow({ mode: "open" });
174
+ const style = document.createElement("style");
175
+ style.textContent = `${base}:host{display:block;min-width:0}:host([label]){padding-block:4px}:host([label])::before{content:attr(label);display:block;padding:4px 10px;color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#605e5c));font-size:12px;font-weight:600}`;
176
+ shadow.append(style, document.createElement("slot"));
177
+ }
178
+ if (!this.hasAttribute("role"))
179
+ this.setAttribute("role", "group");
180
+ this.updateAccessibleName();
181
+ this.observer = new MutationObserver(() => this.updateAccessibleName());
182
+ this.observer.observe(this, { childList: true, subtree: true, attributes: true, attributeFilter: ["id"] });
183
+ }
184
+ disconnectedCallback() {
185
+ this.observer?.disconnect();
186
+ this.observer = undefined;
187
+ }
188
+ updateAccessibleName() {
189
+ const header = this.querySelector("spui-menu-group-header");
190
+ if (header) {
191
+ if (!header.id)
192
+ header.id = `spui-menu-group-header-${crypto.randomUUID()}`;
193
+ if (!this.hasAttribute("aria-labelledby") || this.getAttribute("aria-labelledby") === this.managedHeaderId) {
194
+ this.setAttribute("aria-labelledby", header.id);
195
+ this.managedHeaderId = header.id;
196
+ }
197
+ return;
198
+ }
199
+ if (this.managedHeaderId && this.getAttribute("aria-labelledby") === this.managedHeaderId) {
200
+ this.removeAttribute("aria-labelledby");
201
+ this.managedHeaderId = undefined;
202
+ }
203
+ if (!this.hasAttribute("aria-labelledby") && !this.hasAttribute("aria-label") && this.hasAttribute("label")) {
204
+ this.setAttribute("aria-label", this.getAttribute("label") ?? "");
205
+ }
206
+ }
207
+ }
208
+ customElements.define("spui-menu-group", SpuiMenuGroup);
209
+ }
210
+ export function defineMenuGroupHeader() {
211
+ if (customElements.get("spui-menu-group-header"))
212
+ return;
213
+ class SpuiMenuGroupHeader extends HTMLElement {
214
+ connectedCallback() {
215
+ if (this.shadowRoot)
216
+ return;
217
+ const shadow = this.attachShadow({ mode: "open" });
218
+ const style = document.createElement("style");
219
+ style.textContent = `${base}:host{display:block;padding:5px 10px;color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#605e5c));font-size:12px;font-weight:600}`;
220
+ const slot = document.createElement("slot");
221
+ shadow.append(style, slot);
222
+ }
223
+ }
224
+ customElements.define("spui-menu-group-header", SpuiMenuGroupHeader);
225
+ }
226
+ export function defineMenuItemLink() {
227
+ if (customElements.get("spui-menu-item-link"))
228
+ return;
229
+ class SpuiMenuItemLink extends HTMLElement {
230
+ static observedAttributes = ["href", "target", "rel", "download", "disabled", "aria-label"];
231
+ connectedCallback() { if (!this.shadowRoot)
232
+ this.render();
233
+ else
234
+ this.sync(); }
235
+ attributeChangedCallback() { if (this.isConnected)
236
+ this.sync(); }
237
+ focus(options) { this.shadowRoot?.querySelector("a")?.focus(options); }
238
+ refreshMenuListState() { this.sync(); }
239
+ render() {
240
+ const shadow = this.attachShadow({ mode: "open" });
241
+ const style = document.createElement("style");
242
+ style.textContent = `${base}:host{display:block;min-width:0}a{box-sizing:border-box;display:flex;align-items:center;gap:10px;min-height:36px;padding:6px 10px;border-radius:var(--spui-menu-item-radius,4px);color:inherit;text-decoration:none;line-height:1.35}a:hover:not([aria-disabled=true]),a:focus-visible{background:var(--spui-menu-item-hover,var(--spui-ui-surface-hover,#f5f5f5))}a:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:-2px}a[aria-disabled=true]{opacity:.5;cursor:not-allowed}.checkmark,.icon{display:inline-grid;place-items:center;flex:none;width:16px;height:16px}.checkmark{color:var(--spui-ui-accent,var(--spui-semantic-accent,#0078d4))}.icon{color:inherit}.content{min-width:0;flex:1}.secondary{margin-inline-start:auto;color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#605e5c));font-size:.85em;white-space:nowrap}slot[name=checkmark],slot[name=content]{display:contents}[hidden]{display:none!important}@media(forced-colors:active){a:focus-visible{outline-color:Highlight}}`;
243
+ const anchor = document.createElement("a");
244
+ anchor.setAttribute("role", "menuitem");
245
+ const checkmark = document.createElement("span");
246
+ checkmark.className = "checkmark";
247
+ checkmark.setAttribute("aria-hidden", "true");
248
+ const checkmarkSlot = document.createElement("slot");
249
+ checkmarkSlot.name = "checkmark";
250
+ checkmark.append(checkmarkSlot);
251
+ const icon = document.createElement("span");
252
+ icon.className = "icon";
253
+ icon.setAttribute("aria-hidden", "true");
254
+ const iconSlot = document.createElement("slot");
255
+ iconSlot.name = "icon";
256
+ icon.append(iconSlot);
257
+ const content = document.createElement("span");
258
+ content.className = "content";
259
+ const contentSlot = document.createElement("slot");
260
+ contentSlot.name = "content";
261
+ contentSlot.append(document.createElement("slot"));
262
+ content.append(contentSlot);
263
+ const secondary = document.createElement("span");
264
+ secondary.className = "secondary";
265
+ const secondarySlot = document.createElement("slot");
266
+ secondarySlot.name = "secondary-content";
267
+ secondary.append(secondarySlot);
268
+ anchor.append(checkmark, icon, content, secondary);
269
+ checkmarkSlot.addEventListener("slotchange", () => this.sync());
270
+ contentSlot.addEventListener("slotchange", () => this.sync());
271
+ for (const [slot, wrapper] of [[iconSlot, icon], [secondarySlot, secondary]])
272
+ slot.addEventListener("slotchange", () => { wrapper.hidden = slot.assignedNodes().length === 0 && !(slot.name === "icon" && this.closest("[has-icons], spui-menu-list[has-icons]")?.hasAttribute("has-icons")); });
273
+ anchor.addEventListener("click", event => {
274
+ if (this.hasAttribute("disabled")) {
275
+ event.preventDefault();
276
+ event.stopPropagation();
277
+ return;
278
+ }
279
+ this.dispatchEvent(new CustomEvent("spui-select", { bubbles: true, composed: true, detail: { value: this.getAttribute("href") ?? "", label: this.textContent?.trim() ?? "", checked: false, kind: "link", event } }));
280
+ });
281
+ shadow.append(style, anchor);
282
+ this.sync();
283
+ }
284
+ sync() {
285
+ const anchor = this.shadowRoot?.querySelector("a");
286
+ if (!anchor)
287
+ return;
288
+ for (const name of ["href", "target", "rel", "download", "aria-label"]) {
289
+ const value = this.getAttribute(name);
290
+ if (value === null)
291
+ anchor.removeAttribute(name);
292
+ else
293
+ anchor.setAttribute(name, value);
294
+ }
295
+ const icon = this.shadowRoot?.querySelector(".icon");
296
+ const iconSlot = this.shadowRoot?.querySelector("slot[name=icon]");
297
+ if (icon && iconSlot)
298
+ icon.hidden = iconSlot.assignedNodes().length === 0 && !this.closest("[has-icons], spui-menu-list[has-icons]")?.hasAttribute("has-icons");
299
+ const checkmark = this.shadowRoot?.querySelector(".checkmark");
300
+ const checkmarkSlot = this.shadowRoot?.querySelector("slot[name=checkmark]");
301
+ if (checkmark && checkmarkSlot)
302
+ checkmark.hidden = checkmarkSlot.assignedNodes({ flatten: true }).length === 0 && !this.closest("[has-checkmarks], spui-menu-list[has-checkmarks]")?.hasAttribute("has-checkmarks");
303
+ const secondary = this.shadowRoot?.querySelector(".secondary");
304
+ const secondarySlot = this.shadowRoot?.querySelector("slot[name=secondary-content]");
305
+ if (secondary && secondarySlot)
306
+ secondary.hidden = secondarySlot.assignedNodes().length === 0;
307
+ if (this.hasAttribute("disabled")) {
308
+ anchor.removeAttribute("href");
309
+ anchor.setAttribute("aria-disabled", "true");
310
+ anchor.tabIndex = -1;
311
+ }
312
+ else {
313
+ anchor.removeAttribute("aria-disabled");
314
+ anchor.tabIndex = -1;
315
+ if (this.hasAttribute("href"))
316
+ anchor.setAttribute("href", this.getAttribute("href"));
317
+ }
318
+ }
319
+ }
320
+ customElements.define("spui-menu-item-link", SpuiMenuItemLink);
321
+ }
322
+ export function defineMenuList() {
323
+ if (customElements.get("spui-menu-list"))
324
+ return;
325
+ class SpuiMenuList extends HTMLElement {
326
+ static observedAttributes = ["has-icons", "has-checkmarks"];
327
+ connectedCallback() {
328
+ if (this.shadowRoot)
329
+ return;
330
+ const shadow = this.attachShadow({ mode: "open" });
331
+ const style = document.createElement("style");
332
+ style.textContent = `${base}:host{display:block;min-width:0}:host([has-icons]) spui-menu-item, :host([has-checkmarks]) spui-menu-item{--spui-menu-reserve-leading:24px}`;
333
+ const slot = document.createElement("slot");
334
+ slot.addEventListener("slotchange", this.syncItems);
335
+ shadow.append(style, slot);
336
+ this.setAttribute("role", "group");
337
+ this.syncItems();
338
+ }
339
+ attributeChangedCallback() { this.syncItems(); }
340
+ syncItems = () => { for (const item of this.querySelectorAll("spui-menu-item,spui-menu-item-link"))
341
+ item.refreshMenuListState?.(); };
342
+ }
343
+ customElements.define("spui-menu-list", SpuiMenuList);
344
+ }
345
+ export function defineMenuPopover() {
346
+ if (customElements.get("spui-menu-popover"))
347
+ return;
348
+ class SpuiMenuPopover extends HTMLElement {
349
+ connectedCallback() {
350
+ if (this.shadowRoot)
351
+ return;
352
+ const shadow = this.attachShadow({ mode: "open" });
353
+ const style = document.createElement("style");
354
+ style.textContent = `${base}:host{display:block;min-width:0}`;
355
+ const slot = document.createElement("slot");
356
+ shadow.append(style, slot);
357
+ }
358
+ }
359
+ customElements.define("spui-menu-popover", SpuiMenuPopover);
360
+ }
361
+ export function defineMenuSplitGroup() {
362
+ if (customElements.get("spui-menu-split-group"))
363
+ return;
364
+ class SpuiMenuSplitGroup extends HTMLElement {
365
+ connectedCallback() {
366
+ if (this.shadowRoot)
367
+ return;
368
+ const shadow = this.attachShadow({ mode: "open" });
369
+ const style = document.createElement("style");
370
+ style.textContent = `${base}:host{display:flex;align-items:stretch;gap:4px}:host([orientation=vertical]){flex-direction:column}`;
371
+ const slot = document.createElement("slot");
372
+ shadow.append(style, slot);
373
+ this.setAttribute("role", "group");
374
+ this.addEventListener("keydown", this.onKeyDown);
375
+ }
376
+ disconnectedCallback() { this.removeEventListener("keydown", this.onKeyDown); }
377
+ onKeyDown = (event) => {
378
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
379
+ return;
380
+ const items = [...this.querySelectorAll("spui-menu-item,spui-menu-item-link")];
381
+ const active = items.findIndex(item => item === event.composedPath()[0] || item.shadowRoot?.activeElement === this.ownerDocument.activeElement || item.shadowRoot?.activeElement === event.composedPath()[0]);
382
+ if (active < 0 || !items.length)
383
+ return;
384
+ const rtl = getComputedStyle(this).direction === "rtl";
385
+ const delta = (event.key === "ArrowRight" ? 1 : -1) * (rtl ? -1 : 1);
386
+ event.preventDefault();
387
+ event.stopPropagation();
388
+ items[(active + delta + items.length) % items.length].focus();
389
+ };
390
+ }
391
+ customElements.define("spui-menu-split-group", SpuiMenuSplitGroup);
392
+ }
393
+ export function defineMenuTrigger() {
394
+ if (customElements.get("spui-menu-trigger"))
395
+ return;
396
+ class SpuiMenuTrigger extends HTMLElement {
397
+ static observedAttributes = ["aria-haspopup", "aria-controls", "aria-expanded", "disabled"];
398
+ connectedCallback() {
399
+ if (!this.shadowRoot) {
400
+ const shadow = this.attachShadow({ mode: "open" });
401
+ const style = document.createElement("style");
402
+ style.textContent = `${base}:host{display:inline-flex;cursor:pointer}:host([disabled]){opacity:.55;cursor:not-allowed}:host(:focus-visible){outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:2px}slot{display:contents}`;
403
+ const slot = document.createElement("slot");
404
+ shadow.append(style, slot);
405
+ this.setAttribute("role", "button");
406
+ this.tabIndex = this.hasAttribute("disabled") ? -1 : this.tabIndex < 0 ? 0 : this.tabIndex;
407
+ }
408
+ this.addEventListener("keydown", this.onKeyDown);
409
+ this.addEventListener("click", this.onClick);
410
+ this.sync();
411
+ }
412
+ disconnectedCallback() { this.removeEventListener("keydown", this.onKeyDown); this.removeEventListener("click", this.onClick); }
413
+ focus(options) { HTMLElement.prototype.focus.call(this, options); }
414
+ sync() { for (const name of ["aria-haspopup", "aria-controls", "aria-expanded"]) {
415
+ const value = this.getAttribute(name);
416
+ if (value === null)
417
+ this.removeAttribute(name);
418
+ } this.setAttribute("aria-haspopup", "menu"); if (this.hasAttribute("disabled"))
419
+ this.setAttribute("aria-disabled", "true");
420
+ else
421
+ this.removeAttribute("aria-disabled"); }
422
+ onKeyDown = (event) => { if (this.hasAttribute("disabled") || event.repeat || (event.key !== "Enter" && event.key !== " "))
423
+ return; event.preventDefault(); this.click(); };
424
+ onClick = (event) => { if (!this.hasAttribute("disabled"))
425
+ return; event.preventDefault(); event.stopPropagation(); };
426
+ }
427
+ customElements.define("spui-menu-trigger", SpuiMenuTrigger);
428
+ }
429
+ export function defineMenu() {
430
+ if (customElements.get("spui-menu"))
431
+ return;
432
+ defineMenuItem();
433
+ defineMenuItemLink();
434
+ defineMenuDivider();
435
+ defineMenuGroup();
436
+ defineMenuGroupHeader();
437
+ defineMenuList();
438
+ defineMenuPopover();
439
+ defineMenuSplitGroup();
440
+ defineMenuTrigger();
441
+ class SpuiMenu extends HTMLElement {
442
+ static observedAttributes = ["open", "default-open", "motion", "label", "placement", "positioning", "position-offset", "context-menu", "open-on-context", "open-on-hover", "hover-delay", "close-on-scroll", "persist-on-item-click", "inline"];
443
+ events;
444
+ surfaceEvents;
445
+ panel;
446
+ internalPanel;
447
+ externalPanel;
448
+ parentElementOverride;
449
+ trigger;
450
+ triggerState;
451
+ isOpen = false;
452
+ typeahead = "";
453
+ typeaheadTimer;
454
+ contextPoint;
455
+ hoverTimer;
456
+ motionExitPending = false;
457
+ exitGeneration = 0;
458
+ get open() { return this.isOpen; }
459
+ set open(value) { this.setOpen(Boolean(value)); }
460
+ get menuTrigger() { return this.trigger; }
461
+ set menuSurface(value) {
462
+ const previousPanel = this.panel;
463
+ this.externalPanel = value ?? undefined;
464
+ this.panel = this.externalPanel ?? this.internalPanel;
465
+ if (previousPanel && previousPanel !== this.panel) {
466
+ previousPanel.hidePopover?.();
467
+ previousPanel.hidden = true;
468
+ }
469
+ if (this.panel) {
470
+ this.panel.id ||= nextId();
471
+ this.panel.setAttribute("role", "menu");
472
+ this.panel.setAttribute("aria-orientation", "vertical");
473
+ this.panel.setAttribute("popover", "manual");
474
+ this.panel.addEventListener("toggle", this.onNativeToggle);
475
+ }
476
+ this.bindSurfaceEvents();
477
+ if (this.isConnected) {
478
+ this.syncLabel();
479
+ this.syncVisibility();
480
+ }
481
+ this.syncTrigger();
482
+ }
483
+ get menuSurface() { return this.externalPanel ?? null; }
484
+ set parentMenuElement(value) { this.parentElementOverride = value ?? undefined; }
485
+ get parentMenuElement() { return this.parentElementOverride; }
486
+ connectedCallback() {
487
+ this.style.display ||= "contents";
488
+ if (!this.shadowRoot)
489
+ this.render();
490
+ this.events?.abort();
491
+ this.events = new AbortController();
492
+ const options = { signal: this.events.signal };
493
+ this.addEventListener("click", this.onClick, options);
494
+ this.addEventListener("keydown", this.onKeyDown, options);
495
+ this.addEventListener("spui-select", this.onItemSelect, options);
496
+ this.addEventListener("contextmenu", this.onContextMenu, options);
497
+ this.addEventListener("keydown", this.onContextMenuKeyDown, options);
498
+ this.addEventListener("pointerover", this.onPointerOver, options);
499
+ this.addEventListener("pointerout", this.onPointerOut, options);
500
+ this.addEventListener("mouseover", this.onPanelMouseOver, options);
501
+ this.ownerDocument.addEventListener("pointerdown", this.onOutsidePointerDown, { ...options, capture: true });
502
+ this.ownerDocument.addEventListener("spui-menu-enter", this.onMenuEnter, options);
503
+ this.ownerDocument.addEventListener("wheel", this.onScrollOutside, { ...options, capture: true });
504
+ this.ownerDocument.addEventListener("touchmove", this.onScrollOutside, { ...options, capture: true });
505
+ window.addEventListener("resize", this.position, options);
506
+ window.addEventListener("scroll", this.position, { ...options, capture: true });
507
+ this.shadowRoot?.querySelector("slot[name=trigger]")?.addEventListener("slotchange", this.syncTrigger, options);
508
+ this.internalPanel = this.shadowRoot?.querySelector(".surface");
509
+ this.panel = this.externalPanel ?? this.internalPanel;
510
+ this.bindSurfaceEvents();
511
+ if (!this.hasAttribute("placement"))
512
+ this.setAttribute("placement", this.parentMenu() ? "end" : "bottom");
513
+ this.isOpen = this.hasAttribute("open") || this.hasAttribute("default-open");
514
+ if (this.isOpen)
515
+ this.toggleAttribute("open", true);
516
+ this.syncTrigger();
517
+ this.syncLabel();
518
+ this.syncVisibility();
519
+ }
520
+ disconnectedCallback() {
521
+ this.events?.abort();
522
+ this.surfaceEvents?.abort();
523
+ this.clearTypeahead();
524
+ this.clearHoverTimer();
525
+ this.restoreTrigger();
526
+ this.panel?.hidePopover?.();
527
+ }
528
+ attributeChangedCallback(name) {
529
+ if (!this.isConnected)
530
+ return;
531
+ if (name === "open") {
532
+ const next = this.hasAttribute("open");
533
+ if (next !== this.isOpen) {
534
+ this.isOpen = next;
535
+ this.syncVisibility();
536
+ this.emitOpenChange();
537
+ }
538
+ }
539
+ else if (name === "label")
540
+ this.syncLabel();
541
+ else if (name === "inline") {
542
+ this.syncPopoverMode();
543
+ this.syncVisibility();
544
+ }
545
+ else
546
+ this.position();
547
+ }
548
+ render() {
549
+ const shadow = this.attachShadow({ mode: "open" });
550
+ const style = document.createElement("style");
551
+ style.textContent = `${base}:host{display:contents}.surface{position:fixed;inset:auto;margin:0;z-index:var(--spui-layer-popover,10000);box-sizing:border-box;width:max-content;min-width:180px;max-width:min(320px,calc(100vw - 16px));max-height:min(70vh,calc(100dvh - 24px));overflow:auto;padding:4px;border:1px solid var(--spui-menu-border,var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1)));border-radius:var(--spui-menu-radius,var(--spui-card-radius,8px));background:var(--spui-menu-background,var(--spui-ui-surface,var(--colorNeutralBackground1,#fff)));color:var(--spui-menu-color,var(--spui-ui-text,var(--colorNeutralForeground1,#242424)));box-shadow:var(--spui-menu-shadow,0 8px 24px rgb(0 0 0 / 18%));font:var(--spui-font-family,inherit);line-height:1.45}.surface:not(:popover-open){display:none}:host([inline]) .surface:not([hidden]){display:block!important}.surface[hidden]{display:none!important}slot[name=trigger]{display:contents}@media(forced-colors:active){.surface{border-color:CanvasText;box-shadow:none}}`;
552
+ const triggerSlot = document.createElement("slot");
553
+ triggerSlot.name = "trigger";
554
+ const panel = document.createElement("div");
555
+ panel.className = "surface";
556
+ panel.part.add("surface");
557
+ panel.setAttribute("popover", "manual");
558
+ panel.setAttribute("role", "menu");
559
+ panel.setAttribute("aria-orientation", "vertical");
560
+ panel.id = nextId();
561
+ const items = document.createElement("slot");
562
+ items.name = "menu";
563
+ panel.append(items);
564
+ panel.addEventListener("toggle", this.onNativeToggle);
565
+ shadow.append(style, triggerSlot, panel);
566
+ this.internalPanel = panel;
567
+ this.panel = this.externalPanel ?? panel;
568
+ }
569
+ bindSurfaceEvents() {
570
+ this.surfaceEvents?.abort();
571
+ this.surfaceEvents = undefined;
572
+ if (!this.isConnected || !this.externalPanel)
573
+ return;
574
+ const events = new AbortController();
575
+ const options = { signal: events.signal };
576
+ const listeners = [["click", this.onClick], ["keydown", this.onKeyDown], ["spui-select", this.onItemSelect], ["pointerover", this.onPointerOver], ["pointerout", this.onPointerOut], ["mouseover", this.onPanelMouseOver]];
577
+ for (const [type, listener] of listeners)
578
+ this.externalPanel.addEventListener(type, listener, options);
579
+ this.externalPanel.addEventListener("spui-select", event => {
580
+ const selection = event;
581
+ this.dispatchEvent(new CustomEvent("spui-select", { bubbles: true, composed: true, detail: selection.detail }));
582
+ }, options);
583
+ this.surfaceEvents = events;
584
+ }
585
+ assignedTriggers() { return this.shadowRoot?.querySelector("slot[name=trigger]")?.assignedElements({ flatten: true }).filter((node) => node instanceof HTMLElement) ?? []; }
586
+ syncTrigger = () => {
587
+ const slotted = this.assignedTriggers()[0];
588
+ const next = slotted ? findFocusable(slotted.shadowRoot ?? slotted) ?? slotted : undefined;
589
+ if (next !== this.trigger) {
590
+ this.restoreTrigger();
591
+ this.trigger = next;
592
+ this.triggerState = next ? { haspopup: next.getAttribute("aria-haspopup"), controls: next.getAttribute("aria-controls"), expanded: next.getAttribute("aria-expanded") } : undefined;
593
+ }
594
+ if (this.trigger && this.panel) {
595
+ this.trigger.setAttribute("aria-haspopup", "menu");
596
+ if (this.trigger.getRootNode() === this.panel.getRootNode())
597
+ this.trigger.setAttribute("aria-controls", this.panel.id);
598
+ else
599
+ this.trigger.removeAttribute("aria-controls");
600
+ this.trigger.setAttribute("aria-expanded", String(this.isOpen));
601
+ }
602
+ this.dispatchEvent(new CustomEvent("spui-menu-trigger-change", { bubbles: true, composed: true }));
603
+ };
604
+ restoreTrigger() {
605
+ if (!this.trigger || !this.triggerState)
606
+ return;
607
+ for (const [name, value] of [["aria-haspopup", this.triggerState.haspopup], ["aria-expanded", this.triggerState.expanded]]) {
608
+ if (value === null)
609
+ this.trigger.removeAttribute(name);
610
+ else
611
+ this.trigger.setAttribute(name, value);
612
+ }
613
+ if (this.triggerState.controls === null)
614
+ this.trigger.removeAttribute("aria-controls");
615
+ else
616
+ this.trigger.setAttribute("aria-controls", this.triggerState.controls);
617
+ this.trigger = undefined;
618
+ this.triggerState = undefined;
619
+ }
620
+ syncLabel() { if (this.panel)
621
+ this.panel.setAttribute("aria-label", this.getAttribute("label") || "Menu"); }
622
+ syncPopoverMode() {
623
+ if (!this.panel)
624
+ return;
625
+ if (this.hasAttribute("inline")) {
626
+ this.panel.removeAttribute("popover");
627
+ this.panel.hidden = !this.isOpen && !this.motionExitPending;
628
+ }
629
+ else {
630
+ this.panel.setAttribute("popover", "manual");
631
+ this.panel.hidden = false;
632
+ }
633
+ }
634
+ syncVisibility() {
635
+ if (!this.panel)
636
+ return;
637
+ if (this.isOpen) {
638
+ this.exitGeneration++;
639
+ this.motionExitPending = false;
640
+ this.panel.inert = false;
641
+ this.panel.removeAttribute("aria-hidden");
642
+ }
643
+ else if (this.hasAttribute("motion")) {
644
+ this.motionExitPending = true;
645
+ this.panel.inert = true;
646
+ this.panel.setAttribute("aria-hidden", "true");
647
+ }
648
+ this.syncPopoverMode();
649
+ if (this.hasAttribute("inline"))
650
+ this.panel.hidden = !this.isOpen && !this.motionExitPending;
651
+ else if (this.isOpen) {
652
+ try {
653
+ this.panel.showPopover?.();
654
+ }
655
+ catch { /* The open attribute remains the fallback state. */ }
656
+ }
657
+ else if (!this.motionExitPending)
658
+ this.panel.hidePopover?.();
659
+ if (this.motionExitPending) {
660
+ if (!this.hasAttribute("inline") && !this.panel.matches(":popover-open")) {
661
+ try {
662
+ this.panel.showPopover?.();
663
+ }
664
+ catch { /* Keep the closed state if the browser cannot restore the top layer. */ }
665
+ }
666
+ const generation = ++this.exitGeneration;
667
+ requestAnimationFrame(() => requestAnimationFrame(() => {
668
+ const animations = new Set();
669
+ const visit = (element) => {
670
+ for (const animation of element.getAnimations())
671
+ if (animation.playState === "running")
672
+ animations.add(animation);
673
+ if (element.shadowRoot)
674
+ for (const child of element.shadowRoot.children)
675
+ visit(child);
676
+ for (const child of element.children)
677
+ visit(child);
678
+ };
679
+ visit(this);
680
+ const finish = () => {
681
+ if (generation !== this.exitGeneration || this.isOpen || !this.panel)
682
+ return;
683
+ this.motionExitPending = false;
684
+ if (!this.hasAttribute("inline"))
685
+ this.panel.hidePopover?.();
686
+ this.panel.hidden = true;
687
+ };
688
+ if (animations.size)
689
+ void Promise.allSettled([...animations].map(animation => animation.finished)).then(finish);
690
+ else
691
+ finish();
692
+ }));
693
+ }
694
+ if (this.isOpen)
695
+ requestAnimationFrame(this.position);
696
+ this.syncTrigger();
697
+ }
698
+ position = () => {
699
+ if (!this.isOpen || !this.trigger || !this.panel)
700
+ return;
701
+ const anchor = this.trigger.getBoundingClientRect();
702
+ const panel = this.panel.getBoundingClientRect();
703
+ const offsetValue = this.getAttribute("position-offset");
704
+ const offset = Number(offsetValue);
705
+ const gap = offsetValue === null ? 4 : Number.isFinite(offset) ? Math.max(0, Math.min(64, offset)) : 4;
706
+ const direction = this.getAttribute("dir") || getComputedStyle(this).direction;
707
+ if (this.hasAttribute("context-menu")) {
708
+ const point = this.contextPoint ?? { x: direction === "rtl" ? anchor.right : anchor.left, y: anchor.bottom };
709
+ const left = Math.max(gap, Math.min(point.x, innerWidth - panel.width - gap));
710
+ const top = Math.max(gap, Math.min(point.y, innerHeight - panel.height - gap));
711
+ this.panel.style.left = `${left}px`;
712
+ this.panel.style.top = `${top}px`;
713
+ return;
714
+ }
715
+ const value = this.getAttribute("positioning") || this.getAttribute("placement") || "bottom";
716
+ const side = value.startsWith("above") ? "top" : value.startsWith("below") ? "bottom" : value === "before" ? "start" : value === "after" ? "end" : value.split("-")[0];
717
+ const alignment = value.endsWith("-end") ? "end" : "start";
718
+ let top = side === "top" ? anchor.top - panel.height - gap : side === "bottom" ? anchor.bottom + gap : alignment === "end" ? anchor.bottom - panel.height : anchor.top;
719
+ let left = side === "start" ? (direction === "rtl" ? anchor.right + gap : anchor.left - panel.width - gap) : side === "end" ? (direction === "rtl" ? anchor.left - panel.width - gap : anchor.right + gap) : alignment === "end" ? (direction === "rtl" ? anchor.left : anchor.right - panel.width) : (direction === "rtl" ? anchor.right - panel.width : anchor.left);
720
+ if (side === "bottom" && top + panel.height > innerHeight - gap)
721
+ top = anchor.top - panel.height - gap;
722
+ if (side === "top" && top < gap)
723
+ top = anchor.bottom + gap;
724
+ left = Math.max(gap, Math.min(left, innerWidth - panel.width - gap));
725
+ top = Math.max(gap, Math.min(top, innerHeight - panel.height - gap));
726
+ this.panel.style.left = `${left}px`;
727
+ this.panel.style.top = `${top}px`;
728
+ };
729
+ contextEnabled() { return this.hasAttribute("context-menu") || this.hasAttribute("open-on-context"); }
730
+ onContextMenu = (event) => {
731
+ if (!this.contextEnabled() || !this.trigger || !event.composedPath().includes(this.trigger))
732
+ return;
733
+ event.preventDefault();
734
+ this.contextPoint = { x: event.clientX, y: event.clientY };
735
+ this.setOpen(true, { event, type: "menuTriggerContextMenu" });
736
+ requestAnimationFrame(() => this.focusItem(0));
737
+ };
738
+ onContextMenuKeyDown = (event) => {
739
+ if (!this.contextEnabled() || !this.trigger || !event.composedPath().includes(this.trigger))
740
+ return;
741
+ if (event.key !== "ContextMenu" && !(event.key === "F10" && event.shiftKey))
742
+ return;
743
+ event.preventDefault();
744
+ const rect = this.trigger.getBoundingClientRect();
745
+ this.contextPoint = { x: getComputedStyle(this).direction === "rtl" ? rect.right : rect.left, y: rect.bottom };
746
+ this.setOpen(true, { event, type: "menuTriggerKeyDown", keyboard: true });
747
+ requestAnimationFrame(() => this.focusItem(0));
748
+ };
749
+ items() { return [...(this.externalPanel ?? this).querySelectorAll("spui-menu-item,spui-menu-item-link")]; }
750
+ menuItems() {
751
+ return this.items().filter(item => {
752
+ const owner = item.closest("spui-menu");
753
+ if (owner === this || !owner && this.externalPanel?.contains(item))
754
+ return item.getAttribute("slot") !== "trigger";
755
+ const ownerMenu = owner;
756
+ return item.getAttribute("slot") === "trigger" && (ownerMenu?.parentMenuElement ?? ownerMenu?.parentElement?.closest("spui-menu")) === this;
757
+ });
758
+ }
759
+ focusItem(index) { const items = this.menuItems(); if (!items.length)
760
+ return; items[(index + items.length) % items.length].focus(); }
761
+ focusItemAfterOpen(index) { requestAnimationFrame(() => requestAnimationFrame(() => { if (this.isOpen)
762
+ this.focusItem(index); })); }
763
+ activeIndex() { return this.menuItems().findIndex(item => item.matches(":focus-within") || item.shadowRoot?.activeElement != null); }
764
+ onClick = (event) => {
765
+ const target = event.target;
766
+ if (!(target instanceof Node))
767
+ return;
768
+ const path = event.composedPath();
769
+ if (this.trigger && (target === this.trigger || this.trigger.contains(target) || path.includes(this.trigger))) {
770
+ if (path.some(node => node instanceof HTMLElement && (node.hasAttribute("disabled") || node.getAttribute("aria-disabled") === "true")))
771
+ return;
772
+ if (this.contextEnabled() && !this.hasAttribute("open-on-hover"))
773
+ return;
774
+ this.setOpen(!this.isOpen, { event, type: "menuTriggerClick" });
775
+ if (this.isOpen)
776
+ requestAnimationFrame(() => this.focusItem(0));
777
+ }
778
+ };
779
+ onOutsidePointerDown = (event) => {
780
+ if (!this.isOpen || event.composedPath().some(target => target === this || target === this.panel || target instanceof Node && (this.contains(target) || this.shadowRoot?.contains(target) || this.panel?.contains(target))))
781
+ return;
782
+ this.setOpen(false, { event, type: "clickOutside" });
783
+ };
784
+ hoverDelay() { const raw = this.getAttribute("hover-delay"); if (raw === null)
785
+ return 300; const value = Number(raw); return Number.isFinite(value) ? Math.max(0, Math.min(2000, value)) : 300; }
786
+ onPointerOver = (event) => {
787
+ if (!this.hasAttribute("open-on-hover") || !this.trigger || !event.composedPath().includes(this.trigger))
788
+ return;
789
+ this.clearHoverTimer();
790
+ this.hoverTimer = setTimeout(() => this.setOpen(true, { event, type: "menuTriggerMouseEnter" }), this.hoverDelay());
791
+ };
792
+ onPointerOut = (event) => {
793
+ if (!this.hasAttribute("open-on-hover") || !this.isOpen || !this.trigger || !event.composedPath().includes(this.trigger))
794
+ return;
795
+ const target = event.relatedTarget;
796
+ if (target instanceof Node && (this.contains(target) || this.shadowRoot?.contains(target) || this.panel?.contains(target)))
797
+ return;
798
+ this.clearHoverTimer();
799
+ this.hoverTimer = setTimeout(() => this.setOpen(false, { event, type: "menuTriggerMouseLeave" }), this.hoverDelay());
800
+ };
801
+ onPanelMouseOver = (event) => {
802
+ if (!this.panel || !event.composedPath().includes(this.panel))
803
+ return;
804
+ this.dispatchEvent(new CustomEvent("spui-menu-enter", { bubbles: true, composed: true, detail: { nativeEvent: event } }));
805
+ };
806
+ onMenuEnter = (event) => {
807
+ const target = event.target;
808
+ const path = event.composedPath();
809
+ if (!this.isOpen || target === this || path.includes(this.panel) || target instanceof Node && (this.contains(target) || this.shadowRoot?.contains(target) || this.panel?.contains(target)))
810
+ return;
811
+ const targetMenu = path.find((node) => node instanceof HTMLElement && node.localName === "spui-menu");
812
+ for (let ancestor = this.parentMenu(); ancestor; ancestor = ancestor.parentMenu())
813
+ if (ancestor === targetMenu)
814
+ return;
815
+ if (targetMenu?.contains(this))
816
+ return;
817
+ const nativeEvent = event.detail?.nativeEvent;
818
+ if (nativeEvent instanceof MouseEvent)
819
+ this.setOpen(false, { event: nativeEvent, type: "menuMouseEnter" });
820
+ };
821
+ onScrollOutside = (event) => {
822
+ if (!(this.hasAttribute("close-on-scroll") || this.contextEnabled()) || !this.isOpen)
823
+ return;
824
+ if (!event.composedPath().some(target => target === this || target === this.panel || target instanceof Node && (this.contains(target) || this.shadowRoot?.contains(target) || this.panel?.contains(target)))) {
825
+ this.setOpen(false, { event, type: "scrollOutside" });
826
+ }
827
+ };
828
+ onKeyDown = (event) => {
829
+ const direction = getComputedStyle(this).direction;
830
+ const forwardKey = direction === "rtl" ? "ArrowLeft" : "ArrowRight";
831
+ const backwardKey = direction === "rtl" ? "ArrowRight" : "ArrowLeft";
832
+ if (event.key === backwardKey && this.isOpen && this.parentMenu()) {
833
+ event.preventDefault();
834
+ event.stopPropagation();
835
+ this.setOpen(false, { event, type: "menuPopoverKeyDown", keyboard: true });
836
+ this.focusTrigger();
837
+ return;
838
+ }
839
+ if (event.key === forwardKey && this.isOpen) {
840
+ const active = this.menuItems()[this.activeIndex()];
841
+ const submenu = active?.parentElement?.localName === "spui-menu" ? active.parentElement : undefined;
842
+ if (submenu && submenu !== this) {
843
+ event.preventDefault();
844
+ event.stopPropagation();
845
+ submenu.setOpen(true, { event, type: "menuTriggerKeyDown", keyboard: true });
846
+ requestAnimationFrame(() => submenu.focusFirstItem());
847
+ return;
848
+ }
849
+ }
850
+ if (event.key === "Escape" && this.isOpen) {
851
+ event.preventDefault();
852
+ event.stopPropagation();
853
+ this.setOpen(false, { event, type: "menuPopoverKeyDown", keyboard: true });
854
+ this.focusTrigger();
855
+ return;
856
+ }
857
+ if (event.key === "Tab" && this.isOpen) {
858
+ this.setOpen(false, { event, type: "menuPopoverKeyDown", keyboard: true });
859
+ return;
860
+ }
861
+ if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Home" || event.key === "End") {
862
+ event.preventDefault();
863
+ const items = this.menuItems();
864
+ if (!items.length)
865
+ return;
866
+ if (!this.isOpen) {
867
+ const inTrigger = event.composedPath().includes(this.trigger);
868
+ this.setOpen(true, { event, type: inTrigger ? "menuTriggerKeyDown" : "menuPopoverKeyDown", keyboard: true });
869
+ this.focusItemAfterOpen(event.key === "ArrowUp" || event.key === "End" ? items.length - 1 : 0);
870
+ return;
871
+ }
872
+ const index = this.activeIndex();
873
+ this.focusItem(event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : index + (event.key === "ArrowDown" ? 1 : -1));
874
+ return;
875
+ }
876
+ if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
877
+ const character = event.key.toLocaleLowerCase(this.lang || undefined);
878
+ this.typeahead = this.typeahead === character ? character : this.typeahead + character;
879
+ this.clearTypeahead();
880
+ this.typeaheadTimer = setTimeout(() => { this.typeahead = ""; }, 500);
881
+ const items = this.menuItems();
882
+ const start = (this.activeIndex() + 1 + items.length) % items.length;
883
+ for (let offset = 0; offset < items.length; offset++) {
884
+ const index = (start + offset) % items.length;
885
+ const content = items[index].shadowRoot?.querySelector('slot[name="content"]');
886
+ const visibleText = content?.assignedNodes({ flatten: true }).map(node => node.textContent ?? "").join("") || items[index].textContent || "";
887
+ const label = visibleText.trim().toLocaleLowerCase(this.lang || undefined);
888
+ if (label.startsWith(this.typeahead)) {
889
+ event.preventDefault();
890
+ this.focusItem(index);
891
+ return;
892
+ }
893
+ }
894
+ }
895
+ };
896
+ focusFirstItem() { this.focusItem(0); }
897
+ parentMenu() { return this.parentElementOverride ?? this.parentElement?.closest("spui-menu"); }
898
+ onItemSelect = (event) => {
899
+ const item = event.target;
900
+ if (item?.getAttribute("kind") === "radio") {
901
+ const name = item.getAttribute("name");
902
+ if (name)
903
+ for (const option of (this.externalPanel ?? this).querySelectorAll(`spui-menu-item[kind=radio][name="${CSS.escape(name)}"]`))
904
+ option.toggleAttribute("checked", option === item);
905
+ }
906
+ if (event.detail?.persistOnClick || item?.hasAttribute("persist-on-click") || this.hasAttribute("persist-on-item-click"))
907
+ return;
908
+ this.setOpen(false, { event: event.detail?.event ?? new MouseEvent("click"), type: "menuItemClick" });
909
+ this.focusTrigger();
910
+ };
911
+ onNativeToggle = (event) => {
912
+ const next = event.newState === "open";
913
+ if (next === this.isOpen)
914
+ return;
915
+ this.isOpen = next;
916
+ this.toggleAttribute("open", next);
917
+ this.syncTrigger();
918
+ this.emitOpenChange({ event, type: "programmatic" });
919
+ };
920
+ focusTrigger() { const inner = this.trigger?.shadowRoot?.querySelector("button,[tabindex]"); if (inner)
921
+ inner.focus();
922
+ else
923
+ this.trigger?.focus(); }
924
+ setOpen(value, source = { event: new Event("programmatic"), type: "programmatic" }) {
925
+ const next = Boolean(value);
926
+ if (next === this.isOpen)
927
+ return;
928
+ this.isOpen = next;
929
+ if (!next)
930
+ for (const child of new Set([...this.querySelectorAll("spui-menu"), ...(this.externalPanel?.querySelectorAll("spui-menu") ?? [])]))
931
+ if (child.isOpen)
932
+ child.setOpen(false, source);
933
+ if (this.hasAttribute("open") !== next)
934
+ this.toggleAttribute("open", next);
935
+ this.syncVisibility();
936
+ this.emitOpenChange(source);
937
+ }
938
+ emitOpenChange(source = { event: new Event("programmatic"), type: "programmatic" }) { const detail = { open: this.isOpen, ...source, keyboard: source.keyboard ?? false, bubble: Boolean(this.parentMenu()) }; this.dispatchEvent(new CustomEvent("spui-open-change", { bubbles: true, composed: true, detail })); }
939
+ clearTypeahead() { if (this.typeaheadTimer !== undefined)
940
+ clearTimeout(this.typeaheadTimer); this.typeaheadTimer = undefined; }
941
+ clearHoverTimer() { if (this.hoverTimer !== undefined)
942
+ clearTimeout(this.hoverTimer); this.hoverTimer = undefined; }
943
+ }
944
+ customElements.define("spui-menu", SpuiMenu);
945
+ }
946
+ export function defineMenuBarItem() {
947
+ if (customElements.get("spui-menu-bar-item"))
948
+ return;
949
+ class SpuiMenuBarItem extends HTMLElement {
950
+ static observedAttributes = ["disabled", "aria-label", "aria-haspopup", "aria-controls", "aria-expanded"];
951
+ button;
952
+ connectedCallback() { if (!this.shadowRoot)
953
+ this.render();
954
+ else
955
+ this.sync(); }
956
+ attributeChangedCallback() { if (this.isConnected)
957
+ this.sync(); }
958
+ get tabStop() { return this.button?.tabIndex === 0; }
959
+ set tabStop(value) { if (this.button)
960
+ this.button.tabIndex = value ? 0 : -1; }
961
+ focus(options) { this.button?.focus(options); }
962
+ render() {
963
+ const shadow = this.attachShadow({ mode: "open" });
964
+ const style = document.createElement("style");
965
+ style.textContent = `${base}:host{display:inline-flex;min-width:0}button{box-sizing:border-box;display:flex;align-items:center;gap:8px;min-height:36px;padding:7px 12px;border:1px solid transparent;border-radius:var(--spui-menu-bar-item-radius,var(--spui-control-radius,6px));background:transparent;color:inherit;font:inherit;text-align:start;cursor:pointer}button:hover:not(:disabled),button:focus-visible{background:var(--spui-menu-bar-item-hover,var(--spui-ui-surface-hover,#f5f5f5))}button:focus-visible{outline:2px solid var(--spui-focus-ring,var(--spui-ui-accent,#0078d4));outline-offset:1px}button[aria-expanded=true]{background:var(--spui-menu-bar-item-open,var(--spui-ui-surface-secondary,#f0f0f0));border-color:var(--spui-menu-bar-item-border,var(--spui-ui-stroke,#d1d1d1))}button:disabled{opacity:.5;cursor:not-allowed}button[aria-haspopup=menu]::after{content:"▾";margin-inline-start:4px;color:var(--spui-ui-muted,var(--spui-semantic-body-subtext,#605e5c));font-size:.8em}[hidden]{display:none!important}@media(forced-colors:active){button:focus-visible{outline-color:Highlight}button[aria-expanded=true]{border-color:Highlight}}`;
966
+ this.button = document.createElement("button");
967
+ this.button.type = "button";
968
+ this.button.tabIndex = -1;
969
+ const slot = document.createElement("slot");
970
+ this.button.append(slot);
971
+ shadow.append(style, this.button);
972
+ this.sync();
973
+ }
974
+ sync() {
975
+ if (!this.button)
976
+ return;
977
+ this.button.disabled = this.hasAttribute("disabled");
978
+ this.button.setAttribute("role", "menuitem");
979
+ this.button.tabIndex = this.button.tabIndex < 0 ? -1 : this.button.tabIndex;
980
+ for (const name of ["aria-label", "aria-haspopup", "aria-controls", "aria-expanded"]) {
981
+ const value = this.getAttribute(name);
982
+ if (value === null)
983
+ this.button.removeAttribute(name);
984
+ else
985
+ this.button.setAttribute(name, value);
986
+ }
987
+ if (this.hasAttribute("disabled"))
988
+ this.button.setAttribute("aria-disabled", "true");
989
+ else
990
+ this.button.removeAttribute("aria-disabled");
991
+ }
992
+ }
993
+ customElements.define("spui-menu-bar-item", SpuiMenuBarItem);
994
+ }
995
+ export function defineMenuBar() {
996
+ if (customElements.get("spui-menu-bar"))
997
+ return;
998
+ defineMenu();
999
+ defineMenuBarItem();
1000
+ class SpuiMenuBar extends HTMLElement {
1001
+ slotElement;
1002
+ activeIndex = 0;
1003
+ connectedCallback() {
1004
+ if (!this.shadowRoot)
1005
+ this.render();
1006
+ this.setAttribute("role", "menubar");
1007
+ this.setAttribute("aria-orientation", "horizontal");
1008
+ if (!this.hasAttribute("aria-label"))
1009
+ this.setAttribute("aria-label", "Menu bar");
1010
+ this.slotElement.addEventListener("slotchange", this.syncItems);
1011
+ this.addEventListener("keydown", this.onKeyDown);
1012
+ this.addEventListener("focusin", this.onFocusIn);
1013
+ this.addEventListener("spui-open-change", this.onOpenChange);
1014
+ this.addEventListener("spui-menu-trigger-change", this.syncItems);
1015
+ this.syncItems();
1016
+ }
1017
+ disconnectedCallback() {
1018
+ this.slotElement?.removeEventListener("slotchange", this.syncItems);
1019
+ this.removeEventListener("keydown", this.onKeyDown);
1020
+ this.removeEventListener("focusin", this.onFocusIn);
1021
+ this.removeEventListener("spui-open-change", this.onOpenChange);
1022
+ this.removeEventListener("spui-menu-trigger-change", this.syncItems);
1023
+ }
1024
+ menus() {
1025
+ return [...this.querySelectorAll("spui-menu")].filter(menu => !menu.parentElement?.closest("spui-menu") && menu.parentElement?.closest("spui-menu-bar") === this);
1026
+ }
1027
+ items() { return this.menus().map(menu => menu.menuTrigger).filter((item) => Boolean(item)); }
1028
+ syncItems = () => {
1029
+ const items = this.items();
1030
+ const focused = items.findIndex(item => item === this.ownerDocument.activeElement || item.contains(this.ownerDocument.activeElement));
1031
+ if (focused >= 0)
1032
+ this.activeIndex = focused;
1033
+ else
1034
+ this.activeIndex = Math.min(this.activeIndex, Math.max(0, items.length - 1));
1035
+ items.forEach((item, index) => this.setTabStop(item, index === this.activeIndex));
1036
+ };
1037
+ setTabStop(item, value) {
1038
+ if ("tabStop" in item)
1039
+ item.tabStop = value;
1040
+ else {
1041
+ const focusTarget = item.shadowRoot?.querySelector("button,[tabindex]");
1042
+ if (focusTarget)
1043
+ focusTarget.tabIndex = value ? 0 : -1;
1044
+ else
1045
+ item.tabIndex = value ? 0 : -1;
1046
+ }
1047
+ }
1048
+ setActive(index, focus) {
1049
+ const items = this.items();
1050
+ if (!items.length)
1051
+ return;
1052
+ this.activeIndex = (index + items.length) % items.length;
1053
+ items.forEach((item, itemIndex) => this.setTabStop(item, itemIndex === this.activeIndex));
1054
+ if (focus)
1055
+ items[this.activeIndex].focus();
1056
+ }
1057
+ focusedIndex(event) { const active = this.ownerDocument.activeElement; const path = event?.composedPath() ?? []; return this.items().findIndex(item => path.includes(item) || item === active || item.contains(active)); }
1058
+ onFocusIn = (event) => {
1059
+ const path = event.composedPath();
1060
+ const index = this.items().findIndex(item => path.includes(item));
1061
+ if (index >= 0)
1062
+ this.setActive(index, false);
1063
+ };
1064
+ onKeyDown = (event) => {
1065
+ const index = this.focusedIndex(event);
1066
+ if (index < 0)
1067
+ return;
1068
+ const direction = getComputedStyle(this).direction;
1069
+ const forward = direction === "rtl" ? "ArrowLeft" : "ArrowRight";
1070
+ const backward = direction === "rtl" ? "ArrowRight" : "ArrowLeft";
1071
+ if (event.key === "Home" || event.key === "End") {
1072
+ event.preventDefault();
1073
+ this.setActive(event.key === "Home" ? 0 : this.items().length - 1, true);
1074
+ return;
1075
+ }
1076
+ if (event.key !== forward && event.key !== backward)
1077
+ return;
1078
+ event.preventDefault();
1079
+ const currentMenu = this.menus()[index];
1080
+ if (currentMenu?.open)
1081
+ currentMenu.open = false;
1082
+ this.setActive(index + (event.key === forward ? 1 : -1), true);
1083
+ };
1084
+ onOpenChange = (event) => {
1085
+ if (!event.detail?.open)
1086
+ return;
1087
+ const opened = event.target;
1088
+ if (!this.menus().includes(opened))
1089
+ return;
1090
+ for (const menu of this.menus())
1091
+ if (menu !== opened && menu.open)
1092
+ menu.open = false;
1093
+ };
1094
+ render() {
1095
+ const shadow = this.attachShadow({ mode: "open" });
1096
+ const style = document.createElement("style");
1097
+ style.textContent = `${base}:host{display:flex;align-items:center;gap:var(--spui-menu-bar-gap,4px);min-width:0;padding:var(--spui-menu-bar-padding,4px);border:1px solid var(--spui-menu-bar-border,var(--spui-ui-stroke,var(--spui-semantic-body-divider,#d1d1d1)));border-radius:var(--spui-menu-bar-radius,var(--spui-control-radius,8px));background:var(--spui-menu-bar-background,var(--spui-ui-surface,#fff));color:var(--spui-menu-bar-color,var(--spui-ui-text,#242424));font:inherit}slot{display:contents}@media(forced-colors:active){:host{border-color:CanvasText;color:CanvasText}}`;
1098
+ this.slotElement = document.createElement("slot");
1099
+ shadow.append(style, this.slotElement);
1100
+ }
1101
+ }
1102
+ customElements.define("spui-menu-bar", SpuiMenuBar);
1103
+ }