@datagouv/components-next 1.3.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 (131) hide show
  1. package/dist/{Control-b-43UT2B.js → Control-B9WQxlpJ.js} +10 -10
  2. package/dist/{Datafair.client-BrjISlaZ.js → Datafair.client-D_6yzJUG.js} +9 -9
  3. package/dist/{Event-py_AAwJY.js → Event-iXDrp-fy.js} +15 -15
  4. package/dist/{Image-BHQYZWzH.js → Image-mNMBOofs.js} +22 -22
  5. package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
  6. package/dist/{JsonPreview.client-DivFAnbN.js → JsonPreview.client-DSl1kiwC.js} +2 -2
  7. package/dist/{Map-CtP7leHl.js → Map-BfqKMw0S.js} +165 -165
  8. package/dist/{MapContainer.client-Cq8tNLZZ.js → MapContainer.client-BSfpk_rT.js} +17 -17
  9. package/dist/{OSM-gdDSIsDA.js → OSM-CcrUhItr.js} +1 -1
  10. package/dist/{PdfPreview.client-BNBgLsK2.js → PdfPreview.client-BANFeM6K.js} +438 -427
  11. package/dist/{Pmtiles.client-CQi5Rw8W.js → Pmtiles.client-DH9ZrsZR.js} +6177 -5967
  12. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
  13. package/dist/{ScaleLine-hG-FhNqy.js → ScaleLine-VGQ7nkMS.js} +2 -2
  14. package/dist/{Tile-D6rFonpT.js → Tile-CSpn0Ca8.js} +47 -47
  15. package/dist/{TileImage-cfrEgz5y.js → TileImage-CpbaEFwd.js} +44 -44
  16. package/dist/{View-vp7vDb9q.js → View-BYBpt-AU.js} +58 -58
  17. package/dist/{XmlPreview.client-XCsNvnsZ.js → XmlPreview.client-KCB5mwVf.js} +3 -3
  18. package/dist/{common-C8efyyuc.js → common-BoeTUnDZ.js} +58 -58
  19. package/dist/components-next.css +1 -1
  20. package/dist/components-next.js +199 -172
  21. package/dist/components.css +1 -1
  22. package/dist/{index-CXUamc8Z.js → index-DHWzX5vK.js} +4668 -4466
  23. package/dist/{leaflet-src-BRKv9Ksv.js → leaflet-src-pVQfonBI.js} +1 -1
  24. package/dist/{main-DKu5SOAl.js → main-BDcYkhOc.js} +41684 -33432
  25. package/dist/{proj-CsjVeojN.js → proj-DxGbobp7.js} +110 -110
  26. package/dist/{tilecoord-CPcQJnrF.js → tilecoord-DvX7WWiA.js} +94 -94
  27. package/dist/{vue3-xml-viewer.common-BIaEIFYG.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
  28. package/package.json +28 -27
  29. package/src/components/ActivityList/ActivityList.vue +21 -12
  30. package/src/components/AppLink.vue +3 -0
  31. package/src/components/BannerAction.vue +6 -2
  32. package/src/components/Chart/ChartViewer.vue +111 -17
  33. package/src/components/Chart/ChartViewerWrapper.vue +28 -16
  34. package/src/components/ChartCard.vue +112 -0
  35. package/src/components/CopyButton.vue +6 -2
  36. package/src/components/DataserviceCard.vue +23 -6
  37. package/src/components/DataserviceQuality.vue +82 -0
  38. package/src/components/DataserviceQualityInline.vue +31 -0
  39. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  40. package/src/components/DatasetCard.vue +17 -6
  41. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  42. package/src/components/DatasetQuality.vue +11 -11
  43. package/src/components/DatasetQualityInline.vue +2 -2
  44. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  45. package/src/components/DiscussionMessageCard.vue +14 -15
  46. package/src/components/Form/SchemaSelect.vue +2 -0
  47. package/src/components/FormattedDate.vue +69 -0
  48. package/src/components/LoadingBlock.vue +4 -1
  49. package/src/components/ObjectCard.vue +4 -1
  50. package/src/components/ObjectCardOwner.vue +13 -1
  51. package/src/components/Pagination.vue +18 -58
  52. package/src/components/Placeholder.vue +3 -2
  53. package/src/components/PostCard.vue +13 -15
  54. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  55. package/src/components/ResourceAccordion/Downloads.vue +19 -7
  56. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  57. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
  58. package/src/components/ResourceAccordion/Metadata.vue +21 -5
  59. package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
  60. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  61. package/src/components/ResourceAccordion/Pmtiles.client.vue +27 -18
  62. package/src/components/ResourceAccordion/Preview.vue +8 -4
  63. package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
  64. package/src/components/ResourceAccordion/ResourceAccordion.vue +29 -9
  65. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -209
  66. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  67. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  68. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +136 -183
  69. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  70. package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
  71. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
  72. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
  73. package/src/components/ResourceListItem.vue +168 -0
  74. package/src/components/ReuseCard.vue +18 -6
  75. package/src/components/ReuseDetails.vue +2 -2
  76. package/src/components/ReuseHorizontalCard.vue +4 -3
  77. package/src/components/SchemaCard.vue +10 -1
  78. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  79. package/src/components/Search/GlobalSearch.vue +12 -8
  80. package/src/components/Search/SearchInput.vue +1 -1
  81. package/src/components/Search/Sidemenu.vue +4 -3
  82. package/src/components/SmallChart.vue +11 -10
  83. package/src/components/StatBox.vue +13 -5
  84. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  85. package/src/components/TabularExplorer/TabularCell.vue +7 -8
  86. package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
  87. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  88. package/src/components/TabularExplorer/TabularDateField.vue +41 -0
  89. package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
  90. package/src/components/TabularExplorer/TabularExplorer.vue +122 -913
  91. package/src/components/TabularExplorer/TabularFilterContent.vue +33 -25
  92. package/src/components/TabularExplorer/TabularFilterPopover.vue +7 -7
  93. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  94. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  95. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  96. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  97. package/src/components/TabularExplorer/TabularTable.vue +484 -0
  98. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  99. package/src/components/TabularExplorer/types.ts +32 -20
  100. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  101. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  102. package/src/components/TopicCard.vue +15 -14
  103. package/src/composables/useActiveDescendant.ts +5 -0
  104. package/src/composables/useDatasetResources.ts +164 -0
  105. package/src/composables/useHasTabularData.ts +2 -1
  106. package/src/composables/useResizable.ts +46 -0
  107. package/src/composables/useResourceCapabilities.ts +47 -39
  108. package/src/composables/useTranslation.ts +24 -9
  109. package/src/config.ts +1 -0
  110. package/src/functions/charts.ts +32 -1
  111. package/src/functions/dates.ts +26 -7
  112. package/src/functions/helpers.ts +15 -0
  113. package/src/functions/metrics.ts +57 -56
  114. package/src/functions/owned.ts +21 -0
  115. package/src/functions/paginate.ts +46 -0
  116. package/src/functions/resourceCapabilities.ts +34 -0
  117. package/src/functions/resources.ts +83 -0
  118. package/src/functions/schemas.ts +27 -0
  119. package/src/functions/scroll.ts +20 -0
  120. package/src/functions/tabular.ts +119 -4
  121. package/src/functions/tabularApi.ts +1 -43
  122. package/src/main.ts +32 -6
  123. package/src/types/dataservices.ts +15 -0
  124. package/src/types/organizations.ts +10 -0
  125. package/src/types/reuses.ts +12 -1
  126. package/src/types/search.ts +2 -2
  127. package/src/types/visualizations.ts +5 -0
  128. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js +0 -61
  129. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  130. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  131. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -61,8 +61,18 @@
61
61
  </div>
62
62
  </div>
63
63
 
64
+ <!-- Date filter -->
65
+ <TabularDateFilter
66
+ v-if="columnType === 'date'"
67
+ v-model:filters="filters"
68
+ :column="column"
69
+ />
70
+
64
71
  <!-- Search (contains) -->
65
- <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
+ >
66
76
  <div class="relative">
67
77
  <RiSearchLine
68
78
  class="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-gray-medium"
@@ -70,7 +80,7 @@
70
80
  />
71
81
  <input
72
82
  v-model="search"
73
- type="text"
83
+ :type="isNumeric ? 'number' : 'text'"
74
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"
75
85
  :placeholder="t('Rechercher...')"
76
86
  >
@@ -100,14 +110,7 @@
100
110
  />
101
111
  </span>
102
112
  <span class="flex-1 truncate text-left text-xs">
103
- <span
104
- v-if="categoryBadgeStyles?.[top.value]"
105
- class="inline-block rounded font-medium px-2 py-0.5 text-xs"
106
- :style="{ backgroundColor: categoryBadgeStyles![top.value]!.backgroundColor, color: categoryBadgeStyles![top.value]!.color }"
107
- >{{ top.value }}</span>
108
- <template v-else>
109
- {{ top.value ?? 'null' }}
110
- </template>
113
+ {{ top.value ?? 'null' }}
111
114
  </span>
112
115
  <span class="font-mono text-xs text-gray-low tabular-nums shrink-0">{{ top.count }}</span>
113
116
  </button>
@@ -154,14 +157,14 @@
154
157
 
155
158
  <!-- Number range -->
156
159
  <form
157
- v-if="columnType === 'number' && columnProfile"
160
+ v-if="(columnType === 'number' || columnType === 'year') && columnProfile"
158
161
  class="px-3 py-2 border-b border-black/10 space-y-2"
159
162
  @submit.prevent="applyRange"
160
163
  >
161
164
  <div class="flex items-center gap-2 text-xs text-gray-plain">
162
- <span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
165
+ <span class="tabular-nums">{{ columnType === 'year' ? profileMin : formatNumber(profileMin) }}</span>
163
166
  <span class="text-gray-medium">—</span>
164
- <span class="tabular-nums">{{ formatNumber(profileMax) }}</span>
167
+ <span class="tabular-nums">{{ columnType === 'year' ? profileMax : formatNumber(profileMax) }}</span>
165
168
  </div>
166
169
  <div class="flex items-center gap-2">
167
170
  <input
@@ -184,14 +187,7 @@
184
187
  :max="profileMax"
185
188
  >
186
189
  </div>
187
- <div class="flex items-center gap-2">
188
- <BrandedButton
189
- color="primary"
190
- size="2xs"
191
- type="submit"
192
- >
193
- {{ t('Appliquer') }}
194
- </BrandedButton>
190
+ <div class="flex items-center justify-end gap-2">
195
191
  <BrandedButton
196
192
  color="tertiary"
197
193
  size="2xs"
@@ -201,6 +197,13 @@
201
197
  >
202
198
  {{ t('Effacer') }}
203
199
  </BrandedButton>
200
+ <BrandedButton
201
+ color="primary"
202
+ size="2xs"
203
+ type="submit"
204
+ >
205
+ {{ t('Appliquer') }}
206
+ </BrandedButton>
204
207
  </div>
205
208
  </form>
206
209
  </div>
@@ -219,7 +222,8 @@ import { useTranslation } from '../../composables/useTranslation'
219
222
  import { useFormatTabular } from '../../functions/tabular'
220
223
  import BrandedButton from '../BrandedButton.vue'
221
224
  import ProgressBar from '../ProgressBar.vue'
222
- 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'
223
227
 
224
228
  const props = defineProps<{
225
229
  column: string
@@ -227,7 +231,6 @@ const props = defineProps<{
227
231
  columnProfile: TabularColumnProfile | null
228
232
  nullPercent: string
229
233
  totalLines: number
230
- categoryBadgeStyles?: Record<string, BadgeStyle>
231
234
  booleanCounts?: { trueCount: number, falseCount: number }
232
235
  }>()
233
236
 
@@ -239,13 +242,18 @@ const { formatNumber } = useFormatTabular()
239
242
 
240
243
  const search = ref('')
241
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
+
242
249
  watchDebounced(search, (q) => {
243
250
  const existing = filters.value[props.column] ?? {}
251
+ const operator = isNumeric.value ? 'exact' : 'contains'
244
252
  if (q) {
245
- filters.value = { ...filters.value, [props.column]: { ...existing, contains: q } }
253
+ filters.value = { ...filters.value, [props.column]: { ...existing, [operator]: q } }
246
254
  }
247
255
  else {
248
- const { contains: _, ...rest } = existing
256
+ const { [operator]: _, ...rest } = existing
249
257
  filters.value = { ...filters.value, [props.column]: rest }
250
258
  }
251
259
  }, { debounce: 300 })
@@ -5,11 +5,12 @@
5
5
  class="relative shrink-0"
6
6
  >
7
7
  <PopoverButton
8
- class="p-0.5 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
9
- :class="hasColumnFilter ? 'bg-primary text-white' : 'hover:bg-gray-100'"
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
11
  <RiFilterLine
12
- class="size-3.5"
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>
@@ -21,8 +22,9 @@
21
22
  v-show="open"
22
23
  ref="panel"
23
24
  static
24
- class="bg-white border border-black/10 rounded-lg shadow-md w-64 absolute z-[800]"
25
+ class="bg-white border border-black/10 rounded-lg shadow-md w-80 absolute z-[800]"
25
26
  :style="floatingStyles"
27
+ :data-testid="`column-filter-${column}`"
26
28
  >
27
29
  <!-- Title -->
28
30
  <div class="flex items-center justify-between px-3 py-2 border-b border-black/10">
@@ -48,7 +50,6 @@
48
50
  :column-profile="columnProfile"
49
51
  :null-percent="nullPercent"
50
52
  :total-lines="totalLines"
51
- :category-badge-styles="categoryBadgeStyles"
52
53
  :boolean-counts="booleanCounts"
53
54
  />
54
55
  </PopoverPanel>
@@ -67,7 +68,7 @@ import { hasFilterForColumn as _hasFilterForColumn } from '../../functions/tabul
67
68
  import BrandedButton from '../BrandedButton.vue'
68
69
  import ClientOnly from '../ClientOnly.vue'
69
70
  import TabularFilterContent from './TabularFilterContent.vue'
70
- import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig, BadgeStyle } from './types'
71
+ import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig } from './types'
71
72
 
72
73
  const props = defineProps<{
73
74
  column: string
@@ -75,7 +76,6 @@ const props = defineProps<{
75
76
  columnProfile: TabularColumnProfile | null
76
77
  nullPercent: string
77
78
  totalLines: number
78
- categoryBadgeStyles?: Record<string, BadgeStyle>
79
79
  booleanCounts?: { trueCount: number, falseCount: number }
80
80
  }>()
81
81
 
@@ -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>