@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
@@ -26,20 +26,22 @@
26
26
  <!-- Null filter with progress bar -->
27
27
  <div
28
28
  v-if="columnProfile && columnProfile.nb_missing_values > 0"
29
- class="flex items-center gap-2 px-3 py-2 border-b border-black/10"
29
+ class="px-3 py-2 border-b border-black/10 space-y-1.5"
30
30
  >
31
- <span class="text-xs text-gray-plain whitespace-nowrap">
32
- <span class="font-mono tabular-nums">{{ columnProfile.nb_missing_values }}</span>
33
- null
34
- <span class="text-gray-low">({{ nullPercent }})</span>
35
- </span>
36
- <ProgressBar
37
- :value="columnProfile.nb_missing_values"
38
- :max="totalLines"
39
- bar-class="bg-gray-low"
40
- class="flex-1 !h-1.5 !min-w-0 !border-0 !bg-gray-default"
41
- />
42
- <div class="flex items-center gap-0.5">
31
+ <div class="flex items-center gap-2">
32
+ <span class="text-xs text-gray-plain whitespace-nowrap">
33
+ <span class="font-mono tabular-nums">{{ columnProfile.nb_missing_values }}</span>
34
+ null
35
+ <span class="text-gray-low">({{ nullPercent }})</span>
36
+ </span>
37
+ <ProgressBar
38
+ :value="columnProfile.nb_missing_values"
39
+ :max="totalLines"
40
+ bar-class="bg-gray-low"
41
+ class="flex-1 !h-1.5 !min-w-0 !border-0 !bg-gray-default"
42
+ />
43
+ </div>
44
+ <div class="flex items-center justify-end gap-0.5">
43
45
  <BrandedButton
44
46
  :color="nullFilter === 'only' ? 'primary' : 'tertiary'"
45
47
  size="2xs"
@@ -59,8 +61,18 @@
59
61
  </div>
60
62
  </div>
61
63
 
64
+ <!-- Date filter -->
65
+ <TabularDateFilter
66
+ v-if="columnType === 'date'"
67
+ v-model:filters="filters"
68
+ :column="column"
69
+ />
70
+
62
71
  <!-- Search (contains) -->
63
- <div class="px-3 py-2 border-b border-black/10">
72
+ <div
73
+ v-if="columnType !== 'boolean' && columnType !== 'date'"
74
+ class="px-3 py-2 border-b border-black/10"
75
+ >
64
76
  <div class="relative">
65
77
  <RiSearchLine
66
78
  class="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-gray-medium"
@@ -68,7 +80,7 @@
68
80
  />
69
81
  <input
70
82
  v-model="search"
71
- type="text"
83
+ :type="isNumeric ? 'number' : 'text'"
72
84
  class="w-full h-8 text-sm border border-transparent rounded-lg py-1 pl-8 pr-3 bg-[#f3f3f5] focus:outline-none focus:border-new-primary"
73
85
  :placeholder="t('Rechercher...')"
74
86
  >
@@ -98,14 +110,7 @@
98
110
  />
99
111
  </span>
100
112
  <span class="flex-1 truncate text-left text-xs">
101
- <span
102
- v-if="categoryBadgeStyles?.[top.value]"
103
- class="inline-block rounded font-medium px-2 py-0.5 text-xs"
104
- :style="{ backgroundColor: categoryBadgeStyles![top.value]!.backgroundColor, color: categoryBadgeStyles![top.value]!.color }"
105
- >{{ top.value }}</span>
106
- <template v-else>
107
- {{ top.value ?? 'null' }}
108
- </template>
113
+ {{ top.value ?? 'null' }}
109
114
  </span>
110
115
  <span class="font-mono text-xs text-gray-low tabular-nums shrink-0">{{ top.count }}</span>
111
116
  </button>
@@ -152,14 +157,14 @@
152
157
 
153
158
  <!-- Number range -->
154
159
  <form
155
- v-if="columnType === 'number' && columnProfile"
160
+ v-if="(columnType === 'number' || columnType === 'year') && columnProfile"
156
161
  class="px-3 py-2 border-b border-black/10 space-y-2"
157
162
  @submit.prevent="applyRange"
