@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
@@ -0,0 +1,240 @@
1
+ <template>
2
+ <!-- A resource that is only a link is visited, not downloaded. -->
3
+ <BrandedButton
4
+ v-if="isResourceUrl"
5
+ :href="resource.latest"
6
+ :title="t('Lien du fichier - ouvre une nouvelle fenêtre')"
7
+ rel="ugc nofollow noopener"
8
+ new-tab
9
+ size="xs"
10
+ external
11
+ @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
12
+ >
13
+ {{ t('Visiter') }}
14
+ </BrandedButton>
15
+
16
+ <!-- An OGC endpoint with nothing to export: its URL is the only useful thing to
17
+ hand over, to paste into a GIS. -->
18
+ <BrandedButton
19
+ v-else-if="!groups.length"
20
+ :icon="RiFileCopyLine"
21
+ color="primary"
22
+ size="xs"
23
+ @click="copyServiceUrl"
24
+ >
25
+ {{ t('Copier le lien') }}
26
+ </BrandedButton>
27
+
28
+ <!-- A single downloadable format needs no menu: a plain download button links
29
+ straight to it, matching the previous behaviour. -->
30
+ <BrandedButton
31
+ v-else-if="singleDownload"
32
+ :href="singleDownload.url"
33
+ :icon="RiDownloadLine"
34
+ rel="ugc nofollow noopener"
35
+ download
36
+ external
37
+ class="matomo_download"
38
+ size="xs"
39
+ color="primary"
40
+ @click="trackDownload(singleDownload.format)"
41
+ >
42
+ {{ t('Télécharger') }}
43
+ </BrandedButton>
44
+
45
+ <Popover
46
+ v-else
47
+ ref="anchor"
48
+ class="relative"
49
+ >
50
+ <PopoverButton
51
+ :as="BrandedButton"
52
+ :icon="RiDownloadLine"
53
+ size="xs"
54
+ color="primary"
55
+ aria-haspopup="menu"
56
+ >
57
+ {{ t('Télécharger') }}
58
+ </PopoverButton>
59
+
60
+ <ClientOnly>
61
+ <Teleport to="#tooltips">
62
+ <PopoverPanel
63
+ v-slot="{ close }"
64
+ ref="panel"
65
+ class="absolute z-[800] w-72 overflow-hidden rounded border border-gray-default bg-white shadow-lg"
66
+ :style="floatingStyles"
67
+ >
68
+ <div
69
+ v-for="group in groups"
70
+ :key="group.title"
71
+ >
72
+ <div class="border-b border-gray-default bg-gray-some px-2 py-1.5 text-[12px] font-bold uppercase leading-tight text-gray-title">
73
+ {{ group.title }}
74
+ </div>
75
+ <div
76
+ v-for="item in group.items"
77
+ :key="item.url"
78
+ class="group/dl flex items-center border-b border-gray-default last:border-b-0 hover:bg-gray-some"
79
+ >
80
+ <a
81
+ :href="item.url"
82
+ class="matomo_download flex min-w-0 flex-1 items-center gap-2 py-2 pl-2 pr-1 text-[14px] leading-tight text-gray-title !bg-none !no-underline"
83
+ rel="ugc nofollow noopener"
84
+ download
85
+ @click="trackDownload(item.format); close()"
86
+ >
87
+ <span class="min-w-0 flex-1 truncate">{{ t('Format {format}', { format: item.format }) }}</span>
88
+ <span
89
+ v-if="item.size"
90
+ class="shrink-0 rounded bg-gray-lower px-1 text-[13px] tabular-nums text-gray-medium"
91
+ >{{ filesize(item.size) }}</span>
92
+ </a>
93
+ <CopyButton
94
+ :label="t('Copier le lien')"
95
+ :copied-label="t('Lien copié !')"
96
+ :text="item.url"
97
+ icon-only
98
+ class="mr-1.5 shrink-0 opacity-0 transition-opacity focus-visible:opacity-100 group-hover/dl:opacity-100"
99
+ />
100
+ </div>
101
+ </div>
102
+
103
+ <!-- The exports above are files; the service itself is consumed live by a GIS,
104
+ so its URL stays reachable next to them. -->
105
+ <div v-if="ogcService">
106
+ <div class="border-y border-gray-default bg-gray-some px-2 py-1.5 text-[12px] font-bold uppercase leading-tight text-gray-title">
107
+ {{ t('Service') }}
108
+ </div>
109
+ <button
110
+ type="button"
111
+ class="flex w-full items-center gap-2 px-2 py-2 text-left text-[14px] leading-tight text-gray-title hover:bg-gray-some"
112
+ @click="copyServiceUrl(); close()"
113
+ >
114
+ <RiFileCopyLine
115
+ class="size-4 shrink-0 text-gray-medium"
116
+ aria-hidden="true"
117
+ />
118
+ {{ t('Copier le lien du service') }}
119
+ </button>
120
+ </div>
121
+ </PopoverPanel>
122
+ </Teleport>
123
+ </ClientOnly>
124
+ </Popover>
125
+ </template>
126
+
127
+ <script setup lang="ts">
128
+ import { computed, useTemplateRef } from 'vue'
129
+ import { autoUpdate, flip, shift, useFloating } from '@floating-ui/vue'
130
+ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
131
+ import { RiDownloadLine, RiFileCopyLine } from '@remixicon/vue'
132
+ import { toast } from 'vue-sonner'
133
+ import BrandedButton from '../BrandedButton.vue'
134
+ import CopyButton from '../CopyButton.vue'
135
+ import ClientOnly from '../ClientOnly.vue'
136
+ import { filesize } from '../../functions/helpers'
137
+ import { getResourceFilesize } from '../../functions/resources'
138
+ import { trackEvent } from '../../functions/matomo'
139
+ import { useTranslation } from '../../composables/useTranslation'
140
+ import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
141
+ import type { Resource } from '../../types/resources'
142
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
143
+
144
+ type DownloadItem = { url: string, format: string, size?: number }
145
+ type DownloadGroup = { title: string, items: DownloadItem[] }
146
+
147
+ // The one action a resource offers, whatever its kind: the two explorer headers mount
148
+ // this so the triage lives in a single place instead of drifting between them.
149
+ const props = defineProps<{
150
+ resource: Resource
151
+ dataset: Dataset | DatasetV2
152
+ }>()
153
+
154
+ const { t } = useTranslation()
155
+
156
+ const {
157
+ generatedFormats,
158
+ wfsFormats,
159
+ defaultWfsProjection,
160
+ isResourceUrl,
161
+ ogcService,
162
+ } = useResourceCapabilities(() => props.resource, () => props.dataset)
163
+
164
+ const groups = computed<DownloadGroup[]>(() => {
165
+ const result: DownloadGroup[] = []
166
+
167
+ // An OGC endpoint is not a file: `latest` points at its GetCapabilities, which is
168
+ // nothing a visitor wants to download. Only its exports below are.
169
+ if (!ogcService.value) {
170
+ result.push({
171
+ title: t('Format original'),
172
+ items: [{
173
+ url: props.resource.latest,
174
+ format: props.resource.format,
175
+ size: getResourceFilesize(props.resource) ?? undefined,
176
+ }],
177
+ })
178
+ }
179
+
180
+ if (generatedFormats.value.length) {
181
+ result.push({
182
+ title: t('Formats générés automatiquement'),
183
+ items: generatedFormats.value.map(format => ({
184
+ url: format.url,
185
+ format: format.format,
186
+ size: format.size,
187
+ })),
188
+ })
189
+ }
190
+
191
+ if (wfsFormats.value.length) {
192
+ const projection = defaultWfsProjection.value
193
+ result.push({
194
+ title: projection
195
+ ? t('Formats exportés depuis le service WFS (projection {crs})', { crs: projection })
196
+ : t('Formats exportés depuis le service WFS'),
197
+ items: wfsFormats.value.map(format => ({
198
+ url: format.url,
199
+ format: format.format,
200
+ })),
201
+ })
202
+ }
203
+
204
+ return result
205
+ })
206
+
207
+ // A single downloadable format needs no menu: expose it directly for the plain button.
208
+ // Never for an OGC service, whose menu also carries the link to the service itself.
209
+ const singleDownload = computed<DownloadItem | null>(() => {
210
+ if (ogcService.value) return null
211
+ if (groups.value.length !== 1) return null
212
+ const items = groups.value[0]!.items
213
+ return items.length === 1 ? items[0]! : null
214
+ })
215
+
216
+ const trackDownload = (format: string) => {
217
+ trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${format}`)
218
+ }
219
+
220
+ const copyServiceUrl = async () => {
221
+ try {
222
+ await navigator.clipboard.writeText(props.resource.url)
223
+ toast.success(t('Lien copié !'))
224
+ }
225
+ catch {
226
+ toast.error(t('Impossible de copier dans le presse-papier'))
227
+ }
228
+ }
229
+
230
+ // The panel is teleported, so it is positioned against its trigger with floating-ui.
231
+ const anchor = useTemplateRef<InstanceType<typeof Popover>>('anchor')
232
+ const panel = useTemplateRef<InstanceType<typeof PopoverPanel>>('panel')
233
+ const anchorEl = computed(() => anchor.value?.$el as HTMLElement | undefined)
234
+ const panelEl = computed(() => panel.value?.$el as HTMLElement | undefined)
235
+ const { floatingStyles } = useFloating(anchorEl, panelEl, {
236
+ placement: 'bottom-end',
237
+ middleware: [flip(), shift({ padding: 8 })],
238
+ whileElementsMounted: autoUpdate,
239
+ })
240
+ </script>
@@ -3,111 +3,49 @@
3
3
  v-slot="{ open, close }"
4
4
  class="relative inline-block"
5
5
  >
6
- <slot
7
- name="trigger"
8
- :open="open"
6
+ <PopoverButton
7
+ class="inline-flex items-center justify-center size-6 rounded text-gray-plain hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
8
+ :aria-label="t('Choisir une autre ressource')"
9
9
  >
10
- <PopoverButton
11
- class="inline-flex items-center justify-center size-6 rounded text-gray-plain hover:bg-gray-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
12
- :aria-label="t('Choisir une autre ressource')"
13
- >
14
- <RiArrowDownSLine
15
- class="size-4"
16
- :class="{ 'rotate-180': open }"
17
- aria-hidden="true"
18
- />
19
- </PopoverButton>
20
- </slot>
21
- <PopoverPanel
22
- :class="searchable
23
- ? 'absolute left-0 top-full z-50 mt-2 w-96 max-w-[calc(100vw-2rem)] bg-white border border-gray-default rounded shadow-lg p-3 space-y-2'
24
- : 'absolute left-0 top-full z-50 mt-1 w-80 max-h-96 overflow-auto bg-white border border-gray-default rounded shadow-lg p-1'"
25
- >
26
- <input
27
- v-if="searchable"
28
- v-model="searchQuery"
29
- type="search"
30
- class="w-full border border-gray-default rounded px-2.5 py-1.5 text-sm"
31
- :placeholder="t('Rechercher dans les ressources…')"
32
- >
33
- <ul
34
- v-if="filteredResources.length > 0"
35
- class="list-none p-0 m-0 space-y-0.5 max-h-80 overflow-y-auto"
36
- >
10
+ <RiArrowDownSLine
11
+ class="size-4"
12
+ :class="{ 'rotate-180': open }"
13
+ aria-hidden="true"
14
+ />
15
+ </PopoverButton>
16
+ <PopoverPanel class="absolute left-0 top-full z-50 mt-1 w-80 max-h-96 overflow-auto bg-white border border-gray-default rounded shadow-lg p-1">
17
+ <ul class="list-none p-0 m-0 space-y-0.5 max-h-80 overflow-y-auto">
37
18
  <li
38
- v-for="r in filteredResources"
19
+ v-for="r in resources"
39
20
  :key="r.id"
40
21
  >
41
- <button
42
- v-if="!isDisabled?.(r)"
43
- type="button"
44
- class="flex items-center gap-1.5 w-full text-left px-2 py-1.5 rounded text-sm hover:bg-gray-100 focus:outline-none focus-visible:bg-gray-100"
45
- :class="{ 'font-bold bg-blue-50 text-new-primary': r.id === selectedId }"
46
- @click="emit('select', r); close()"
47
- >
48
- <ResourceIcon
49
- :resource="r"
50
- class="size-3.5 shrink-0"
51
- />
52
- <span class="truncate">{{ r.title || t('Fichier sans nom') }}</span>
53
- <span
54
- v-if="r.format"
55
- class="ml-auto text-xs text-gray-medium uppercase shrink-0"
56
- >
57
- {{ r.format }}
58
- </span>
59
- </button>
60
- <div
61
- v-else
62
- class="flex items-center gap-1.5 px-2 py-1.5 rounded text-sm text-gray-medium cursor-not-allowed"
63
- :title="disabledTitle"
64
- >
65
- <ResourceIcon
66
- :resource="r"
67
- class="size-3.5 shrink-0 opacity-50"
68
- />
69
- <span class="truncate opacity-70">{{ r.title || t('Fichier sans nom') }}</span>
70
- </div>
22
+ <ResourceListItem
23
+ :resource="r"
24
+ :to="resourceTo(r)"
25
+ :replace
26
+ :selected="r.id === selectedId"
27
+ @click="close()"
28
+ />
71
29
  </li>
72
30
  </ul>
73
- <p
74
- v-else
75
- class="text-sm text-gray-medium italic mb-0 px-2 py-2"
76
- >
77
- {{ t('Aucune ressource correspondante') }}
78
- </p>
79
31
  </PopoverPanel>
80
32
  </Popover>
81
33
  </template>
82
34
 
83
35
  <script setup lang="ts">
84
- import { computed, ref } from 'vue'
36
+ import type { RouteLocationRaw } from 'vue-router'
85
37
  import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
86
38
  import { RiArrowDownSLine } from '@remixicon/vue'
87
39
  import { useTranslation } from '../../composables/useTranslation'
88
- import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
40
+ import ResourceListItem from '../ResourceListItem.vue'
89
41
  import type { Resource } from '../../types/resources'
90
42
 
91
- const props = defineProps<{
43
+ defineProps<{
92
44
  resources: Resource[]
93
45
  selectedId: string
94
- searchable?: boolean
95
- isDisabled?: (resource: Resource) => boolean
96
- disabledTitle?: string
97
- }>()
98
-
99
- const emit = defineEmits<{
100
- select: [resource: Resource]
46
+ resourceTo: (resource: Resource) => RouteLocationRaw
47
+ replace?: boolean
101
48
  }>()
102
49
 
103
50
  const { t } = useTranslation()
104
-
105
- const searchQuery = ref('')
106
-
107
- const filteredResources = computed(() => {
108
- if (!props.searchable) return props.resources
109
- const q = searchQuery.value.trim().toLowerCase()
110
- if (!q) return props.resources
111
- return props.resources.filter(r => (r.title ?? '').toLowerCase().includes(q))
112
- })
113
51
  </script>
@@ -0,0 +1,124 @@
1
+ <template>
2
+ <header class="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-gray-default bg-gray-some px-3">
3
+ <div class="flex min-w-0 items-center gap-1.5 overflow-hidden text-[13px] text-gray-medium">
4
+ <ResourceIcon
5
+ :resource
6
+ class="size-4 shrink-0"
7
+ />
8
+ <span
9
+ class="min-w-0 truncate font-medium text-gray-title"
10
+ :title="resource.title || t('Fichier sans nom')"
11
+ >{{ resource.title || t('Fichier sans nom') }}</span>
12
+ <ResourceSelector
13
+ v-if="resources && resources.length > 1 && resourceTo"
14
+ :resources
15
+ :selected-id="resource.id"
16
+ :resource-to
17
+ :replace
18
+ class="shrink-0 md:hidden"
19
+ />
20
+ <!-- Metadata gives way long before the title does (hence the shrink factor), so
21
+ the resource name stays readable — and the title truncates rather than being
22
+ clipped by the row, which would push the copy button out of view.
23
+ Inline flow (not flex) so text-overflow renders the ellipsis; spacing is
24
+ carried by the separators' margins. -->
25
+ <div class="min-w-0 truncate [flex-shrink:9999]">
26
+ <span class="mr-1.5">·</span>
27
+ <TranslationT keypath="mis à jour {date}">
28
+ <template #date>
29
+ <FormattedDate
30
+ :date="resource.last_modified"
31
+ format="relative"
32
+ />
33
+ </template>
34
+ </TranslationT>
35
+ <template v-if="resourceFilesize">
36
+ <span class="mx-1.5">·</span>
37
+ <span>{{ filesize(resourceFilesize) }}</span>
38
+ </template>
39
+ <template v-if="resource.format">
40
+ <span class="mx-1.5">·</span>
41
+ <span class="rounded bg-gray-lower px-1.5 py-0.5 text-[12px] uppercase leading-4 text-gray-medium">{{ resource.format }}</span>
42
+ </template>
43
+ <template v-if="resource.schema?.name || resource.schema?.url">
44
+ <span class="mx-1.5">·</span>
45
+ <SchemaBadge :resource />
46
+ </template>
47
+ <span class="mx-1.5">·</span>
48
+ <span class="inline-flex items-center gap-0.5 align-middle">
49
+ <RiDownloadLine class="size-3" />
50
+ {{ summarize(resource.metrics.views) }}
51
+ </span>
52
+ </div>
53
+ <CopyButton
54
+ :label="t('Copier le lien')"
55
+ :copied-label="t('Lien copié !')"
56
+ :text="externalUrl"
57
+ icon-only
58
+ class="hidden shrink-0 md:inline-flex"
59
+ />
60
+ </div>
61
+ <div
62
+ v-if="!fullscreen"
63
+ class="flex shrink-0 items-center gap-2"
64
+ >
65
+ <ResourceMainAction
66
+ :resource="resource"
67
+ :dataset="dataset"
68
+ />
69
+ <BrandedButton
70
+ v-if="exploreTo"
71
+ :href="exploreTo(resource)"
72
+ :icon="RiFullscreenLine"
73
+ icon-only
74
+ color="secondary"
75
+ size="xs"
76
+ :title="t('Explorer les données')"
77
+ @click="trackEvent('Jeux de données', 'Explorer les données', 'Bouton : explorer les données')"
78
+ >
79
+ {{ t('Explorer les données') }}
80
+ </BrandedButton>
81
+ </div>
82
+ </header>
83
+ </template>
84
+
85
+ <script setup lang="ts">
86
+ import { computed } from 'vue'
87
+ import { RiDownloadLine, RiFullscreenLine } from '@remixicon/vue'
88
+ import BrandedButton from '../BrandedButton.vue'
89
+ import CopyButton from '../CopyButton.vue'
90
+ import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
91
+ import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
92
+ import ResourceSelector from './ResourceSelector.vue'
93
+ import ResourceMainAction from './ResourceMainAction.vue'
94
+ import FormattedDate from '../FormattedDate.vue'
95
+ import TranslationT from '../TranslationT.vue'
96
+ import { filesize, summarize } from '../../functions/helpers'
97
+ import { getResourceFilesize, resolveResourceExternalUrl } from '../../functions/resources'
98
+ import { trackEvent } from '../../functions/matomo'
99
+ import { useTranslation } from '../../composables/useTranslation'
100
+ import type { RouteLocationRaw } from 'vue-router'
101
+ import type { Resource } from '../../types/resources'
102
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
103
+
104
+ // Shared by the viewer and its loading skeleton: everything here comes from the
105
+ // resource itself, so the skeleton renders the real header instead of pulsing
106
+ // placeholders over data it already holds.
107
+ const props = defineProps<{
108
+ dataset: Dataset | DatasetV2
109
+ resource: Resource
110
+ resources?: Resource[]
111
+ resourceTo?: (resource: Resource) => RouteLocationRaw
112
+ exploreTo?: (resource: Resource) => string
113
+ // Overrides the "Copier le lien" target.
114
+ resourceExternalUrl?: (resource: Resource) => string
115
+ replace?: boolean
116
+ // Fullscreen mode hides the inline actions — they live in the dataset context bar above.
117
+ fullscreen?: boolean
118
+ }>()
119
+
120
+ const { t } = useTranslation()
121
+
122
+ const resourceFilesize = computed(() => getResourceFilesize(props.resource))
123
+ const externalUrl = computed(() => resolveResourceExternalUrl(props.dataset, props.resource, props.resourceExternalUrl))
124
+ </script>
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <div :class="fullscreen ? 'flex min-h-0 flex-1 flex-col' : ''">
3
+ <!-- Header and tabs are the real ones: everything they show comes from the
4
+ resource we already hold, so there is nothing to wait for. Only the preview
5
+ body below is a placeholder. -->
6
+ <ResourceViewerHeader
7
+ :dataset
8
+ :resource
9
+ :resources
10
+ :resource-to
11
+ :explore-to="exploreTo"
12
+ :resource-external-url="resourceExternalUrl"
13
+ :replace
14
+ :fullscreen
15
+ />
16
+
17
+ <!-- Inert copy of the viewer's tab row: same components, so the pills keep their
18
+ exact size and labels, but nothing to select until the viewer resolves. -->
19
+ <TabGroup
20
+ size="sm"
21
+ aria-hidden="true"
22
+ class="pointer-events-none"
23
+ >
24
+ <div class="flex shrink-0 items-center border-b border-gray-default p-2">
25
+ <TabList class="max-w-full overflow-x-auto">
26
+ <Tab
27
+ v-for="tab in tabsOptions"
28
+ :key="tab.key"
29
+ >
30
+ {{ tab.label }}
31
+ </Tab>
32
+ </TabList>
33
+ </div>
34
+ </TabGroup>
35
+
36
+ <!-- Tabular preview: the same toolbar + table skeleton the data tab shows while
37
+ TabularExplorer loads. Other previews fall back below. -->
38
+ <TabularSkeleton
39
+ v-if="previewKind === 'tabular'"
40
+ :fill="fullscreen"
41
+ />
42
+
43
+ <!-- PDF preview: the same A4 skeleton the PdfPreview shows while pdfjs loads. -->
44
+ <PdfSkeleton
45
+ v-else-if="previewKind === 'pdf'"
46
+ :fill="fullscreen"
47
+ />
48
+
49
+ <!-- Other non-tabular previews (JSON, map…) — a simple placeholder for now. -->
50
+ <div
51
+ v-else
52
+ class="animate-pulse-placeholder p-4"
53
+ :class="fullscreen ? 'min-h-0 flex-1' : ''"
54
+ role="status"
55
+ :aria-label="t('Chargement de l\'aperçu…')"
56
+ >
57
+ <div
58
+ class="w-full rounded bg-gray-200"
59
+ :class="fullscreen ? 'h-full' : 'h-96'"
60
+ />
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <script setup lang="ts">
66
+ import PdfSkeleton from '../ResourceAccordion/PdfSkeleton.vue'
67
+ import TabularSkeleton from '../TabularExplorer/TabularSkeleton.vue'
68
+ import TabGroup from '../Tabs/TabGroup.vue'
69
+ import TabList from '../Tabs/TabList.vue'
70
+ import Tab from '../Tabs/Tab.vue'
71
+ import ResourceViewerHeader from './ResourceViewerHeader.vue'
72
+ import { useTranslation } from '../../composables/useTranslation'
73
+ import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
74
+ import type { RouteLocationRaw } from 'vue-router'
75
+ import type { Resource } from '../../types/resources'
76
+ import type { Dataset, DatasetV2 } from '../../types/datasets'
77
+
78
+ const props = defineProps<{
79
+ resource: Resource
80
+ dataset: Dataset | DatasetV2
81
+ resources?: Resource[]
82
+ resourceTo?: (resource: Resource) => RouteLocationRaw
83
+ exploreTo?: (resource: Resource) => string
84
+ resourceExternalUrl?: (resource: Resource) => string
85
+ replace?: boolean
86
+ fullscreen?: boolean
87
+ }>()
88
+
89
+ const { t } = useTranslation()
90
+ // `previewKind` is the viewer's own decision, so the skeleton always picks the
91
+ // shape the resolved viewer will show.
92
+ const { previewKind, tabsOptions } = useResourceCapabilities(
93
+ () => props.resource,
94
+ () => props.dataset,
95
+ )
96
+ </script>