@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,460 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref, shallowRef, useId, watch } from 'vue';
|
|
3
|
+
import { resolveFloatingTeleportTarget } from '../../lib/vue/floatingTeleportTarget.js';
|
|
4
|
+
import DomFieldLoadingSpinner from '../_shared/DomFieldLoadingSpinner.vue';
|
|
5
|
+
import { booleanProp } from '../_shared/options.js';
|
|
6
|
+
import DomField from '../field/DomField.vue';
|
|
7
|
+
import { fieldProps } from '../field/fieldProps.js';
|
|
8
|
+
import { useField } from '../field/useField.js';
|
|
9
|
+
import {
|
|
10
|
+
createIconSelectorItems,
|
|
11
|
+
fallbackIconSelectorItem,
|
|
12
|
+
iconSelectorItemKey,
|
|
13
|
+
iconSelectorOptionValue,
|
|
14
|
+
iconSelectorValuesMatch,
|
|
15
|
+
} from './iconSelectorItems.js';
|
|
16
|
+
|
|
17
|
+
const listId = `dom-icon-selector-${useId()}`;
|
|
18
|
+
|
|
19
|
+
defineOptions({
|
|
20
|
+
__doc: {
|
|
21
|
+
name: 'Icon selector',
|
|
22
|
+
tag: '<DomIconSelector>',
|
|
23
|
+
description: 'A searchable select-style field for choosing SVG path icons, emoji, or app-defined symbol rows.',
|
|
24
|
+
icon: 'M5 5h5v5H5V5Zm9 0h5v5h-5V5ZM5 14h5v5H5v-5Zm9 0h5v5h-5v-5Z',
|
|
25
|
+
playground: {
|
|
26
|
+
initial: {
|
|
27
|
+
label: 'Icon',
|
|
28
|
+
modelValue: 'M12 5v14M5 12h14',
|
|
29
|
+
kind: 'icons',
|
|
30
|
+
placeholder: 'Search icons',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
slots: [
|
|
34
|
+
{ name: 'preview', payload: '{ item }', description: 'Replace the selected-value preview inside the input.' },
|
|
35
|
+
{ name: 'item', payload: '{ item, selected }', description: 'Replace each icon, emoji, or custom row in the picker panel.' },
|
|
36
|
+
],
|
|
37
|
+
events: [
|
|
38
|
+
{ name: 'update:modelValue', payload: 'string | number | boolean | Component', description: 'Fired with the selected icon path, emoji, component, or custom item value.' },
|
|
39
|
+
{ name: 'select', payload: '{ item, value, label }', description: 'Fired when a picker item is selected.' },
|
|
40
|
+
{ name: 'query', payload: 'string', description: 'Fired as the user searches the picker.' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const props = defineProps({
|
|
46
|
+
...fieldProps,
|
|
47
|
+
modelValue: {
|
|
48
|
+
type: [String, Number, Boolean, Object, Function],
|
|
49
|
+
default: '',
|
|
50
|
+
_edit: {
|
|
51
|
+
group: 'Control props',
|
|
52
|
+
component: 'DomIconSelector',
|
|
53
|
+
typeRef: 'string | number | boolean | Component',
|
|
54
|
+
description: 'Selected icon path, emoji, component, or custom item value.',
|
|
55
|
+
props: {
|
|
56
|
+
kind: 'mixed',
|
|
57
|
+
placeholder: 'Search icons or emoji',
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
kind: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: 'icons',
|
|
64
|
+
_edit: { group: 'Control props', options: ['icons', 'emoji', 'mixed', 'custom'], description: 'Built-in collection to show before custom items.' },
|
|
65
|
+
},
|
|
66
|
+
items: {
|
|
67
|
+
type: Array,
|
|
68
|
+
/**
|
|
69
|
+
* Build the default custom item collection.
|
|
70
|
+
*
|
|
71
|
+
* @returns {Array<unknown>}
|
|
72
|
+
*/
|
|
73
|
+
default: () => [],
|
|
74
|
+
_edit: {
|
|
75
|
+
group: 'Control props',
|
|
76
|
+
component: 'DomJsonListInput',
|
|
77
|
+
description: 'Custom icon, emoji, or symbol rows.',
|
|
78
|
+
props: {
|
|
79
|
+
compact: true,
|
|
80
|
+
addLabel: '+ Add item',
|
|
81
|
+
schema: [
|
|
82
|
+
{ key: 'label', label: 'Label', placeholder: 'Priority' },
|
|
83
|
+
{ key: 'value', label: 'Value', placeholder: 'priority' },
|
|
84
|
+
{ key: 'path', label: 'SVG path', placeholder: 'M12 5v14M5 12h14' },
|
|
85
|
+
{ key: 'emoji', label: 'Emoji', placeholder: '🚀' },
|
|
86
|
+
{ key: 'group', label: 'Group', placeholder: 'Status' },
|
|
87
|
+
{ key: 'keywords', label: 'Keywords', placeholder: 'launch success' },
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
includeBuiltIns: {
|
|
93
|
+
type: Boolean,
|
|
94
|
+
default: true,
|
|
95
|
+
_edit: { group: 'Control props', description: 'Include DOM Studio icon and emoji collections for the selected kind.' },
|
|
96
|
+
},
|
|
97
|
+
placeholder: {
|
|
98
|
+
type: String,
|
|
99
|
+
default: 'Search icons',
|
|
100
|
+
_edit: { group: 'Control props', description: 'Search input placeholder.' },
|
|
101
|
+
},
|
|
102
|
+
clearable: {
|
|
103
|
+
type: Boolean,
|
|
104
|
+
default: true,
|
|
105
|
+
_edit: { group: 'Control props', description: 'Show a clear button when a value is selected.' },
|
|
106
|
+
},
|
|
107
|
+
loading: {
|
|
108
|
+
type: [Boolean, String],
|
|
109
|
+
default: false,
|
|
110
|
+
_edit: { group: 'Control props', description: 'Show an inline spinner while custom symbol rows are loading.' },
|
|
111
|
+
},
|
|
112
|
+
placement: {
|
|
113
|
+
type: String,
|
|
114
|
+
default: 'bottom',
|
|
115
|
+
_edit: { group: 'Control props', options: ['bottom', 'top', 'right', 'left'], description: 'Preferred side before collision handling.' },
|
|
116
|
+
},
|
|
117
|
+
floatingMode: {
|
|
118
|
+
type: String,
|
|
119
|
+
default: 'viewport',
|
|
120
|
+
_edit: { group: 'Control props', options: ['viewport', 'anchor'], description: 'viewport keeps the picker inside the browser; anchor keeps it attached while scrolling.' },
|
|
121
|
+
},
|
|
122
|
+
panelMinWidth: {
|
|
123
|
+
type: String,
|
|
124
|
+
default: '20rem',
|
|
125
|
+
_edit: { group: 'Control props', description: 'Minimum floating panel width as a CSS length.' },
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const emit = defineEmits(['update:modelValue', 'select', 'query', 'focus', 'blur']);
|
|
129
|
+
|
|
130
|
+
const root = ref(null);
|
|
131
|
+
const inputEl = ref(null);
|
|
132
|
+
const isMounted = ref(false);
|
|
133
|
+
const teleportTarget = shallowRef(null);
|
|
134
|
+
const field = useField(props, emit, { idPrefix: 'dom-icon-selector' });
|
|
135
|
+
const inputAttrs = computed(getInputAttrs);
|
|
136
|
+
const optionList = computed(getOptionList);
|
|
137
|
+
const selectedItem = computed(getSelectedItem);
|
|
138
|
+
const hasSelection = computed(hasSelectedValue);
|
|
139
|
+
const isLoading = computed(isLoadingState);
|
|
140
|
+
const canClear = computed(canClearSelection);
|
|
141
|
+
const inputPaddingClass = computed(getInputPaddingClass);
|
|
142
|
+
const loadingPositionClass = computed(getLoadingPositionClass);
|
|
143
|
+
|
|
144
|
+
onMounted(mountIconSelector);
|
|
145
|
+
onBeforeUnmount(unmountIconSelector);
|
|
146
|
+
|
|
147
|
+
watch(field.value, syncInputFromModel);
|
|
148
|
+
watch(optionList, syncInputAfterOptionsChange);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Wire the headless combobox after Vue has rendered the input and teleported list.
|
|
152
|
+
*
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
async function mountIconSelector() {
|
|
156
|
+
teleportTarget.value = resolveFloatingTeleportTarget(root.value);
|
|
157
|
+
isMounted.value = true;
|
|
158
|
+
await import('../../lib/headless/combobox.js');
|
|
159
|
+
syncInputFromModel();
|
|
160
|
+
root.value?.addEventListener('dom:change', onComboboxChange);
|
|
161
|
+
root.value?.addEventListener('dom:select', onComboboxSelect);
|
|
162
|
+
root.value?.addEventListener('dom:query', onComboboxQuery);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Remove native event listeners from the custom element host.
|
|
167
|
+
*
|
|
168
|
+
* @returns {void}
|
|
169
|
+
*/
|
|
170
|
+
function unmountIconSelector() {
|
|
171
|
+
root.value?.removeEventListener('dom:change', onComboboxChange);
|
|
172
|
+
root.value?.removeEventListener('dom:select', onComboboxSelect);
|
|
173
|
+
root.value?.removeEventListener('dom:query', onComboboxQuery);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check whether the selector currently has a committed value.
|
|
178
|
+
*
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
function hasSelectedValue() {
|
|
182
|
+
return field.value.value !== null && field.value.value !== undefined && field.value.value !== '';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve the normalized loading state from boolean-style prop input.
|
|
187
|
+
*
|
|
188
|
+
* @returns {boolean}
|
|
189
|
+
*/
|
|
190
|
+
function isLoadingState() {
|
|
191
|
+
return booleanProp(props.loading, false);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Check whether the clear button can be shown.
|
|
196
|
+
*
|
|
197
|
+
* @returns {boolean}
|
|
198
|
+
*/
|
|
199
|
+
function canClearSelection() {
|
|
200
|
+
return props.clearable && hasSelection.value && !field.disabled.value && !field.readOnly.value;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Choose input padding that leaves room for the preview, spinner, and controls.
|
|
205
|
+
*
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
function getInputPaddingClass() {
|
|
209
|
+
if (canClear.value && isLoading.value) return 'pl-11 pr-28';
|
|
210
|
+
if (canClear.value) return 'pl-11 pr-20';
|
|
211
|
+
if (isLoading.value) return 'pl-11 pr-20';
|
|
212
|
+
return 'pl-11 pr-10';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Position the loading spinner next to the available trailing controls.
|
|
217
|
+
*
|
|
218
|
+
* @returns {string}
|
|
219
|
+
*/
|
|
220
|
+
function getLoadingPositionClass() {
|
|
221
|
+
return canClear.value ? 'right-[4.25rem]' : 'right-9';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Remove the native value attribute so the headless combobox owns display text.
|
|
226
|
+
*
|
|
227
|
+
* @returns {Object}
|
|
228
|
+
*/
|
|
229
|
+
function getInputAttrs() {
|
|
230
|
+
const { value: _value, ...attrs } = field.inputAttrs.value;
|
|
231
|
+
return attrs;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Build the icon, emoji, and custom item rows available to the picker.
|
|
236
|
+
*
|
|
237
|
+
* @returns {Array<Object>}
|
|
238
|
+
*/
|
|
239
|
+
function getOptionList() {
|
|
240
|
+
return createIconSelectorItems({
|
|
241
|
+
kind: props.kind,
|
|
242
|
+
items: props.items,
|
|
243
|
+
includeBuiltIns: props.includeBuiltIns,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Resolve the selected item from the option list or current ad-hoc value.
|
|
249
|
+
*
|
|
250
|
+
* @returns {Object|null}
|
|
251
|
+
*/
|
|
252
|
+
function getSelectedItem() {
|
|
253
|
+
return optionList.value.find((option) => iconSelectorValuesMatch(option.value, field.value.value))
|
|
254
|
+
|| fallbackIconSelectorItem(field.value.value);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Resolve the display label shown in the search input for a committed value.
|
|
259
|
+
*
|
|
260
|
+
* @param {unknown} value Current model value.
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
function displayForValue(value) {
|
|
264
|
+
const option = optionList.value.find((item) => iconSelectorValuesMatch(item.value, value))
|
|
265
|
+
|| fallbackIconSelectorItem(value);
|
|
266
|
+
return option?.label || '';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Synchronize the headless host and search input with the Vue model value.
|
|
271
|
+
*
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
function syncInputFromModel() {
|
|
275
|
+
const optionValue = selectedItem.value ? iconSelectorOptionValue(selectedItem.value) : String(field.value.value ?? '');
|
|
276
|
+
if (root.value && root.value.value !== optionValue) {
|
|
277
|
+
root.value.value = optionValue;
|
|
278
|
+
}
|
|
279
|
+
if (inputEl.value) inputEl.value.value = displayForValue(field.value.value);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Refresh display text after icon inventory or custom item rows change.
|
|
284
|
+
*
|
|
285
|
+
* @returns {void}
|
|
286
|
+
*/
|
|
287
|
+
function syncInputAfterOptionsChange() {
|
|
288
|
+
if (!hasSelection.value || field.focused.value) return;
|
|
289
|
+
syncInputFromModel();
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Clear the selected value and close any open picker panel.
|
|
294
|
+
*
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*/
|
|
297
|
+
function clearSelection() {
|
|
298
|
+
field.setValue('');
|
|
299
|
+
if (root.value) root.value.value = '';
|
|
300
|
+
if (inputEl.value) inputEl.value.value = '';
|
|
301
|
+
root.value?.removeAttribute('open');
|
|
302
|
+
emit('select', { item: null, value: '', label: '', option: null });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Check whether an item matches the current selected value.
|
|
307
|
+
*
|
|
308
|
+
* @param {Object} item Selector item.
|
|
309
|
+
* @returns {boolean}
|
|
310
|
+
*/
|
|
311
|
+
function isSelectedItem(item) {
|
|
312
|
+
return iconSelectorValuesMatch(item?.value, field.value.value);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle committed value changes from the headless combobox.
|
|
317
|
+
*
|
|
318
|
+
* @param {CustomEvent} event Headless combobox change event.
|
|
319
|
+
* @returns {void}
|
|
320
|
+
*/
|
|
321
|
+
function onComboboxChange(event) {
|
|
322
|
+
const item = itemFromEvent(event);
|
|
323
|
+
const value = item ? item.value : event.detail.value;
|
|
324
|
+
field.onInput(value);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Emit richer selection details after an item is picked.
|
|
329
|
+
*
|
|
330
|
+
* @param {CustomEvent} event Headless combobox select event.
|
|
331
|
+
* @returns {void}
|
|
332
|
+
*/
|
|
333
|
+
function onComboboxSelect(event) {
|
|
334
|
+
const item = itemFromEvent(event);
|
|
335
|
+
const value = item ? item.value : event.detail.value;
|
|
336
|
+
emit('select', {
|
|
337
|
+
item,
|
|
338
|
+
value,
|
|
339
|
+
label: item?.label ?? event.detail.label,
|
|
340
|
+
option: event.detail.option,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Emit search query updates from the headless combobox.
|
|
346
|
+
*
|
|
347
|
+
* @param {CustomEvent} event Headless combobox query event.
|
|
348
|
+
* @returns {void}
|
|
349
|
+
*/
|
|
350
|
+
function onComboboxQuery(event) {
|
|
351
|
+
emit('query', event.detail.query);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolve the normalized item associated with a headless option element.
|
|
356
|
+
*
|
|
357
|
+
* @param {CustomEvent} event Headless combobox event.
|
|
358
|
+
* @returns {Object|null}
|
|
359
|
+
*/
|
|
360
|
+
function itemFromEvent(event) {
|
|
361
|
+
const index = Number(event.detail.option?.dataset.index);
|
|
362
|
+
if (Number.isFinite(index) && optionList.value[index]) return optionList.value[index];
|
|
363
|
+
return optionList.value.find((item) => iconSelectorOptionValue(item) === event.detail.value) || null;
|
|
364
|
+
}
|
|
365
|
+
</script>
|
|
366
|
+
|
|
367
|
+
<template>
|
|
368
|
+
<DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
|
|
369
|
+
<dom-combobox
|
|
370
|
+
ref="root"
|
|
371
|
+
:data-menu-id="listId"
|
|
372
|
+
:placement="placement"
|
|
373
|
+
:floating-mode="floatingMode"
|
|
374
|
+
class="relative block"
|
|
375
|
+
>
|
|
376
|
+
<div class="relative">
|
|
377
|
+
<span class="pointer-events-none absolute left-3 top-1/2 z-10 grid size-5 -translate-y-1/2 place-items-center text-muted-fg">
|
|
378
|
+
<slot name="preview" :item="selectedItem">
|
|
379
|
+
<component v-if="selectedItem?.component" :is="selectedItem.component" class="size-4" aria-hidden="true" focusable="false" />
|
|
380
|
+
<svg v-else-if="selectedItem?.path" :viewBox="selectedItem.viewBox || '0 0 24 24'" class="size-4" fill="none" aria-hidden="true">
|
|
381
|
+
<path :d="selectedItem.path" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
382
|
+
</svg>
|
|
383
|
+
<span v-else-if="selectedItem?.emoji" class="text-base leading-none">{{ selectedItem.emoji }}</span>
|
|
384
|
+
<svg v-else viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
|
|
385
|
+
<path d="M10.5 18a7.5 7.5 0 1 1 5.3-2.2L21 21" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
386
|
+
</svg>
|
|
387
|
+
</slot>
|
|
388
|
+
</span>
|
|
389
|
+
<input
|
|
390
|
+
v-bind="inputAttrs"
|
|
391
|
+
ref="inputEl"
|
|
392
|
+
slot="input"
|
|
393
|
+
type="text"
|
|
394
|
+
class="skin-input h-10 rounded-full focus:ring-ring/60"
|
|
395
|
+
:class="inputPaddingClass"
|
|
396
|
+
:placeholder="placeholder"
|
|
397
|
+
@focus="field.onFocus"
|
|
398
|
+
@blur="field.onBlur"
|
|
399
|
+
/>
|
|
400
|
+
<DomFieldLoadingSpinner
|
|
401
|
+
v-if="isLoading"
|
|
402
|
+
class="absolute top-1"
|
|
403
|
+
:class="loadingPositionClass"
|
|
404
|
+
/>
|
|
405
|
+
<button
|
|
406
|
+
v-if="canClear"
|
|
407
|
+
type="button"
|
|
408
|
+
class="absolute right-9 top-1 inline-flex size-8 items-center justify-center rounded-full text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
409
|
+
aria-label="Clear selection"
|
|
410
|
+
@click.stop="clearSelection"
|
|
411
|
+
@mousedown.prevent
|
|
412
|
+
>
|
|
413
|
+
<svg viewBox="0 0 20 20" class="size-4" fill="none" aria-hidden="true">
|
|
414
|
+
<circle cx="10" cy="10" r="7" stroke="currentColor" stroke-width="1.5" />
|
|
415
|
+
<path d="M7.5 7.5l5 5M12.5 7.5l-5 5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
|
|
416
|
+
</svg>
|
|
417
|
+
</button>
|
|
418
|
+
<button
|
|
419
|
+
slot="toggle"
|
|
420
|
+
type="button"
|
|
421
|
+
class="absolute right-1 top-1 inline-flex size-8 items-center justify-center rounded-full text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
422
|
+
aria-label="Show icon options"
|
|
423
|
+
>
|
|
424
|
+
<svg viewBox="0 0 20 20" class="size-4" fill="none" aria-hidden="true">
|
|
425
|
+
<path d="M6 8l4 4 4-4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
|
426
|
+
</svg>
|
|
427
|
+
</button>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
430
|
+
<Teleport :to="teleportTarget || 'body'" :disabled="!isMounted || !teleportTarget">
|
|
431
|
+
<ul
|
|
432
|
+
:id="listId"
|
|
433
|
+
class="dom-floating-transition skin-popover z-50 grid max-h-[min(18rem,var(--dom-floating-available-height))] grid-cols-4 gap-1 overflow-auto rounded-2xl p-2 sm:grid-cols-5"
|
|
434
|
+
:style="{ minWidth: panelMinWidth }"
|
|
435
|
+
>
|
|
436
|
+
<li
|
|
437
|
+
v-for="(option, index) in optionList"
|
|
438
|
+
:key="iconSelectorItemKey(option)"
|
|
439
|
+
:data-value="iconSelectorOptionValue(option)"
|
|
440
|
+
:data-label="option.label"
|
|
441
|
+
:data-index="index"
|
|
442
|
+
class="min-w-0 cursor-pointer rounded-xl p-2 text-center text-xs transition data-[active]:bg-accent aria-selected:bg-accent"
|
|
443
|
+
>
|
|
444
|
+
<slot name="item" :item="option" :selected="isSelectedItem(option)">
|
|
445
|
+
<span class="mx-auto grid size-9 place-items-center rounded-lg bg-secondary/60 text-canvas-fg ring-1 ring-border/60">
|
|
446
|
+
<component v-if="option.component" :is="option.component" class="size-5" aria-hidden="true" focusable="false" />
|
|
447
|
+
<svg v-else-if="option.path" :viewBox="option.viewBox || '0 0 24 24'" class="size-5" fill="none" aria-hidden="true">
|
|
448
|
+
<path :d="option.path" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
|
|
449
|
+
</svg>
|
|
450
|
+
<span v-else class="text-xl leading-none">{{ option.emoji || option.label }}</span>
|
|
451
|
+
</span>
|
|
452
|
+
<span class="mt-1 block truncate font-medium text-canvas-fg">{{ option.label }}</span>
|
|
453
|
+
<span class="sr-only">{{ option.group }} {{ option.searchText }}</span>
|
|
454
|
+
</slot>
|
|
455
|
+
</li>
|
|
456
|
+
</ul>
|
|
457
|
+
</Teleport>
|
|
458
|
+
</dom-combobox>
|
|
459
|
+
</DomField>
|
|
460
|
+
</template>
|