@datagouv/components-next 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +7 -6
  2. package/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
  3. package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
  4. package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
  5. package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
  6. package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
  7. package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
  8. package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
  9. package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
  10. package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
  11. package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
  12. package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
  13. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
  14. package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
  15. package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
  16. package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
  17. package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
  18. package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
  19. package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
  20. package/dist/components-next.css +1 -52
  21. package/dist/components-next.js +201 -170
  22. package/dist/components.css +2 -2
  23. package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
  24. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
  25. package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
  26. package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
  27. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  28. package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
  29. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  30. package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
  31. package/package.json +40 -39
  32. package/src/components/ActivityList/ActivityList.vue +21 -12
  33. package/src/components/AppLink.vue +3 -0
  34. package/src/components/BannerAction.vue +6 -2
  35. package/src/components/Chart/ChartViewer.vue +111 -17
  36. package/src/components/Chart/ChartViewerWrapper.vue +28 -16
  37. package/src/components/ChartCard.vue +112 -0
  38. package/src/components/CopyButton.vue +6 -2
  39. package/src/components/DataserviceCard.vue +23 -6
  40. package/src/components/DataserviceQuality.vue +82 -0
  41. package/src/components/DataserviceQualityInline.vue +31 -0
  42. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  43. package/src/components/DatasetCard.vue +17 -6
  44. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  45. package/src/components/DatasetQuality.vue +11 -11
  46. package/src/components/DatasetQualityInline.vue +2 -2
  47. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  48. package/src/components/DiscussionMessageCard.vue +14 -15
  49. package/src/components/Form/SchemaSelect.vue +2 -0
  50. package/src/components/FormattedDate.vue +69 -0
  51. package/src/components/LoadingBlock.vue +4 -1
  52. package/src/components/ObjectCard.vue +4 -1
  53. package/src/components/ObjectCardOwner.vue +13 -1
  54. package/src/components/Pagination.vue +18 -58
  55. package/src/components/Placeholder.vue +3 -2
  56. package/src/components/PostCard.vue +13 -15
  57. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  58. package/src/components/ResourceAccordion/Downloads.vue +19 -7
  59. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  60. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
  61. package/src/components/ResourceAccordion/Metadata.vue +21 -5
  62. package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
  63. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  64. package/src/components/ResourceAccordion/Pmtiles.client.vue +30 -19
  65. package/src/components/ResourceAccordion/Preview.vue +8 -4
  66. package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
  67. package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
  68. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  69. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  70. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  71. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +140 -184
  72. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  73. package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
  74. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
  75. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
  76. package/src/components/ResourceListItem.vue +168 -0
  77. package/src/components/ReuseCard.vue +18 -6
  78. package/src/components/ReuseDetails.vue +2 -2
  79. package/src/components/ReuseHorizontalCard.vue +4 -3
  80. package/src/components/SchemaCard.vue +10 -1
  81. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  82. package/src/components/Search/GlobalSearch.vue +28 -10
  83. package/src/components/Search/SearchInput.vue +1 -1
  84. package/src/components/Search/Sidemenu.vue +4 -3
  85. package/src/components/SmallChart.vue +11 -10
  86. package/src/components/StatBox.vue +13 -5
  87. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  88. package/src/components/TabularExplorer/TabularCell.vue +7 -8
  89. package/src/components/TabularExplorer/TabularCellPopover.vue +21 -5
  90. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  91. package/src/components/TabularExplorer/TabularDateField.vue +41 -0
  92. package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
  93. package/src/components/TabularExplorer/TabularExplorer.vue +127 -780
  94. package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
  95. package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
  96. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  97. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  98. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  99. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  100. package/src/components/TabularExplorer/TabularTable.vue +484 -0
  101. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  102. package/src/components/TabularExplorer/types.ts +40 -28
  103. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  104. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  105. package/src/components/TopicCard.vue +15 -14
  106. package/src/composables/useActiveDescendant.ts +5 -0
  107. package/src/composables/useDatasetResources.ts +164 -0
  108. package/src/composables/useHasTabularData.ts +2 -1
  109. package/src/composables/useResizable.ts +46 -0
  110. package/src/composables/useResourceCapabilities.ts +48 -40
  111. package/src/composables/useSearchFilter.ts +7 -5
  112. package/src/composables/useTranslation.ts +24 -9
  113. package/src/config.ts +1 -0
  114. package/src/functions/api.ts +13 -7
  115. package/src/functions/charts.ts +32 -1
  116. package/src/functions/dates.ts +26 -7
  117. package/src/functions/helpers.ts +15 -0
  118. package/src/functions/metrics.ts +57 -56
  119. package/src/functions/owned.ts +21 -0
  120. package/src/functions/paginate.ts +46 -0
  121. package/src/functions/resourceCapabilities.ts +34 -0
  122. package/src/functions/resources.ts +88 -0
  123. package/src/functions/schemas.ts +27 -0
  124. package/src/functions/scroll.ts +20 -0
  125. package/src/functions/tabular.ts +222 -5
  126. package/src/functions/tabularApi.ts +1 -43
  127. package/src/main.ts +32 -6
  128. package/src/types/dataservices.ts +15 -0
  129. package/src/types/organizations.ts +10 -0
  130. package/src/types/resources.ts +0 -1
  131. package/src/types/reuses.ts +12 -1
  132. package/src/types/search.ts +2 -2
  133. package/src/types/visualizations.ts +5 -0
  134. package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
  135. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js +0 -61
  136. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  137. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  138. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -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>
