@datagouv/components-next 1.0.2-dev.11 → 1.0.2-dev.111

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 (104) 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-8haHXl47.js → Datafair.client-CKB2P_X1.js} +1 -1
  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-Bx11-jfT.js +40 -0
  7. package/dist/{Map-BjUnLyj8.js → Map-BUtPf5GN.js} +756 -756
  8. package/dist/{MapContainer.client-l6HuXTHR.js → MapContainer.client-CdZSeT_L.js} +37 -38
  9. package/dist/{OSM-s40W6sQ2.js → OSM-D4MTdBtk.js} +2 -2
  10. package/dist/{PdfPreview.client-4OueK-2Z.js → PdfPreview.client-Bh9lP-qU.js} +822 -850
  11. package/dist/{Pmtiles.client-4j3VTYkz.js → Pmtiles.client-Bi46wN14.js} +1 -1
  12. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BnC7vWGP.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-oFAOv828.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 +165 -142
  21. package/dist/components.css +1 -1
  22. package/dist/{index-CVTIoZQ0.js → index-CxCuKQ81.js} +32886 -27183
  23. package/dist/main-CQ9ZQG7n.js +73607 -0
  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-CWer_T5-.js → vue3-xml-viewer.common-B9qp90K_.js} +1 -1
  27. package/package.json +25 -11
  28. package/src/chart.ts +5 -0
  29. package/src/components/ActivityList/ActivityList.vue +3 -2
  30. package/src/components/Chart/ChartViewer.vue +226 -0
  31. package/src/components/Chart/ChartViewerWrapper.vue +170 -0
  32. package/src/components/DataserviceCard.vue +3 -0
  33. package/src/components/DatasetCard.vue +9 -4
  34. package/src/components/Form/Listbox.vue +101 -0
  35. package/src/components/Form/SearchableSelect.vue +2 -1
  36. package/src/components/InfiniteLoader.vue +53 -0
  37. package/src/components/ObjectCardHeader.vue +11 -4
  38. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  39. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  40. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  41. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  42. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  43. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  44. package/src/components/OpenApiViewer/openapi.ts +150 -0
  45. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  46. package/src/components/Pagination.vue +8 -5
  47. package/src/components/RadioInput.vue +7 -2
  48. package/src/components/ReadMore.vue +1 -1
  49. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  50. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  51. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  52. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -3
  53. package/src/components/ResourceAccordion/Metadata.vue +1 -2
  54. package/src/components/ResourceAccordion/PdfPreview.client.vue +24 -87
  55. package/src/components/ResourceAccordion/Preview.vue +11 -11
  56. package/src/components/ResourceAccordion/PreviewWrapper.vue +82 -0
  57. package/src/components/ResourceAccordion/ResourceAccordion.vue +11 -110
  58. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  59. package/src/components/ResourceExplorer/ResourceExplorer.vue +14 -10
  60. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +2 -2
  61. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +50 -148
  62. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  63. package/src/components/ReuseCard.vue +12 -4
  64. package/src/components/Search/GlobalSearch.vue +201 -113
  65. package/src/components/Search/SearchInput.vue +5 -4
  66. package/src/components/TabularExplorer/TabularCell.vue +51 -0
  67. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  68. package/src/components/TabularExplorer/TabularExplorer.vue +973 -0
  69. package/src/components/TabularExplorer/TabularFilterContent.vue +351 -0
  70. package/src/components/TabularExplorer/TabularFilterPopover.vue +111 -0
  71. package/src/components/TabularExplorer/types.ts +83 -0
  72. package/src/composables/useHasTabularData.ts +13 -0
  73. package/src/composables/useMetrics.ts +1 -1
  74. package/src/composables/useResourceCapabilities.ts +1 -1
  75. package/src/composables/useSearchFilter.ts +118 -0
  76. package/src/composables/useStableQueryParams.ts +38 -6
  77. package/src/composables/useTabularProfile.ts +70 -0
  78. package/src/config.ts +20 -3
  79. package/src/functions/activities.ts +3 -3
  80. package/src/functions/api.ts +9 -37
  81. package/src/functions/api.types.ts +1 -0
  82. package/src/functions/charts.ts +68 -0
  83. package/src/functions/datasets.ts +0 -17
  84. package/src/functions/metrics.ts +6 -4
  85. package/src/functions/resources.ts +56 -1
  86. package/src/functions/tabular.ts +60 -0
  87. package/src/functions/tabularApi.ts +138 -11
  88. package/src/main.ts +90 -9
  89. package/src/types/dataservices.ts +2 -0
  90. package/src/types/pages.ts +0 -5
  91. package/src/types/posts.ts +2 -2
  92. package/src/types/reports.ts +5 -1
  93. package/src/types/search.ts +63 -1
  94. package/src/types/site.ts +5 -3
  95. package/src/types/ui.ts +2 -0
  96. package/src/types/users.ts +2 -1
  97. package/src/types/visualizations.ts +89 -0
  98. package/assets/swagger-themes/newspaper.css +0 -1670
  99. package/dist/JsonPreview.client-D53pj9Cw.js +0 -72
  100. package/dist/Swagger.client-DPBmsH9q.js +0 -4
  101. package/dist/XmlPreview.client-XElkoA4F.js +0 -64
  102. package/dist/main-BbT-LUXy.js +0 -105854
  103. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  104. package/src/functions/pagination.ts +0 -9
