@datagouv/components-next 1.3.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 (131) hide show
  1. package/dist/{Control-b-43UT2B.js → Control-B9WQxlpJ.js} +10 -10
  2. package/dist/{Datafair.client-BrjISlaZ.js → Datafair.client-D_6yzJUG.js} +9 -9
  3. package/dist/{Event-py_AAwJY.js → Event-iXDrp-fy.js} +15 -15
  4. package/dist/{Image-BHQYZWzH.js → Image-mNMBOofs.js} +22 -22
  5. package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
  6. package/dist/{JsonPreview.client-DivFAnbN.js → JsonPreview.client-DSl1kiwC.js} +2 -2
  7. package/dist/{Map-CtP7leHl.js → Map-BfqKMw0S.js} +165 -165
  8. package/dist/{MapContainer.client-Cq8tNLZZ.js → MapContainer.client-BSfpk_rT.js} +17 -17
  9. package/dist/{OSM-gdDSIsDA.js → OSM-CcrUhItr.js} +1 -1
  10. package/dist/{PdfPreview.client-BNBgLsK2.js → PdfPreview.client-BANFeM6K.js} +438 -427
  11. package/dist/{Pmtiles.client-CQi5Rw8W.js → Pmtiles.client-DH9ZrsZR.js} +6177 -5967
  12. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
  13. package/dist/{ScaleLine-hG-FhNqy.js → ScaleLine-VGQ7nkMS.js} +2 -2
  14. package/dist/{Tile-D6rFonpT.js → Tile-CSpn0Ca8.js} +47 -47
  15. package/dist/{TileImage-cfrEgz5y.js → TileImage-CpbaEFwd.js} +44 -44
  16. package/dist/{View-vp7vDb9q.js → View-BYBpt-AU.js} +58 -58
  17. package/dist/{XmlPreview.client-XCsNvnsZ.js → XmlPreview.client-KCB5mwVf.js} +3 -3
  18. package/dist/{common-C8efyyuc.js → common-BoeTUnDZ.js} +58 -58
  19. package/dist/components-next.css +1 -1
  20. package/dist/components-next.js +199 -172
  21. package/dist/components.css +1 -1
  22. package/dist/{index-CXUamc8Z.js → index-DHWzX5vK.js} +4668 -4466
  23. package/dist/{leaflet-src-BRKv9Ksv.js → leaflet-src-pVQfonBI.js} +1 -1
  24. package/dist/{main-DKu5SOAl.js → main-BDcYkhOc.js} +41684 -33432
  25. package/dist/{proj-CsjVeojN.js → proj-DxGbobp7.js} +110 -110
  26. package/dist/{tilecoord-CPcQJnrF.js → tilecoord-DvX7WWiA.js} +94 -94
  27. package/dist/{vue3-xml-viewer.common-BIaEIFYG.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
  28. package/package.json +28 -27
  29. package/src/components/ActivityList/ActivityList.vue +21 -12
  30. package/src/components/AppLink.vue +3 -0
  31. package/src/components/BannerAction.vue +6 -2
  32. package/src/components/Chart/ChartViewer.vue +111 -17
  33. package/src/components/Chart/ChartViewerWrapper.vue +28 -16
  34. package/src/components/ChartCard.vue +112 -0
  35. package/src/components/CopyButton.vue +6 -2
  36. package/src/components/DataserviceCard.vue +23 -6
  37. package/src/components/DataserviceQuality.vue +82 -0
  38. package/src/components/DataserviceQualityInline.vue +31 -0
  39. package/src/components/DataserviceQualityTooltipContent.vue +79 -0
  40. package/src/components/DatasetCard.vue +17 -6
  41. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
  42. package/src/components/DatasetQuality.vue +11 -11
  43. package/src/components/DatasetQualityInline.vue +2 -2
  44. package/src/components/DatasetQualityTooltipContent.vue +1 -1
  45. package/src/components/DiscussionMessageCard.vue +14 -15
  46. package/src/components/Form/SchemaSelect.vue +2 -0
  47. package/src/components/FormattedDate.vue +69 -0
  48. package/src/components/LoadingBlock.vue +4 -1
  49. package/src/components/ObjectCard.vue +4 -1
  50. package/src/components/ObjectCardOwner.vue +13 -1
  51. package/src/components/Pagination.vue +18 -58
  52. package/src/components/Placeholder.vue +3 -2
  53. package/src/components/PostCard.vue +13 -15
  54. package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
  55. package/src/components/ResourceAccordion/Downloads.vue +19 -7
  56. package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
  57. package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
  58. package/src/components/ResourceAccordion/Metadata.vue +21 -5
  59. package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
  60. package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
  61. package/src/components/ResourceAccordion/Pmtiles.client.vue +27 -18
  62. package/src/components/ResourceAccordion/Preview.vue +8 -4
  63. package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
  64. package/src/components/ResourceAccordion/ResourceAccordion.vue +29 -9
  65. package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -209
  66. package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
  67. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
  68. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +136 -183
  69. package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
  70. package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
  71. package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
  72. package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
  73. package/src/components/ResourceListItem.vue +168 -0
  74. package/src/components/ReuseCard.vue +18 -6
  75. package/src/components/ReuseDetails.vue +2 -2
  76. package/src/components/ReuseHorizontalCard.vue +4 -3
  77. package/src/components/SchemaCard.vue +10 -1
  78. package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
  79. package/src/components/Search/GlobalSearch.vue +12 -8
  80. package/src/components/Search/SearchInput.vue +1 -1
  81. package/src/components/Search/Sidemenu.vue +4 -3
  82. package/src/components/SmallChart.vue +11 -10
  83. package/src/components/StatBox.vue +13 -5
  84. package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
  85. package/src/components/TabularExplorer/TabularCell.vue +7 -8
  86. package/src/components/TabularExplorer/TabularCellPopover.vue +19 -3
  87. package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
  88. package/src/components/TabularExplorer/TabularDateField.vue +41 -0
  89. package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
  90. package/src/components/TabularExplorer/TabularExplorer.vue +122 -913
  91. package/src/components/TabularExplorer/TabularFilterContent.vue +33 -25
  92. package/src/components/TabularExplorer/TabularFilterPopover.vue +7 -7
  93. package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
  94. package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
  95. package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
  96. package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
  97. package/src/components/TabularExplorer/TabularTable.vue +484 -0
  98. package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
  99. package/src/components/TabularExplorer/types.ts +32 -20
  100. package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
  101. package/src/components/TabularExplorer/useTabularContext.ts +82 -0
  102. package/src/components/TopicCard.vue +15 -14
  103. package/src/composables/useActiveDescendant.ts +5 -0
  104. package/src/composables/useDatasetResources.ts +164 -0
  105. package/src/composables/useHasTabularData.ts +2 -1
  106. package/src/composables/useResizable.ts +46 -0
  107. package/src/composables/useResourceCapabilities.ts +47 -39
  108. package/src/composables/useTranslation.ts +24 -9
  109. package/src/config.ts +1 -0
  110. package/src/functions/charts.ts +32 -1
  111. package/src/functions/dates.ts +26 -7
  112. package/src/functions/helpers.ts +15 -0
  113. package/src/functions/metrics.ts +57 -56
  114. package/src/functions/owned.ts +21 -0
  115. package/src/functions/paginate.ts +46 -0
  116. package/src/functions/resourceCapabilities.ts +34 -0
  117. package/src/functions/resources.ts +83 -0
  118. package/src/functions/schemas.ts +27 -0
  119. package/src/functions/scroll.ts +20 -0
  120. package/src/functions/tabular.ts +119 -4
  121. package/src/functions/tabularApi.ts +1 -43
  122. package/src/main.ts +32 -6
  123. package/src/types/dataservices.ts +15 -0
  124. package/src/types/organizations.ts +10 -0
  125. package/src/types/reuses.ts +12 -1
  126. package/src/types/search.ts +2 -2
  127. package/src/types/visualizations.ts +5 -0
  128. package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js +0 -61
  129. /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
  130. /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
  131. /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
@@ -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>
@@ -6,7 +6,6 @@
6
6
  >
7
7
  <div
8
8
  v-if="!hideSearchInput"
9
- ref="search"
10
9
  class="flex flex-wrap items-center justify-between"
11
10
  data-cy="search"
12
11
  >
@@ -228,6 +227,7 @@
228
227
  size="sm"
229
228
  :icon="RiRssLine"
230
229
  icon-only
230
+ external
231
231
  target="_blank"
232
232
  />
233
233
  </div>
@@ -266,7 +266,7 @@
266
266
  name="reuse"
267
267
  :reuse="result"
268
268
  >
269
- <ReuseHorizontalCard :reuse="(result as Reuse)" />
269
+ <ReuseHorizontalCard :reuse="(result as ReuseV2)" />
270
270
  </slot>
271
271
  </template>
272
272
  <template v-else-if="currentTypeConfig?.class === 'organizations'">
@@ -356,6 +356,7 @@
356
356
  <script setup lang="ts">
357
357
  import { computed, provide, shallowReactive, useSlots, watch, useTemplateRef, type Component, type Ref } from 'vue'
358
358
  import { useRouteQuery } from '@vueuse/router'
359
+ import { useRoute } from 'vue-router'
359
360
  import { RiBookShelfLine, RiBuilding2Line, RiCloseCircleLine, RiDatabase2Line, RiLightbulbLine, RiLineChartLine, RiRssLine, RiTerminalLine } from '@remixicon/vue'
360
361
  import magnifyingGlassSrc from '../../../assets/illustrations/magnifying_glass.svg?url'
361
362
  import { useTranslation } from '../../composables/useTranslation'
@@ -364,11 +365,12 @@ import { configKey, forEachActiveCustomFilter, isCustomFilterActive, searchFilte
364
365
  import { useStableQueryParams } from '../../composables/useStableQueryParams'
365
366
  import { useComponentsConfig } from '../../config'
366
367
  import { useFetch } from '../../functions/api'
368
+ import { scrollToBlockTop } from '../../functions/scroll'
367
369
  import type { AsyncDataRequestStatus } from '../../functions/api.types'
368
370
  import type { Dataset } from '../../types/datasets'
369
371
  import type { Dataservice } from '../../types/dataservices'
370
372
  import type { Organization } from '../../types/organizations'
371
- import type { Reuse } from '../../types/reuses'
373
+ import type { ReuseV2 } from '../../types/reuses'
372
374
  import type { TopicV2 } from '../../types/topics'
373
375
  import type { GlobalSearchConfig, SearchResponseByClass, SearchType, SortOption, FacetItem } from '../../types/search'
374
376
  import { getDefaultGlobalSearchConfig } from '../../types/search'
@@ -422,6 +424,7 @@ if (!currentType.value) currentType.value = configKey(props.config[0] ?? { class
422
424
 
423
425
  const { t } = useTranslation()
424
426
  const componentsConfig = useComponentsConfig()
427
+ const route = useRoute()
425
428
 
426
429
  // Custom filter registry for useSearchFilter composable
427
430
  const customFilterRegistry = shallowReactive(new Map<string, CustomFilterEntry>())
@@ -796,14 +799,15 @@ function getFacets(key: string): FacetItem[] | undefined {
796
799
  }
797
800
 
798
801
  // Scroll handling
799
- const searchRef = useTemplateRef('search')
802
+ const resultsRef = useTemplateRef('results')
800
803
 
801
- function scrollToTop() {
802
- searchRef.value?.scrollIntoView({ behavior: 'smooth' })
803
- }
804
+ // Every criteria lives in the URL, custom filters included, so watching the
805
+ // query covers them all: whenever the result list is replaced, bring its top
806
+ // back into view rather than leaving the reader in the middle of a list they
807
+ // have not seen yet.
808
+ watch(() => route.query, () => scrollToBlockTop(resultsRef.value))
804
809
 
805
810
  function changePage(newPage: number) {
806
811
  page.value = newPage
807
- scrollToTop()
808
812
  }
809
813
  </script>
@@ -12,7 +12,7 @@
12
12
  v-model="q"
13
13
  type="search"
14
14
  name="q"
15
- class="input max-h-12 m-0 rounded-tl shadow-input-blue"
15
+ class="input flex-1 max-h-12 m-0 rounded-tl shadow-input-blue"
16
16
  :aria-label="placeholder === null ? t('Rechercher...') : placeholder ?? t('Rechercher...')"
17
17
  :placeholder="placeholder === null ? undefined : placeholder ?? t('Rechercher...')"
18
18
  >
@@ -11,7 +11,10 @@
11
11
  :class="{ 'rotate-180': open }"
12
12
  />
13
13
  </button>
14
- <div v-if="open || !isMobile">
14
+ <!-- Collapsing with CSS rather than `v-if` on a JS media query: the server has no
15
+ viewport, so it always renders the desktop state and a narrow client would drop
16
+ the whole panel during hydration. -->
17
+ <div :class="{ 'hidden md:block': !open }">
15
18
  <p
16
19
  :id="titleId"
17
20
  class="text-sm font-bold leading-tight mb-6 hidden md:block"
@@ -26,13 +29,11 @@
26
29
  <script setup lang="ts">
27
30
  import { ref, useId } from 'vue'
28
31
  import { RiArrowDownSLine } from '@remixicon/vue'
29
- import { useMediaQuery } from '@vueuse/core'
30
32
 
31
33
  defineProps<{
32
34
  buttonText: string
33
35
  }>()
34
36
 
35
37
  const titleId = useId()
36
- const isMobile = useMediaQuery('(max-width: 767px)')
37
38
  const open = ref(false)
38
39
  </script>
@@ -11,10 +11,16 @@
11
11
  class="flex flex-wrap justify-between"
12
12
  >
13
13
  <p class="text-[0.625rem] m-0 text-gray-medium">
14
- {{ startDate }}
14
+ <FormattedDate
15
+ :date="startDate"
16
+ :options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
17
+ />
15
18
  </p>
16
19
  <p class="text-[0.625rem] m-0 text-gray-medium">
17
- {{ endDate }}
20
+ <FormattedDate
21
+ :date="endDate"
22
+ :options="{ dateStyle: undefined, year: '2-digit', month: '2-digit' }"
23
+ />
18
24
  </p>
19
25
  </div>
20
26
  </div>
@@ -24,6 +30,7 @@
24
30
  import type { ChartOptions, ScriptableLineSegmentContext } from 'chart.js'
25
31
  import Chart from 'chart.js/auto'
26
32
  import { computed, onMounted, ref, useTemplateRef, watchEffect } from 'vue'
33
+ import FormattedDate from './FormattedDate.vue'
27
34
 
28
35
  const LIGHT_COLOR = '#B6CFFB'
29
36
  const COLOR = '#3558A2'
@@ -107,14 +114,8 @@ const additionalDatasetConfig = computed<{
107
114
  return {}
108
115
  })
109
116
 
110
- const getMonthYear = (dateAsString: string): string => {
111
- const date = new Date(dateAsString)
112
-
113
- return `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getFullYear().toString().slice(-2)}`
114
- }
115
-
116
- const startDate = computed(() => months.value.length ? getMonthYear(months.value[0]!) : null)
117
- const endDate = computed(() => months.value.length ? getMonthYear(months.value[months.value.length - 1]!) : null)
117
+ const startDate = computed(() => months.value[0] ?? null)
118
+ const endDate = computed(() => months.value[months.value.length - 1] ?? null)
118
119
 
119
120
  const OPTIONS = {
120
121
  // @ts-expect-error animation can be `true` but the typing is not expecting it
@@ -65,14 +65,20 @@
65
65
  class="mt-1 mb-0 text-xs"
66
66
  >
67
67
  {{ t("depuis ") }}
68
- {{ formatDate(since, { dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }) }}
68
+ <FormattedDate
69
+ :date="since"
70
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
71
+ />
69
72
  </p>
70
73
  <p class="mt-1 mb-0 text-xs text-success-darkest">
71
74
  <strong>
72
75
  + {{ summarize(lastValue, 2) }}
73
76
  </strong>
74
77
  {{ t(" en ") }}
75
- {{ formatDate(lastMonth, { dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }) }}
78
+ <FormattedDate
79
+ :date="lastMonth"
80
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
81
+ />
76
82
  </p>
77
83
  </template>
78
84
  </div>
@@ -146,7 +152,10 @@
146
152
  + {{ summarize(lastValue, 2) }}
147
153
  </strong>
148
154
  {{ t(" en ") }}
149
- {{ formatDate(lastMonth, { dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }) }}
155
+ <FormattedDate
156
+ :date="lastMonth"
157
+ :options="{ dateStyle: undefined, year: 'numeric', month: 'short', day: undefined }"
158
+ />
150
159
  </p>
151
160
  </div>
152
161
  </template>
@@ -154,7 +163,7 @@
154
163
  <script setup lang="ts">
155
164
  import { computed } from 'vue'
156
165
  import { ContentLoader } from 'vue-content-loader'
157
- import { useFormatDate } from '../functions/dates'
166
+ import FormattedDate from './FormattedDate.vue'
158
167
  import { summarize } from '../functions/helpers'
159
168
  import { useTranslation } from '../composables/useTranslation'
160
169
  import SmallChart from './SmallChart.vue'
@@ -169,7 +178,6 @@ const props = defineProps<{
169
178
  }>()
170
179
 
171
180
  const { t } = useTranslation()
172
- const { formatDate } = useFormatDate()
173
181
 
174
182
  const months = computed(() => props.data ? Object.keys(props.data) : [])
175
183
  const values = computed(() => props.data ? Object.values(props.data) : [])
@@ -0,0 +1,79 @@
1
+ <template>
2
+ <div
3
+ v-if="activeFilters.length > 0 || sort"
4
+ class="flex flex-wrap items-center gap-1.5"
5
+ >
6
+ <!-- Sort chip -->
7
+ <span
8
+ v-if="sort"
9
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
10
+ >
11
+ <component
12
+ :is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
13
+ class="size-3.5"
14
+ aria-hidden="true"
15
+ />
16
+ <span class="font-medium">{{ sort.column }}</span>
17
+ <span>{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
18
+ <button
19
+ :aria-label="t('Supprimer le tri')"
20
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
21
+ @click="sort = null"
22
+ >
23
+ <RiCloseLine
24
+ class="size-3.5"
25
+ aria-hidden="true"
26
+ />
27
+ </button>
28
+ </span>
29
+
30
+ <!-- Filter chips -->
31
+ <span
32
+ v-for="af in activeFilters"
33
+ :key="af.column"
34
+ :data-testid="`active-filter-${af.column}`"
35
+ class="inline-flex h-6 items-center gap-1 rounded border border-new-primary/20 bg-new-primary/10 pl-1.5 pr-0.5 text-[12px] leading-4 text-new-primary"
36
+ >
37
+ <component
38
+ :is="getColumnDisplay(af.column).icon"
39
+ class="size-3.5"
40
+ aria-hidden="true"
41
+ />
42
+ <span class="font-medium">{{ af.column }}</span>
43
+ <span>{{ af.label }}</span>
44
+ <button
45
+ :aria-label="t('Supprimer ce filtre')"
46
+ class="flex size-5 items-center justify-center rounded hover:bg-new-primary/15"
47
+ @click="removeFilter(af.column)"
48
+ >
49
+ <RiCloseLine
50
+ class="size-3.5"
51
+ aria-hidden="true"
52
+ />
53
+ </button>
54
+ </span>
55
+
56
+ <!-- Clear all -->
57
+ <BrandedButton
58
+ v-if="withClear"
59
+ color="tertiary"
60
+ size="2xs"
61
+ :icon="RiCloseLine"
62
+ @click="clearAllFilters(); sort = null"
63
+ >
64
+ {{ t('Tout effacer') }}
65
+ </BrandedButton>
66
+ </div>
67
+ </template>
68
+
69
+ <script setup lang="ts">
70
+ import { RiArrowDownLine, RiArrowUpLine, RiCloseLine } from '@remixicon/vue'
71
+ import BrandedButton from '../BrandedButton.vue'
72
+ import { useTranslation } from '../../composables/useTranslation'
73
+ import { useTabularContext } from './useTabularContext'
74
+
75
+ defineProps<{ withClear?: boolean }>()
76
+
77
+ const { t } = useTranslation()
78
+ const { activeFilters, sort, removeFilter, clearAllFilters, getColumnDisplay } = useTabularContext()
79
+ </script>
@@ -17,16 +17,15 @@
17
17
  </span>
18
18
  <span
19
19
  v-else-if="columnType === 'categorical'"
20
- class="inline-block rounded font-medium px-2 py-0.5 text-xs max-w-full truncate"
21
- :style="categoryBadgeStyle ? { backgroundColor: categoryBadgeStyle.backgroundColor, color: categoryBadgeStyle.color } : undefined"
20
+ class="inline-block max-w-full truncate rounded bg-gray-lower px-2 py-0.5 text-xs text-gray-plain"
22
21
  >{{ value }}</span>
23
22
  <span
24
- v-else-if="columnType === 'number'"
25
- :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
26
- >{{ formatNumber(value) }}</span>
23
+ v-else-if="columnType === 'number' || columnType === 'year'"
24
+ :class="compact ? 'font-mono tabular-nums text-xs' : ''"
25
+ >{{ columnType === 'year' || noNumberFormat ? value : formatNumber(value) }}</span>
27
26
  <span
28
27
  v-else-if="columnType === 'date'"
29
- :class="compact ? 'font-mono tabular-nums text-xs text-gray-title' : ''"
28
+ :class="compact ? 'font-mono tabular-nums text-xs' : ''"
30
29
  >{{ formatCellDate(value) }}</span>
31
30
  <span
32
31
  v-else
@@ -37,13 +36,13 @@
37
36
  <script setup lang="ts">
38
37
  import { useTranslation } from '../../composables/useTranslation'
39
38
  import { useFormatTabular, isTruthy } from '../../functions/tabular'
40
- import type { ColumnType, BadgeStyle } from './types'
39
+ import type { ColumnType } from './types'
41
40
 
42
41
  defineProps<{
43
42
  value: unknown
44
43
  columnType: ColumnType
45
- categoryBadgeStyle?: BadgeStyle
46
44
  compact?: boolean
45
+ noNumberFormat?: boolean
47
46
  }>()
48
47
 
49
48
  const { t } = useTranslation()