@@ -0,0 +1,168 @@
1
+ <template>
2
+ <AppLink
3
+ ref="row"
4
+ v-bind="$attrs"
5
+ :to
6
+ :replace
7
+ :class="selected ? '[&&]:!bg-gray-200' : '[&&]:hover:!bg-gray-100'"
8
+ class="grid h-7 w-full grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-1 rounded px-1 py-1 text-left !bg-none !no-underline"
9
+ @pointerenter="openOnHover"
10
+ @pointerleave="closeTooltip"
11
+ @focus="show = true"
12
+ @blur="closeTooltip"
13
+ >
14
+ <span
15
+ :class="[iconColor, '[&_svg]:fill-current']"
16
+ class="flex size-5 shrink-0 items-center justify-center rounded-[1px]"
17
+ >
18
+ <component
19
+ :is="iconComponent"
20
+ class="size-4"
21
+ aria-hidden="true"
22
+ />
23
+ </span>
24
+ <div class="flex min-w-0 items-baseline gap-0.5 whitespace-nowrap leading-4">
25
+ <span
26
+ class="truncate text-[13px]"
27
+ :class="selected ? 'font-extrabold text-gray-title' : 'font-medium text-gray-medium'"
28
+ >{{ resource.title || t('Fichier sans nom') }}</span>
29
+ <template v-if="humanFilesize">
30
+ <span class="shrink-0 text-[13px] text-gray-medium">·</span>
31
+ <span class="shrink-0 text-[12px] text-gray-medium">{{ humanFilesize }}</span>
32
+ </template>
33
+ </div>
34
+ <!-- Capped and truncated: an `auto` grid track floors at its content width, so an
35
+ unusually long format (`www:link-1.0-http--samples`) would otherwise squeeze
36
+ the title track to nothing and overflow the fixed-height row. -->
37
+ <span
38
+ v-if="resource.format"
39
+ class="max-w-24 truncate rounded bg-gray-lower px-1.5 py-0.5 text-[12px] uppercase leading-4 text-gray-medium"
40
+ :title="resource.format"
41
+ >{{ resource.format }}</span>
42
+ </AppLink>
43
+
44
+ <!-- Hover card: the row truncates the title, so surface the full name plus the
45
+ same metadata line as the viewer header. Placed beside the row (not below) so
46
+ it doesn't hide the sibling rows we're scanning.
47
+ ClientOnly: Vue hydrates a teleport by scanning the target from its first child,
48
+ so an SSR-rendered teleport to `body` breaks as soon as anything else is prepended
49
+ there — and Nuxt emits the `bodyOpen` head tags before it. The card only ever opens
50
+ on hover, so there is nothing to render on the server anyway. -->
51
+ <ClientOnly>
52
+ <Teleport to="body">
53
+ <div
54
+ v-if="show"
55
+ ref="card"
56
+ role="tooltip"
57
+ class="pointer-events-none z-[80] w-max rounded border border-gray-default bg-white p-2 text-left shadow-[0_2px_4px_rgba(0,0,0,0.04),2px_4px_16px_rgba(0,0,0,0.12)]"
58
+ :style="floatingStyles"
59
+ >
60
+ <span class="block whitespace-nowrap text-[13px] font-medium leading-5 text-gray-title">{{ resource.title || t('Fichier sans nom') }}</span>
61
+ <div class="mt-1 flex items-center gap-1 text-[12px] leading-4 text-gray-medium">
62
+ <TranslationT keypath="mis à jour {date}">
63
+ <template #date>
64
+ <FormattedDate
65
+ :date="resource.last_modified"
66
+ format="relative"
67
+ />
68
+ </template>
69
+ </TranslationT>
70
+ <template v-if="humanFilesize">
71
+ <span>·</span>
72
+ <span>{{ humanFilesize }}</span>
73
+ </template>
74
+ <template v-if="resource.format">
75
+ <span>·</span>
76
+ <span class="rounded bg-gray-lower px-1.5 py-0.5 uppercase leading-4">{{ resource.format }}</span>
77
+ </template>
78
+ <span>·</span>
79
+ <span class="inline-flex items-center gap-0.5">
80
+ <RiDownloadLine
81
+ class="size-3"
82
+ aria-hidden="true"
83
+ />
84
+ {{ summarize(resource.metrics.views) }}
85
+ </span>
86
+ </div>
87
+ </div>
88
+ </Teleport>
89
+ </ClientOnly>
90
+ </template>
91
+
92
+ <script setup lang="ts">
93
+ import { computed, ref, useTemplateRef } from 'vue'
94
+ import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue'
95
+ import { useEventListener } from '@vueuse/core'
96
+ import type { RouteLocationRaw } from 'vue-router'
97
+ import { RiDownloadLine } from '@remixicon/vue'
98
+ import AppLink from './AppLink.vue'
99
+ import ClientOnly from './ClientOnly.vue'
100
+ import File from './Icons/File.vue'
101
+ import FormattedDate from './FormattedDate.vue'
102
+ import TranslationT from './TranslationT.vue'
103
+ import { getResourceFormatIcon, getResourceIconColor, getResourceFilesize } from '../functions/resources'
104
+ import { filesize, summarize } from '../functions/helpers'
105
+ import { useTranslation } from '../composables/useTranslation'
106
+ import type { Resource } from '../types/resources'
107
+
108
+ // The hover card below is a second root node, so Vue drops fallthrough attributes
109
+ // unless we place them ourselves: without this, a listener bound by the parent
110
+ // (the selector's `@click="close()"`) would silently never fire.
111
+ defineOptions({ inheritAttrs: false })
112
+
113
+ // Shared row for a single resource (sidebar + resource selector). Renders as a
114
+ // navigation link so switching resource is a real link — the URL is the source of
115
+ // truth for the selection.
116
+ const props = withDefaults(defineProps<{
117
+ resource: Resource
118
+ to: RouteLocationRaw
119
+ selected?: boolean
120
+ replace?: boolean
121
+ }>(), {
122
+ selected: false,
123
+ replace: false,
124
+ })
125
+
126
+ const { t } = useTranslation()
127
+
128
+ // Render the icon directly (not via ResourceIcon which forces a gray color) so the
129
+ // colored badge can tint it through currentColor + [&_svg]:fill-current.
130
+ const iconComponent = computed(() => (props.resource.format ? getResourceFormatIcon(props.resource.format) : null) ?? File)
131
+ const iconColor = computed(() => getResourceIconColor(props.resource.format))
132
+
133
+ const humanFilesize = computed(() => {
134
+ const size = getResourceFilesize(props.resource)
135
+ return size ? filesize(size) : null
136
+ })
137
+
138
+ // Hover card teleported to <body> so the sidebar's `overflow` doesn't clip it, hence
139
+ // the fixed strategy. `shift` keeps it inside the viewport for a row near an edge, and
140
+ // `autoUpdate` follows the row when its scrollable container moves under the pointer.
141
+ const show = ref(false)
142
+ const row = useTemplateRef<InstanceType<typeof AppLink>>('row')
143
+ const card = useTemplateRef<HTMLElement>('card')
144
+ const rowEl = computed(() => row.value?.$el as HTMLElement | undefined)
145
+ const { floatingStyles } = useFloating(rowEl, card, {
146
+ placement: 'right-start',
147
+ strategy: 'fixed',
148
+ middleware: [offset(16), flip(), shift({ padding: 8 })],
149
+ whileElementsMounted: autoUpdate,
150
+ })
151
+
152
+ // A tap fires a pointer enter too, so the card would flash on every touch selection
153
+ // in the mobile resource picker. Only a real pointer opens it — keyboard focus still
154
+ // does, through @focus.
155
+ function openOnHover(event: PointerEvent) {
156
+ if (event.pointerType === 'mouse') show.value = true
157
+ }
158
+
159
+ function closeTooltip() {
160
+ show.value = false
161
+ }
162
+
163
+ // A hover-shown card gets no `mouseleave` when the window loses focus (alt-tab) or
164
+ // the pointer leaves the document, so it would linger on return — close it in those
165
+ // cases too.
166
+ useEventListener(window, 'blur', closeTooltip)
167
+ useEventListener(document, 'mouseleave', closeTooltip)
168
+ </script>
@@ -21,7 +21,18 @@
21
21
  :organization-url="organizationUrl"