158
163
  >
159
164
  <div class="flex items-center gap-2 text-xs text-gray-plain">
160
- <span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
165
+ <span class="tabular-nums">{{ columnType === 'year' ? profileMin : formatNumber(profileMin) }}</span>
161
166
  <span class="text-gray-medium">—</span>
162
- <span class="tabular-nums">{{ formatNumber(profileMax) }}</span>
167
+ <span class="tabular-nums">{{ columnType === 'year' ? profileMax : formatNumber(profileMax) }}</span>
163
168
  </div>
164
169
  <div class="flex items-center gap-2">
165
170
  <input
@@ -182,14 +187,7 @@
182
187
  :max="profileMax"
183
188
  >
184
189
  </div>
185
- <div class="flex items-center gap-2">
186
- <BrandedButton
187
- color="primary"
188
- size="2xs"
189
- type="submit"
190
- >
191
- {{ t('Appliquer') }}
192
- </BrandedButton>
190
+ <div class="flex items-center justify-end gap-2">
193
191
  <BrandedButton
194
192
  color="tertiary"
195
193
  size="2xs"
@@ -199,6 +197,13 @@
199
197
  >
200
198
  {{ t('Effacer') }}
201
199
  </BrandedButton>
200
+ <BrandedButton
201
+ color="primary"
202
+ size="2xs"
203
+ type="submit"
204
+ >
205
+ {{ t('Appliquer') }}
206
+ </BrandedButton>
202
207
  </div>
203
208
  </form>
204
209
  </div>
@@ -217,7 +222,8 @@ import { useTranslation } from '../../composables/useTranslation'
217
222
  import { useFormatTabular } from '../../functions/tabular'
218
223
  import BrandedButton from '../BrandedButton.vue'
219
224
  import ProgressBar from '../ProgressBar.vue'
220
- import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig, SortDirection, BadgeStyle } from './types'
225
+ import TabularDateFilter from './TabularDateFilter.vue'
226
+ import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig, SortDirection } from './types'
221
227
 
