@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,630 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { fieldProps } from '../field/fieldProps.js';
|
|
3
|
+
|
|
4
|
+
export const popoverEditorDefinitions = {
|
|
5
|
+
position: {
|
|
6
|
+
type: String,
|
|
7
|
+
default: 'bottom-start',
|
|
8
|
+
_edit: {
|
|
9
|
+
component: 'DomPositionInput',
|
|
10
|
+
props: { label: 'Position' },
|
|
11
|
+
description: 'Preferred side and alignment around the trigger.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
offset: {
|
|
15
|
+
type: Number,
|
|
16
|
+
default: 8,
|
|
17
|
+
_edit: { description: 'Gap in pixels between the trigger and the popover.' },
|
|
18
|
+
},
|
|
19
|
+
collisionPadding: {
|
|
20
|
+
type: Number,
|
|
21
|
+
default: 8,
|
|
22
|
+
_edit: { description: 'Viewport padding used when the panel flips or shifts.' },
|
|
23
|
+
},
|
|
24
|
+
floatingMode: {
|
|
25
|
+
type: String,
|
|
26
|
+
default: 'viewport',
|
|
27
|
+
_edit: { options: ['viewport', 'anchor'], description: 'viewport keeps the panel inside the browser; anchor keeps it attached while scrolling.' },
|
|
28
|
+
},
|
|
29
|
+
flip: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: true,
|
|
32
|
+
_edit: { description: 'Allow the popover to flip to the opposite side when it would collide with the viewport.' },
|
|
33
|
+
},
|
|
34
|
+
lockScroll: {
|
|
35
|
+
type: Boolean,
|
|
36
|
+
default: false,
|
|
37
|
+
_edit: { description: 'Lock browser scrolling while the popover is open.' },
|
|
38
|
+
},
|
|
39
|
+
arrow: {
|
|
40
|
+
type: Boolean,
|
|
41
|
+
default: true,
|
|
42
|
+
_edit: { description: 'Show a small arrow pointing back to the trigger.' },
|
|
43
|
+
},
|
|
44
|
+
width: {
|
|
45
|
+
type: String,
|
|
46
|
+
default: 'min-w-[14rem] max-w-[20rem]',
|
|
47
|
+
_edit: { description: 'Tailwind width utility(ies) for the panel.' },
|
|
48
|
+
},
|
|
49
|
+
padding: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: 'p-4',
|
|
52
|
+
_edit: { description: 'Tailwind padding utility for the panel.' },
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const calendarEditorDefinitions = {
|
|
57
|
+
locale: {
|
|
58
|
+
type: String,
|
|
59
|
+
default: 'en-GB',
|
|
60
|
+
_edit: { description: 'Locale used for month and weekday labels.' },
|
|
61
|
+
},
|
|
62
|
+
weekStartsOn: {
|
|
63
|
+
type: Number,
|
|
64
|
+
default: 1,
|
|
65
|
+
_edit: { options: [0, 1, 2, 3, 4, 5, 6], description: 'First day of week. 0 is Sunday, 1 is Monday.' },
|
|
66
|
+
},
|
|
67
|
+
initialMonth: {
|
|
68
|
+
type: Number,
|
|
69
|
+
default: null,
|
|
70
|
+
_edit: { description: 'Initial visible month, 1-12. Ignored when modelValue has a valid date.' },
|
|
71
|
+
},
|
|
72
|
+
initialYear: {
|
|
73
|
+
type: Number,
|
|
74
|
+
default: null,
|
|
75
|
+
_edit: { description: 'Initial visible year. Ignored when modelValue has a valid date.' },
|
|
76
|
+
},
|
|
77
|
+
min: {
|
|
78
|
+
type: String,
|
|
79
|
+
default: '',
|
|
80
|
+
_edit: { description: 'Minimum selectable date as YYYY-MM-DD.' },
|
|
81
|
+
},
|
|
82
|
+
max: {
|
|
83
|
+
type: String,
|
|
84
|
+
default: '',
|
|
85
|
+
_edit: { description: 'Maximum selectable date as YYYY-MM-DD.' },
|
|
86
|
+
},
|
|
87
|
+
showAdjacentDays: {
|
|
88
|
+
type: Boolean,
|
|
89
|
+
default: true,
|
|
90
|
+
_edit: { description: 'Show dates from the previous and next months in the grid.' },
|
|
91
|
+
},
|
|
92
|
+
fixedWeeks: {
|
|
93
|
+
type: Boolean,
|
|
94
|
+
default: true,
|
|
95
|
+
_edit: { description: 'Always render six weeks so the calendar height stays stable.' },
|
|
96
|
+
},
|
|
97
|
+
disabled: {
|
|
98
|
+
type: Boolean,
|
|
99
|
+
default: false,
|
|
100
|
+
_edit: { description: 'Disable date selection.' },
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const dateTimePickerProps = {
|
|
105
|
+
...fieldProps,
|
|
106
|
+
modelValue: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: '',
|
|
109
|
+
_edit: { group: 'Control props', description: 'Selected date and time as YYYY-MM-DDTHH:mm, or YYYY-MM-DDTHH:mm:ss when showSeconds is enabled.' },
|
|
110
|
+
},
|
|
111
|
+
dateFormat: {
|
|
112
|
+
type: String,
|
|
113
|
+
default: 'dd/mm/yyyy',
|
|
114
|
+
_edit: { options: ['dd/mm/yyyy', 'mm/dd/yyyy'], description: 'How typed dates are parsed and displayed.' },
|
|
115
|
+
},
|
|
116
|
+
timeFormat: {
|
|
117
|
+
type: String,
|
|
118
|
+
default: '24h',
|
|
119
|
+
validator: (value) => ['24h', '12h'].includes(value),
|
|
120
|
+
_edit: { options: ['24h', '12h'], description: 'How times are displayed in the input and option list.' },
|
|
121
|
+
},
|
|
122
|
+
minuteStep: {
|
|
123
|
+
type: Number,
|
|
124
|
+
default: 15,
|
|
125
|
+
_edit: { group: 'Control props', description: 'Minute interval used to generate time options.' },
|
|
126
|
+
},
|
|
127
|
+
defaultTime: {
|
|
128
|
+
type: String,
|
|
129
|
+
default: '09:00',
|
|
130
|
+
_edit: { group: 'Control props', description: 'Time used when a date is selected before the user has chosen a time.' },
|
|
131
|
+
},
|
|
132
|
+
min: {
|
|
133
|
+
type: String,
|
|
134
|
+
default: '',
|
|
135
|
+
_edit: { group: 'Control props', description: 'Minimum selectable date-time as YYYY-MM-DDTHH:mm.' },
|
|
136
|
+
},
|
|
137
|
+
max: {
|
|
138
|
+
type: String,
|
|
139
|
+
default: '',
|
|
140
|
+
_edit: { group: 'Control props', description: 'Maximum selectable date-time as YYYY-MM-DDTHH:mm.' },
|
|
141
|
+
},
|
|
142
|
+
timeOptions: {
|
|
143
|
+
type: Array,
|
|
144
|
+
default: () => [],
|
|
145
|
+
_edit: {
|
|
146
|
+
component: 'DomJsonListInput',
|
|
147
|
+
group: 'Control props',
|
|
148
|
+
description: 'Optional custom time options. Use strings such as 09:30 or records with value, label, and disabled.',
|
|
149
|
+
props: {
|
|
150
|
+
compact: true,
|
|
151
|
+
schema: [
|
|
152
|
+
{ key: 'value', label: 'Value', placeholder: '09:30' },
|
|
153
|
+
{ key: 'label', label: 'Label', placeholder: '9:30 AM' },
|
|
154
|
+
{ key: 'disabled', label: 'Disabled', type: 'boolean' },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
showSeconds: {
|
|
160
|
+
type: Boolean,
|
|
161
|
+
default: false,
|
|
162
|
+
_edit: { group: 'Control props', description: 'Store and display seconds in addition to hours and minutes.' },
|
|
163
|
+
},
|
|
164
|
+
popoverProps: {
|
|
165
|
+
type: Object,
|
|
166
|
+
default: () => ({}),
|
|
167
|
+
_edit: {
|
|
168
|
+
component: 'DomPropDefinitionInput',
|
|
169
|
+
description: 'Props passed to the inner DomPopover. Date time picker defaults position to bottom-end, max width to 34rem, padding to p-0, and arrow to true.',
|
|
170
|
+
props: {
|
|
171
|
+
definitions: popoverEditorDefinitions,
|
|
172
|
+
include: ['position', 'offset', 'collisionPadding', 'floatingMode', 'flip', 'lockScroll', 'arrow', 'width', 'padding'],
|
|
173
|
+
compact: true,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
calendarProps: {
|
|
178
|
+
type: Object,
|
|
179
|
+
default: () => ({}),
|
|
180
|
+
_edit: {
|
|
181
|
+
component: 'DomPropDefinitionInput',
|
|
182
|
+
description: 'Props passed to the inner DomCalendar, such as locale, weekStartsOn, min, max, fixedWeeks, and showAdjacentDays.',
|
|
183
|
+
props: {
|
|
184
|
+
definitions: calendarEditorDefinitions,
|
|
185
|
+
include: ['locale', 'weekStartsOn', 'initialMonth', 'initialYear', 'min', 'max', 'showAdjacentDays', 'fixedWeeks', 'disabled'],
|
|
186
|
+
compact: true,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<script setup>
|
|
194
|
+
import { computed, nextTick, ref, useId, watch } from 'vue';
|
|
195
|
+
import DomPopover, { popoverProps } from '../../components/popover/DomPopover.vue';
|
|
196
|
+
import DomCalendar, { calendarProps } from '../calendar/DomCalendar.vue';
|
|
197
|
+
import DomField from '../field/DomField.vue';
|
|
198
|
+
import { useField } from '../field/useField.js';
|
|
199
|
+
import {
|
|
200
|
+
buildTimeOptions,
|
|
201
|
+
compareTimeValues,
|
|
202
|
+
formatTimeDisplay,
|
|
203
|
+
normalizeTimeValue,
|
|
204
|
+
parseTimeInput,
|
|
205
|
+
timeToSeconds,
|
|
206
|
+
} from '../time-selector/timeUtils.js';
|
|
207
|
+
|
|
208
|
+
defineOptions({
|
|
209
|
+
__doc: {
|
|
210
|
+
name: 'Date time picker',
|
|
211
|
+
tag: '<DomDateTimePicker>',
|
|
212
|
+
description: 'A typed date and time input with a popover calendar and time selector. Stores a local YYYY-MM-DDTHH:mm value.',
|
|
213
|
+
icon: 'M7 4v3M17 4v3M5 8h14M6 6h12v13H6V6Zm7 7v3l2 1',
|
|
214
|
+
events: [
|
|
215
|
+
{ name: 'update:modelValue', payload: 'YYYY-MM-DDTHH:mm', description: 'Fired when a valid date-time is typed or selected.' },
|
|
216
|
+
{ name: 'change', payload: 'YYYY-MM-DDTHH:mm', description: 'Fired when a valid date-time is committed.' },
|
|
217
|
+
{ name: 'invalid-date-time', payload: 'string', description: 'Fired when typed text cannot be parsed or is outside the allowed range.' },
|
|
218
|
+
{ name: 'focus', payload: 'FocusEvent', description: 'Fired when the text input receives focus.' },
|
|
219
|
+
{ name: 'blur', payload: 'FocusEvent', description: 'Fired when the text input loses focus.' },
|
|
220
|
+
],
|
|
221
|
+
keyboard: [
|
|
222
|
+
{ keys: 'Alt + ArrowDown', action: 'Open the calendar and time popover.' },
|
|
223
|
+
{ keys: 'Enter', action: 'Commit the typed date-time when it is valid.' },
|
|
224
|
+
{ keys: 'Esc', action: 'Close the popover.' },
|
|
225
|
+
{ keys: 'Arrow keys in calendar', action: 'Move focus by day or week once the calendar is open.' },
|
|
226
|
+
{ keys: 'Tab', action: 'Move through generated time options.' },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const props = defineProps(dateTimePickerProps);
|
|
232
|
+
const emit = defineEmits(['update:modelValue', 'change', 'invalid-date-time', 'focus', 'blur']);
|
|
233
|
+
const field = useField(props, emit, { idPrefix: 'dom-date-time-picker' });
|
|
234
|
+
const popover = ref(null);
|
|
235
|
+
const calendar = ref(null);
|
|
236
|
+
const displayValue = ref(formatDisplay(field.value.value));
|
|
237
|
+
const parseError = ref('');
|
|
238
|
+
const optionRefs = new Map();
|
|
239
|
+
const uniqueId = useId();
|
|
240
|
+
const triggerId = computed(() => `${field.id.value || uniqueId}-date-time-button`);
|
|
241
|
+
const selectedParts = computed(() => splitDateTime(field.value.value));
|
|
242
|
+
const selectedDate = computed(() => selectedParts.value?.date || '');
|
|
243
|
+
const selectedTime = computed(() => selectedParts.value?.time || normalizeTimeValue(props.defaultTime, props) || '09:00');
|
|
244
|
+
const placeholderText = computed(() => props.placeholder || `${props.dateFormat} ${props.timeFormat === '12h' ? '9:30 AM' : props.showSeconds ? '09:30:00' : '09:30'}`);
|
|
245
|
+
const resolvedCalendarProps = computed(() => ({
|
|
246
|
+
...defaultPropsFrom(calendarProps),
|
|
247
|
+
...props.calendarProps,
|
|
248
|
+
modelValue: selectedDate.value,
|
|
249
|
+
min: props.calendarProps.min || datePart(props.min),
|
|
250
|
+
max: props.calendarProps.max || datePart(props.max),
|
|
251
|
+
disabled: field.disabled.value || props.calendarProps.disabled,
|
|
252
|
+
chrome: 'none',
|
|
253
|
+
registerField: false,
|
|
254
|
+
nativeInput: false,
|
|
255
|
+
}));
|
|
256
|
+
const resolvedPopoverProps = computed(() => ({
|
|
257
|
+
...defaultPropsFrom(popoverProps),
|
|
258
|
+
position: 'bottom-end',
|
|
259
|
+
width: 'w-[calc(100vw-1rem)] max-w-[34rem]',
|
|
260
|
+
padding: 'p-0',
|
|
261
|
+
arrow: true,
|
|
262
|
+
...props.popoverProps,
|
|
263
|
+
triggerId: triggerId.value,
|
|
264
|
+
}));
|
|
265
|
+
const timeOptions = computed(() => buildTimeOptions({
|
|
266
|
+
options: props.timeOptions,
|
|
267
|
+
minuteStep: props.minuteStep,
|
|
268
|
+
min: minTimeForDate(selectedDate.value),
|
|
269
|
+
max: maxTimeForDate(selectedDate.value),
|
|
270
|
+
timeFormat: props.timeFormat,
|
|
271
|
+
showSeconds: props.showSeconds,
|
|
272
|
+
}).map((option) => ({
|
|
273
|
+
...option,
|
|
274
|
+
disabled: option.disabled || Boolean(selectedDate.value && !isDateTimeInRange(combineDateTime(selectedDate.value, option.value))),
|
|
275
|
+
})));
|
|
276
|
+
const combinedErrors = computed(() => [
|
|
277
|
+
...field.errors.value,
|
|
278
|
+
...(parseError.value ? [parseError.value] : []),
|
|
279
|
+
]);
|
|
280
|
+
const invalid = computed(() => Boolean(field.invalid.value || parseError.value));
|
|
281
|
+
const fieldAttrs = computed(() => ({
|
|
282
|
+
...field.fieldAttrs.value,
|
|
283
|
+
invalid: invalid.value,
|
|
284
|
+
errors: combinedErrors.value,
|
|
285
|
+
}));
|
|
286
|
+
const inputAttrs = computed(() => ({
|
|
287
|
+
...field.inputAttrs.value,
|
|
288
|
+
value: displayValue.value,
|
|
289
|
+
placeholder: placeholderText.value,
|
|
290
|
+
'aria-invalid': invalid.value || undefined,
|
|
291
|
+
'data-invalid': invalid.value ? '' : undefined,
|
|
292
|
+
}));
|
|
293
|
+
|
|
294
|
+
watch(field.value, (value) => {
|
|
295
|
+
displayValue.value = formatDisplay(value);
|
|
296
|
+
if (isDateTimeValue(value)) clearParseError();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
watch(() => [props.dateFormat, props.timeFormat, props.showSeconds], () => {
|
|
300
|
+
displayValue.value = formatDisplay(field.value.value);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
function defaultPropsFrom(definitions) {
|
|
304
|
+
return Object.fromEntries(Object.entries(definitions).map(([key, definition]) => [key, defaultPropValue(definition)]));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function defaultPropValue(definition) {
|
|
308
|
+
if (!definition || !Object.prototype.hasOwnProperty.call(definition, 'default')) {
|
|
309
|
+
return definition?.type === Boolean ? false : undefined;
|
|
310
|
+
}
|
|
311
|
+
if (typeof definition.default === 'function' && definition.type !== Function) return definition.default();
|
|
312
|
+
return definition.default;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function pad(value) {
|
|
316
|
+
return String(value).padStart(2, '0');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isIsoDate(value) {
|
|
320
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
|
|
321
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
322
|
+
const date = new Date(year, month - 1, day);
|
|
323
|
+
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function isoToParts(value) {
|
|
327
|
+
if (!isIsoDate(value)) return null;
|
|
328
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
329
|
+
return { year, month, day };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function partsToIso({ year, month, day }) {
|
|
333
|
+
return `${year}-${pad(month)}-${pad(day)}`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function formatDateDisplay(value) {
|
|
337
|
+
const parts = isoToParts(value);
|
|
338
|
+
if (!parts) return '';
|
|
339
|
+
if (props.dateFormat === 'mm/dd/yyyy') return `${pad(parts.month)}/${pad(parts.day)}/${parts.year}`;
|
|
340
|
+
return `${pad(parts.day)}/${pad(parts.month)}/${parts.year}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function parseDateDisplay(value) {
|
|
344
|
+
const trimmed = String(value || '').trim();
|
|
345
|
+
if (!trimmed) return { value: '', error: '' };
|
|
346
|
+
const isoMatch = trimmed.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
347
|
+
if (isoMatch) {
|
|
348
|
+
const iso = trimmed;
|
|
349
|
+
return isIsoDate(iso) ? { value: iso, error: '' } : { value: '', error: 'Enter a real calendar date.' };
|
|
350
|
+
}
|
|
351
|
+
const match = trimmed.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
352
|
+
if (!match) return { value: '', error: `Use ${props.dateFormat}.` };
|
|
353
|
+
const first = Number(match[1]);
|
|
354
|
+
const second = Number(match[2]);
|
|
355
|
+
const year = Number(match[3]);
|
|
356
|
+
const month = props.dateFormat === 'mm/dd/yyyy' ? first : second;
|
|
357
|
+
const day = props.dateFormat === 'mm/dd/yyyy' ? second : first;
|
|
358
|
+
const iso = partsToIso({ year, month, day });
|
|
359
|
+
if (!isIsoDate(iso)) return { value: '', error: 'Enter a real calendar date.' };
|
|
360
|
+
return { value: iso, error: '' };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function normalizeDateTimeValue(value) {
|
|
364
|
+
const parts = splitDateTime(value);
|
|
365
|
+
if (!parts) return '';
|
|
366
|
+
return combineDateTime(parts.date, parts.time);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isDateTimeValue(value) {
|
|
370
|
+
return normalizeDateTimeValue(value) === value;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function splitDateTime(value) {
|
|
374
|
+
const match = String(value || '').match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?)$/);
|
|
375
|
+
if (!match) return null;
|
|
376
|
+
const date = match[1];
|
|
377
|
+
const time = normalizeTimeValue(match[2], { showSeconds: props.showSeconds });
|
|
378
|
+
if (!isIsoDate(date) || !time) return null;
|
|
379
|
+
return { date, time };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function combineDateTime(date, time) {
|
|
383
|
+
const normalizedTime = normalizeTimeValue(time, { showSeconds: props.showSeconds });
|
|
384
|
+
if (!isIsoDate(date) || !normalizedTime) return '';
|
|
385
|
+
return `${date}T${normalizedTime}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function datePart(value) {
|
|
389
|
+
const match = String(value || '').match(/^(\d{4}-\d{2}-\d{2})/);
|
|
390
|
+
return match && isIsoDate(match[1]) ? match[1] : '';
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function timePart(value) {
|
|
394
|
+
const match = String(value || '').match(/[T ](\d{2}:\d{2}(?::\d{2})?)$/);
|
|
395
|
+
return match ? normalizeTimeValue(match[1], { showSeconds: props.showSeconds }) : '';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function minTimeForDate(date) {
|
|
399
|
+
return date && date === datePart(props.min) ? timePart(props.min) : '';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function maxTimeForDate(date) {
|
|
403
|
+
return date && date === datePart(props.max) ? timePart(props.max) : '';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isDateTimeInRange(value) {
|
|
407
|
+
const normalized = normalizeDateTimeValue(value);
|
|
408
|
+
if (!normalized) return false;
|
|
409
|
+
const min = normalizeDateTimeValue(props.min);
|
|
410
|
+
const max = normalizeDateTimeValue(props.max);
|
|
411
|
+
if (min && normalized < min) return false;
|
|
412
|
+
if (max && normalized > max) return false;
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function rangeError(value) {
|
|
417
|
+
const min = normalizeDateTimeValue(props.min);
|
|
418
|
+
const max = normalizeDateTimeValue(props.max);
|
|
419
|
+
if (min && value < min) return `Date and time must be on or after ${formatDisplay(min)}.`;
|
|
420
|
+
if (max && value > max) return `Date and time must be on or before ${formatDisplay(max)}.`;
|
|
421
|
+
return '';
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function formatDisplay(value) {
|
|
425
|
+
const parts = splitDateTime(value);
|
|
426
|
+
if (!parts) return '';
|
|
427
|
+
return `${formatDateDisplay(parts.date)} ${formatTimeDisplay(parts.time, props)}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function parseDisplay(value) {
|
|
431
|
+
const trimmed = String(value || '').trim();
|
|
432
|
+
if (!trimmed) return { value: '', error: '' };
|
|
433
|
+
const normalized = normalizeDateTimeValue(trimmed);
|
|
434
|
+
if (normalized) {
|
|
435
|
+
const error = rangeError(normalized);
|
|
436
|
+
return error ? { value: '', error } : { value: normalized, error: '' };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const [dateText, ...timeTokens] = trimmed.split(/\s+/);
|
|
440
|
+
const timeText = timeTokens.join(' ');
|
|
441
|
+
if (!dateText || !timeText) return { value: '', error: `Use ${props.dateFormat} plus a time.` };
|
|
442
|
+
const parsedDate = parseDateDisplay(dateText);
|
|
443
|
+
if (parsedDate.error) return { value: '', error: parsedDate.error };
|
|
444
|
+
const parsedTime = parseTimeInput(timeText, props);
|
|
445
|
+
if (parsedTime.error) return { value: '', error: parsedTime.error };
|
|
446
|
+
const nextValue = combineDateTime(parsedDate.value, parsedTime.value);
|
|
447
|
+
const error = rangeError(nextValue);
|
|
448
|
+
return error ? { value: '', error } : { value: nextValue, error: '' };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function clearParseError() {
|
|
452
|
+
parseError.value = '';
|
|
453
|
+
field.setFieldState({ errors: [], invalid: false });
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function setParseError(message) {
|
|
457
|
+
parseError.value = message;
|
|
458
|
+
field.setFieldState({ errors: message ? [message] : [], invalid: Boolean(message) });
|
|
459
|
+
if (message) emit('invalid-date-time', message);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function commitDateTime(value) {
|
|
463
|
+
const normalized = normalizeDateTimeValue(value);
|
|
464
|
+
const error = normalized ? rangeError(normalized) : '';
|
|
465
|
+
if (error) {
|
|
466
|
+
setParseError(error);
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
clearParseError();
|
|
470
|
+
field.setValue(normalized);
|
|
471
|
+
displayValue.value = formatDisplay(normalized);
|
|
472
|
+
emit('change', normalized);
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function commitTypedValue() {
|
|
477
|
+
const parsed = parseDisplay(displayValue.value);
|
|
478
|
+
if (parsed.error) {
|
|
479
|
+
setParseError(parsed.error);
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
commitDateTime(parsed.value);
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function onInput(event) {
|
|
487
|
+
displayValue.value = event.target.value;
|
|
488
|
+
if (!displayValue.value.trim()) commitDateTime('');
|
|
489
|
+
else parseError.value = '';
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async function onBlur(event) {
|
|
493
|
+
let errorMessage = '';
|
|
494
|
+
if (displayValue.value.trim()) {
|
|
495
|
+
const parsed = parseDisplay(displayValue.value);
|
|
496
|
+
if (parsed.error) {
|
|
497
|
+
errorMessage = parsed.error;
|
|
498
|
+
parseError.value = parsed.error;
|
|
499
|
+
} else {
|
|
500
|
+
commitDateTime(parsed.value);
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
clearParseError();
|
|
504
|
+
}
|
|
505
|
+
await field.onBlur(event);
|
|
506
|
+
if (errorMessage) setParseError(errorMessage);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function onKeydown(event) {
|
|
510
|
+
if (event.key === 'Enter') {
|
|
511
|
+
event.preventDefault();
|
|
512
|
+
commitTypedValue();
|
|
513
|
+
} else if (event.key === 'ArrowDown' && event.altKey) {
|
|
514
|
+
event.preventDefault();
|
|
515
|
+
popover.value?.open();
|
|
516
|
+
} else if (event.key === 'Escape') {
|
|
517
|
+
popover.value?.close();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function constrainedTimeForDate(date, preferredTime) {
|
|
522
|
+
const preferred = normalizeTimeValue(preferredTime, { showSeconds: props.showSeconds }) || normalizeTimeValue(props.defaultTime, props) || '09:00';
|
|
523
|
+
const min = minTimeForDate(date);
|
|
524
|
+
const max = maxTimeForDate(date);
|
|
525
|
+
if (min && compareTimeValues(preferred, min) < 0) return min;
|
|
526
|
+
if (max && compareTimeValues(preferred, max) > 0) return max;
|
|
527
|
+
return preferred;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function selectCalendarDate(value) {
|
|
531
|
+
const time = constrainedTimeForDate(value, selectedTime.value);
|
|
532
|
+
commitDateTime(combineDateTime(value, time));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function selectTime(option) {
|
|
536
|
+
if (option.disabled) return;
|
|
537
|
+
const date = selectedDate.value || todayValue();
|
|
538
|
+
const time = constrainedTimeForDate(date, option.value);
|
|
539
|
+
commitDateTime(combineDateTime(date, time));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function setOptionRef(value, element) {
|
|
543
|
+
if (element) optionRefs.set(value, element);
|
|
544
|
+
else optionRefs.delete(value);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function focusCalendar() {
|
|
548
|
+
await nextTick();
|
|
549
|
+
await calendar.value?.focusSelectedDate?.();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function todayValue() {
|
|
553
|
+
const now = new Date();
|
|
554
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function selectedTimeSeconds(value) {
|
|
558
|
+
const seconds = timeToSeconds(value);
|
|
559
|
+
return Number.isFinite(seconds) ? seconds : -1;
|
|
560
|
+
}
|
|
561
|
+
</script>
|
|
562
|
+
|
|
563
|
+
<template>
|
|
564
|
+
<DomField v-bind="fieldAttrs" :chrome="chrome">
|
|
565
|
+
<div class="relative w-full">
|
|
566
|
+
<input
|
|
567
|
+
v-bind="inputAttrs"
|
|
568
|
+
type="text"
|
|
569
|
+
inputmode="numeric"
|
|
570
|
+
autocomplete="off"
|
|
571
|
+
class="skin-input h-10 pr-11"
|
|
572
|
+
@input="onInput"
|
|
573
|
+
@focus="field.onFocus"
|
|
574
|
+
@blur="onBlur"
|
|
575
|
+
@keydown="onKeydown"
|
|
576
|
+
/>
|
|
577
|
+
<button
|
|
578
|
+
:id="triggerId"
|
|
579
|
+
type="button"
|
|
580
|
+
class="absolute inset-y-1 right-1 grid size-8 cursor-pointer place-items-center rounded-md text-muted-fg transition hover:bg-secondary hover:text-canvas-fg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50"
|
|
581
|
+
:disabled="field.disabled.value || field.readOnly.value"
|
|
582
|
+
:aria-label="`Choose date and time for ${props.label || field.name.value}`"
|
|
583
|
+
@click.stop
|
|
584
|
+
@focus="field.onFocus"
|
|
585
|
+
@blur="field.onBlur"
|
|
586
|
+
>
|
|
587
|
+
<svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
|
|
588
|
+
<path d="M7 4v3M17 4v3M5 8h14M6 6h12v13H6V6Zm7 7v3l2 1" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
|
|
589
|
+
</svg>
|
|
590
|
+
</button>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<DomPopover
|
|
594
|
+
ref="popover"
|
|
595
|
+
v-bind="resolvedPopoverProps"
|
|
596
|
+
@open="focusCalendar"
|
|
597
|
+
>
|
|
598
|
+
<div class="grid max-h-[calc(100vh-1rem)] gap-0 overflow-auto md:grid-cols-[20rem_14rem]">
|
|
599
|
+
<DomCalendar
|
|
600
|
+
ref="calendar"
|
|
601
|
+
v-bind="resolvedCalendarProps"
|
|
602
|
+
@change="selectCalendarDate"
|
|
603
|
+
/>
|
|
604
|
+
|
|
605
|
+
<div class="border-t border-border p-3 md:border-l md:border-t-0">
|
|
606
|
+
<div class="mb-2 flex items-center justify-between gap-3">
|
|
607
|
+
<p class="text-sm font-semibold text-canvas-fg">Time</p>
|
|
608
|
+
<p class="text-xs text-muted-fg">{{ selectedTime ? formatTimeDisplay(selectedTime, props) : 'Choose time' }}</p>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="grid max-h-72 grid-cols-2 gap-1 overflow-auto pr-1" role="listbox" :aria-label="`${props.label || 'Date time'} time options`">
|
|
611
|
+
<button
|
|
612
|
+
v-for="option in timeOptions"
|
|
613
|
+
:key="option.value"
|
|
614
|
+
:ref="(element) => setOptionRef(option.value, element)"
|
|
615
|
+
type="button"
|
|
616
|
+
role="option"
|
|
617
|
+
class="rounded-md px-2.5 py-2 text-left text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-40"
|
|
618
|
+
:class="selectedTimeSeconds(option.value) === selectedTimeSeconds(selectedTime) ? 'bg-primary text-primary-fg' : 'text-canvas-fg hover:bg-secondary'"
|
|
619
|
+
:aria-selected="selectedTimeSeconds(option.value) === selectedTimeSeconds(selectedTime)"
|
|
620
|
+
:disabled="option.disabled"
|
|
621
|
+
@click="selectTime(option)"
|
|
622
|
+
>
|
|
623
|
+
{{ option.label }}
|
|
624
|
+
</button>
|
|
625
|
+
</div>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
</DomPopover>
|
|
629
|
+
</DomField>
|
|
630
|
+
</template>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import DomTextInput from '../text-input/DomTextInput.vue';
|
|
4
|
+
import { fieldProps } from '../field/fieldProps.js';
|
|
5
|
+
import { emailValidator } from '../field/validators.js';
|
|
6
|
+
|
|
7
|
+
defineOptions({
|
|
8
|
+
__doc: {
|
|
9
|
+
name: 'Email input',
|
|
10
|
+
tag: '<DomEmailInput>',
|
|
11
|
+
description: 'A convention text field for email addresses with email type, autocomplete, and an email validator.',
|
|
12
|
+
icon: 'M4.75 6.75h14.5v10.5H4.75V6.75Zm0 0 7.25 5.5 7.25-5.5',
|
|
13
|
+
events: [
|
|
14
|
+
{ name: 'update:modelValue', payload: 'string', description: 'Fired when the email address changes.' },
|
|
15
|
+
{ name: 'focus', payload: 'FocusEvent', description: 'Fired when the input receives focus.' },
|
|
16
|
+
{ name: 'blur', payload: 'FocusEvent', description: 'Fired when the input loses focus.' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const props = defineProps({
|
|
22
|
+
...fieldProps,
|
|
23
|
+
label: {
|
|
24
|
+
type: String,
|
|
25
|
+
default: 'Email address',
|
|
26
|
+
_edit: { description: 'Visible field label.' },
|
|
27
|
+
},
|
|
28
|
+
placeholder: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: 'you@example.com',
|
|
31
|
+
_edit: { description: 'Placeholder text shown before entry.' },
|
|
32
|
+
},
|
|
33
|
+
autocomplete: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: 'email',
|
|
36
|
+
_edit: { description: 'Native autocomplete hint.' },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
|
|
40
|
+
|
|
41
|
+
const inputProps = computed(() => ({
|
|
42
|
+
...props,
|
|
43
|
+
type: 'email',
|
|
44
|
+
validators: [emailValidator, ...(props.validators || [])],
|
|
45
|
+
}));
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<DomTextInput
|
|
50
|
+
v-bind="inputProps"
|
|
51
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
52
|
+
@focus="emit('focus', $event)"
|
|
53
|
+
@blur="emit('blur', $event)"
|
|
54
|
+
/>
|
|
55
|
+
</template>
|