@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,442 @@
1
+ <script>
2
+ import { fieldProps } from '../field/fieldProps.js';
3
+
4
+ export const calendarProps = {
5
+ ...fieldProps,
6
+ modelValue: {
7
+ type: String,
8
+ default: '',
9
+ _edit: { group: 'Control props', description: 'Selected date as YYYY-MM-DD.' },
10
+ },
11
+ locale: {
12
+ type: String,
13
+ default: 'en-GB',
14
+ _edit: { description: 'Locale used for month and weekday labels.' },
15
+ },
16
+ weekStartsOn: {
17
+ type: Number,
18
+ default: 1,
19
+ _edit: { options: [0, 1, 2, 3, 4, 5, 6], description: 'First day of week. 0 is Sunday, 1 is Monday.' },
20
+ },
21
+ initialMonth: {
22
+ type: Number,
23
+ default: null,
24
+ _edit: { description: 'Initial visible month, 1-12. Ignored when modelValue has a valid date.' },
25
+ },
26
+ initialYear: {
27
+ type: Number,
28
+ default: null,
29
+ _edit: { description: 'Initial visible year. Ignored when modelValue has a valid date.' },
30
+ },
31
+ min: {
32
+ type: String,
33
+ default: '',
34
+ _edit: { description: 'Minimum selectable date as YYYY-MM-DD.' },
35
+ },
36
+ max: {
37
+ type: String,
38
+ default: '',
39
+ _edit: { description: 'Maximum selectable date as YYYY-MM-DD.' },
40
+ },
41
+ enabledDates: {
42
+ type: Array,
43
+ default: () => [],
44
+ _edit: {
45
+ component: 'DomJsonListInput',
46
+ props: {
47
+ description: 'Optional allow-list of selectable YYYY-MM-DD dates. When empty, every date inside the min/max range is selectable.',
48
+ compact: true,
49
+ schema: [
50
+ { key: 'value', label: 'Date', placeholder: '2026-06-18' },
51
+ ],
52
+ },
53
+ description: 'Optional allow-list of selectable dates as YYYY-MM-DD values.',
54
+ },
55
+ },
56
+ showAdjacentDays: {
57
+ type: Boolean,
58
+ default: true,
59
+ _edit: { description: 'Show dates from the previous and next months in the grid.' },
60
+ },
61
+ fixedWeeks: {
62
+ type: Boolean,
63
+ default: true,
64
+ _edit: { description: 'Always render six weeks so the calendar height stays stable.' },
65
+ },
66
+ registerField: {
67
+ type: Boolean,
68
+ default: true,
69
+ _edit: { description: 'Register with a parent form. Disable when embedding the calendar inside another form control.' },
70
+ },
71
+ nativeInput: {
72
+ type: Boolean,
73
+ default: true,
74
+ _edit: { description: 'Render the hidden native input used for HTML form submission.' },
75
+ },
76
+ disabled: {
77
+ type: Boolean,
78
+ default: false,
79
+ _edit: { description: 'Disable date selection.' },
80
+ },
81
+ };
82
+ </script>
83
+
84
+ <script setup>
85
+ import { computed, nextTick, ref, watch } from 'vue';
86
+ import DomField from '../field/DomField.vue';
87
+ import { useField } from '../field/useField.js';
88
+
89
+ defineOptions({
90
+ __doc: {
91
+ name: 'Calendar',
92
+ tag: '<DomCalendar>',
93
+ description: 'A compact month calendar for choosing a date, with month and year navigation.',
94
+ events: [
95
+ { name: 'update:modelValue', payload: 'YYYY-MM-DD', description: 'Fired when a date is selected.' },
96
+ { name: 'change', payload: 'YYYY-MM-DD', description: 'Fired when a date is selected.' },
97
+ { name: 'view-change', payload: '({ month, year })', description: 'Fired when the visible month or year changes.' },
98
+ ],
99
+ keyboard: [
100
+ { keys: 'ArrowLeft / ArrowRight', action: 'Move focus one day backward or forward.' },
101
+ { keys: 'ArrowUp / ArrowDown', action: 'Move focus one week backward or forward.' },
102
+ { keys: 'Home / End', action: 'Move focus to the first or last day of the current week.' },
103
+ { keys: 'PageUp / PageDown', action: 'Move focus to the same day in the previous or next month.' },
104
+ { keys: 'Shift + PageUp / PageDown', action: 'Move focus to the same day in the previous or next year.' },
105
+ { keys: 'Enter / Space', action: 'Select the focused date.' },
106
+ ],
107
+ },
108
+ });
109
+
110
+ const props = defineProps(calendarProps);
111
+
112
+ const emit = defineEmits(['update:modelValue', 'change', 'view-change', 'focus', 'blur']);
113
+ const field = useField(props, emit, { idPrefix: 'dom-calendar', register: props.registerField });
114
+
115
+ const today = new Date();
116
+ const initialDate = parseDate(field.value.value);
117
+ const viewYear = ref(initialDate?.getFullYear() || props.initialYear || today.getFullYear());
118
+ const viewMonth = ref(initialDate?.getMonth() ?? clampMonth(props.initialMonth) ?? today.getMonth());
119
+ const activeDateValue = ref(formatDate(initialDate || today));
120
+ const dayRefs = new Map();
121
+
122
+ const monthLabel = computed(() => new Intl.DateTimeFormat(props.locale, {
123
+ month: 'long',
124
+ year: 'numeric',
125
+ }).format(new Date(viewYear.value, viewMonth.value, 1)));
126
+
127
+ const weekdayLabels = computed(() => {
128
+ const formatter = new Intl.DateTimeFormat(props.locale, { weekday: 'short' });
129
+ const sunday = new Date(2026, 0, 4);
130
+ return Array.from({ length: 7 }, (_, index) => {
131
+ const dayOffset = (index + props.weekStartsOn) % 7;
132
+ return formatter.format(new Date(2026, 0, sunday.getDate() + dayOffset));
133
+ });
134
+ });
135
+ const dateLabelFormatter = computed(() => new Intl.DateTimeFormat(props.locale, {
136
+ weekday: 'long',
137
+ day: 'numeric',
138
+ month: 'long',
139
+ year: 'numeric',
140
+ }));
141
+ const enabledDateSet = computed(() => new Set(props.enabledDates.map(normalizeEnabledDate).filter(Boolean)));
142
+
143
+ const cells = computed(() => {
144
+ const first = new Date(viewYear.value, viewMonth.value, 1);
145
+ const offset = (first.getDay() - props.weekStartsOn + 7) % 7;
146
+ const daysInMonth = new Date(viewYear.value, viewMonth.value + 1, 0).getDate();
147
+ const total = props.fixedWeeks ? 42 : Math.ceil((offset + daysInMonth) / 7) * 7;
148
+
149
+ return Array.from({ length: total }, (_, index) => {
150
+ const date = new Date(viewYear.value, viewMonth.value, index - offset + 1);
151
+ const value = formatDate(date);
152
+ const currentMonth = date.getMonth() === viewMonth.value;
153
+ return {
154
+ date,
155
+ value,
156
+ day: date.getDate(),
157
+ currentMonth,
158
+ selected: value === field.value.value,
159
+ today: value === formatDate(today),
160
+ disabled: field.disabled.value || field.readOnly.value || unavailableDate(value),
161
+ hidden: !currentMonth && !props.showAdjacentDays,
162
+ };
163
+ });
164
+ });
165
+ const tabbableDateValue = computed(() => {
166
+ const activeCell = cells.value.find((cell) => cell.value === activeDateValue.value && !cell.disabled && !cell.hidden);
167
+ return activeCell?.value || visibleSelectableValue();
168
+ });
169
+
170
+ watch(field.value, (value) => {
171
+ const date = parseDate(value);
172
+ if (!date) return;
173
+ viewYear.value = date.getFullYear();
174
+ viewMonth.value = date.getMonth();
175
+ activeDateValue.value = value;
176
+ });
177
+
178
+ function clampMonth(month) {
179
+ if (month == null) return null;
180
+ const value = Number(month);
181
+ if (!Number.isFinite(value)) return null;
182
+ return Math.min(11, Math.max(0, value - 1));
183
+ }
184
+
185
+ function parseDate(value) {
186
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return null;
187
+ const [year, month, day] = value.split('-').map(Number);
188
+ const date = new Date(year, month - 1, day);
189
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
190
+ return date;
191
+ }
192
+
193
+ function pad(value) {
194
+ return String(value).padStart(2, '0');
195
+ }
196
+
197
+ function formatDate(date) {
198
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
199
+ }
200
+
201
+ function dateLabel(cell) {
202
+ const label = dateLabelFormatter.value.format(cell.date);
203
+ return cell.disabled && !field.disabled.value && !field.readOnly.value ? `${label}, unavailable` : label;
204
+ }
205
+
206
+ function outsideRange(value) {
207
+ if (props.min && value < props.min) return true;
208
+ if (props.max && value > props.max) return true;
209
+ return false;
210
+ }
211
+
212
+ function normalizeEnabledDate(item) {
213
+ const value = item && typeof item === 'object' ? item.value : item;
214
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : '';
215
+ }
216
+
217
+ function unavailableDate(value) {
218
+ if (outsideRange(value)) return true;
219
+ return enabledDateSet.value.size > 0 && !enabledDateSet.value.has(value);
220
+ }
221
+
222
+ function emitViewChange() {
223
+ emit('view-change', { month: viewMonth.value + 1, year: viewYear.value });
224
+ }
225
+
226
+ function moveMonth(delta) {
227
+ const next = new Date(viewYear.value, viewMonth.value + delta, 1);
228
+ viewYear.value = next.getFullYear();
229
+ viewMonth.value = next.getMonth();
230
+ emitViewChange();
231
+ }
232
+
233
+ function moveYear(delta) {
234
+ viewYear.value += delta;
235
+ emitViewChange();
236
+ }
237
+
238
+ function selectDate(cell) {
239
+ if (cell.disabled || cell.hidden) return;
240
+ activeDateValue.value = cell.value;
241
+ field.onInput(cell.value);
242
+ emit('change', cell.value);
243
+ if (!cell.currentMonth) {
244
+ viewYear.value = cell.date.getFullYear();
245
+ viewMonth.value = cell.date.getMonth();
246
+ emitViewChange();
247
+ }
248
+ }
249
+
250
+ function setDayRef(value, element) {
251
+ if (element) dayRefs.set(value, element);
252
+ else dayRefs.delete(value);
253
+ }
254
+
255
+ function visibleSelectableValue() {
256
+ return cells.value.find((cell) => !cell.hidden && !cell.disabled)?.value || '';
257
+ }
258
+
259
+ async function focusDate(value, updateView = true) {
260
+ const date = parseDate(value);
261
+ if (!date || unavailableDate(value)) return false;
262
+ activeDateValue.value = value;
263
+ if (updateView && (date.getFullYear() !== viewYear.value || date.getMonth() !== viewMonth.value)) {
264
+ viewYear.value = date.getFullYear();
265
+ viewMonth.value = date.getMonth();
266
+ emitViewChange();
267
+ }
268
+ await nextTick();
269
+ const button = dayRefs.get(value);
270
+ if (!button || button.disabled) return false;
271
+ button.focus();
272
+ return true;
273
+ }
274
+
275
+ function dateByOffset(value, { days = 0, months = 0, years = 0 } = {}) {
276
+ const date = parseDate(value);
277
+ if (!date) return null;
278
+ return new Date(date.getFullYear() + years, date.getMonth() + months, date.getDate() + days);
279
+ }
280
+
281
+ function weekBoundary(value, end = false) {
282
+ const date = parseDate(value);
283
+ if (!date) return null;
284
+ const dayOffset = (date.getDay() - props.weekStartsOn + 7) % 7;
285
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + (end ? 6 - dayOffset : -dayOffset));
286
+ }
287
+
288
+ async function onCellKeydown(event, cell) {
289
+ let target = null;
290
+ if (event.key === 'Enter' || event.key === ' ') {
291
+ event.preventDefault();
292
+ selectDate(cell);
293
+ return;
294
+ }
295
+ if (event.key === 'ArrowLeft') target = dateByOffset(cell.value, { days: -1 });
296
+ else if (event.key === 'ArrowRight') target = dateByOffset(cell.value, { days: 1 });
297
+ else if (event.key === 'ArrowUp') target = dateByOffset(cell.value, { days: -7 });
298
+ else if (event.key === 'ArrowDown') target = dateByOffset(cell.value, { days: 7 });
299
+ else if (event.key === 'Home') target = weekBoundary(cell.value);
300
+ else if (event.key === 'End') target = weekBoundary(cell.value, true);
301
+ else if (event.key === 'PageUp') target = dateByOffset(cell.value, event.shiftKey ? { years: -1 } : { months: -1 });
302
+ else if (event.key === 'PageDown') target = dateByOffset(cell.value, event.shiftKey ? { years: 1 } : { months: 1 });
303
+ if (!target) return;
304
+ event.preventDefault();
305
+ await focusDate(formatDate(target));
306
+ }
307
+
308
+ async function focusSelectedDate() {
309
+ const selected = parseDate(field.value.value) && !unavailableDate(field.value.value)
310
+ ? field.value.value
311
+ : visibleSelectableValue();
312
+ if (selected) await focusDate(selected);
313
+ }
314
+
315
+ defineExpose({
316
+ focusDate,
317
+ focusSelectedDate,
318
+ });
319
+ </script>
320
+
321
+ <template>
322
+ <DomField v-bind="field.fieldAttrs.value" :chrome="chrome">
323
+ <input
324
+ v-if="nativeInput && field.htmlName.value"
325
+ type="hidden"
326
+ :name="field.htmlName.value"
327
+ :value="field.value.value"
328
+ />
329
+ <div
330
+ :id="field.id.value"
331
+ role="group"
332
+ class="skin-card w-full rounded-2xl p-3 data-[invalid]:border-destructive"
333
+ :aria-label="label || undefined"
334
+ :aria-describedby="field.describedBy.value || undefined"
335
+ :aria-invalid="field.invalid.value || undefined"
336
+ :aria-errormessage="field.errorId.value || undefined"
337
+ :data-invalid="field.invalid.value ? '' : undefined"
338
+ >
339
+ <div class="flex items-center justify-between gap-2">
340
+ <div class="flex items-center gap-1">
341
+ <button
342
+ type="button"
343
+ class="grid size-8 place-items-center rounded-lg 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:opacity-40"
344
+ :disabled="field.disabled.value"
345
+ aria-label="Previous year"
346
+ @click="moveYear(-1)"
347
+ @focus="field.onFocus"
348
+ @blur="field.onBlur"
349
+ >
350
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
351
+ <path d="m11 7-5 5 5 5M18 7l-5 5 5 5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
352
+ </svg>
353
+ </button>
354
+ <button
355
+ type="button"
356
+ class="grid size-8 place-items-center rounded-lg 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:opacity-40"
357
+ :disabled="field.disabled.value"
358
+ aria-label="Previous month"
359
+ @click="moveMonth(-1)"
360
+ @focus="field.onFocus"
361
+ @blur="field.onBlur"
362
+ >
363
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
364
+ <path d="m15 18-6-6 6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
365
+ </svg>
366
+ </button>
367
+ </div>
368
+
369
+ <p :id="`${field.id.value}-month`" class="min-w-0 truncate text-sm font-semibold tracking-tight text-canvas-fg">{{ monthLabel }}</p>
370
+
371
+ <div class="flex items-center gap-1">
372
+ <button
373
+ type="button"
374
+ class="grid size-8 place-items-center rounded-lg 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:opacity-40"
375
+ :disabled="field.disabled.value"
376
+ aria-label="Next month"
377
+ @click="moveMonth(1)"
378
+ @focus="field.onFocus"
379
+ @blur="field.onBlur"
380
+ >
381
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
382
+ <path d="m9 18 6-6-6-6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
383
+ </svg>
384
+ </button>
385
+ <button
386
+ type="button"
387
+ class="grid size-8 place-items-center rounded-lg 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:opacity-40"
388
+ :disabled="field.disabled.value"
389
+ aria-label="Next year"
390
+ @click="moveYear(1)"
391
+ @focus="field.onFocus"
392
+ @blur="field.onBlur"
393
+ >
394
+ <svg viewBox="0 0 24 24" class="size-4" fill="none" aria-hidden="true">
395
+ <path d="m6 17 5-5-5-5M13 17l5-5-5-5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
396
+ </svg>
397
+ </button>
398
+ </div>
399
+ </div>
400
+
401
+ <div
402
+ role="grid"
403
+ :aria-labelledby="`${field.id.value}-month`"
404
+ class="mt-3 grid grid-cols-7 gap-1 text-center"
405
+ >
406
+ <span
407
+ v-for="day in weekdayLabels"
408
+ :key="day"
409
+ role="columnheader"
410
+ class="py-1 text-[11px] font-medium uppercase tracking-wider text-muted-fg"
411
+ >{{ day }}</span>
412
+
413
+ <button
414
+ v-for="cell in cells"
415
+ :key="cell.value"
416
+ :ref="(element) => setDayRef(cell.value, element)"
417
+ type="button"
418
+ role="gridcell"
419
+ class="relative grid aspect-square min-h-9 place-items-center rounded-lg text-sm font-medium transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-35"
420
+ :class="[
421
+ cell.hidden && 'invisible',
422
+ !cell.hidden && !cell.currentMonth && 'text-muted-fg/60 hover:bg-secondary/60',
423
+ cell.currentMonth && !cell.selected && 'text-canvas-fg hover:bg-secondary',
424
+ cell.today && !cell.selected && 'ring-1 ring-border',
425
+ cell.selected && 'bg-primary text-primary-fg shadow-sm hover:bg-primary',
426
+ ]"
427
+ :disabled="cell.disabled || cell.hidden"
428
+ :tabindex="cell.value === tabbableDateValue ? 0 : -1"
429
+ :aria-label="dateLabel(cell)"
430
+ :aria-selected="cell.selected"
431
+ :aria-current="cell.today ? 'date' : undefined"
432
+ @click="selectDate(cell)"
433
+ @focus="field.onFocus"
434
+ @blur="field.onBlur"
435
+ @keydown="onCellKeydown($event, cell)"
436
+ >
437
+ {{ cell.day }}
438
+ </button>
439
+ </div>
440
+ </div>
441
+ </DomField>
442
+ </template>
@@ -0,0 +1,69 @@
1
+ <script setup>
2
+ import { computed, onMounted, ref, watch } from 'vue';
3
+ import { fieldProps } from '../field/fieldProps.js';
4
+ import { useField } from '../field/useField.js';
5
+
6
+ defineOptions({
7
+ __doc: {
8
+ name: 'Checkbox',
9
+ tag: '<DomCheckbox>',
10
+ description: 'A single accessible checkbox with a visible label and keyboard support.',
11
+ events: [{ name: 'update:modelValue', payload: '(checked: boolean)', description: 'Emitted when checked changes.' }],
12
+ },
13
+ });
14
+
15
+ const props = defineProps({
16
+ ...fieldProps,
17
+ modelValue: {
18
+ type: Boolean,
19
+ default: false,
20
+ _edit: { description: 'Checked state.' },
21
+ },
22
+ });
23
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur']);
24
+ const root = ref(null);
25
+ const field = useField(props, emit, { idPrefix: 'dom-checkbox' });
26
+ const labelId = computed(() => (props.label ? `${field.id.value}-label` : ''));
27
+
28
+ function onLabelClick(event) {
29
+ if (field.disabled.value) return;
30
+ if (event.target === root.value) return;
31
+ root.value?.focus?.();
32
+ root.value?.toggle?.();
33
+ }
34
+
35
+ onMounted(async () => {
36
+ await import('../../lib/headless/checkbox.js');
37
+ root.value?.addEventListener('dom:change', (event) => field.onInput(Boolean(event.detail.checked)));
38
+ });
39
+
40
+ watch(field.value, (value) => {
41
+ if (root.value && root.value.checked !== value) root.value.checked = value;
42
+ }, { immediate: true });
43
+ </script>
44
+
45
+ <template>
46
+ <label v-if="field.visible.value" class="flex w-full cursor-pointer items-start gap-3" @click="onLabelClick">
47
+ <dom-checkbox
48
+ ref="root"
49
+ :id="field.id.value"
50
+ :name="field.htmlName.value"
51
+ :checked="Boolean(field.value.value) || null"
52
+ :disabled="field.disabled.value || null"
53
+ :aria-label="!labelId ? label || undefined : undefined"
54
+ :aria-labelledby="labelId || undefined"
55
+ :aria-describedby="field.describedBy.value || undefined"
56
+ :aria-invalid="field.invalid.value || undefined"
57
+ :aria-errormessage="field.errorId.value || undefined"
58
+ :data-invalid="field.invalid.value ? '' : undefined"
59
+ class="mt-0.5"
60
+ @focus="field.onFocus"
61
+ @blur="field.onBlur"
62
+ />
63
+ <span v-if="chrome !== 'none'" class="grid gap-0.5">
64
+ <span v-if="label" :id="labelId" class="text-sm font-medium text-canvas-fg">{{ label }}</span>
65
+ <span v-if="description" :id="field.descriptionId.value" class="text-xs text-muted-fg">{{ description }}</span>
66
+ <span v-if="field.errors.value.length" :id="field.errorId.value" class="text-xs text-destructive">{{ field.errors.value[0].message || field.errors.value[0] }}</span>
67
+ </span>
68
+ </label>
69
+ </template>