@datagouv/components-next 1.0.2-dev.20 → 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 (165) 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-Ds3VQrRi.js → Datafair.client-CFTmBFoY.js} +9 -9
  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-B2BijMx5.js → MapContainer.client-D9N3664r.js} +41 -42
  11. package/dist/{OSM-s40W6sQ2.js → OSM-CcrUhItr.js} +15 -23
  12. package/dist/{PdfPreview.client-DJBMzrlC.js → PdfPreview.client-C6A6ZiKl.js} +835 -856
  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-BFQX7Ccz.js → index-B_tPYi5Z.js} +34094 -27864
  25. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-PeylHk0o.js} +162 -164
  26. package/dist/{main-vqhkVhVS.js → main-CVOZp_Nx.js} +41939 -73132
  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-BcNJo35o.js → vue3-xml-viewer.common-CcF3Xd23.js} +1 -1
  32. package/package.json +47 -38
  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/FormattedDate.vue +81 -0
  53. package/src/components/InfiniteLoader.vue +53 -0
  54. package/src/components/LoadingBlock.vue +4 -1
  55. package/src/components/ObjectCardHeader.vue +11 -4
  56. package/src/components/OpenApiViewer/ContentTypeSelect.vue +48 -0
  57. package/src/components/OpenApiViewer/EndpointRequest.vue +164 -0
  58. package/src/components/OpenApiViewer/EndpointResponses.vue +149 -0
  59. package/src/components/OpenApiViewer/OpenApiViewer.vue +308 -0
  60. package/src/components/OpenApiViewer/SchemaPanel.vue +53 -0
  61. package/src/components/OpenApiViewer/SchemaTree.vue +77 -0
  62. package/src/components/OpenApiViewer/openapi.ts +150 -0
  63. package/src/components/OrganizationNameWithCertificate.vue +3 -2
  64. package/src/components/Pagination.vue +25 -62
  65. package/src/components/PostCard.vue +13 -15
  66. package/src/components/RadioInput.vue +7 -2
  67. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  68. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  69. package/src/components/ResourceAccordion/Downloads.vue +172 -0
  70. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  71. package/src/components/ResourceAccordion/JsonPreview.client.vue +23 -104
  72. package/src/components/ResourceAccordion/MapContainer.client.vue +2 -11
  73. package/src/components/ResourceAccordion/Metadata.vue +4 -6
  74. package/src/components/ResourceAccordion/PdfPreview.client.vue +58 -97
  75. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  76. package/src/components/ResourceAccordion/Pmtiles.client.vue +28 -13
  77. package/src/components/ResourceAccordion/Preview.vue +19 -15
  78. package/src/components/ResourceAccordion/PreviewWrapper.vue +89 -0
  79. package/src/components/ResourceAccordion/ResourceAccordion.vue +36 -117
  80. package/src/components/ResourceAccordion/XmlPreview.client.vue +16 -98
  81. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
  82. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  83. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  84. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +143 -291
  85. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  86. package/src/components/ResourceExplorer/ResourceSelector.vue +51 -0
  87. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +122 -0
  88. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +94 -0
  89. package/src/components/ResourceListItem.vue +168 -0
  90. package/src/components/ReuseCard.vue +28 -9
  91. package/src/components/ReuseDetails.vue +2 -2
  92. package/src/components/ReuseHorizontalCard.vue +2 -2
  93. package/src/components/SchemaCard.vue +10 -1
  94. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  95. package/src/components/Search/GlobalSearch.vue +229 -123
  96. package/src/components/Search/SearchInput.vue +5 -4
  97. package/src/components/Search/Sidemenu.vue +4 -3
  98. package/src/components/SmallChart.vue +11 -10
  99. package/src/components/StatBox.vue +13 -5
  100. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  101. package/src/components/TabularExplorer/TabularCell.vue +50 -0
  102. package/src/components/TabularExplorer/TabularCellPopover.vue +170 -0
  103. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  104. package/src/components/TabularExplorer/TabularExplorer.vue +299 -0
  105. package/src/components/TabularExplorer/TabularFilterContent.vue +362 -0
  106. package/src/components/TabularExplorer/TabularFilterPopover.vue +106 -0
  107. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  108. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  109. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  110. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  111. package/src/components/TabularExplorer/TabularTable.vue +481 -0
  112. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  113. package/src/components/TabularExplorer/types.ts +84 -0
  114. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  115. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  116. package/src/components/TopicCard.vue +13 -14
  117. package/src/composables/useActiveDescendant.ts +5 -0
  118. package/src/composables/useDatasetResources.ts +164 -0
  119. package/src/composables/useHasTabularData.ts +15 -1
  120. package/src/composables/useMetrics.ts +1 -1
  121. package/src/composables/useResizable.ts +46 -0
  122. package/src/composables/useResourceCapabilities.ts +48 -40
  123. package/src/composables/useSearchFilter.ts +120 -0
  124. package/src/composables/useStableQueryParams.ts +38 -6
  125. package/src/composables/useTabularProfile.ts +70 -0
  126. package/src/composables/useTranslation.ts +24 -9
  127. package/src/config.ts +21 -3
  128. package/src/functions/activities.ts +3 -3
  129. package/src/functions/api.ts +20 -42
  130. package/src/functions/api.types.ts +1 -0
  131. package/src/functions/charts.ts +99 -0
  132. package/src/functions/datasets.ts +0 -17
  133. package/src/functions/dates.ts +6 -7
  134. package/src/functions/helpers.ts +15 -0
  135. package/src/functions/metrics.ts +63 -60
  136. package/src/functions/paginate.ts +46 -0
  137. package/src/functions/resourceCapabilities.ts +34 -0
  138. package/src/functions/resources.ts +136 -1
  139. package/src/functions/schemas.ts +27 -0
  140. package/src/functions/scroll.ts +20 -0
  141. package/src/functions/tabular.ts +214 -0
  142. package/src/functions/tabularApi.ts +96 -11
  143. package/src/main.ts +119 -14
  144. package/src/types/dataservices.ts +17 -0
  145. package/src/types/organizations.ts +10 -0
  146. package/src/types/pages.ts +0 -5
  147. package/src/types/posts.ts +2 -2
  148. package/src/types/reports.ts +5 -1
  149. package/src/types/resources.ts +0 -1
  150. package/src/types/reuses.ts +12 -1
  151. package/src/types/search.ts +63 -1
  152. package/src/types/site.ts +5 -3
  153. package/src/types/ui.ts +2 -0
  154. package/src/types/users.ts +2 -1
  155. package/src/types/visualizations.ts +93 -0
  156. package/assets/swagger-themes/newspaper.css +0 -1670
  157. package/dist/JsonPreview.client-CYxmz0yV.js +0 -72
  158. package/dist/Pmtiles.client-C_hvRDuy.js +0 -25185
  159. package/dist/Swagger.client-B4y0hXME.js +0 -4
  160. package/dist/XmlPreview.client-LETEfNBH.js +0 -64
  161. package/src/components/ResourceAccordion/Swagger.client.vue +0 -48
  162. package/src/functions/pagination.ts +0 -9
  163. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  164. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  165. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div class="m-0 text-sm text-gray-medium">