22
22
  />
23
23
  <RiSubtractLine class="size-4 flex-none fill-gray-medium" />
24
- <span class="block flex-none">{{ t('publié {date}', { date: formatRelativeIfRecentDate(reuse.created_at, { dateStyle: 'medium' }) }) }}</span>
24
+ <TranslationT
25
+ class="block flex-none"
26
+ keypath="publié {date}"
27
+ >
28
+ <template #date>
29
+ <FormattedDate
30
+ :date="reuse.created_at"
31
+ format="relative"
32
+ :options="{ dateStyle: 'medium' }"
33
+ />
34
+ </template>
35
+ </TranslationT>
25
36
  </div>
26
37
  <ReuseDetails :reuse />
27
38
  </div>
@@ -67,17 +78,18 @@
67
78
  import { RiLockLine, RiSubtractLine } from '@remixicon/vue'
68
79
  import { computed } from 'vue'
69
80
  import type { RouteLocationRaw } from 'vue-router'
70
- import { useFormatDate } from '../functions/dates'
71
- import type { Reuse } from '../types/reuses'
81
+ import type { Reuse, ReuseV2 } from '../types/reuses'
72
82
  import type { TitleTag } from '../types/ui'
73
83
  import { useTranslation } from '../composables/useTranslation'
74
84
  import AppLink from './AppLink.vue'
