@getdom/studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE.md +109 -0
  2. package/README.md +44 -0
  3. package/SKILL.md +83 -0
  4. package/package.json +73 -0
  5. package/public/llms.txt +57 -0
  6. package/public/skills.sh +11 -0
  7. package/src/pages/_layout/componentCatalog.generated.js +4514 -0
  8. package/src/pages/_layout/componentInspector.js +60 -0
  9. package/src/pages/_layout/componentManager.js +192 -0
  10. package/src/pages/_layout/componentRuntimeRecords.js +49 -0
  11. package/src/pages/_layout/inspector/componentRegistry.js +639 -0
  12. package/src/pages/_layout/inspector/serialize.js +134 -0
  13. package/src/pages/_layout/inspector/useInspector.js +293 -0
  14. package/src/pages/components/accordion/DomAccordion.vue +39 -0
  15. package/src/pages/components/breadcrumbs/DomBreadcrumbs.vue +347 -0
  16. package/src/pages/components/button/DomButton.vue +132 -0
  17. package/src/pages/components/command-palette/DomCommandPalette.vue +482 -0
  18. package/src/pages/components/context-menu/DomContextMenu.vue +178 -0
  19. package/src/pages/components/dialog/DomDialog.vue +271 -0
  20. package/src/pages/components/dialog/DomDialogSchemaForm.vue +72 -0
  21. package/src/pages/components/dialog/DomDialogStack.vue +289 -0
  22. package/src/pages/components/dialog/useDialogs.js +253 -0
  23. package/src/pages/components/drawer/DomDrawer.vue +159 -0
  24. package/src/pages/components/dropdown/DomDropdown.vue +261 -0
  25. package/src/pages/components/icon-button/DomIconButton.vue +155 -0
  26. package/src/pages/components/lightbox/DomLightbox.vue +1561 -0
  27. package/src/pages/components/menu/DomMenu.vue +359 -0
  28. package/src/pages/components/menu/DomMenuItem.vue +233 -0
  29. package/src/pages/components/menu/DomMenuLabel.vue +19 -0
  30. package/src/pages/components/menu/DomMenuSeparator.vue +14 -0
  31. package/src/pages/components/menu/menuContext.js +11 -0
  32. package/src/pages/components/menu/menuPlacement.js +26 -0
  33. package/src/pages/components/menu/submenu.js +58 -0
  34. package/src/pages/components/pagination/DomPagination.vue +265 -0
  35. package/src/pages/components/playground/DomRenderer.vue +48 -0
  36. package/src/pages/components/popover/DomPopover.vue +244 -0
  37. package/src/pages/components/profile-menu/DomProfileMenu.vue +408 -0
  38. package/src/pages/components/profile-menu/DomProfileMenuItemContent.vue +45 -0
  39. package/src/pages/components/scroll-menu/DomScrollMenu.vue +430 -0
  40. package/src/pages/components/scroll-menu/DomScrollMenuItem.vue +212 -0
  41. package/src/pages/components/scroll-menu/scrollMenuContext.js +22 -0
  42. package/src/pages/components/splitter-panel/DomSplitterPanel.vue +283 -0
  43. package/src/pages/components/stepper/DomStepper.vue +386 -0
  44. package/src/pages/components/tabs/DomTabs.vue +149 -0
  45. package/src/pages/components/theme-switcher/DomThemeSwitcher.vue +226 -0
  46. package/src/pages/components/toast/DomToastItem.vue +52 -0
  47. package/src/pages/components/toast/DomToastStack.vue +768 -0
  48. package/src/pages/components/toast/useToasts.js +237 -0
  49. package/src/pages/components/toolbar/DomToolbar.vue +158 -0
  50. package/src/pages/components/tooltip/DomTooltip.vue +69 -0
  51. package/src/pages/components/tooltip/vDomTooltip.js +106 -0
  52. package/src/pages/components/tree-view/DomTreeView.vue +961 -0
  53. package/src/pages/components/tree-view/treeSchemas.js +129 -0
  54. package/src/pages/components/tree-view/treeUtils.js +228 -0
  55. package/src/pages/data/_shared/calculateVirtualWindow.js +101 -0
  56. package/src/pages/data/_shared/resourceRecord.js +29 -0
  57. package/src/pages/data/activity-timeline/DomActivityTimeline.vue +417 -0
  58. package/src/pages/data/agent-chat/DomAgentChatShell.vue +565 -0
  59. package/src/pages/data/agent-chat/DomAgentConversation.vue +737 -0
  60. package/src/pages/data/agent-chat/DomAgentConversationList.vue +319 -0
  61. package/src/pages/data/agent-chat/examples/agentChatMockApi.js +620 -0
  62. package/src/pages/data/agent-chat/examples/useAgentChatDemoStore.js +730 -0
  63. package/src/pages/data/data-grid/DataGridVirtualRows.vue +220 -0
  64. package/src/pages/data/data-grid/DomDataGrid.vue +1643 -0
  65. package/src/pages/data/document-editor/DomDocumentEditor.vue +3030 -0
  66. package/src/pages/data/document-editor/documentEditorCodeBlock.js +282 -0
  67. package/src/pages/data/document-editor/examples/documentEditorMockApi.js +272 -0
  68. package/src/pages/data/document-editor/examples/useDocumentEditorDemoStore.js +284 -0
  69. package/src/pages/data/filter-bar/DomFilterBar.vue +533 -0
  70. package/src/pages/data/kanban/DomKanban.vue +1703 -0
  71. package/src/pages/data/kanban/kanbanApi.js +80 -0
  72. package/src/pages/data/media-browser/DomMediaBrowser.vue +2344 -0
  73. package/src/pages/data/media-browser/examples/mediaBrowserMockApi.js +653 -0
  74. package/src/pages/data/media-browser/examples/useMediaBrowserDemoStore.js +1157 -0
  75. package/src/pages/data/month-calendar/DomMonthCalendar.vue +1718 -0
  76. package/src/pages/data/week-planner/DomWeekPlanner.vue +2308 -0
  77. package/src/pages/dev/code-block/DomCodeBlock.vue +263 -0
  78. package/src/pages/dev/json-viewer/DomJsonViewer.vue +962 -0
  79. package/src/pages/forms/_shared/DomFieldLoadingSpinner.vue +12 -0
  80. package/src/pages/forms/_shared/options.js +61 -0
  81. package/src/pages/forms/_shared/useOptionMenu.js +59 -0
  82. package/src/pages/forms/autocomplete/DomAutocomplete.vue +180 -0
  83. package/src/pages/forms/calendar/DomCalendar.vue +442 -0
  84. package/src/pages/forms/checkbox/DomCheckbox.vue +69 -0
  85. package/src/pages/forms/class-toggle-input/DomClassToggleInput.vue +316 -0
  86. package/src/pages/forms/code-input/DomCodeInput.vue +202 -0
  87. package/src/pages/forms/color-input/DomColorInput.vue +118 -0
  88. package/src/pages/forms/combobox/DomCombobox.vue +237 -0
  89. package/src/pages/forms/confirmation-code-input/DomConfirmationCodeInput.vue +345 -0
  90. package/src/pages/forms/date-picker/DomDatePicker.vue +299 -0
  91. package/src/pages/forms/date-time-picker/DomDateTimePicker.vue +630 -0
  92. package/src/pages/forms/email-input/DomEmailInput.vue +55 -0
  93. package/src/pages/forms/field/DomField.vue +267 -0
  94. package/src/pages/forms/field/fieldLayout.js +1 -0
  95. package/src/pages/forms/field/fieldProps.js +114 -0
  96. package/src/pages/forms/field/layouts/DomGridFieldLayout.vue +38 -0
  97. package/src/pages/forms/field/useField.js +343 -0
  98. package/src/pages/forms/field/validators.js +364 -0
  99. package/src/pages/forms/file-upload/DomFileUpload.vue +361 -0
  100. package/src/pages/forms/form/DomForm.vue +985 -0
  101. package/src/pages/forms/form/adapters/fieldBuilder.js +45 -0
  102. package/src/pages/forms/form/adapters/jsonSchema.js +264 -0
  103. package/src/pages/forms/form/adapters/shared.js +53 -0
  104. package/src/pages/forms/form/adapters/zod.js +235 -0
  105. package/src/pages/forms/form/formApi.js +38 -0
  106. package/src/pages/forms/form/formDefinition.js +392 -0
  107. package/src/pages/forms/form/schemaAdapter.js +54 -0
  108. package/src/pages/forms/icon-selector/DomIconSelector.vue +460 -0
  109. package/src/pages/forms/icon-selector/iconSelectorItems.js +490 -0
  110. package/src/pages/forms/input-group/DomInputGroup.vue +232 -0
  111. package/src/pages/forms/json-input/DomJsonInput.vue +227 -0
  112. package/src/pages/forms/json-list-input/DomJsonListInput.vue +206 -0
  113. package/src/pages/forms/listbox/DomListbox.vue +105 -0
  114. package/src/pages/forms/money-input/DomMoneyInput.vue +147 -0
  115. package/src/pages/forms/native-select/DomNativeSelect.vue +67 -0
  116. package/src/pages/forms/number-input/DomNumberInput.vue +47 -0
  117. package/src/pages/forms/password-input/DomPasswordInput.vue +218 -0
  118. package/src/pages/forms/password-input/pwnedPasswords.js +47 -0
  119. package/src/pages/forms/password-strength/DomPasswordStrength.vue +128 -0
  120. package/src/pages/forms/password-strength/passwordStrength.js +92 -0
  121. package/src/pages/forms/position-input/DomPositionInput.vue +83 -0
  122. package/src/pages/forms/radio-group/DomRadioGroup.vue +102 -0
  123. package/src/pages/forms/range-input/DomRangeInput.vue +296 -0
  124. package/src/pages/forms/select/DomSelect.vue +479 -0
  125. package/src/pages/forms/select-input/DomSelectInput.vue +155 -0
  126. package/src/pages/forms/tag-combobox/DomTagCombobox.vue +358 -0
  127. package/src/pages/forms/text-input/DomTextInput.vue +48 -0
  128. package/src/pages/forms/textarea-composer/DomTextareaComposer.vue +234 -0
  129. package/src/pages/forms/textarea-input/DomTextareaInput.vue +86 -0
  130. package/src/pages/forms/textarea-input/useTextareaAutosize.js +147 -0
  131. package/src/pages/forms/time-selector/DomTimeSelector.vue +389 -0
  132. package/src/pages/forms/time-selector/timeUtils.js +194 -0
  133. package/src/pages/forms/toggle/DomToggle.vue +158 -0
  134. package/src/pages/forms/toggle-button/DomToggleButton.vue +88 -0
  135. package/src/pages/forms/toggle-button-group/DomToggleButtonGroup.vue +340 -0
  136. package/src/pages/forms/url-input/DomUrlInput.vue +55 -0
  137. package/src/pages/forms/wysiwyg/DomWysiwyg.vue +204 -0
  138. package/src/pages/lib/css/color.js +245 -0
  139. package/src/pages/lib/headless/accordion.d.ts +7 -0
  140. package/src/pages/lib/headless/accordion.js +246 -0
  141. package/src/pages/lib/headless/autocomplete.d.ts +4 -0
  142. package/src/pages/lib/headless/autocomplete.js +33 -0
  143. package/src/pages/lib/headless/base.d.ts +25 -0
  144. package/src/pages/lib/headless/base.js +127 -0
  145. package/src/pages/lib/headless/checkbox.d.ts +19 -0
  146. package/src/pages/lib/headless/checkbox.js +99 -0
  147. package/src/pages/lib/headless/combobox.d.ts +7 -0
  148. package/src/pages/lib/headless/combobox.js +488 -0
  149. package/src/pages/lib/headless/dialog.d.ts +23 -0
  150. package/src/pages/lib/headless/dialog.js +384 -0
  151. package/src/pages/lib/headless/drawer.d.ts +21 -0
  152. package/src/pages/lib/headless/drawer.js +682 -0
  153. package/src/pages/lib/headless/dropdown.d.ts +19 -0
  154. package/src/pages/lib/headless/dropdown.js +412 -0
  155. package/src/pages/lib/headless/floating-effect.js +118 -0
  156. package/src/pages/lib/headless/floating.d.ts +34 -0
  157. package/src/pages/lib/headless/floating.js +382 -0
  158. package/src/pages/lib/headless/index.d.ts +19 -0
  159. package/src/pages/lib/headless/index.js +19 -0
  160. package/src/pages/lib/headless/listbox.d.ts +19 -0
  161. package/src/pages/lib/headless/listbox.js +126 -0
  162. package/src/pages/lib/headless/menu.d.ts +18 -0
  163. package/src/pages/lib/headless/menu.js +117 -0
  164. package/src/pages/lib/headless/popover-panel.d.ts +18 -0
  165. package/src/pages/lib/headless/popover-panel.js +309 -0
  166. package/src/pages/lib/headless/popover.d.ts +19 -0
  167. package/src/pages/lib/headless/popover.js +391 -0
  168. package/src/pages/lib/headless/radio-group.d.ts +19 -0
  169. package/src/pages/lib/headless/radio-group.js +100 -0
  170. package/src/pages/lib/headless/tabs.d.ts +18 -0
  171. package/src/pages/lib/headless/tabs.js +119 -0
  172. package/src/pages/lib/headless/toast.d.ts +13 -0
  173. package/src/pages/lib/headless/toast.js +65 -0
  174. package/src/pages/lib/headless/toggle.d.ts +19 -0
  175. package/src/pages/lib/headless/toggle.js +162 -0
  176. package/src/pages/lib/headless/tooltip-controller.d.ts +22 -0
  177. package/src/pages/lib/headless/tooltip-controller.js +549 -0
  178. package/src/pages/lib/headless/tooltip.d.ts +19 -0
  179. package/src/pages/lib/headless/tooltip.js +153 -0
  180. package/src/pages/lib/headless/transitions.d.ts +5 -0
  181. package/src/pages/lib/headless/transitions.js +89 -0
  182. package/src/pages/lib/vue/_internal/DomLiquidGlassSurface.vue +310 -0
  183. package/src/pages/lib/vue/_internal/liquidGlass.js +112 -0
  184. package/src/pages/lib/vue/_internal/liquidGlassConfig.js +10 -0
  185. package/src/pages/lib/vue/codeHighlighter.js +132 -0
  186. package/src/pages/lib/vue/floatingTeleportTarget.js +23 -0
  187. package/src/pages/lib/vue/index.d.ts +6806 -0
  188. package/src/pages/lib/vue/index.js +218 -0
  189. package/src/pages/lib/vue/useTheme.js +116 -0
  190. package/src/pages/mobile/action-sheet/DomActionSheet.vue +186 -0
  191. package/src/pages/mobile/bottom-nav/DomAppBottomNav.vue +94 -0
  192. package/src/pages/mobile/list-item/DomAppListItem.vue +90 -0
  193. package/src/pages/mobile/safe-area/DomSafeArea.vue +39 -0
  194. package/src/pages/mobile/shell/DomAppShell.vue +51 -0
  195. package/src/pages/mobile/stack/AppStackScreen.vue +63 -0
  196. package/src/pages/mobile/stack/DomAppStack.vue +711 -0
  197. package/src/pages/mobile/top-bar/DomAppTopBar.vue +67 -0
  198. package/src/pages/visual/alert/DomAlert.vue +363 -0
  199. package/src/pages/visual/avatar/DomAvatar.vue +110 -0
  200. package/src/pages/visual/avatar-stack/DomAvatarStack.vue +89 -0
  201. package/src/pages/visual/badge/DomBadge.vue +105 -0
  202. package/src/pages/visual/card/DomCard.vue +65 -0
  203. package/src/pages/visual/card-rail/DomCardRail.vue +99 -0
  204. package/src/pages/visual/empty-state/DomEmptyState.vue +88 -0
  205. package/src/pages/visual/progress/DomProgress.vue +212 -0
  206. package/src/pages/visual/progress/DomRadialProgress.vue +186 -0
  207. package/src/pages/visual/progress/DomSegmentedProgress.vue +350 -0
  208. package/src/pages/visual/skeleton/DomSkeleton.vue +174 -0
  209. package/src/pages/visual/spinner/DomSpinner.vue +122 -0
  210. package/src/pages/visual/status-pill/DomStatusPill.vue +75 -0
  211. package/src/pages/visual/text-diff/DomTextDiff.vue +577 -0
  212. package/src/pages/visual/text-diff/textDiff.js +300 -0
  213. package/src/style.css +446 -0
