@datagouv/components-next 1.2.0 → 1.4.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 (138) hide show
  1. package/README.md +7 -6
  2. package/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
  3. package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
  4. package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
  5. package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
  6. package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
  7. package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
  8. package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
  9. package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
  10. package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
  11. package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
  12. package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
  13. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
  14. package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
  15. package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
  16. package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
  17. package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
  18. package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
  19. package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
  20. package/dist/components-next.css +1 -52
  21. package/dist/components-next.js +201 -170
  22. package/dist/components.css +2 -2
  23. package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
  24. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
  25. package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
  26. package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
  27. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  28. package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
  29. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  30. package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
  31. package/package.json +40 -39
  32. package/src/components/ActivityList/ActivityList.vue +21 -12
  33. package/src/components/AppLink.vue +3 -0
  34. package/src/components/BannerAction.vue +6 -2
  35. package/src/components/Chart/ChartViewer.vue +111 -17
  36. package/src/components/Chart/ChartViewerWrapper.vue +28 -16
  37. package/src/components/ChartCard.vue +112 -0
  38. package/src/components/CopyButton.vue +6 -2
  39. package/src/components/DataserviceCard.vue +23 -6
  40. package/src/components/DataserviceQuality.vue +82 -0
  41. package/src/components/DataserviceQualityInline.vue +31 -0
  42. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  43. package/src/components/DatasetCard.vue +17 -6
  44. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  45. package/src/components/DatasetQuality.vue +11 -11
  46. package/src/components/DatasetQualityInline.vue +2 -2
  47. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  48. package/src/components/DiscussionMessageCard.vue +14 -15
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/FormattedDate.vue +69 -0
  51. package/src/components/LoadingBlock.vue +4 -1
  52. package/src/components/ObjectCard.vue +4 -1
  53. package/src/components/ObjectCardOwner.vue +13 -1
  54. package/src/components/Pagination.vue +18 -58
  55. package/src/components/Placeholder.vue +3 -2
  56. package/src/components/PostCard.vue +13 -15
  57. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  58. package/src/components/ResourceAccordion/Downloads.vue +19 -7
  59. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  60. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
  61. package/src/components/ResourceAccordion/Metadata.vue +21 -5
  62. package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
  63. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  64. package/src/components/ResourceAccordion/Pmtiles.client.vue +30 -19
  65. package/src/components/ResourceAccordion/Preview.vue +8 -4
  66. package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
  67. package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
  68. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  69. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  70. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  71. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +140 -184
  72. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  73. package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
  74. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
  75. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
  76. package/src/components/ResourceListItem.vue +168 -0
  77. package/src/components/ReuseCard.vue +18 -6
  78. package/src/components/ReuseDetails.vue +2 -2
  79. package/src/components/ReuseHorizontalCard.vue +4 -3
  80. package/src/components/SchemaCard.vue +10 -1
  81. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  82. package/src/components/Search/GlobalSearch.vue +28 -10
  83. package/src/components/Search/SearchInput.vue +1 -1
  84. package/src/components/Search/Sidemenu.vue +4 -3
  85. package/src/components/SmallChart.vue +11 -10
  86. package/src/components/StatBox.vue +13 -5
  87. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  88. package/src/components/TabularExplorer/TabularCell.vue +7 -8
  89. package/src/components/TabularExplorer/TabularCellPopover.vue +21 -5
  90. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  91. package/src/components/TabularExplorer/TabularDateField.vue +41 -0
  92. package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
  93. package/src/components/TabularExplorer/TabularExplorer.vue +127 -780
  94. package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
  95. package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
  96. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  97. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  98. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  99. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  100. package/src/components/TabularExplorer/TabularTable.vue +484 -0
  101. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  102. package/src/components/TabularExplorer/types.ts +40 -28
  103. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  104. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  105. package/src/components/TopicCard.vue +15 -14
  106. package/src/composables/useActiveDescendant.ts +5 -0
  107. package/src/composables/useDatasetResources.ts +164 -0
  108. package/src/composables/useHasTabularData.ts +2 -1
  109. package/src/composables/useResizable.ts +46 -0
  110. package/src/composables/useResourceCapabilities.ts +48 -40
  111. package/src/composables/useSearchFilter.ts +7 -5
  112. package/src/composables/useTranslation.ts +24 -9
  113. package/src/config.ts +1 -0
  114. package/src/functions/api.ts +13 -7
  115. package/src/functions/charts.ts +32 -1
  116. package/src/functions/dates.ts +26 -7
  117. package/src/functions/helpers.ts +15 -0
  118. package/src/functions/metrics.ts +57 -56
  119. package/src/functions/owned.ts +21 -0
  120. package/src/functions/paginate.ts +46 -0
  121. package/src/functions/resourceCapabilities.ts +34 -0
  122. package/src/functions/resources.ts +88 -0
  123. package/src/functions/schemas.ts +27 -0
  124. package/src/functions/scroll.ts +20 -0
  125. package/src/functions/tabular.ts +222 -5
  126. package/src/functions/tabularApi.ts +1 -43
  127. package/src/main.ts +32 -6
  128. package/src/types/dataservices.ts +15 -0
  129. package/src/types/organizations.ts +10 -0
  130. package/src/types/resources.ts +0 -1
  131. package/src/types/reuses.ts +12 -1
  132. package/src/types/search.ts +2 -2
  133. package/src/types/visualizations.ts +5 -0
  134. package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
  135. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js +0 -61
  136. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  137. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  138. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -0,0 +1,403 @@
