@getdom/studio 0.1.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 (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,412 @@
1
+ import { DomElementBase, defineDomElement, uid, createRoving } from './base.js';
2
+ import {
3
+ bindPopoverToggle,
4
+ bindFloatingReflow,
5
+ closePopoverPanel,
6
+ isPopoverOpen,
7
+ openPopoverPanel,
8
+ positionPopoverPanel,
9
+ preparePopoverPanel,
10
+ readFloatingPositionOptions,
11
+ syncFloatingScrollLock,
12
+ unbindFloatingReflow,
13
+ } from './popover-panel.js';
14
+
15
+ // Popover API for the menu layer (same as dom-popover); dropdown adds
16
+ // menu semantics, roving focus, and dom:select.
17
+ import './popover.js';
18
+
19
+ const menuItemSelector = [
20
+ '[role="menuitem"]',
21
+ '[role="menuitemcheckbox"]',
22
+ '[role="menuitemradio"]',
23
+ ].join(',');
24
+
25
+ // <dom-dropdown>
26
+ // <button slot="trigger">…</button>
27
+ // <div slot="menu">
28
+ // <button role="menuitem">…</button>
29
+ // </div>
30
+ // </dom-dropdown>
31
+ export class DomDropdownElement extends DomElementBase {
32
+ static get observedAttributes() {
33
+ return ['open', 'align', 'offset', 'placement', 'collision-padding', 'floating-mode', 'lock-scroll'];
34
+ }
35
+
36
+ static __doc = {
37
+ name: 'dom-dropdown',
38
+ description: 'A menu that opens from a button. The menu panel uses the same native Popover API as dom-popover (top layer, light-dismiss, Esc), plus menu keyboard navigation and dom:select.',
39
+ slots: [
40
+ { name: 'trigger', description: 'Button that opens the menu.' },
41
+ { name: 'menu', description: 'Menu panel (popover="auto"). Items inside must carry role="menuitem". data-value flows through dom:select.' },
42
+ ],
43
+ attributes: [
44
+ { name: 'open', type: 'boolean', description: 'Reflects open state (toggle by setting / removing).' },
45
+ { name: 'placement', type: "'top' | 'right' | 'bottom' | 'left' | '<side>-<align>'", description: 'Preferred placement before collision handling (default bottom).' },
46
+ { name: 'align', type: "'left' | 'right'", description: 'Horizontal alignment under the trigger (default left).' },
47
+ { name: 'offset', type: 'number', description: 'Gap in pixels between trigger and menu (default 4).' },
48
+ { name: 'collision-padding', type: 'number', description: 'Viewport padding used when flipping or shifting the menu (default 8).' },
49
+ { name: 'floating-mode', type: "'viewport' | 'anchor'", description: 'viewport keeps the menu inside the browser; anchor keeps it attached to the trigger while scrolling.' },
50
+ { name: 'lock-scroll', type: 'boolean', description: 'Lock document scrolling while the menu is open.' },
51
+ { name: 'data-focus-value', type: 'string', description: 'Preferred menu item value to focus when opening from the keyboard.' },
52
+ { name: 'data-menu-id', type: 'string', description: 'Id of an external (teleported) menu element. Used when the menu lives outside the host — e.g. portalled to <body>.' },
53
+ ],
54
+ events: [
55
+ { name: 'dom:open', description: 'Fired when the menu opens.' },
56
+ { name: 'dom:close', description: 'Fired when the menu closes.' },
57
+ { name: 'dom:select', payload: '{ value, event }', description: 'Fired when a menu item is chosen.' },
58
+ ],
59
+ keyboard: [
60
+ { keys: 'Enter / Space / ↓', action: 'Open menu (when trigger is focused).' },
61
+ { keys: '↑ / ↓', action: 'Move active item.' },
62
+ { keys: 'Home / End', action: 'Jump to first / last item.' },
63
+ { keys: 'Enter', action: 'Select active item.' },
64
+ { keys: 'Esc / Tab', action: 'Close menu and return focus.' },
65
+ ],
66
+ example: `<dom-dropdown>
67
+ <button slot="trigger">Account ▾</button>
68
+ <div slot="menu">
69
+ <button role="menuitem" data-value="profile">Profile</button>
70
+ <button role="menuitem" data-value="billing">Billing</button>
71
+ <button role="menuitem" data-value="signout">Sign out</button>
72
+ </div>
73
+ </dom-dropdown>
74
+
75
+ <script type="module">
76
+ import '@getdom/studio/headless';
77
+ document.querySelector('dom-dropdown')
78
+ .addEventListener('dom:select', (e) => console.log(e.detail.value));
79
+ <\/script>`,
80
+ };
81
+
82
+ constructor() {
83
+ super();
84
+ this._open = false;
85
+ this._roving = null;
86
+ this._unbindToggle = null;
87
+ this._reflowOff = null;
88
+ this._onKey = this._onKey.bind(this);
89
+ this._onCompositeKey = this._onCompositeKey.bind(this);
90
+ this._itemClickHandlers = [];
91
+ this._scrollLocked = false;
92
+ this._dismissOff = null;
93
+ this._dismissBindFrame = 0;
94
+ this._focusOnOpen = false;
95
+ this._focusPosition = 'preferred';
96
+ }
97
+
98
+ _positionOpts() {
99
+ return readFloatingPositionOptions(this, { offset: 4 });
100
+ }
101
+
102
+ _bindReflow() {
103
+ bindFloatingReflow(this, this._menu, this._trigger, this._positionOpts());
104
+ }
105
+
106
+ _unbindReflow() {
107
+ unbindFloatingReflow(this);
108
+ }
109
+
110
+ _syncScrollLock(next) {
111
+ syncFloatingScrollLock(this, this._menu, next);
112
+ }
113
+
114
+ connectedCallback() {
115
+ this._trigger = this.querySelector('[slot="trigger"], [data-trigger]');
116
+ this._menu = this._resolveMenu();
117
+ if (!this._trigger) return;
118
+
119
+ if (this._menu) this._prepareMenu();
120
+
121
+ this._trigger.setAttribute('aria-haspopup', this._menu?.dataset.haspopup || 'menu');
122
+ this._trigger.setAttribute('aria-expanded', 'false');
123
+
124
+ this.on(this._trigger, 'click', () => {
125
+ this._focusOnOpen = false;
126
+ this._focusPosition = 'preferred';
127
+ this.toggle();
128
+ });
129
+ this.on(this._trigger, 'keydown', (e) => {
130
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Enter' || e.key === ' ') {
131
+ e.preventDefault();
132
+ const position = e.key === 'ArrowUp' ? 'last' : 'preferred';
133
+ if (this._open || isPopoverOpen(this._menu)) {
134
+ this._focusMenuStart(position, true);
135
+ return;
136
+ }
137
+ this._focusOnOpen = true;
138
+ this._focusPosition = position;
139
+ this.open = true;
140
+ }
141
+ });
142
+
143
+ if (this.hasAttribute('open')) this.open = true;
144
+ }
145
+
146
+ _resolveMenu() {
147
+ const inner = this.querySelector('[slot="menu"], [data-menu]');
148
+ if (inner) return inner;
149
+ const id = this.dataset.menuId;
150
+ return id ? this.ownerDocument?.getElementById(id) : null;
151
+ }
152
+
153
+ _prepareMenu() {
154
+ const id = uid('dd');
155
+ this._menu.id = this._menu.id || `${id}-menu`;
156
+ if (this._isCompositePanel()) {
157
+ if (this._menu.getAttribute('role') === 'menu') this._menu.removeAttribute('role');
158
+ this._trigger?.setAttribute('aria-haspopup', this._menu.dataset.haspopup || 'true');
159
+ } else {
160
+ this._menu.setAttribute('role', 'menu');
161
+ this._trigger?.setAttribute('aria-haspopup', 'menu');
162
+ }
163
+ if (!this._menu.classList.contains('dom-dropdown-menu')) {
164
+ this._menu.classList.add('dom-dropdown-menu');
165
+ }
166
+ preparePopoverPanel(this._menu, this._trigger);
167
+ this._menu.setAttribute('popover', 'manual');
168
+ this._unbindToggle?.();
169
+ this._unbindToggle = bindPopoverToggle(this._menu, (open) => this._syncOpen(open, true));
170
+ }
171
+
172
+ _ensureMenu() {
173
+ if (!this._menu || !this._menu.isConnected) {
174
+ this._menu = this._resolveMenu();
175
+ if (this._menu) this._prepareMenu();
176
+ }
177
+ return this._menu;
178
+ }
179
+
180
+ _isCompositePanel() {
181
+ return this._menu?.dataset.compositePanel === 'true';
182
+ }
183
+
184
+ _focusValue() {
185
+ const value = this._menu?.dataset.focusValue ?? this.dataset.focusValue;
186
+ return value == null || value === '' ? null : String(value);
187
+ }
188
+
189
+ _focusableCompositeElements() {
190
+ const preferredSelector = [
191
+ '[data-autofocus]',
192
+ '[autofocus]',
193
+ '[tabindex="0"]',
194
+ '[aria-selected="true"]',
195
+ ].join(',');
196
+ const selector = [
197
+ menuItemSelector,
198
+ 'button:not([disabled])',
199
+ '[href]',
200
+ 'input:not([disabled])',
201
+ 'select:not([disabled])',
202
+ 'textarea:not([disabled])',
203
+ '[tabindex]:not([tabindex="-1"])',
204
+ ].join(',');
205
+ const findFocusable = (query) => Array.from(this._menu?.querySelectorAll(query) || [])
206
+ .find((element) => element.tabIndex >= 0 && element.getAttribute('aria-disabled') !== 'true');
207
+ return {
208
+ preferred: findFocusable(preferredSelector),
209
+ all: Array.from(this._menu?.querySelectorAll(selector) || [])
210
+ .filter((element) => element.tabIndex >= 0 && element.getAttribute('aria-disabled') !== 'true'),
211
+ };
212
+ }
213
+
214
+ _preferredElement(elements, position = 'preferred') {
215
+ if (!elements.length) return null;
216
+ if (position === 'last') return elements[elements.length - 1];
217
+ const focusValue = this._focusValue();
218
+ if (focusValue != null) {
219
+ const byValue = elements.find((element) => element.dataset?.value === focusValue);
220
+ if (byValue) return byValue;
221
+ }
222
+ return elements.find((element) => element.hasAttribute('data-autofocus') || element.hasAttribute('autofocus'))
223
+ || elements.find((element) => element.getAttribute('aria-selected') === 'true')
224
+ || elements.find((element) => element.tabIndex === 0)
225
+ || elements[0];
226
+ }
227
+
228
+ _focusCompositeElement(position = 'preferred', force = false) {
229
+ if (!force && this._menu?.dataset.autofocusPanel === 'false') return;
230
+ const { preferred, all } = this._focusableCompositeElements();
231
+ const target = position === 'preferred'
232
+ ? (preferred || this._preferredElement(all, position))
233
+ : this._preferredElement(all, position);
234
+ requestAnimationFrame(() => target?.focus());
235
+ }
236
+
237
+ _menuItems() {
238
+ return Array.from(this._menu.querySelectorAll(menuItemSelector))
239
+ .filter((item) => item.closest('dom-menu') === this._menu || !item.closest('dom-menu'))
240
+ .filter((item) => !item.hasAttribute('disabled') && item.getAttribute('aria-disabled') !== 'true');
241
+ }
242
+
243
+ _setupMenuInteractions({ focus = false, position = 'preferred' } = {}) {
244
+ if (this._isCompositePanel()) {
245
+ this._menu.addEventListener('keydown', this._onCompositeKey);
246
+ this._focusCompositeElement(position, focus);
247
+ return;
248
+ }
249
+ const items = this._menuItems();
250
+ this._roving = createRoving({ items, orientation: 'vertical', loop: true });
251
+ const active = Math.max(0, items.indexOf(this._preferredElement(items, position)));
252
+ items.forEach((el, i) => { el.tabIndex = i === active ? 0 : -1; });
253
+ this._roving.setActive(active, { focus, select: false });
254
+ this._menu.addEventListener('keydown', this._onKey);
255
+ this._itemClickHandlers = items.map((el) => {
256
+ const handler = (ev) => this._select(el, ev);
257
+ el.addEventListener('click', handler);
258
+ return () => el.removeEventListener('click', handler);
259
+ });
260
+ }
261
+
262
+ _teardownMenuInteractions() {
263
+ this._menu?.removeEventListener('keydown', this._onKey);
264
+ this._menu?.removeEventListener('keydown', this._onCompositeKey);
265
+ for (const off of this._itemClickHandlers) off();
266
+ this._itemClickHandlers = [];
267
+ this._roving = null;
268
+ this._trigger?.focus();
269
+ }
270
+
271
+ _focusMenuStart(position = 'preferred', force = false) {
272
+ if (!this._ensureMenu()) return;
273
+ if (this._isCompositePanel()) {
274
+ this._focusCompositeElement(position, force);
275
+ return;
276
+ }
277
+ const items = this._menuItems();
278
+ const target = this._preferredElement(items, position);
279
+ const index = Math.max(0, items.indexOf(target));
280
+ this._roving?.setActive(index, { focus: true, select: false });
281
+ }
282
+
283
+ _placeMenu() {
284
+ const r = this._trigger.getBoundingClientRect();
285
+ this._menu.style.minWidth = `${r.width}px`;
286
+ positionPopoverPanel(this._menu, this._trigger, this._positionOpts());
287
+ }
288
+
289
+ _bindDismiss() {
290
+ this._unbindDismiss();
291
+ this._dismissBindFrame = requestAnimationFrame(() => {
292
+ const isInside = (event) => {
293
+ const path = event.composedPath?.() || [];
294
+ return path.includes(this._menu) || path.includes(this._trigger);
295
+ };
296
+ const onPointerDown = (event) => {
297
+ if (!isPopoverOpen(this._menu) || isInside(event)) return;
298
+ this._syncOpen(false, false);
299
+ };
300
+ const onKeydown = (event) => {
301
+ if (event.key !== 'Escape' || !isPopoverOpen(this._menu)) return;
302
+ event.preventDefault();
303
+ this._syncOpen(false, false);
304
+ };
305
+ document.addEventListener('pointerdown', onPointerDown, true);
306
+ document.addEventListener('keydown', onKeydown, true);
307
+ this._dismissOff = () => {
308
+ document.removeEventListener('pointerdown', onPointerDown, true);
309
+ document.removeEventListener('keydown', onKeydown, true);
310
+ };
311
+ });
312
+ }
313
+
314
+ _unbindDismiss() {
315
+ if (this._dismissBindFrame) {
316
+ cancelAnimationFrame(this._dismissBindFrame);
317
+ this._dismissBindFrame = 0;
318
+ }
319
+ this._dismissOff?.();
320
+ this._dismissOff = null;
321
+ }
322
+
323
+ _syncOpen(next, fromPopover = false) {
324
+ if (!this._ensureMenu()) return;
325
+ if (next && this._open) {
326
+ this._placeMenu();
327
+ return;
328
+ }
329
+ if (!next && !this._open) return;
330
+
331
+ if (next) {
332
+ const focus = this._focusOnOpen;
333
+ const position = this._focusPosition || 'preferred';
334
+ this._focusOnOpen = false;
335
+ this._focusPosition = 'preferred';
336
+ if (!fromPopover) {
337
+ openPopoverPanel(this._menu, this._trigger, this._positionOpts(), () => this._placeMenu());
338
+ } else {
339
+ this._placeMenu();
340
+ }
341
+ this._bindReflow();
342
+ this._setupMenuInteractions({ focus, position });
343
+ this._syncScrollLock(true);
344
+ this._bindDismiss();
345
+ } else {
346
+ this._unbindReflow();
347
+ this._unbindDismiss();
348
+ this._teardownMenuInteractions();
349
+ this._syncScrollLock(false);
350
+ if (!fromPopover) closePopoverPanel(this._menu);
351
+ }
352
+
353
+ this._open = next;
354
+ this.setBoolAttr('open', next);
355
+ this._trigger?.setAttribute('aria-expanded', String(next));
356
+ this.emit(next ? 'dom:open' : 'dom:close');
357
+ }
358
+
359
+ get open() { return this._open; }
360
+ set open(value) {
361
+ const next = !!value;
362
+ if (next === this._open) return;
363
+ this._syncOpen(next, false);
364
+ }
365
+
366
+ toggle() {
367
+ if (!this._ensureMenu()) return;
368
+ this._syncOpen(!isPopoverOpen(this._menu), false);
369
+ }
370
+
371
+ _select(el, ev) {
372
+ this.emit('dom:select', { value: el.dataset.value ?? el.textContent.trim(), event: ev });
373
+ this.open = false;
374
+ }
375
+
376
+ _onCompositeKey(e) {
377
+ if (e.key === 'Escape' || e.key === 'Tab') {
378
+ if (e.key === 'Escape') e.preventDefault();
379
+ this.open = false;
380
+ }
381
+ }
382
+
383
+ _onKey(e) {
384
+ this._roving?.onKey(e);
385
+ if (e.key === 'Escape' || e.key === 'Tab') {
386
+ if (e.key === 'Escape') e.preventDefault();
387
+ this.open = false;
388
+ }
389
+ }
390
+
391
+ attributeChangedCallback(name) {
392
+ if (!this._open || !this._menu || !this._trigger) return;
393
+ if (name === 'lock-scroll') {
394
+ this._syncScrollLock(true);
395
+ }
396
+ if (name === 'align' || name === 'offset' || name === 'placement' || name === 'collision-padding' || name === 'floating-mode') {
397
+ this._placeMenu();
398
+ }
399
+ }
400
+
401
+ disconnectedCallback() {
402
+ this._teardownMenuInteractions();
403
+ this._unbindToggle?.();
404
+ this._unbindToggle = null;
405
+ this._unbindReflow();
406
+ this._unbindDismiss();
407
+ this._syncScrollLock(false);
408
+ super.disconnectedCallback();
409
+ }
410
+ }
411
+
412
+ defineDomElement('dom-dropdown', DomDropdownElement);
@@ -0,0 +1,118 @@
1
+ const effectTimers = new WeakMap();
2
+
3
+ export const FLOATING_EFFECT_NAME = 'scale-fade';
4
+ export const FLOATING_EFFECT_CLASS = 'dom-floating-transition';
5
+ export const FLOATING_EFFECT_DURATION_MS = 320;
6
+
7
+ function viewFor(element) {
8
+ return element?.ownerDocument?.defaultView || window;
9
+ }
10
+
11
+ function flushFloatingEffectStyle(element) {
12
+ if (!element) return;
13
+ const style = viewFor(element).getComputedStyle(element);
14
+ element.getBoundingClientRect();
15
+ void style.opacity;
16
+ void style.transform;
17
+ }
18
+
19
+ export function prepareFloatingEffect(element, name = FLOATING_EFFECT_NAME) {
20
+ if (!element) return;
21
+ if (!element.dataset.floatingEffect && !element.classList.contains(FLOATING_EFFECT_CLASS)) return;
22
+ element.dataset.floatingEffect ||= name;
23
+ element.classList.add(FLOATING_EFFECT_CLASS);
24
+ }
25
+
26
+ export function hasFloatingEffect(element) {
27
+ return Boolean(element && (
28
+ element.dataset.floatingEffect
29
+ || element.classList.contains(FLOATING_EFFECT_CLASS)
30
+ ));
31
+ }
32
+
33
+ export function clearFloatingEffectTimer(element) {
34
+ if (!element) return;
35
+ const timer = effectTimers.get(element);
36
+ if (!timer) return;
37
+ viewFor(element).clearTimeout(timer);
38
+ effectTimers.delete(element);
39
+ }
40
+
41
+ export function resetFloatingEffectState(element) {
42
+ if (!element) return;
43
+ clearFloatingEffectTimer(element);
44
+ delete element.dataset.floatingOpen;
45
+ delete element.dataset.entering;
46
+ delete element.dataset.leaving;
47
+ delete element.dataset.animatePlacement;
48
+ }
49
+
50
+ export function beginFloatingEffectEnter(element, {
51
+ shouldContinue = () => true,
52
+ beforeMeasure,
53
+ beforeFrame,
54
+ delayFrames = 0,
55
+ durationMs = FLOATING_EFFECT_DURATION_MS,
56
+ animatePlacement = true,
57
+ } = {}) {
58
+ if (!element || !hasFloatingEffect(element)) return false;
59
+ const view = viewFor(element);
60
+ clearFloatingEffectTimer(element);
61
+ delete element.dataset.leaving;
62
+ element.dataset.floatingEffect ||= FLOATING_EFFECT_NAME;
63
+ element.dataset.floatingOpen = 'true';
64
+ element.toggleAttribute('data-animate-placement', animatePlacement);
65
+ element.dataset.entering = 'from';
66
+ beforeMeasure?.();
67
+ flushFloatingEffectStyle(element);
68
+ const enter = () => {
69
+ if (!shouldContinue() || element.dataset.entering !== 'from') return;
70
+ beforeFrame?.();
71
+ element.dataset.entering = 'to';
72
+ const timer = view.setTimeout(() => {
73
+ if (element.dataset.entering === 'to') delete element.dataset.entering;
74
+ effectTimers.delete(element);
75
+ }, durationMs);
76
+ effectTimers.set(element, timer);
77
+ };
78
+ const queueEnter = (frames) => {
79
+ view.requestAnimationFrame(() => {
80
+ if (frames > 0) queueEnter(frames - 1);
81
+ else enter();
82
+ });
83
+ };
84
+ queueEnter(delayFrames);
85
+ return true;
86
+ }
87
+
88
+ export function beginFloatingEffectLeave(element, {
89
+ finish,
90
+ shouldContinue = () => true,
91
+ immediate = false,
92
+ durationMs = FLOATING_EFFECT_DURATION_MS,
93
+ } = {}) {
94
+ if (!element) return;
95
+ const view = viewFor(element);
96
+ clearFloatingEffectTimer(element);
97
+ delete element.dataset.entering;
98
+ if (immediate || !hasFloatingEffect(element)) {
99
+ resetFloatingEffectState(element);
100
+ finish?.();
101
+ return;
102
+ }
103
+ element.dataset.floatingEffect ||= FLOATING_EFFECT_NAME;
104
+ element.dataset.floatingOpen = 'true';
105
+ element.dataset.leaving = 'from';
106
+ flushFloatingEffectStyle(element);
107
+ view.requestAnimationFrame(() => {
108
+ if (!shouldContinue() || element.dataset.leaving !== 'from') return;
109
+ element.dataset.leaving = 'to';
110
+ });
111
+ const timer = view.setTimeout(() => {
112
+ effectTimers.delete(element);
113
+ if (!shouldContinue()) return;
114
+ resetFloatingEffectState(element);
115
+ finish?.();
116
+ }, durationMs);
117
+ effectTimers.set(element, timer);
118
+ }
@@ -0,0 +1,34 @@
1
+ /* eslint-disable */
2
+ // Generated by scripts/generate-types.mjs. Do not edit by hand.
3
+ export type FloatingSide = 'top' | 'right' | 'bottom' | 'left';
4
+ export type FloatingAlign = 'start' | 'center' | 'end' | 'left' | 'right';
5
+ export type FloatingMode = 'viewport' | 'anchor' | 'element';
6
+ export interface FloatingPositionOptions {
7
+ placement?: FloatingSide | `${FloatingSide}-${'start' | 'center' | 'end'}`;
8
+ align?: FloatingAlign;
9
+ offset?: number;
10
+ padding?: number;
11
+ mode?: FloatingMode;
12
+ floatingMode?: FloatingMode;
13
+ flip?: boolean;
14
+ shift?: boolean;
15
+ strategy?: 'fixed' | 'absolute';
16
+ }
17
+ export interface FloatingPositionResult {
18
+ x: number;
19
+ y: number;
20
+ mode: 'viewport' | 'anchor';
21
+ side: FloatingSide;
22
+ align: 'start' | 'center' | 'end';
23
+ placement: string;
24
+ flipped: boolean;
25
+ collision: Record<FloatingSide, number>;
26
+ collisionSides: Array<FloatingSide>;
27
+ overflow: Record<FloatingSide, number>;
28
+ overflowSides: Array<FloatingSide>;
29
+ availableWidth: number;
30
+ availableHeight: number;
31
+ }
32
+ export declare function computeFloatingPosition(referenceEl: Element, floatingEl: HTMLElement, options?: FloatingPositionOptions): FloatingPositionResult | null;
33
+ export declare function applyFloatingPosition(referenceEl: Element, floatingEl: HTMLElement, options?: FloatingPositionOptions): FloatingPositionResult | null;
34
+ export declare function autoUpdateFloating(referenceEl: Element, floatingEl: HTMLElement, update: () => void): () => void;