@@ -2,24 +2,31 @@
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">
20
27
  <SchemaBadge :resource />
21
28
  <RiSubtractLine
22
- v-if="resource.schema"
29
+ v-if="resource.schema?.name || resource.schema?.url"
23
30
  aria-hidden="true"
24
31
  class="size-3 fill-gray-medium"
25
32
  />
@@ -134,16 +141,27 @@
134
141
  :resource="resource"
135
142
  :dataset="dataset"
136
143
  />
137
- <SwaggerClient
144
+ <OpenApiViewer
138
145
  v-else-if="hasOpenAPIPreview"
139
146
  :url="resource.extras['apidocUrl'] as string"
140
147
  />
141
- <Preview
148
+ <TabularExplorer
142
149
  v-else-if="hasTabularData"
143
- :resource="resource"
150
+ :resource-id="resource.id"
144
151
  />
145
152
  <PreviewUnavailable v-else>
146
- {{ t("Ce fichier ne peut pas être prévisualisé. Téléchargez-le depuis l'onglet Téléchargements.") }}
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>
147
165
  </PreviewUnavailable>
148
166
  </div>
149
167
  <div v-if="tab.key === 'description'">
@@ -162,137 +180,22 @@
162
180
  <Metadata :resource />
163
181
  </div>
164
182
  <div v-if="tab.key === 'downloads'">