75
85
  import ObjectCardOwner from './ObjectCardOwner.vue'
76
86
  import ReuseDetails from './ReuseDetails.vue'
77
87
  import Placeholder from './Placeholder.vue'
88
+ import FormattedDate from './FormattedDate.vue'
89
+ import TranslationT from './TranslationT.vue'
78
90
 
79
91
  const props = withDefaults(defineProps<{
80
- reuse: Reuse
92
+ reuse: Reuse | ReuseV2
81
93
 
82
94
  /**
83
95
  * The reuseUrl is a route location object to allow Vue Router to navigate to the details of a reuse.
@@ -88,8 +100,9 @@ const props = withDefaults(defineProps<{
88
100
  /**
89
101
  * The organizationUrl is an optional route location object to allow Vue Router to navigate to the details of the organization linked to tha reuse.
90
102
  * It is used as a separate prop to allow other sites using the package to define their own organization pages.
103
+ * Pass null instead of a route to render the organization name as plain, non-linked text.
91
104
  */
92
- organizationUrl?: RouteLocationRaw
105
+ organizationUrl?: RouteLocationRaw | null
93
106
 
94
107
  titleTag?: TitleTag
95
108
  }>(), {
@@ -97,7 +110,6 @@ const props = withDefaults(defineProps<{
97
110
  })
98
111
 
99
112
  const { t } = useTranslation()
100
- const { formatRelativeIfRecentDate } = useFormatDate()
101
113
 
102
114
  const reuseUrl = computed(() => props.reuseUrl || props.reuse.page)
103
115
  </script>
@@ -32,11 +32,11 @@
32
32
  import { RiEyeLine, RiStarLine, RiSubtractLine } from '@remixicon/vue'
33
33
  import { useReuseType } from '../composables/useReuseType'
34
34
  import { summarize } from '../functions/helpers'
35
- import type { Reuse } from '../types/reuses'
35
+ import type { Reuse, ReuseV2 } from '../types/reuses'
36
36
  import { useTranslation } from '../composables/useTranslation'
37
37
 
38
38
  const props = defineProps<{
39
- reuse: Reuse
39
+ reuse: Reuse | ReuseV2
40
40
  }>()
41
41
 
42
42
  const { t } = useTranslation()
@@ -60,7 +60,7 @@
60
60
  <script setup lang="ts">
61
61
  import { RiArchiveLine, RiLineChartLine, RiLockLine } from '@remixicon/vue'
62
62
  import type { RouteLocationRaw } from 'vue-router'
63
- import type { Reuse } from '../types/reuses'
63
+ import type { Reuse, ReuseV2 } from '../types/reuses'
64
64
  import { useTranslation } from '../composables/useTranslation'
65
65
  import ReuseDetails from './ReuseDetails.vue'
66
66
  import Placeholder from './Placeholder.vue'
@@ -71,9 +71,10 @@ import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
71
71
  import ObjectCardBadge from './ObjectCardBadge.vue'
72
72
 
73
73
  defineProps<{
74
- reuse: Reuse
74
+ reuse: Reuse | ReuseV2
75
75
  reuseUrl?: RouteLocationRaw
76
- organizationUrl?: RouteLocationRaw
76
+ // undefined falls back to the organization's data.gouv.fr page; pass null to render plain, non-linked text
77
+ organizationUrl?: RouteLocationRaw | null
77
78
  }>()
78
79
 
79
80
  const { t } = useTranslation()
@@ -1,8 +1,14 @@
1
1
  <template>
2
2
  <div
3
+ :id
3
4
  class="p-4 relative border bg-white hover:bg-gray-some"
4
- :class="{ 'border-gray-default': !selected, 'border-datagouv': selected }"
5
+ :class="{
6
+ 'border-gray-default': !selected && !active,
7
+ 'border-datagouv': selected && !active,
8
+ 'border-blue-outline': active,
9
+ }"
5
10
  :role="selectable ? 'option' : undefined"
11
+ :tabindex="selectable ? -1 : undefined"
6
12
  :aria-selected="selectable && selected"
7
13
  >
8
14
  <h3 class="text-base font-bold mb-0 flex items-center gap-1">
@@ -73,7 +79,10 @@ import { useComponentsConfig } from '../config'
73
79
 
74
80
  defineProps<{
75
81
  schema: RegisteredSchema
82
+ id?: string
76
83
  selectable?: boolean
84
+ // Highlighted by the keyboard inside the listbox, which is not the same as chosen
85
+ active?: boolean
77
86
  selected?: boolean
78
87
  showLinks?: boolean
79
88
  }>()
@@ -31,7 +31,7 @@ const { t } = useTranslation()
31
31
 
32
32
  const options = [
33
33
  { value: 'open', label: t('Téléchargement libre') },
34
- { value: 'open_with_account', label: t('Ouvert sous condition') },
35
- { value: 'restricted', label: t('Accessible sous habilitation') },
34
+ { value: 'open_with_account', label: t('Ouvert avec un compte') },
35
+ { value: 'restricted', label: t('Accès restreint') },
36
36
  ]
37
37
  </script>