@datagouv/components-next 1.0.2-dev.10 → 1.0.2-dev.101

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 (108) hide show
  1. package/assets/main.css +4 -0
  2. package/dist/{Control-DuZJdKV_.js → Control-ZFh5ta_U.js} +1 -1
  3. package/dist/Datafair.client-DPUnHwTy.js +30 -0
  4. package/dist/{Event--kp8kMdJ.js → Event-DSQcW7OF.js} +24 -24
  5. package/dist/{Image-34hvypZI.js → Image-BijNEG0p.js} +6 -6
  6. package/dist/JsonPreview.client-Dnw5AT0O.js +40 -0
  7. package/dist/{Map-BjUnLyj8.js → Map-BUtPf5GN.js} +756 -756
  8. package/dist/MapContainer.client-DLueJ6cI.js +101 -0
  9. package/dist/{OSM-s40W6sQ2.js → OSM-D4MTdBtk.js} +2 -2
  10. package/dist/{PdfPreview.client-BVjPxlPu.js → PdfPreview.client-BqnobDUu.js} +822 -865
  11. package/dist/{Pmtiles.client-CRJ56yX2.js → Pmtiles.client-CSNW39JA.js} +574 -579
  12. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BZ-QqWt1.js +61 -0
  13. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-hJQIqcZm.js} +2 -2
  14. package/dist/{Tile-DbNFNPfU.js → Tile-Dcl7oIVu.js} +35 -35
  15. package/dist/{TileImage-BsXBxMtq.js → TileImage-BJeHipMX.js} +4 -4
  16. package/dist/{View-BR92hTWP.js → View-xp_P_OHw.js} +412 -401
  17. package/dist/XmlPreview.client-D3IKFPyo.js +34 -0
  18. package/dist/{common-PJfpC179.js → common-BjQlan3k.js} +36 -36
  19. package/dist/components-next.css +6 -6
  20. package/dist/components-next.js +175 -149
  21. package/dist/components.css +1 -1
  22. package/dist/{index-BZsAZ7iw.js → index-Bdd6FTMM.js} +32886 -27183
  23. package/dist/{main-qc4CO9Kn.js → main-CD1QdcDZ.js} +92258 -76684
  24. package/dist/{proj-DsetBcW7.js → proj-CsNo9yH1.js} +532 -512
  25. package/dist/{tilecoord-Db24Px13.js → tilecoord-A0fLnBZr.js} +28 -28
  26. package/dist/{vue3-xml-viewer.common-CCOV_ohP.js → vue3-xml-viewer.common-B9EH7RPQ.js} +1 -1
  27. package/package.json +18 -11
  28. package/src/components/ActivityList/ActivityList.vue +0 -2
  29. package/src/components/Chart/ChartViewer.vue +226 -0
  30. package/src/components/Chart/ChartViewerWrapper.vue +170 -0
  31. package/src/components/DataserviceCard.vue +3 -0
  32. package/src/components/DatasetCard.vue +9 -4
  33. package/src/components/Form/Listbox.vue +101 -0
  34. package/src/components/Form/SearchableSelect.vue +2 -1
  35. package/src/components/InfiniteLoader.vue +53 -0
  36. package/src/components/ObjectCardHeader.vue +11 -4
  37. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  38. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  39. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  40. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  41. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  42. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  43. package/src/components/OpenApiViewer/openapi.ts +150 -0
  44. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  45. package/src/components/Pagination.vue +8 -5
  46. package/src/components/RadioInput.vue +7 -2
  47. package/src/components/ReadMore.vue +1 -1
  48. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  49. package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
  50. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  51. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
  52. package/src/components/ResourceAccordion/MapContainer.client.vue +5 -14
  53. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  54. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -103
  55. package/src/components/ResourceAccordion/Pmtiles.client.vue +5 -10
  56. package/src/components/ResourceAccordion/Preview.vue +16 -21
  57. package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
  58. package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
  59. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  60. package/src/components/ResourceAccordion/ResourceAccordion.vue +10 -109
  61. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
  62. package/src/components/ResourceExplorer/ResourceExplorer.vue +35 -20
  63. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  64. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +56 -146
  65. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  66. package/src/components/ReuseCard.vue +12 -4
  67. package/src/components/Search/GlobalSearch.vue +191 -110
  68. package/src/components/Search/SearchInput.vue +5 -4
  69. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  70. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  71. package/src/components/TabularExplorer/TabularExplorer.vue +973 -0
  72. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  73. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  74. package/src/components/TabularExplorer/types.ts +83 -0
  75. package/src/composables/useHasTabularData.ts +13 -0
  76. package/src/composables/useMetrics.ts +1 -1
  77. package/src/composables/useResourceCapabilities.ts +1 -1
  78. package/src/composables/useSearchFilter.ts +118 -0
  79. package/src/composables/useStableQueryParams.ts +31 -3
  80. package/src/composables/useTabularProfile.ts +70 -0
  81. package/src/config.ts +20 -3
  82. package/src/functions/api.ts +9 -37
  83. package/src/functions/api.types.ts +1 -0
  84. package/src/functions/charts.ts +68 -0
  85. package/src/functions/datasets.ts +0 -17
  86. package/src/functions/metrics.ts +6 -4
  87. package/src/functions/resources.ts +56 -1
  88. package/src/functions/tabular.ts +60 -0
  89. package/src/functions/tabularApi.ts +138 -11
  90. package/src/main.ts +94 -7
  91. package/src/types/dataservices.ts +2 -0
  92. package/src/types/pages.ts +0 -5
  93. package/src/types/posts.ts +2 -2
  94. package/src/types/reports.ts +5 -1
  95. package/src/types/search.ts +58 -1
  96. package/src/types/site.ts +5 -3
  97. package/src/types/ui.ts +2 -0
  98. package/src/types/users.ts +2 -1
  99. package/src/types/visualizations.ts +89 -0
  100. package/assets/swagger-themes/newspaper.css +0 -1670
  101. package/dist/Datafair.client-0UYUu5yf.js +0 -35
  102. package/dist/JsonPreview.client-BrTMBWHZ.js +0 -87
  103. package/dist/MapContainer.client-CUmKyByc.js +0 -107
  104. package/dist/Swagger.client-2Yn7iF0A.js +0 -4
  105. package/dist/XmlPreview.client-DxqlVnKu.js +0 -79
  106. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  107. package/src/functions/pagination.ts +0 -9
  108. /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
