@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.
- package/LICENSE.md +109 -0
- package/README.md +44 -0
- package/SKILL.md +83 -0
- package/package.json +73 -0
- package/public/llms.txt +57 -0
- package/public/skills.sh +11 -0
- package/src/pages/_layout/componentCatalog.generated.js +4514 -0
- package/src/pages/_layout/componentInspector.js +60 -0
- package/src/pages/_layout/componentManager.js +192 -0
- package/src/pages/_layout/componentRuntimeRecords.js +49 -0
- package/src/pages/_layout/inspector/componentRegistry.js +639 -0
- package/src/pages/_layout/inspector/serialize.js +134 -0
- package/src/pages/_layout/inspector/useInspector.js +293 -0
- package/src/pages/components/accordion/DomAccordion.vue +39 -0
- package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
- package/src/pages/components/button/DomButton.vue +132 -0
- package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
- package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
- package/src/pages/components/dialog/DomDialog.vue +271 -0
- package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
- package/src/pages/components/dialog/DomDialogStack.vue +289 -0
- package/src/pages/components/dialog/useDialogs.js +253 -0
- package/src/pages/components/drawer/DomDrawer.vue +159 -0
- package/src/pages/components/dropdown/DomDropdown.vue +261 -0
- package/src/pages/components/icon-button/DomIconButton.vue +155 -0
- package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
- package/src/pages/components/menu/DomMenu.vue +359 -0
- package/src/pages/components/menu/DomMenuItem.vue +233 -0
- package/src/pages/components/menu/DomMenuLabel.vue +19 -0
- package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
- package/src/pages/components/menu/menuContext.js +11 -0
- package/src/pages/components/menu/menuPlacement.js +26 -0
- package/src/pages/components/menu/submenu.js +58 -0
- package/src/pages/components/pagination/DomPagination.vue +265 -0
- package/src/pages/components/playground/DomRenderer.vue +48 -0
- package/src/pages/components/popover/DomPopover.vue +244 -0
- package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
- package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
- package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
- package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
- package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
- package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
- package/src/pages/components/stepper/DomStepper.vue +386 -0
- package/src/pages/components/tabs/DomTabs.vue +149 -0
- package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
- package/src/pages/components/toast/DomToastItem.vue +52 -0
- package/src/pages/components/toast/DomToastStack.vue +768 -0
- package/src/pages/components/toast/useToasts.js +237 -0
- package/src/pages/components/toolbar/DomToolbar.vue +158 -0
- package/src/pages/components/tooltip/DomTooltip.vue +69 -0
- package/src/pages/components/tooltip/vDomTooltip.js +106 -0
- package/src/pages/components/tree-view/DomTreeView.vue +961 -0
- package/src/pages/components/tree-view/treeSchemas.js +129 -0
- package/src/pages/components/tree-view/treeUtils.js +228 -0
- package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
- package/src/pages/data/_shared/resourceRecord.js +29 -0
- package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
- package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
- package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
- package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
- package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
- package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
- package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
- package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
- package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
- package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
- package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
- package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
- package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
- package/src/pages/data/kanban/DomKanban.vue +1703 -0
- package/src/pages/data/kanban/kanbanApi.js +80 -0
- package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
- package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
- package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
- package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
- package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
- package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
- package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
- package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
- package/src/pages/forms/_shared/options.js +61 -0
- package/src/pages/forms/_shared/useOptionMenu.js +59 -0
- package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
- package/src/pages/forms/calendar/DomCalendar.vue +442 -0
- package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
- package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
- package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
- package/src/pages/forms/color-input/DomColorInput.vue +118 -0
- package/src/pages/forms/combobox/DomCombobox.vue +237 -0
- package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
- package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
- package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
- package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
- package/src/pages/forms/field/DomField.vue +267 -0
- package/src/pages/forms/field/fieldLayout.js +1 -0
- package/src/pages/forms/field/fieldProps.js +114 -0
- package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
- package/src/pages/forms/field/useField.js +343 -0
- package/src/pages/forms/field/validators.js +364 -0
- package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
- package/src/pages/forms/form/DomForm.vue +985 -0
- package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
- package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
- package/src/pages/forms/form/adapters/shared.js +53 -0
- package/src/pages/forms/form/adapters/zod.js +235 -0
- package/src/pages/forms/form/formApi.js +38 -0
- package/src/pages/forms/form/formDefinition.js +392 -0
- package/src/pages/forms/form/schemaAdapter.js +54 -0
- package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
- package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
- package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
- package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
- package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
- package/src/pages/forms/listbox/DomListbox.vue +105 -0
- package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
- package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
- package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
- package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
- package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
- package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
- package/src/pages/forms/password-strength/passwordStrength.js +92 -0
- package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
- package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
- package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
- package/src/pages/forms/select/DomSelect.vue +479 -0
- package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
- package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
- package/src/pages/forms/text-input/DomTextInput.vue +48 -0
- package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
- package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
- package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
- package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
- package/src/pages/forms/time-selector/timeUtils.js +194 -0
- package/src/pages/forms/toggle/DomToggle.vue +158 -0
- package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
- package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
- package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
- package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
- package/src/pages/lib/css/color.js +245 -0
- package/src/pages/lib/headless/accordion.d.ts +7 -0
- package/src/pages/lib/headless/accordion.js +246 -0
- package/src/pages/lib/headless/autocomplete.d.ts +4 -0
- package/src/pages/lib/headless/autocomplete.js +33 -0
- package/src/pages/lib/headless/base.d.ts +25 -0
- package/src/pages/lib/headless/base.js +127 -0
- package/src/pages/lib/headless/checkbox.d.ts +19 -0
- package/src/pages/lib/headless/checkbox.js +99 -0
- package/src/pages/lib/headless/combobox.d.ts +7 -0
- package/src/pages/lib/headless/combobox.js +488 -0
- package/src/pages/lib/headless/dialog.d.ts +23 -0
- package/src/pages/lib/headless/dialog.js +384 -0
- package/src/pages/lib/headless/drawer.d.ts +21 -0
- package/src/pages/lib/headless/drawer.js +682 -0
- package/src/pages/lib/headless/dropdown.d.ts +19 -0
- package/src/pages/lib/headless/dropdown.js +412 -0
- package/src/pages/lib/headless/floating-effect.js +118 -0
- package/src/pages/lib/headless/floating.d.ts +34 -0
- package/src/pages/lib/headless/floating.js +382 -0
- package/src/pages/lib/headless/index.d.ts +19 -0
- package/src/pages/lib/headless/index.js +19 -0
- package/src/pages/lib/headless/listbox.d.ts +19 -0
- package/src/pages/lib/headless/listbox.js +126 -0
- package/src/pages/lib/headless/menu.d.ts +18 -0
- package/src/pages/lib/headless/menu.js +117 -0
- package/src/pages/lib/headless/popover-panel.d.ts +18 -0
- package/src/pages/lib/headless/popover-panel.js +309 -0
- package/src/pages/lib/headless/popover.d.ts +19 -0
- package/src/pages/lib/headless/popover.js +391 -0
- package/src/pages/lib/headless/radio-group.d.ts +19 -0
- package/src/pages/lib/headless/radio-group.js +100 -0
- package/src/pages/lib/headless/tabs.d.ts +18 -0
- package/src/pages/lib/headless/tabs.js +119 -0
- package/src/pages/lib/headless/toast.d.ts +13 -0
- package/src/pages/lib/headless/toast.js +65 -0
- package/src/pages/lib/headless/toggle.d.ts +19 -0
- package/src/pages/lib/headless/toggle.js +162 -0
- package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
- package/src/pages/lib/headless/tooltip-controller.js +549 -0
- package/src/pages/lib/headless/tooltip.d.ts +19 -0
- package/src/pages/lib/headless/tooltip.js +153 -0
- package/src/pages/lib/headless/transitions.d.ts +5 -0
- package/src/pages/lib/headless/transitions.js +89 -0
- package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
- package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
- package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
- package/src/pages/lib/vue/codeHighlighter.js +132 -0
- package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
- package/src/pages/lib/vue/index.d.ts +6806 -0
- package/src/pages/lib/vue/index.js +218 -0
- package/src/pages/lib/vue/useTheme.js +116 -0
- package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
- package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
- package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
- package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
- package/src/pages/mobile/shell/DomAppShell.vue +51 -0
- package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
- package/src/pages/mobile/stack/DomAppStack.vue +711 -0
- package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
- package/src/pages/visual/alert/DomAlert.vue +363 -0
- package/src/pages/visual/avatar/DomAvatar.vue +110 -0
- package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
- package/src/pages/visual/badge/DomBadge.vue +105 -0
- package/src/pages/visual/card/DomCard.vue +65 -0
- package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
- package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
- package/src/pages/visual/progress/DomProgress.vue +212 -0
- package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
- package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
- package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
- package/src/pages/visual/spinner/DomSpinner.vue +122 -0
- package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
- package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
- package/src/pages/visual/text-diff/textDiff.js +300 -0
- package/src/style.css +446 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('vue').Component} DialogComponent
|
|
5
|
+
* @typedef {Record<string, unknown>} DialogProps
|
|
6
|
+
* @typedef {Array<{ component: string, props?: Record<string, unknown> }>} DialogFormSchema
|
|
7
|
+
*
|
|
8
|
+
* @typedef {Object} DialogOptions
|
|
9
|
+
* @property {string} [id] Stable dialog id. Generated automatically when omitted.
|
|
10
|
+
* @property {string} [title] Dialog title.
|
|
11
|
+
* @property {string} [description] Supporting text under the title.
|
|
12
|
+
* @property {string} [message] Body text for the default dialog body.
|
|
13
|
+
* @property {DialogComponent|string} [component] Component rendered as the dialog body.
|
|
14
|
+
* @property {DialogProps} [props] Props passed to a custom component body.
|
|
15
|
+
* @property {DialogFormSchema} [formSchema] DomForm children schema rendered as a dialog form.
|
|
16
|
+
* @property {Record<string, unknown>} [formValues] Initial values for a schema dialog form.
|
|
17
|
+
* @property {boolean} [backdrop] Show the visual backdrop. Defaults to true.
|
|
18
|
+
* @property {boolean} [static] Disable backdrop and Esc dismissal.
|
|
19
|
+
* @property {boolean} [showCancel] Render a cancel action in the default footer.
|
|
20
|
+
* @property {boolean} [footer] Set false to hide the default footer.
|
|
21
|
+
* @property {string} [cancelText] Cancel button label.
|
|
22
|
+
* @property {string} [confirmText] Confirm button label.
|
|
23
|
+
* @property {string} [invalidMessage] Validation message shown by schema dialog forms.
|
|
24
|
+
* @property {'default'|'danger'|string} [tone] Visual tone for the confirm button.
|
|
25
|
+
* @property {'dialog'|'confirm'|'form'|string} [type] Dialog interaction type.
|
|
26
|
+
*
|
|
27
|
+
* Promise returned by dialog helpers. Resolves with submitted form data, a custom
|
|
28
|
+
* component payload, or a truthy/falsey result for default and confirm dialogs.
|
|
29
|
+
* The assigned dialog id is exposed on `.id`.
|
|
30
|
+
* @typedef {Promise<unknown> & { id: string }} DialogPromise
|
|
31
|
+
*
|
|
32
|
+
* @typedef {Object} DialogApi
|
|
33
|
+
* @property {import('vue').Ref<Array<DialogOptions & { id: string }>>} dialogStack Reactive dialog array for <DomDialogStack>.
|
|
34
|
+
* @property {(input?: string|DialogOptions|DialogComponent, propsOrOptions?: DialogProps|DialogOptions, options?: DialogOptions) => DialogPromise} showDialog Show a dialog and resolve with the user action.
|
|
35
|
+
* @property {(input?: string|DialogOptions, options?: DialogOptions) => Promise<boolean>} confirmDialog Show a confirmation dialog and resolve true or false.
|
|
36
|
+
* @property {(input: DialogComponent|DialogFormSchema, propsOrOptions?: DialogProps|DialogOptions, options?: DialogOptions) => DialogPromise} dialogForm Show a custom component or schema-driven form dialog.
|
|
37
|
+
* @property {(id: string, value?: unknown) => void} resolveDialog Resolve and remove a dialog by id.
|
|
38
|
+
* @property {(id: string, value?: unknown) => void} dismissDialog Dismiss and remove a dialog by id.
|
|
39
|
+
* @property {() => void} clearDialogs Dismiss all dialogs.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a small controlled dialog store for <DomDialogStack>.
|
|
44
|
+
*
|
|
45
|
+
* @param {DialogOptions} [defaults] Options applied to every dialog shown by this store.
|
|
46
|
+
* @returns {DialogApi}
|
|
47
|
+
*/
|
|
48
|
+
export function useDialogs(defaults = {}) {
|
|
49
|
+
const dialogStack = ref([]);
|
|
50
|
+
const pending = new Map();
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Show a programmatic dialog.
|
|
54
|
+
*
|
|
55
|
+
* Strings and plain objects render a default body/footer:
|
|
56
|
+
* `showDialog({ title: 'Saved', message: 'Everything is up to date.' })`.
|
|
57
|
+
*
|
|
58
|
+
* Components render as custom dialog bodies:
|
|
59
|
+
* `showDialog(InviteForm, { email }, { title: 'Invite teammate' })`.
|
|
60
|
+
*
|
|
61
|
+
* @param {string|DialogOptions|DialogComponent} [input] Dialog title string, full dialog options, or Vue component body.
|
|
62
|
+
* @param {DialogProps|DialogOptions} [propsOrOptions] Props for component dialogs, or option overrides for default dialogs.
|
|
63
|
+
* @param {DialogOptions} [options] Additional options when the first argument is a Vue component.
|
|
64
|
+
* @returns {DialogPromise}
|
|
65
|
+
*/
|
|
66
|
+
function showDialog(input = {}, propsOrOptions = {}, options = {}) {
|
|
67
|
+
const dialog = normalizeDialog(input, propsOrOptions, options, defaults);
|
|
68
|
+
const promise = createDialogPromise(dialog);
|
|
69
|
+
dialogStack.value = [...dialogStack.value, dialog];
|
|
70
|
+
return promise;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Show a confirmation dialog and resolve with a boolean.
|
|
75
|
+
*
|
|
76
|
+
* `confirmDialog('Archive project?', { tone: 'danger', confirmText: 'Archive' })`.
|
|
77
|
+
*
|
|
78
|
+
* @param {string|DialogOptions} [input] Confirmation title string or full dialog options.
|
|
79
|
+
* @param {DialogOptions} [options] Additional option overrides merged into the confirm dialog.
|
|
80
|
+
* @returns {Promise<boolean>} True when confirmed, false when cancelled or dismissed.
|
|
81
|
+
*/
|
|
82
|
+
function confirmDialog(input = {}, options = {}) {
|
|
83
|
+
const base = typeof input === 'string' ? { title: input } : { ...input };
|
|
84
|
+
return showDialog({
|
|
85
|
+
type: 'confirm',
|
|
86
|
+
confirmText: 'Confirm',
|
|
87
|
+
...base,
|
|
88
|
+
...options,
|
|
89
|
+
}).then(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Show a custom component dialog or render a DomForm from a children schema.
|
|
94
|
+
*
|
|
95
|
+
* Component forms:
|
|
96
|
+
* `dialogForm(InviteForm, { defaultEmail }, { title: 'Invite teammate' })`.
|
|
97
|
+
*
|
|
98
|
+
* Schema forms:
|
|
99
|
+
* `dialogForm(schema, { initialValues: { email: '' } }, { title: 'Invite' })`.
|
|
100
|
+
*
|
|
101
|
+
* @param {DialogComponent|DialogFormSchema} input Vue component body or DomForm children schema.
|
|
102
|
+
* @param {DialogProps|DialogOptions} [propsOrOptions] Component props, or for schema forms `{ initialValues, values, ...dialogOptions }`.
|
|
103
|
+
* @param {DialogOptions} [options] Dialog chrome overrides such as title, description, and confirmText.
|
|
104
|
+
* @returns {DialogPromise}
|
|
105
|
+
*/
|
|
106
|
+
function dialogForm(input, propsOrOptions = {}, options = {}) {
|
|
107
|
+
if (Array.isArray(input)) {
|
|
108
|
+
const { initialValues, values, ...dialogOptions } = propsOrOptions || {};
|
|
109
|
+
return showDialog({
|
|
110
|
+
title: 'Form',
|
|
111
|
+
confirmText: 'Submit',
|
|
112
|
+
footer: false,
|
|
113
|
+
...dialogOptions,
|
|
114
|
+
...options,
|
|
115
|
+
type: 'form',
|
|
116
|
+
formSchema: input,
|
|
117
|
+
formValues: values || initialValues || {},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return showDialog(input, propsOrOptions, {
|
|
122
|
+
footer: false,
|
|
123
|
+
...options,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve and remove a dialog. Wire to <DomDialogStack> `@resolve`.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} id Dialog id from the returned promise `.id` or `dialogStack` entry.
|
|
131
|
+
* @param {unknown} [value] Value passed to the dialog promise resolver. Defaults to `true`.
|
|
132
|
+
* @returns {void}
|
|
133
|
+
*/
|
|
134
|
+
function resolveDialog(id, value = true) {
|
|
135
|
+
settle(id, value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Dismiss and remove a dialog. Wire to <DomDialogStack> `@dismiss`.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} id Dialog id from the returned promise `.id` or `dialogStack` entry.
|
|
142
|
+
* @param {unknown} [value] Value passed to the dialog promise resolver. Defaults to `false`.
|
|
143
|
+
* @returns {void}
|
|
144
|
+
*/
|
|
145
|
+
function dismissDialog(id, value = false) {
|
|
146
|
+
settle(id, value);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Dismiss all visible dialogs. Each open promise resolves with `false`.
|
|
151
|
+
*
|
|
152
|
+
* @returns {void}
|
|
153
|
+
*/
|
|
154
|
+
function clearDialogs() {
|
|
155
|
+
for (const dialog of dialogStack.value) dismissDialog(dialog.id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {DialogOptions & { id: string }} dialog
|
|
160
|
+
* @returns {DialogPromise}
|
|
161
|
+
*/
|
|
162
|
+
function createDialogPromise(dialog) {
|
|
163
|
+
let resolver = null;
|
|
164
|
+
const promise = new Promise((resolvePromise) => {
|
|
165
|
+
resolver = resolvePromise;
|
|
166
|
+
});
|
|
167
|
+
pending.set(dialog.id, resolver);
|
|
168
|
+
promise.id = dialog.id;
|
|
169
|
+
return promise;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} id
|
|
174
|
+
* @param {unknown} value
|
|
175
|
+
* @returns {void}
|
|
176
|
+
*/
|
|
177
|
+
function settle(id, value) {
|
|
178
|
+
const resolver = pending.get(id);
|
|
179
|
+
pending.delete(id);
|
|
180
|
+
dialogStack.value = dialogStack.value.filter((dialog) => dialog.id !== id);
|
|
181
|
+
if (resolver) resolver(value);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
dialogStack,
|
|
186
|
+
showDialog,
|
|
187
|
+
confirmDialog,
|
|
188
|
+
dialogForm,
|
|
189
|
+
resolveDialog,
|
|
190
|
+
dismissDialog,
|
|
191
|
+
clearDialogs,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Merge caller input into a normalized dialog record for the stack.
|
|
197
|
+
*
|
|
198
|
+
* @param {string|DialogOptions|DialogComponent} input
|
|
199
|
+
* @param {DialogProps|DialogOptions} propsOrOptions
|
|
200
|
+
* @param {DialogOptions} options
|
|
201
|
+
* @param {DialogOptions} defaults
|
|
202
|
+
* @returns {DialogOptions & { id: string }}
|
|
203
|
+
*/
|
|
204
|
+
function normalizeDialog(input, propsOrOptions, options, defaults) {
|
|
205
|
+
const isComponentDialog = isDialogComponent(input);
|
|
206
|
+
const base = isComponentDialog
|
|
207
|
+
? { component: input, props: propsOrOptions || {}, footer: false }
|
|
208
|
+
: typeof input === 'string'
|
|
209
|
+
? { title: input }
|
|
210
|
+
: { ...input };
|
|
211
|
+
const overrides = isComponentDialog ? options : propsOrOptions;
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
id: createDialogId(),
|
|
215
|
+
type: 'dialog',
|
|
216
|
+
backdrop: true,
|
|
217
|
+
static: false,
|
|
218
|
+
footer: true,
|
|
219
|
+
cancelText: 'Cancel',
|
|
220
|
+
confirmText: 'OK',
|
|
221
|
+
invalidMessage: 'Complete the required fields before continuing.',
|
|
222
|
+
tone: 'default',
|
|
223
|
+
...defaults,
|
|
224
|
+
...base,
|
|
225
|
+
...overrides,
|
|
226
|
+
props: {
|
|
227
|
+
...(defaults.props || {}),
|
|
228
|
+
...(base.props || {}),
|
|
229
|
+
...(overrides.props || {}),
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {unknown} value
|
|
236
|
+
* @returns {value is DialogComponent}
|
|
237
|
+
*/
|
|
238
|
+
function isDialogComponent(value) {
|
|
239
|
+
return Boolean(
|
|
240
|
+
value
|
|
241
|
+
&& (typeof value === 'function'
|
|
242
|
+
|| (typeof value === 'object'
|
|
243
|
+
&& (value.setup || value.render || value.template || value.__name || value.__file))),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
function createDialogId() {
|
|
251
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
|
252
|
+
return `dialog-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
253
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onMounted, ref, useId, watch } from 'vue';
|
|
3
|
+
|
|
4
|
+
defineOptions({
|
|
5
|
+
__doc: {
|
|
6
|
+
name: 'Drawer',
|
|
7
|
+
tag: '<DomDrawer>',
|
|
8
|
+
description: 'A slide-out panel for navigation menus, filters, or secondary app panels with backdrop dismiss, drag dismiss, scroll lock, and focus trap built in.',
|
|
9
|
+
slots: [
|
|
10
|
+
{ name: 'trigger', description: 'Element that opens the drawer when clicked (omit when controlling open state yourself).' },
|
|
11
|
+
{ name: '(default)', description: 'Panel body.' },
|
|
12
|
+
{ name: 'header', description: 'Replace the default header row (title + close button).' },
|
|
13
|
+
{ name: 'footer', description: 'Optional footer below the scrollable body.' },
|
|
14
|
+
],
|
|
15
|
+
keyboard: [
|
|
16
|
+
{ keys: 'Esc', action: 'Closes the drawer.' },
|
|
17
|
+
{ keys: 'Tab / Shift+Tab', action: 'Focus stays within the panel while open.' },
|
|
18
|
+
{ keys: 'Click backdrop', action: 'Dismisses the drawer. Add `static` on the host to disable.' },
|
|
19
|
+
{ keys: 'Drag handle', action: 'Bottom drawers can be dragged down from the pill handle to close.' },
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
const props = defineProps({
|
|
27
|
+
modelValue: {
|
|
28
|
+
type: Boolean,
|
|
29
|
+
default: false,
|
|
30
|
+
_edit: { description: 'Whether the drawer is open.' },
|
|
31
|
+
},
|
|
32
|
+
side: {
|
|
33
|
+
type: String,
|
|
34
|
+
default: 'right',
|
|
35
|
+
_edit: { options: ['left', 'right', 'bottom'], description: 'Edge the panel slides in from.' },
|
|
36
|
+
},
|
|
37
|
+
title: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: '',
|
|
40
|
+
_edit: { description: 'Optional heading at the top of the panel.' },
|
|
41
|
+
},
|
|
42
|
+
width: {
|
|
43
|
+
type: String,
|
|
44
|
+
default: 'var(--container-sm)',
|
|
45
|
+
_edit: {
|
|
46
|
+
description: 'Panel width as a CSS length — pick a Tailwind container variable or type your own (e.g. 50rem).',
|
|
47
|
+
component: 'DomCombobox',
|
|
48
|
+
props: {
|
|
49
|
+
// Tailwind v4 max-w-* resolves to var(--container-*). Store the variable so
|
|
50
|
+
// theme overrides apply; combobox hints show the matching utility name.
|
|
51
|
+
options: [
|
|
52
|
+
{ value: 'var(--container-3xs)', label: 'var(--container-3xs) · max-w-3xs' },
|
|
53
|
+
{ value: 'var(--container-2xs)', label: 'var(--container-2xs) · max-w-2xs' },
|
|
54
|
+
{ value: 'var(--container-xs)', label: 'var(--container-xs) · max-w-xs' },
|
|
55
|
+
{ value: 'var(--container-sm)', label: 'var(--container-sm) · max-w-sm' },
|
|
56
|
+
{ value: 'var(--container-md)', label: 'var(--container-md) · max-w-md' },
|
|
57
|
+
{ value: 'var(--container-lg)', label: 'var(--container-lg) · max-w-lg' },
|
|
58
|
+
{ value: 'var(--container-xl)', label: 'var(--container-xl) · max-w-xl' },
|
|
59
|
+
{ value: 'var(--container-2xl)', label: 'var(--container-2xl) · max-w-2xl' },
|
|
60
|
+
{ value: 'var(--container-3xl)', label: 'var(--container-3xl) · max-w-3xl' },
|
|
61
|
+
{ value: 'var(--container-4xl)', label: 'var(--container-4xl) · max-w-4xl' },
|
|
62
|
+
{ value: 'var(--container-5xl)', label: 'var(--container-5xl) · max-w-5xl' },
|
|
63
|
+
{ value: 'var(--container-6xl)', label: 'var(--container-6xl) · max-w-6xl' },
|
|
64
|
+
{ value: 'var(--container-7xl)', label: 'var(--container-7xl) · max-w-7xl' },
|
|
65
|
+
],
|
|
66
|
+
placeholder: 'var(--container-sm)',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
static: {
|
|
71
|
+
type: Boolean,
|
|
72
|
+
default: false,
|
|
73
|
+
_edit: { description: 'Disable backdrop-click dismiss. Esc and data-close still work.' },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
const emit = defineEmits(['update:modelValue', 'open', 'close']);
|
|
77
|
+
|
|
78
|
+
const drawer = ref(null);
|
|
79
|
+
const uniqueId = useId();
|
|
80
|
+
const titleId = computed(() => (props.title ? `dom-drawer-${uniqueId}-title` : ''));
|
|
81
|
+
|
|
82
|
+
const hostStyle = computed(() => ({
|
|
83
|
+
'--dom-drawer-width': props.width,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
function setOpen(v) {
|
|
87
|
+
if (drawer.value) drawer.value.open = v;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
onMounted(async () => {
|
|
91
|
+
await import('../../lib/headless/drawer.js');
|
|
92
|
+
drawer.value?.addEventListener('dom:open', () => {
|
|
93
|
+
emit('update:modelValue', true);
|
|
94
|
+
emit('open');
|
|
95
|
+
});
|
|
96
|
+
drawer.value?.addEventListener('dom:close', () => {
|
|
97
|
+
emit('update:modelValue', false);
|
|
98
|
+
emit('close');
|
|
99
|
+
});
|
|
100
|
+
if (props.modelValue) setOpen(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
watch(() => props.modelValue, setOpen);
|
|
104
|
+
|
|
105
|
+
defineExpose({
|
|
106
|
+
open: () => setOpen(true),
|
|
107
|
+
close: () => setOpen(false),
|
|
108
|
+
toggle: () => { if (drawer.value) drawer.value.toggle(); },
|
|
109
|
+
get element() { return drawer.value; },
|
|
110
|
+
});
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<dom-drawer
|
|
115
|
+
ref="drawer"
|
|
116
|
+
:side="side"
|
|
117
|
+
:static="static || null"
|
|
118
|
+
:style="hostStyle"
|
|
119
|
+
:aria-labelledby="titleId || null"
|
|
120
|
+
>
|
|
121
|
+
<span slot="trigger" class="contents">
|
|
122
|
+
<slot name="trigger" />
|
|
123
|
+
</span>
|
|
124
|
+
<div
|
|
125
|
+
class="skin-overlay flex h-full flex-col"
|
|
126
|
+
>
|
|
127
|
+
<button
|
|
128
|
+
v-if="side === 'bottom'"
|
|
129
|
+
data-drag-handle
|
|
130
|
+
type="button"
|
|
131
|
+
class="group mx-auto mt-1 flex h-9 w-28 shrink-0 items-center justify-center"
|
|
132
|
+
aria-label="Drag to close drawer"
|
|
133
|
+
>
|
|
134
|
+
<span class="block h-1.5 w-12 rounded-full bg-muted-fg/35 transition group-hover:bg-muted-fg/50"></span>
|
|
135
|
+
</button>
|
|
136
|
+
<header v-if="title || $slots.header" class="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-4">
|
|
137
|
+
<slot name="header">
|
|
138
|
+
<h2 v-if="title" :id="titleId" class="text-base font-semibold tracking-tight text-canvas-fg">{{ title }}</h2>
|
|
139
|
+
</slot>
|
|
140
|
+
<button
|
|
141
|
+
data-close
|
|
142
|
+
type="button"
|
|
143
|
+
class="skin-control inline-flex size-9 shrink-0 items-center justify-center rounded-full text-muted-fg transition hover:brightness-[0.98]"
|
|
144
|
+
aria-label="Close"
|
|
145
|
+
>
|
|
146
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5" aria-hidden="true">
|
|
147
|
+
<path d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" />
|
|
148
|
+
</svg>
|
|
149
|
+
</button>
|
|
150
|
+
</header>
|
|
151
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
152
|
+
<slot />
|
|
153
|
+
</div>
|
|
154
|
+
<footer v-if="$slots.footer" class="shrink-0 border-t border-border p-4">
|
|
155
|
+
<slot name="footer" />
|
|
156
|
+
</footer>
|
|
157
|
+
</div>
|
|
158
|
+
</dom-drawer>
|
|
159
|
+
</template>
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, nextTick, onMounted, ref, shallowRef, useId, useSlots } from 'vue';
|
|
3
|
+
import { resolveFloatingTeleportTarget } from '../../lib/vue/floatingTeleportTarget.js';
|
|
4
|
+
|
|
5
|
+
defineOptions({
|
|
6
|
+
__doc: {
|
|
7
|
+
name: 'Dropdown',
|
|
8
|
+
tag: '<DomDropdown>',
|
|
9
|
+
description: 'A menu that opens from a button — fully keyboard accessible, with focus return, optional scroll lock, and outside-click handling.',
|
|
10
|
+
slots: [
|
|
11
|
+
{ name: '(default)', payload: '{ close, open, toggle }', description: 'Custom panel content, usually a DomMenu composed with MenuItem rows.' },
|
|
12
|
+
{ name: 'trigger', description: 'Replaces the inner content of the trigger button.' },
|
|
13
|
+
{ name: 'item', payload: '{ item, index }', description: 'Replaces the rendering of each menu item.' },
|
|
14
|
+
{ name: 'panel', payload: '{ close, open, toggle }', description: 'Replaces menu items with custom dropdown content, such as a tree view or form.' },
|
|
15
|
+
],
|
|
16
|
+
events: [
|
|
17
|
+
{ name: 'select', payload: '(value: string)', description: 'Fired when a menu item is chosen.' },
|
|
18
|
+
],
|
|
19
|
+
keyboard: [
|
|
20
|
+
{ keys: 'Enter / Space / ↓', action: 'Open menu (when trigger is focused).' },
|
|
21
|
+
{ keys: '↑ / ↓', action: 'Move active item.' },
|
|
22
|
+
{ keys: 'Home / End', action: 'Jump to first / last item.' },
|
|
23
|
+
{ keys: 'Enter', action: 'Select active item.' },
|
|
24
|
+
{ keys: 'Esc / Tab', action: 'Close menu and return focus.' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const menuId = `dom-dropdown-${useId()}`;
|
|
30
|
+
|
|
31
|
+
const props = defineProps({
|
|
32
|
+
items: {
|
|
33
|
+
type: Array,
|
|
34
|
+
default: () => [],
|
|
35
|
+
_edit: {
|
|
36
|
+
component: 'DomJsonListInput',
|
|
37
|
+
description: 'Menu items shown when the dropdown opens.',
|
|
38
|
+
props: {
|
|
39
|
+
compact: true,
|
|
40
|
+
addLabel: '+ Add item',
|
|
41
|
+
schema: [
|
|
42
|
+
{ key: 'label', label: 'Label', placeholder: 'Menu item', default: (index) => `Item ${index + 1}` },
|
|
43
|
+
{ key: 'value', label: 'Value', placeholder: 'item-value', default: (index) => `item-${index + 1}` },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
label: {
|
|
49
|
+
type: String,
|
|
50
|
+
default: 'Options',
|
|
51
|
+
_edit: { description: 'Trigger button label (overridden by the #trigger slot if used).' },
|
|
52
|
+
},
|
|
53
|
+
align: {
|
|
54
|
+
type: String,
|
|
55
|
+
default: 'left',
|
|
56
|
+
_edit: {
|
|
57
|
+
options: ['left', 'right'],
|
|
58
|
+
description: 'Where the menu opens relative to the trigger.',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
placement: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: 'bottom',
|
|
64
|
+
_edit: {
|
|
65
|
+
options: ['bottom', 'top', 'right', 'left'],
|
|
66
|
+
description: 'Preferred side before collision handling.',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
collisionPadding: {
|
|
70
|
+
type: Number,
|
|
71
|
+
default: 8,
|
|
72
|
+
_edit: { description: 'Viewport padding used when the menu flips or shifts.' },
|
|
73
|
+
},
|
|
74
|
+
floatingMode: {
|
|
75
|
+
type: String,
|
|
76
|
+
default: 'viewport',
|
|
77
|
+
_edit: { options: ['viewport', 'anchor'], description: 'viewport keeps the menu inside the browser; anchor keeps it attached while scrolling.' },
|
|
78
|
+
},
|
|
79
|
+
lockScroll: {
|
|
80
|
+
type: Boolean,
|
|
81
|
+
default: false,
|
|
82
|
+
_edit: { description: 'Lock browser scrolling while the dropdown is open.' },
|
|
83
|
+
},
|
|
84
|
+
width: {
|
|
85
|
+
type: String,
|
|
86
|
+
default: 'min-w-[12rem]',
|
|
87
|
+
_edit: { description: 'Tailwind width utility for the menu (e.g. min-w-[16rem]).' },
|
|
88
|
+
},
|
|
89
|
+
panelType: {
|
|
90
|
+
type: String,
|
|
91
|
+
default: 'menu',
|
|
92
|
+
_edit: {
|
|
93
|
+
options: ['menu', 'tree', 'dialog', 'listbox', 'grid'],
|
|
94
|
+
description: 'Accessible popup type. Use tree/dialog/etc when rendering the #panel slot.',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
autoFocusPanel: {
|
|
98
|
+
type: Boolean,
|
|
99
|
+
default: true,
|
|
100
|
+
_edit: { description: 'Focus the first focusable element when opening a custom panel.' },
|
|
101
|
+
},
|
|
102
|
+
focusValue: {
|
|
103
|
+
type: [String, Number],
|
|
104
|
+
default: null,
|
|
105
|
+
_edit: { description: 'Menu item value to prefer when opening from the keyboard.' },
|
|
106
|
+
},
|
|
107
|
+
disabled: {
|
|
108
|
+
type: Boolean,
|
|
109
|
+
default: false,
|
|
110
|
+
_edit: { description: 'Disable the trigger and prevent the dropdown from opening.' },
|
|
111
|
+
},
|
|
112
|
+
triggerClass: {
|
|
113
|
+
type: [String, Array, Object],
|
|
114
|
+
default: null,
|
|
115
|
+
_edit: { description: 'Override the trigger button classes when composing dropdowns into toolbars or compact controls.' },
|
|
116
|
+
},
|
|
117
|
+
showIndicator: {
|
|
118
|
+
type: Boolean,
|
|
119
|
+
default: true,
|
|
120
|
+
_edit: { description: 'Show the chevron indicator in the trigger.' },
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
const emit = defineEmits(['select']);
|
|
124
|
+
|
|
125
|
+
const root = ref(null);
|
|
126
|
+
const isMounted = ref(false);
|
|
127
|
+
const teleportTarget = shallowRef(null);
|
|
128
|
+
const slots = useSlots();
|
|
129
|
+
const hasDefaultPanel = computed(() => Boolean(slots.default && !slots.panel));
|
|
130
|
+
const hasCustomPanel = computed(() => Boolean(slots.panel || slots.default));
|
|
131
|
+
|
|
132
|
+
onMounted(async () => {
|
|
133
|
+
teleportTarget.value = resolveFloatingTeleportTarget(root.value);
|
|
134
|
+
isMounted.value = true;
|
|
135
|
+
await nextTick();
|
|
136
|
+
await import('../../lib/headless/dropdown.js');
|
|
137
|
+
root.value?.addEventListener('dom:select', (e) => emit('select', e.detail.value));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the visible label for a dropdown item.
|
|
142
|
+
*
|
|
143
|
+
* @param {string|number|Record<string, unknown>} item Dropdown item definition.
|
|
144
|
+
* @returns {unknown} Item label, value fallback, or primitive item.
|
|
145
|
+
*/
|
|
146
|
+
function labelOf(item) {
|
|
147
|
+
return item && typeof item === 'object' ? (item.label ?? item.value) : item;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Resolve the submitted value for a dropdown item.
|
|
152
|
+
*
|
|
153
|
+
* @param {string|number|Record<string, unknown>} item Dropdown item definition.
|
|
154
|
+
* @returns {unknown} Item value, label fallback, or primitive item.
|
|
155
|
+
*/
|
|
156
|
+
function valueOf(item) {
|
|
157
|
+
return item && typeof item === 'object' ? (item.value ?? item.label) : item;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Open the dropdown panel programmatically.
|
|
162
|
+
*
|
|
163
|
+
* @returns {void}
|
|
164
|
+
*/
|
|
165
|
+
function open() {
|
|
166
|
+
if (root.value) root.value.open = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Close the dropdown panel programmatically.
|
|
171
|
+
*
|
|
172
|
+
* @returns {void}
|
|
173
|
+
*/
|
|
174
|
+
function close() {
|
|
175
|
+
if (root.value) root.value.open = false;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Toggle the dropdown panel programmatically.
|
|
180
|
+
*
|
|
181
|
+
* @returns {void}
|
|
182
|
+
*/
|
|
183
|
+
function toggle() {
|
|
184
|
+
root.value?.toggle?.();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Forward custom panel selections and close one-shot menu rows.
|
|
189
|
+
*
|
|
190
|
+
* @param {CustomEvent<{ value?: string, item?: Element }>} event Selection event from nested menu primitives.
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
193
|
+
function onPanelDomSelect(event) {
|
|
194
|
+
if (!slots.default) return;
|
|
195
|
+
emit('select', event.detail?.value);
|
|
196
|
+
const role = event.detail?.item?.getAttribute?.('role');
|
|
197
|
+
if (role === 'menuitemcheckbox' || role === 'menuitemradio') return;
|
|
198
|
+
close();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
defineExpose({
|
|
202
|
+
open,
|
|
203
|
+
close,
|
|
204
|
+
toggle,
|
|
205
|
+
});
|
|
206
|
+
</script>
|
|
207
|
+
|
|
208
|
+
<template>
|
|
209
|
+
<dom-dropdown
|
|
210
|
+
ref="root"
|
|
211
|
+
:data-menu-id="menuId"
|
|
212
|
+
:align="align"
|
|
213
|
+
:offset="4"
|
|
214
|
+
:placement="placement"
|
|
215
|
+
:collision-padding="collisionPadding"
|
|
216
|
+
:floating-mode="floatingMode"
|
|
217
|
+
:lock-scroll="lockScroll ? '' : null"
|
|
218
|
+
class="relative inline-block"
|
|
219
|
+
>
|
|
220
|
+
<button
|
|
221
|
+
slot="trigger"
|
|
222
|
+
type="button"
|
|
223
|
+
:disabled="disabled || null"
|
|
224
|
+
:class="triggerClass || 'skin-control inline-flex h-10 items-center gap-2 rounded-full px-4 text-sm font-medium transition hover:brightness-[0.98] focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 disabled:cursor-not-allowed disabled:opacity-50'"
|
|
225
|
+
>
|
|
226
|
+
<slot name="trigger">{{ label }}</slot>
|
|
227
|
+
<svg v-if="showIndicator" viewBox="0 0 20 20" class="size-4 opacity-70" fill="none">
|
|
228
|
+
<path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
|
229
|
+
</svg>
|
|
230
|
+
</button>
|
|
231
|
+
<Teleport :to="teleportTarget || 'body'" :disabled="!isMounted || !teleportTarget">
|
|
232
|
+
<div
|
|
233
|
+
:id="menuId"
|
|
234
|
+
popover="manual"
|
|
235
|
+
class="dom-dropdown-menu dom-floating-transition"
|
|
236
|
+
:class="[width, hasDefaultPanel ? '' : 'skin-popover rounded-2xl p-1']"
|
|
237
|
+
:data-composite-panel="hasCustomPanel ? 'true' : null"
|
|
238
|
+
:data-haspopup="hasCustomPanel ? panelType : null"
|
|
239
|
+
:data-autofocus-panel="autoFocusPanel ? null : 'false'"
|
|
240
|
+
:data-focus-value="focusValue === null || focusValue === undefined || focusValue === '' ? null : String(focusValue)"
|
|
241
|
+
@dom:select="onPanelDomSelect"
|
|
242
|
+
>
|
|
243
|
+
<slot v-if="$slots.panel" name="panel" :close="close" :open="open" :toggle="toggle" />
|
|
244
|
+
<slot v-else-if="$slots.default" :close="close" :open="open" :toggle="toggle" />
|
|
245
|
+
<template v-else>
|
|
246
|
+
<template v-for="(item, i) in items" :key="valueOf(item) ?? i">
|
|
247
|
+
<hr v-if="item && item.separator" class="my-1 border-t border-border" />
|
|
248
|
+
<button
|
|
249
|
+
v-else
|
|
250
|
+
role="menuitem"
|
|
251
|
+
:data-value="valueOf(item)"
|
|
252
|
+
class="block w-full rounded-xl px-3 py-2 text-left text-sm outline-none transition hover:bg-accent focus:bg-accent"
|
|
253
|
+
>
|
|
254
|
+
<slot name="item" :item="item" :index="i">{{ labelOf(item) }}</slot>
|
|
255
|
+
</button>
|
|
256
|
+
</template>
|
|
257
|
+
</template>
|
|
258
|
+
</div>
|
|
259
|
+
</Teleport>
|
|
260
|
+
</dom-dropdown>
|
|
261
|
+
</template>
|