@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,482 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue';
|
|
3
|
+
import DomLiquidGlassSurface from '../../lib/vue/_internal/DomLiquidGlassSurface.vue';
|
|
4
|
+
import { LIQUID_GLASS_EFFECTS } from '../../lib/vue/_internal/liquidGlassConfig.js';
|
|
5
|
+
|
|
6
|
+
defineOptions({
|
|
7
|
+
__doc: {
|
|
8
|
+
name: 'Command palette',
|
|
9
|
+
tag: '<DomCommandPalette>',
|
|
10
|
+
description: 'A keyboard-first command launcher composed from dialog and listbox patterns.',
|
|
11
|
+
events: [
|
|
12
|
+
{ name: 'update:modelValue', payload: '(open: boolean)', description: 'Emitted when the palette opens or closes.' },
|
|
13
|
+
{ name: 'select', payload: '({ command, value })', description: 'Emitted when a command is selected.' },
|
|
14
|
+
],
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const props = defineProps({
|
|
19
|
+
modelValue: {
|
|
20
|
+
type: Boolean,
|
|
21
|
+
default: false,
|
|
22
|
+
_edit: { description: 'Whether the palette is open.' },
|
|
23
|
+
},
|
|
24
|
+
commands: {
|
|
25
|
+
type: Array,
|
|
26
|
+
required: true,
|
|
27
|
+
_edit: { component: 'DomJsonListInput', description: 'Commands shown in the palette.', props: { compact: true } },
|
|
28
|
+
},
|
|
29
|
+
placeholder: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: 'Search commands...',
|
|
32
|
+
_edit: { description: 'Input placeholder.' },
|
|
33
|
+
},
|
|
34
|
+
shortcut: {
|
|
35
|
+
type: String,
|
|
36
|
+
default: 'mod+k',
|
|
37
|
+
_edit: { description: 'Global keyboard shortcut. Use mod for Cmd on Mac and Ctrl elsewhere. Set empty string to disable.' },
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const emit = defineEmits(['update:modelValue', 'select']);
|
|
41
|
+
|
|
42
|
+
const query = ref('');
|
|
43
|
+
const activeIndex = ref(0);
|
|
44
|
+
const input = ref(null);
|
|
45
|
+
const listPanel = ref(null);
|
|
46
|
+
const panel = ref(null);
|
|
47
|
+
const isMounted = ref(false);
|
|
48
|
+
const isRendered = ref(false);
|
|
49
|
+
const isVisible = ref(false);
|
|
50
|
+
const motion = ref('closed');
|
|
51
|
+
const teleportTarget = shallowRef(null);
|
|
52
|
+
const listId = `dom-command-palette-${useId()}-list`;
|
|
53
|
+
const dialogLabel = 'Command palette';
|
|
54
|
+
const useLiquidGlass = LIQUID_GLASS_EFFECTS.commandPalette;
|
|
55
|
+
|
|
56
|
+
const filtered = computed(() => {
|
|
57
|
+
const q = normalizeSearchText(query.value);
|
|
58
|
+
if (!q) return props.commands;
|
|
59
|
+
return props.commands
|
|
60
|
+
.map((command, index) => ({
|
|
61
|
+
command,
|
|
62
|
+
index,
|
|
63
|
+
score: commandSearchScore(command, q),
|
|
64
|
+
}))
|
|
65
|
+
.filter((result) => Number.isFinite(result.score))
|
|
66
|
+
.sort((a, b) => a.score - b.score || a.index - b.index)
|
|
67
|
+
.map((result) => result.command);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
watch(() => props.modelValue, async (open) => {
|
|
71
|
+
if (!open) {
|
|
72
|
+
if (!isRendered.value) return;
|
|
73
|
+
isVisible.value = false;
|
|
74
|
+
motion.value = 'leaving';
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isRendered.value = true;
|
|
79
|
+
motion.value = 'entering';
|
|
80
|
+
query.value = '';
|
|
81
|
+
activeIndex.value = 0;
|
|
82
|
+
await nextTick();
|
|
83
|
+
scrollActiveCommandIntoView();
|
|
84
|
+
window.requestAnimationFrame(() => {
|
|
85
|
+
isVisible.value = true;
|
|
86
|
+
input.value?.focus();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
watch(query, resetActiveIndex);
|
|
90
|
+
watch(filtered, syncActiveIndex);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns keyboard navigation to the first filtered command.
|
|
94
|
+
*
|
|
95
|
+
* @returns {void}
|
|
96
|
+
*/
|
|
97
|
+
function resetActiveIndex() {
|
|
98
|
+
activeIndex.value = 0;
|
|
99
|
+
scrollActiveCommandIntoView();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Keeps the active index valid when filtering changes the command collection.
|
|
104
|
+
*
|
|
105
|
+
* @param {Array<Record<string, unknown>>} items Filtered command collection.
|
|
106
|
+
* @returns {void}
|
|
107
|
+
*/
|
|
108
|
+
function syncActiveIndex(items) {
|
|
109
|
+
if (activeIndex.value >= items.length) activeIndex.value = Math.max(0, items.length - 1);
|
|
110
|
+
scrollActiveCommandIntoView();
|
|
111
|
+
}
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
teleportTarget.value = panel.value?.ownerDocument?.body || document.body;
|
|
114
|
+
isMounted.value = true;
|
|
115
|
+
window.addEventListener('keydown', onShortcut, { capture: true });
|
|
116
|
+
document.addEventListener('keydown', onShortcut, { capture: true });
|
|
117
|
+
window.addEventListener('keyup', onShortcutFallback, { capture: true });
|
|
118
|
+
});
|
|
119
|
+
onBeforeUnmount(() => {
|
|
120
|
+
window.removeEventListener('keydown', onShortcut, { capture: true });
|
|
121
|
+
document.removeEventListener('keydown', onShortcut, { capture: true });
|
|
122
|
+
window.removeEventListener('keyup', onShortcutFallback, { capture: true });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Completes the current panel motion after the browser renders its final frame.
|
|
127
|
+
*
|
|
128
|
+
* @returns {void}
|
|
129
|
+
*/
|
|
130
|
+
function onPanelAnimationEnd() {
|
|
131
|
+
if (motion.value === 'entering' && props.modelValue) {
|
|
132
|
+
motion.value = 'open';
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (motion.value !== 'leaving' || props.modelValue) return;
|
|
136
|
+
isRendered.value = false;
|
|
137
|
+
motion.value = 'closed';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function close() {
|
|
141
|
+
emit('update:modelValue', false);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function open() {
|
|
145
|
+
if (props.modelValue) {
|
|
146
|
+
input.value?.focus();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
emit('update:modelValue', true);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function move(delta) {
|
|
153
|
+
if (!filtered.value.length) return;
|
|
154
|
+
activeIndex.value = (activeIndex.value + delta + filtered.value.length) % filtered.value.length;
|
|
155
|
+
scrollActiveCommandIntoView();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Keeps the highlighted command fully visible inside the scrollable list.
|
|
160
|
+
*
|
|
161
|
+
* @returns {void}
|
|
162
|
+
*/
|
|
163
|
+
function scrollActiveCommandIntoView() {
|
|
164
|
+
nextTick(() => {
|
|
165
|
+
const list = listPanel.value;
|
|
166
|
+
const option = list?.querySelector?.('[aria-selected="true"]');
|
|
167
|
+
if (!list || !option) return;
|
|
168
|
+
|
|
169
|
+
const listRect = list.getBoundingClientRect();
|
|
170
|
+
const optionRect = option.getBoundingClientRect();
|
|
171
|
+
const edgeInset = 4;
|
|
172
|
+
if (optionRect.top < listRect.top + edgeInset) {
|
|
173
|
+
list.scrollTop += optionRect.top - listRect.top - edgeInset;
|
|
174
|
+
} else if (optionRect.bottom > listRect.bottom - edgeInset) {
|
|
175
|
+
list.scrollTop += optionRect.bottom - listRect.bottom + edgeInset;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function select(command = filtered.value[activeIndex.value]) {
|
|
181
|
+
if (!command) return;
|
|
182
|
+
emit('select', { command, value: command.value ?? command.label });
|
|
183
|
+
close();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function onKeydown(event) {
|
|
187
|
+
if (event.key === 'Escape') {
|
|
188
|
+
event.preventDefault();
|
|
189
|
+
close();
|
|
190
|
+
} else if (event.key === 'Tab') {
|
|
191
|
+
const nodes = panel.value?.querySelectorAll('button:not([disabled]):not([tabindex="-1"]), input:not([disabled]), [tabindex]:not([tabindex="-1"])') || [];
|
|
192
|
+
const first = nodes[0];
|
|
193
|
+
const last = nodes[nodes.length - 1];
|
|
194
|
+
if (!first || !last) return;
|
|
195
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
196
|
+
event.preventDefault();
|
|
197
|
+
last.focus();
|
|
198
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
first.focus();
|
|
201
|
+
}
|
|
202
|
+
} else if (event.key === 'ArrowDown') {
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
move(1);
|
|
205
|
+
} else if (event.key === 'ArrowUp') {
|
|
206
|
+
event.preventDefault();
|
|
207
|
+
move(-1);
|
|
208
|
+
} else if (event.key === 'Enter') {
|
|
209
|
+
event.preventDefault();
|
|
210
|
+
select();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Rank a command match so title matches appear before supporting metadata matches.
|
|
216
|
+
*
|
|
217
|
+
* @param {Record<string, unknown>} command Command candidate.
|
|
218
|
+
* @param {string} query Normalized search query.
|
|
219
|
+
* @returns {number} Lower relevance score, or positive infinity when the command does not match.
|
|
220
|
+
*/
|
|
221
|
+
function commandSearchScore(command, query) {
|
|
222
|
+
const title = normalizeSearchText(command.label);
|
|
223
|
+
if (title === query) return 0;
|
|
224
|
+
if (title.startsWith(`${query} `)) return 10;
|
|
225
|
+
if (title.split(/[\s/\-–—:]+/).includes(query)) return 20;
|
|
226
|
+
if (title.startsWith(query)) return 30;
|
|
227
|
+
if (title.includes(query)) return 40;
|
|
228
|
+
return searchableCommandText(command).includes(query) ? 100 : Number.POSITIVE_INFINITY;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Build the broad searchable text for a command's secondary fields.
|
|
233
|
+
*
|
|
234
|
+
* @param {Record<string, unknown>} command Command candidate.
|
|
235
|
+
* @returns {string} Normalized searchable command text.
|
|
236
|
+
*/
|
|
237
|
+
function searchableCommandText(command) {
|
|
238
|
+
return normalizeSearchText([
|
|
239
|
+
command.label,
|
|
240
|
+
command.description,
|
|
241
|
+
command.value,
|
|
242
|
+
command.to,
|
|
243
|
+
command.search,
|
|
244
|
+
command.keywords,
|
|
245
|
+
]
|
|
246
|
+
.flatMap((part) => Array.isArray(part) ? part : [part])
|
|
247
|
+
.map((part) => String(part || '').trim())
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.join(' '));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Normalize user-entered and command-provided text for predictable matching.
|
|
254
|
+
*
|
|
255
|
+
* @param {unknown} value Text-like value to normalize.
|
|
256
|
+
* @returns {string} Lowercase text with trimmed, collapsed whitespace.
|
|
257
|
+
*/
|
|
258
|
+
function normalizeSearchText(value) {
|
|
259
|
+
return String(value || '')
|
|
260
|
+
.trim()
|
|
261
|
+
.toLowerCase()
|
|
262
|
+
.replace(/\s+/g, ' ');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function onShortcut(event) {
|
|
266
|
+
if (isSiteSearchShortcut()) return;
|
|
267
|
+
if (event.defaultPrevented || event.__domStudioGlobalSearchHandled) return;
|
|
268
|
+
if (!matchesShortcut(event, props.shortcut)) return;
|
|
269
|
+
if (event.repeat) return;
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
event.stopPropagation();
|
|
272
|
+
open();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function onShortcutFallback(event) {
|
|
276
|
+
if (props.modelValue) return;
|
|
277
|
+
if (isSiteSearchShortcut()) return;
|
|
278
|
+
if (event.defaultPrevented || event.__domStudioGlobalSearchHandled) return;
|
|
279
|
+
if (!matchesShortcut(event, props.shortcut)) return;
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
event.stopPropagation();
|
|
282
|
+
open();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isSiteSearchShortcut() {
|
|
286
|
+
return typeof window !== 'undefined'
|
|
287
|
+
&& Boolean(window.__domStudioGlobalSearchShortcutHandler)
|
|
288
|
+
&& normalizeShortcut(props.shortcut) === 'mod+k';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeShortcut(shortcut) {
|
|
292
|
+
return String(shortcut || '')
|
|
293
|
+
.toLowerCase()
|
|
294
|
+
.split('+')
|
|
295
|
+
.map((part) => part.trim())
|
|
296
|
+
.filter(Boolean)
|
|
297
|
+
.join('+');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function matchesShortcut(event, shortcut) {
|
|
301
|
+
if (!shortcut) return false;
|
|
302
|
+
const parts = shortcut.toLowerCase().split('+').map((part) => part.trim()).filter(Boolean);
|
|
303
|
+
const key = parts.find((part) => !['mod', 'meta', 'cmd', 'command', 'ctrl', 'control', 'alt', 'option', 'shift'].includes(part));
|
|
304
|
+
if (!key) return false;
|
|
305
|
+
|
|
306
|
+
const wantsMod = parts.includes('mod');
|
|
307
|
+
const wantsMeta = wantsMod || parts.includes('meta') || parts.includes('cmd') || parts.includes('command');
|
|
308
|
+
const wantsCtrl = wantsMod || parts.includes('ctrl') || parts.includes('control');
|
|
309
|
+
const wantsAlt = parts.includes('alt') || parts.includes('option');
|
|
310
|
+
const wantsShift = parts.includes('shift');
|
|
311
|
+
const hasMod = event.metaKey || event.ctrlKey;
|
|
312
|
+
const keyMatches = event.key.toLowerCase() === key;
|
|
313
|
+
|
|
314
|
+
if (wantsMod && !hasMod) return false;
|
|
315
|
+
if (!wantsMod && event.metaKey !== wantsMeta) return false;
|
|
316
|
+
if (!wantsMod && event.ctrlKey !== wantsCtrl) return false;
|
|
317
|
+
if (event.altKey !== wantsAlt) return false;
|
|
318
|
+
if (event.shiftKey !== wantsShift) return false;
|
|
319
|
+
return keyMatches;
|
|
320
|
+
}
|
|
321
|
+
</script>
|
|
322
|
+
|
|
323
|
+
<template>
|
|
324
|
+
<Teleport :to="teleportTarget || 'body'" :disabled="!isMounted || !teleportTarget">
|
|
325
|
+
<div
|
|
326
|
+
v-if="isRendered"
|
|
327
|
+
class="fixed inset-0 z-[90] p-4"
|
|
328
|
+
:class="modelValue ? 'pointer-events-auto' : 'pointer-events-none'"
|
|
329
|
+
>
|
|
330
|
+
<div
|
|
331
|
+
class="absolute inset-0 bg-black/25 opacity-0 backdrop-blur-[2px] transition-opacity duration-[180ms] ease-out"
|
|
332
|
+
:class="isVisible ? 'opacity-100' : 'opacity-0'"
|
|
333
|
+
aria-hidden="true"
|
|
334
|
+
@click="close"
|
|
335
|
+
></div>
|
|
336
|
+
|
|
337
|
+
<div
|
|
338
|
+
ref="panel"
|
|
339
|
+
role="dialog"
|
|
340
|
+
aria-modal="true"
|
|
341
|
+
:aria-label="dialogLabel"
|
|
342
|
+
class="relative mx-auto mt-[12vh] w-[min(92vw,38rem)] origin-top rounded-[1.25rem]"
|
|
343
|
+
:class="[
|
|
344
|
+
useLiquidGlass ? '' : 'command-panel-blur skin-overlay overflow-hidden',
|
|
345
|
+
{
|
|
346
|
+
'command-panel-entering': motion === 'entering',
|
|
347
|
+
'command-panel-leaving': motion === 'leaving',
|
|
348
|
+
},
|
|
349
|
+
]"
|
|
350
|
+
@keydown="onKeydown"
|
|
351
|
+
@animationend.self="onPanelAnimationEnd"
|
|
352
|
+
>
|
|
353
|
+
<DomLiquidGlassSurface
|
|
354
|
+
v-if="useLiquidGlass"
|
|
355
|
+
:refraction="40"
|
|
356
|
+
class="pointer-events-none absolute inset-0"
|
|
357
|
+
:style="{
|
|
358
|
+
'--dom-liquid-glass-position': 'absolute',
|
|
359
|
+
'--dom-liquid-glass-radius': '1.25rem',
|
|
360
|
+
'--dom-liquid-glass-surface-fill': 'color-mix(in oklch, var(--canvas) 46%, transparent)',
|
|
361
|
+
}"
|
|
362
|
+
aria-hidden="true"
|
|
363
|
+
/>
|
|
364
|
+
|
|
365
|
+
<div class="relative z-[1] overflow-hidden rounded-[1.25rem]">
|
|
366
|
+
<input
|
|
367
|
+
ref="input"
|
|
368
|
+
v-model="query"
|
|
369
|
+
type="text"
|
|
370
|
+
role="combobox"
|
|
371
|
+
aria-autocomplete="list"
|
|
372
|
+
aria-expanded="true"
|
|
373
|
+
:aria-controls="listId"
|
|
374
|
+
:aria-activedescendant="filtered[activeIndex] ? `${listId}-option-${activeIndex}` : undefined"
|
|
375
|
+
:placeholder="placeholder"
|
|
376
|
+
class="h-14 w-full border-b border-border/70 bg-transparent px-5 text-[1.05rem] outline-none placeholder:text-muted-fg"
|
|
377
|
+
/>
|
|
378
|
+
<div ref="listPanel" :id="listId" role="listbox" class="max-h-80 overflow-auto p-2">
|
|
379
|
+
<button
|
|
380
|
+
v-for="(command, index) in filtered"
|
|
381
|
+
:id="`${listId}-option-${index}`"
|
|
382
|
+
:key="command.value ?? command.label ?? index"
|
|
383
|
+
role="option"
|
|
384
|
+
type="button"
|
|
385
|
+
tabindex="-1"
|
|
386
|
+
class="block w-full rounded-xl px-3 py-2 text-left transition-colors"
|
|
387
|
+
:class="index === activeIndex
|
|
388
|
+
? 'bg-primary text-primary-fg shadow-sm'
|
|
389
|
+
: 'text-muted-fg hover:bg-secondary hover:text-canvas-fg'"
|
|
390
|
+
:aria-selected="index === activeIndex"
|
|
391
|
+
@mouseenter="activeIndex = index"
|
|
392
|
+
@click="select(command)"
|
|
393
|
+
>
|
|
394
|
+
<span class="flex min-w-0 items-start gap-3">
|
|
395
|
+
<span
|
|
396
|
+
v-if="command.icon"
|
|
397
|
+
class="grid size-8 shrink-0 place-items-center rounded-lg border"
|
|
398
|
+
:class="index === activeIndex
|
|
399
|
+
? 'border-primary-fg/20 bg-primary-fg/15 text-primary-fg'
|
|
400
|
+
: 'border-border/70 bg-secondary text-muted-fg'"
|
|
401
|
+
aria-hidden="true"
|
|
402
|
+
>
|
|
403
|
+
<svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
|
|
404
|
+
<path :d="command.icon" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
405
|
+
</svg>
|
|
406
|
+
</span>
|
|
407
|
+
<span v-else class="size-8 shrink-0" aria-hidden="true"></span>
|
|
408
|
+
<span class="min-w-0 flex-1 pt-0.5">
|
|
409
|
+
<span class="block truncate text-sm font-medium">{{ command.label }}</span>
|
|
410
|
+
<span
|
|
411
|
+
v-if="command.description"
|
|
412
|
+
class="mt-0.5 block truncate text-xs"
|
|
413
|
+
:class="index === activeIndex ? 'text-primary-fg/75' : 'text-muted-fg'"
|
|
414
|
+
>
|
|
415
|
+
{{ command.description }}
|
|
416
|
+
</span>
|
|
417
|
+
</span>
|
|
418
|
+
</span>
|
|
419
|
+
</button>
|
|
420
|
+
<p v-if="!filtered.length" class="px-3 py-6 text-center text-sm text-muted-fg">No commands found.</p>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
</Teleport>
|
|
426
|
+
</template>
|
|
427
|
+
|
|
428
|
+
<style scoped>
|
|
429
|
+
.command-panel-blur {
|
|
430
|
+
--skin-overlay-bg: var(
|
|
431
|
+
--dom-command-palette-background,
|
|
432
|
+
color-mix(in oklch, var(--canvas) 82%, transparent)
|
|
433
|
+
);
|
|
434
|
+
--skin-overlay-backdrop: var(
|
|
435
|
+
--dom-command-palette-backdrop,
|
|
436
|
+
blur(20px) saturate(1.12)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.command-panel-entering {
|
|
441
|
+
animation: command-panel-in 300ms both;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.command-panel-leaving {
|
|
445
|
+
animation: command-panel-out 130ms both;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@keyframes command-panel-in {
|
|
449
|
+
0% {
|
|
450
|
+
opacity: 0;
|
|
451
|
+
transform: translateY(-10px) scale(0.965);
|
|
452
|
+
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
453
|
+
}
|
|
454
|
+
60% {
|
|
455
|
+
opacity: 1;
|
|
456
|
+
transform: translateY(1px) scale(1.01);
|
|
457
|
+
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
|
|
458
|
+
}
|
|
459
|
+
100% {
|
|
460
|
+
opacity: 1;
|
|
461
|
+
transform: translateY(0) scale(1);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@keyframes command-panel-out {
|
|
466
|
+
0% {
|
|
467
|
+
opacity: 1;
|
|
468
|
+
transform: translateY(0) scale(1);
|
|
469
|
+
}
|
|
470
|
+
100% {
|
|
471
|
+
opacity: 0;
|
|
472
|
+
transform: translateY(-5px) scale(0.985);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
@media (prefers-reduced-motion: reduce) {
|
|
477
|
+
.command-panel-entering,
|
|
478
|
+
.command-panel-leaving {
|
|
479
|
+
animation-duration: 1ms;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
</style>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { nextTick, onBeforeUnmount, onMounted, ref, useId } from 'vue';
|
|
3
|
+
import { lockDocumentScroll, unlockDocumentScroll } from '../../lib/headless/popover-panel.js';
|
|
4
|
+
import DomMenu from '../menu/DomMenu.vue';
|
|
5
|
+
|
|
6
|
+
defineOptions({
|
|
7
|
+
__doc: {
|
|
8
|
+
name: 'Context menu',
|
|
9
|
+
tag: '<DomContextMenu>',
|
|
10
|
+
description: 'A right-click menu built on DomMenu, with popover top-layer placement and keyboard navigation.',
|
|
11
|
+
slots: [
|
|
12
|
+
{ name: '(default)', description: 'The area that opens the context menu on right click.' },
|
|
13
|
+
{ name: 'item', payload: '{ item, index, value, label, checked, open, hasChildren }', description: 'Custom label/body content inside the default menu row.' },
|
|
14
|
+
{ name: 'item-row', payload: '{ item, index, value, label, checked, open, hasChildren }', description: 'Replace the full inner row markup while DomMenu keeps the interactive wrapper.' },
|
|
15
|
+
],
|
|
16
|
+
events: [
|
|
17
|
+
{ name: 'open', payload: '({ event })', description: 'Fired when the menu is opened.' },
|
|
18
|
+
{ name: 'select', payload: '({ value, item })', description: 'Fired when a menu item is selected.' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const props = defineProps({
|
|
24
|
+
items: {
|
|
25
|
+
type: Array,
|
|
26
|
+
required: true,
|
|
27
|
+
_edit: {
|
|
28
|
+
component: 'DomJsonListInput',
|
|
29
|
+
description: 'Context menu rows. Use children for nested menus.',
|
|
30
|
+
props: { compact: true },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
width: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: 'min-w-48',
|
|
36
|
+
_edit: { description: 'Tailwind width utility for the menu panel.' },
|
|
37
|
+
},
|
|
38
|
+
lockScroll: {
|
|
39
|
+
type: Boolean,
|
|
40
|
+
default: true,
|
|
41
|
+
_edit: { description: 'Lock browser scrolling while the context menu is open.' },
|
|
42
|
+
},
|
|
43
|
+
openOnContextMenu: {
|
|
44
|
+
type: Boolean,
|
|
45
|
+
default: true,
|
|
46
|
+
_edit: { description: 'Open automatically when the default slot is right-clicked. Disable when opening from another component event.' },
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const emit = defineEmits(['open', 'select']);
|
|
50
|
+
|
|
51
|
+
const panelId = `dom-context-menu-${useId()}`;
|
|
52
|
+
const trigger = ref(null);
|
|
53
|
+
const panel = ref(null);
|
|
54
|
+
const mounted = ref(false);
|
|
55
|
+
let scrollLocked = false;
|
|
56
|
+
|
|
57
|
+
function lockScroll() {
|
|
58
|
+
if (!props.lockScroll || scrollLocked) return;
|
|
59
|
+
lockDocumentScroll(panel.value);
|
|
60
|
+
scrollLocked = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unlockScroll() {
|
|
64
|
+
if (!scrollLocked) return;
|
|
65
|
+
unlockDocumentScroll(panel.value);
|
|
66
|
+
scrollLocked = false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function open(event) {
|
|
70
|
+
event.preventDefault();
|
|
71
|
+
if (!panel.value) return;
|
|
72
|
+
panel.value.style.left = `${event.clientX}px`;
|
|
73
|
+
panel.value.style.top = `${event.clientY}px`;
|
|
74
|
+
if (!panel.value.matches?.(':popover-open')) {
|
|
75
|
+
panel.value.showPopover?.();
|
|
76
|
+
}
|
|
77
|
+
lockScroll();
|
|
78
|
+
emit('open', { event });
|
|
79
|
+
nextTick(() => {
|
|
80
|
+
panel.value?.querySelector('[role="menuitem"]')?.focus();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onTriggerContextMenu(event) {
|
|
85
|
+
if (!props.openOnContextMenu) return;
|
|
86
|
+
open(event);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isOpen() {
|
|
90
|
+
return panel.value?.matches?.(':popover-open');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isSubmenuTarget(target) {
|
|
94
|
+
return target instanceof Element && target.closest('[data-dom-menu-submenu-panel]');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isInside(event) {
|
|
98
|
+
const path = event.composedPath?.() || [];
|
|
99
|
+
return path.includes(panel.value)
|
|
100
|
+
|| path.includes(trigger.value)
|
|
101
|
+
|| path.some(isSubmenuTarget);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function onDocumentPointerDown(event) {
|
|
105
|
+
if (!isOpen() || isInside(event)) return;
|
|
106
|
+
close();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onDocumentContextMenu(event) {
|
|
110
|
+
if (!isOpen() || isInside(event)) return;
|
|
111
|
+
close();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function onWindowBlur() {
|
|
115
|
+
if (!isOpen()) return;
|
|
116
|
+
close();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function close() {
|
|
120
|
+
panel.value?.hidePopover?.();
|
|
121
|
+
unlockScroll();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onSelect(event) {
|
|
125
|
+
emit('select', event);
|
|
126
|
+
close();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function onKeydown(event) {
|
|
130
|
+
if (event.key !== 'Escape') return;
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
close();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
onMounted(async () => {
|
|
136
|
+
mounted.value = true;
|
|
137
|
+
await nextTick();
|
|
138
|
+
panel.value?.addEventListener('keydown', onKeydown);
|
|
139
|
+
document.addEventListener('pointerdown', onDocumentPointerDown, true);
|
|
140
|
+
document.addEventListener('contextmenu', onDocumentContextMenu, true);
|
|
141
|
+
window.addEventListener('blur', onWindowBlur);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
onBeforeUnmount(() => {
|
|
145
|
+
panel.value?.removeEventListener('keydown', onKeydown);
|
|
146
|
+
document.removeEventListener('pointerdown', onDocumentPointerDown, true);
|
|
147
|
+
document.removeEventListener('contextmenu', onDocumentContextMenu, true);
|
|
148
|
+
window.removeEventListener('blur', onWindowBlur);
|
|
149
|
+
unlockScroll();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
defineExpose({
|
|
153
|
+
open,
|
|
154
|
+
close,
|
|
155
|
+
isOpen,
|
|
156
|
+
});
|
|
157
|
+
</script>
|
|
158
|
+
|
|
159
|
+
<template>
|
|
160
|
+
<div ref="trigger" class="contents" @contextmenu="onTriggerContextMenu">
|
|
161
|
+
<slot />
|
|
162
|
+
</div>
|
|
163
|
+
<Teleport v-if="mounted" to="body">
|
|
164
|
+
<div
|
|
165
|
+
:id="panelId"
|
|
166
|
+
ref="panel"
|
|
167
|
+
popover="manual"
|
|
168
|
+
class="dom-popover-panel skin-popover fixed rounded-2xl p-1 outline-none"
|
|
169
|
+
:class="width"
|
|
170
|
+
>
|
|
171
|
+
<DomMenu :items="items" :skin="false" @select="onSelect">
|
|
172
|
+
<template v-for="(_, slotName) in $slots" #[slotName]="slotProps">
|
|
173
|
+
<slot :name="slotName" v-bind="slotProps" />
|
|
174
|
+
</template>
|
|
175
|
+
</DomMenu>
|
|
176
|
+
</div>
|
|
177
|
+
</Teleport>
|
|
178
|
+
</template>
|