@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,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 DomCheckboxElement extends DomElementBase {
5
+ static readonly observedAttributes?: Array<string>;
6
+ static readonly __doc?: HeadlessElementDoc;
7
+ checked: boolean;
8
+ toggle(): void;
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,99 @@
1
+ import { DomElementBase, defineDomElement, isBrowser } from './base.js';
2
+
3
+ const STYLE_ID = 'dom-checkbox-styles';
4
+
5
+ function ensureStyles() {
6
+ if (!isBrowser || document.getElementById(STYLE_ID)) return;
7
+ const style = document.createElement('style');
8
+ style.id = STYLE_ID;
9
+ style.textContent = `
10
+ dom-checkbox {
11
+ display: inline-grid;
12
+ place-items: center;
13
+ width: 1.25rem;
14
+ height: 1.25rem;
15
+ border: 1px solid var(--border);
16
+ border-radius: 0.375rem;
17
+ background: var(--canvas);
18
+ color: var(--primary-fg);
19
+ cursor: pointer;
20
+ vertical-align: middle;
21
+ }
22
+ dom-checkbox::after {
23
+ content: '';
24
+ width: 0.55rem;
25
+ height: 0.32rem;
26
+ border-left: 2px solid currentColor;
27
+ border-bottom: 2px solid currentColor;
28
+ transform: rotate(-45deg) translate(1px, -1px);
29
+ opacity: 0;
30
+ }
31
+ dom-checkbox[aria-checked="true"] {
32
+ border-color: var(--primary);
33
+ background: var(--primary);
34
+ }
35
+ dom-checkbox[aria-checked="true"]::after {
36
+ opacity: 1;
37
+ }
38
+ dom-checkbox:focus-visible {
39
+ outline: 2px solid var(--ring);
40
+ outline-offset: 2px;
41
+ }
42
+ dom-checkbox[aria-disabled="true"] {
43
+ opacity: 0.5;
44
+ cursor: not-allowed;
45
+ }
46
+ `;
47
+ document.head.appendChild(style);
48
+ }
49
+
50
+ ensureStyles();
51
+
52
+ export class DomCheckboxElement extends DomElementBase {
53
+ static get observedAttributes() { return ['checked', 'disabled']; }
54
+
55
+ static __doc = {
56
+ name: 'dom-checkbox',
57
+ description: 'Accessible checkbox with checked state, keyboard support, and dom:change.',
58
+ attributes: [
59
+ { name: 'checked', type: 'boolean', description: 'Checked state.' },
60
+ { name: 'disabled', type: 'boolean', description: 'Disable interaction.' },
61
+ { name: 'aria-label', type: 'string', description: 'Accessible name when no visible label is provided.' },
62
+ ],
63
+ events: [{ name: 'dom:change', payload: '{ checked }', description: 'Fired when checked state changes.' }],
64
+ keyboard: [{ keys: 'Space / Enter', action: 'Toggle checked state.' }],
65
+ };
66
+
67
+ connectedCallback() {
68
+ this.setAttribute('role', 'checkbox');
69
+ this.tabIndex = this.hasAttribute('disabled') ? -1 : 0;
70
+ this._sync();
71
+ this.on(this, 'click', () => this.toggle());
72
+ this.on(this, 'keydown', (event) => {
73
+ if (event.key !== ' ' && event.key !== 'Enter') return;
74
+ event.preventDefault();
75
+ this.toggle();
76
+ });
77
+ }
78
+
79
+ attributeChangedCallback() { this._sync(); }
80
+
81
+ get checked() { return this.boolAttr('checked'); }
82
+ set checked(value) { this.setBoolAttr('checked', !!value); }
83
+
84
+ _sync() {
85
+ this.setAttribute('aria-checked', String(this.checked));
86
+ const disabled = this.hasAttribute('disabled');
87
+ this.tabIndex = disabled ? -1 : 0;
88
+ if (disabled) this.setAttribute('aria-disabled', 'true');
89
+ else this.removeAttribute('aria-disabled');
90
+ }
91
+
92
+ toggle() {
93
+ if (this.hasAttribute('disabled')) return;
94
+ this.checked = !this.checked;
95
+ this.emit('dom:change', { checked: this.checked });
96
+ }
97
+ }
98
+
99
+ defineDomElement('dom-checkbox', DomCheckboxElement);
@@ -0,0 +1,7 @@
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 ComboBase extends DomElementBase {
5
+ value: string;
6
+ }
7
+ export declare class DomComboboxElement extends ComboBase {}
@@ -0,0 +1,488 @@
1
+ import { DomElementBase, defineDomElement, uid } from './base.js';
2
+ import {
3
+ applyFloatingPosition,
4
+ autoUpdateFloating,
5
+ } from './floating.js';
6
+ import {
7
+ beginFloatingEffectEnter,
8
+ beginFloatingEffectLeave,
9
+ clearFloatingEffectTimer,
10
+ hasFloatingEffect,
11
+ prepareFloatingEffect,
12
+ resetFloatingEffectState,
13
+ } from './floating-effect.js';
14
+
15
+ // <dom-combobox value="">
16
+ // <input slot="input" />
17
+ // <button slot="toggle" type="button">...</button>
18
+ // <ul slot="list">
19
+ // <li data-value="apple">Apple</li>
20
+ // </ul>
21
+ // </dom-combobox>
22
+
23
+ const OBSERVED = [
24
+ 'value',
25
+ 'open',
26
+ 'align',
27
+ 'offset',
28
+ 'placement',
29
+ 'collision-padding',
30
+ 'floating-mode',
31
+ ];
32
+
33
+ function textOf(option) {
34
+ return option.textContent.trim();
35
+ }
36
+
37
+ function valueOf(option) {
38
+ return option.dataset.value ?? option.getAttribute('value') ?? option.value ?? textOf(option);
39
+ }
40
+
41
+ function labelOf(option) {
42
+ return option.dataset.label ?? option.getAttribute('label') ?? textOf(option);
43
+ }
44
+
45
+ function matchesOption(option, query) {
46
+ if (!query) return true;
47
+ const haystack = `${option.dataset.value || ''} ${option.dataset.label || ''} ${option.textContent}`.toLowerCase();
48
+ return haystack.includes(query);
49
+ }
50
+
51
+ function dispatchCancelable(host, type, detail) {
52
+ const event = new CustomEvent(type, {
53
+ detail,
54
+ bubbles: true,
55
+ cancelable: true,
56
+ composed: true,
57
+ });
58
+ host.dispatchEvent(event);
59
+ return event;
60
+ }
61
+
62
+ /**
63
+ * Check whether a listbox is currently promoted to the browser top layer.
64
+ *
65
+ * @param {HTMLElement|null|undefined} list Floating listbox element.
66
+ * @returns {boolean} True when the element is an open native popover.
67
+ */
68
+ function isListPopoverOpen(list) {
69
+ return Boolean(list?.matches?.(':popover-open'));
70
+ }
71
+
72
+ /**
73
+ * Show a listbox as a manual popover when the browser supports the Popover API.
74
+ *
75
+ * @param {HTMLElement|null|undefined} list Floating listbox element.
76
+ * @returns {void}
77
+ */
78
+ function showListPopover(list) {
79
+ if (!list || typeof list.showPopover !== 'function' || isListPopoverOpen(list)) return;
80
+ try {
81
+ list.showPopover();
82
+ } catch {
83
+ // The list remains in-place for browsers or states that reject promotion.
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Hide a listbox manual popover when it is currently open.
89
+ *
90
+ * @param {HTMLElement|null|undefined} list Floating listbox element.
91
+ * @returns {void}
92
+ */
93
+ function hideListPopover(list) {
94
+ if (!list || typeof list.hidePopover !== 'function' || !isListPopoverOpen(list)) return;
95
+ try {
96
+ list.hidePopover();
97
+ } catch {
98
+ // Ignore stale top-layer state; hidden/reset below still closes the list.
99
+ }
100
+ }
101
+
102
+ export class ComboBase extends DomElementBase {
103
+ static get observedAttributes() { return OBSERVED; }
104
+
105
+ constructor() {
106
+ super();
107
+ this._active = -1;
108
+ this._open = false;
109
+ this._options = [];
110
+ this._unbindFloating = null;
111
+ this._mutationObserver = null;
112
+ this._wiredList = null;
113
+ this._onDocDown = this._onDocDown.bind(this);
114
+ this._onListPointerDown = this._onListPointerDown.bind(this);
115
+ this._onListPointerMove = this._onListPointerMove.bind(this);
116
+ }
117
+
118
+ get _freeText() { return false; }
119
+ get _inputRole() { return 'combobox'; }
120
+ get _valueMode() { return 'option'; }
121
+ get _activeOnFilter() { return true; }
122
+
123
+ _positionOpts() {
124
+ return {
125
+ align: this.getAttribute('align') || 'left',
126
+ offset: Number(this.getAttribute('offset') || 4),
127
+ placement: this.getAttribute('placement') || 'bottom',
128
+ padding: Number(this.getAttribute('collision-padding') || 8),
129
+ mode: this.getAttribute('floating-mode') || 'viewport',
130
+ };
131
+ }
132
+
133
+ connectedCallback() {
134
+ this._input = this.querySelector('[slot="input"], [data-input]');
135
+ this._toggle = this.querySelector('[slot="toggle"], [data-toggle]');
136
+ this._list = this._resolveList();
137
+ if (!this._input) return;
138
+
139
+ this._input.setAttribute('role', this._inputRole);
140
+ this._input.setAttribute('aria-autocomplete', 'list');
141
+ this._input.setAttribute('aria-expanded', 'false');
142
+
143
+ if (this._toggle) {
144
+ this._toggle.setAttribute('aria-haspopup', 'listbox');
145
+ this._toggle.setAttribute('aria-expanded', 'false');
146
+ this.on(this._toggle, 'click', () => this._toggleList());
147
+ }
148
+
149
+ if (this._list) this._wireList();
150
+
151
+ this.on(this._input, 'input', () => this._handleInput());
152
+ this.on(this._input, 'focus', () => this._filter(this._input.value, { open: true }));
153
+ this.on(this._input, 'keydown', (e) => this._onKey(e));
154
+ if (this.hasAttribute('value')) this._syncInputFromValue();
155
+ if (this.hasAttribute('open')) this._setOpen(true);
156
+ }
157
+
158
+ disconnectedCallback() {
159
+ this._setOpen(false);
160
+ clearFloatingEffectTimer(this._list);
161
+ this._finishListClose();
162
+ this._mutationObserver?.disconnect();
163
+ this._mutationObserver = null;
164
+ this._wiredList?.removeEventListener('mousedown', this._onListPointerDown);
165
+ this._wiredList?.removeEventListener('mousemove', this._onListPointerMove);
166
+ this._wiredList = null;
167
+ super.disconnectedCallback();
168
+ document.removeEventListener('mousedown', this._onDocDown);
169
+ }
170
+
171
+ _resolveList() {
172
+ const inner = this.querySelector('[slot="list"], [data-list]');
173
+ if (inner) return inner;
174
+ const id = this.dataset.menuId || this.dataset.listId;
175
+ return id ? document.getElementById(id) : null;
176
+ }
177
+
178
+ _ensureList() {
179
+ if (!this._list || !this._list.isConnected) {
180
+ this._list = this._resolveList();
181
+ if (this._list) this._wireList();
182
+ }
183
+ return this._list;
184
+ }
185
+
186
+ _wireList() {
187
+ if (!this._list || this._list.__elementComboOwner === this) return;
188
+ const id = uid('cmb');
189
+ this._wiredList?.removeEventListener('mousedown', this._onListPointerDown);
190
+ this._wiredList?.removeEventListener('mousemove', this._onListPointerMove);
191
+ this._list.__elementComboOwner = this;
192
+ this._wiredList = this._list;
193
+ this._list.id = this._list.id || `${id}-list`;
194
+ this._list.setAttribute('role', 'listbox');
195
+ if (typeof this._list.showPopover === 'function' && !this._list.hasAttribute('popover')) {
196
+ this._list.setAttribute('popover', 'manual');
197
+ }
198
+ prepareFloatingEffect(this._list);
199
+ this._list.hidden = !this._open;
200
+ this._input.setAttribute('aria-controls', this._list.id);
201
+ this._list.addEventListener('mousedown', this._onListPointerDown);
202
+ this._list.addEventListener('mousemove', this._onListPointerMove);
203
+ this._mutationObserver?.disconnect();
204
+ this._mutationObserver = new MutationObserver(() => {
205
+ this._refreshOptions();
206
+ if (this._open || document.activeElement === this._input) {
207
+ this._filter(this._input.value, { open: true, emitQuery: false });
208
+ this._positionList();
209
+ }
210
+ });
211
+ this._mutationObserver.observe(this._list, { childList: true, subtree: true, characterData: true });
212
+ this._refreshOptions();
213
+ this._syncInputFromValue();
214
+ }
215
+
216
+ _refreshOptions() {
217
+ if (!this._list) return;
218
+ const id = this._list.id || uid('cmb-list');
219
+ this._options = Array.from(this._list.children).filter((option) => !option.hasAttribute('data-disabled'));
220
+ this._options.forEach((option, index) => {
221
+ option.setAttribute('role', 'option');
222
+ option.id = option.id || `${id}-opt-${index}`;
223
+ });
224
+ }
225
+
226
+ _handleInput() {
227
+ const value = this._input.value;
228
+ this.emit('dom:input', { value });
229
+ this._filter(value, { open: true });
230
+ if (this._freeText) this.setAttribute('value', value);
231
+ }
232
+
233
+ _syncInputFromValue() {
234
+ if (!this._input || !this.hasAttribute('value')) return;
235
+ const value = this.getAttribute('value') || '';
236
+ if (this._valueMode === 'text') {
237
+ if (this._input.value !== value) this._input.value = value;
238
+ return;
239
+ }
240
+ const selected = this._options.find((option) => valueOf(option) === value);
241
+ const display = selected ? labelOf(selected) : value;
242
+ if (this._input.value !== display) this._input.value = display;
243
+ }
244
+
245
+ _onDocDown(e) {
246
+ if (this.contains(e.target)) return;
247
+ if (this._list && this._list.contains(e.target)) return;
248
+ this._setOpen(false);
249
+ }
250
+
251
+ _positionList() {
252
+ if (!this._input || !this._list || this._list.hidden) return;
253
+ const rect = this._input.getBoundingClientRect();
254
+ this._list.style.width = `${Math.round(rect.width)}px`;
255
+ applyFloatingPosition(this._input, this._list, this._positionOpts());
256
+ }
257
+
258
+ _bindFloating() {
259
+ this._unbindFloating?.();
260
+ this._unbindFloating = autoUpdateFloating(this._input, this._list, () => this._positionList());
261
+ }
262
+
263
+ _unbindFloating() {
264
+ this._unbindFloating?.();
265
+ this._unbindFloating = null;
266
+ }
267
+
268
+ _finishListClose(list = this._list) {
269
+ if (!list) return;
270
+ hideListPopover(list);
271
+ list.hidden = true;
272
+ resetFloatingEffectState(list);
273
+ }
274
+
275
+ _showList() {
276
+ const list = this._list;
277
+ list.hidden = false;
278
+ showListPopover(list);
279
+ if (!hasFloatingEffect(list)) return;
280
+ beginFloatingEffectEnter(list, {
281
+ shouldContinue: () => this._open && this._list === list,
282
+ });
283
+ }
284
+
285
+ _hideList() {
286
+ const list = this._list;
287
+ beginFloatingEffectLeave(list, {
288
+ shouldContinue: () => !this._open && this._list === list,
289
+ finish: () => this._finishListClose(list),
290
+ });
291
+ }
292
+
293
+ _setOpen(open) {
294
+ if (!this._ensureList()) return;
295
+ if (open === this._open && this._list.hidden === !open) {
296
+ if (open) this._positionList();
297
+ return;
298
+ }
299
+ this._open = open;
300
+ if (open) this._showList();
301
+ else this._hideList();
302
+ this._input.setAttribute('aria-expanded', String(open));
303
+ this._toggle?.setAttribute('aria-expanded', String(open));
304
+ this.setBoolAttr('open', open);
305
+
306
+ if (open) {
307
+ this._positionList();
308
+ this._bindFloating();
309
+ document.addEventListener('mousedown', this._onDocDown);
310
+ } else {
311
+ this._unbindFloating();
312
+ this._setActive(-1);
313
+ document.removeEventListener('mousedown', this._onDocDown);
314
+ }
315
+ }
316
+
317
+ _filter(value, { open = true, emitQuery = true } = {}) {
318
+ if (!this._ensureList()) {
319
+ if (emitQuery) this.emit('dom:query', { query: value });
320
+ return;
321
+ }
322
+ if (emitQuery) this.emit('dom:query', { query: value });
323
+
324
+ this._refreshOptions();
325
+ const query = value.trim().toLowerCase();
326
+ let firstVisible = -1;
327
+
328
+ this._options.forEach((option, index) => {
329
+ const match = matchesOption(option, query);
330
+ option.hidden = !match;
331
+ if (match && firstVisible === -1) firstVisible = index;
332
+ });
333
+
334
+ this._list.toggleAttribute('data-empty', firstVisible < 0);
335
+ this._setOpen(open && firstVisible >= 0);
336
+ this._setActive(this._activeOnFilter ? firstVisible : -1);
337
+ this._positionList();
338
+ }
339
+
340
+ _setActive(index) {
341
+ this._active = index;
342
+ this._options.forEach((option, optionIndex) => {
343
+ const active = optionIndex === index;
344
+ option.toggleAttribute('data-active', active);
345
+ option.setAttribute('aria-selected', String(active));
346
+ });
347
+ if (index >= 0 && this._options[index]) this._input.setAttribute('aria-activedescendant', this._options[index].id);
348
+ else this._input.removeAttribute('aria-activedescendant');
349
+ }
350
+
351
+ _visible() {
352
+ this._refreshOptions();
353
+ return this._options
354
+ .map((option, index) => ({ option, index }))
355
+ .filter(({ option }) => !option.hidden);
356
+ }
357
+
358
+ _move(delta) {
359
+ const visible = this._visible();
360
+ if (!visible.length) return;
361
+ const current = visible.findIndex(({ index }) => index === this._active);
362
+ if (current === -1) {
363
+ this._setActive(delta > 0 ? visible[0].index : visible[visible.length - 1].index);
364
+ return;
365
+ }
366
+ const next = visible[(current + delta + visible.length) % visible.length];
367
+ this._setActive(next.index);
368
+ }
369
+
370
+ _onKey(e) {
371
+ if (e.key === 'ArrowDown') {
372
+ e.preventDefault();
373
+ if (!this._open) this._filter(this._input.value, { open: true });
374
+ this._move(1);
375
+ } else if (e.key === 'ArrowUp') {
376
+ e.preventDefault();
377
+ if (!this._open) this._filter(this._input.value, { open: true });
378
+ this._move(-1);
379
+ } else if (e.key === 'Enter') {
380
+ if (this._active >= 0) {
381
+ e.preventDefault();
382
+ this._commit(this._options[this._active]);
383
+ } else if (this._freeText) {
384
+ e.preventDefault();
385
+ this._commitCustom(this._input.value);
386
+ }
387
+ } else if (e.key === 'Escape') {
388
+ e.preventDefault();
389
+ this._setOpen(false);
390
+ }
391
+ }
392
+
393
+ _onListPointerDown(e) {
394
+ const option = e.target.closest?.('[role="option"]');
395
+ if (!option || !this._list.contains(option) || option.hidden) return;
396
+ e.preventDefault();
397
+ this._commit(option);
398
+ }
399
+
400
+ _onListPointerMove(e) {
401
+ const option = e.target.closest?.('[role="option"]');
402
+ if (!option || !this._list.contains(option) || option.hidden) return;
403
+ const index = this._options.indexOf(option);
404
+ if (index >= 0) this._setActive(index);
405
+ }
406
+
407
+ _toggleList() {
408
+ if (!this._ensureList()) return;
409
+ const wasOpen = this._open;
410
+ this._input.focus();
411
+ if (wasOpen) this._setOpen(false);
412
+ else this._filter('', { open: true });
413
+ }
414
+
415
+ _commit(option) {
416
+ if (!option) return;
417
+ const value = valueOf(option);
418
+ const detail = {
419
+ value,
420
+ label: labelOf(option),
421
+ option,
422
+ custom: false,
423
+ };
424
+ const inputValue = this._valueMode === 'text' ? detail.label : detail.label;
425
+ const hostValue = this._valueMode === 'text' ? detail.label : value;
426
+
427
+ this._input.value = inputValue;
428
+ this.setAttribute('value', hostValue);
429
+ this._setOpen(false);
430
+ this.emit('dom:select', detail);
431
+ this.emit('dom:change', detail);
432
+ }
433
+
434
+ _commitCustom(rawValue) {
435
+ const value = rawValue.trim();
436
+ if (!value) return;
437
+ const detail = { value, label: value, option: null, custom: true };
438
+ const event = dispatchCancelable(this, 'dom:custom', detail);
439
+ if (event.defaultPrevented) return;
440
+ this._input.value = value;
441
+ this.setAttribute('value', value);
442
+ this._setOpen(false);
443
+ this.emit('dom:change', detail);
444
+ }
445
+
446
+ attributeChangedCallback(name, _old, value) {
447
+ if (!this.isConnected || !this._input) return;
448
+ if (name === 'value') this._syncInputFromValue();
449
+ if (name === 'open') this._setOpen(value !== null);
450
+ if (['align', 'offset', 'placement', 'collision-padding', 'floating-mode'].includes(name) && this._open) this._positionList();
451
+ }
452
+
453
+ get value() { return this.getAttribute('value') || ''; }
454
+ set value(value) { this.setAttribute('value', value ?? ''); }
455
+ }
456
+
457
+ export class DomComboboxElement extends ComboBase {
458
+ static __doc = {
459
+ name: 'dom-combobox',
460
+ description: 'Select-like combobox with a text input, optional toggle button, managed activedescendant, keyboard navigation, and floating list positioning.',
461
+ slots: [
462
+ { name: 'input', description: 'The text input. Gets role="combobox" and aria-controls wired automatically.' },
463
+ { name: 'toggle', description: 'Optional button that opens the list like a select.' },
464
+ { name: 'list', description: 'The listbox container. Each option must carry data-value.' },
465
+ ],
466
+ attributes: [
467
+ { name: 'value', type: 'string', description: 'Currently-selected value.' },
468
+ { name: 'open', type: 'boolean', description: 'Reflects whether the list is visible.' },
469
+ { name: 'placement', type: 'string', description: 'Preferred floating placement before collision handling.' },
470
+ { name: 'floating-mode', type: "'viewport' | 'anchor'", description: 'viewport keeps the list inside the browser; anchor keeps it attached to the input while scrolling.' },
471
+ { name: 'data-menu-id', type: 'string', description: 'Id of an external or teleported list element.' },
472
+ ],
473
+ events: [
474
+ { name: 'dom:input', payload: '{ value }', description: 'Fired whenever the text value changes.' },
475
+ { name: 'dom:query', payload: '{ query }', description: 'Fired whenever the user types. Useful for server lookups.' },
476
+ { name: 'dom:select', payload: '{ value, label, option }', description: 'Fired when an option is selected.' },
477
+ { name: 'dom:change', payload: '{ value, option, custom }', description: 'Fired when an option is committed.' },
478
+ ],
479
+ keyboard: [
480
+ { keys: '↑ / ↓', action: 'Move active option.' },
481
+ { keys: 'Enter', action: 'Commit active option.' },
482
+ { keys: 'Esc', action: 'Close the list.' },
483
+ { keys: 'Type', action: 'Filter the list and emit dom:query.' },
484
+ ],
485
+ };
486
+ }
487
+
488
+ defineDomElement('dom-combobox', DomComboboxElement);
@@ -0,0 +1,23 @@
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 DomDialogElement extends DomElementBase {
5
+ static readonly observedAttributes?: Array<string>;
6
+ static readonly __doc?: HeadlessElementDoc;
7
+ isOpen: boolean;
8
+ open(): void;
9
+ close(): void;
10
+ toggle(): void;
11
+ show(): void;
12
+ hide(): void;
13
+ }
14
+ export interface HeadlessElementDoc {
15
+ name?: string;
16
+ description?: string;
17
+ slots?: Array<Record<string, unknown>>;
18
+ attributes?: Array<Record<string, unknown>>;
19
+ events?: Array<Record<string, unknown>>;
20
+ keyboard?: Array<Record<string, unknown>>;
21
+ example?: string;
22
+ [key: string]: unknown;
23
+ }