@@ -2,21 +2,34 @@
2
2
  <div class="border border-gray-default">
3
3
  <header class="p-4 flex flex-wrap md:flex-nowrap gap-4 items-center justify-between">
4
4
  <div>
5
- <div class="flex items-center mb-1">
5
+ <div class="flex items-center gap-1 mb-1">
6
6
  <h3 class="m-0 flex items-baseline text-base font-bold leading-tight">
7
7
  <ResourceIcon
8
8
  :resource
9
- class="size-3.5 mr-1"
9
+ class="size-3.5 mr-1 shrink-0 translate-y-px"
10
10
  />
11
11
  <span class="line-clamp-2">{{ resource.title || t('Fichier sans nom') }}</span>
12
12
  </h3>
13
+ <ResourceSelector
14
+ v-if="resources && resources.length > 1"
15
+ :resources
16
+ :selected-id="resource.id"
17
+ @select="emit('select', $event)"
18
+ />
13
19
  <CopyButton
14
20
  :label="t('Copier le lien')"
15
21
  :copied-label="t('Lien copié !')"
16
22
  :text="resourceExternalUrl"
23
+ class="hidden md:inline-flex"
17
24
  />
18
25
  </div>
19
- <div class="text-gray-medium text-xs flex items-center gap-1">
26
+ <div class="text-gray-medium text-xs flex items-center gap-1 flex-wrap">
27
+ <SchemaBadge :resource />
28
+ <RiSubtractLine
29
+ v-if="resource.schema?.name || resource.schema?.url"
30
+ aria-hidden="true"
31
+ class="size-3 fill-gray-medium"
32
+ />
20
33
  <span>{{ t('mis à jour {date}', { date: formatRelativeIfRecentDate(resource.last_modified) }) }}</span>
21
34
  <RiSubtractLine
22
35
  aria-hidden="true"
@@ -128,14 +141,28 @@
128
141
  :resource="resource"
129
142
  :dataset="dataset"
130
143
  />
131
- <SwaggerClient
144
+ <OpenApiViewer
132
145
  v-else-if="hasOpenAPIPreview"