3
+ <Toggletip
4
+ :styled-button="false"
5
+ button-class="border-transparent -outline-offset-2 inline-flex items-center justify-center hover:bg-gray-lower transition-colors"
6
+ :button-props="{ class: 'relative z-2 gap-1 rounded-sm px-1 -mx-1 group/quality', title: t('Qualité des métadonnées') }"
7
+ >
8
+ <RiInformationLine class="size-3.5 shrink-0" />
9
+ <span class="text-gray-medium text-sm group-hover/quality:underline">{{ t('Métadonnées :') }}</span>
10
+ <QualityScore :score="quality.score" />
11
+ <template #toggletip>
12
+ <DataserviceQualityTooltipContent :quality />
13
+ </template>
14
+ </Toggletip>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { RiInformationLine } from '@remixicon/vue'
20
+ import type { DataserviceQuality } from '../types/dataservices'
21
+ import QualityScore from './QualityScore.vue'
22
+ import DataserviceQualityTooltipContent from './DataserviceQualityTooltipContent.vue'
23
+ import Toggletip from './Toggletip.vue'
24
+ import { useTranslation } from '../composables/useTranslation'
25
+
26
+ defineProps<{
27
+ quality: DataserviceQuality
28
+ }>()
29
+
30
+ const { t } = useTranslation()
31
+ </script>
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <p class="text-sm text-gray-plain font-bold mb-0">
3
+ {{ t("Qualité des métadonnées :") }}
4
+ </p>
5
+ <ul class="list-none pl-0 space-y-2">
6
+ <QualityItem
7
+ :passed="quality.has_description"
8
+ :message-passed="t('Description renseignée')"
9
+ :message-failed="t('Description non renseignée')"
10
+ class="pb-0"
11
+ />
12
+ <QualityItem
13
+ :passed="quality.has_machine_documentation"
14
+ :message-passed="t('Documentation machine (OpenAPI) disponible')"
15
+ :message-failed="t('Documentation machine (OpenAPI) manquante')"
16
+ class="pb-0"
17
+ />
18
+ <QualityItem
19
+ :passed="quality.has_technical_documentation"
20
+ :message-passed="t('Documentation technique renseignée')"
21
+ :message-failed="t('Documentation technique manquante')"
22
+ class="pb-0"
23
+ />
24
+ <QualityItem
25
+ :passed="quality.has_business_documentation"
26
+ :message-passed="t('Documentation métier renseignée')"
27
+ :message-failed="t('Documentation métier manquante')"
28
+ class="pb-0"
29
+ />
30
+ <QualityItem
31
+ :passed="quality.license"
32
+ :message-passed="t('Licence renseignée')"
33
+ :message-failed="t('Licence non renseignée')"
34
+ class="pb-0"
35
+ />
36
+ <QualityItem
37
+ :passed="quality.has_contact_point"
38
+ :message-passed="t('Point de contact renseigné')"
39
+ :message-failed="t('Point de contact non renseigné')"
40
+ class="pb-0"
41
+ />
42
+ <QualityItem
43
+ :passed="quality.has_base_api_url"
44
+ :message-passed="t(`URL de base de l'API renseignée`)"
45
+ :message-failed="t(`URL de base de l'API non renseignée`)"
46
+ class="pb-0"
47
+ />
48
+ <QualityItem
49
+ :passed="quality.availability_documented"
50
+ :message-passed="t('Disponibilité documentée')"
51
+ :message-failed="t('Disponibilité non documentée')"
52
+ class="pb-0"
53
+ />
54
+ <QualityItem
55
+ :passed="quality.rate_limiting_documented"
56
+ :message-passed="t(`Limite d'appels documentée`)"
57
+ :message-failed="t(`Limite d'appels non documentée`)"
58
+ class="pb-0"
59
+ />
60
+ <QualityItem
61
+ :passed="quality.access_conditions_clear"
62
+ :message-passed="t(`Conditions d'accès claires`)"
63
+ :message-failed="t(`Conditions d'accès non précisées`)"
64
+ class="pb-0"
65
+ />
66
+ </ul>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import type { DataserviceQuality } from '../types/dataservices'
71
+ import { useTranslation } from '../composables/useTranslation'
72
+ import QualityItem from './QualityItem.vue'
73
+
74
+ defineProps<{
75
+ quality: DataserviceQuality
76
+ }>()
77
+
78
+ const { t } = useTranslation()
79
+ </script>
@@ -36,6 +36,7 @@
36
36
  :icon="RiDatabase2Line"
