@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,391 @@
1
+ import { DomElementBase, defineDomElement } 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
+ const TRIGGER_SELECTOR = [
16
+ 'button',
17
+ 'a[href]',
18
+ 'input:not([type="hidden"])',
19
+ 'select',
20
+ 'textarea',
21
+ '[role="button"]',
22
+ '[role="link"]',
23
+ '[tabindex]:not([tabindex="-1"])',
24
+ ].join(',');
25
+
26
+ function isNativeTrigger(element) {
27
+ return ['BUTTON', 'A', 'INPUT', 'SELECT', 'TEXTAREA'].includes(element?.tagName);
28
+ }
29
+
30
+ function resolveTriggerElement(root) {
31
+ if (!root) return null;
32
+ if (root.matches(TRIGGER_SELECTOR)) return root;
33
+ return root.querySelector(TRIGGER_SELECTOR) || root.firstElementChild || root;
34
+ }
35
+
36
+ // <dom-popover>
37
+ // <button slot="trigger">Open</button>
38
+ // <div slot="panel">…</div>
39
+ // </dom-popover>
40
+ //
41
+ // Built on the native HTML Popover API (`popover="auto"`). The browser handles
42
+ // top-layer promotion, light-dismiss, and Esc — we just wire the trigger and
43
+ // surface our usual dom:open / dom:close events.
44
+ export class DomPopoverElement extends DomElementBase {
45
+ static get observedAttributes() {
46
+ return ['open', 'align', 'offset', 'placement', 'collision-padding', 'floating-mode', 'flip', 'lock-scroll', 'trigger', 'hover-close-delay', 'data-trigger-id'];
47
+ }
48
+
49
+ static __doc = {
50
+ name: 'dom-popover',
51
+ description: 'Floating panel anchored to a trigger. Built on the native HTML Popover API — top layer, light-dismiss and Esc are handled by the browser.',
52
+ slots: [
53
+ { name: 'trigger', description: 'Element that toggles the panel when clicked.' },
54
+ { name: 'panel', description: 'Panel content. Gets popover="auto" applied automatically.' },
55
+ ],
56
+ attributes: [
57
+ { name: 'open', type: 'boolean', description: 'Reflects open state.' },
58
+ { name: 'placement', type: "'top' | 'right' | 'bottom' | 'left' | '<side>-<align>'", description: 'Preferred placement before collision handling (default bottom).' },
59
+ { name: 'align', type: "'left' | 'right' | 'center'", description: 'Horizontal alignment under the trigger (default left).' },
60
+ { name: 'offset', type: 'number', description: 'Gap in pixels between trigger and panel (default 8).' },
61
+ { name: 'collision-padding', type: 'number', description: 'Viewport padding used when flipping or shifting the panel (default 8).' },
62
+ { name: 'floating-mode', type: "'viewport' | 'anchor'", description: 'viewport keeps the panel inside the browser; anchor keeps it attached to the trigger while scrolling.' },
63
+ { name: 'flip', type: 'boolean', description: 'Allow the panel to flip to the opposite side when it would collide with the viewport.' },
64
+ { name: 'lock-scroll', type: 'boolean', description: 'Lock document scrolling while the panel is open.' },
65
+ { name: 'trigger', type: "'click' | 'hover' | 'hover-click'", description: 'How the trigger opens the panel. hover-click opens on hover and pins open on click.' },
66
+ { name: 'hover-close-delay', type: 'number', description: 'Delay before closing a hover-triggered panel after pointer/focus leaves.' },
67
+ { name: 'data-trigger-id', type: 'string', description: 'Id of an external trigger element.' },
68
+ { name: 'data-panel-id', type: 'string', description: 'Id of an external (teleported) panel element.' },
69
+ ],
70
+ events: [
71
+ { name: 'dom:open', description: 'Fired when the panel opens.' },
72
+ { name: 'dom:close', description: 'Fired when the panel closes.' },
73
+ ],
74
+ keyboard: [
75
+ { keys: 'Click trigger', action: 'Toggles the panel.' },
76
+ { keys: 'Click outside', action: 'Light-dismiss via the native popover API.' },
77
+ { keys: 'Esc', action: 'Closes the panel.' },
78
+ ],
79
+ example: `<dom-popover>
80
+ <button slot="trigger">More</button>
81
+ <div slot="panel">
82
+ <p>Anything you like — text, buttons, even forms.</p>
83
+ </div>
84
+ </dom-popover>`,
85
+ };
86
+
87
+ constructor() {
88
+ super();
89
+ this._open = false;
90
+ this._unbindToggle = null;
91
+ this._reflowOff = null;
92
+ this._triggerOff = null;
93
+ this._panelHoverOff = null;
94
+ this._scrollLocked = false;
95
+ this._dismissOff = null;
96
+ this._dismissBindFrame = 0;
97
+ this._hoverCloseTimer = 0;
98
+ this._hoverPinned = false;
99
+ }
100
+
101
+ _positionOpts() {
102
+ return readFloatingPositionOptions(this, { offset: 8, flip: true });
103
+ }
104
+
105
+ _bindReflow() {
106
+ bindFloatingReflow(this, this._panel, this._trigger, this._positionOpts());
107
+ }
108
+
109
+ _unbindReflow() {
110
+ unbindFloatingReflow(this);
111
+ }
112
+
113
+ _bindDismiss() {
114
+ this._unbindDismiss();
115
+ this._dismissBindFrame = requestAnimationFrame(() => {
116
+ const onPointerDown = (event) => {
117
+ const target = event.target;
118
+ if (this._panel?.contains(target) || this._trigger?.contains(target)) return;
119
+ this._syncOpen(false, false);
120
+ };
121
+ const onKeydown = (event) => {
122
+ if (event.key !== 'Escape') return;
123
+ event.preventDefault();
124
+ this._syncOpen(false, false);
125
+ };
126
+ document.addEventListener('pointerdown', onPointerDown, true);
127
+ document.addEventListener('keydown', onKeydown, true);
128
+ this._dismissOff = () => {
129
+ document.removeEventListener('pointerdown', onPointerDown, true);
130
+ document.removeEventListener('keydown', onKeydown, true);
131
+ };
132
+ });
133
+ }
134
+
135
+ _unbindDismiss() {
136
+ if (this._dismissBindFrame) {
137
+ cancelAnimationFrame(this._dismissBindFrame);
138
+ this._dismissBindFrame = 0;
139
+ }
140
+ this._dismissOff?.();
141
+ this._dismissOff = null;
142
+ }
143
+
144
+ _triggerMode() {
145
+ return this.getAttribute('trigger') || 'click';
146
+ }
147
+
148
+ _isHoverMode() {
149
+ return this._triggerMode() === 'hover' || this._triggerMode() === 'hover-click';
150
+ }
151
+
152
+ _isHoverClickMode() {
153
+ return this._triggerMode() === 'hover-click';
154
+ }
155
+
156
+ _hoverCloseDelay() {
157
+ return Math.max(0, Number(this.getAttribute('hover-close-delay') || 140));
158
+ }
159
+
160
+ _clearHoverClose() {
161
+ if (!this._hoverCloseTimer) return;
162
+ clearTimeout(this._hoverCloseTimer);
163
+ this._hoverCloseTimer = 0;
164
+ }
165
+
166
+ _openFromHover() {
167
+ this._clearHoverClose();
168
+ this._syncOpen(true, false);
169
+ }
170
+
171
+ _closeFromHoverSoon() {
172
+ if (this._hoverPinned) return;
173
+ this._clearHoverClose();
174
+ this._hoverCloseTimer = setTimeout(() => {
175
+ this._hoverCloseTimer = 0;
176
+ if (this._hoverPinned) return;
177
+ this._syncOpen(false, false);
178
+ }, this._hoverCloseDelay());
179
+ }
180
+
181
+ _pinFromClick() {
182
+ this._clearHoverClose();
183
+ this._hoverPinned = true;
184
+ this._syncOpen(true, false);
185
+ }
186
+
187
+ _bindPanelHover() {
188
+ this._unbindPanelHover();
189
+ if (!this._isHoverMode() || !this._panel) return;
190
+ const offPointerEnter = this.on(this._panel, 'pointerenter', () => this._clearHoverClose());
191
+ const offPointerLeave = this.on(this._panel, 'pointerleave', () => this._closeFromHoverSoon());
192
+ const offMouseEnter = this.on(this._panel, 'mouseenter', () => this._clearHoverClose());
193
+ const offMouseLeave = this.on(this._panel, 'mouseleave', () => this._closeFromHoverSoon());
194
+ const offFocusIn = this.on(this._panel, 'focusin', () => this._clearHoverClose());
195
+ const offFocusOut = this.on(this._panel, 'focusout', () => this._closeFromHoverSoon());
196
+ this._panelHoverOff = () => {
197
+ offPointerEnter?.();
198
+ offPointerLeave?.();
199
+ offMouseEnter?.();
200
+ offMouseLeave?.();
201
+ offFocusIn?.();
202
+ offFocusOut?.();
203
+ };
204
+ }
205
+
206
+ _unbindPanelHover() {
207
+ this._panelHoverOff?.();
208
+ this._panelHoverOff = null;
209
+ }
210
+
211
+ _syncScrollLock(next) {
212
+ syncFloatingScrollLock(this, this._panel, next);
213
+ }
214
+
215
+ connectedCallback() {
216
+ this._trigger = this._resolveTrigger();
217
+ this._panel = this._resolvePanel();
218
+ if (!this._trigger) return;
219
+
220
+ if (this._panel) this._preparePanel();
221
+ this._prepareTrigger();
222
+
223
+ if (this.hasAttribute('open')) this.open = true;
224
+ }
225
+
226
+ _prepareTrigger() {
227
+ this._triggerOff?.();
228
+ this._triggerOff = null;
229
+ this._unbindPanelHover();
230
+ if (!this._trigger) return;
231
+ this._trigger.setAttribute('aria-haspopup', 'dialog');
232
+ this._trigger.setAttribute('aria-expanded', String(this._open));
233
+ if (!isNativeTrigger(this._trigger) && !this._trigger.hasAttribute('role')) {
234
+ this._trigger.setAttribute('role', 'button');
235
+ }
236
+ if (!isNativeTrigger(this._trigger) && !this._trigger.hasAttribute('tabindex')) {
237
+ this._trigger.setAttribute('tabindex', '0');
238
+ }
239
+ if (this._isHoverMode()) {
240
+ const offPointerEnter = this.on(this._trigger, 'pointerenter', () => this._openFromHover());
241
+ const offPointerLeave = this.on(this._trigger, 'pointerleave', () => this._closeFromHoverSoon());
242
+ const offMouseEnter = this.on(this._trigger, 'mouseenter', () => this._openFromHover());
243
+ const offMouseLeave = this.on(this._trigger, 'mouseleave', () => this._closeFromHoverSoon());
244
+ const offFocusIn = this.on(this._trigger, 'focusin', () => this._openFromHover());
245
+ const offFocusOut = this.on(this._trigger, 'focusout', () => this._closeFromHoverSoon());
246
+ const offClick = this._isHoverClickMode() ? this.on(this._trigger, 'click', () => this._pinFromClick()) : null;
247
+ const offKeydown = this._isHoverClickMode() ? this.on(this._trigger, 'keydown', (event) => {
248
+ if (event.key !== 'Enter' && event.key !== ' ') return;
249
+ event.preventDefault();
250
+ this._pinFromClick();
251
+ }) : null;
252
+ this._bindPanelHover();
253
+ this._triggerOff = () => {
254
+ offPointerEnter?.();
255
+ offPointerLeave?.();
256
+ offMouseEnter?.();
257
+ offMouseLeave?.();
258
+ offFocusIn?.();
259
+ offFocusOut?.();
260
+ offClick?.();
261
+ offKeydown?.();
262
+ };
263
+ return;
264
+ }
265
+
266
+ const offClick = this.on(this._trigger, 'click', () => this.toggle());
267
+ const offKeydown = this.on(this._trigger, 'keydown', (event) => {
268
+ if (event.key !== 'Enter' && event.key !== ' ') return;
269
+ event.preventDefault();
270
+ this.toggle();
271
+ });
272
+ this._triggerOff = () => {
273
+ offClick?.();
274
+ offKeydown?.();
275
+ };
276
+ }
277
+
278
+ _resolveTrigger() {
279
+ const id = this.dataset.triggerId;
280
+ if (id) return resolveTriggerElement(document.getElementById(id));
281
+ const root = this.querySelector('[slot="trigger"], [data-trigger]');
282
+ return resolveTriggerElement(root);
283
+ }
284
+
285
+ _preparePanel() {
286
+ if (!this._panel.classList.contains('dom-popover-panel')) {
287
+ this._panel.classList.add('dom-popover-panel');
288
+ }
289
+ preparePopoverPanel(this._panel, this._trigger);
290
+ this._panel.setAttribute('popover', 'manual');
291
+ this._unbindToggle?.();
292
+ this._unbindToggle = bindPopoverToggle(this._panel, (open) => this._syncOpen(open, true));
293
+ this._bindPanelHover();
294
+ }
295
+
296
+ _resolvePanel() {
297
+ const inner = this.querySelector('[slot="panel"], [data-panel]');
298
+ if (inner) return inner;
299
+ const id = this.dataset.panelId;
300
+ return id ? document.getElementById(id) : null;
301
+ }
302
+
303
+ _ensurePanel() {
304
+ if (!this._trigger || !this._trigger.isConnected) {
305
+ this._trigger = this._resolveTrigger();
306
+ if (this._trigger) this._prepareTrigger();
307
+ }
308
+ if (!this._panel || !this._panel.isConnected) {
309
+ this._panel = this._resolvePanel();
310
+ if (this._panel) this._preparePanel();
311
+ }
312
+ return this._panel;
313
+ }
314
+
315
+ _placePanel() {
316
+ positionPopoverPanel(this._panel, this._trigger, this._positionOpts());
317
+ }
318
+
319
+ _syncOpen(next, fromPopover = false) {
320
+ if (!this._ensurePanel()) return;
321
+ if (next && this._open) {
322
+ this._placePanel();
323
+ return;
324
+ }
325
+ if (!next && !this._open) return;
326
+
327
+ if (next) {
328
+ if (!fromPopover) {
329
+ openPopoverPanel(this._panel, this._trigger, this._positionOpts());
330
+ } else {
331
+ this._placePanel();
332
+ }
333
+ this._bindReflow();
334
+ this._syncScrollLock(true);
335
+ this._bindDismiss();
336
+ } else {
337
+ this._hoverPinned = false;
338
+ this._unbindReflow();
339
+ this._unbindDismiss();
340
+ this._syncScrollLock(false);
341
+ if (!fromPopover) closePopoverPanel(this._panel);
342
+ }
343
+
344
+ this._open = next;
345
+ this.setBoolAttr('open', next);
346
+ this._trigger?.setAttribute('aria-expanded', String(next));
347
+ this.emit(next ? 'dom:open' : 'dom:close');
348
+ }
349
+
350
+ attributeChangedCallback(name) {
351
+ if (name === 'data-trigger-id' || name === 'trigger') {
352
+ this._trigger = this._resolveTrigger();
353
+ if (this._trigger) this._prepareTrigger();
354
+ }
355
+ if (!this._open || !this._panel || !this._trigger) return;
356
+ if (name === 'lock-scroll') {
357
+ this._syncScrollLock(true);
358
+ }
359
+ if (name === 'align' || name === 'offset' || name === 'placement' || name === 'collision-padding' || name === 'floating-mode' || name === 'flip') {
360
+ positionPopoverPanel(this._panel, this._trigger, this._positionOpts());
361
+ }
362
+ }
363
+
364
+ get open() { return this._open; }
365
+ set open(value) {
366
+ const next = !!value;
367
+ if (next === this._open) return;
368
+ if (!this._ensurePanel()) return;
369
+ this._syncOpen(next, false);
370
+ }
371
+
372
+ toggle() {
373
+ if (!this._ensurePanel()) return;
374
+ this._syncOpen(!this._open, false);
375
+ }
376
+
377
+ disconnectedCallback() {
378
+ this._unbindToggle?.();
379
+ this._unbindToggle = null;
380
+ this._unbindReflow();
381
+ this._triggerOff?.();
382
+ this._triggerOff = null;
383
+ this._unbindPanelHover();
384
+ this._clearHoverClose();
385
+ this._unbindDismiss();
386
+ this._syncScrollLock(false);
387
+ super.disconnectedCallback();
388
+ }
389
+ }
390
+
391
+ defineDomElement('dom-popover', DomPopoverElement);
@@ -0,0 +1,19 @@
1
+ /* eslint-disable */
2
+ // Generated by scripts/generate-types.mjs. Do not edit by hand.
3
+ import { DomElementBase } from './base.js';
4
+ export declare class DomRadioGroupElement extends DomElementBase {
5
+ static readonly observedAttributes?: Array<string>;
6
+ static readonly __doc?: HeadlessElementDoc;
7
+ value: string;
8
+ readonly radios: Array<HTMLElement>;
9
+ }
10
+ export interface HeadlessElementDoc {
11
+ name?: string;
12
+ description?: string;
13
+ slots?: Array<Record<string, unknown>>;
14
+ attributes?: Array<Record<string, unknown>>;
15
+ events?: Array<Record<string, unknown>>;
16
+ keyboard?: Array<Record<string, unknown>>;
17
+ example?: string;
18
+ [key: string]: unknown;
19
+ }
@@ -0,0 +1,100 @@
1
+ import { DomElementBase, createRoving, defineDomElement } from './base.js';
2
+
3
+ function valueOf(radio) {
4
+ return radio.dataset.value ?? radio.textContent.trim();
5
+ }
6
+
7
+ export class DomRadioGroupElement extends DomElementBase {
8
+ static get observedAttributes() { return ['value', 'orientation']; }
9
+
10
+ static __doc = {
11
+ name: 'dom-radio-group',
12
+ description: 'A radio group with roving focus and single-value selection.',
13
+ slots: [{ name: '(default)', description: 'Radio options with role="radio" and data-value.' }],
14
+ attributes: [
15
+ { name: 'value', type: 'string', description: 'Selected value.' },
16
+ { name: 'orientation', type: "'vertical' | 'horizontal'", description: 'Arrow-key direction. Defaults to vertical.' },
17
+ ],
18
+ events: [{ name: 'dom:change', payload: '{ value, radio }', description: 'Fired when selected value changes.' }],
19
+ keyboard: [
20
+ { keys: '↑ / ↓ or ← / →', action: 'Move selection depending on orientation.' },
21
+ { keys: 'Home / End', action: 'Jump to first / last option.' },
22
+ { keys: 'Space / Enter', action: 'Select focused option.' },
23
+ ],
24
+ };
25
+
26
+ constructor() {
27
+ super();
28
+ this._roving = null;
29
+ this._refreshing = false;
30
+ this._onKey = this._onKey.bind(this);
31
+ this._onClick = this._onClick.bind(this);
32
+ }
33
+
34
+ connectedCallback() {
35
+ this.setAttribute('role', this.getAttribute('role') || 'radiogroup');
36
+ this._refresh();
37
+ this.on(this, 'keydown', this._onKey);
38
+ this.on(this, 'click', this._onClick);
39
+ }
40
+
41
+ attributeChangedCallback() {
42
+ this._refresh();
43
+ }
44
+
45
+ get value() { return this.getAttribute('value') || ''; }
46
+ set value(value) {
47
+ if (value == null || value === '') this.removeAttribute('value');
48
+ else this.setAttribute('value', value);
49
+ }
50
+
51
+ get radios() {
52
+ return Array.from(this.querySelectorAll('[role="radio"]'))
53
+ .filter((radio) => !radio.hasAttribute('disabled') && radio.getAttribute('aria-disabled') !== 'true');
54
+ }
55
+
56
+ _refresh() {
57
+ const radios = this.radios;
58
+ const orientation = this.getAttribute('orientation') || 'vertical';
59
+ this.setAttribute('aria-orientation', orientation);
60
+ let active = Math.max(0, radios.findIndex((radio) => valueOf(radio) === this.value));
61
+ radios.forEach((radio, index) => {
62
+ radio.tabIndex = index === active ? 0 : -1;
63
+ radio.setAttribute('aria-checked', String(valueOf(radio) === this.value));
64
+ });
65
+ this._refreshing = true;
66
+ this._roving = createRoving({
67
+ items: radios,
68
+ orientation,
69
+ loop: true,
70
+ onSelect: (_, radio) => this._select(radio),
71
+ });
72
+ this._roving.setActive(active, { focus: false, select: false });
73
+ this._refreshing = false;
74
+ }
75
+
76
+ _select(radio, event) {
77
+ const value = valueOf(radio);
78
+ if (value === this.value) return;
79
+ this.value = value;
80
+ this._refresh();
81
+ if (!this._refreshing) this.emit('dom:change', { value, radio, event });
82
+ }
83
+
84
+ _onClick(event) {
85
+ const radio = event.target.closest('[role="radio"]');
86
+ if (!radio || !this.contains(radio)) return;
87
+ this._select(radio, event);
88
+ }
89
+
90
+ _onKey(event) {
91
+ this._roving?.onKey(event);
92
+ if (event.key !== 'Enter' && event.key !== ' ') return;
93
+ const radio = document.activeElement?.closest?.('[role="radio"]');
94
+ if (!radio || !this.contains(radio)) return;
95
+ event.preventDefault();
96
+ this._select(radio, event);
97
+ }
98
+ }
99
+
100
+ defineDomElement('dom-radio-group', DomRadioGroupElement);
@@ -0,0 +1,18 @@
1
+ /* eslint-disable */
2
+ // Generated by scripts/generate-types.mjs. Do not edit by hand.
3
+ import { DomElementBase } from './base.js';
4
+ export declare class DomTabsElement extends DomElementBase {
5
+ static readonly observedAttributes?: Array<string>;
6
+ static readonly __doc?: HeadlessElementDoc;
7
+ value: string | null;
8
+ }
9
+ export interface HeadlessElementDoc {
10
+ name?: string;
11
+ description?: string;
12
+ slots?: Array<Record<string, unknown>>;
13
+ attributes?: Array<Record<string, unknown>>;
14
+ events?: Array<Record<string, unknown>>;
15
+ keyboard?: Array<Record<string, unknown>>;
16
+ example?: string;
17
+ [key: string]: unknown;
18
+ }
@@ -0,0 +1,119 @@
1
+ import { DomElementBase, defineDomElement, uid, createRoving } from './base.js';
2
+
3
+ // <dom-tabs value="overview">
4
+ // <div role="tablist">
5
+ // <button data-tab="overview">Overview</button>
6
+ // </div>
7
+ // <div data-panel="overview">…</div>
8
+ // </dom-tabs>
9
+ export class DomTabsElement extends DomElementBase {
10
+ static get observedAttributes() { return ['value']; }
11
+
12
+ static __doc = {
13
+ name: 'dom-tabs',
14
+ description: 'Tab list with managed ARIA, roving tabindex and panel linking via data-tab / data-panel pairs.',
15
+ slots: [
16
+ { name: '(structural)', description: 'Pair each <button data-tab="key"> with a panel <div data-panel="key">. The tablist wrapper carries role="tablist".' },
17
+ ],
18
+ attributes: [
19
+ { name: 'value', type: 'string', description: 'Active tab key. Mirrors the currently-selected data-tab value.' },
20
+ ],
21
+ events: [
22
+ { name: 'dom:change', payload: '{ value }', description: 'Fired when the active tab changes.' },
23
+ ],
24
+ keyboard: [
25
+ { keys: '← / →', action: 'Move to previous / next tab.' },
26
+ { keys: 'Home / End', action: 'Jump to first / last tab.' },
27
+ { keys: 'Enter / Space', action: 'Activate focused tab.' },
28
+ ],
29
+ example: `<dom-tabs value="overview">
30
+ \t<div role="tablist">
31
+ \t\t<button data-tab="overview">Overview</button>
32
+ \t\t<button data-tab="pricing">Pricing</button>
33
+ \t</div>
34
+ \t<div data-panel="overview">Overview content…</div>
35
+ \t<div data-panel="pricing">Pricing content…</div>
36
+ </dom-tabs>`,
37
+ };
38
+
39
+ connectedCallback() {
40
+ if (this._connected) return;
41
+ this._connected = true;
42
+ this._observer = new MutationObserver(() => this._refresh());
43
+ this._observer.observe(this, { childList: true, subtree: true });
44
+ this._refresh();
45
+ }
46
+
47
+ disconnectedCallback() {
48
+ this._observer?.disconnect();
49
+ this._observer = null;
50
+ this._teardownTabs();
51
+ this._connected = false;
52
+ super.disconnectedCallback();
53
+ }
54
+
55
+ _refresh() {
56
+ this._teardownTabs();
57
+ this._tablist = this.querySelector('[role="tablist"], [data-tablist]');
58
+ this._tabs = Array.from(this.querySelectorAll('[data-tab]'));
59
+ this._panels = Array.from(this.querySelectorAll('[data-panel]'));
60
+ if (!this._tabs.length) return;
61
+
62
+ const id = uid('tabs');
63
+ this._tablist?.setAttribute('role', 'tablist');
64
+ this._tabs.forEach((tab, i) => {
65
+ tab.setAttribute('role', 'tab');
66
+ tab.id = tab.id || `${id}-tab-${i}`;
67
+ const key = tab.dataset.tab;
68
+ const panel = this._panels.find((p) => p.dataset.panel === key);
69
+ if (panel) {
70
+ panel.setAttribute('role', 'tabpanel');
71
+ panel.id = panel.id || `${id}-panel-${i}`;
72
+ panel.setAttribute('aria-labelledby', tab.id);
73
+ tab.setAttribute('aria-controls', panel.id);
74
+ }
75
+ this._tabOffs.push(this.on(tab, 'click', () => { this.value = key; }));
76
+ });
77
+
78
+ this._roving = createRoving({
79
+ items: this._tabs,
80
+ orientation: 'horizontal',
81
+ onSelect: (_, el) => { this.value = el.dataset.tab; },
82
+ });
83
+ if (this._tablist) this._tabOffs.push(this.on(this._tablist, 'keydown', this._roving.onKey));
84
+
85
+ const current = this.getAttribute('value');
86
+ const next = this._tabs.some((tab) => tab.dataset.tab === current)
87
+ ? current
88
+ : this._tabs[0].dataset.tab;
89
+ this.value = next;
90
+ }
91
+
92
+ _teardownTabs() {
93
+ for (const off of this._tabOffs || []) off();
94
+ this._tabOffs = [];
95
+ }
96
+
97
+ get value() { return this.getAttribute('value'); }
98
+ set value(next) {
99
+ // Always run the DOM sync — even when `next` equals the current attribute
100
+ // (which happens on initial render when `<dom-tabs value="…">` ships
101
+ // with the value pre-set), otherwise panels stay visible until the first
102
+ // tab click. Only the change event is gated on an actual change.
103
+ const prev = this.getAttribute('value');
104
+ if (prev !== next) this.setAttribute('value', next);
105
+ if (!this._tabs || !this._panels) return;
106
+ this._tabs.forEach((tab) => {
107
+ const active = tab.dataset.tab === next;
108
+ tab.setAttribute('aria-selected', String(active));
109
+ tab.tabIndex = active ? 0 : -1;
110
+ tab.toggleAttribute('data-active', active);
111
+ });
112
+ this._panels.forEach((panel) => {
113
+ panel.hidden = panel.dataset.panel !== next;
114
+ });
115
+ if (prev !== next) this.emit('dom:change', { value: next });
116
+ }
117
+ }
118
+
119
+ defineDomElement('dom-tabs', DomTabsElement);
@@ -0,0 +1,13 @@
1
+ /* eslint-disable */
2
+ // Generated by scripts/generate-types.mjs. Do not edit by hand.
3
+ import { DomElementBase } from './base.js';
4
+ export interface HeadlessToastOptions {
5
+ title?: string;
6
+ description?: string;
7
+ duration?: number;
8
+ tone?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ export declare class DomToastRegionElement extends DomElementBase {
12
+ show(options?: HeadlessToastOptions): string;
13
+ }