133
146
  :url="resource.extras['apidocUrl'] as string"
134
147
  />
135
- <Preview
136
- v-else
137
- :resource="resource"
148
+ <TabularExplorer
149
+ v-else-if="hasTabularData"
150
+ :resource-id="resource.id"
138
151
  />
152
+ <PreviewUnavailable v-else>
153
+ <!-- "File too large to download" is the only analysis:error value from hydra for now -->
154
+ <template v-if="resource.extras['analysis:error'] === 'File too large to download'">
155
+ {{ t("Ce fichier est trop volumineux pour être analysé et prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.") }}
156
+ </template>
157
+ <template v-else-if="resource.extras['analysis:parsing:error']">
158
+ {{ t("L'analyse de ce fichier a rencontré une erreur, l'aperçu n'est pas disponible. Téléchargez-le depuis l'onglet Téléchargements.") }}
159
+ <br>
160
+ <span class="text-gray-medium text-xs">{{ resource.extras['analysis:parsing:error'] }}</span>
161
+ </template>
162
+ <template v-else>
163
+ {{ t("Ce fichier ne peut pas être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.") }}
164
+ </template>
165
+ </PreviewUnavailable>
139
166
  </div>
140
167
  <div v-if="tab.key === 'description'">
141
168
  <MarkdownViewer
@@ -153,128 +180,10 @@
153
180
  <Metadata :resource />
154
181
  </div>
155
182
  <div v-if="tab.key === 'downloads'">