1
+ <template>
2
+ <!-- Spacing carries the grouping: what the filter is (operator, dates), then the
3
+ calendar with the bar that drives it, then the actions. -->
4
+ <form
5
+ class="border-b border-black/10 px-3 py-2"
6
+ @submit.prevent="apply"
7
+ >
8
+ <select
9
+ v-model="operator"
10
+ class="h-8 w-full rounded-lg border border-transparent bg-[#f3f3f5] px-2 text-sm focus:border-new-primary focus:outline-none"
11
+ :aria-label="t('Condition du filtre')"
12
+ >
13
+ <option
14
+ v-for="option in operatorOptions"
15
+ :key="option.value"
16
+ :value="option.value"
17
+ >
18
+ {{ option.label }}
19
+ </option>
20
+ </select>
21
+
22
+ <div class="mt-2 flex items-stretch gap-2">
23
+ <TabularDateField
24
+ v-model="start"
25
+ v-model:placeholder="placeholder"
26
+ :label="isRange ? t('Date de début') : t('Date')"
27
+ />
28
+ <TabularDateField
29
+ v-if="isRange"
30
+ v-model="end"
31
+ v-model:placeholder="placeholder"
32
+ :min-value="start ?? undefined"
33
+ :label="t('Date de fin')"
34
+ />
35
+ </div>
36
+
37
+ <!-- Month and year are pickers rather than a heading: reaching a date years
38
+ away otherwise takes one click per month. -->
39
+ <div class="mt-4 flex items-center gap-1">
40
+ <select
41
+ :value="placeholder.month"
42
+ class="h-7 min-w-0 flex-1 rounded border border-black/10 bg-white px-1 text-xs focus:border-new-primary focus:outline-none"
43
+ :aria-label="t('Mois')"
44
+ @change="goToMonth(Number(($event.target as HTMLSelectElement).value))"
45
+ >
46
+ <option
47
+ v-for="(label, index) in monthNames"
48
+ :key="label"
49
+ :value="index + 1"
50
+ >
51
+ {{ label }}
52
+ </option>
53
+ </select>
54
+ <select
55
+ :value="placeholder.year"
56
+ class="h-7 w-18 rounded border border-black/10 bg-white px-1 text-xs tabular-nums focus:border-new-primary focus:outline-none"
57
+ :aria-label="t('Année')"
58
+ @change="goToYear(Number(($event.target as HTMLSelectElement).value))"
59
+ >
60
+ <option
61
+ v-for="year in selectableYears"
62
+ :key="year"
63
+ :value="year"
64
+ >
65
+ {{ year }}
66
+ </option>
67
+ </select>
68
+ <button
69
+ type="button"
70
+ class="flex size-7 shrink-0 items-center justify-center rounded text-gray-plain transition-colors hover:bg-gray-some focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
71
+ :aria-label="t('Mois précédent')"
72
+ @click="shiftMonths(-1)"
73
+ >
74
+ <RiArrowLeftSLine
75
+ class="size-4"
76
+ aria-hidden="true"
77
+ />
78
+ </button>
79
+ <button
80
+ type="button"
81
+ class="flex size-7 shrink-0 items-center justify-center rounded text-gray-plain transition-colors hover:bg-gray-some focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
82
+ :aria-label="t('Mois suivant')"
83
+ @click="shiftMonths(1)"
84
+ >
85
+ <RiArrowRightSLine
86
+ class="size-4"
87
+ aria-hidden="true"
88
+ />
89
+ </button>
90
+ </div>
91
+
92
+ <RangeCalendarRoot
93
+ v-if="isRange"
94
+ v-slot="{ grid, weekDays }"
95
+ v-model="range"
96
+ v-model:placeholder="placeholder"
97
+ :locale="locale"
98
+ fixed-weeks
99
+ weekday-format="short"
100
+ :calendar-label="t('Choisir une plage de dates')"
101
+ >
102
+ <RangeCalendarGrid
103
+ v-for="month in grid"
104
+ :key="month.value.toString()"
105
+ class="w-full border-collapse select-none"
106
+ >
107
+ <RangeCalendarGridHead>
108
+ <RangeCalendarGridRow>
109
+ <RangeCalendarHeadCell
110
+ v-for="day in weekDays"
111
+ :key="day"
112
+ :class="HEAD_CELL_CLASS"
113
+ >
114
+ {{ day }}
115
+ </RangeCalendarHeadCell>
116
+ </RangeCalendarGridRow>
117
+ </RangeCalendarGridHead>
118
+ <RangeCalendarGridBody>
119
+ <RangeCalendarGridRow
120
+ v-for="(week, index) in month.rows"
121
+ :key="`week-${index}`"
122
+ >
123
+ <RangeCalendarCell
124
+ v-for="day in week"
125
+ :key="day.toString()"
126
+ :date="day"
127
+ class="p-0"
128
+ >
129
+ <RangeCalendarCellTrigger
130
+ v-slot="cell"
131
+ :day="day"
132
+ :month="month.value"
133
+ :class="TRIGGER_CLASS"
134
+ >
135
+ <span :class="rangeDayClass(cell)">{{ cell.dayValue }}</span>
136
+ </RangeCalendarCellTrigger>
137
+ </RangeCalendarCell>
138
+ </RangeCalendarGridRow>
139
+ </RangeCalendarGridBody>
140
+ </RangeCalendarGrid>
141
+ </RangeCalendarRoot>
142
+
143
+ <CalendarRoot
144
+ v-else
145
+ v-slot="{ grid, weekDays }"
146
+ v-model="start"
147
+ v-model:placeholder="placeholder"
148
+ :locale="locale"
149
+ fixed-weeks
150
+ weekday-format="short"
151
+ :calendar-label="t('Choisir une date')"
152
+ >
153
+ <CalendarGrid
154
+ v-for="month in grid"
155
+ :key="month.value.toString()"
156
+ class="w-full border-collapse select-none"
157
+ >
158
+ <CalendarGridHead>
159
+ <CalendarGridRow>
160
+ <CalendarHeadCell
161
+ v-for="day in weekDays"
162
+ :key="day"
163
+ :class="HEAD_CELL_CLASS"
164
+ >
165
+ {{ day }}
166
+ </CalendarHeadCell>
167
+ </CalendarGridRow>
168
+ </CalendarGridHead>
169
+ <CalendarGridBody>
170
+ <CalendarGridRow
171
+ v-for="(week, index) in month.rows"
172
+ :key="`week-${index}`"
173
+ >
174
+ <CalendarCell
175
+ v-for="day in week"
176
+ :key="day.toString()"
177
+ :date="day"
178
+ class="p-0"
179
+ >
180
+ <CalendarCellTrigger
181
+ v-slot="cell"
182
+ :day="day"
183
+ :month="month.value"
184
+ :class="TRIGGER_CLASS"
185
+ >
186
+ <span :class="dayClass(cell)">{{ cell.dayValue }}</span>
187
+ </CalendarCellTrigger>
188
+ </CalendarCell>
189
+ </CalendarGridRow>
190
+ </CalendarGridBody>
191
+ </CalendarGrid>
192
+ </CalendarRoot>
193
+
194
+ <div class="mt-4 flex items-center justify-end gap-2">
195
+ <BrandedButton
196
+ color="tertiary"
197
+ size="2xs"
198
+ type="button"
199
+ keep-margins-even-without-borders
200
+ data-testid="clear-date-filter"
201
+ @click="clear"
202
+ >
203
+ {{ t('Effacer') }}
204
+ </BrandedButton>
205
+ <BrandedButton
206
+ color="primary"
207
+ size="2xs"
208
+ type="submit"
209
+ :disabled="!isComplete"
210
+ >
211
+ {{ t('Appliquer') }}
212
+ </BrandedButton>
213
+ </div>
214
+ </form>
215
+ </template>
216
+
217
+ <script setup lang="ts">
218
+ import { computed, ref, shallowRef, watch } from 'vue'
219
+ import { today, getLocalTimeZone, type DateValue } from '@internationalized/date'
220
+ import {
221
+ CalendarCell,
222
+ CalendarCellTrigger,
223
+ CalendarGrid,
224
+ CalendarGridBody,
225
+ CalendarGridHead,
226
+ CalendarGridRow,
227
+ CalendarHeadCell,
228
+ CalendarRoot,
229
+ RangeCalendarCell,
230
+ RangeCalendarCellTrigger,
231
+ RangeCalendarGrid,
232
+ RangeCalendarGridBody,
233
+ RangeCalendarGridHead,
234
+ RangeCalendarGridRow,
235
+ RangeCalendarHeadCell,
236
+ RangeCalendarRoot,
237
+ } from 'reka-ui'
238
+ import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/vue'
239
+ import { useTranslation } from '../../composables/useTranslation'
240
+ import { parseIsoDate } from '../../functions/tabular'
241
+ import BrandedButton from '../BrandedButton.vue'
242
+ import TabularDateField from './TabularDateField.vue'
243
+ import type { ColumnFilters, DateFilter, DateFilterOperator } from './types'
244
+
245
+ const props = defineProps<{
246
+ column: string
247
+ }>()
248
+
249
+ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default: () => ({}) })
250
+
251
+ const { t, locale } = useTranslation()
252
+
253
+ const operatorOptions = computed<Array<{ value: DateFilterOperator, label: string }>>(() => [
254
+ { value: 'is', label: t('Est') },
255
+ { value: 'before', label: t('Est avant') },
256
+ { value: 'after', label: t('Est après') },
257
+ { value: 'between', label: t('Est entre') },
258
+ ])
259
+
260
+ const HEAD_CELL_CLASS = 'pb-1 text-xs font-normal text-gray-medium'
261
+ const TRIGGER_CLASS = 'group flex h-8 w-full items-center justify-center focus:outline-none'
262
+
263
+ type DayState = {
264
+ selected: boolean
265
+ today: boolean
266
+ outsideView: boolean
267
+ }
268
+
269
+ const DAY_BASE_CLASS = 'flex size-8 items-center justify-center text-sm tabular-nums transition-colors'
270
+
271
+ // One utility per axis, never two: `!important` puts every Tailwind class at the
272
+ // same specificity, so a cell carrying two `text-*` is resolved by the order they
273
+ // happen to be emitted in rather than by the order written here.
274
+ function dayText(cell: DayState): string {
275
+ if (cell.selected) return 'font-medium text-white'
276
+ if (cell.today) return 'font-bold text-new-primary'
277
+ return cell.outsideView ? 'text-gray-low' : 'text-gray-title'
278
+ }
279
+
280
+ function dayClass(cell: DayState): string[] {
281
+ return [
282
+ DAY_BASE_CLASS,
283
+ 'rounded-md',
284
+ dayText(cell),
285
+ cell.selected
286
+ ? 'bg-new-primary'
287
+ : 'group-hover:bg-gray-some group-focus-visible:ring-2 group-focus-visible:ring-new-primary',
288
+ ]
289
+ }
290
+
291
+ // The range background has to be continuous across cells, so it is the day
292
+ // element that fills its cell and only the two ends get rounded corners.
293
+ function rangeDayClass(cell: DayState & { highlighted: boolean, selectionStart: boolean, selectionEnd: boolean }): string[] {
294
+ const isEnd = cell.selectionStart || cell.selectionEnd
295
+ const inRange = cell.highlighted || cell.selected
296
+ return [
297
+ DAY_BASE_CLASS,
298
+ 'w-full',
299
+ // Only the two ends read as selected here: the days in between are carried by
300
+ // the background, not by the text
301
+ dayText({ ...cell, selected: isEnd }),
302
+ isEnd ? 'bg-new-primary' : inRange ? 'bg-new-primary/10' : 'group-hover:bg-gray-some',
303
+ isEnd || !inRange ? 'rounded-md' : '',
304
+ cell.selectionStart ? 'rounded-r-none' : '',
305
+ cell.selectionEnd ? 'rounded-l-none' : '',
306
+ 'group-focus-visible:ring-2 group-focus-visible:ring-new-primary',
307
+ ]
308
+ }
309
+
310
+ const operator = ref<DateFilterOperator>('is')
311
+
312
+ // `shallowRef` rather than `ref`: these dates are immutable values, always
313
+ // replaced whole, and `UnwrapRef` would flatten the class into a plain object
314
+ // type that no longer matches what the calendar components expect.
315
+ const start = shallowRef<DateValue | undefined>()
316
+ const end = shallowRef<DateValue | undefined>()
317
+ // The month the calendar and the fields are looking at.
318
+ const placeholder = shallowRef<DateValue>(today(getLocalTimeZone()))
319
+
320
+ const isRange = computed(() => operator.value === 'between')
321
+
322
+ // `range` bridges the two separate refs the fields bind to and the single
323
+ // object the range calendar expects.
324
+ const range = computed({
325
+ get: () => ({ start: start.value, end: end.value }),
326
+ set: (value) => {
327
+ start.value = value.start
328
+ end.value = value.end
329
+ },
330
+ })
331
+
332
+ const isComplete = computed(() => Boolean(start.value && (!isRange.value || end.value)))
333
+
334
+ const monthNames = computed(() => {
335
+ // `timeZone` matches the instants below: west of UTC, midnight UTC on the 1st
336
+ // falls in the previous month locally, and every name shifts by one
337
+ const formatter = new Intl.DateTimeFormat(locale, { month: 'long', timeZone: 'UTC' })
338
+ return Array.from({ length: 12 }, (_, index) =>
339
+ formatter.format(new Date(Date.UTC(2000, index, 1))))
340
+ })
341
+
342
+ // A century back covers historical datasets; the year of the current view is
343
+ // added so a date typed in the field is always representable in the picker.
344
+ const selectableYears = computed(() => {
345
+ const currentYear = today(getLocalTimeZone()).year
346
+ const years = new Set<number>([placeholder.value.year])
347
+ for (let year = currentYear + 10; year >= currentYear - 100; year--) {
348
+ years.add(year)
349
+ }
350
+ return [...years].sort((a, b) => b - a)
351
+ })
352
+
353
+ function goToMonth(month: number) {
354
+ placeholder.value = placeholder.value.set({ month })
355
+ }
356
+
357
+ function goToYear(year: number) {
358
+ placeholder.value = placeholder.value.set({ year })
359
+ }
360
+
361
+ function shiftMonths(amount: number) {
362
+ placeholder.value = placeholder.value.add({ months: amount })
363
+ }
364
+
365
+ // Reflect the applied filter: the panel stays mounted between openings, and the
366
+ // filter can also be dropped from the active-filter chip or the panel header.
367
+ watch(() => filters.value[props.column]?.date, (dateFilter) => {
368
+ if (!dateFilter) {
369
+ start.value = undefined
370
+ end.value = undefined
371
+ return
372
+ }
373
+ operator.value = dateFilter.operator
374
+ start.value = parseIsoDate(dateFilter.start) ?? undefined
375
+ end.value = parseIsoDate(dateFilter.end) ?? undefined
376
+ if (start.value) {
377
+ placeholder.value = start.value
378
+ }
379
+ }, { immediate: true })
380
+
381
+ function apply() {
382
+ if (!start.value) return
383
+ const dateFilter: DateFilter = {
384
+ operator: operator.value,
385
+ start: start.value.toString(),
386
+ }
387
+ if (isRange.value) {
388
+ if (!end.value) return
389
+ dateFilter.end = end.value.toString()
390
+ }
391
+ const existing = filters.value[props.column] ?? {}
392
+ filters.value = { ...filters.value, [props.column]: { ...existing, date: dateFilter } }
393
+ }
394
+
395
+ function clear() {
396
+ start.value = undefined
397
+ end.value = undefined
398
+ const existing = filters.value[props.column]
399
+ if (!existing?.date) return
400
+ const { date: _date, ...rest } = existing
401
+ filters.value = { ...filters.value, [props.column]: rest }
402
+ }
403
+ </script>