@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,1718 @@
1
+ <script>
2
+ /**
3
+ * @typedef {object} MonthCalendarCell
4
+ * @property {Date} date Local Date instance for the cell.
5
+ * @property {string} value YYYY-MM-DD date key.
6
+ * @property {number} day Day of the month.
7
+ * @property {string} weekday Formatted weekday label.
8
+ * @property {string} label Full accessible date label.
9
+ * @property {boolean} currentMonth True when the date belongs to the rendered month.
10
+ * @property {boolean} previousMonth True when the date belongs to the previous month relative to the rendered month.
11
+ * @property {boolean} nextMonth True when the date belongs to the next month relative to the rendered month.
12
+ * @property {boolean} adjacentMonth True when the date belongs to either adjacent month outside the rendered month.
13
+ * @property {boolean} hidden True for adjacent dates when showAdjacentDays is false.
14
+ * @property {string} monthValue YYYY-MM month key for the rendered month.
15
+ * @property {number} row Zero-based week row in the grid.
16
+ * @property {number} column Zero-based weekday column in the grid.
17
+ * @property {boolean} today
18
+ * @property {boolean} past
19
+ * @property {boolean} future
20
+ * @property {boolean} muted
21
+ * @property {boolean} disabled
22
+ */
23
+
24
+ /**
25
+ * @typedef {object} MonthCalendarWeek
26
+ * @property {number} index
27
+ * @property {number} weekNumber ISO-8601 week number for the row.
28
+ * @property {string} label Accessible week number label.
29
+ * @property {Array<MonthCalendarCell>} days
30
+ * @property {string} [activeMonthValue] Month key used by continuous mode's sticky month header.
31
+ * @property {Array<string>} [monthBoundaryValues] Date keys where a visible continuous month segment starts.
32
+ * @property {Array<string>} [monthLabels] Month labels that start or become visible in a continuous row.
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} MonthCalendarMonth
37
+ * @property {Date} date Local Date instance for the first day of the rendered month.
38
+ * @property {string} value YYYY-MM month key.
39
+ * @property {string} id Element id used to label the month grid.
40
+ * @property {number} month Month number, 1-12.
41
+ * @property {number} year Four-digit year.
42
+ * @property {string} label Formatted month heading.
43
+ * @property {Array<MonthCalendarCell>} cells Flat list of visible day cells.
44
+ * @property {Array<MonthCalendarWeek>} weeks Day cells grouped into week rows.
45
+ */
46
+
47
+ /**
48
+ * @typedef {object} MonthCalendarDaySlotPayload
49
+ * @property {MonthCalendarCell} cell
50
+ * @property {MonthCalendarMonth} month
51
+ * @property {MonthCalendarDragState} drag
52
+ * @property {MonthCalendarDragAttrs} dragAttrs
53
+ */
54
+
55
+ /**
56
+ * @typedef {object} MonthCalendarDayClickPayload
57
+ * @property {MonthCalendarCell} cell
58
+ * @property {MouseEvent} event
59
+ */
60
+
61
+ /**
62
+ * @typedef {object} MonthCalendarDragState
63
+ * @property {boolean} active True while a calendar drag is in progress.
64
+ * @property {boolean} source True for the day where the drag started.
65
+ * @property {boolean} over True for the day currently previewed as the drop target.
66
+ * @property {MonthCalendarCell|null} sourceCell Cell where the drag started, when known.
67
+ * @property {MonthCalendarCell|null} overCell Cell currently previewed as the drop target, when known.
68
+ * @property {unknown} data App-owned data passed to dragAttrs.
69
+ */
70
+
71
+ /**
72
+ * @typedef {object} MonthCalendarDragAttrsOptions
73
+ * @property {string} [effectAllowed] Native DataTransfer effectAllowed value.
74
+ */
75
+
76
+ /**
77
+ * @typedef {(data?: unknown, options?: MonthCalendarDragAttrsOptions) => Record<string, unknown>} MonthCalendarDragAttrs
78
+ */
79
+
80
+ /**
81
+ * @typedef {object} MonthCalendarDayDragPayload
82
+ * @property {MonthCalendarCell} cell
83
+ * @property {MonthCalendarCell|null} sourceCell
84
+ * @property {MonthCalendarCell} targetCell
85
+ * @property {string} sourceValue
86
+ * @property {string} targetValue
87
+ * @property {unknown} data
88
+ * @property {DragEvent} event
89
+ * @property {DataTransfer|null} dataTransfer
90
+ * @property {Array<string>} types
91
+ * @property {(type: string) => string} getData
92
+ */
93
+
94
+ /**
95
+ * @typedef {(cell: MonthCalendarCell) => boolean} MonthCalendarDisabledDate
96
+ */
97
+
98
+ /**
99
+ * @typedef {string|Array<unknown>|Record<string, boolean>|((cell: MonthCalendarCell) => unknown)} MonthCalendarDayClass
100
+ */
101
+
102
+ /**
103
+ * @typedef {string|Record<string, string|number>|((cell: MonthCalendarCell) => string|Record<string, string|number>|null|undefined)} MonthCalendarDayStyle
104
+ */
105
+
106
+ export const monthCalendarProps = {
107
+ startDate: {
108
+ type: String,
109
+ default: '',
110
+ _edit: { group: 'Range', description: 'First date in the rendered range as YYYY-MM-DD. The first month starts on the week containing this date.' },
111
+ },
112
+ initialMonth: {
113
+ type: Number,
114
+ default: null,
115
+ _edit: { group: 'Range', description: 'Initial month, 1-12. Used when startDate is not set.' },
116
+ },
117
+ initialYear: {
118
+ type: Number,
119
+ default: null,
120
+ _edit: { group: 'Range', description: 'Initial year. Used when startDate is not set.' },
121
+ },
122
+ months: {
123
+ type: Number,
124
+ default: 3,
125
+ _edit: { group: 'Range', description: 'Number of consecutive months to render.' },
126
+ },
127
+ locale: {
128
+ type: String,
129
+ default: 'en-GB',
130
+ _edit: { description: 'Locale used for month, weekday, and date labels.' },
131
+ },
132
+ weekStartsOn: {
133
+ type: Number,
134
+ default: 1,
135
+ _edit: { options: [0, 1, 2, 3, 4, 5, 6], description: 'First day of week. 0 is Sunday, 1 is Monday.' },
136
+ },
137
+ weekdayFormat: {
138
+ type: String,
139
+ default: 'short',
140
+ validator: (value) => ['narrow', 'short', 'long'].includes(value),
141
+ _edit: { options: ['narrow', 'short', 'long'], description: 'Weekday label length.' },
142
+ },
143
+ showAdjacentDays: {
144
+ type: Boolean,
145
+ default: true,
146
+ _edit: { description: 'Show days from the previous and next months so the grid keeps its weekday alignment.' },
147
+ },
148
+ fixedWeeks: {
149
+ type: Boolean,
150
+ default: false,
151
+ _edit: { description: 'Render six weeks per month even when the month needs fewer rows.' },
152
+ },
153
+ continuous: {
154
+ type: Boolean,
155
+ default: false,
156
+ _edit: { description: 'Render one continuous week grid across the range instead of separated month blocks with repeated adjacent days.' },
157
+ },
158
+ stickyMonthHeaders: {
159
+ type: Boolean,
160
+ default: false,
161
+ _edit: { description: 'Keep separated month headings and the compact continuous month label pinned to the active scrollport.' },
162
+ },
163
+ containedContinuousScroll: {
164
+ type: Boolean,
165
+ default: true,
166
+ _edit: { description: 'Constrain continuous mode to its own scrollport. Disable when a parent layout already owns the vertical scroll.' },
167
+ },
168
+ hoverableDays: {
169
+ type: Boolean,
170
+ default: true,
171
+ _edit: { description: 'Apply the default hover background to clickable day cells.' },
172
+ },
173
+ showWeekNumbers: {
174
+ type: Boolean,
175
+ default: false,
176
+ _edit: { description: 'Render ISO-8601 week numbers at the start of each week row.' },
177
+ },
178
+ mutedBefore: {
179
+ type: String,
180
+ default: '',
181
+ _edit: { description: 'Dates before this YYYY-MM-DD value render muted.' },
182
+ },
183
+ mutedAfter: {
184
+ type: String,
185
+ default: '',
186
+ _edit: { description: 'Dates after this YYYY-MM-DD value render muted.' },
187
+ },
188
+ /**
189
+ * @type {MonthCalendarDayClass}
190
+ */
191
+ mutedDayClass: {
192
+ type: [String, Array, Object, Function],
193
+ default: '',
194
+ _edit: { group: 'Styling', description: 'Classes for visible muted days. Replaces the default muted day classes.' },
195
+ },
196
+ /**
197
+ * @type {MonthCalendarDayClass}
198
+ */
199
+ adjacentDayClass: {
200
+ type: [String, Array, Object, Function],
201
+ default: '',
202
+ _edit: { group: 'Styling', description: 'Classes for visible previous and next month days. Overrides mutedDayClass for adjacent days.' },
203
+ },
204
+ min: {
205
+ type: String,
206
+ default: '',
207
+ _edit: { group: 'Interaction', description: 'Minimum clickable date as YYYY-MM-DD.' },
208
+ },
209
+ max: {
210
+ type: String,
211
+ default: '',
212
+ _edit: { group: 'Interaction', description: 'Maximum clickable date as YYYY-MM-DD.' },
213
+ },
214
+ clickable: {
215
+ type: Boolean,
216
+ default: false,
217
+ _edit: { group: 'Interaction', description: 'Render days as keyboard-reachable buttons and emit day-click.' },
218
+ },
219
+ clickableAdjacentDays: {
220
+ type: Boolean,
221
+ default: false,
222
+ _edit: { group: 'Interaction', description: 'Allow day-click on visible days outside the displayed month.' },
223
+ },
224
+ dragAndDrop: {
225
+ type: Boolean,
226
+ default: false,
227
+ _edit: { group: 'Interaction', description: 'Expose dragAttrs in the day slot and emit day-drop when a draggable item is dropped onto a day.' },
228
+ },
229
+ dropEffect: {
230
+ type: String,
231
+ default: 'move',
232
+ validator: (value) => ['copy', 'move', 'link'].includes(value),
233
+ _edit: { group: 'Interaction', options: ['copy', 'move', 'link'], description: 'Native drop effect shown when a day can receive a dragged item.' },
234
+ },
235
+ /**
236
+ * @type {MonthCalendarDisabledDate}
237
+ */
238
+ disabledDate: {
239
+ type: Function,
240
+ default: null,
241
+ _edit: { group: 'Interaction', description: 'Function that receives a day cell and returns true when the day should not be clickable or droppable.' },
242
+ },
243
+ dayHeaders: {
244
+ type: Boolean,
245
+ default: true,
246
+ _edit: { description: 'Render weekday headers.' },
247
+ },
248
+ /**
249
+ * @type {MonthCalendarDayClass}
250
+ */
251
+ dayClass: {
252
+ type: [String, Array, Object, Function],
253
+ default: '',
254
+ _edit: { description: 'Extra classes, or a function that receives a day cell and returns classes for the day cell.' },
255
+ },
256
+ /**
257
+ * @type {MonthCalendarDayStyle}
258
+ */
259
+ dayStyle: {
260
+ type: [String, Object, Function],
261
+ default: '',
262
+ _edit: { description: 'Extra styles, or a function that receives a day cell and returns styles for the day cell.' },
263
+ },
264
+ };
265
+ </script>
266
+
267
+ <script setup>
268
+ import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, watch } from 'vue';
269
+
270
+ defineOptions({
271
+ __doc: {
272
+ name: 'Month calendar',
273
+ tag: '<DomMonthCalendar>',
274
+ description: 'A large scrolling month calendar that keeps weekday grids aligned, supports continuous date streams, and lets each day render custom UI.',
275
+ icon: 'M7 4v3M17 4v3M5 8h14M6 6h12v13H6V6Zm3 6h2M13 12h2M9 16h2M13 16h2',
276
+ events: [
277
+ { name: 'day-click', payload: '({ cell, event })', typeRef: 'MonthCalendarDayClickPayload', description: 'Fired when a clickable day is activated.' },
278
+ { name: 'day-drag-start', payload: '({ cell, sourceCell, sourceValue, data, event })', typeRef: 'MonthCalendarDayDragPayload', description: 'Fired when an item using dragAttrs starts dragging from a day.' },
279
+ { name: 'day-drag-enter', payload: '({ cell, sourceCell, targetCell, sourceValue, targetValue, data, event })', typeRef: 'MonthCalendarDayDragPayload', description: 'Fired when a dragged item previews a day as the drop target.' },
280
+ { name: 'day-drag-leave', payload: '({ cell, sourceCell, targetCell, sourceValue, targetValue, data, event })', typeRef: 'MonthCalendarDayDragPayload', description: 'Fired when a dragged item leaves a previewed day.' },
281
+ { name: 'day-drop', payload: '({ cell, sourceCell, targetCell, sourceValue, targetValue, data, event })', typeRef: 'MonthCalendarDayDragPayload', description: 'Fired when a draggable item is dropped onto a day. The calendar does not mutate records.' },
282
+ { name: 'day-drag-end', payload: '({ cell, sourceCell, targetCell, sourceValue, targetValue, data, event })', typeRef: 'MonthCalendarDayDragPayload', description: 'Fired when a drag started with dragAttrs ends.' },
283
+ ],
284
+ slots: [
285
+ { name: '(default)', payload: '{ cell, month, drag, dragAttrs }', typeRef: 'MonthCalendarDaySlotPayload', description: 'Custom content inside each visible calendar day. Use dragAttrs(data) on items that should be draggable.' },
286
+ { name: 'month-header', payload: '{ month }', description: 'Custom content rendered opposite separated month titles or above a continuous range.' },
287
+ ],
288
+ keyboard: [
289
+ { keys: 'Tab', action: 'Move through clickable days when clickable is enabled.' },
290
+ { keys: 'Enter / Space', action: 'Activate the focused day.' },
291
+ ],
292
+ },
293
+ });
294
+
295
+ const props = defineProps(monthCalendarProps);
296
+ const emit = defineEmits(['day-click', 'day-drag-start', 'day-drag-enter', 'day-drag-leave', 'day-drop', 'day-drag-end']);
297
+ const rootId = useId();
298
+ const calendarDragType = 'application/x-dom-month-calendar-drag';
299
+
300
+ const todayValue = formatDate(new Date());
301
+ const dragState = ref(createEmptyDragState());
302
+ const activeContinuousMonthValue = ref('');
303
+ const calendarRoot = ref(null);
304
+ const externalContinuousScrollHost = ref(null);
305
+ const resolvedWeekStartsOn = computed(() => clampWeekStartsOn(props.weekStartsOn));
306
+ const defaultMutedDayClass = 'bg-secondary/20 text-muted-fg/55';
307
+ const rangeStart = computed(() => parseDate(props.startDate));
308
+ const anchorDate = computed(() => rangeStart.value || initialDateFromProps() || new Date());
309
+ const calendarRange = computed(() => resolveCalendarRange());
310
+ const monthLabelFormatter = computed(() => new Intl.DateTimeFormat(props.locale, {
311
+ month: 'long',
312
+ year: 'numeric',
313
+ }));
314
+ const continuousMonthLabelFormatter = computed(() => new Intl.DateTimeFormat(props.locale, {
315
+ month: 'short',
316
+ year: '2-digit',
317
+ }));
318
+ const dateLabelFormatter = computed(() => new Intl.DateTimeFormat(props.locale, {
319
+ weekday: 'long',
320
+ day: 'numeric',
321
+ month: 'long',
322
+ year: 'numeric',
323
+ }));
324
+ const weekdayLabelFormatter = computed(() => new Intl.DateTimeFormat(props.locale, {
325
+ weekday: props.weekdayFormat,
326
+ }));
327
+ const weekdayLabels = computed(() => {
328
+ const sunday = new Date(2026, 0, 4);
329
+ return Array.from({ length: 7 }, (_, index) => {
330
+ const offset = (index + resolvedWeekStartsOn.value) % 7;
331
+ return weekdayLabelFormatter.value.format(addDays(sunday, offset));
332
+ });
333
+ });
334
+ const renderedMonths = computed(() => {
335
+ const range = calendarRange.value;
336
+ return Array.from({ length: range.count }, (_, index) => buildMonth(range.firstMonth, index));
337
+ });
338
+ const continuousWeeks = computed(() => buildContinuousWeeks(calendarRange.value));
339
+ const continuousMonths = computed(() => collectContinuousMonths(continuousWeeks.value));
340
+ const firstContinuousMonth = computed(() => continuousMonths.value.values().next().value || null);
341
+ const firstContinuousMonthLabel = computed(() => firstContinuousMonth.value ? continuousMonthLabelForDate(firstContinuousMonth.value.date) : 'Month');
342
+ const continuousHeaderMonthLabel = computed(() => {
343
+ const month = continuousMonths.value.get(activeContinuousMonthValue.value) || firstContinuousMonth.value;
344
+ return month ? continuousMonthLabelForDate(month.date) : firstContinuousMonthLabel.value;
345
+ });
346
+ const calendarSections = computed(() => props.continuous ? [buildContinuousSection()] : renderedMonths.value.map((month) => ({
347
+ key: month.value,
348
+ id: month.id,
349
+ label: month.label,
350
+ month,
351
+ weeks: month.weeks,
352
+ continuous: false,
353
+ })));
354
+ const calendarCells = computed(() => props.continuous
355
+ ? continuousWeeks.value.flatMap((week) => week.days)
356
+ : renderedMonths.value.flatMap((month) => month.cells));
357
+ const hasRowRail = computed(() => props.showWeekNumbers || props.continuous);
358
+ const rowRailHeaderLabel = computed(() => {
359
+ if (!props.continuous) return 'Week';
360
+ return continuousHeaderMonthLabel.value;
361
+ });
362
+ /** Seven-column template activated by the selected container breakpoint. */
363
+ const wideWeekGridColumns = computed(() => {
364
+ if (props.continuous) return '5.5rem repeat(7, minmax(0, 1fr))';
365
+ return hasRowRail.value ? '3.5rem repeat(7, minmax(0, 1fr))' : 'repeat(7, minmax(0, 1fr))';
366
+ });
367
+
368
+ onMounted(() => {
369
+ nextTick(configureExternalContinuousScrollTracking);
370
+ });
371
+
372
+ onBeforeUnmount(() => {
373
+ removeExternalContinuousScrollTracking();
374
+ });
375
+
376
+ watch(
377
+ () => [props.continuous, props.containedContinuousScroll, props.startDate, props.months, props.weekStartsOn],
378
+ () => {
379
+ nextTick(configureExternalContinuousScrollTracking);
380
+ },
381
+ { flush: 'post' },
382
+ );
383
+
384
+ /**
385
+ * Resolves the month and date range that all rendered calendar modes share.
386
+ *
387
+ * @returns {{ count: number, firstMonth: Date, firstVisibleDate: Date, lastMonth: Date, lastDate: Date }} Normalized range metadata.
388
+ */
389
+ function resolveCalendarRange() {
390
+ const count = clampMonthCount(props.months);
391
+ const firstMonth = new Date(anchorDate.value.getFullYear(), anchorDate.value.getMonth(), 1);
392
+ const firstVisibleDate = rangeStart.value ? maxDate(firstMonth, rangeStart.value) : firstMonth;
393
+ const lastMonth = new Date(firstMonth.getFullYear(), firstMonth.getMonth() + count - 1, 1);
394
+ const lastDate = new Date(lastMonth.getFullYear(), lastMonth.getMonth() + 1, 0);
395
+ return {
396
+ count,
397
+ firstMonth,
398
+ firstVisibleDate,
399
+ lastMonth,
400
+ lastDate,
401
+ };
402
+ }
403
+
404
+ /**
405
+ * Creates a month record used for labels, ARIA relationships, slots, and docs payloads.
406
+ *
407
+ * @param {Date} monthStart First day of the month represented by the record.
408
+ * @returns {MonthCalendarMonth} Month metadata with mutable cells and weeks arrays.
409
+ */
410
+ function createMonthRecord(monthStart) {
411
+ const value = monthValue(monthStart);
412
+ return {
413
+ date: monthStart,
414
+ value,
415
+ id: `${rootId}-${value}`,
416
+ month: monthStart.getMonth() + 1,
417
+ year: monthStart.getFullYear(),
418
+ label: monthLabelFormatter.value.format(monthStart),
419
+ cells: [],
420
+ weeks: [],
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Builds one separated month grid for the default stacked calendar layout.
426
+ *
427
+ * @param {Date} firstMonth First month in the rendered range.
428
+ * @param {number} index Zero-based month offset from the first rendered month.
429
+ * @returns {MonthCalendarMonth} Month record with visible cells and week rows.
430
+ */
431
+ function buildMonth(firstMonth, index) {
432
+ const monthDate = new Date(firstMonth.getFullYear(), firstMonth.getMonth() + index, 1);
433
+ const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
434
+ const monthEnd = new Date(monthDate.getFullYear(), monthDate.getMonth() + 1, 0);
435
+ const firstVisibleDate = index === 0 && rangeStart.value
436
+ ? maxDate(monthStart, rangeStart.value)
437
+ : monthStart;
438
+ const gridStart = startOfWeek(firstVisibleDate, resolvedWeekStartsOn.value);
439
+ const gridEnd = props.fixedWeeks
440
+ ? addDays(gridStart, 41)
441
+ : endOfWeek(monthEnd, resolvedWeekStartsOn.value);
442
+ const month = createMonthRecord(monthStart);
443
+ const cells = [];
444
+ let cursor = gridStart;
445
+ let indexInGrid = 0;
446
+
447
+ while (cursor <= gridEnd) {
448
+ const date = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate());
449
+ const cell = buildCell(date, month, indexInGrid);
450
+ cells.push(cell);
451
+ cursor = addDays(cursor, 1);
452
+ indexInGrid += 1;
453
+ }
454
+
455
+ month.cells = cells;
456
+ month.weeks = chunkWeeks(cells);
457
+ return month;
458
+ }
459
+
460
+ /**
461
+ * Builds one day cell and applies range, muted, disabled, and adjacent-month state.
462
+ *
463
+ * @param {Date} date Date represented by the cell.
464
+ * @param {MonthCalendarMonth} month Month context for the cell.
465
+ * @param {number} indexInGrid Zero-based cell index inside the rendered grid.
466
+ * @param {{ continuous?: boolean, rangeStart?: Date, rangeEnd?: Date }} [options] Layout-specific cell options.
467
+ * @returns {MonthCalendarCell} Calendar cell payload passed to rendering and slots.
468
+ */
469
+ function buildCell(date, month, indexInGrid, options = {}) {
470
+ const value = formatDate(date);
471
+ const continuous = Boolean(options.continuous);
472
+ const previousMonth = continuous ? date < options.rangeStart : monthDistance(date, month.date) < 0;
473
+ const nextMonth = continuous ? date > options.rangeEnd : monthDistance(date, month.date) > 0;
474
+ const adjacentMonth = previousMonth || nextMonth;
475
+ const currentMonth = !adjacentMonth;
476
+ const hidden = continuous ? adjacentMonth : !currentMonth && !props.showAdjacentDays;
477
+ const cell = {
478
+ date,
479
+ value,
480
+ day: date.getDate(),
481
+ weekday: weekdayLabelFormatter.value.format(date),
482
+ label: dateLabelFormatter.value.format(date),
483
+ currentMonth,
484
+ previousMonth,
485
+ nextMonth,
486
+ adjacentMonth,
487
+ hidden,
488
+ monthValue: month.value,
489
+ row: Math.floor(indexInGrid / 7),
490
+ column: indexInGrid % 7,
491
+ today: value === todayValue,
492
+ past: value < todayValue,
493
+ future: value > todayValue,
494
+ muted: false,
495
+ disabled: false,
496
+ };
497
+ cell.muted = hidden || (!continuous && adjacentMonth) || beforeBoundary(value, props.mutedBefore) || afterBoundary(value, props.mutedAfter);
498
+ cell.disabled = hidden
499
+ || outsideRange(value)
500
+ || (!continuous && !currentMonth && !props.clickableAdjacentDays)
501
+ || Boolean(props.disabledDate?.(cell));
502
+ return cell;
503
+ }
504
+
505
+ /**
506
+ * Builds a single week stream for continuous mode without duplicating month-edge dates.
507
+ *
508
+ * @param {{ firstVisibleDate: Date, lastDate: Date }} range Normalized calendar range.
509
+ * @returns {Array<MonthCalendarWeek>} Continuous weeks with subtle month labels attached.
510
+ */
511
+ function buildContinuousWeeks(range) {
512
+ const cells = [];
513
+ const gridStart = startOfWeek(range.firstVisibleDate, resolvedWeekStartsOn.value);
514
+ const gridEnd = endOfWeek(range.lastDate, resolvedWeekStartsOn.value);
515
+ let cursor = gridStart;
516
+ let indexInGrid = 0;
517
+
518
+ while (cursor <= gridEnd) {
519
+ const date = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate());
520
+ const month = createMonthRecord(firstDayOfMonth(date));
521
+ cells.push(buildCell(date, month, indexInGrid, {
522
+ continuous: true,
523
+ rangeStart: range.firstVisibleDate,
524
+ rangeEnd: range.lastDate,
525
+ }));
526
+ cursor = addDays(cursor, 1);
527
+ indexInGrid += 1;
528
+ }
529
+
530
+ return chunkWeeks(cells).map((week) => {
531
+ const boundaryCells = continuousBoundaryCellsForWeek(week, range);
532
+ return {
533
+ ...week,
534
+ activeMonthValue: continuousActiveMonthValueForWeek(week, range),
535
+ monthBoundaryValues: boundaryCells.map((cell) => cell.value),
536
+ monthLabels: continuousMonthLabelsForBoundaryCells(boundaryCells),
537
+ };
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Collects month records from the continuous week stream for day-slot payloads.
543
+ *
544
+ * @param {Array<MonthCalendarWeek>} weeks Continuous weeks to group by cell month.
545
+ * @returns {Map<string, MonthCalendarMonth>} Month records keyed by YYYY-MM.
546
+ */
547
+ function collectContinuousMonths(weeks) {
548
+ const months = new Map();
549
+
550
+ for (const week of weeks) {
551
+ for (const cell of week.days) {
552
+ if (cell.hidden) continue;
553
+ if (!months.has(cell.monthValue)) {
554
+ months.set(cell.monthValue, createMonthRecord(firstDayOfMonth(cell.date)));
555
+ }
556
+ months.get(cell.monthValue).cells.push(cell);
557
+ }
558
+ }
559
+
560
+ for (const week of weeks) {
561
+ for (const month of months.values()) {
562
+ const days = week.days.filter((cell) => !cell.hidden && cell.monthValue === month.value);
563
+ if (days.length) {
564
+ month.weeks.push({
565
+ ...week,
566
+ days,
567
+ });
568
+ }
569
+ }
570
+ }
571
+
572
+ return months;
573
+ }
574
+
575
+ /**
576
+ * Builds the synthetic section used by continuous mode's single-grid template path.
577
+ *
578
+ * @returns {{ key: string, id: string, label: string, month: MonthCalendarMonth, weeks: Array<MonthCalendarWeek>, continuous: boolean }} Render section metadata.
579
+ */
580
+ function buildContinuousSection() {
581
+ const firstMonth = continuousMonths.value.values().next().value || createMonthRecord(calendarRange.value.firstMonth);
582
+ return {
583
+ key: 'continuous',
584
+ id: `${rootId}-continuous`,
585
+ label: 'Continuous calendar',
586
+ month: firstMonth,
587
+ weeks: continuousWeeks.value,
588
+ continuous: true,
589
+ };
590
+ }
591
+
592
+ /**
593
+ * Lists cells that begin visible continuous month segments in a week row.
594
+ *
595
+ * @param {MonthCalendarWeek} week Week row being rendered.
596
+ * @param {{ firstVisibleDate: Date }} range Normalized calendar range.
597
+ * @returns {Array<MonthCalendarCell>} Boundary cells that start a visible month segment.
598
+ */
599
+ function continuousBoundaryCellsForWeek(week, range) {
600
+ const firstVisibleValue = formatDate(range.firstVisibleDate);
601
+ return week.days
602
+ .filter((cell) => !cell.hidden && (cell.day === 1 || cell.value === firstVisibleValue));
603
+ }
604
+
605
+ /**
606
+ * Formats the visible continuous-mode month label with a compact year.
607
+ *
608
+ * @param {Date} date Date inside the month being labelled.
609
+ * @returns {string} Locale-aware month label such as Jun 26.
610
+ */
611
+ function continuousMonthLabelForDate(date) {
612
+ return continuousMonthLabelFormatter.value.format(firstDayOfMonth(date));
613
+ }
614
+
615
+ /**
616
+ * Lists month labels that should appear in the continuous mode row rail.
617
+ *
618
+ * @param {Array<MonthCalendarCell>} boundaryCells Cells that begin visible month segments.
619
+ * @returns {Array<string>} Month labels that begin or become visible in the row.
620
+ */
621
+ function continuousMonthLabelsForBoundaryCells(boundaryCells) {
622
+ return boundaryCells.map((cell) => continuousMonthLabelForDate(cell.date));
623
+ }
624
+
625
+ /**
626
+ * Chooses the month that should label the sticky row rail while a continuous row is at the top.
627
+ *
628
+ * @param {MonthCalendarWeek} week Week row being inspected.
629
+ * @param {{ firstVisibleDate: Date }} range Normalized calendar range.
630
+ * @returns {string} Month key that should represent the row in the sticky header.
631
+ */
632
+ function continuousActiveMonthValueForWeek(week, range) {
633
+ const firstVisibleValue = formatDate(range.firstVisibleDate);
634
+ const firstVisibleCell = week.days.find((cell) => !cell.hidden && cell.value === firstVisibleValue);
635
+ const monthStartCell = week.days.find((cell) => !cell.hidden && cell.day === 1);
636
+ const fallbackCell = week.days.find((cell) => !cell.hidden);
637
+ return (firstVisibleCell || monthStartCell || fallbackCell)?.monthValue || '';
638
+ }
639
+
640
+ /**
641
+ * Resolves the month payload that should be exposed for a rendered day cell.
642
+ *
643
+ * @param {MonthCalendarCell} cell Day cell being rendered.
644
+ * @param {MonthCalendarMonth} fallbackMonth Section month used by the default layout.
645
+ * @returns {MonthCalendarMonth} Month context for the day slot.
646
+ */
647
+ function monthForCell(cell, fallbackMonth) {
648
+ if (!props.continuous) return fallbackMonth;
649
+ return continuousMonths.value.get(cell.monthValue) || fallbackMonth;
650
+ }
651
+
652
+ /**
653
+ * Creates a local Date for the first day of the month containing the provided date.
654
+ *
655
+ * @param {Date} date Date whose month should be normalized.
656
+ * @returns {Date} First day of the date's month.
657
+ */
658
+ function firstDayOfMonth(date) {
659
+ return new Date(date.getFullYear(), date.getMonth(), 1);
660
+ }
661
+
662
+ /**
663
+ * Resolves the accessible label for the row header rail.
664
+ *
665
+ * @param {MonthCalendarWeek} week Week row being rendered.
666
+ * @returns {string} ARIA label combining month and week information where available.
667
+ */
668
+ function rowHeaderLabelFor(week) {
669
+ const monthLabel = week.monthLabels?.join(', ') || '';
670
+ if (props.continuous && monthLabel && props.showWeekNumbers) return `${monthLabel}, ${week.label}`;
671
+ if (props.continuous && monthLabel) return monthLabel;
672
+ return week.label;
673
+ }
674
+
675
+ /**
676
+ * Builds the initial month date from the separate initialYear and initialMonth props.
677
+ *
678
+ * @returns {Date|null} First day of the configured month, or null when the props are incomplete.
679
+ */
680
+ function initialDateFromProps() {
681
+ const year = Number(props.initialYear);
682
+ const month = Number(props.initialMonth);
683
+ if (!Number.isFinite(year) || !Number.isFinite(month)) return null;
684
+ const clampedMonth = Math.min(12, Math.max(1, Math.round(month)));
685
+ return new Date(year, clampedMonth - 1, 1);
686
+ }
687
+
688
+ /**
689
+ * Parses a YYYY-MM-DD string into a local Date while rejecting invalid calendar dates.
690
+ *
691
+ * @param {string} value Date string to parse.
692
+ * @returns {Date|null} Parsed local date, or null when invalid.
693
+ */
694
+ function parseDate(value) {
695
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value || '')) return null;
696
+ const [year, month, day] = value.split('-').map(Number);
697
+ const date = new Date(year, month - 1, day);
698
+ if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null;
699
+ return date;
700
+ }
701
+
702
+ /**
703
+ * Left-pads a numeric date part to two digits.
704
+ *
705
+ * @param {number} value Numeric date part.
706
+ * @returns {string} Two-digit string.
707
+ */
708
+ function pad(value) {
709
+ return String(value).padStart(2, '0');
710
+ }
711
+
712
+ /**
713
+ * Formats a local Date as a YYYY-MM-DD date key.
714
+ *
715
+ * @param {Date} date Date to format.
716
+ * @returns {string} Date key used by slots, events, and data attributes.
717
+ */
718
+ function formatDate(date) {
719
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
720
+ }
721
+
722
+ /**
723
+ * Formats a local Date as a YYYY-MM month key.
724
+ *
725
+ * @param {Date} date Date whose month should be formatted.
726
+ * @returns {string} Month key used for grouping cells.
727
+ */
728
+ function monthValue(date) {
729
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}`;
730
+ }
731
+
732
+ /**
733
+ * Returns the number of calendar months between a day and its rendered month.
734
+ *
735
+ * @param {Date} date Date represented by the rendered cell.
736
+ * @param {Date} monthDate First day of the rendered month.
737
+ * @returns {number} Negative for previous-month cells, positive for next-month cells, and zero for current-month cells.
738
+ */
739
+ function monthDistance(date, monthDate) {
740
+ return ((date.getFullYear() - monthDate.getFullYear()) * 12) + date.getMonth() - monthDate.getMonth();
741
+ }
742
+
743
+ /**
744
+ * Normalizes the first day of week into the supported 0-6 range.
745
+ *
746
+ * @param {number} value Candidate first day of week.
747
+ * @returns {number} Clamped weekday index where 0 is Sunday.
748
+ */
749
+ function clampWeekStartsOn(value) {
750
+ const day = Number(value);
751
+ if (!Number.isFinite(day)) return 1;
752
+ return Math.min(6, Math.max(0, Math.round(day)));
753
+ }
754
+
755
+ /**
756
+ * Normalizes the requested month count to a bounded positive integer.
757
+ *
758
+ * @param {number} value Candidate month count.
759
+ * @returns {number} Month count between 1 and 120.
760
+ */
761
+ function clampMonthCount(value) {
762
+ const count = Number(value);
763
+ if (!Number.isFinite(count)) return 1;
764
+ return Math.min(120, Math.max(1, Math.round(count)));
765
+ }
766
+
767
+ /**
768
+ * Adds whole calendar days using local-date arithmetic.
769
+ *
770
+ * @param {Date} date Starting date.
771
+ * @param {number} days Number of days to add.
772
+ * @returns {Date} New local Date offset by the requested days.
773
+ */
774
+ function addDays(date, days) {
775
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
776
+ }
777
+
778
+ /**
779
+ * Returns the later of two local Date values.
780
+ *
781
+ * @param {Date} a First date.
782
+ * @param {Date} b Second date.
783
+ * @returns {Date} Later date.
784
+ */
785
+ function maxDate(a, b) {
786
+ return a > b ? a : b;
787
+ }
788
+
789
+ /**
790
+ * Finds the first date in the week containing a date.
791
+ *
792
+ * @param {Date} date Date inside the target week.
793
+ * @param {number} weekStartsOn First day of week where 0 is Sunday.
794
+ * @returns {Date} Local Date at the start of the week.
795
+ */
796
+ function startOfWeek(date, weekStartsOn) {
797
+ const offset = (date.getDay() - weekStartsOn + 7) % 7;
798
+ return addDays(date, -offset);
799
+ }
800
+
801
+ /**
802
+ * Finds the last date in the week containing a date.
803
+ *
804
+ * @param {Date} date Date inside the target week.
805
+ * @param {number} weekStartsOn First day of week where 0 is Sunday.
806
+ * @returns {Date} Local Date at the end of the week.
807
+ */
808
+ function endOfWeek(date, weekStartsOn) {
809
+ return addDays(startOfWeek(date, weekStartsOn), 6);
810
+ }
811
+
812
+ /**
813
+ * Splits a flat cell list into seven-day week rows.
814
+ *
815
+ * @param {Array<MonthCalendarCell>} cells Calendar cells ordered by date.
816
+ * @returns {Array<MonthCalendarWeek>} Week rows with ISO week labels.
817
+ */
818
+ function chunkWeeks(cells) {
819
+ const weeks = [];
820
+ for (let index = 0; index < cells.length; index += 7) {
821
+ const days = cells.slice(index, index + 7);
822
+ const weekNumber = isoWeekNumberForWeek(days);
823
+ weeks.push({
824
+ index: weeks.length,
825
+ weekNumber,
826
+ label: `Week ${weekNumber}`,
827
+ days,
828
+ });
829
+ }
830
+ return weeks;
831
+ }
832
+
833
+ /**
834
+ * Resolves the ISO week number represented by a row of day cells.
835
+ *
836
+ * @param {Array<MonthCalendarCell>} days Day cells in a week row.
837
+ * @returns {number} ISO-8601 week number.
838
+ */
839
+ function isoWeekNumberForWeek(days) {
840
+ const thursday = days.find((cell) => cell.date.getDay() === 4)?.date || days[0]?.date || new Date();
841
+ return isoWeekNumber(thursday);
842
+ }
843
+
844
+ /**
845
+ * Calculates an ISO-8601 week number for a local date.
846
+ *
847
+ * @param {Date} date Date whose ISO week should be calculated.
848
+ * @returns {number} ISO-8601 week number.
849
+ */
850
+ function isoWeekNumber(date) {
851
+ const target = new Date(date.getFullYear(), date.getMonth(), date.getDate());
852
+ const day = target.getDay() || 7;
853
+ target.setDate(target.getDate() + 4 - day);
854
+ const yearStart = new Date(target.getFullYear(), 0, 1);
855
+ return Math.ceil((((target - yearStart) / 86400000) + 1) / 7);
856
+ }
857
+
858
+ /**
859
+ * Checks whether a string is shaped like a YYYY-MM-DD date boundary.
860
+ *
861
+ * @param {string} value Candidate boundary value.
862
+ * @returns {boolean} True when the value can be compared as a date key.
863
+ */
864
+ function isDateBoundary(value) {
865
+ return /^\d{4}-\d{2}-\d{2}$/.test(value || '');
866
+ }
867
+
868
+ /**
869
+ * Checks whether a date key falls before a configured boundary.
870
+ *
871
+ * @param {string} value Date key to compare.
872
+ * @param {string} boundary Boundary date key.
873
+ * @returns {boolean} True when the boundary is valid and value is earlier.
874
+ */
875
+ function beforeBoundary(value, boundary) {
876
+ return isDateBoundary(boundary) && value < boundary;
877
+ }
878
+
879
+ /**
880
+ * Checks whether a date key falls after a configured boundary.
881
+ *
882
+ * @param {string} value Date key to compare.
883
+ * @param {string} boundary Boundary date key.
884
+ * @returns {boolean} True when the boundary is valid and value is later.
885
+ */
886
+ function afterBoundary(value, boundary) {
887
+ return isDateBoundary(boundary) && value > boundary;
888
+ }
889
+
890
+ /**
891
+ * Checks whether a date key is outside the interactive min and max range.
892
+ *
893
+ * @param {string} value Date key to test.
894
+ * @returns {boolean} True when the day is before min or after max.
895
+ */
896
+ function outsideRange(value) {
897
+ return beforeBoundary(value, props.min) || afterBoundary(value, props.max);
898
+ }
899
+
900
+ /**
901
+ * Creates the inert drag state used before, after, or outside a drag gesture.
902
+ *
903
+ * @returns {MonthCalendarDragState} Empty drag state.
904
+ */
905
+ function createEmptyDragState() {
906
+ return {
907
+ active: false,
908
+ sourceCell: null,
909
+ overCell: null,
910
+ data: null,
911
+ };
912
+ }
913
+
914
+ /**
915
+ * Builds a slot helper that returns drag attributes for app-rendered day content.
916
+ *
917
+ * @param {MonthCalendarCell} cell Cell that owns the draggable content.
918
+ * @returns {MonthCalendarDragAttrs} Function exposed through the day slot.
919
+ */
920
+ function dragAttrsFor(cell) {
921
+ return (data = null, options = {}) => {
922
+ if (!canStartDrag(cell)) {
923
+ return {
924
+ draggable: false,
925
+ };
926
+ }
927
+
928
+ return {
929
+ draggable: true,
930
+ 'data-dom-month-calendar-drag': '',
931
+ onDragstart: (event) => onItemDragStart(cell, data, options, event),
932
+ onDragend: (event) => onItemDragEnd(cell, data, event),
933
+ };
934
+ };
935
+ }
936
+
937
+ /**
938
+ * Resolves drag state booleans for a specific day cell.
939
+ *
940
+ * @param {MonthCalendarCell} cell Cell being rendered.
941
+ * @returns {MonthCalendarDragState & { source: boolean, over: boolean }} Drag state scoped to the cell.
942
+ */
943
+ function dragStateFor(cell) {
944
+ const state = dragState.value;
945
+ return {
946
+ active: state.active,
947
+ source: isDragSource(cell),
948
+ over: isDropTarget(cell),
949
+ sourceCell: state.sourceCell,
950
+ overCell: state.overCell,
951
+ data: state.data,
952
+ };
953
+ }
954
+
955
+ /**
956
+ * Checks whether a drag can begin from a cell.
957
+ *
958
+ * @param {MonthCalendarCell} cell Candidate drag source cell.
959
+ * @returns {boolean} True when drag-and-drop is enabled and the cell is interactive.
960
+ */
961
+ function canStartDrag(cell) {
962
+ return props.dragAndDrop && !cell.hidden && !cell.disabled;
963
+ }
964
+
965
+ /**
966
+ * Checks whether a cell can receive the current dragged item.
967
+ *
968
+ * @param {MonthCalendarCell} cell Candidate drop target cell.
969
+ * @returns {boolean} True when drag-and-drop is enabled and the cell is interactive.
970
+ */
971
+ function canDropOnCell(cell) {
972
+ return props.dragAndDrop && !cell.hidden && !cell.disabled;
973
+ }
974
+
975
+ /**
976
+ * Checks whether a cell is the active drag source.
977
+ *
978
+ * @param {MonthCalendarCell} cell Cell being rendered.
979
+ * @returns {boolean} True when the active drag began from this date.
980
+ */
981
+ function isDragSource(cell) {
982
+ return dragState.value.sourceCell?.value === cell.value;
983
+ }
984
+
985
+ /**
986
+ * Checks whether a cell is the current drop preview target.
987
+ *
988
+ * @param {MonthCalendarCell} cell Cell being rendered.
989
+ * @returns {boolean} True when the active drag is over this date.
990
+ */
991
+ function isDropTarget(cell) {
992
+ return dragState.value.overCell?.value === cell.value;
993
+ }
994
+
995
+ /**
996
+ * Handles native dragstart for an app-rendered item inside a day cell.
997
+ *
998
+ * @param {MonthCalendarCell} cell Source day cell.
999
+ * @param {unknown} data App-owned data passed through dragAttrs.
1000
+ * @param {MonthCalendarDragAttrsOptions} options Native transfer options.
1001
+ * @param {DragEvent} event Native drag event.
1002
+ * @returns {void}
1003
+ */
1004
+ function onItemDragStart(cell, data, options, event) {
1005
+ if (!canStartDrag(cell)) {
1006
+ event.preventDefault();
1007
+ return;
1008
+ }
1009
+
1010
+ dragState.value = {
1011
+ active: true,
1012
+ sourceCell: cell,
1013
+ overCell: null,
1014
+ data,
1015
+ };
1016
+
1017
+ if (event.dataTransfer) {
1018
+ event.dataTransfer.effectAllowed = options.effectAllowed || 'move';
1019
+ safeSetData(event, calendarDragType, serializeDragData({
1020
+ sourceValue: cell.value,
1021
+ sourceMonth: cell.monthValue,
1022
+ data,
1023
+ }));
1024
+ safeSetData(event, 'text/plain', plainTextForDrag(data, cell));
1025
+ }
1026
+
1027
+ emit('day-drag-start', dragPayload(cell, event));
1028
+ }
1029
+
1030
+ /**
1031
+ * Handles native dragend and clears calendar drag state.
1032
+ *
1033
+ * @param {MonthCalendarCell} cell Source day cell.
1034
+ * @param {unknown} data App-owned data passed through dragAttrs.
1035
+ * @param {DragEvent} event Native drag event.
1036
+ * @returns {void}
1037
+ */
1038
+ function onItemDragEnd(cell, data, event) {
1039
+ emit('day-drag-end', {
1040
+ ...dragPayload(cell, event),
1041
+ data,
1042
+ });
1043
+ dragState.value = createEmptyDragState();
1044
+ }
1045
+
1046
+ /**
1047
+ * Handles dragenter for a day cell and emits a target preview event.
1048
+ *
1049
+ * @param {MonthCalendarCell} cell Candidate drop target cell.
1050
+ * @param {DragEvent} event Native drag event.
1051
+ * @returns {void}
1052
+ */
1053
+ function onDayDragEnter(cell, event) {
1054
+ if (!canDropOnCell(cell)) return;
1055
+ event.preventDefault();
1056
+ applyDropEffect(event);
1057
+ if (!isDropTarget(cell)) {
1058
+ dragState.value = {
1059
+ ...dragState.value,
1060
+ active: true,
1061
+ overCell: cell,
1062
+ };
1063
+ emit('day-drag-enter', dragPayload(cell, event));
1064
+ }
1065
+ }
1066
+
1067
+ /**
1068
+ * Handles dragover for a day cell and keeps the drop effect current.
1069
+ *
1070
+ * @param {MonthCalendarCell} cell Candidate drop target cell.
1071
+ * @param {DragEvent} event Native drag event.
1072
+ * @returns {void}
1073
+ */
1074
+ function onDayDragOver(cell, event) {
1075
+ if (!canDropOnCell(cell)) return;
1076
+ event.preventDefault();
1077
+ applyDropEffect(event);
1078
+ if (!isDropTarget(cell)) {
1079
+ dragState.value = {
1080
+ ...dragState.value,
1081
+ active: true,
1082
+ overCell: cell,
1083
+ };
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Handles dragleave for a day cell without clearing state for child transitions.
1089
+ *
1090
+ * @param {MonthCalendarCell} cell Cell being left.
1091
+ * @param {DragEvent} event Native drag event.
1092
+ * @returns {void}
1093
+ */
1094
+ function onDayDragLeave(cell, event) {
1095
+ if (!isDropTarget(cell) || event.currentTarget.contains(event.relatedTarget)) return;
1096
+ emit('day-drag-leave', dragPayload(cell, event));
1097
+ const nextState = {
1098
+ ...dragState.value,
1099
+ overCell: null,
1100
+ };
1101
+ dragState.value = nextState.sourceCell ? nextState : createEmptyDragState();
1102
+ }
1103
+
1104
+ /**
1105
+ * Handles a completed drop onto a day cell.
1106
+ *
1107
+ * @param {MonthCalendarCell} cell Drop target cell.
1108
+ * @param {DragEvent} event Native drag event.
1109
+ * @returns {void}
1110
+ */
1111
+ function onDayDrop(cell, event) {
1112
+ if (!canDropOnCell(cell)) return;
1113
+ event.preventDefault();
1114
+ applyDropEffect(event);
1115
+ emit('day-drop', dragPayload(cell, event));
1116
+ const nextState = {
1117
+ ...dragState.value,
1118
+ overCell: null,
1119
+ };
1120
+ dragState.value = nextState.sourceCell ? nextState : createEmptyDragState();
1121
+ }
1122
+
1123
+ /**
1124
+ * Applies the configured native drop effect to the active drag event.
1125
+ *
1126
+ * @param {DragEvent} event Native drag event.
1127
+ * @returns {void}
1128
+ */
1129
+ function applyDropEffect(event) {
1130
+ if (event.dataTransfer) event.dataTransfer.dropEffect = props.dropEffect;
1131
+ }
1132
+
1133
+ /**
1134
+ * Chooses a plain-text fallback payload for browser drag data stores.
1135
+ *
1136
+ * @param {unknown} data App-owned drag data.
1137
+ * @param {MonthCalendarCell} cell Source day cell.
1138
+ * @returns {string} Text payload for native transfer compatibility.
1139
+ */
1140
+ function plainTextForDrag(data, cell) {
1141
+ if (typeof data === 'string') return data;
1142
+ if (data && typeof data === 'object' && 'id' in data) return String(data.id);
1143
+ return cell.value;
1144
+ }
1145
+
1146
+ /**
1147
+ * Serializes app drag data while falling back to date-only metadata when needed.
1148
+ *
1149
+ * @param {{ sourceValue: string, sourceMonth: string, data: unknown }} payload Drag payload to serialize.
1150
+ * @returns {string} JSON string safe for a DataTransfer store.
1151
+ */
1152
+ function serializeDragData(payload) {
1153
+ try {
1154
+ return JSON.stringify(payload);
1155
+ } catch {
1156
+ return JSON.stringify({
1157
+ sourceValue: payload.sourceValue,
1158
+ sourceMonth: payload.sourceMonth,
1159
+ });
1160
+ }
1161
+ }
1162
+
1163
+ /**
1164
+ * Builds the payload emitted for calendar drag events.
1165
+ *
1166
+ * @param {MonthCalendarCell} cell Target or source cell associated with the event.
1167
+ * @param {DragEvent} event Native drag event.
1168
+ * @returns {MonthCalendarDayDragPayload} Public drag event payload.
1169
+ */
1170
+ function dragPayload(cell, event) {
1171
+ const transferPayload = dragTransferPayload(event);
1172
+ const sourceValue = dragState.value.sourceCell?.value || transferPayload.sourceValue || transferPayload.sourceDate || '';
1173
+ const data = dragState.value.data ?? transferPayload.data ?? null;
1174
+ return {
1175
+ cell,
1176
+ targetCell: cell,
1177
+ sourceCell: dragState.value.sourceCell || cellForValue(sourceValue),
1178
+ sourceValue,
1179
+ targetValue: cell.value,
1180
+ data,
1181
+ event,
1182
+ dataTransfer: event.dataTransfer,
1183
+ types: dataTransferTypes(event),
1184
+ getData: (type) => safeGetData(event, type),
1185
+ };
1186
+ }
1187
+
1188
+ /**
1189
+ * Reads the DOM Studio calendar payload from a native drag transfer.
1190
+ *
1191
+ * @param {DragEvent} event Native drag event.
1192
+ * @returns {Record<string, unknown>} Parsed drag payload, or an empty object.
1193
+ */
1194
+ function dragTransferPayload(event) {
1195
+ const payload = safeGetData(event, calendarDragType);
1196
+ if (!payload) return {};
1197
+ try {
1198
+ const parsed = JSON.parse(payload);
1199
+ return parsed && typeof parsed === 'object' ? parsed : { data: parsed };
1200
+ } catch {
1201
+ return {};
1202
+ }
1203
+ }
1204
+
1205
+ /**
1206
+ * Finds the currently rendered cell for a date key.
1207
+ *
1208
+ * @param {string} value YYYY-MM-DD date key.
1209
+ * @returns {MonthCalendarCell|null} Matching visible or hidden cell, when rendered.
1210
+ */
1211
+ function cellForValue(value) {
1212
+ if (!value) return null;
1213
+ return calendarCells.value.find((day) => day.value === value) || null;
1214
+ }
1215
+
1216
+ /**
1217
+ * Safely lists DataTransfer types from a drag event.
1218
+ *
1219
+ * @param {DragEvent} event Native drag event.
1220
+ * @returns {Array<string>} DataTransfer types available to the drop handler.
1221
+ */
1222
+ function dataTransferTypes(event) {
1223
+ return Array.from(event.dataTransfer?.types || []);
1224
+ }
1225
+
1226
+ /**
1227
+ * Safely reads one DataTransfer payload type.
1228
+ *
1229
+ * @param {DragEvent} event Native drag event.
1230
+ * @param {string} type DataTransfer MIME type.
1231
+ * @returns {string} Stored payload, or an empty string when unavailable.
1232
+ */
1233
+ function safeGetData(event, type) {
1234
+ try {
1235
+ return event.dataTransfer?.getData(type) || '';
1236
+ } catch {
1237
+ return '';
1238
+ }
1239
+ }
1240
+
1241
+ /**
1242
+ * Safely writes one DataTransfer payload type.
1243
+ *
1244
+ * @param {DragEvent} event Native drag event.
1245
+ * @param {string} type DataTransfer MIME type.
1246
+ * @param {string} value Payload value to store.
1247
+ * @returns {void}
1248
+ */
1249
+ function safeSetData(event, type, value) {
1250
+ try {
1251
+ event.dataTransfer?.setData(type, value);
1252
+ } catch {
1253
+ // Some browsers reject non-string payloads or unavailable drag stores.
1254
+ }
1255
+ }
1256
+
1257
+ /**
1258
+ * Resolves a class prop that may be static or computed from the day cell.
1259
+ *
1260
+ * @param {MonthCalendarDayClass} classValue Class prop value to resolve.
1261
+ * @param {MonthCalendarCell} cell Day cell passed to function class props.
1262
+ * @returns {unknown} Class value accepted by Vue's class binding.
1263
+ */
1264
+ function resolveDayClass(classValue, cell) {
1265
+ if (!classValue) return '';
1266
+ if (typeof classValue === 'function') return classValue(cell);
1267
+ return classValue;
1268
+ }
1269
+
1270
+ /**
1271
+ * Chooses the base visual state classes for a day cell.
1272
+ *
1273
+ * @param {MonthCalendarCell} cell Day cell being rendered.
1274
+ * @returns {unknown} Class value for the cell's default, muted, adjacent, or hidden state.
1275
+ */
1276
+ function stateDayClassFor(cell) {
1277
+ if (cell.hidden) return 'pointer-events-none bg-secondary/20 text-transparent';
1278
+ if (cell.adjacentMonth) return adjacentDayClassFor(cell);
1279
+ if (cell.muted) return mutedDayClassFor(cell);
1280
+ return 'text-canvas-fg';
1281
+ }
1282
+
1283
+ /**
1284
+ * Resolves classes for a muted current-month day.
1285
+ *
1286
+ * @param {MonthCalendarCell} cell Day cell being rendered.
1287
+ * @returns {unknown} Custom muted classes when provided, otherwise the component default.
1288
+ */
1289
+ function mutedDayClassFor(cell) {
1290
+ return props.mutedDayClass ? resolveDayClass(props.mutedDayClass, cell) : defaultMutedDayClass;
1291
+ }
1292
+
1293
+ /**
1294
+ * Resolves classes for a visible previous or next month day.
1295
+ *
1296
+ * @param {MonthCalendarCell} cell Day cell being rendered.
1297
+ * @returns {unknown} Adjacent classes, muted classes, or the component default in priority order.
1298
+ */
1299
+ function adjacentDayClassFor(cell) {
1300
+ if (props.adjacentDayClass) return resolveDayClass(props.adjacentDayClass, cell);
1301
+ if (props.mutedDayClass) return resolveDayClass(props.mutedDayClass, cell);
1302
+ return defaultMutedDayClass;
1303
+ }
1304
+
1305
+ /**
1306
+ * Resolves consumer-supplied classes for a day cell.
1307
+ *
1308
+ * @param {MonthCalendarCell} cell Day cell being rendered.
1309
+ * @returns {unknown} Class value accepted by Vue's class binding.
1310
+ */
1311
+ function dayClassFor(cell) {
1312
+ return resolveDayClass(props.dayClass, cell);
1313
+ }
1314
+
1315
+ /**
1316
+ * Resolves the subtle partial month divider used by continuous mode.
1317
+ *
1318
+ * @param {MonthCalendarCell} cell Day cell being rendered.
1319
+ * @param {MonthCalendarWeek} week Week row containing the day cell.
1320
+ * @returns {string} Classes that mark the visible month segment edges, or an empty string.
1321
+ */
1322
+ function continuousMonthDividerClassFor(cell, week) {
1323
+ if (cell.hidden || !week.monthBoundaryValues?.length) return '';
1324
+ const boundaryCells = week.days.filter((day) => week.monthBoundaryValues.includes(day.value));
1325
+ const incomingBoundary = boundaryCells.find((boundary) => boundary.monthValue === cell.monthValue && cell.column >= boundary.column);
1326
+ const outgoingBoundary = boundaryCells.find((boundary) => cell.monthValue !== boundary.monthValue && cell.column < boundary.column);
1327
+ const classes = [];
1328
+
1329
+ if (incomingBoundary) {
1330
+ classes.push('dom-month-calendar__day--continuous-month-segment');
1331
+ if (cell.value === incomingBoundary.value && incomingBoundary.column > 0) {
1332
+ classes.push('dom-month-calendar__day--continuous-month-start');
1333
+ }
1334
+ }
1335
+
1336
+ if (outgoingBoundary) {
1337
+ classes.push('dom-month-calendar__day--continuous-month-outgoing');
1338
+ }
1339
+
1340
+ return classes.join(' ');
1341
+ }
1342
+
1343
+ /**
1344
+ * Tracks the month represented by the top visible row inside a continuous calendar scrollport.
1345
+ *
1346
+ * @param {{ continuous?: boolean }} section Render section receiving the scroll event.
1347
+ * @param {Event} event Native scroll event from the section wrapper.
1348
+ * @returns {void}
1349
+ */
1350
+ function onSectionScroll(section, event) {
1351
+ if (!section.continuous || !props.containedContinuousScroll) return;
1352
+ const scrollHost = event.currentTarget;
1353
+ if (!(scrollHost instanceof HTMLElement)) return;
1354
+ updateActiveContinuousMonthForScrollHost(scrollHost, scrollHost);
1355
+ }
1356
+
1357
+ /**
1358
+ * Attaches active-month tracking to the nearest parent scrollport when continuous mode is externally scrolled.
1359
+ *
1360
+ * @returns {void}
1361
+ */
1362
+ function configureExternalContinuousScrollTracking() {
1363
+ removeExternalContinuousScrollTracking();
1364
+ if (!props.continuous || props.containedContinuousScroll) {
1365
+ activeContinuousMonthValue.value = '';
1366
+ return;
1367
+ }
1368
+
1369
+ const sectionElement = getContinuousSectionElement();
1370
+ const scrollHost = sectionElement ? findNearestVerticalScrollHost(sectionElement) : null;
1371
+ if (!sectionElement || !scrollHost) return;
1372
+
1373
+ externalContinuousScrollHost.value = scrollHost;
1374
+ scrollHost.addEventListener('scroll', onExternalContinuousScroll, { passive: true });
1375
+ updateActiveContinuousMonthForScrollHost(scrollHost, sectionElement);
1376
+ }
1377
+
1378
+ /**
1379
+ * Removes the external scroll listener used by parent-owned continuous scrolling.
1380
+ *
1381
+ * @returns {void}
1382
+ */
1383
+ function removeExternalContinuousScrollTracking() {
1384
+ if (!externalContinuousScrollHost.value) return;
1385
+ externalContinuousScrollHost.value.removeEventListener('scroll', onExternalContinuousScroll);
1386
+ externalContinuousScrollHost.value = null;
1387
+ }
1388
+
1389
+ /**
1390
+ * Updates the sticky month label when the parent-owned scrollport moves.
1391
+ *
1392
+ * @returns {void}
1393
+ */
1394
+ function onExternalContinuousScroll() {
1395
+ const sectionElement = getContinuousSectionElement();
1396
+ const scrollHost = externalContinuousScrollHost.value;
1397
+ if (!sectionElement || !scrollHost) return;
1398
+ updateActiveContinuousMonthForScrollHost(scrollHost, sectionElement);
1399
+ }
1400
+
1401
+ /**
1402
+ * Resolves the rendered continuous calendar section element.
1403
+ *
1404
+ * @returns {HTMLElement|null} Continuous calendar wrapper element.
1405
+ */
1406
+ function getContinuousSectionElement() {
1407
+ const element = calendarRoot.value?.querySelector('[data-continuous-section]');
1408
+ return element instanceof HTMLElement ? element : null;
1409
+ }
1410
+
1411
+ /**
1412
+ * Finds the closest ancestor that can own vertical scrolling for an externally contained calendar.
1413
+ *
1414
+ * @param {HTMLElement} element Calendar section element.
1415
+ * @returns {HTMLElement|Window|null} Nearest vertical scroll host, falling back to the browser viewport.
1416
+ */
1417
+ function findNearestVerticalScrollHost(element) {
1418
+ let current = element.parentElement;
1419
+ while (current) {
1420
+ const overflowY = getComputedStyle(current).overflowY;
1421
+ if (/(auto|scroll|overlay)/.test(overflowY)) return current;
1422
+ current = current.parentElement;
1423
+ }
1424
+ return typeof window !== 'undefined' ? window : null;
1425
+ }
1426
+
1427
+ /**
1428
+ * Tracks the month represented by the top visible row inside the supplied scroll host.
1429
+ *
1430
+ * @param {HTMLElement|Window} scrollHost Element or browser viewport owning vertical scroll.
1431
+ * @param {HTMLElement} scope Element containing the continuous week rows.
1432
+ * @returns {void}
1433
+ */
1434
+ function updateActiveContinuousMonthForScrollHost(scrollHost, scope) {
1435
+ const scopedRows = Array.from(scope.querySelectorAll('[data-continuous-week-row]'));
1436
+ if (!scopedRows.length) return;
1437
+
1438
+ const hostTop = scrollHost === window ? 0 : scrollHost.getBoundingClientRect().top;
1439
+ const header = scope.querySelector('[data-continuous-day-headers]');
1440
+ const headerHeight = header instanceof HTMLElement ? header.getBoundingClientRect().height : 0;
1441
+ const activationTop = hostTop + headerHeight + 1;
1442
+ const activeRow = scopedRows.reduce((current, row) => {
1443
+ if (!(row instanceof HTMLElement)) return current;
1444
+ return row.getBoundingClientRect().top <= activationTop ? row : current;
1445
+ }, scopedRows[0]);
1446
+ const monthValue = activeRow instanceof HTMLElement ? activeRow.dataset.activeMonth : '';
1447
+ if (monthValue) activeContinuousMonthValue.value = monthValue;
1448
+ }
1449
+
1450
+ /**
1451
+ * Resolves consumer-supplied inline styles for a day cell.
1452
+ *
1453
+ * @param {MonthCalendarCell} cell Day cell being rendered.
1454
+ * @returns {string|Record<string, string|number>} Style value accepted by Vue's style binding.
1455
+ */
1456
+ function dayStyleFor(cell) {
1457
+ if (!props.dayStyle) return '';
1458
+ if (typeof props.dayStyle === 'function') return props.dayStyle(cell);
1459
+ return props.dayStyle;
1460
+ }
1461
+
1462
+ /**
1463
+ * Emits day-click when clickable mode allows activation for a cell.
1464
+ *
1465
+ * @param {MonthCalendarCell} cell Day cell being activated.
1466
+ * @param {MouseEvent} event Native click event.
1467
+ * @returns {void}
1468
+ */
1469
+ function onDayClick(cell, event) {
1470
+ if (!props.clickable || cell.disabled) return;
1471
+ emit('day-click', { cell, event });
1472
+ }
1473
+ </script>
1474
+
1475
+ <template>
1476
+ <div
1477
+ ref="calendarRoot"
1478
+ class="dom-month-calendar @container w-full min-w-0 space-y-10"
1479
+ >
1480
+ <section
1481
+ v-for="section in calendarSections"
1482
+ :key="section.key"
1483
+ class="min-w-0"
1484
+ >
1485
+ <div
1486
+ v-if="section.continuous && stickyMonthHeaders"
1487
+ class="dom-month-calendar__continuous-mobile-header sticky top-0 z-30 mb-3 items-center justify-between gap-3 border-b border-border bg-canvas/95 py-3 shadow-sm backdrop-blur"
1488
+ >
1489
+ <span class="text-[11px] font-semibold uppercase tracking-wide text-muted-fg">Month</span>
1490
+ <span class="text-base font-bold text-canvas-fg">{{ continuousHeaderMonthLabel }}</span>
1491
+ </div>
1492
+ <header
1493
+ v-if="!section.continuous"
1494
+ class="mb-4 flex flex-wrap items-end justify-between gap-3"
1495
+ :class="stickyMonthHeaders && 'sticky top-0 z-20 border-b border-border bg-canvas/95 py-3 shadow-sm backdrop-blur'"
1496
+ >
1497
+ <h2 :id="section.id" class="text-3xl font-bold tracking-tight text-canvas-fg">{{ section.label }}</h2>
1498
+ <div class="text-sm font-medium text-muted-fg">
1499
+ <slot name="month-header" :month="section.month" />
1500
+ </div>
1501
+ </header>
1502
+ <header
1503
+ v-else-if="$slots['month-header']"
1504
+ class="mb-3 flex justify-end text-sm font-medium text-muted-fg"
1505
+ >
1506
+ <h2 :id="section.id" class="sr-only">{{ section.label }}</h2>
1507
+ <slot name="month-header" :month="section.month" />
1508
+ </header>
1509
+ <h2 v-else :id="section.id" class="sr-only">{{ section.label }}</h2>
1510
+
1511
+ <div
1512
+ class="rounded-2xl border border-border bg-canvas"
1513
+ :data-continuous-section="section.continuous ? '' : undefined"
1514
+ :class="[
1515
+ section.continuous && containedContinuousScroll && 'max-h-[70vh] overflow-auto overscroll-contain',
1516
+ section.continuous && !containedContinuousScroll && 'overflow-x-auto md:overflow-visible',
1517
+ !section.continuous && 'overflow-x-auto',
1518
+ ]"
1519
+ @scroll.passive="onSectionScroll(section, $event)"
1520
+ >
1521
+ <div
1522
+ role="grid"
1523
+ :aria-labelledby="section.id"
1524
+ class="dom-month-calendar__grid min-w-0"
1525
+ :style="{ '--dom-month-calendar-wide-week-columns': wideWeekGridColumns }"
1526
+ >
1527
+ <div
1528
+ v-if="dayHeaders"
1529
+ role="row"
1530
+ :data-continuous-day-headers="section.continuous ? '' : undefined"
1531
+ class="dom-month-calendar__day-headers grid border-b border-border"
1532
+ :class="[
1533
+ section.continuous ? 'sticky top-0 z-20 bg-secondary shadow-sm' : 'bg-secondary/30',
1534
+ ]"
1535
+ >
1536
+ <div
1537
+ v-if="hasRowRail"
1538
+ role="columnheader"
1539
+ class="px-3 py-2 text-left text-[11px] font-semibold uppercase tracking-wide text-muted-fg"
1540
+ >
1541
+ {{ rowRailHeaderLabel }}
1542
+ </div>
1543
+ <div
1544
+ v-for="(weekday, index) in weekdayLabels"
1545
+ :key="weekday"
1546
+ role="columnheader"
1547
+ class="px-3 py-2 text-right text-[11px] font-semibold uppercase tracking-wide text-muted-fg"
1548
+ :class="(index > 0 || hasRowRail) && 'border-l border-border'"
1549
+ >
1550
+ {{ weekday }}
1551
+ </div>
1552
+ </div>
1553
+
1554
+ <div
1555
+ v-for="week in section.weeks"
1556
+ :key="week.index"
1557
+ role="row"
1558
+ :data-continuous-week-row="section.continuous ? '' : undefined"
1559
+ :data-active-month="section.continuous ? week.activeMonthValue : undefined"
1560
+ class="dom-month-calendar__week grid"
1561
+ >
1562
+ <div
1563
+ v-if="hasRowRail"
1564
+ role="rowheader"
1565
+ :aria-label="rowHeaderLabelFor(week)"
1566
+ class="dom-month-calendar__row-rail flex gap-3 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-muted-fg"
1567
+ :class="[
1568
+ week.index > 0 && 'border-t border-border',
1569
+ section.continuous ? 'bg-canvas' : 'bg-secondary/35',
1570
+ section.continuous && week.monthLabels?.length && 'bg-secondary/20',
1571
+ ]"
1572
+ >
1573
+ <div v-if="section.continuous && week.monthLabels?.length" class="space-y-1">
1574
+ <span
1575
+ v-for="label in week.monthLabels"
1576
+ :key="label"
1577
+ class="block text-[11px] font-bold leading-tight text-canvas-fg/70"
1578
+ >
1579
+ {{ label }}
1580
+ </span>
1581
+ </div>
1582
+ <span v-if="showWeekNumbers" class="text-base leading-none tabular-nums">{{ week.weekNumber }}</span>
1583
+ </div>
1584
+ <component
1585
+ :is="clickable ? 'button' : 'div'"
1586
+ v-for="cell in week.days"
1587
+ :key="cell.value"
1588
+ :type="clickable ? 'button' : undefined"
1589
+ :role="cell.hidden ? undefined : 'gridcell'"
1590
+ :tabindex="clickable && !cell.disabled ? 0 : undefined"
1591
+ :disabled="clickable && cell.disabled ? true : undefined"
1592
+ :aria-hidden="cell.hidden ? 'true' : undefined"
1593
+ :aria-label="cell.hidden ? undefined : cell.label"
1594
+ :aria-current="cell.today ? 'date' : undefined"
1595
+ :aria-disabled="clickable && cell.disabled ? 'true' : undefined"
1596
+ :data-date="cell.hidden ? undefined : cell.value"
1597
+ :data-current-month="cell.currentMonth ? '' : undefined"
1598
+ :data-adjacent-month="cell.adjacentMonth ? '' : undefined"
1599
+ :data-previous-month="cell.previousMonth ? '' : undefined"
1600
+ :data-next-month="cell.nextMonth ? '' : undefined"
1601
+ :data-muted="cell.muted ? '' : undefined"
1602
+ :data-today="cell.today ? '' : undefined"
1603
+ :data-drag-source="isDragSource(cell) ? '' : undefined"
1604
+ :data-drop-target="isDropTarget(cell) ? '' : undefined"
1605
+ class="dom-month-calendar__day flex w-full min-w-0 flex-col items-stretch justify-start p-3 text-left outline-none transition"
1606
+ :class="[
1607
+ (hasRowRail || cell.row > 0 || cell.column > 0) && 'border-t border-border',
1608
+ cell.row === 0 && (hasRowRail || cell.column > 0) && 'dom-month-calendar__day--wide-no-border-top',
1609
+ (cell.column > 0 || hasRowRail) && 'dom-month-calendar__day--wide-border-left',
1610
+ stateDayClassFor(cell),
1611
+ clickable && !cell.disabled && 'cursor-pointer focus-visible:z-10 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/50',
1612
+ clickable && !cell.disabled && hoverableDays && 'hover:bg-secondary/50',
1613
+ clickable && cell.disabled && 'cursor-not-allowed opacity-55',
1614
+ !clickable && 'cursor-default',
1615
+ cell.today && !cell.hidden && 'ring-2 ring-inset ring-ring/40',
1616
+ dayClassFor(cell),
1617
+ dragAndDrop && dragState.active && canDropOnCell(cell) && !isDropTarget(cell) && 'bg-primary/5',
1618
+ dragAndDrop && isDropTarget(cell) && 'bg-primary/10 ring-2 ring-inset ring-ring/60',
1619
+ dragAndDrop && isDragSource(cell) && 'opacity-75',
1620
+ section.continuous && continuousMonthDividerClassFor(cell, week),
1621
+ ]"
1622
+ :style="dayStyleFor(cell)"
1623
+ @click="onDayClick(cell, $event)"
1624
+ @dragenter="onDayDragEnter(cell, $event)"
1625
+ @dragover="onDayDragOver(cell, $event)"
1626
+ @dragleave="onDayDragLeave(cell, $event)"
1627
+ @drop="onDayDrop(cell, $event)"
1628
+ >
1629
+ <template v-if="!cell.hidden">
1630
+ <div class="flex items-start justify-between gap-2">
1631
+ <span class="text-base font-bold leading-none tabular-nums">{{ cell.day }}</span>
1632
+ <span class="text-base lowercase leading-none tabular-nums">{{ cell.weekday }}</span>
1633
+ </div>
1634
+ <div class="mt-3 min-h-0 flex-1 h-full">
1635
+ <slot
1636
+ :cell="cell"
1637
+ :month="monthForCell(cell, section.month)"
1638
+ :drag="dragStateFor(cell)"
1639
+ :dragAttrs="dragAttrsFor(cell)"
1640
+ />
1641
+ </div>
1642
+ </template>
1643
+ </component>
1644
+ </div>
1645
+ </div>
1646
+ </div>
1647
+ </section>
1648
+ </div>
1649
+ </template>
1650
+
1651
+ <style scoped>
1652
+ .dom-month-calendar__day-headers {
1653
+ display: var(--dom-month-calendar-day-headers-display);
1654
+ grid-template-columns: var(--dom-month-calendar-week-columns);
1655
+ }
1656
+
1657
+ .dom-month-calendar__week {
1658
+ grid-template-columns: var(--dom-month-calendar-week-columns);
1659
+ }
1660
+
1661
+ .dom-month-calendar__row-rail {
1662
+ align-items: var(--dom-month-calendar-row-rail-align);
1663
+ flex-direction: var(--dom-month-calendar-row-rail-direction);
1664
+ justify-content: var(--dom-month-calendar-row-rail-justify);
1665
+ min-height: var(--dom-month-calendar-row-rail-min-height);
1666
+ }
1667
+
1668
+ .dom-month-calendar__day {
1669
+ min-height: var(--dom-month-calendar-day-min-height);
1670
+ }
1671
+
1672
+ .dom-month-calendar__day--wide-no-border-top {
1673
+ border-top-width: var(--dom-month-calendar-wide-border-top-width, 1px);
1674
+ }
1675
+
1676
+ .dom-month-calendar__day--wide-border-left {
1677
+ border-left-width: var(--dom-month-calendar-wide-border-left-width, 0);
1678
+ border-left-color: var(--border);
1679
+ }
1680
+
1681
+ .dom-month-calendar__day--continuous-month-segment,
1682
+ .dom-month-calendar__day--continuous-month-start,
1683
+ .dom-month-calendar__day--continuous-month-outgoing {
1684
+ --dom-month-calendar-boundary-color: var(--dom-calendar-continuous-month-border, var(--muted));
1685
+ background-image:
1686
+ var(--dom-month-calendar-boundary-top, none),
1687
+ var(--dom-month-calendar-boundary-left, none),
1688
+ var(--dom-month-calendar-boundary-bottom, none);
1689
+ background-position:
1690
+ top,
1691
+ left top,
1692
+ bottom;
1693
+ background-repeat:
1694
+ no-repeat,
1695
+ no-repeat,
1696
+ no-repeat;
1697
+ background-size:
1698
+ var(--dom-month-calendar-boundary-top-size, 0 0),
1699
+ var(--dom-month-calendar-boundary-left-size, 0 0),
1700
+ var(--dom-month-calendar-boundary-bottom-size, 0 0);
1701
+ }
1702
+
1703
+ .dom-month-calendar__day--continuous-month-segment {
1704
+ --dom-month-calendar-boundary-top: var(--dom-month-calendar-wide-boundary-gradient, none);
1705
+ --dom-month-calendar-boundary-top-size: var(--dom-month-calendar-wide-boundary-top-size, 0 0);
1706
+ }
1707
+
1708
+ .dom-month-calendar__day--continuous-month-start {
1709
+ --dom-month-calendar-boundary-left: var(--dom-month-calendar-wide-boundary-gradient, none);
1710
+ --dom-month-calendar-boundary-left-size: var(--dom-month-calendar-wide-boundary-left-size, 0 0);
1711
+ }
1712
+
1713
+ .dom-month-calendar__day--continuous-month-outgoing {
1714
+ --dom-month-calendar-boundary-bottom: var(--dom-month-calendar-wide-boundary-gradient, none);
1715
+ --dom-month-calendar-boundary-bottom-size: var(--dom-month-calendar-wide-boundary-bottom-size, 0 0);
1716
+ }
1717
+
1718
+ </style>