@@ -0,0 +1,299 @@
1
+ <script setup>
2
+ import { computed, nextTick, ref, useId, watch } from 'vue';
3
+ import DomPopover, { popoverProps } from '../../components/popover/DomPopover.vue';
4
+ import DomCalendar, { calendarProps } from '../calendar/DomCalendar.vue';
5
+ import DomField from '../field/DomField.vue';
6
+ import { fieldProps } from '../field/fieldProps.js';
7
+ import { useField } from '../field/useField.js';
8
+
9
+ defineOptions({
10
+ __doc: {
11
+ name: 'Date picker',
12
+ tag: '<DomDatePicker>',
13
+ description: 'A text date input with a popover calendar picker. Stores YYYY-MM-DD while letting people type dd/mm/yyyy or mm/dd/yyyy.',
14
+ icon: 'M7 4v3M17 4v3M5 8h14M6 6h12v13H6V6Zm3 6h2M13 12h2M9 16h2M13 16h2',
15
+ events: [
16
+ { name: 'update:modelValue', payload: 'YYYY-MM-DD', description: 'Fired when a valid date is typed or selected.' },
17
+ { name: 'change', payload: 'YYYY-MM-DD', description: 'Fired when a valid date is committed.' },
18
+ { name: 'invalid-date', payload: 'string', description: 'Fired when typed text cannot be parsed or is outside the allowed range.' },
19
+ { name: 'focus', payload: 'FocusEvent', description: 'Fired when the text input receives focus.' },
20
+ { name: 'blur', payload: 'FocusEvent', description: 'Fired when the text input loses focus.' },
21
+ ],
22
+ keyboard: [
23
+ { keys: 'Alt + ArrowDown', action: 'Open the calendar popover.' },
24
+ { keys: 'Enter', action: 'Commit the typed date when it is valid.' },
25
+ { keys: 'Esc', action: 'Close the popover.' },
26
+ { keys: 'Arrow keys in calendar', action: 'Move focus by day or week once the calendar is open.' },
27
+ ],
28
+ },
29
+ });
30
+
31
+ const props = defineProps({
32
+ ...fieldProps,
33
+ modelValue: {
34
+ type: String,
35
+ default: '',
36
+ _edit: { group: 'Control props', description: 'Selected date as YYYY-MM-DD.' },
37
+ },
38
+ dateFormat: {
39
+ type: String,
40
+ default: 'dd/mm/yyyy',
41
+ _edit: { options: ['dd/mm/yyyy', 'mm/dd/yyyy'], description: 'How typed dates are parsed and displayed.' },
42
+ },
43
+ popoverProps: {
44
+ type: Object,
45
+ default: () => ({}),
46
+ _edit: {
47
+ component: 'DomPropDefinitionInput',
48
+ description: 'Props passed to the inner DomPopover. Date picker defaults position to bottom-end, width to w-[20rem], padding to p-0, and arrow to true.',
49
+ props: {
50
+ definitions: popoverProps,
51
+ include: ['position', 'offset', 'collisionPadding', 'floatingMode', 'flip', 'lockScroll', 'arrow', 'width', 'padding'],
52
+ compact: true,
53
+ },
54
+ },
55
+ },
56
+ calendarProps: {
57
+ type: Object,
58
+ default: () => ({}),
59
+ _edit: {
60
+ component: 'DomPropDefinitionInput',
61
+ description: 'Props passed to the inner DomCalendar, such as locale, weekStartsOn, min, max, fixedWeeks, and showAdjacentDays.',
62
+ props: {
63
+ definitions: calendarProps,
64
+ include: ['locale', 'weekStartsOn', 'initialMonth', 'initialYear', 'min', 'max', 'showAdjacentDays', 'fixedWeeks', 'disabled'],
65
+ compact: true,
66
+ },
67
+ },
68
+ },
69
+ });
70
+
71
+ const emit = defineEmits(['update:modelValue', 'change', 'invalid-date', 'focus', 'blur']);
72
+ const field = useField(props, emit, { idPrefix: 'dom-date-picker' });
73
+ const popover = ref(null);
74
+ const calendar = ref(null);
75
+ const displayValue = ref(formatDisplay(field.value.value));
76
+ const parseError = ref('');
77
+ const uniqueId = useId();
78
+ const triggerId = computed(() => `${field.id.value || uniqueId}-calendar-button`);
79
+ const placeholderText = computed(() => props.placeholder || props.dateFormat);
80
+ const resolvedCalendarProps = computed(() => ({
81
+ ...defaultPropsFrom(calendarProps),
82
+ ...props.calendarProps,
83
+ modelValue: field.value.value,
84
+ disabled: field.disabled.value || props.calendarProps.disabled,
85
+ chrome: 'none',
86
+ registerField: false,
87
+ nativeInput: false,
88
+ }));
89
+ const resolvedPopoverProps = computed(() => ({
90
+ ...defaultPropsFrom(popoverProps),
91
+ position: 'bottom-end',
92
+ width: 'w-[20rem]',
93
+ padding: 'p-0',
94
+ arrow: true,
95
+ ...props.popoverProps,
96
+ triggerId: triggerId.value,
97
+ }));
98
+ const combinedErrors = computed(() => [
99
+ ...field.errors.value,
100
+ ...(parseError.value ? [parseError.value] : []),
101
+ ]);
102
+ const invalid = computed(() => Boolean(field.invalid.value || parseError.value));
103
+ const fieldAttrs = computed(() => ({
104
+ ...field.fieldAttrs.value,
105
+ invalid: invalid.value,
106
+ errors: combinedErrors.value,
107
+ }));
108
+ const inputAttrs = computed(() => ({
109
+ ...field.inputAttrs.value,
110
+ value: displayValue.value,
111
+ placeholder: placeholderText.value,
112
+ 'aria-invalid': invalid.value || undefined,
113
+ 'data-invalid': invalid.value ? '' : undefined,
114
+ }));
115
+
116
+ watch(field.value, (value) => {
117
+ displayValue.value = formatDisplay(value);
118
+ if (isIsoDate(value)) clearParseError();
119
+ });
120
+
121
+ function defaultPropsFrom(definitions) {
122
+ return Object.fromEntries(Object.entries(definitions).map(([key, definition]) => [key, defaultPropValue(definition)]));
123
+ }
124
+
125
+ function defaultPropValue(definition) {
126
+ if (!definition || !Object.prototype.hasOwnProperty.call(definition, 'default')) {
127
+ return definition?.type === Boolean ? false : undefined;
128
+ }
129
+ if (typeof definition.default === 'function' && definition.type !== Function) return definition.default();
130
+ return definition.default;
131
+ }
132
+
133
+ function pad(value) {
134
+ return String(value).padStart(2, '0');
135
+ }
136
+
137
+ function isIsoDate(value) {
138
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return false;
139
+ const [year, month, day] = value.split('-').map(Number);
140
+ const date = new Date(year, month - 1, day);
141
+ return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
142
+ }
143
+
144
+ function isoToParts(value) {
145
+ if (!isIsoDate(value)) return null;
146
+ const [year, month, day] = value.split('-').map(Number);
147
+ return { year, month, day };
148
+ }
149
+
150
+ function partsToIso({ year, month, day }) {
151
+ return `${year}-${pad(month)}-${pad(day)}`;
152
+ }
153
+
154
+ function formatDisplay(value) {
155
+ const parts = isoToParts(value);
156
+ if (!parts) return '';
157
+ if (props.dateFormat === 'mm/dd/yyyy') return `${pad(parts.month)}/${pad(parts.day)}/${parts.year}`;
158
+ return `${pad(parts.day)}/${pad(parts.month)}/${parts.year}`;
159
+ }
160
+
161
+ function parseDisplay(value) {
162
+ const trimmed = String(value || '').trim();
163
+ if (!trimmed) return { iso: '', error: '' };
164
+ const match = trimmed.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
165
+ if (!match) return { iso: '', error: `Use ${props.dateFormat}.` };
166
+ const first = Number(match[1]);
167
+ const second = Number(match[2]);
168
+ const year = Number(match[3]);
169
+ const month = props.dateFormat === 'mm/dd/yyyy' ? first : second;
170
+ const day = props.dateFormat === 'mm/dd/yyyy' ? second : first;
171
+ const iso = partsToIso({ year, month, day });
172
+ if (!isIsoDate(iso)) return { iso: '', error: 'Enter a real calendar date.' };
173
+ if (resolvedCalendarProps.value.min && iso < resolvedCalendarProps.value.min) return { iso: '', error: `Date must be on or after ${formatDisplay(resolvedCalendarProps.value.min)}.` };
174
+ if (resolvedCalendarProps.value.max && iso > resolvedCalendarProps.value.max) return { iso: '', error: `Date must be on or before ${formatDisplay(resolvedCalendarProps.value.max)}.` };
175
+ return { iso, error: '' };
176
+ }
177
+
178
+ function clearParseError() {
179
+ parseError.value = '';
180
+ field.setFieldState({ errors: [], invalid: false });
181
+ }
182
+
183
+ function setParseError(message) {
184
+ parseError.value = message;
185
+ field.setFieldState({ errors: message ? [message] : [], invalid: Boolean(message) });
186
+ if (message) emit('invalid-date', message);
187
+ }
188
+
189
+ function commitDate(iso) {
190
+ clearParseError();
191
+ field.setValue(iso);
192
+ displayValue.value = formatDisplay(iso);
193
+ emit('change', iso);
194
+ }
195
+
196
+ function commitTypedValue() {
197
+ const parsed = parseDisplay(displayValue.value);
198
+ if (parsed.error) {
199
+ setParseError(parsed.error);
200
+ return false;
201
+ }
202
+ commitDate(parsed.iso);
203
+ return true;
204
+ }
205
+
206
+ function onInput(event) {
207
+ displayValue.value = event.target.value;
208
+ if (!displayValue.value.trim()) {
209
+ commitDate('');
210
+ return;
211
+ }
212
+ const parsed = parseDisplay(displayValue.value);
213
+ if (!parsed.error) commitDate(parsed.iso);
214
+ else parseError.value = '';
215
+ }
216
+
217
+ async function onBlur(event) {
218
+ let errorMessage = '';
219
+ if (displayValue.value.trim()) {
220
+ const parsed = parseDisplay(displayValue.value);
221
+ if (parsed.error) {
222
+ errorMessage = parsed.error;
223
+ parseError.value = parsed.error;
224
+ } else {
225
+ commitDate(parsed.iso);
226
+ }
227
+ } else {
228
+ clearParseError();
229
+ }
230
+ await field.onBlur(event);
231
+ if (errorMessage) setParseError(errorMessage);
232
+ }
233
+
234
+ function onKeydown(event) {
235
+ if (event.key === 'Enter') {
236
+ event.preventDefault();
237
+ commitTypedValue();
238
+ } else if (event.key === 'ArrowDown' && event.altKey) {
239
+ event.preventDefault();
240
+ popover.value?.open();
241
+ } else if (event.key === 'Escape') {
242
+ popover.value?.close();
243
+ }
244
+ }
245
+
246
+ function selectCalendarDate(value) {
247
+ commitDate(value);
248
+ popover.value?.close();
249
+ }
250
+
251
+ async function focusCalendar() {
252
+ await nextTick();
253
+ await calendar.value?.focusSelectedDate?.();
254
+ }
255
+ </script>
256
+
257
+ <template>
258
+ <DomField v-bind="fieldAttrs" :chrome="chrome">
259
+ <div class="relative w-full">
260
+ <input
261
+ v-bind="inputAttrs"
262
+ type="text"
263
+ inputmode="numeric"
264
+ autocomplete="off"
265
+ class="skin-input h-10 pr-11"
266
+ @input="onInput"
267
+ @focus="field.onFocus"
268
+ @blur="onBlur"
269
+ @keydown="onKeydown"
270
+ />
271
+ <button
272
+ :id="triggerId"
273
+ type="button"
274
+ 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"
275
+ :disabled="field.disabled.value || field.readOnly.value"
276
+ :aria-label="`Choose date for ${props.label || field.name.value}`"
277
+ @click.stop
278
+ @focus="field.onFocus"
279
+ @blur="field.onBlur"
280
+ >
281
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
282
+ <path d="M7 4v3M17 4v3M5 8h14M6 6h12v13H6V6Z" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
283
+ </svg>
284
+ </button>
285
+ </div>
286
+
287
+ <DomPopover
288
+ ref="popover"
289
+ v-bind="resolvedPopoverProps"
290
+ @open="focusCalendar"
291
+ >
292
+ <DomCalendar
293
+ ref="calendar"
294
+ v-bind="resolvedCalendarProps"
295
+ @change="selectCalendarDate"
296
+ />
297
+ </DomPopover>
298
+ </DomField>
299
+ </template>