37
37
  :url="datasetUrl || dataset.page"
38
38
  :target="datasetUrlInNewTab ? '_blank' : undefined"
39
+ :title-tag="titleTag"
39
40
  >
40
41
  {{ dataset.title }}
41
42
  <template
@@ -55,9 +56,19 @@
55
56
  :organization-url="organizationUrl"
56
57
  />
57
58
  <RiSubtractLine class="hidden md:block size-4 flex-none fill-gray-medium" />
58
- <div class="w-full md:w-auto text-gray-medium whitespace-nowrap">
59
- {{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(dataset.last_update, { dateStyle: 'medium' }) }) }}
60
- </div>
59
+ <TranslationT
60
+ tag="div"
61
+ class="w-full md:w-auto text-gray-medium whitespace-nowrap"
62
+ keypath="Mis à jour {date}"
63
+ >
64
+ <template #date>
65
+ <FormattedDate
66
+ :date="dataset.last_update"
67
+ format="relative"
68
+ :options="{ dateStyle: 'medium' }"
69
+ />
70
+ </template>
71
+ </TranslationT>
61
72
  </div>
62
73
  <div class="mx-0 -mb-1 flex flex-wrap items-center text-sm text-gray-medium">
63
74
  <div class="hidden sm:flex text-gray-medium">
@@ -106,10 +117,12 @@
106
117
  </p>
107
118
  </div>
108
119
  </div>
