@datagouv/components-next 1.1.2 → 1.3.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 (62) hide show
  1. package/README.md +7 -6
  2. package/dist/{Control-ZFh5ta_U.js → Control-b-43UT2B.js} +10 -10
  3. package/dist/{Datafair.client-8bXp6UeQ.js → Datafair.client-BrjISlaZ.js} +4 -4
  4. package/dist/{Event-DSQcW7OF.js → Event-py_AAwJY.js} +18 -18
  5. package/dist/{Image-BijNEG0p.js → Image-BHQYZWzH.js} +23 -23
  6. package/dist/{JsonPreview.client-BxuoPK_w.js → JsonPreview.client-DivFAnbN.js} +5 -5
  7. package/dist/{Map-BUtPf5GN.js → Map-CtP7leHl.js} +1313 -1173
  8. package/dist/{MapContainer.client-CTz0wmJG.js → MapContainer.client-Cq8tNLZZ.js} +17 -17
  9. package/dist/{OSM-D4MTdBtk.js → OSM-gdDSIsDA.js} +15 -23
  10. package/dist/{PdfPreview.client-DpVreUSl.js → PdfPreview.client-BNBgLsK2.js} +6 -10
  11. package/dist/Pmtiles.client-CQi5Rw8W.js +26123 -0
  12. package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-stmU5qEB.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js} +2 -2
  13. package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-hG-FhNqy.js} +2 -2
  14. package/dist/{Tile-Dcl7oIVu.js → Tile-D6rFonpT.js} +163 -166
  15. package/dist/{TileImage-BJeHipMX.js → TileImage-cfrEgz5y.js} +131 -119
  16. package/dist/{View-xp_P_OHw.js → View-vp7vDb9q.js} +169 -119
  17. package/dist/{XmlPreview.client-DAOs89cR.js → XmlPreview.client-XCsNvnsZ.js} +5 -5
  18. package/dist/{common-BjQlan3k.js → common-C8efyyuc.js} +49 -49
  19. package/dist/components-next.css +1 -52
  20. package/dist/components-next.js +166 -157
  21. package/dist/components.css +2 -2
  22. package/dist/{index-JqjPja1u.js → index-CXUamc8Z.js} +6382 -6057
  23. package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-BRKv9Ksv.js} +162 -164
  24. package/dist/{main-D4WQMky0.js → main-DKu5SOAl.js} +48480 -97188
  25. package/dist/{proj-CsNo9yH1.js → proj-CsjVeojN.js} +322 -306
  26. package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
  27. package/dist/{tilecoord-A0fLnBZr.js → tilecoord-CPcQJnrF.js} +270 -261
  28. package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
  29. package/dist/{vue3-xml-viewer.common-BGsoNyZe.js → vue3-xml-viewer.common-BIaEIFYG.js} +1 -1
  30. package/package.json +42 -35
  31. package/src/chart.ts +5 -0
  32. package/src/components/ActivityList/ActivityList.vue +3 -0
  33. package/src/components/DatasetCard.vue +6 -4
  34. package/src/components/ObjectCardHeader.vue +1 -1
  35. package/src/components/RadioInput.vue +7 -2
  36. package/src/components/ResourceAccordion/DataStructure.vue +11 -33
  37. package/src/components/ResourceAccordion/Downloads.vue +160 -0
  38. package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
  39. package/src/components/ResourceAccordion/ResourceAccordion.vue +7 -104
  40. package/src/components/ResourceExplorer/ResourceExplorer.vue +10 -64
  41. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +30 -136
  42. package/src/components/ResourceExplorer/ResourceSelector.vue +113 -0
  43. package/src/components/Search/GlobalSearch.vue +27 -6
  44. package/src/components/TabularExplorer/TabularCellPopover.vue +2 -2
  45. package/src/components/TabularExplorer/TabularExplorer.vue +404 -163
  46. package/src/components/TabularExplorer/TabularFilterContent.vue +15 -13
  47. package/src/components/TabularExplorer/TabularFilterPopover.vue +21 -26
  48. package/src/components/TabularExplorer/types.ts +9 -9
  49. package/src/composables/useHasTabularData.ts +7 -0
  50. package/src/composables/useResourceCapabilities.ts +1 -1
  51. package/src/composables/useSearchFilter.ts +7 -5
  52. package/src/composables/useStableQueryParams.ts +7 -3
  53. package/src/composables/useTabularProfile.ts +70 -0
  54. package/src/functions/activities.ts +3 -3
  55. package/src/functions/api.ts +13 -7
  56. package/src/functions/resources.ts +5 -0
  57. package/src/functions/tabular.ts +103 -1
  58. package/src/main.ts +12 -6
  59. package/src/types/resources.ts +0 -1
  60. package/src/types/search.ts +11 -0
  61. package/src/types/ui.ts +1 -1
  62. package/dist/Pmtiles.client-BwmHo3T8.js +0 -25185