222
228
  const props = defineProps<{
223
229
  column: string
@@ -225,7 +231,6 @@ const props = defineProps<{
225
231
  columnProfile: TabularColumnProfile | null
226
232
  nullPercent: string
227
233
  totalLines: number
228
- categoryBadgeStyles?: Record<string, BadgeStyle>
229
234
  booleanCounts?: { trueCount: number, falseCount: number }
230
235
  }>()
231
236
 
@@ -237,13 +242,18 @@ const { formatNumber } = useFormatTabular()
237
242
 
238
243
  const search = ref('')
239
244
 
245
+ // Numbers and years are matched exactly (the API has no `contains` for them),
246
+ // so the field offers a number input instead of a text search.
247
+ const isNumeric = computed(() => props.columnType === 'number' || props.columnType === 'year')
248
+
240
249
  watchDebounced(search, (q) => {
241
250
  const existing = filters.value[props.column] ?? {}
251
+ const operator = isNumeric.value ? 'exact' : 'contains'
242
252
  if (q) {
243
- filters.value = { ...filters.value, [props.column]: { ...existing, contains: q } }
253
+ filters.value = { ...filters.value, [props.column]: { ...existing, [operator]: q } }
244
254
  }
245
255
  else {
246
- const { contains: _, ...rest } = existing
256
+ const { [operator]: _, ...rest } = existing
247
257
  filters.value = { ...filters.value, [props.column]: rest }
248
258
  }
249
259
  }, { debounce: 300 })
@@ -1,27 +1,30 @@
1
1
  <template>
2
- <div
2
+ <Popover
3
3
  ref="anchor"
4
+ v-slot="{ open }"
4
5
  class="relative shrink-0"
5
6
  >
6
- <button
7
- class="p-0.5 rounded focus:outline-none"
8
- :class="hasColumnFilter ? 'bg-primary text-white' : 'hover:bg-gray-100'"
9
- @click.stop="togglePopover"
7
+ <PopoverButton
8
+ class="flex size-5 items-center justify-center rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
9
+ :class="open ? 'bg-new-primary/10' : 'hover:bg-gray-100'"
10
10
  >
11
- <RiFilter2Line
12
- class="size-3.5"
11
+ <RiFilterLine
12
+ class="size-4"
13
+ :class="open || hasColumnFilter ? 'text-new-primary' : 'text-gray-low'"
13
14
  aria-hidden="true"
14
15
  />
15
16
  <span class="sr-only">{{ t('Filtrer') }} {{ column }}</span>
16
- </button>
17
+ </PopoverButton>
17
18
 
18
19
  <ClientOnly>
19
20
  <Teleport to="#tooltips">
20
- <div
21
- v-show="isOpen"
21
+ <PopoverPanel
22
+ v-show="open"
22
23
  ref="panel"
23
- class="bg-white border border-black/10 rounded-lg shadow-md w-64 absolute z-[800]"
24
+ static
25
+ class="bg-white border border-black/10 rounded-lg shadow-md w-80 absolute z-[800]"
24
26
  :style="floatingStyles"
27
+ :data-testid="`column-filter-${column}`"
25
28
  >
26
29
  <!-- Title -->
27
30
  <div class="flex items-center justify-between px-3 py-2 border-b border-black/10">
@@ -47,26 +50,25 @@
47
50
  :column-profile="columnProfile"
48
51
  :null-percent="nullPercent"
49
52
  :total-lines="totalLines"
50
- :category-badge-styles="categoryBadgeStyles"
51
53
  :boolean-counts="booleanCounts"
52
54
  />
53
- </div>
55
+ </PopoverPanel>
54
56
  </Teleport>
55
57
  </ClientOnly>
56
- </div>
58
+ </Popover>
57
59
  </template>
58
60
 
59
61
  <script setup lang="ts">
60
- import { computed, ref, useTemplateRef } from 'vue'
62
+ import { computed, useTemplateRef } from 'vue'
61
63
  import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
62
- import { onClickOutside } from '@vueuse/core'
63
- import { RiFilter2Line, RiCloseLine } from '@remixicon/vue'
64
+ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
65
+ import { RiFilterLine, RiCloseLine } from '@remixicon/vue'
64
66
  import { useTranslation } from '../../composables/useTranslation'
65
67
  import { hasFilterForColumn as _hasFilterForColumn } from '../../functions/tabular'
66
68
  import BrandedButton from '../BrandedButton.vue'
67
69
  import ClientOnly from '../ClientOnly.vue'
68
70
  import TabularFilterContent from './TabularFilterContent.vue'
69
- import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig, BadgeStyle } from './types'
71
+ import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig } from './types'
70
72
 