109
- <ObjectCardShortDescription
110
- v-if="showDescriptionShort"
111
- :text="getDescriptionShort(props.dataset)"
112
- />
120
+ <slot>
121
+ <ObjectCardShortDescription
122
+ v-if="showDescriptionShort"
123
+ :text="getDescriptionShort(props.dataset)"
124
+ />
125
+ </slot>
113
126
  </ObjectCard>
114
127
  </template>
115
128
 
@@ -117,8 +130,8 @@
117
130
  import type { RouteLocationRaw } from 'vue-router'
118
131
  import { RiArchiveLine, RiDatabase2Line, RiDownloadLine, RiEyeLine, RiLineChartLine, RiLockLine, RiStarLine, RiSubtractLine } from '@remixicon/vue'
119
132
  import type { Dataset, DatasetV2 } from '../types/datasets'
133
+ import type { TitleTag } from '../types/ui'
120
134
  import { summarize } from '../functions/helpers'
121
- import { useFormatDate } from '../functions/dates'
122
135
  import { getDescriptionShort } from '../functions/description'
123
136
  import { useTranslation } from '../composables/useTranslation'
124
137
  import DatasetQualityInline from './DatasetQualityInline.vue'
@@ -130,6 +143,8 @@ import ObjectCard from './ObjectCard.vue'
130
143
  import ObjectCardBadge from './ObjectCardBadge.vue'
131
144
  import ObjectCardHeader from './ObjectCardHeader.vue'
132
145
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
146
+ import FormattedDate from './FormattedDate.vue'
147
+ import TranslationT from './TranslationT.vue'
133
148
 