@@ -26,20 +26,22 @@
26
26
  <!-- Null filter with progress bar -->
27
27
  <div
28
28
  v-if="columnProfile && columnProfile.nb_missing_values > 0"
29
- class="flex items-center gap-2 px-3 py-2 border-b border-black/10"
29
+ class="px-3 py-2 border-b border-black/10 space-y-1.5"
30
30
  >
31
- <span class="text-xs text-gray-plain whitespace-nowrap">
32
- <span class="font-mono tabular-nums">{{ columnProfile.nb_missing_values }}</span>
33
- null
34
- <span class="text-gray-low">({{ nullPercent }})</span>
35
- </span>
36
- <ProgressBar
37
- :value="columnProfile.nb_missing_values"
38
- :max="totalLines"
39
- bar-class="bg-gray-low"
40
- class="flex-1 !h-1.5 !min-w-0 !border-0 !bg-gray-default"
41
- />
42
- <div class="flex items-center gap-0.5">
31
+ <div class="flex items-center gap-2">
32
+ <span class="text-xs text-gray-plain whitespace-nowrap">
33
+ <span class="font-mono tabular-nums">{{ columnProfile.nb_missing_values }}</span>
34
+ null
35
+ <span class="text-gray-low">({{ nullPercent }})</span>
36
+ </span>
37
+ <ProgressBar
38
+ :value="columnProfile.nb_missing_values"
39
+ :max="totalLines"
40
+ bar-class="bg-gray-low"
41
+ class="flex-1 !h-1.5 !min-w-0 !border-0 !bg-gray-default"
42
+ />
43
+ </div>
44
+ <div class="flex items-center justify-end gap-0.5">
43
45
  <BrandedButton
44
46
  :color="nullFilter === 'only' ? 'primary' : 'tertiary'"
45
47
  size="2xs"
@@ -1,25 +1,26 @@
1
1
  <template>
2
- <div
2
+ <Popover
3
3
  ref="anchor"
4
+ v-slot="{ open }"
4
5
  class="relative shrink-0"
5
6
  >
6
- <button
7
- class="p-0.5 rounded focus:outline-none"
7
+ <PopoverButton
8
+ class="p-0.5 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
8
9
  :class="hasColumnFilter ? 'bg-primary text-white' : 'hover:bg-gray-100'"
9
- @click.stop="togglePopover"
10
10
  >
11
- <RiFilter2Line
11
+ <RiFilterLine
12
12
  class="size-3.5"
13
13
  aria-hidden="true"
14
14
  />
15
15
  <span class="sr-only">{{ t('Filtrer') }} {{ column }}</span>
16
- </button>
16
+ </PopoverButton>
17
17
 
18
18
  <ClientOnly>
19
19
  <Teleport to="#tooltips">
20
- <div
21
- v-show="isOpen"
20
+ <PopoverPanel
21
+ v-show="open"
22
22
  ref="panel"
23
+ static
23
24
  class="bg-white border border-black/10 rounded-lg shadow-md w-64 absolute z-[800]"
24
25
  :style="floatingStyles"
25
26
  >
@@ -50,17 +51,17 @@
50
51
  :category-badge-styles="categoryBadgeStyles"
51
52
  :boolean-counts="booleanCounts"
52
53
  />
53
- </div>
54
+ </PopoverPanel>
54
55
  </Teleport>
55
56
  </ClientOnly>
56
- </div>
57
+ </Popover>
57
58
  </template>
58
59
 
59
60
  <script setup lang="ts">
60
- import { computed, ref, useTemplateRef } from 'vue'
61
+ import { computed, useTemplateRef } from 'vue'
61
62
  import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
62
- import { onClickOutside } from '@vueuse/core'
63
- import { RiFilter2Line, RiCloseLine } from '@remixicon/vue'
63
+ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
64
+ import { RiFilterLine, RiCloseLine } from '@remixicon/vue'
64
65
  import { useTranslation } from '../../composables/useTranslation'
65
66
  import { hasFilterForColumn as _hasFilterForColumn } from '../../functions/tabular'
66
67
  import BrandedButton from '../BrandedButton.vue'
@@ -83,18 +84,12 @@ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default:
83
84
 
84
85
  const { t } = useTranslation()
85
86
 
