@datagouv/components-next 1.0.2-dev.2 → 1.0.2-dev.200

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 (173) hide show
  1. package/README.md +7 -6
  2. package/assets/main.css +4 -0
  3. package/dist/{Control-DuZJdKV_.js → Control-B9WQxlpJ.js} +1 -1
  4. package/dist/Datafair.client-CFTmBFoY.js +30 -0
  5. package/dist/{Event--kp8kMdJ.js → Event-iXDrp-fy.js} +31 -31
  6. package/dist/{Image-34hvypZI.js → Image-mNMBOofs.js} +9 -9
  7. package/dist/ImagePreview.client-BvDM3qjE.js +36 -0
  8. package/dist/JsonPreview.client-BP6gPRxf.js +40 -0
  9. package/dist/{Map-BjUnLyj8.js → Map-BfqKMw0S.js} +1739 -1599
  10. package/dist/MapContainer.client-D9N3664r.js +101 -0
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-C-w6-w44.js → PdfPreview.client-C6A6ZiKl.js} +835 -871
  13. package/dist/Pmtiles.client-B_sDZoqu.js +26333 -0
  14. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-DGLXBys_.js +60 -0
  15. package/dist/{ScaleLine-KW-nXqp3.js → ScaleLine-VGQ7nkMS.js} +2 -2
  16. package/dist/{Tile-DbNFNPfU.js → Tile-CSpn0Ca8.js} +160 -163
  17. package/dist/{TileImage-BsXBxMtq.js → TileImage-CpbaEFwd.js} +143 -131
  18. package/dist/{View-BR92hTWP.js → View-BYBpt-AU.js} +483 -422
  19. package/dist/XmlPreview.client-DFQ1T-LN.js +34 -0
  20. package/dist/{common-PJfpC179.js → common-BoeTUnDZ.js} +52 -52
  21. package/dist/components-next.css +1 -52
  22. package/dist/components-next.js +196 -150
  23. package/dist/components.css +2 -2
  24. package/dist/{index-SrYZwgCT.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/main-CVOZp_Nx.js +75398 -0
  27. package/dist/{proj-DsetBcW7.js → proj-DxGbobp7.js} +662 -626
  28. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  29. package/dist/{tilecoord-Db24Px13.js → tilecoord-DvX7WWiA.js} +273 -264
  30. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  31. package/dist/{vue3-xml-viewer.common-BRxsqI9j.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +50 -37
  33. package/src/chart.ts +5 -0
  34. package/src/components/ActivityList/ActivityList.vue +24 -14
  35. package/src/components/AppLink.vue +3 -0
  36. package/src/components/BannerAction.vue +6 -2
  37. package/src/components/Chart/ChartViewer.vue +320 -0
  38. package/src/components/Chart/ChartViewerWrapper.vue +182 -0
  39. package/src/components/CopyButton.vue +6 -2
  40. package/src/components/DataserviceCard.vue +24 -5
  41. package/src/components/DataserviceQuality.vue +82 -0
  42. package/src/components/DataserviceQualityInline.vue +31 -0
  43. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  44. package/src/components/DatasetCard.vue +24 -9
  45. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  46. package/src/components/DatasetQuality.vue +11 -11
  47. package/src/components/DatasetQualityInline.vue +2 -2
  48. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  49. package/src/components/DiscussionMessageCard.vue +14 -15
  50. package/src/components/Form/Listbox.vue +101 -0
  51. package/src/components/Form/SchemaSelect.vue +2 -0
  52. package/src/components/Form/SearchableSelect.vue +2 -1
  53. package/src/components/FormattedDate.vue +81 -0
  54. package/src/components/InfiniteLoader.vue +53 -0
  55. package/src/components/LoadingBlock.vue +4 -1
  56. package/src/components/ObjectCardHeader.vue +11 -4
  57. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  58. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  59. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  60. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  61. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  62. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  63. package/src/components/OpenApiViewer/openapi.ts +150 -0
  64. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  65. package/src/components/Pagination.vue +25 -62
  66. package/src/components/PostCard.vue +13 -15
  67. package/src/components/RadioInput.vue +7 -2
  68. package/src/components/ReadMore.vue +1 -1
  69. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  70. package/src/components/ResourceAccordion/Datafair.client.vue +4 -10
  71. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  72. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  73. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -121
  74. package/src/components/ResourceAccordion/MapContainer.client.vue +6 -19
  75. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  76. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -113
  77. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  78. package/src/components/ResourceAccordion/Pmtiles.client.vue +33 -23
  79. package/src/components/ResourceAccordion/Preview.vue +24 -25
  80. package/src/components/ResourceAccordion/PreviewLoader.vue +1 -2
  81. package/src/components/ResourceAccordion/PreviewUnavailable.vue +22 -0
  82. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  83. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  84. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -115
  85. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -199
  86. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  87. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  88. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -280
  89. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  90. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  91. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  92. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  93. package/src/components/ResourceListItem.vue +168 -0
  94. package/src/components/ReuseCard.vue +28 -9
  95. package/src/components/ReuseDetails.vue +2 -2
  96. package/src/components/ReuseHorizontalCard.vue +2 -2
  97. package/src/components/SchemaCard.vue +10 -1
  98. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  99. package/src/components/Search/GlobalSearch.vue +229 -123
  100. package/src/components/Search/SearchInput.vue +5 -4
  101. package/src/components/Search/Sidemenu.vue +4 -3
  102. package/src/components/SmallChart.vue +11 -10
  103. package/src/components/StatBox.vue +13 -5
  104. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  105. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  106. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  107. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  108. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  109. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  110. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  111. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  112. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  113. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  114. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  115. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  116. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  117. package/src/components/TabularExplorer/types.ts +84 -0
  118. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  119. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  120. package/src/components/TopicCard.vue +13 -14
  121. package/src/composables/useActiveDescendant.ts +5 -0
  122. package/src/composables/useDatasetResources.ts +164 -0
  123. package/src/composables/useHasTabularData.ts +15 -1
  124. package/src/composables/useMetrics.ts +1 -1
  125. package/src/composables/useResizable.ts +46 -0
  126. package/src/composables/useResourceCapabilities.ts +49 -41
  127. package/src/composables/useSearchFilter.ts +120 -0
  128. package/src/composables/useStableQueryParams.ts +38 -6
  129. package/src/composables/useTabularProfile.ts +70 -0
  130. package/src/composables/useTranslation.ts +24 -9
  131. package/src/config.ts +21 -3
  132. package/src/functions/activities.ts +3 -3
  133. package/src/functions/api.ts +20 -42
  134. package/src/functions/api.types.ts +1 -0
  135. package/src/functions/charts.ts +99 -0
  136. package/src/functions/datasets.ts +0 -17
  137. package/src/functions/dates.ts +6 -7
  138. package/src/functions/helpers.ts +15 -0
  139. package/src/functions/metrics.ts +63 -60
  140. package/src/functions/paginate.ts +46 -0
  141. package/src/functions/resourceCapabilities.ts +34 -0
  142. package/src/functions/resources.ts +136 -1
  143. package/src/functions/schemas.ts +27 -0
  144. package/src/functions/scroll.ts +20 -0
  145. package/src/functions/tabular.ts +214 -0
  146. package/src/functions/tabularApi.ts +96 -11
  147. package/src/main.ts +119 -14
  148. package/src/types/dataservices.ts +17 -0
  149. package/src/types/organizations.ts +11 -1
  150. package/src/types/pages.ts +0 -5
  151. package/src/types/posts.ts +2 -2
  152. package/src/types/reports.ts +5 -1
  153. package/src/types/resources.ts +0 -1
  154. package/src/types/reuses.ts +12 -1
  155. package/src/types/search.ts +63 -1
  156. package/src/types/site.ts +5 -3
  157. package/src/types/ui.ts +2 -0
  158. package/src/types/users.ts +2 -1
  159. package/src/types/visualizations.ts +93 -0
  160. package/assets/swagger-themes/newspaper.css +0 -1670
  161. package/dist/Datafair.client-E5D6ePRC.js +0 -35
  162. package/dist/JsonPreview.client-C-6eBbPw.js +0 -87
  163. package/dist/MapContainer.client-DRkAmdOc.js +0 -105
  164. package/dist/Pmtiles.client-BR7_ldHY.js +0 -25190
  165. package/dist/Swagger.client-D4-F6yEf.js +0 -4
  166. package/dist/XmlPreview.client-Dl2VCgXF.js +0 -79
  167. package/dist/main-B2kXxWRG.js +0 -105833
  168. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  169. package/src/functions/pagination.ts +0 -9
  170. /package/assets/illustrations/{_microscope.svg → microscope.svg} +0 -0
  171. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  172. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  173. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -1,65 +1,31 @@
1
1
  <template>
2
- <div class="fr-text--xs">
3
- <div v-if="jsonData">
4
- <JsonViewer
5
- :value="jsonData"
6
- boxed
7
- sort
8
- theme="light"
9
- :max-depth="3"
10
- :expand-depth="2"
11
- :indent-width="2"
12
- />
13
- </div>
14
- <div
15
- v-else-if="loading"
16
- class="text-gray-medium"
17
- >
18
- {{ t("Chargement de l'aperçu JSON...") }}
19
- </div>
20
- <SimpleBanner
21
- v-else-if="fileTooLarge"
22
- type="warning"
23
- class="flex items-center space-x-2"
24
- >
25
- <RiErrorWarningLine class="shrink-0 size-6" />
26
- <span>{{ fileSizeBytes
27
- ? t("Fichier JSON trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
28
- : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
29
- }}</span>
30
- </SimpleBanner>
31
- <SimpleBanner
32
- v-else-if="error === 'network'"
33
- type="warning"
34
- class="flex items-center space-x-2"
35
- >
36
- <RiErrorWarningLine class="shrink-0 size-6" />
37
- <span>{{ t("Ce fichier JSON ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
38
- </SimpleBanner>
39
- <SimpleBanner
40
- v-else-if="error"
41
- type="warning"
42
- class="flex items-center space-x-2"
43
- >
44
- <RiErrorWarningLine class="shrink-0 size-6" />
45
- <span>{{ t("Erreur lors du chargement de l'aperçu JSON.") }}</span>
46
- </SimpleBanner>
47
- </div>
2
+ <PreviewWrapper
3
+ v-slot="{ data }"
4
+ file-type="JSON"
5
+ :resource="resource"
6
+ :max-size="config.maxJsonPreviewCharSize"
7
+ :load="load"
8
+ >
9
+ <JsonViewer
10
+ :value="data"
11
+ boxed
12
+ sort
13
+ theme="light"
14
+ :max-depth="3"
15
+ :expand-depth="2"
16
+ :indent-width="2"
17
+ />
18
+ </PreviewWrapper>
48
19
  </template>
49
20
 
50
21
  <script setup lang="ts">
51
- import { computed, defineAsyncComponent, onMounted, ref } from 'vue'
52
- import { RiErrorWarningLine } from '@remixicon/vue'
53
-
22
+ import { defineAsyncComponent } from 'vue'
54
23
  import { useComponentsConfig } from '../../config'
55
- import SimpleBanner from '../SimpleBanner.vue'
24
+ import PreviewWrapper from './PreviewWrapper.vue'
56
25
  import type { Resource } from '../../types/resources'
57
- import { useTranslation } from '../../composables/useTranslation'
58
- import { getResourceFilesize } from '../../functions/datasets'
59
26
 
60
27
  const JsonViewer = defineAsyncComponent(() =>
61
28
  import('vue3-json-viewer').then((module) => {
62
- // Import CSS when component loads
63
29
  import('vue3-json-viewer/dist/vue3-json-viewer.css')
64
30
  return module.JsonViewer
65
31
  }),
@@ -70,74 +36,10 @@ const props = defineProps<{
70
36
  }>()
71
37
 
72
38
  const config = useComponentsConfig()
73
- const { t } = useTranslation()
74
-
75
- const jsonData = ref<unknown>(null)
76
- const loading = ref(false)
77
- const error = ref<string | null>(null)
78
- const fileTooLarge = ref(false)
79
-
80
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
81
-
82
- const shouldLoadJson = computed(() => {
83
- const size = fileSizeBytes.value
84
- if (!size) {
85
- // If we don't know the size, don't risk loading a potentially huge file
86
- return false
87
- }
88
-
89
- // Check if maxJsonPreviewCharSize is configured
90
- if (!config.maxJsonPreviewCharSize) {
91
- // If no limit is set, don't load unknown files
92
- return false
93
- }
94
-
95
- // Convert maxJsonPreviewCharSize from characters to bytes (rough estimate)
96
- // Assuming average 1 byte per character for JSON
97
- const maxByteSize = config.maxJsonPreviewCharSize
98
-
99
- return size <= maxByteSize
100
- })
101
39
 
102
- const fetchJsonData = async () => {
103
- // Check if file is too large or size is unknown before making the request
104
- if (!shouldLoadJson.value) {
105
- fileTooLarge.value = true
106
- return
107
- }
108
-
109
- loading.value = true
110
- error.value = null
111
-
112
- try {
113
- const response = await fetch(props.resource.url)
114
- // const response = await fetch('/test-data.json') // For testing locally without CORS issues
115
- if (!response.ok) {
116
- throw new Error(`HTTP error! status: ${response.status}`)
117
- }
118
- const data = await response.json()
119
-
120
- // Use the original data directly - let the JSON viewer handle large files
121
- jsonData.value = data
122
- }
123
- catch (err) {
124
- console.error('Error loading JSON:', err)
125
-
126
- if (err instanceof TypeError) {
127
- error.value = 'network'
128
- }
129
- else {
130
- error.value = 'generic'
131
- }
132
-
133
- jsonData.value = null
134
- }
135
- finally {
136
- loading.value = false
137
- }
40
+ const load = async () => {
41
+ const response = await fetch(props.resource.url)
42
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
43
+ return response.json()
138
44
  }
139
-
140
- onMounted(() => {
141
- fetchJsonData()
142
- })
143
45
  </script>
@@ -1,24 +1,17 @@
1
1
  <template>
2
- <SimpleBanner
3
- v-if="hasError"
4
- type="warning"
5
- class="flex items-center space-x-2"
6
- >
7
- <RiErrorWarningLine class="shrink-0 size-6" />
8
- <span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
9
- </SimpleBanner>
2
+ <PreviewUnavailable v-if="hasError">
3
+ {{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
4
+ </PreviewUnavailable>
10
5
  <div
11
6
  v-else
12
- id="map"
13
7
  ref="mapRef"
8
+ class="size-full"
14
9
  />
15
10
  </template>
16
11
 
17
12
  <script setup lang = "ts">
18
13
  import { onMounted, ref, useTemplateRef } from 'vue'
19
- import { RiErrorWarningLine } from '@remixicon/vue'
20
-
21
- import SimpleBanner from '../SimpleBanner.vue'
14
+ import PreviewUnavailable from './PreviewUnavailable.vue'
22
15
  import type { Resource } from '../../types/resources'
23
16
  import { useTranslation } from '../../composables/useTranslation'
24
17
 
@@ -96,6 +89,7 @@ async function displayMap() {
96
89
 
97
90
  const attributions = new GeoportalAttribution({
98
91
  position: 'bottom-right',
92
+ collapsed: false,
99
93
  })
100
94
  map.addControl(attributions)
101
95
 
@@ -134,10 +128,3 @@ onMounted(() => {
134
128
  displayMap()
135
129
  })
136
130
  </script>
137
-
138
- <style>
139
- #map {
140
- width: 100%;
141
- height: 500px;
142
- }
143
- </style>
@@ -4,12 +4,11 @@ import type { Resource } from '../../types/resources'
4
4
  import CopyButton from '../CopyButton.vue'
5
5
  import DescriptionDetails from '../DescriptionDetails.vue'
6
6
  import DescriptionTerm from '../DescriptionTerm.vue'
7
- import { useFormatDate } from '../../functions/dates'
7
+ import FormattedDate from '../FormattedDate.vue'
8
8
  import { filesize } from '../../functions/helpers'
9
9
  import ExtraAccordion from '../ExtraAccordion.vue'
10
- import { getResourceTitleId, getResourceLabel } from '../../functions/resources'
10
+ import { getResourceTitleId, getResourceLabel, getResourceFilesize } from '../../functions/resources'
11
11
  import { useTranslation } from '../../composables/useTranslation'
12
- import { getResourceFilesize } from '../../functions/datasets'
13
12
 
14
13
  const props = defineProps<{
15
14
  resource: Resource
@@ -20,7 +19,6 @@ const resourceTitleId = computed(() => getResourceTitleId(props.resource))
20
19
  const resourceFilesize = computed(() => getResourceFilesize(props.resource))
21
20
 
22
21
  const { t } = useTranslation()
23
- const { formatDate } = useFormatDate()
24
22
  </script>
25
23
 
26
24
  <template>
@@ -95,11 +93,11 @@ const { formatDate } = useFormatDate()
95
93
  >
96
94
  <DescriptionTerm>{{ t('Créée le') }}</DescriptionTerm>
97
95
  <DescriptionDetails>
98
- {{ formatDate(resource.created_at) }}
96
+ <FormattedDate :date="resource.created_at" />
99
97
  </DescriptionDetails>
100
98
  <DescriptionTerm>{{ t('Modifiée le') }}</DescriptionTerm>
101
99
  <DescriptionDetails>
102
- {{ formatDate(resource.last_modified) }}
100
+ <FormattedDate :date="resource.last_modified" />
103
101
  </DescriptionDetails>
104
102
  </dl>
105
103
  <dl
@@ -1,81 +1,68 @@
1
1
  <template>
2
- <div class="text-xs">
3
- <div
4
- v-if="pdfReady"
5
- ref="containerRef"
6
- class="w-full overflow-y-auto max-h-[80vh] space-y-3"
7
- >
8
- <canvas
9
- v-for="page in totalPages"
10
- :key="page"
11
- :ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
12
- class="w-full"
13
- />
14
- </div>
15
- <div
16
- v-else-if="loading"
17
- class="text-gray-medium"
18
- >
19
- {{ t("Chargement de l'aperçu PDF...") }}
20
- </div>
21
- <SimpleBanner
22
- v-else-if="fileTooLarge"
23
- type="warning"
24
- class="flex items-center space-x-2"
25
- >
26
- <RiErrorWarningLine class="flex-none size-6" />
27
- <span>{{ fileSizeBytes
28
- ? t("Fichier PDF trop volumineux pour l'aperçu. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
29
- : t("L'aperçu n'est pas disponible car la taille du fichier est inconnue. Pour consulter le fichier complet, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.")
30
- }}</span>
31
- </SimpleBanner>
32
- <SimpleBanner
33
- v-else-if="error === 'network'"
34
- type="warning"
35
- class="flex items-center space-x-2"
36
- >
37
- <RiErrorWarningLine class="flex-none size-6" />
38
- <span>{{ t("Ce fichier PDF ne peut pas être prévisualisé, peut-être parce qu'il est hébergé sur un autre site qui ne l'autorise pas. Pour le consulter, téléchargez-le en cliquant sur le bouton bleu ou depuis l'onglet Téléchargements.") }}</span>
39
- </SimpleBanner>
40
- <SimpleBanner
41
- v-else-if="error"
42
- type="warning"
43
- class="flex items-center space-x-2"
44
- >
45
- <RiErrorWarningLine class="flex-none size-6" />
46
- <span>{{ t("Erreur lors du chargement de l'aperçu PDF. Pour consulter le fichier, téléchargez-le depuis l'onglet Téléchargements.") }}</span>
47
- </SimpleBanner>
48
- </div>
2
+ <PreviewWrapper
3
+ file-type="PDF"
4
+ :resource="resource"
5
+ :max-size="config.maxPdfPreviewByteSize"
6
+ :load="load"
7
+ :fill="fill"
8
+ @loaded="renderAllPages"
9
+ >
10
+ <!-- While pdfjs downloads/parses the document, show the A4 skeleton instead of a
11
+ bare "loading" line — it matches the reader framing below seamlessly. -->
12
+ <template #loading>
13
+ <PdfSkeleton :fill="fill" />
14
+ </template>
15
+
16
+ <!-- Cap the page width (A4 never needs more) and center on a light backdrop so
17
+ it reads like a document reader. The width cap applies in both modes since
18
+ the render width follows the container; `fill` only drives the height
19
+ (fullscreen fills down, inline stays capped). The scroll container is a plain
20
+ block (not a flex) so the trailing `pb` is reliably scrollable — flex + overflow
21
+ drops the end padding of flex items. -->
22
+ <template #default="{ data }">
23
+ <div
24
+ class="overflow-y-auto bg-gray-100"
25
+ :class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
26
+ >
27
+ <div class="flex justify-center p-4 pb-16">
28
+ <div
29
+ ref="containerRef"
30
+ class="w-[800px] max-w-full space-y-3"
31
+ >
32
+ <canvas
33
+ v-for="page in (data as PDFDocumentProxy).numPages"
34
+ :key="page"
35
+ :ref="(el) => setCanvasRef(el as HTMLCanvasElement, page)"
36
+ class="w-full bg-white shadow-sm"
37
+ />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </template>
42
+ </PreviewWrapper>
49
43
  </template>
50
44
 
51
45
  <script setup lang="ts">
52
- import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
53
- import { RiErrorWarningLine } from '@remixicon/vue'
46
+ import { onBeforeUnmount, ref } from 'vue'
54
47
  import * as pdfjsLib from 'pdfjs-dist'
55
48
  import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
56
49
  import type { PDFDocumentProxy } from 'pdfjs-dist'
57
- import SimpleBanner from '../SimpleBanner.vue'
50
+ import PreviewWrapper from './PreviewWrapper.vue'
51
+ import PdfSkeleton from './PdfSkeleton.vue'
58
52
  import { useComponentsConfig } from '../../config'
59
53
  import type { Resource } from '../../types/resources'
60
- import { useTranslation } from '../../composables/useTranslation'
61
- import { getResourceFilesize } from '../../functions/datasets'
62
54
 
63
55
  pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker
64
56
 
65
57
  const props = defineProps<{
66
58
  resource: Resource
59
+ // Fill the available height (fullscreen) instead of the default capped height.
60
+ fill?: boolean
67
61
  }>()
68
62
 
69
63
  const config = useComponentsConfig()
70
- const { t } = useTranslation()
71
64
 
72
65
  const containerRef = ref<HTMLElement | null>(null)
73
- const pdfReady = ref(false)
74
- const loading = ref(false)
75
- const error = ref<string | null>(null)
76
- const fileTooLarge = ref(false)
77
- const totalPages = ref(0)
78
-
79
66
  let pdfDoc: PDFDocumentProxy | null = null
80
67
  const canvasRefs = new Map<number, HTMLCanvasElement>()
81
68
 
@@ -111,64 +98,22 @@ async function renderPage(pageNum: number) {
111
98
  }).promise
112
99
  }
113
100
 
114
- const fileSizeBytes = computed(() => getResourceFilesize(props.resource))
115
-
116
- const shouldLoadPdf = computed(() => {
117
- const size = fileSizeBytes.value
118
- if (!size) {
119
- // If we don't know the size, don't risk loading a potentially huge file
120
- return false
121
- }
122
-
123
- // Use maxPdfPreviewByteSize from config, fallback to 10 MB if not set
124
- const maxByteSize = config.maxPdfPreviewByteSize ?? 10_000_000
125
- return size <= maxByteSize
126
- })
127
-
128
- const loadPdf = async () => {
129
- if (!shouldLoadPdf.value) {
130
- fileTooLarge.value = true
131
- return
132
- }
133
-
134
- loading.value = true
135
- error.value = null
136
-
137
- try {
138
- const loadingTask = pdfjsLib.getDocument({
139
- url: props.resource.url,
140
- isEvalSupported: false,
141
- })
142
-
143
- pdfDoc = await loadingTask.promise
144
- totalPages.value = pdfDoc.numPages
145
- pdfReady.value = true
146
-
147
- await nextTick()
101
+ const load = async () => {
102
+ const loadingTask = pdfjsLib.getDocument({
103
+ url: props.resource.url,
104
+ isEvalSupported: false,
105
+ })
106
+ pdfDoc = await loadingTask.promise
107
+ return pdfDoc
108
+ }
148
109
 
149
- for (let i = 1; i <= pdfDoc.numPages; i++) {
150
- await renderPage(i)
151
- }
152
- }
153
- catch (err) {
154
- console.error('Error loading PDF:', err)
155
-
156
- if (err instanceof TypeError) {
157
- error.value = 'network'
158
- }
159
- else {
160
- error.value = 'generic'
161
- }
162
- }
163
- finally {
164
- loading.value = false
110
+ const renderAllPages = async () => {
111
+ if (!pdfDoc) return
112
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
113
+ await renderPage(i)
165
114
  }
166
115
  }
167
116
 
168
- onMounted(() => {
169
- loadPdf()
170
- })
171
-
172
117
  onBeforeUnmount(() => {
173
118
  pdfDoc?.destroy()
174
119
  })
@@ -0,0 +1,44 @@
1
+ <template>
2
+ <!-- An A4 sheet on the reader backdrop with a few pulsing text lines, matching the
3
+ real PdfPreview framing (centered, capped ~800px, gray backdrop). -->
4
+ <div
5
+ class="overflow-hidden bg-gray-100"
6
+ :class="fill ? 'min-h-0 flex-1' : 'max-h-[80vh]'"
7
+ role="status"
8
+ :aria-label="t('Chargement de l\'aperçu…')"
9
+ >
10
+ <div class="flex justify-center p-4">
11
+ <div class="aspect-[210/297] w-[800px] max-w-full bg-white p-10 shadow-sm">
12
+ <div class="animate-pulse-placeholder space-y-6">
13
+ <div class="space-y-3">
14
+ <div class="h-5 w-1/2 rounded bg-gray-200" />
15
+ <div class="h-3 w-full rounded bg-gray-200" />
16
+ <div class="h-3 w-full rounded bg-gray-200" />
17
+ <div class="h-3 w-5/6 rounded bg-gray-200" />
18
+ </div>
19
+ <div class="space-y-3">
20
+ <div class="h-3 w-full rounded bg-gray-200" />
21
+ <div class="h-3 w-11/12 rounded bg-gray-200" />
22
+ <div class="h-3 w-full rounded bg-gray-200" />
23
+ <div class="h-3 w-3/4 rounded bg-gray-200" />
24
+ </div>
25
+ <div class="space-y-3">
26
+ <div class="h-3 w-full rounded bg-gray-200" />
27
+ <div class="h-3 w-5/6 rounded bg-gray-200" />
28
+ </div>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { useTranslation } from '../../composables/useTranslation'
37
+
38
+ defineProps<{
39
+ // Fill the available height (fullscreen) instead of the default capped height.
40
+ fill?: boolean
41
+ }>()
42
+
43
+ const { t } = useTranslation()
44
+ </script>
@@ -1,20 +1,15 @@
1
1
  <template>
2
- <div>
3
- <SimpleBanner
4
- v-if="hasError"
5
- type="warning"
6
- class="flex items-center space-x-2"
7
- >
8
- <RiErrorWarningLine class="shrink-0 size-6" />
9
- <span>{{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}</span>
10
- </SimpleBanner>
2
+ <div class="flex h-full flex-col">
3
+ <PreviewUnavailable v-if="hasError">
4
+ {{ t("L'aperçu cartographique de ce fichier n'a pas pu être chargé.") }}
5
+ </PreviewUnavailable>
11
6
  <div
12
7
  v-else
13
- class="-mx-4"
8
+ class="flex min-h-0 flex-1 flex-col"
14
9
  >
15
10
  <div
16
11
  v-if="pmtilesViewerUrl"
17
- class="bg-blue-100 text-datagouv fr-hidden fr-unhidden-md p-4"
12
+ class="bg-blue-100 text-datagouv fr-hidden fr-unhidden-md p-4 shrink-0"
18
13
  >
19
14
  <div class="fr-grid-row fr-grid-row--middle fr-grid-row--gutters">
20
15
  <div
@@ -40,12 +35,17 @@
40
35
  </p>
41
36
  </div>
42
37
  </div>
43
- <div
44
- ref="containerRef"
45
- style="height: 600px;"
46
- />
47
- <div class="fr-px-5v fr-pt-5v">
48
- {{ t("Aperçu de la carte mis à jour le {date}", { date: lastUpdate }) }}
38
+ <div class="relative min-h-0 flex-1">
39
+ <div
40
+ ref="containerRef"
41
+ class="size-full"
42
+ />
43
+ <!-- Freshness caption overlaid inside the map (bottom-left, the corner left
44
+ free by the navigation/geolocate controls top-right and the attribution
45
+ bottom-right) so the map itself can use the whole height. -->
46
+ <div class="pointer-events-none absolute bottom-2 left-2 z-10 rounded bg-white/90 px-2 py-1 text-[12px] leading-4 text-gray-medium shadow-sm">
47
+ {{ t("Aperçu de la carte mis à jour le {date}", { date: lastUpdate }) }}
48
+ </div>
49
49
  </div>
50
50
  </div>
51
51
  </div>
@@ -53,7 +53,7 @@
53
53
 
54
54
  <script setup lang="ts">
55
55
  import { computed, onMounted, ref, useTemplateRef } from 'vue'
56
- import { RiErrorWarningLine, RiExternalLinkFill } from '@remixicon/vue'
56
+ import { RiExternalLinkFill } from '@remixicon/vue'
57
57
  import { Protocol, PMTiles } from 'pmtiles'
58
58
  import maplibregl from 'maplibre-gl'
59
59
  import DOMPurify from 'dompurify'
@@ -64,7 +64,7 @@ import type { Resource } from '../../types/resources'
64
64
  import type { Dataset, DatasetV2 } from '../../types/datasets'
65
65
  import BrandedButton from '../BrandedButton.vue'
66
66
  import styleVector from '../../../assets/json/vector.json'
67
- import SimpleBanner from '../SimpleBanner.vue'
67
+ import PreviewUnavailable from './PreviewUnavailable.vue'
68
68
  import { useTranslation } from '../../composables/useTranslation'
69
69
  import franceSvg from './france.svg?raw'
70
70
  import { getOwnerName, getOwnerPage } from '../../functions/owned'
@@ -109,12 +109,22 @@ async function displayMap() {
109
109
  const p = new PMTiles(pmtilesUrl.value)
110
110
  protocol.add(p)
111
111
 
112
- p.getHeader().then((h: { maxZoom: number, centerLon: number, centerLat: number }) => {
112
+ p.getHeader().then((h: { minLon: number, minLat: number, maxLon: number, maxLat: number, centerZoom: number, centerLon: number, centerLat: number }) => {
113
+ // Frame the data's bounding box (carried by the archive header) so we land on
114
+ // the data. Fall back to the header center when the bbox is degenerate — an
115
+ // archive with a single feature, or bounds left unset (all zeroes).
116
+ const hasBounds = h.minLon < h.maxLon && h.minLat < h.maxLat
113
117
  const map = new maplibregl.Map({
114
118
  container: container.value!,
115
- style: styleVector as maplibregl.StyleSpecification,
116
- zoom: h.maxZoom - 2,
117
- center: [h.centerLon, h.centerLat],
119
+ // JSON import is inferred too loosely (e.g. center as number[]) to match
120
+ // maplibre's strict StyleSpecification, so cast through unknown.
121
+ style: styleVector as unknown as maplibregl.StyleSpecification,
122
+ ...(hasBounds
123
+ ? {
124
+ bounds: [[h.minLon, h.minLat], [h.maxLon, h.maxLat]] as [[number, number], [number, number]],
125
+ fitBoundsOptions: { padding: 24 },
126
+ }
127
+ : { center: [h.centerLon, h.centerLat] as [number, number], zoom: h.centerZoom }),
118
128
  })
119
129
  map.addControl(new maplibregl.NavigationControl())
120
130
  map.addControl(