134
149
  type Props = {
135
150
  dataset: Dataset | DatasetV2
@@ -137,6 +152,7 @@ type Props = {
137
152
  datasetUrlInNewTab?: boolean
138
153
  organizationUrl?: RouteLocationRaw
139
154
  showDescriptionShort?: boolean
155
+ titleTag?: TitleTag
140
156
  }
141
157
 
142
158
  const props = withDefaults(defineProps<Props>(), {
@@ -144,5 +160,4 @@ const props = withDefaults(defineProps<Props>(), {
144
160
  })
145
161
 
146
162
  const { t } = useTranslation()
147
- const { formatRelativeIfRecentDate } = useFormatDate()
148
163
  </script>
@@ -6,7 +6,9 @@
6
6
  <DescriptionList>
7
7
  <div>
8
8
  <DescriptionListTerm>{{ t('Création') }}</DescriptionListTerm>
9
- <DescriptionListDetails>{{ formatDate(dataset.created_at) }}</DescriptionListDetails>
9
+ <DescriptionListDetails>
10
+ <FormattedDate :date="dataset.created_at" />
11
+ </DescriptionListDetails>
10
12
  </div>
11
13
  <div v-if="dataset.frequency">
12
14
  <DescriptionListTerm>{{ t('Fréquence') }}</DescriptionListTerm>
@@ -20,7 +22,9 @@
20
22
  </div>
21
23
  <div>
22
24
  <DescriptionListTerm>{{ t('Dernière mise à jour') }}</DescriptionListTerm>
23
- <DescriptionListDetails>{{ formatDate(dataset.last_update) }}</DescriptionListDetails>
25
+ <DescriptionListDetails>
26
+ <FormattedDate :date="dataset.last_update" />
27
+ </DescriptionListDetails>
24
28
  </div>
25
29
  <slot />
26
30
  </DescriptionList>
@@ -29,17 +33,16 @@
29
33
 
30
34
  <script setup lang="ts">
31
35
  import { useTranslation } from '../../composables/useTranslation'
32
- import { useFormatDate } from '../../functions/dates'
33
36
  import type { DatasetV2WithFullObject } from '../../types/datasets'
34
37
  import DescriptionList from '../DescriptionList.vue'
35
38
  import DescriptionListTerm from '../DescriptionListTerm.vue'
36
39
  import DescriptionListDetails from '../DescriptionListDetails.vue'
37
40
  import DateRangeDetails from '../DateRangeDetails.vue'
41
+ import FormattedDate from '../FormattedDate.vue'
38
42
 
39
43
  defineProps<{
40
44
  dataset: DatasetV2WithFullObject
41
45
  }>()
42
46
 
43
47
  const { t } = useTranslation()
44
- const { formatDate } = useFormatDate()
45
48
  </script>
@@ -13,7 +13,7 @@
13
13
  {{ t('Qualité des métadonnées:') }}
14
14
  </span>
15
15
  </span>
16
- <DatasetQualityScore
16
+ <QualityScore
17
17
  :score="quality.score"
18
18
  class="w-full"
19
19
  />
@@ -23,35 +23,35 @@
23
23
  </Toggletip>
24
24
  <template v-if="!hideWarnings">
25
25
  <ul class="list-none pl-0">
26
- <DatasetQualityItemWarning
26
+ <QualityItemWarning
27
27
  :quality-item="quality.dataset_description_quality"
28
28
  :message="t('Description des données non renseignée')"
29
29
  />
30
- <DatasetQualityItemWarning
30
+ <QualityItemWarning
31
31
  :quality-item="quality.resources_documentation"
32
32
  :message="t('Documentation des fichiers manquante')"
33
33
  />
34
- <DatasetQualityItemWarning
34
+ <QualityItemWarning
35
35
  :quality-item="quality.license"
36
36
  :message="t('Licence non renseignée')"
37
37
  />
38
- <DatasetQualityItemWarning
38
+ <QualityItemWarning
39
39
  :quality-item="quality.update_frequency && quality.update_fulfilled_in_time"
40
40
  :message="quality.update_frequency ? t('Fréquence de mise à jour non respectée') : t('Fréquence de mise à jour non renseignée')"
41
41
  />
42
- <DatasetQualityItemWarning
42
+ <QualityItemWarning
43
43
  :quality-item="quality.has_open_format"
44
44
  :message="t('Formats de fichiers non standards')"
45
45
  />
46
- <DatasetQualityItemWarning
46
+ <QualityItemWarning
47
47
  :quality-item="quality.temporal_coverage"
48
48
  :message="t('Couverture temporelle non renseignée')"
49
49
  />
50
- <DatasetQualityItemWarning
50
+ <QualityItemWarning
51
51
  :quality-item="quality.spatial"
52
52
  :message="t('Couverture spatiale non renseignée')"
53
53
  />
54
- <DatasetQualityItemWarning
54
+ <QualityItemWarning
55
55
  :quality-item="quality.all_resources_available"
56
56
  :message="t('Certains fichiers ne sont pas disponibles')"
57
57
  />
@@ -62,8 +62,8 @@
62
62
  <script setup lang="ts">
63
63
  import { RiInformationLine } from '@remixicon/vue'
64
64
  import type { Quality } from '../types/datasets'
65
- import DatasetQualityItemWarning from './DatasetQualityItemWarning.vue'
66
- import DatasetQualityScore from './DatasetQualityScore.vue'
65
+ import QualityItemWarning from './QualityItemWarning.vue'
66
+ import QualityScore from './QualityScore.vue'
67
67
  import Toggletip from './Toggletip.vue'
68
68
  import DatasetQualityTooltipContent from './DatasetQualityTooltipContent.vue'
69
69
  import { useTranslation } from '../composables/useTranslation'
@@ -7,7 +7,7 @@
7
7
  >
8
8
  <RiInformationLine class="size-3.5 shrink-0" />
9
9
  <span class="text-gray-medium text-sm group-hover/quality:underline">{{ t('Métadonnées :') }}</span>
10
- <DatasetQualityScore :score="quality.score" />
10
+ <QualityScore :score="quality.score" />
11
11
  <template #toggletip>
12
12
  <DatasetQualityTooltipContent :quality />
13
13
  </template>
@@ -18,7 +18,7 @@
18
18
  <script setup lang="ts">
19
19
  import { RiInformationLine } from '@remixicon/vue'
20
20
  import type { Quality } from '../types/datasets'
21
- import DatasetQualityScore from './DatasetQualityScore.vue'
21
+ import QualityScore from './QualityScore.vue'
22
22
  import DatasetQualityTooltipContent from './DatasetQualityTooltipContent.vue'
23
23
  import Toggletip from './Toggletip.vue'
24
24
  import { useTranslation } from '../composables/useTranslation'
@@ -68,7 +68,7 @@
68
68
  import type { Quality } from '../types/datasets'
69
69
  import { useComponentsConfig } from '../config'
70
70
  import { useTranslation } from '../composables/useTranslation'
71
- import DatasetQualityItem from './DatasetQualityItem.vue'
71
+ import DatasetQualityItem from './QualityItem.vue'
72
72
 
73
73
  defineProps<{
74
74
  quality: Quality
@@ -20,9 +20,18 @@
20
20
  aria-hidden="true"
21
21
  class="size-4 flex-none fill-gray-medium"
22
22
  />
23
- <span class="text-gray-medium whitespace-nowrap">
24
- {{ t('Posté {date}', { date: formatDate(discussion.created) }) }}
25
- </span>
23
+ <TranslationT
24
+ class="text-gray-medium whitespace-nowrap"
25
+ keypath="Posté {date}"
26
+ >
27
+ <template #date>
28
+ <FormattedDate
29
+ :date="discussion.created"
30
+ format="relative"
31
+ :options="{ dateStyle: 'long', timeStyle: 'short' }"
32
+ />
33
+ </template>
34
+ </TranslationT>
26
35
  </div>
27
36
 
28
37
  <ObjectCardShortDescription :text="firstMessageContent" />
@@ -35,13 +44,13 @@
35
44
  import { RiChat3Line, RiSubtractLine } from '@remixicon/vue'
36
45
  import { computed } from 'vue'
37
46
  import type { RouteLocationRaw } from 'vue-router'
38
- import { useFormatDate } from '../functions/dates'
39
- import { useTranslation } from '../composables/useTranslation'
40
47
  import type { Thread } from '../types/discussions'
41
48
  import ObjectCard from './ObjectCard.vue'
42
49
  import ObjectCardHeader from './ObjectCardHeader.vue'
43
50
  import ObjectCardOwner from './ObjectCardOwner.vue'
44
51
  import ObjectCardShortDescription from './ObjectCardShortDescription.vue'
52
+ import FormattedDate from './FormattedDate.vue'
53
+ import TranslationT from './TranslationT.vue'
45
54
 
46
55
  const props = defineProps<{
47
56
  discussion: Thread
@@ -49,15 +58,5 @@ const props = defineProps<{
49
58
  organizationUrl?: RouteLocationRaw
50
59
  }>()
51
60
 
52
- const { t } = useTranslation()
53
- const { formatRelativeIfRecentDate } = useFormatDate()
54
-
55
61
  const firstMessageContent = computed(() => props.discussion.discussion?.[0]?.content)
56
-
57
- const formatDate = (dateString: string) => {
58
- return formatRelativeIfRecentDate(dateString, {
59
- dateStyle: 'long',
60
- timeStyle: 'short',
61
- })
62
- }
63
62
  </script>
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <Listbox v-model="model">
3
+ <div class="relative min-w-0">
4
+ <div
5
+ ref="floatingReference"
6
+ class="relative w-full cursor-default overflow-hidden bg-white text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-teal-300 sm:text-sm"
7
+ >
8
+ <ListboxButton class="input shadow-input text-sm flex items-center gap-2">
9
+ <slot name="button">
10
+ <div class="w-full flex items-center justify-between gap-2">
11
+ <div
12
+ class="truncate"
13
+ :class="{ 'text-new-disabled-text': isDisabled(model) }"
14
+ >
15
+ {{ displayValue(model) }}
16
+ </div>
17
+ <RiArrowDownSLine class="flex-none size-4 justify-self-end" />
18
+ </div>
19
+ </slot>
20
+ </ListboxButton>
21
+ </div>
22
+
23
+ <ListboxOptions
24
+ ref="popover"
25
+ :style="floatingStyles"
26
+ class="z-10 mt-1 absolute max-h-60 min-w-80 w-full overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm pl-0"
27
+ >
28
+ <ListboxOption
29
+ v-for="option in options"
30
+ :key="getOptionId(toValue(option))"
31
+ v-slot="{ active, selected }"
32
+ as="template"
33
+ :value="option"
34
+ >
35
+ <li
36
+ class="relative cursor-default select-none py-2 pr-4 list-none flex items-center gap-2 text-gray-900"
37
+ :class="{
38
+ 'bg-gray-lower': active && !isDisabled(toValue(option)),
39
+ 'text-new-disabled-text': isDisabled(toValue(option)),
40
+ 'pl-2': selected,
41
+ 'pl-6': !selected,
42
+ }"
43
+ >
44
+ <div class="flex items-center justify-center aspect-square">
45
+ <RiCheckLine
46
+ v-if="selected"
47
+ class="size-4"
48
+ :class="isDisabled(toValue(option)) ?' text-new-disabled-text' : 'text-new-primary'"
49
+ />
50
+ </div>
51
+ <slot
52
+ name="option"
53
+ v-bind="{ option, active }"
54
+ >
55
+ {{ displayValue(option) }}
56
+ </slot>
57
+ </li>
58
+ </ListboxOption>
59
+ </ListboxOptions>
60
+ </div>
61
+ </Listbox>
62
+ </template>
63
+
64
+ <script setup lang="ts" generic="T extends string | number | object">
65
+ import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/vue'
66
+ import { useFloating, autoUpdate, autoPlacement } from '@floating-ui/vue'
67
+ import { toValue, useTemplateRef } from 'vue'
68
+ import { RiArrowDownSLine, RiCheckLine } from '@remixicon/vue'
69
+
70
+ withDefaults(defineProps<{
71
+ options?: Array<T>
72
+ getOptionId?: (option: T) => string | number
73
+ displayValue: (option: T | null) => string
74
+ isDisabled?: (option: T | null) => boolean
75
+ }>(), {
76
+ getOptionId: (option: T): string | number => {
77
+ if (typeof option === 'string') return option
78
+ if (typeof option === 'number') return option
79
+ if (typeof option === 'object' && 'id' in option) return option.id as string
80
+
81
+ throw new Error('Please set getOptionId()')
82
+ },
83
+ isDisabled: (option: T | null): boolean => {
84
+ if (option && typeof option === 'object' && 'disabled' in option) return option.disabled as boolean
85
+
86
+ return false
87
+ },
88
+ })
89
+
90
+ const model = defineModel<T | null>({ required: true })
91
+
92
+ const referenceRef = useTemplateRef('floatingReference')
93
+ const floatingRef = useTemplateRef<InstanceType<typeof ListboxOptions>>('popover')
94
+ const { floatingStyles } = useFloating(referenceRef, floatingRef, {
95
+ middleware: [autoPlacement({
96
+ allowedPlacements: ['bottom-start', 'bottom', 'bottom-end'],
97
+ crossAxis: true,
98
+ })],
99
+ whileElementsMounted: autoUpdate,
100
+ })
101
+ </script>
@@ -8,6 +8,7 @@
8
8
  :placeholder="t('Tous les schémas')"
9
9
  :get-option-id="(s: RegisteredSchema) => s.name"
10
10
  :display-value="(s: RegisteredSchema) => s.name"
11
+ :filter="schemaMatchesQuery"
11
12
  :multiple="false"
12
13
  />
13
14
  </template>
@@ -16,6 +17,7 @@
16
17
  import { useTranslation } from '../../composables/useTranslation'
17
18
  import { useSelectModelSync } from '../../composables/useSelectModelSync'
18
19
  import { useFetch } from '../../functions/api'
20
+ import { schemaMatchesQuery } from '../../functions/schemas'
19
21
  import type { RegisteredSchema } from '../../types/schemas'
20
22
  import SearchableSelect from './SearchableSelect.vue'
21
23
 
@@ -0,0 +1,81 @@
1
+ <template>
2
+ <!-- An instant is formatted in the server's timezone at render time and in the
3
+ reader's at read time, so its text and `title` legitimately differ and Vue is
4
+ told to accept it. A plain date carries no time, so nothing about it varies —
5
+ exempting it would hide a real bug instead of a legitimate difference. Vue only
6
+ honours `data-allow-mismatch` on an element whose children are plain text,
7
+ never on a text node coming from a slot: hence two static elements instead of a
8
+ `<component :is>`, which would pass the text through a slot. -->
9
+ <time
10
+ v-if="datetime"
11
+ :datetime
12
+ :title
13
+ :data-allow-mismatch="allowMismatch"
14
+ >{{ formatted }}</time>
15
+ <!-- No date to format: nothing here depends on the timezone or on "now". -->
16
+ <span v-else>{{ formatted }}</span>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { computed } from 'vue'
21
+ import { useFormatDate } from '../functions/dates'
22
+
23
+ const props = withDefaults(defineProps<{
24
+ date: Date | string | null | undefined
25
+ /**
26
+ * `date`: the full date. `relative`: "3 days ago" while the date is less than a
27
+ * month old, the full date beyond. `from-now`: always relative, however old.
28
+ */
29
+ format?: 'date' | 'relative' | 'from-now'
30
+ options?: Intl.DateTimeFormatOptions
31
+ }>(), {
32
+ format: 'date',
33
+ options: () => ({}),
34
+ })
35
+
36
+ const { formatDate, formatFromNow, formatRelativeIfRecentDate } = useFormatDate()
37
+
38
+ // `2026`, `2026-04` and `2026-04-24` carry no time of day. `new Date()` reads them as
39
+ // midnight UTC, an instant they never claimed, and then converts it: a reader west of
40
+ // UTC gets the day before — and with it the month, and sometimes the year.
41
+ const PLAIN_DATE = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/
42
+
43
+ const plainDate = computed(() => typeof props.date === 'string' ? props.date.match(PLAIN_DATE) : null)
44
+
45
+ const parsed = computed(() => {
46
+ if (!props.date) return null
47
+ const plain = plainDate.value
48
+ const date = plain
49
+ ? new Date(Number(plain[1]), Number(plain[2] ?? 1) - 1, Number(plain[3] ?? 1))
50
+ : new Date(props.date)
51
+ return isNaN(date.getTime()) ? null : date
52
+ })
53
+
54
+ // Announce the precision the value carries, never more: a month stays a month.
55
+ const datetime = computed(() => {
56
+ if (!parsed.value) return undefined
57
+ return plainDate.value ? plainDate.value[0] : parsed.value.toISOString()
58
+ })
59
+
60
+ // Only what genuinely differs between server and browser is exempted: an instant, and
61
+ // the relative forms, which are measured against "now".
62
+ const allowMismatch = computed(() => !plainDate.value || props.format !== 'date'
63
+ ? 'text,attribute'
64
+ : undefined)
65
+
66
+ const formatted = computed(() => {
67
+ if (props.format === 'relative') return formatRelativeIfRecentDate(parsed.value, props.options)
68
+ if (props.format === 'from-now') return formatFromNow(parsed.value)
69
+ return formatDate(parsed.value, props.options)
70
+ })
71
+
72
+ // Hovering an instant gives the exact moment, which the displayed form usually omits:
73
+ // the relative forms drop it entirely, and most styles drop the time. A plain date has
74
+ // no moment to reveal — a `title` there would be one we made up, and a form that already
75
+ // spells the moment out needs no tooltip repeating it word for word.
76
+ const title = computed(() => {
77
+ if (!parsed.value || plainDate.value) return undefined
78
+ const exact = formatDate(parsed.value, { dateStyle: 'long', timeStyle: 'short' })
79
+ return exact === formatted.value ? undefined : exact
80
+ })
81
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <div ref="sentinel">
3
+ <slot>
4
+ <div class="flex items-center justify-center p-4">
5
+ <span class="inline-flex items-center gap-2 text-xs text-gray-medium">
6
+ <RiLoader4Line
7
+ class="size-4 animate-spin"
8
+ aria-hidden="true"
9
+ />
10
+ {{ t('Chargement…') }}
11
+ </span>
12
+ </div>
13
+ </slot>
14
+ </div>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import { onMounted, onUnmounted, useTemplateRef, watch } from 'vue'
19
+ import { RiLoader4Line } from '@remixicon/vue'
20
+ import { useTranslation } from '../composables/useTranslation'
21
+
22
+ const props = defineProps<{
23
+ root?: HTMLElement | null
24
+ }>()
25
+
26
+ const emit = defineEmits<{
27
+ intersect: []
28
+ }>()
29
+
30
+ const { t } = useTranslation()
31
+
32
+ const sentinelRef = useTemplateRef<HTMLElement>('sentinel')
33
+ let observer: IntersectionObserver | null = null
34
+
35
+ function setupObserver() {
36
+ observer?.disconnect()
37
+ const el = sentinelRef.value
38
+ if (!el) return
39
+ observer = new IntersectionObserver(
40
+ (entries) => {
41
+ if (entries[0]?.isIntersecting) {
42
+ emit('intersect')
43
+ }
44
+ },
45
+ { root: props.root ?? null, rootMargin: '200px' },
46
+ )
47
+ observer.observe(el)
48
+ }
49
+
50
+ onMounted(setupObserver)
51
+ watch([sentinelRef, () => props.root], setupObserver)
52
+ onUnmounted(() => observer?.disconnect())
53
+ </script>
@@ -1,8 +1,11 @@
1
1
  <template>
2
2
  <div class="relative">
3
3
  <div :class="{ 'opacity-50 min-h-64': loading }">
4
+ <!-- Keep the previous content while reloading, dimmed under the loader:
5
+ dropping it collapses the page height, which makes the browser clamp
6
+ the scroll and throws the reader back to the top. -->
4
7
  <slot
5
- v-if="!loading && !hasError && data"
8
+ v-if="!hasError && data"
6
9
  :data="data"
7
10
  />
8
11
  <slot