156
- <dl class="fr-pl-0">
157
- <dt
158
- v-if="resource.format === 'url'"
159
- class="font-bold fr-text--sm fr-mb-0"
160
- >
161
- {{ t("URL d'origine") }}
162
- </dt>
163
- <dt
164
- v-else
165
- class="font-bold fr-text--sm fr-mb-0"
166
- >
167
- {{ t('Format original') }}
168
- </dt>
169
- <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
170
- <span
171
- v-if="resource.format === 'url'"
172
- class="inline-flex items-center max-w-full"
173
- >
174
- <a
175
- :href="resource.latest"
176
- class="fr-link no-icon-after truncate"
177
- rel="ugc nofollow noopener"
178
- target="_blank"
179
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
180
- >
181
- {{ resource.url }}
182
- </a>
183
- <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
184
- </span>
185
- <span v-else>
186
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
187
- <a
188
- :href="resource.latest"
189
- class="fr-link"
190
- rel="ugc nofollow noopener"
191
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
192
- >
193
- <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
194
- </a>
195
- </span>
196
- <CopyButton
197
- :label="t('Copier le lien')"
198
- :copied-label="t('Lien copié !')"
199
- :text="resource.latest"
200
- class="relative"
201
- />
202
- </dd>
203
- <template v-if="generatedFormats.length">
204
- <dt class="font-bold fr-text--sm fr-mb-0">
205
- {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
206
- </dt>
207
- <dd
208
- v-for="generatedFormat in generatedFormats"
209
- :key="generatedFormat.format"
210
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
211
- >
212
- <span>
213
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
214
- <a
215
- :href="generatedFormat.url"
216
- class="fr-link"
217
- rel="ugc nofollow noopener"
218
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
219
- >
220
- <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
221
- </a>
222
- </span>
223
- <CopyButton
224
- :label="t('Copier le lien')"
225
- :copied-label="t('Lien copié !')"
226
- :text="generatedFormat.url"
227
- class="relative"
228
- />
229
- </dd>
230
- </template>
231
- <template v-if="wfsFormats.length">
232
- <dt class="font-bold fr-text--sm fr-mb-0">
233
- <div class="flex gap-1 items-center">
234
- {{ t('Formats exportés depuis le service WFS') }}
235
- <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
236
- <Tooltip>
237
- <RiInformationLine
238
- class="flex-none size-4"
239
- :aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
240
- aria-hidden="true"
241
- />
242
- <template #tooltip>
243
- <p class="text-sm font-normal mb-0">
244
- {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
245
- </p>
246
- <p class="text-sm font-normal mb-0">
247
- {{ t(`Le nombre de features téléchargées peut être limité.`) }}
248
- </p>
249
- </template>
250
- </Tooltip>
251
- </div>
252
- </dt>
253
- <dd
254
- v-for="wfsFormat in wfsFormats"
255
- :key="wfsFormat.format"
256
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
257
- >
258
- <span>
259
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
260
- <a
261
- :href="wfsFormat.url"
262
- class="fr-link"
263
- rel="ugc nofollow noopener"
264
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
265
- >
266
- <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
267
- </a>
268
- </span>
269
- <CopyButton
270
- :label="t('Copier le lien')"
271
- :copied-label="t('Lien copié !')"
272
- :text="wfsFormat.url"
273
- class="relative"
274
- />
275
- </dd>
276
- </template>
277
- </dl>
183
+ <Downloads
184
+ :resource="resource"
185
+ :dataset="dataset"
186
+ />
278
187
  </div>
279
188
  <div v-if="tab.key === 'swagger'">
280
189
  <div class="fr-mb-4w">
@@ -283,7 +192,7 @@
283
192
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
284
193
  <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
285
194
  </div>
286
- <Swagger
195
+ <OpenApiViewer
287
196
  v-if="hasTabularData"
288
197
  :url="`${config.tabularApiUrl}/api/resources/${resource.id}/swagger/`"
289
198
  />
@@ -297,30 +206,33 @@
297
206
 
298
207
  <script setup lang="ts">
299
208
  import { computed, defineAsyncComponent } from 'vue'
300
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
209
+ import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
210
+ import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
301
211
  import { toast } from 'vue-sonner'
302
212
  import BrandedButton from '../BrandedButton.vue'
303
213
  import CopyButton from '../CopyButton.vue'
304
214
  import MarkdownViewer from '../MarkdownViewer.vue'
305
215
  import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
306
- import Swagger from '../ResourceAccordion/Swagger.client.vue'
216
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
307
217
  import TabGroup from '../Tabs/TabGroup.vue'
308
218
  import TabList from '../Tabs/TabList.vue'
309
219
  import Tab from '../Tabs/Tab.vue'
310
220
  import TabPanels from '../Tabs/TabPanels.vue'
311
221
  import TabPanel from '../Tabs/TabPanel.vue'
312
- import Tooltip from '../Tooltip.vue'
313
- import Preview from '../ResourceAccordion/Preview.vue'
222
+ import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
314
223
  import DataStructure from '../ResourceAccordion/DataStructure.vue'
224
+ import Downloads from '../ResourceAccordion/Downloads.vue'
315
225
  import Metadata from '../ResourceAccordion/Metadata.vue'
226
+ import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
227
+ import ResourceSelector from './ResourceSelector.vue'
316
228
  import { filesize, summarize } from '../../functions/helpers'
317
- import { getResourceFormatIcon } from '../../functions/resources'
318
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
229
+ import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
319
230
  import { trackEvent } from '../../functions/matomo'
320
231
  import { useComponentsConfig } from '../../config'
321
232
  import { useFormatDate } from '../../functions/dates'
322
233
  import { useTranslation } from '../../composables/useTranslation'
323
234
  import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
235
+ import { provideTabularProfile } from '../../composables/useTabularProfile'
324
236
  import type { Resource } from '../../types/resources'
325
237
  import type { Dataset, DatasetV2 } from '../../types/datasets'
326
238
 
@@ -342,13 +254,15 @@ const MapContainer = defineAsyncComponent(() =>
342
254
  const Pmtiles = defineAsyncComponent(() =>
343
255
  import('../ResourceAccordion/Pmtiles.client.vue'),
344
256
  )
345
- const SwaggerClient = defineAsyncComponent(() =>
346
- import('../ResourceAccordion/Swagger.client.vue'),
347
- )
348
257
 
349
258
  const props = defineProps<{
350
259
  dataset: Dataset | DatasetV2
351
260
  resource: Resource
261
+ resources?: Resource[]
262
+ }>()
263
+
264
+ const emit = defineEmits<{
265
+ select: [resource: Resource]
352
266
  }>()
353
267
 
354
268
  const { t } = useTranslation()
@@ -362,13 +276,13 @@ const {
362
276
  hasOpenAPIPreview,
363
277
  ogcService,
364
278
  ogcWms,
365
- generatedFormats,
366
- wfsFormats,
367
- defaultWfsProjection,
368
279
  isResourceUrl,
369
280
  tabsOptions,
370
281
  } = useResourceCapabilities(() => props.resource, () => props.dataset)
371
282
 
283
+ // Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
284
+ await provideTabularProfile(() => props.resource.id)
285
+
372
286
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
373
287
  const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
374
288
 
@@ -382,10 +296,6 @@ const downloadButtonTitle = computed(() => {
382
296
  return t('Télécharger le fichier en {format}', { format: format.value })
383
297
  })
384
298
 
385
- const conversionsLastUpdate = computed(() =>
386
- formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
387
- )
388
-
389
299
  const copyResourceUrl = async () => {
390
300
  try {
391
301
  await navigator.clipboard.writeText(props.resource.url)
@@ -0,0 +1,113 @@
1
+ <template>
2
+ <Popover
3
+ v-slot="{ open, close }"
4
+ class="relative inline-block"
5
+ >
6
+ <slot
7
+ name="trigger"
8
+ :open="open"
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
+ >
37
+ <li
38
+ v-for="r in filteredResources"
39
+ :key="r.id"
40
+ >
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>
71
+ </li>
72
+ </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
+ </PopoverPanel>
80
+ </Popover>
81
+ </template>
82
+
83
+ <script setup lang="ts">
84
+ import { computed, ref } from 'vue'
85
+ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
86
+ import { RiArrowDownSLine } from '@remixicon/vue'
87
+ import { useTranslation } from '../../composables/useTranslation'
88
+ import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
89
+ import type { Resource } from '../../types/resources'
90
+
91
+ const props = defineProps<{
92
+ resources: Resource[]
93
+ selectedId: string
94
+ searchable?: boolean
95
+ isDisabled?: (resource: Resource) => boolean
96
+ disabledTitle?: string
97
+ }>()
98
+
99
+ const emit = defineEmits<{
100
+ select: [resource: Resource]
101
+ }>()
102
+
103
+ 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
+ </script>
@@ -2,14 +2,17 @@
2
2
  <article class="fr-enlarge-link group/reuse-card bg-white border border-gray-default hover:bg-gray-some flex flex-col relative">
3
3
  <div class="flex flex-col h-full flex-1 order-2 px-8">
4
4
  <div class="order-1 flex flex-col px-4 py-1 h-full -mx-8">
5
- <h3 class="font-bold text-base mt-1 mb-0 truncate">
5
+ <component
6
+ :is="titleTag"
7
+ class="font-bold text-base mt-1 mb-0 truncate"
8
+ >
6
9
  <AppLink
7
10
  class="text-gray-title overflow-hidden"
8
11
  :to="reuseUrl"
9
12
  >
10
13
  {{ reuse.title }}
11
14
  </AppLink>
12
- </h3>
15
+ </component>
13
16
  <div class="order-3 text-sm m-0 text-gray-medium">
14
17
  <div class="text-sm mb-0 flex items-center">
15
18
  <ObjectCardOwner
@@ -66,13 +69,14 @@ import { computed } from 'vue'
66
69
  import type { RouteLocationRaw } from 'vue-router'
67
70
  import { useFormatDate } from '../functions/dates'
68
71
  import type { Reuse } from '../types/reuses'
72
+ import type { TitleTag } from '../types/ui'
69
73
  import { useTranslation } from '../composables/useTranslation'
70
74
  import AppLink from './AppLink.vue'
71
75
  import ObjectCardOwner from './ObjectCardOwner.vue'
72
76
  import ReuseDetails from './ReuseDetails.vue'
73
77
  import Placeholder from './Placeholder.vue'
74
78
 
75
- const props = defineProps<{
79
+ const props = withDefaults(defineProps<{
76
80
  reuse: Reuse
77
81
 
78
82
  /**
@@ -86,7 +90,11 @@ const props = defineProps<{
86
90
  * It is used as a separate prop to allow other sites using the package to define their own organization pages.
87
91
  */
88
92
  organizationUrl?: RouteLocationRaw
89
- }>()
93
+
94
+ titleTag?: TitleTag
95
+ }>(), {
96
+ titleTag: 'h3',
97
+ })
90
98
 
91
99
  const { t } = useTranslation()
92
100
  const { formatRelativeIfRecentDate } = useFormatDate()