86
- const isOpen = ref(false)
87
-
88
- function togglePopover() {
89
- isOpen.value = !isOpen.value
90
- }
91
-
92
- const anchorRef = useTemplateRef<HTMLElement>('anchor')
93
- const panelRef = useTemplateRef<HTMLElement>('panel')
94
-
95
- onClickOutside(panelRef, () => {
96
- isOpen.value = false
97
- }, { ignore: [anchorRef] })
87
+ // Headless UI Popover manages open state, Escape-to-close, outside-click,
88
+ // focus handling and ARIA. floating-ui only positions the teleported panel.
89
+ const anchorComponent = useTemplateRef<InstanceType<typeof Popover>>('anchor')
90
+ const panelComponent = useTemplateRef<InstanceType<typeof PopoverPanel>>('panel')
91
+ const anchorEl = computed(() => anchorComponent.value?.$el as HTMLElement | undefined)
92
+ const panelEl = computed(() => panelComponent.value?.$el as HTMLElement | undefined)
98
93
 
99
94
  const hasColumnFilter = computed(() => _hasFilterForColumn(filters.value, props.column))
100
95
 
@@ -103,7 +98,7 @@ function clearColumnFilter() {
103
98
  filters.value = rest
104
99
  }
105
100
 
106
- const { floatingStyles } = useFloating(anchorRef, panelRef, {
101
+ const { floatingStyles } = useFloating(anchorEl, panelEl, {
107
102
  placement: 'bottom-start',
108
103
  middleware: [flip(), shift()],
109
104
  whileElementsMounted: autoUpdate,
@@ -1,5 +1,5 @@
1
1
  /** Response from /api/resources/{rid}/data/ */
2
- export interface TabularDataResponse {
2
+ export type TabularDataResponse = {
3
3
  data: TabularRow[]
4
4
  meta: {
5
5
  page: number
@@ -17,11 +17,11 @@ export interface TabularDataResponse {
17
17
  export type TabularRow = Record<string, unknown> & { __id: number }
18
18
 
19
19
  /** Response from /api/resources/{rid}/profile/ */
20
- export interface TabularProfileResponse {
20
+ export type TabularProfileResponse = {
21
21
  profile: TabularProfile
22
22
  }
23
23
 
24
- export interface TabularProfile {
24
+ export type TabularProfile = {
25
25
  header: string[]
26
26
  columns: Record<string, TabularColumnInfo>
27
27
  formats: Record<string, string[]>
@@ -38,13 +38,13 @@ export interface TabularProfile {
38
38
  trailing_columns: number
39
39
  }
40
40
 
41
- export interface TabularColumnInfo {
41
+ export type TabularColumnInfo = {
42
42
  score: number
43
43
  format: string
44
44
  python_type: string
45
45
  }
46
46
 
47
- export interface TabularColumnProfile {
47
+ export type TabularColumnProfile = {
48
48
  tops: TabularTopValue[]
49
49
  nb_distinct: number
50
50
  nb_missing_values: number
@@ -54,14 +54,14 @@ export interface TabularColumnProfile {
54
54
  mean?: number
55
55
  }
56
56
 
57
- export interface TabularTopValue {
57
+ export type TabularTopValue = {
58
58
  value: string
59
59
  count: number
60
60
  }
61
61
 
62
62
  export type ColumnType = 'number' | 'categorical' | 'text' | 'date' | 'boolean'
63
63
 
64
- export interface ColumnFilters {
64
+ export type ColumnFilters = {
65
65
  in?: string[]
66
66
  exact?: string
67
67
  min?: number
@@ -72,12 +72,12 @@ export interface ColumnFilters {
72
72
 
73
73
  export type SortDirection = 'asc' | 'desc'
74
74
 
75
- export interface SortConfig {
75
+ export type SortConfig = {
76
76
  column: string
77
77
  direction: SortDirection
78
78
  }
79
79
 
80
- export interface BadgeStyle {
80
+ export type BadgeStyle = {
81
81
  backgroundColor: string
82
82
  color: string
83
83
  }
@@ -11,10 +11,17 @@ export const useHasTabularData = () => {
11
11
  const config = useComponentsConfig()
12
12
 
13
13
  return (resource: Resource) => {
14
+ // Reject resources whose source URL last check failed (>= 400):
15
+ // the tabular-api purges its parquet when the source dies, but
16
+ // `parsing_table` may still be set — leading to a 404 on fetch.
17
+ const checkStatus = resource.extras['check:status']
18
+ const sourceUnreachable = typeof checkStatus === 'number' && checkStatus >= 400
19
+
14
20
  return (
15
21
  config.tabularApiUrl
16
22
  && resource.extras['analysis:parsing:parsing_table']
17
23
  && !resource.extras['analysis:parsing:error']
24
+ && !sourceUnreachable
18
25
  && (config.tabularAllowRemote || resource.filetype === 'file')
19
26
  )
20
27
  }
@@ -108,7 +108,7 @@ export function useResourceCapabilities(
108
108
  options.push({ key: 'downloads', label: t('Téléchargements') })
109
109
 
110
110
  if (hasTabularData.value) {
111
- options.push({ key: 'swagger', label: t('Swagger') })
111
+ options.push({ key: 'api', label: t('API') })
112
112
  }
113
113
 
114
114
  return options
@@ -94,10 +94,12 @@ export function useSearchFilter(
94
94
  })
95
95
 
96
96
  onScopeDispose(() => {
97
- // Clear the URL param when the scope ends. This mirrors GlobalSearch's
98
- // own `watch(currentType)` logic that drops built-in filters which don't
99
- // apply to the new type: a custom filter's applicability is signalled
100
- // by the consumer via `v-if`, so its unmount is the equivalent signal.
97
+ // Type-scoped filters (typeKeys set) are cleared by GlobalSearch's
98
+ // watch(currentType) instead, so their URL update batches with the other
99
+ // type-change resets via useRouteQuery rather than racing them with the
100
+ // separate router.replace below. Here we only clear filters that apply to
101
+ // all types: their unmount is a consumer signal unrelated to a type switch,
102
+ // which GlobalSearch has no way to detect.
101
103
  //
102
104
  // We cannot use `value.value = defaultValue` here because VueUse's
103
105
  // useRouteQuery registers its own tryOnScopeDispose cleanup that zeroes
@@ -105,7 +107,7 @@ export function useSearchFilter(
105
107
  // The setter then sees `query === v` and early-returns without ever
106
108
  // calling router.replace(). Instead we read the live route.query directly
107
109
  // (which is router state, not affected by that cleanup) and push the update.
108
- if (route.query[urlParam] !== undefined) {
110
+ if (normalizedTypeKeys === undefined && route.query[urlParam] !== undefined) {
109
111
  const { [urlParam]: _removed, ...restQuery } = route.query
110
112
  router.replace({
111
113
  query: defaultValue === undefined ? restQuery : { ...restQuery, [urlParam]: String(defaultValue) },
@@ -72,10 +72,14 @@ export function useStableQueryParams(options: StableQueryParamsOptions) {
72
72
  if (q.value) {
73
73
  params.q = q.value
74
74
  }
75
- if (sort.value) {
75
+ const sortToUse = sort.value ?? typeConfig?.defaultSort
76
+ if (sortToUse) {
76
77
  const validSortValues = typeConfig?.sortOptions?.map(o => o.value as string) ?? []
77
- if (validSortValues.includes(sort.value)) {
78
- params.sort = sort.value
78
+ if (validSortValues.includes(sortToUse)) {
79
+ params.sort = sortToUse
80
+ }
81
+ else if (import.meta.env.DEV && typeConfig?.defaultSort && typeConfig?.sortOptions && sortToUse === typeConfig.defaultSort) {
82
+ console.warn(`[GlobalSearch] defaultSort "${typeConfig.defaultSort}" is not in sortOptions for "${typeConfig.class}". Valid values: ${validSortValues.join(', ')}`)
79
83
  }
80
84
  }
81
85
  params.page = page.value
@@ -0,0 +1,70 @@
1
+ import { computed, inject, provide, toValue, type MaybeRefOrGetter, type Ref } from 'vue'
2
+ import { useComponentsConfig } from '../config'
3
+ import { useFetch } from '../functions/api'
4
+ import type { AsyncDataRequestStatus } from '../functions/api.types'
5
+ import type { TabularProfileResponse } from '../components/TabularExplorer/types'
6
+
7
+ const TABULAR_PROFILE_KEY = Symbol('tabular-profile')
8
+
9
+ export type TabularProfileState = {
10
+ resourceId: Readonly<Ref<string>>
11
+ data: Readonly<Ref<TabularProfileResponse | null>>
12
+ error: Readonly<Ref<unknown | null>>
13
+ status: Readonly<Ref<AsyncDataRequestStatus>>
14
+ refresh: () => Promise<void>
15
+ }
16
+
17
+ // What is shared through provide/inject: the resourceId (to let descendants
18
+ // check they target the same resource) and the in-flight fetch promise. We
19
+ // share the promise rather than the resolved state because `provide()` must
20
+ // run synchronously during setup — see `provideTabularProfile`.
21
+ type TabularProfileShared = {
22
+ resourceId: Readonly<Ref<string>>
23
+ state: Promise<TabularProfileState>
24
+ }
25
+
26
+ async function createProfileState(resourceId: MaybeRefOrGetter<string>): Promise<TabularProfileState> {
27
+ const config = useComponentsConfig()
28
+ const ridRef = computed(() => toValue(resourceId))
29
+
30
+ // Goes through the package's useFetch, which delegates to the host's
31
+ // customUseFetch (Nuxt useFetch in cdata) so the response is shared
32
+ // between SSR and CSR via the Nuxt payload — avoiding a double fetch.
33
+ const profileUrl = computed(() =>
34
+ ridRef.value ? `${config.tabularApiUrl}/api/resources/${ridRef.value}/profile/` : null,
35
+ )
36
+
37
+ const { data, error, status, refresh } = await useFetch<TabularProfileResponse>(profileUrl, { raw: true })
38
+
39
+ return { resourceId: ridRef, data, error, status, refresh }
40
+ }
41
+
42
+ /**
43
+ * Parent: fetch the tabular profile once and share it with descendants
44
+ * (TabularExplorer, DataStructure...) via provide/inject.
45
+ *
46
+ * Not async on purpose: `provide()` only works while the active component
47
+ * instance is set, which is lost after the first `await`. So we kick off the
48
+ * fetch, `provide()` the resulting promise synchronously, then return it for
49
+ * the caller to await.
50
+ */
51
+ export function provideTabularProfile(resourceId: MaybeRefOrGetter<string>): Promise<TabularProfileState> {
52
+ const ridRef = computed(() => toValue(resourceId))
53
+ const state = createProfileState(resourceId)
54
+ provide<TabularProfileShared>(TABULAR_PROFILE_KEY, { resourceId: ridRef, state })
55
+ return state
56
+ }
57
+
58
+ /**
59
+ * Child: get the tabular profile from an ancestor that called
60
+ * `provideTabularProfile` for the same resourceId. Falls back to
61
+ * fetching on its own if no compatible ancestor is found — preserves
62
+ * standalone usage of TabularExplorer / DataStructure.
63
+ */
64
+ export async function injectTabularProfile(resourceId: MaybeRefOrGetter<string>): Promise<TabularProfileState> {
65
+ const injected = inject<TabularProfileShared | null>(TABULAR_PROFILE_KEY, null)
66
+ if (injected && injected.resourceId.value === toValue(resourceId)) {
67
+ return await injected.state
68
+ }
69
+ return await createProfileState(resourceId)
70
+ }
@@ -11,9 +11,9 @@ export function getActivityTranslation(activity: Activity): string {
11
11
  'dataset:deleted': t('a supprimé le jeu de données'),
12
12
  'dataset:discussed': t('a discuté du jeu de données'),
13
13
  'dataset:followed': t('suit le jeu de données'),
14
- 'dataset:resource:added': t('a ajouté une ressource'),
15
- 'dataset:resource:updated': t('a mis à jour une ressource'),
16
- 'dataset:resource:deleted': t('a supprimé une ressource'),
14
+ 'dataset:resource:added': t('a ajouté la ressource'),
15
+ 'dataset:resource:updated': t('a mis à jour la ressource'),
16
+ 'dataset:resource:deleted': t('a supprimé la ressource'),
17
17
  'dataservice:created': t('a créé le service de données'),
18
18
  'dataservice:updated': t('a mis à jour le service de données'),
19
19
  'dataservice:deleted': t('a supprimé le service de données'),
@@ -26,7 +26,7 @@ export async function useFetch<DataT, ErrorT = never>(
26
26
  const error: Ref<ErrorT | null> = ref(null)
27
27
  const status = ref<AsyncDataRequestStatus>('idle')
28
28
 
29
- const execute = async () => {
29
+ const execute = async (signal?: AbortSignal) => {
30
30
  const urlValue = toValue(url)
31
31
  if (!urlValue) return
32
32
  const params = deepToValue(options?.params)
@@ -36,13 +36,15 @@ export async function useFetch<DataT, ErrorT = never>(
36
36
  data.value = isRaw
37
37
  // Raw targets another data.gouv service (the Tabular API in TabularExplorer) via its own
38
38
  // absolute URL, so it must stay bare ofetch: no datagouv apiBase, no datagouv auth attached.
39
- ? await ofetch<DataT | null>(urlValue, { params: params ?? query })
39
+ ? await ofetch<DataT | null>(urlValue, { params: params ?? query, signal })
40
40
  // The configured `$fetch` carries baseURL + auth + headers (see the `datagouv` plugin install
41
41
  // for the default one). We only forward the URL and params here.
42
- : await config.$fetch<DataT | null>(urlValue, { baseURL: config.apiBase, params: params ?? query })
42
+ : await config.$fetch<DataT | null>(urlValue, { baseURL: config.apiBase, params: params ?? query, signal })
43
43
  status.value = 'success'
44
44
  }
45
45
  catch (e) {
46
+ // Ignore aborted requests — they're superseded by a newer call, not a real error.
47
+ if (signal?.aborted) return
46
48
  error.value = e as ErrorT
47
49
  status.value = 'error'
48
50
  }
@@ -51,14 +53,18 @@ export async function useFetch<DataT, ErrorT = never>(
51
53
  // When server is false, only start watching after mount (client-side only)
52
54
  if (options?.server === false) {
53
55
  onMounted(() => {
54
- watchEffect(async () => {
55
- await execute()
56
+ watchEffect((onCleanup) => {
57
+ const controller = new AbortController()
58
+ onCleanup(() => controller.abort())
59
+ void execute(controller.signal)
56
60
  })
57
61
  })
58
62
  }
59
63
  else {
60
- watchEffect(async () => {
61
- await execute()
64
+ watchEffect((onCleanup) => {
65
+ const controller = new AbortController()
66
+ onCleanup(() => controller.abort())
67
+ void execute(controller.signal)
62
68
  })
63
69
  }
64
70
 
@@ -91,6 +91,11 @@ export function getResourceTitleId(resource: Resource) {
91
91
  export const RESOURCE_TYPE = readonly(['main', 'documentation', 'update', 'api', 'code', 'other'] as const)
92
92
  export type ResourceType = typeof RESOURCE_TYPE[number]
93
93
 
94
+ // Page size used by ResourceExplorer for its per-type resource fetches.
95
+ // Exported so consumers (e.g. the dataset page's `main` prefetch) can build a
96
+ // byte-for-byte identical cache key and let Nuxt dedupe the request.
97
+ export const RESOURCE_EXPLORER_PAGE_SIZE = 10
98
+
94
99
  export const getResourceLabel = (type: ResourceType, count?: number) => {
95
100
  const { t } = useTranslation()
96
101
  switch (type) {
@@ -4,9 +4,24 @@ import {
4
4
  RiPriceTag3Line,
5
5
  RiText,
6
6
  RiCalendarLine,
7
+ RiCalendarEventLine,
8
+ RiTimeLine,
7
9
  RiCheckboxLine,
10
+ RiMapPin2Line,
11
+ RiGlobalLine,
12
+ RiFocus3Line,
13
+ RiFingerprintLine,
14
+ RiMailLine,
15
+ RiLinksLine,
16
+ RiPhoneLine,
17
+ RiUserLine,
18
+ RiMoneyEuroCircleLine,
19
+ RiPercentLine,
20
+ RiBracesLine,
21
+ RiCodeLine,
8
22
  } from '@remixicon/vue'
9
23
  import { useTranslation } from '../composables/useTranslation'
24
+ import type { TranslationFunction } from '../composables/useTranslation'
10
25
  import type { ColumnFilters, ColumnType } from '../components/TabularExplorer/types'
11
26
 
12
27
  export function hasFilterForColumn(filters: Record<string, ColumnFilters>, column: string): boolean {
@@ -15,7 +30,12 @@ export function hasFilterForColumn(filters: Record<string, ColumnFilters>, colum
15
30
  return !!(f.in?.length || f.exact != null || f.contains || f.null || f.min != null || f.max != null)
16
31
  }
17
32
 
18
- export function buildTypeConfig(t: (s: string) => string): Record<ColumnType, { icon: Component, label: string }> {
33
+ export type TypeDisplay = {
34
+ icon: Component
35
+ label: string
36
+ }
37
+
38
+ export function buildTypeConfig(t: TranslationFunction): Record<ColumnType, TypeDisplay> {
19
39
  return {
20
40
  number: { icon: RiHashtag, label: t('Nombre') },
21
41
  categorical: { icon: RiPriceTag3Line, label: t('Catégoriel') },
@@ -25,6 +45,88 @@ export function buildTypeConfig(t: (s: string) => string): Record<ColumnType, {
25
45
  }
26
46
  }
27
47
 
48
+ // csv-detective formats that carry no semantic meaning beyond the generic
49
+ // display type already resolved by `ColumnType` — these fall back to
50
+ // `buildTypeConfig` (Texte / Nombre / Date / Booléen) in the header.
51
+ export const GENERIC_FORMATS = new Set(['string', 'int', 'float', 'date', 'bool', 'booleen'])
52
+
53
+ // Semantic types detected by csv-detective (see ../../../csv-detective/csv_detective/formats).
54
+ // The list is intentionally exhaustive, but unknown formats are tolerated:
55
+ // `getColumnDisplay` humanizes any format missing from this map.
56
+ export function buildFormatConfig(t: TranslationFunction): Record<string, TypeDisplay> {
57
+ return {
58
+ // Geo — territories & addresses
59
+ adresse: { icon: RiMapPin2Line, label: t('Adresse') },
60
+ commune: { icon: RiMapPin2Line, label: t('Commune') },
61
+ code_commune: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
62
+ code_commune_insee: { icon: RiMapPin2Line, label: t('Code INSEE commune') },
63
+ departement: { icon: RiMapPin2Line, label: t('Département') },
64
+ code_departement: { icon: RiMapPin2Line, label: t('Code département') },
65
+ region: { icon: RiMapPin2Line, label: t('Région') },
66
+ code_region: { icon: RiMapPin2Line, label: t('Code région') },
67
+ code_epci: { icon: RiMapPin2Line, label: t('Code EPCI') },
68
+ code_postal: { icon: RiMapPin2Line, label: t('Code postal') },
69
+ insee_canton: { icon: RiMapPin2Line, label: t('Canton') },
70
+ code_fantoir: { icon: RiMapPin2Line, label: t('Code FANTOIR') },
71
+ id_rnb: { icon: RiMapPin2Line, label: t('Identifiant RNB') },
72
+ // Geo — countries
73
+ pays: { icon: RiGlobalLine, label: t('Pays') },
74
+ iso_country_code_alpha2: { icon: RiGlobalLine, label: t('Code pays (ISO α-2)') },
75
+ iso_country_code_alpha3: { icon: RiGlobalLine, label: t('Code pays (ISO α-3)') },
76
+ iso_country_code_numeric: { icon: RiGlobalLine, label: t('Code pays (ISO num.)') },
77
+ // Geo — coordinates
78
+ latitude_wgs: { icon: RiFocus3Line, label: t('Latitude') },
79
+ latitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Latitude') },
80
+ longitude_wgs: { icon: RiFocus3Line, label: t('Longitude') },
81
+ longitude_wgs_fr_metropole: { icon: RiFocus3Line, label: t('Longitude') },
82
+ latitude_l93: { icon: RiFocus3Line, label: t('Latitude (Lambert 93)') },
83
+ longitude_l93: { icon: RiFocus3Line, label: t('Longitude (Lambert 93)') },
84
+ latlon_wgs: { icon: RiFocus3Line, label: t('Latitude / longitude') },
85
+ lonlat_wgs: { icon: RiFocus3Line, label: t('Longitude / latitude') },
86
+ // Identifiers
87
+ siren: { icon: RiFingerprintLine, label: t('SIREN') },
88
+ siret: { icon: RiFingerprintLine, label: t('SIRET') },
89
+ code_rna: { icon: RiFingerprintLine, label: t('Identifiant RNA') },
90
+ code_waldec: { icon: RiFingerprintLine, label: t('Identifiant WALDEC') },
91
+ code_import: { icon: RiFingerprintLine, label: t('Code Import') },
92
+ uai: { icon: RiFingerprintLine, label: t('UAI') },
93
+ insee_ape700: { icon: RiFingerprintLine, label: t('Code APE') },
94
+ code_csp_insee: { icon: RiFingerprintLine, label: t('Code CSP') },
95
+ uuid: { icon: RiFingerprintLine, label: t('UUID') },
96
+ mongo_object_id: { icon: RiFingerprintLine, label: t('Identifiant Mongo') },
97
+ // Contact
98
+ email: { icon: RiMailLine, label: t('Email') },
99
+ url: { icon: RiLinksLine, label: t('URL') },
100
+ tel_fr: { icon: RiPhoneLine, label: t('Téléphone') },
101
+ username: { icon: RiUserLine, label: t('Nom d\'utilisateur') },
102
+ // People
103
+ sexe: { icon: RiUserLine, label: t('Sexe') },
104
+ csp_insee: { icon: RiUserLine, label: t('Catégorie socio-pro.') },
105
+ // Quantities
106
+ money: { icon: RiMoneyEuroCircleLine, label: t('Montant') },
107
+ percent: { icon: RiPercentLine, label: t('Pourcentage') },
108
+ // Temporal
109
+ year: { icon: RiCalendarLine, label: t('Année') },
110
+ date_fr: { icon: RiCalendarLine, label: t('Date') },
111
+ jour_de_la_semaine: { icon: RiCalendarEventLine, label: t('Jour de la semaine') },
112
+ mois_de_lannee: { icon: RiCalendarEventLine, label: t('Mois de l\'année') },
113
+ datetime_naive: { icon: RiTimeLine, label: t('Date et heure') },
114
+ datetime_aware: { icon: RiTimeLine, label: t('Date et heure') },
115
+ datetime_rfc822: { icon: RiTimeLine, label: t('Date et heure') },
116
+ // Structured
117
+ json: { icon: RiBracesLine, label: t('JSON') },
118
+ geojson: { icon: RiBracesLine, label: t('GeoJSON') },
119
+ binary: { icon: RiCodeLine, label: t('Binaire') },
120
+ }
121
+ }
122
+
123
+ // Tolerant fallback for a semantic format we don't explicitly know about:
124
+ // "code_truc_machin" -> "Code truc machin".
125
+ export function humanizeFormat(format: string): string {
126
+ const spaced = format.replace(/_/g, ' ').trim()
127
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1)
128
+ }
129
+
28
130
  export function useFormatTabular() {
29
131
  const { locale } = useTranslation()
30
132
 
package/src/main.ts CHANGED
@@ -40,6 +40,9 @@ import BrandedButton from './components/BrandedButton.vue'
40
40
  import CopyButton from './components/CopyButton.vue'
41
41
  import DataserviceCard from './components/DataserviceCard.vue'
42
42
  import DatasetCard from './components/DatasetCard.vue'
43
+ import DataStructure from './components/ResourceAccordion/DataStructure.vue'
44
+ import Downloads from './components/ResourceAccordion/Downloads.vue'
45
+ import Metadata from './components/ResourceAccordion/Metadata.vue'
43
46
  import DescriptionListTerm from './components/DescriptionListTerm.vue'
44
47
  import DescriptionListDetails from './components/DescriptionListDetails.vue'
45
48
  import DiscussionMessageCard from './components/DiscussionMessageCard.vue'
@@ -60,6 +63,7 @@ import LoadingBlock from './components/LoadingBlock.vue'
60
63
  import MarkdownViewer from './components/MarkdownViewer.vue'
61
64
  import OrganizationCard from './components/OrganizationCard.vue'
62
65
  import OrganizationHorizontalCard from './components/OrganizationHorizontalCard.vue'
66
+ import ObjectCardOwner from './components/ObjectCardOwner.vue'
63
67
  import OrganizationLogo from './components/OrganizationLogo.vue'
64
68
  import OrganizationNameWithCertificate from './components/OrganizationNameWithCertificate.vue'
65
69
  import OwnerType from './components/OwnerType.vue'
@@ -73,6 +77,7 @@ import PostCard from './components/PostCard.vue'
73
77
  import ReadMore from './components/ReadMore.vue'
74
78
  import ResourceAccordion from './components/ResourceAccordion/ResourceAccordion.vue'
75
79
  import ResourceIcon from './components/ResourceAccordion/ResourceIcon.vue'
80
+ import ResourceSelector from './components/ResourceExplorer/ResourceSelector.vue'
76
81
  import ResourceExplorer from './components/ResourceExplorer/ResourceExplorer.vue'
77
82
  import ResourceExplorerSidebar from './components/ResourceExplorer/ResourceExplorerSidebar.vue'
78
83
  import ResourceExplorerViewer from './components/ResourceExplorer/ResourceExplorerViewer.vue'
@@ -82,9 +87,6 @@ import ReuseHorizontalCard from './components/ReuseHorizontalCard.vue'
82
87
  import ReuseDetails from './components/ReuseDetails.vue'
83
88
  import SchemaCard from './components/SchemaCard.vue'
84
89
  import SimpleBanner from './components/SimpleBanner.vue'
85
- import SmallChart from './components/SmallChart.vue'
86
- import ChartViewer from './components/Chart/ChartViewer.vue'
87
- import ChartViewerWrapper from './components/Chart/ChartViewerWrapper.vue'
88
90
  import StatBox from './components/StatBox.vue'
89
91
  import Tab from './components/Tabs/Tab.vue'
90
92
  import TabGroup from './components/Tabs/TabGroup.vue'
@@ -115,6 +117,8 @@ export * from './composables/useMetrics'
115
117
  export * from './composables/useReuseType'
116
118
  export * from './composables/useTranslation'
117
119
  export * from './composables/useHasTabularData'
120
+ export * from './composables/useResourceCapabilities'
121
+ export * from './composables/useTabularProfile'
118
122
 
119
123
  export * from './functions/activities'
120
124
  export * from './functions/datasets'
@@ -326,6 +330,9 @@ export {
326
330
  CopyButton,
327
331
  DataserviceCard,
328
332
  DatasetCard,
333
+ DataStructure,
334
+ Downloads,
335
+ Metadata,
329
336
  DatasetInformationSection,
330
337
  DatasetTemporalitySection,
331
338
  DatasetSpatialSection,
@@ -365,15 +372,14 @@ export {
365
372
  ResourceExplorer,
366
373
  ResourceExplorerSidebar,
367
374
  ResourceExplorerViewer,
375
+ ObjectCardOwner,
368
376
  ResourceIcon,
377
+ ResourceSelector,
369
378
  ReuseCard,
370
379
  ReuseDetails,
371
380
  ReuseHorizontalCard,
372
381
  SchemaCard,
373
382
  SimpleBanner,
374
- SmallChart,
375
- ChartViewer,
376
- ChartViewerWrapper,
377
383
  StatBox,
378
384
  OpenApiViewer,
379
385
  Tab,
@@ -46,7 +46,6 @@ export type OgcLayerInfo = { name: string, default_crs: string | null }
46
46
 
47
47
  export type WfsMetadata = {
48
48
  format: string
49
- layers: Array<OgcLayerInfo>
50
49
  version: string
51
50
  detected_layer: OgcLayerInfo | null
52
51
  output_formats: Array<string>