71
73
  const props = defineProps<{
72
74
  column: string
@@ -74,7 +76,6 @@ const props = defineProps<{
74
76
  columnProfile: TabularColumnProfile | null
75
77
  nullPercent: string
76
78
  totalLines: number
77
- categoryBadgeStyles?: Record<string, BadgeStyle>
78
79
  booleanCounts?: { trueCount: number, falseCount: number }
79
80
  }>()
80
81
 
@@ -83,18 +84,12 @@ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default:
83
84
 
84
85
  const { t } = useTranslation()
85
86
 
86
- const isOpen = ref(false)
87
-
88
- function togglePopover() {
89
- isOpen.value = !isOpen.value
90
- }
91
-
92
- const anchorRef = useTemplateRef<HTMLElement>('anchor')
93
- const panelRef = useTemplateRef<HTMLElement>('panel')
94
-
95
- onClickOutside(panelRef, () => {
96
- isOpen.value = false
97
- }, { ignore: [anchorRef] })
87
+ // Headless UI Popover manages open state, Escape-to-close, outside-click,
88
+ // focus handling and ARIA. floating-ui only positions the teleported panel.
89
+ const anchorComponent = useTemplateRef<InstanceType<typeof Popover>>('anchor')
90
+ const panelComponent = useTemplateRef<InstanceType<typeof PopoverPanel>>('panel')
91
+ const anchorEl = computed(() => anchorComponent.value?.$el as HTMLElement | undefined)
92
+ const panelEl = computed(() => panelComponent.value?.$el as HTMLElement | undefined)
98
93
 
99
94
  const hasColumnFilter = computed(() => _hasFilterForColumn(filters.value, props.column))
100
95
 
@@ -103,7 +98,7 @@ function clearColumnFilter() {
103
98
  filters.value = rest
104
99
  }
105
100
 
106
- const { floatingStyles } = useFloating(anchorRef, panelRef, {
101
+ const { floatingStyles } = useFloating(anchorEl, panelEl, {
107
102
  placement: 'bottom-start',
108
103
  middleware: [flip(), shift()],
109
104
  whileElementsMounted: autoUpdate,
@@ -0,0 +1,21 @@
1
+ <template>
2
+ <BrandedButton
3
+ color="tertiary"
4
+ size="2xs"
5
+ :icon="RiFilterLine"
6
+ keep-margins-even-without-borders
7
+ @click="mobileFilterOpen = true"
8
+ >
9
+ {{ t('Filtres & tri') }}
10
+ </BrandedButton>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ import { RiFilterLine } from '@remixicon/vue'
15
+ import BrandedButton from '../BrandedButton.vue'
16
+ import { useTranslation } from '../../composables/useTranslation'
17
+ import { useTabularContext } from './useTabularContext'
18
+
19
+ const { t } = useTranslation()
20
+ const { mobileFilterOpen } = useTabularContext()
21
+ </script>
@@ -0,0 +1,150 @@
1
+ <template>
2
+ <TransitionRoot
3
+ :show="mobileFilterOpen"
4
+ as="template"
5
+ >
6
+ <Dialog
7
+ class="relative z-[900]"
8
+ @close="mobileFilterOpen = false"
9
+ >
10
+ <!-- Backdrop -->
11
+ <TransitionChild
12
+ as="template"
13
+ enter="ease-out duration-300"
14
+ enter-from="opacity-0"
15
+ enter-to="opacity-100"
16
+ leave="ease-in duration-200"
17
+ leave-from="opacity-100"
18
+ leave-to="opacity-0"
19
+ >
20
+ <div class="fixed inset-0 bg-black/30" />
21
+ </TransitionChild>
22
+ <!-- Panel -->
23
+ <TransitionChild
24
+ as="template"
25
+ enter="ease-out duration-300"
26
+ enter-from="translate-y-full"
27
+ enter-to="translate-y-0"
28
+ leave="ease-in duration-200"
29
+ leave-from="translate-y-0"
30
+ leave-to="translate-y-full"
31
+ >
32
+ <div class="fixed inset-x-0 bottom-0 max-h-[80vh] flex flex-col bg-white rounded-t-2xl shadow-xl">
33
+ <DialogPanel class="flex flex-col max-h-[80vh]">
34
+ <div class="px-4 pt-4 pb-2 border-b border-gray-default">
35
+ <DialogTitle class="text-sm font-bold">
36
+ {{ t('Filtres & tri par colonne') }}
37
+ </DialogTitle>
38
+ <p class="text-xs text-gray-low">
39
+ {{ allColumns.length }} {{ t('colonnes') }} · {{ activeFilters.length }} {{ t('filtre') }}{{ activeFilters.length !== 1 ? 's' : '' }}
40
+ </p>
41
+ </div>
42
+ <!-- Column list -->
43
+ <div class="flex-1 overflow-y-auto">
44
+ <div
45
+ v-for="col in allColumns"
46
+ :key="col"
47
+ class="border-b border-gray-default last:border-b-0"
48
+ >
49
+ <button
50
+ class="flex items-center gap-2 w-full px-3 py-2.5 text-left"
51
+ :class="hasFilterForColumn(col) ? 'bg-new-primary/5' : ''"
52
+ @click="toggleColumn(col)"
53
+ >
54
+ <component
55
+ :is="getColumnDisplay(col).icon"
56
+ class="size-3.5 text-gray-low shrink-0"
57
+ aria-hidden="true"
58
+ />
59
+ <span class="flex-1 text-sm text-gray-title truncate">{{ col }}</span>
60
+ <RiArrowUpLine
61
+ v-if="sort?.column === col && sort.direction === 'asc'"
62
+ class="size-3 text-new-primary shrink-0"
63
+ aria-hidden="true"
64
+ />
65
+ <RiArrowDownLine
66
+ v-if="sort?.column === col && sort.direction === 'desc'"
67
+ class="size-3 text-new-primary shrink-0"
68
+ aria-hidden="true"
69
+ />
70
+ <span
71
+ v-if="hasFilterForColumn(col)"
72
+ class="size-2 rounded-full bg-new-primary shrink-0"
73
+ />
74
+ <RiArrowDownSLine
75
+ class="size-3.5 text-gray-low shrink-0 transition-transform"
76
+ :class="{ 'rotate-180': expandedCol === col }"
77
+ aria-hidden="true"
78
+ />
79
+ </button>
80
+ <div
81
+ v-if="expandedCol === col"
82
+ class="pb-1"
83
+ >
84
+ <TabularFilterContent
85
+ v-model:sort="sort"
86
+ v-model:filters="filters"
87
+ :column="col"
88
+ :column-type="getColumnType(col)"
89
+ :column-profile="getColumnProfile(col)"
90
+ :null-percent="getNullPercent(col)"
91
+ :total-lines="totalLines"
92
+ :boolean-counts="getColumnType(col) === 'boolean' ? getBooleanCounts(col) : undefined"
93
+ />
94
+ </div>
95
+ </div>
96
+ </div>
97
+ <!-- Reset all -->
98
+ <div
99
+ v-if="activeFilters.length > 0 || sort"
100
+ class="border-t border-gray-default px-4 py-3"
101
+ >
102
+ <BrandedButton
103
+ color="secondary"
104
+ size="xs"
105
+ class="w-full"
106
+ @click="clearAllFilters(); sort = null; mobileFilterOpen = false"
107
+ >
108
+ {{ t('Tout réinitialiser') }}
109
+ </BrandedButton>
110
+ </div>
111
+ </DialogPanel>
112
+ </div>
113
+ </TransitionChild>
114
+ </Dialog>
115
+ </TransitionRoot>
116
+ </template>
117
+
118
+ <script setup lang="ts">
119
+ import { ref } from 'vue'
120
+ import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
121
+ import { RiArrowDownLine, RiArrowDownSLine, RiArrowUpLine } from '@remixicon/vue'
122
+ import BrandedButton from '../BrandedButton.vue'
123
+ import TabularFilterContent from './TabularFilterContent.vue'
124
+ import { useTranslation } from '../../composables/useTranslation'
125
+ import { useTabularContext } from './useTabularContext'
126
+
127
+ const { t } = useTranslation()
128
+ const {
129
+ mobileFilterOpen,
130
+ allColumns,
131
+ activeFilters,
132
+ sort,
133
+ filters,
134
+ hasFilterForColumn,
135
+ clearAllFilters,
136
+ totalLines,
137
+ getColumnDisplay,
138
+ getColumnType,
139
+ getColumnProfile,
140
+ getNullPercent,
141
+ getBooleanCounts,
142
+ } = useTabularContext()
143
+
144
+ // The expanded column is local to the sheet.
145
+ const expandedCol = ref<string | null>(null)
146
+
147
+ function toggleColumn(col: string) {
148
+ expandedCol.value = expandedCol.value === col ? null : col
149
+ }
150
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <span class="flex items-center gap-1.5 text-xs text-gray-plain">
3
+ <RiLoader5Line
4
+ v-if="isRefreshing"
5
+ class="size-3 text-new-primary animate-spin"
6
+ aria-hidden="true"
7
+ />
8
+ <RiLayoutRowLine
9
+ v-else
10
+ class="size-3 text-mention-grey"
11
+ aria-hidden="true"
12
+ />
13
+ <span class="font-bold hidden md:inline">{{ t('Lignes') }}</span>
14
+ <!-- The count stays in place while refreshing: the spinner already says it is
15
+ stale, and swapping it for a word of another width would shift the whole
16
+ toolbar on every search. -->
17
+ <span
18
+ data-testid="row-count"
19
+ class="font-mono tabular-nums"
20
+ >{{ (tableData?.meta.total ?? 0).toLocaleString() }}/{{ totalLines.toLocaleString() }}</span>
21
+ </span>
22
+ </template>
23
+
24
+ <script setup lang="ts">
25
+ import { RiLayoutRowLine, RiLoader5Line } from '@remixicon/vue'
26
+ import { useTranslation } from '../../composables/useTranslation'
27
+ import { useTabularContext } from './useTabularContext'
28
+
29
+ const { t } = useTranslation()
30
+ const { tableData, totalLines, isRefreshing } = useTabularContext()
31
+ </script>
@@ -0,0 +1,92 @@
1
+ <template>
2
+ <!-- Toolbar row — mirrors TabularToolbar: the mobile filter button on the left (it
3
+ only shows below md), and the columns menu (h-6 button, which drives the row
4
+ height) plus the rows info on the right. The active-filters area stays empty
5
+ until the user filters, so nothing stands in for it. -->
6
+ <div class="animate-pulse-placeholder flex shrink-0 items-center gap-2 border-b border-gray-default p-2">
7
+ <div class="h-6 w-24 rounded bg-gray-200 md:hidden" />
8
+ <div class="flex flex-1 items-center justify-end gap-4">
9
+ <div class="h-6 w-24 rounded bg-gray-200" />
10
+ <div class="h-5 w-24 rounded bg-gray-200" />
11
+ </div>
12
+ </div>
13
+
14
+ <!-- Table — mirrors TabularTable: tall header cells (name + type line) then rows.
15
+ Desktop only, like the real one. -->
16
+ <div
17
+ class="animate-pulse-placeholder hidden overflow-hidden md:block"
18
+ :class="fill ? 'min-h-0 flex-1' : 'max-h-[70vh]'"
19
+ role="status"
20
+ :aria-label="t('Chargement de l\'aperçu…')"
21
+ >
22
+ <table class="w-full table-fixed text-sm border-collapse">
23
+ <thead class="bg-gray-some shadow-[inset_0_-1px_0_0_#E5E5E5]">
24
+ <tr class="border-b border-gray-default">
25
+ <th
26
+ v-for="i in 6"
27
+ :key="i"
28
+ class="h-12 px-3 text-left align-middle border-r border-gray-default last:border-r-0"
29
+ >
30
+ <div class="flex items-center justify-between gap-1">
31
+ <div class="flex items-center gap-1">
32
+ <div class="size-4 shrink-0 rounded bg-gray-200" />
33
+ <div class="h-3.5 w-20 rounded bg-gray-200" />
34
+ </div>
35
+ <div class="size-4 shrink-0 rounded bg-gray-200" />
36
+ </div>
37
+ </th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <tr
42
+ v-for="row in 16"
43
+ :key="row"
44
+ class="h-10 border-b border-gray-default even:bg-gray-lowest-2"
45
+ >
46
+ <td
47
+ v-for="col in 6"
48
+ :key="col"
49
+ class="px-2 align-middle border-r border-gray-default last:border-r-0"
50
+ >
51
+ <div class="h-3.5 w-3/4 rounded bg-gray-200" />
52
+ </td>
53
+ </tr>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+
58
+ <!-- Mobile — the real table becomes a list of cards below md, each showing four
59
+ fields until it is expanded. -->
60
+ <div
61
+ class="animate-pulse-placeholder space-y-2 px-1 md:hidden"
62
+ role="status"
63
+ :aria-label="t('Chargement de l\'aperçu…')"
64
+ >
65
+ <div
66
+ v-for="card in 4"
67
+ :key="card"
68
+ class="space-y-2 rounded-lg border border-gray-default p-3"
69
+ :class="card % 2 === 0 ? 'bg-gray-lowest-2' : 'bg-white'"
70
+ >
71
+ <div
72
+ v-for="field in 4"
73
+ :key="field"
74
+ class="flex flex-col gap-0.5"
75
+ >
76
+ <div class="h-3 w-24 rounded bg-gray-200" />
77
+ <div class="h-4 w-2/3 rounded bg-gray-200" />
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import { useTranslation } from '../../composables/useTranslation'
85
+
86
+ defineProps<{
87
+ // Fill the available height (fullscreen) instead of the default capped height.
88
+ fill?: boolean
89
+ }>()
90
+
91
+ const { t } = useTranslation()
92
+ </script>