@xy-planning-network/trees 0.13.11 → 0.14.0-dev-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xy-planning-network/trees",
3
- "version": "0.13.11",
3
+ "version": "0.14.0-dev-2",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "repository": "github:xy-planning-network/trees",
@@ -64,7 +64,7 @@
64
64
  "@maskito/kit": "^3.5.0",
65
65
  "@maskito/vue": "^3.5.0",
66
66
  "axios": "^1.5.0",
67
- "flatpickr": "^4.6.9"
67
+ "reka-ui": "^2.9.9"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@tailwindcss/forms": "^0.5.2",
package/src/index.css CHANGED
@@ -229,9 +229,67 @@
229
229
  @apply font-medium border-b-2 border-b-xy-blue hover:border-b-transparent hover:text-xy-blue;
230
230
  }
231
231
 
232
- /* Flatpickr DateRangePicker */
233
- .xy-date-range-picker .flatpickr-wrapper {
234
- display: block;
232
+ /* RangeCalendar */
233
+
234
+ /* Calendar Prev/Next navigation */
235
+ .xy-range-cal-nav[data-disabled] {
236
+ @apply cursor-not-allowed opacity-40 hover:bg-transparent;
237
+ }
238
+
239
+ /* Calendar Day Trigger */
240
+ .xy-range-cal-trigger {
241
+ @apply relative flex items-center justify-center rounded-full whitespace-nowrap text-sm font-normal text-black w-8 h-8 outline-none;
242
+ }
243
+
244
+ .xy-range-cal-trigger:focus-visible {
245
+ @apply shadow-[0_0_0_2px] shadow-xy-blue-500;
246
+ }
247
+
248
+ .xy-range-cal-trigger[data-highlighted] {
249
+ @apply bg-neutral-100;
250
+ }
251
+
252
+ .xy-range-cal-trigger[data-outside-view] {
253
+ @apply text-black/50;
254
+ }
255
+
256
+ .xy-range-cal-trigger[data-unavailable] {
257
+ @apply pointer-events-none text-black/50 line-through;
258
+ }
259
+
260
+ .xy-range-cal-trigger[data-disabled] {
261
+ @apply cursor-not-allowed text-black/50;
262
+ }
263
+
264
+ .xy-range-cal-trigger[data-selected] {
265
+ @apply !bg-xy-blue-700 text-white;
266
+ }
267
+
268
+ .xy-range-cal-trigger:hover {
269
+ @apply bg-xy-blue-700 text-white;
270
+ }
271
+
272
+ .xy-range-cal-trigger[data-disabled]:hover,
273
+ .xy-range-cal-trigger[data-unavailable]:hover {
274
+ @apply bg-transparent text-black/50;
275
+ }
276
+
277
+ /* Today Indicator Dot */
278
+ .xy-range-cal-trigger::before {
279
+ @apply absolute bottom-[3px] hidden h-1 w-1 rounded-full bg-white;
280
+ content: "";
281
+ }
282
+
283
+ .xy-range-cal-trigger[data-today]::before {
284
+ @apply block bg-xy-blue-600;
285
+ }
286
+
287
+ .xy-range-cal-trigger[data-today][data-selected]::before,
288
+ .xy-range-cal-trigger[data-today][data-selection-start]::before,
289
+ .xy-range-cal-trigger[data-today][data-selection-end]::before,
290
+ .xy-range-cal-trigger[data-today][data-highlighted-start]::before,
291
+ .xy-range-cal-trigger[data-today][data-highlighted-end]::before {
292
+ @apply bg-white;
235
293
  }
236
294
  }
237
295
 
@@ -1,166 +1,212 @@
1
1
  <script setup lang="ts">
2
- import InputLabel from "./InputLabel.vue"
3
- import InputHelp from "./InputHelp.vue"
4
- import InputError from "./InputError.vue"
5
- import flatpickr from "flatpickr"
6
- import "flatpickr/dist/flatpickr.min.css"
7
- import { onMounted, useTemplateRef, watch } from "vue"
2
+ import { computed, nextTick, ref, useTemplateRef } from "vue"
8
3
  import {
9
4
  defaultInputProps,
10
5
  defaultModelOpts,
11
6
  useInputField,
12
7
  } from "@/composables/forms"
13
8
  import type { DateRangeInput } from "@/composables/forms"