165
- <dl class="fr-pl-0">
166
- <dt
167
- v-if="resource.format === 'url'"
168
- class="font-bold fr-text--sm fr-mb-0"
169
- >
170
- {{ t("URL d'origine") }}
171
- </dt>
172
- <dt
173
- v-else
174
- class="font-bold fr-text--sm fr-mb-0"
175
- >
176
- {{ t('Format original') }}
177
- </dt>
178
- <dd class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center">
179
- <span
180
- v-if="resource.format === 'url'"
181
- class="inline-flex items-center max-w-full"
182
- >
183
- <a
184
- :href="resource.latest"
185
- class="fr-link no-icon-after truncate"
186
- rel="ugc nofollow noopener"
187
- target="_blank"
188
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
189
- >
190
- {{ resource.url }}
191
- </a>
192
- <span class="fr-ml-1v fr-icon-external-link-line fr-icon--sm shrink-0" />
193
- </span>
194
- <span v-else>
195
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
196
- <a
197
- :href="resource.latest"
198
- class="fr-link"
199
- rel="ugc nofollow noopener"
200
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${resource.format}`)"
201
- >
202
- <span>{{ t('Format {format}', { format: resource.format }) }}<span v-if="resourceFilesize"> - {{ filesize(resourceFilesize) }}</span></span>
203
- </a>
204
- </span>
205
- <CopyButton
206
- :label="t('Copier le lien')"
207
- :copied-label="t('Lien copié !')"
208
- :text="resource.latest"
209
- class="relative"
210
- />
211
- </dd>
212
- <template v-if="generatedFormats.length">
213
- <dt class="font-bold fr-text--sm fr-mb-0">
214
- {{ t('Formats générés automatiquement par {platform} (dernière mise à jour {date})', { platform: config.name, date: conversionsLastUpdate }) }}
215
- </dt>
216
- <dd
217
- v-for="generatedFormat in generatedFormats"
218
- :key="generatedFormat.format"
219
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
220
- >
221
- <span>
222
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
223
- <a
224
- :href="generatedFormat.url"
225
- class="fr-link"
226
- rel="ugc nofollow noopener"
227
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${generatedFormat.format}`)"
228
- >
229
- <span>{{ t('Format {format}', { format: generatedFormat.format }) }}<span v-if="generatedFormat.size"> - {{ filesize(generatedFormat.size) }}</span></span>
230
- </a>
231
- </span>
232
- <CopyButton
233
- :label="t('Copier le lien')"
234
- :copied-label="t('Lien copié !')"
235
- :text="generatedFormat.url"
236
- class="relative"
237
- />
238
- </dd>
239
- </template>
240
- <template v-if="wfsFormats.length">
241
- <dt class="font-bold fr-text--sm fr-mb-0">
242
- <div class="flex gap-1 items-center">
243
- {{ t('Formats exportés depuis le service WFS') }}
244
- <span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
245
- <Tooltip>
246
- <RiInformationLine
247
- class="flex-none size-4"
248
- :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é.`)"
249
- aria-hidden="true"
250
- />
251
- <template #tooltip>
252
- <p class="text-sm font-normal mb-0">
253
- {{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
254
- </p>
255
- <p class="text-sm font-normal mb-0">
256
- {{ t(`Le nombre de features téléchargées peut être limité.`) }}
257
- </p>
258
- </template>
259
- </Tooltip>
260
- </div>
261
- </dt>
262
- <dd
263
- v-for="wfsFormat in wfsFormats"
264
- :key="wfsFormat.format"
265
- class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
266
- >
267
- <span>
268
- <span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
269
- <a
270
- :href="wfsFormat.url"
271
- class="fr-link"
272
- rel="ugc nofollow noopener"
273
- @click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
274
- >
275
- <span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
276
- </a>
277
- </span>
278
- <CopyButton
279
- :label="t('Copier le lien')"
280
- :copied-label="t('Lien copié !')"
281
- :text="wfsFormat.url"
282
- class="relative"
283
- />
284
- </dd>
285
- </template>
286
- </dl>
183
+ <Downloads
184
+ :resource="resource"
185
+ :dataset="dataset"
186
+ />
287
187
  </div>
288
- <div v-if="tab.key === 'swagger'">
188
+ <div v-if="tab.key === 'api'">
289
189
  <div class="fr-mb-4w">
290
190
  <p>{{ t("Cette API est générée automatiquement par {platform} à partir du fichier.", { platform: config.name }) }}</p>
291
191
  <p>{{ t("- Si le fichier est modifié, l'API sera mise à jour et sa structure pourra changer.") }}</p>
292
192
  <p>{{ t("- Si le fichier est supprimé, l'API sera également supprimée.") }}</p>
293
193
  <p>{{ t("Pour des usages pérennes, prévoyez que cette API dépend directement du fichier source.") }}</p>
194
+ <p v-if="config.tabularApiUrl">
195
+ {{ t("L'URL de base de l'API est {url}", { url: config.tabularApiUrl }) }}
196
+ </p>
294
197
  </div>
295
- <Swagger
198
+ <OpenApiViewer
296
199
  v-if="hasTabularData"
297
200
  :url="`${config.tabularApiUrl}/api/resources/${resource.id}/swagger/`"
298
201
  />
@@ -306,32 +209,33 @@
306
209
 
307
210
  <script setup lang="ts">
308
211
  import { computed, defineAsyncComponent } from 'vue'
309
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
212
+ import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
310
213
  import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
311
214
  import { toast } from 'vue-sonner'
312
215
  import BrandedButton from '../BrandedButton.vue'
313
216
  import CopyButton from '../CopyButton.vue'
314
217
  import MarkdownViewer from '../MarkdownViewer.vue'
315
218
  import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
316
- import Swagger from '../ResourceAccordion/Swagger.client.vue'
219
+ import OpenApiViewer from '../OpenApiViewer/OpenApiViewer.vue'
317
220
  import TabGroup from '../Tabs/TabGroup.vue'
318
221
  import TabList from '../Tabs/TabList.vue'
319
222
  import Tab from '../Tabs/Tab.vue'
320
223
  import TabPanels from '../Tabs/TabPanels.vue'
321
224
  import TabPanel from '../Tabs/TabPanel.vue'
322
- import Tooltip from '../Tooltip.vue'
323
- import Preview from '../ResourceAccordion/Preview.vue'
225
+ import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
324
226
  import DataStructure from '../ResourceAccordion/DataStructure.vue'
227
+ import Downloads from '../ResourceAccordion/Downloads.vue'
325
228
  import Metadata from '../ResourceAccordion/Metadata.vue'
326
229
  import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
230
+ import ResourceSelector from './ResourceSelector.vue'
327
231
  import { filesize, summarize } from '../../functions/helpers'
328
- import { getResourceFormatIcon } from '../../functions/resources'
329
- import { getResourceExternalUrl, getResourceFilesize } from '../../functions/datasets'
232
+ import { getResourceFormatIcon, getResourceExternalUrl, getResourceFilesize } from '../../functions/resources'
330
233
  import { trackEvent } from '../../functions/matomo'
331
234
  import { useComponentsConfig } from '../../config'
332
235
  import { useFormatDate } from '../../functions/dates'
333
236
  import { useTranslation } from '../../composables/useTranslation'
334
237
  import { useResourceCapabilities } from '../../composables/useResourceCapabilities'
238
+ import { provideTabularProfile } from '../../composables/useTabularProfile'
335
239
  import type { Resource } from '../../types/resources'
336
240
  import type { Dataset, DatasetV2 } from '../../types/datasets'
337
241
 
@@ -353,13 +257,15 @@ const MapContainer = defineAsyncComponent(() =>
353
257
  const Pmtiles = defineAsyncComponent(() =>
354
258
  import('../ResourceAccordion/Pmtiles.client.vue'),
355
259
  )
356
- const SwaggerClient = defineAsyncComponent(() =>
357
- import('../ResourceAccordion/Swagger.client.vue'),
358
- )
359
260
 
360
261
  const props = defineProps<{
361
262
  dataset: Dataset | DatasetV2
362
263
  resource: Resource
264
+ resources?: Resource[]
265
+ }>()
266
+
267
+ const emit = defineEmits<{
268
+ select: [resource: Resource]
363
269
  }>()
364
270
 
365
271
  const { t } = useTranslation()
@@ -373,13 +279,13 @@ const {
373
279
  hasOpenAPIPreview,
374
280
  ogcService,
375
281
  ogcWms,
376
- generatedFormats,
377
- wfsFormats,
378
- defaultWfsProjection,
379
282
  isResourceUrl,
380
283
  tabsOptions,
381
284
  } = useResourceCapabilities(() => props.resource, () => props.dataset)
382
285
 
286
+ // Share the tabular profile fetch between TabularExplorer and DataStructure tabs.
287
+ await provideTabularProfile(() => props.resource.id)
288
+
383
289
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
384
290
  const resourceExternalUrl = computed(() => getResourceExternalUrl(props.dataset, props.resource))
385
291
 
@@ -393,10 +299,6 @@ const downloadButtonTitle = computed(() => {
393
299
  return t('Télécharger le fichier en {format}', { format: format.value })
394
300
  })
395
301
 
396
- const conversionsLastUpdate = computed(() =>
397
- formatRelativeIfRecentDate(props.resource.extras['analysis:parsing:finished_at'] as string | undefined),
398
- )
399
-
400
302
  const copyResourceUrl = async () => {
401
303
  try {
402
304
  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()