14
- import { Instance } from "flatpickr/dist/types/instance"
15
- import { DateRange } from "@/composables/date"
9
+ import RangeCalendar from "@/lib-components/forms/RangeCalendar.vue"
10
+ import { CalendarDateRangeIcon, XMarkIcon } from "@heroicons/vue/solid"
11
+
12
+ import {
13
+ PopoverAnchor,
14
+ PopoverContent,
15
+ PopoverPortal,
16
+ PopoverRoot,
17
+ PopoverTrigger,
18
+ } from "reka-ui"
16
19
 
17
20
  defineOptions({
18
21
  inheritAttrs: false,
19
22
  })
20
23
 
21
- // maxDate/startDate should be used or maxRange.
22
- // The props combination of maxDate/startDate and maxRange
23
- // will have unexpected results.
24
+ /**
25
+ * NOTE(spk): Default actions made available by Trees do not
26
+ * account for minValue, maxValue, and maxRange boundary restrictions.
27
+ *
28
+ * Likewise a pre-hydrated v-model that is outside those bounds also does
29
+ * not apply boundaries to that initial value.
30
+ */
24
31
  const props = withDefaults(defineProps<DateRangeInput>(), {
25
32
  ...defaultInputProps,
26
- maxDate: () => new Date(),
27
- maxRange: 0,
28
- placeholder: "mm-dd-yyyy range",
29
- startDate: 0,
33
+ actions: () => [],
34
+ maxRange: undefined,
35
+ maxValue: () => new Date(),
36
+ minValue: undefined,
37
+ placeholder: "mm-dd-yyyy to mm-dd-yyyy",
38
+ position: "bottom-start",
30
39
  })
31
40
 
32
41
  const modelState = defineModel<DateRangeInput["modelValue"]>({
33
42
  ...defaultModelOpts,
34
- default: { maxDate: 0, minDate: 0 },
43
+ default: { maxDate: undefined, minDate: undefined },
35
44
  })
36
45
 
37
- function isValidPickerRange(val: any): val is DateRange {
38
- return (
39
- val?.maxDate !== undefined &&
40
- val?.minDate !== undefined &&
41
- val?.minDate != 0 &&
42
- val?.maxDate != 0
43
- )
44
- }
46
+ const {
47
+ aria,
48
+ errorState,
49
+ inputID,
50
+ isDisabled,
51
+ isRequired,
52
+ nameAttr,
53
+ onInvalid,
54
+ } = useInputField(props)
55
+
56
+ const dateFormatter = new Intl.DateTimeFormat("en-US", {
57
+ year: "numeric",
58
+ month: "short",
59
+ day: "2-digit",
60
+ })
61
+
62
+ const display = computed(() => {
63
+ let out = ""
45
64
 
46
- // NOTE(spk): use watcher.pause() and watcher.resume() when mutating
47
- // state inside this component to avoid setting the picker value
48
- // after the state was mutated by the picker itself
49
- //
50
- // This keeps the change handling scoped to a change triggered by
51
- // the parent component on v-model.
52
- const watcher = watch(modelState, () => {
53
- if (isValidPickerRange(modelState.value)) {
54
- picker?.setDate(
55
- [modelState.value.minDate * 1000, modelState.value.maxDate * 1000],
56
- false
57
- )
58
- return
65
+ if (modelState.value?.minDate) {
66
+ out += `${dateFormatter.format(
67
+ new Date(modelState.value.minDate * 1000)
68
+ )} to `
59
69
  }
60
70
 
61
- picker?.clear()
71
+ if (modelState.value?.maxDate) {
72
+ out += dateFormatter.format(new Date(modelState.value.maxDate * 1000))
73
+ }
74
+
75
+ return out
62
76
  })
63
77
 
64
- const { aria, errorState, inputID, isRequired, onInvalid, validate } =
65
- useInputField(props)
78
+ const reset = () => {
79
+ modelState.value = { maxDate: 0, minDate: 0 }
80
+ }
66
81
 
67
- const updateModelState = (value: { minDate: number; maxDate: number }) => {
68
- watcher.pause()
69
- modelState.value = value
70
- watcher.resume()
82
+ const isValid = computed(() => {
83
+ return !!(modelState.value?.maxDate && modelState.value?.minDate)
84
+ })
85
+
86
+ const onUpdate = () => {
87
+ nextTick(() => {
88
+ if (isValid.value) {
89
+ errorState.value = ""
90
+ isOpen.value = false
91
+ }
92
+ })
71
93
  }
72
94
 
73
- const wrapperRef = useTemplateRef("wrapper")
74
-
75
- let picker: Instance | null = null
76
-
77
- onMounted(() => {
78
- const opts: flatpickr.Options.Options = {
79
- allowInput: !props.maxRange,
80
- appendTo: wrapperRef.value || undefined,
81
- dateFormat: "m-d-Y",
82
- mode: "range",
83
- maxDate: props.maxDate,
84
- minDate: props.startDate,
85
- onClose: (selectedDates) => {
86
- if (selectedDates.length === 2) {
87
- updateModelState({
88
- minDate: selectedDates[0].setUTCHours(0, 0, 0, 0) / 1000,
89
- maxDate: Math.floor(
90
- selectedDates[1].setUTCHours(23, 59, 59, 999) / 1000
91
- ),
92
- })
93
- } else if (selectedDates.length === 0) {
94
- updateModelState({
95
- minDate: 0,
96
- maxDate: 0,
97
- })
98
- }
99
- },
100
- static: true,
95
+ const isOpen = ref(false)
96
+ const onUpdateIsOpen = (open: boolean) => {
97
+ if (!open && !isValid.value) {
98
+ reset()
101
99
  }
100
+ }
102
101
 
103
- // Handle initial values if set
104
- if (isValidPickerRange(modelState.value)) {
105
- opts.defaultDate = [
106
- modelState.value.minDate * 1000,
107
- modelState.value.maxDate * 1000,
108
- ]
109
- }
102
+ const calendarRef = useTemplateRef<InstanceType<typeof RangeCalendar> | null>(
103
+ "calendar"
104
+ )
110
105
 
111
- if (props.maxRange) {
112
- // Handle onChange to dynamically adjust maxDate to x days ahead of the selected start date
113
- opts.onChange = (selectedDates, _, self) => {
114
- if (selectedDates.length === 1) {
115
- // Clone date so as to not change selectedDates[0] value
116
- var daysAhead = new Date(selectedDates[0].getTime())
117
- var daysBefore = new Date(selectedDates[0].getTime())
118
- daysAhead.setDate(daysAhead.getDate() + props.maxRange)
119
- daysBefore.setDate(daysBefore.getDate() - props.maxRange)
120
- const now = new Date()
121
-
122
- if (daysAhead > now) {
123
- daysAhead = now
124
- }
125
-
126
- self.set("minDate", daysBefore)
127
- self.set("maxDate", daysAhead)
128
- }
129
- }
130
- }
106
+ const inputRef = useTemplateRef<HTMLInputElement | null>("input")
131
107
 
132
- picker = flatpickr(`#${inputID.value}`, opts) as Instance
108
+ defineExpose({ calendar: calendarRef, input: inputRef })
109
+
110
+ const alignment = computed(() => {
111
+ switch (props.position) {
112
+ case "bottom-start":
113
+ return "start"
114
+ case "bottom-end":
115
+ return "end"
116
+ default: // bottom
117
+ return "center"
118
+ }
133
119
  })
134
120
  </script>
135
121
 
136
122
  <template>
137
- <div ref="wrapper" class="xy-date-range-picker">
138
- <InputLabel
139
- :id="aria.labelledby"
140
- class="mb-2"
141
- :for="inputID"
142
- :label="label"
143
- :required="isRequired"
144
- />
145
- <input
146
- :id="inputID"
147
- :aria-labelledby="aria.labelledby"
148
- :aria-describedby="aria.describedby"
149
- :aria-errormessage="aria.errormessage"
150
- :class="[
151
- 'block w-full rounded-md border-0 py-2 shadow-sm ring-1 ring-inset focus:ring-2 sm:text-sm sm:leading-6',
152
- 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
153
- errorState
154
- ? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700'
155
- : 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500',
156
- ]"
157
- :placeholder="placeholder"
158
- v-bind="$attrs"
159
- type="text"
160
- @input="validate"
161
- @invalid="onInvalid"
162
- />
163
- <InputHelp :id="aria.describedby" class="mt-1" :text="help" />
164
- <InputError :id="aria.errormessage" class="mt-0.5" :text="errorState" />
165
- </div>
123
+ <PopoverRoot v-model:open="isOpen" @update:open="onUpdateIsOpen">
124
+ <div class="xy-date-range-picker">
125
+ <InputLabel
126
+ :id="aria.labelledby"
127
+ class="mb-2"
128
+ :for="inputID"
129
+ :label="label"
130
+ :required="isRequired"
131
+ />
132
+ <PopoverAnchor as="div" class="relative">
133
+ <input
134
+ :id="inputID"
135
+ ref="input"
136
+ :aria-labelledby="aria.labelledby"
137
+ :aria-describedby="aria.describedby"
138
+ :aria-errormessage="aria.errormessage"
139
+ :class="[
140
+ 'block w-full rounded-md border-0 py-2 pr-9 shadow-sm ring-1 ring-inset focus:ring-2 data-[state=open]:ring-2 sm:text-sm sm:leading-6',
141
+ 'disabled:cursor-not-allowed disabled:bg-gray-50 disabled:text-gray-700 disabled:ring-gray-200',
142
+ errorState
143
+ ? 'text-red-900 ring-red-700 placeholder:text-red-300 focus:ring-red-700 data-[state=open]:ring-red-700'
144
+ : 'text-gray-900 ring-gray-300 placeholder:text-gray-400 focus:ring-xy-blue-500 data-[state=open]:ring-xy-blue-500',
145
+ ]"
146
+ :data-state="isOpen ? 'open' : 'closed'"
147
+ :placeholder="placeholder"
148
+ readonly
149
+ :value="display"
150
+ v-bind="$attrs"
151
+ type="text"
152
+ @focus="isOpen = true"
153
+ />
154
+
155
+ <div class="absolute right-0 top-1/2 -translate-y-1/2 text-neutral-400">
156
+ <button v-if="isValid && !isDisabled" class="p-2" @click="reset">
157
+ <XMarkIcon class="w-5 h-5" />
158
+ </button>
159
+
160
+ <PopoverTrigger class="p-2" :disabled="isDisabled" tabindex="-1">
161
+ <CalendarDateRangeIcon class="w-5 h-5" />
162
+ </PopoverTrigger>
163
+ </div>
164
+ </PopoverAnchor>
165
+
166
+ <InputHelp :id="aria.describedby" class="mt-1" :text="help" />
167
+ <InputError :id="aria.errormessage" class="mt-0.5" :text="errorState" />
168
+
169
+ <input
170
+ class="sr-only top-1 left-1"
171
+ aria-hidden="true"
172
+ :name="`${nameAttr}[minDate]`"
173
+ :required="isRequired"
174
+ :value="modelState?.minDate"
175
+ tabindex="-1"
176
+ @invalid="onInvalid"
177
+ />
178
+ <input
179
+ class="sr-only top-1 left-1"
180
+ aria-hidden="true"
181
+ :name="`${nameAttr}[maxDate]`"
182
+ :required="isRequired"
183
+ :value="modelState?.maxDate"
184
+ tabindex="-1"
185
+ @invalid="onInvalid"
186
+ />
187
+ </div>
188
+
189
+ <PopoverPortal disabled>
190
+ <PopoverContent
191
+ :align="alignment"
192
+ :align-flip="false"
193
+ :arrow-padding="10"
194
+ class="border border-neutral-100 overflow-hidden rounded-xy-lg shadow z-50"
195
+ side="bottom"
196
+ :side-flip="false"
197
+ :side-offset="5"
198
+ >
199
+ <RangeCalendar
200
+ ref="calendar"
201
+ v-model="modelState"
202
+ :actions="actions"
203
+ borderless
204
+ :max-range="maxRange"
205
+ :max-value="maxValue"
206
+ :min-value="minValue"
207
+ @update:model-value="onUpdate"
208
+ />
209
+ </PopoverContent>
210
+ </PopoverPortal>
211
+ </PopoverRoot>
166
212
  </template>
@@ -0,0 +1,245 @@
1
+ <script setup lang="ts">
2
+ import type { DateRange as RekaDateRange } from "reka-ui"
3
+ import { getLocalTimeZone, CalendarDate } from "@internationalized/date"
4
+ import {
5
+ RangeCalendarCell,
6
+ RangeCalendarCellTrigger,
7
+ RangeCalendarGrid,
8
+ RangeCalendarGridBody,
9
+ RangeCalendarGridHead,
10
+ RangeCalendarGridRow,
11
+ RangeCalendarHeadCell,
12
+ RangeCalendarNext,
13
+ RangeCalendarPrev,
14
+ RangeCalendarRoot,
15
+ useDateFormatter,
16
+ } from "reka-ui"
17
+ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/vue/solid"
18
+ import { computed } from "vue"
19
+ import { defaultModelOpts } from "@/composables/forms"
20
+ import type { DateRangeAction, DateRangeInput } from "@/composables/forms"
21
+ import { calendarDateToUnix } from "@/composables/dateRange"
22
+
23
+ const formatter = useDateFormatter("en-US")
24
+
25
+ defineOptions({
26
+ inheritAttrs: false,
27
+ })
28
+
29
+ /**
30
+ * FIXME(spk): An oddity of Reka-UI RangeCalendar is that is applies
31
+ * [data-highlighted] attributes to all cells inside the range applicable
32
+ * to the maximum-days range, even those that are outside of min-value/max-value.
33
+ */
34
+ const props = withDefaults(
35
+ defineProps<
36
+ DateRangeInput & {
37
+ actions?: DateRangeAction[]
38
+ borderless?: boolean
39
+ maxRange?: number
40
+ maxValue?: Date | null
41
+ minValue?: Date
42
+ }
43
+ >(),
44
+ {
45
+ actions: () => [],
46
+ borderless: false,
47
+ maxRange: undefined,
48
+ maxValue: undefined,
49
+ minValue: undefined,
50
+ }
51
+ )
52
+
53
+ const modelState = defineModel<DateRangeInput["modelValue"]>({
54
+ ...defaultModelOpts,
55
+ default: { maxDate: 0, minDate: 0 },
56
+ })
57
+
58
+ const selectedRange = computed<RekaDateRange>({
59
+ get: () => {
60
+ if (!modelState.value) {
61
+ return {
62
+ start: undefined,
63
+ end: undefined,
64
+ }
65
+ }
66
+
67
+ return {
68
+ start: unixToCalendarDate(modelState.value.minDate),
69
+ end: unixToCalendarDate(modelState.value.maxDate),
70
+ }
71
+ },
72
+ set: (val) => {
73
+ setModelState({
74
+ minDate: val?.start ? calendarDateToUnix(val.start) : 0,
75
+ maxDate: val?.end
76
+ ? Math.floor(
77
+ val.end.toDate(getLocalTimeZone()).setHours(23, 59, 59, 999) / 1000
78
+ )
79
+ : 0,
80
+ })
81
+ },
82
+ })
83
+
84
+ // NOTE(spk): guard against Reka write-back loop when modelState is set
85
+ // to zero-values as it will trigger multiple update:model-value events.
86
+ const setModelState = (next: NonNullable<DateRangeInput["modelValue"]>) => {
87
+ if (
88
+ modelState.value?.minDate === next.minDate &&
89
+ modelState.value?.maxDate === next.maxDate
90
+ ) {
91
+ return
92
+ }
93
+
94
+ modelState.value = next
95
+ }
96
+
97
+ const unixToCalendarDate = (
98
+ unixSeconds?: number | null
99
+ ): CalendarDate | undefined => {
100
+ if (!unixSeconds) {
101
+ return undefined
102
+ }
103
+
104
+ const localDate = new Date(unixSeconds * 1000)
105
+ return new CalendarDate(
106
+ localDate.getFullYear(),
107
+ localDate.getMonth() + 1,
108
+ localDate.getDate()
109
+ )
110
+ }
111
+
112
+ const bounds = computed(() => {
113
+ return {
114
+ maxValue: props.maxValue
115
+ ? new CalendarDate(
116
+ props.maxValue.getFullYear(),
117
+ props.maxValue.getMonth() + 1,
118
+ props.maxValue.getDate()
119
+ )
120
+ : undefined,
121
+ minValue: props.minValue
122
+ ? new CalendarDate(
123
+ props.minValue.getFullYear(),
124
+ props.minValue.getMonth() + 1,
125
+ props.minValue.getDate()
126
+ )
127
+ : undefined,
128
+ }
129
+ })
130
+
131
+ const quickActions = computed(() => {
132
+ return props.actions.map((action) => {
133
+ return {
134
+ label: action.label,
135
+ action: () => {
136
+ setModelState(action.action(props))
137
+ },
138
+ }
139
+ })
140
+ })
141
+ </script>
142
+
143
+ <template>
144
+ <div class="w-full flex items-start justify-start">
145
+ <div
146
+ :class="[
147
+ 'bg-white overflow-hidden flex flex-col-reverse sm:flex-row',
148
+ !borderless && 'border border-neutral-100 shadow rounded-xy-lg',
149
+ ]"
150
+ >
151
+ <!--Quick Actions-->
152
+ <div
153
+ v-if="quickActions.length > 0"
154
+ class="bg-neutral-50 p-4 grid grid-cols-2 sm:grid-cols-1 lg:max-w-[160px]"
155
+ >
156
+ <button
157
+ v-for="option in quickActions"
158
+ :key="option.label"
159
+ class="flex w-full rounded-md bg-transparent hover:bg-gray-100 transition px-3 py-2 text-xs font-medium"
160
+ @click="option.action"
161
+ >
162
+ {{ option.label }}
163
+ </button>
164
+ </div>
165
+
166
+ <div>
167
+ <!-- Calendar Input-->
168
+ <RangeCalendarRoot
169
+ v-slot="{ weekDays, grid }"
170
+ v-model="selectedRange"
171
+ class="flex space-y-4 flex-col lg:flex-row lg:space-y-0 p-4"
172
+ :number-of-months="1"
173
+ locale="en-US"
174
+ :maximum-days="maxRange"
175
+ :max-value="bounds.maxValue"
176
+ :min-value="bounds.minValue"
177
+ >
178
+ <div
179
+ v-for="(month, index) in grid"
180
+ :key="month.value.toString()"
181
+ :class="{ 'mr-4': index === 0 }"
182
+ >
183
+ <div v-if="index === 0" class="flex items-center">
184
+ <RangeCalendarPrev class="xy-btn-neutral-sm xy-range-cal-nav">
185
+ <ChevronLeftIcon class="w-4 h-4" />
186
+ </RangeCalendarPrev>
187
+
188
+ <!--Left Month Date Display-->
189
+ <span
190
+ class="text-sm font-semibold text-xy-black flex-1 text-center"
191
+ >{{
192
+ formatter.custom(month.value.toDate(getLocalTimeZone()), {
193
+ month: "long",
194
+ year: "numeric",
195
+ })
196
+ }}</span
197
+ >
198
+
199
+ <RangeCalendarNext class="xy-btn-neutral-sm xy-range-cal-nav">
200
+ <ChevronRightIcon class="w-4 h-4" />
201
+ </RangeCalendarNext>
202
+ </div>
203
+
204
+ <div class="pt-4">
205
+ <RangeCalendarGrid class="w-full select-none space-y-1">
206
+ <RangeCalendarGridHead>
207
+ <RangeCalendarGridRow class="mb-1 grid w-full grid-cols-7">
208
+ <RangeCalendarHeadCell
209
+ v-for="day in weekDays"
210
+ :key="day"
211
+ class="text-xs text-xy-blue-600 font-bold"
212
+ >
213
+ {{ day }}
214
+ </RangeCalendarHeadCell>
215
+ </RangeCalendarGridRow>
216
+ </RangeCalendarGridHead>
217
+
218
+ <RangeCalendarGridBody class="grid">
219
+ <RangeCalendarGridRow
220
+ v-for="(weekDates, rowIndex) in month.rows"
221
+ :key="`weekDate-${rowIndex}`"
222
+ class="grid grid-cols-7"
223
+ >
224
+ <RangeCalendarCell
225
+ v-for="weekDate in weekDates"
226
+ :key="weekDate.toString()"
227
+ :date="weekDate"
228
+ class="xy-range-calendar-cell"
229
+ >
230
+ <RangeCalendarCellTrigger
231
+ :day="weekDate"
232
+ :month="month.value"
233
+ class="xy-range-cal-trigger"
234
+ />
235
+ </RangeCalendarCell>
236
+ </RangeCalendarGridRow>
237
+ </RangeCalendarGridBody>
238
+ </RangeCalendarGrid>
239
+ </div>
240
+ </div>
241
+ </RangeCalendarRoot>
242
+ </div>
243
+ </div>
244
+ </div>
245
+ </template>