@datagouv/components-next 0.2.0 → 1.0.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 (140) hide show
  1. package/README.md +1 -1
  2. package/assets/main.css +56 -1
  3. package/dist/Control-BNCDn-8E.js +148 -0
  4. package/dist/{Datafair.client-x39O4yfF.js → Datafair.client-Dls5AHTE.js} +1 -1
  5. package/dist/Event-BOgJUhNR.js +738 -0
  6. package/dist/Image-BN-4XkIn.js +247 -0
  7. package/dist/{JsonPreview.client-BMsC5JcY.js → JsonPreview.client-DPDTs433.js} +14 -14
  8. package/dist/Map-BdT3i2C4.js +7609 -0
  9. package/dist/MapContainer.client-BdAzd7bj.js +105 -0
  10. package/dist/OSM-CamriM9b.js +71 -0
  11. package/dist/{PdfPreview.client-COOkEkRA.js → PdfPreview.client-CopqSDyt.js} +3 -3
  12. package/dist/{Pmtiles.client-BaiIo4VZ.js → Pmtiles.client-mF6xaOO_.js} +2 -2
  13. package/dist/ScaleLine-BiesrgOv.js +165 -0
  14. package/dist/Swagger.client-eJ7gpfZA.js +4 -0
  15. package/dist/Tile-DCuqwNOI.js +1206 -0
  16. package/dist/TileImage-CmZf8EdU.js +1067 -0
  17. package/dist/View-DcDc7N2K.js +2858 -0
  18. package/dist/{XmlPreview.client-CAdN0w_Y.js → XmlPreview.client-C0OgBkSq.js} +7 -7
  19. package/dist/common-C4rDcQpp.js +243 -0
  20. package/dist/components-next.css +1 -1
  21. package/dist/components-next.js +153 -117
  22. package/dist/components.css +1 -1
  23. package/dist/{MapContainer.client-DeSo8EvG.js → index-BRGqW8aQ.js} +4975 -21416
  24. package/dist/leaflet-src-7m1mB8LI.js +6338 -0
  25. package/dist/{main-Dgri3TQL.js → main-CNHxAJ8J.js} +56758 -51450
  26. package/dist/proj-CKwYjU38.js +1569 -0
  27. package/dist/tilecoord-YW3qEH_j.js +884 -0
  28. package/dist/{vue3-xml-viewer.common-D6skc_Ai.js → vue3-xml-viewer.common-CmAdQfIy.js} +1 -1
  29. package/package.json +5 -1
  30. package/src/components/ActivityList/ActivityList.vue +6 -2
  31. package/src/components/AppLink.vue +4 -1
  32. package/src/components/Avatar.vue +2 -2
  33. package/src/components/AvatarWithName.vue +8 -4
  34. package/src/components/BouncingDots.vue +21 -0
  35. package/src/components/BrandedButton.vue +2 -0
  36. package/src/components/CopyButton.vue +19 -7
  37. package/src/components/DataserviceCard.vue +83 -118
  38. package/src/components/DatasetCard.vue +110 -171
  39. package/src/components/DatasetInformation/DatasetEmbedSection.vue +43 -0
  40. package/src/components/DatasetInformation/DatasetInformationSection.vue +73 -0
  41. package/src/components/DatasetInformation/DatasetSchemaSection.vue +74 -0
  42. package/src/components/DatasetInformation/DatasetSpatialSection.vue +59 -0
  43. package/src/components/DatasetInformation/DatasetTemporalitySection.vue +45 -0
  44. package/src/components/DatasetInformation/index.ts +5 -0
  45. package/src/components/DatasetQualityTooltipContent.vue +3 -3
  46. package/src/components/DescriptionList.vue +1 -4
  47. package/src/components/DescriptionListDetails.vue +5 -0
  48. package/src/components/DescriptionListTerm.vue +5 -0
  49. package/src/components/DiscussionMessageCard.vue +63 -0
  50. package/src/components/ExtraAccordion.vue +4 -4
  51. package/src/components/Form/BadgeSelect.vue +35 -0
  52. package/src/components/Form/FormatSelect.vue +28 -0
  53. package/src/components/Form/GeozoneSelect.vue +52 -0
  54. package/src/components/Form/GranularitySelect.vue +29 -0
  55. package/src/components/Form/LicenseSelect.vue +30 -0
  56. package/src/components/Form/OrganizationSelect.vue +62 -0
  57. package/src/components/Form/OrganizationTypeSelect.vue +34 -0
  58. package/src/components/Form/ReuseTopicSelect.vue +29 -0
  59. package/src/components/Form/SchemaSelect.vue +30 -0
  60. package/src/components/Form/SearchableSelect.vue +334 -0
  61. package/src/components/Form/SelectGroup.vue +132 -0
  62. package/src/components/Form/TagSelect.vue +38 -0
  63. package/src/components/LeafletMap.vue +31 -0
  64. package/src/components/LicenseBadge.vue +24 -0
  65. package/src/components/LoadingBlock.vue +23 -2
  66. package/src/components/MarkdownViewer.vue +3 -1
  67. package/src/components/ObjectCard.vue +42 -0
  68. package/src/components/ObjectCardBadge.vue +22 -0
  69. package/src/components/ObjectCardHeader.vue +35 -0
  70. package/src/components/ObjectCardOwner.vue +43 -0
  71. package/src/components/ObjectCardShortDescription.vue +28 -0
  72. package/src/components/OrganizationCard.vue +35 -20
  73. package/src/components/OrganizationLogo.vue +1 -1
  74. package/src/components/OrganizationNameWithCertificate.vue +13 -7
  75. package/src/components/OwnerTypeIcon.vue +1 -0
  76. package/src/components/Pagination.vue +1 -1
  77. package/src/components/Placeholder.vue +5 -2
  78. package/src/components/PostCard.vue +62 -0
  79. package/src/components/RadioGroup.vue +32 -0
  80. package/src/components/RadioInput.vue +64 -0
  81. package/src/components/ResourceAccordion/EditButton.vue +2 -3
  82. package/src/components/ResourceAccordion/MapContainer.client.vue +20 -16
  83. package/src/components/ResourceAccordion/Metadata.vue +11 -24
  84. package/src/components/ResourceAccordion/Pmtiles.client.vue +1 -1
  85. package/src/components/ResourceAccordion/Preview.vue +1 -1
  86. package/src/components/ResourceAccordion/ResourceAccordion.vue +30 -20
  87. package/src/components/ResourceAccordion/ResourceIcon.vue +1 -0
  88. package/src/components/ResourceAccordion/SchemaBadge.vue +2 -2
  89. package/src/components/ResourceExplorer/ResourceExplorer.vue +243 -0
  90. package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +116 -0
  91. package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +361 -0
  92. package/src/components/ReuseCard.vue +8 -28
  93. package/src/components/ReuseHorizontalCard.vue +80 -0
  94. package/src/components/Search/BasicAndAdvancedFilters.vue +49 -0
  95. package/src/components/Search/Filter/AccessTypeFilter.vue +37 -0
  96. package/src/components/Search/Filter/DatasetBadgeFilter.vue +40 -0
  97. package/src/components/Search/Filter/FilterButtonGroup.vue +78 -0
  98. package/src/components/Search/Filter/FormatFamilyFilter.vue +39 -0
  99. package/src/components/Search/Filter/LastUpdateRangeFilter.vue +37 -0
  100. package/src/components/Search/Filter/ProducerTypeFilter.vue +39 -0
  101. package/src/components/Search/Filter/ReuseTypeFilter.vue +42 -0
  102. package/src/components/Search/GlobalSearch.vue +611 -0
  103. package/src/components/Search/SearchInput.vue +63 -0
  104. package/src/components/Search/Sidemenu.vue +38 -0
  105. package/src/components/StatBox.vue +5 -5
  106. package/src/components/Tag.vue +30 -0
  107. package/src/components/Toggletip.vue +6 -2
  108. package/src/components/Tooltip.vue +2 -3
  109. package/src/components/TopicCard.vue +134 -0
  110. package/src/components/radioGroupContext.ts +9 -0
  111. package/src/composables/useDebouncedRef.ts +31 -0
  112. package/src/composables/useMetrics.ts +4 -3
  113. package/src/composables/useResourceCapabilities.ts +118 -0
  114. package/src/composables/useRouteQueryBoolean.ts +10 -0
  115. package/src/composables/useSelectModelSync.ts +89 -0
  116. package/src/composables/useStableQueryParams.ts +84 -0
  117. package/src/config.ts +4 -0
  118. package/src/functions/api.ts +17 -6
  119. package/src/functions/api.types.ts +4 -2
  120. package/src/functions/datasets.ts +1 -29
  121. package/src/functions/description.ts +33 -0
  122. package/src/functions/helpers.ts +11 -0
  123. package/src/functions/markdown.ts +60 -16
  124. package/src/functions/metrics.ts +33 -0
  125. package/src/functions/organizations.ts +5 -5
  126. package/src/main.ts +89 -7
  127. package/src/types/dataservices.ts +14 -12
  128. package/src/types/datasets.ts +20 -7
  129. package/src/types/discussions.ts +20 -0
  130. package/src/types/licenses.ts +3 -3
  131. package/src/types/organizations.ts +13 -1
  132. package/src/types/owned.ts +4 -2
  133. package/src/types/pages.ts +70 -0
  134. package/src/types/posts.ts +27 -0
  135. package/src/types/resources.ts +6 -0
  136. package/src/types/reuses.ts +14 -5
  137. package/src/types/search.ts +379 -0
  138. package/src/types/users.ts +12 -3
  139. package/dist/Swagger.client-CpLgaLg6.js +0 -4
  140. package/src/components/DatasetInformationPanel.vue +0 -211
@@ -9,7 +9,7 @@
9
9
  >
10
10
  <div>
11
11
  <div class="flex items-center fr-mb-1v">
12
- <h4
12
+ <h3
13
13
  :id="resourceTitleId"
14
14
  class="fr-m-0"
15
15
  >
@@ -43,7 +43,7 @@
43
43
  /></span>
44
44
  <span class="absolute inset-0 z-1" />
45
45
  </button>
46
- </h4>
46
+ </h3>
47
47
  <CopyButton
48
48
  :label="t('Copier le lien')"
49
49
  :copied-label="t('Lien copié !')"
@@ -51,22 +51,33 @@
51
51
  class="z-2"
52
52
  />
53
53
  </div>
54
- <div class="text-gray-medium subheaders-infos">
54
+ <div class="text-gray-medium subheaders-infos flex items-center gap-1">
55
55
  <SchemaBadge
56
56
  :resource
57
- class="dash-after"
58
57
  />
59
- <span class="fr-text--xs fr-mb-0 dash-after">{{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
60
- <span
61
- v-if="resource.format"
62
- class="fr-text--xs fr-mb-0 dash-after"
63
- >
64
- <span class="hidden show-on-small">{{ t("Format") }}</span>
65
- {{ resource.format.trim().toLowerCase() }}
66
- <span v-if="resourceFilesize">({{ filesize(resourceFilesize) }})</span>
67
- </span>
58
+ <RiSubtractLine
59
+ v-if="resource.schema"
60
+ aria-hidden="true"
61
+ class="size-3 fill-gray-medium"
62
+ />
63
+ <span class="text-xs mb-0">{{ t('Mis à jour {date}', { date: formatRelativeIfRecentDate(lastUpdate) }) }}</span>
64
+ <RiSubtractLine
65
+ aria-hidden="true"
66
+ class="size-3 fill-gray-medium"
67
+ />
68
+ <template v-if="resource.format">
69
+ <span class="text-xs mb-0">
70
+ <span class="hidden sm:inline">{{ t("Format") }}</span>
71
+ {{ resource.format.trim().toLowerCase() }}
72
+ <span v-if="resourceFilesize">({{ filesize(resourceFilesize) }})</span>
73
+ </span>
74
+ <RiSubtractLine
75
+ aria-hidden="true"
76
+ class="size-3 fill-gray-medium"
77
+ />
78
+ </template>
68
79
  <span
69
- class="inline-flex items-center fr-text--xs fr-mb-0"
80
+ class="inline-flex items-center text-xs mb-0"
70
81
  :aria-label="t('{n} téléchargements', resource.metrics.views)"
71
82
  >
72
83
  <span class="fr-icon-download-line fr-icon--xs fr-mr-1v" />
@@ -132,14 +143,15 @@
132
143
  rel="ugc nofollow noopener"
133
144
  :title="downloadButtonTitle"
134
145
  download
135
- class="relative text-transform-uppercase matomo_download z-2"
146
+ class="relative matomo_download z-2"
136
147
  :icon="unavailable ? RiFileWarningLine : RiDownloadLine"
137
148
  size="xs"
149
+ color="secondary"
138
150
  :aria-describedby="resourceTitleId"
139
151
  external
140
152
  @click="trackEvent('Jeux de données', 'Télécharger un fichier', 'Bouton : télécharger un fichier')"
141
153
  >
142
- <span class="sr-only">{{ t('Télécharger le fichier au format ') }}</span>{{ format }}
154
+ {{ t('Télécharger') }}
143
155
  </BrandedButton>
144
156
  </p>
145
157
  <p
@@ -358,7 +370,7 @@
358
370
 
359
371
  <script setup lang="ts">
360
372
  import { ref, computed, defineAsyncComponent } from 'vue'
361
- import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine } from '@remixicon/vue'
373
+ import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
362
374
  import OrganizationNameWithCertificate from '../OrganizationNameWithCertificate.vue'
363
375
  import { filesize, summarize } from '../../functions/helpers'
364
376
  import { useFormatDate } from '../../functions/dates'
@@ -595,9 +607,7 @@ article {
595
607
  article header .subheaders-infos .hidden.show-on-small {
596
608
  display: inline !important;
597
609
  }
598
- article header .dash-after::after {
599
- content: ''
600
- } */
610
+ */
601
611
 
602
612
  /* article .fr-pl-4v fr-pr-4v {
603
613
  padding: 0.75rem !important;
@@ -2,6 +2,7 @@
2
2
  <component
3
3
  :is="(resource.format ? getResourceFormatIcon(resource.format) : null) || File"
4
4
  class="text-gray-800 shrink-0"
5
+ aria-hidden="true"
5
6
  />
6
7
  </template>
7
8
 
@@ -9,12 +9,12 @@
9
9
  >
10
10
  <RiInformationLine class="size-4" />
11
11
  <template #toggletip="{ close }">
12
- <div class="flex justify-between border-bottom">
12
+ <div class="flex justify-between border-b">
13
13
  <h5 class="fr-text--sm fr-my-0 fr-p-2v">{{ t("Schéma de données") }}</h5>
14
14
  <button
15
15
  type="button"
16
16
  :title="t('Fermer')"
17
- class="border-left close-button flex items-center justify-center"
17
+ class="border-l close-button flex items-center justify-center"
18
18
  @click="close"
19
19
  >&times;</button>
20
20
  </div>
@@ -0,0 +1,243 @@
1
+ <template>
2
+ <div v-if="allResources.length || hasAnyResources">
3
+ <div class="flex gap-6">
4
+ <div class="flex-1 min-w-0">
5
+ <ResourceExplorerViewer
6
+ v-if="selectedResource && allResources.length"
7
+ :key="selectedResource.id"
8
+ :dataset
9
+ :resource="selectedResource"
10
+ />
11
+ <div
12
+ v-else-if="search"
13
+ class="flex flex-col items-center py-12"
14
+ >
15
+ <slot name="no-results-image">
16
+ <img
17
+ :src="noResultsImage"
18
+ class="h-20"
19
+ alt=""
20
+ >
21
+ </slot>
22
+ <p class="fr-text--bold fr-my-3v">
23
+ {{ t('Pas de résultats pour « {q} »', { q: search }) }}
24
+ </p>
25
+ <BrandedButton
26
+ color="primary"
27
+ @click="updateSearch('')"
28
+ >
29
+ {{ t('Réinitialiser la recherche') }}
30
+ </BrandedButton>
31
+ </div>
32
+ </div>
33
+ <ResourceExplorerSidebar
34
+ :resources="allResources"
35
+ :selected-resource-id="selectedResource?.id ?? null"
36
+ :collapsed="sidebarCollapsed"
37
+ :search
38
+ @select="selectResource"
39
+ @load-more="loadMore"
40
+ @update:collapsed="sidebarCollapsed = $event"
41
+ @update:search="updateSearch($event)"
42
+ />
43
+ </div>
44
+ </div>
45
+ <div
46
+ v-else
47
+ class="flex flex-col items-center py-12"
48
+ >
49
+ <slot name="empty-image">
50
+ <img
51
+ :src="noResultsImage"
52
+ class="h-20"
53
+ alt=""
54
+ >
55
+ </slot>
56
+ <p class="fr-text--bold fr-my-3v">
57
+ {{ t('Ce jeu de données ne contient aucune ressource.') }}
58
+ </p>
59
+ </div>
60
+ </template>
61
+
62
+ <script setup lang="ts">
63
+ import { computed, ref, watch, type Ref } from 'vue'
64
+ import { useRouter } from 'vue-router'
65
+ import { useRouteQuery } from '@vueuse/router'
66
+ import { useComponentsConfig } from '../../config'
67
+ import { useTranslation } from '../../composables/useTranslation'
68
+ import { useDebouncedRef } from '../../composables/useDebouncedRef'
69
+ import { useFetch } from '../../functions/api'
70
+ import { RESOURCE_TYPE } from '../../functions/resources'
71
+ import type { PaginatedArray } from '../../types/api'
72
+ import type { DatasetV2 } from '../../types/datasets'
73
+ import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
74
+ import ResourceExplorerSidebar from './ResourceExplorerSidebar.vue'
75
+ import ResourceExplorerViewer from './ResourceExplorerViewer.vue'
76
+ import BrandedButton from '../BrandedButton.vue'
77
+
78
+ const props = withDefaults(defineProps<{
79
+ dataset: DatasetV2
80
+ noResultsImage?: string
81
+ }>(), {
82
+ noResultsImage: '',
83
+ })
84
+
85
+ const { t } = useTranslation()
86
+ const router = useRouter()
87
+ const config = useComponentsConfig()
88
+
89
+ const sidebarCollapsed = ref(false)
90
+ const search = ref('')
91
+ const { debounced: searchDebounced, flush } = useDebouncedRef(search, config.searchDebounce ?? 300)
92
+
93
+ const PAGE_SIZE = 10
94
+ const url = computed(() => `/api/2/datasets/${props.dataset.id}/resources/`)
95
+
96
+ // Resource ID from URL query
97
+ const resourceIdQuery = useRouteQuery<string | undefined>('resource_id')
98
+
99
+ // Fetch resources for each type
100
+ const mainParams = computed(() => ({
101
+ type: 'main' as const,
102
+ page_size: PAGE_SIZE,
103
+ q: searchDebounced.value || undefined,
104
+ }))
105
+ const documentationParams = computed(() => ({
106
+ type: 'documentation' as const,
107
+ page_size: PAGE_SIZE,
108
+ q: searchDebounced.value || undefined,
109
+ }))
110
+ const updateParams = computed(() => ({
111
+ type: 'update' as const,
112
+ page_size: PAGE_SIZE,
113
+ q: searchDebounced.value || undefined,
114
+ }))
115
+ const apiParams = computed(() => ({
116
+ type: 'api' as const,
117
+ page_size: PAGE_SIZE,
118
+ q: searchDebounced.value || undefined,
119
+ }))
120
+ const codeParams = computed(() => ({
121
+ type: 'code' as const,
122
+ page_size: PAGE_SIZE,
123
+ q: searchDebounced.value || undefined,
124
+ }))
125
+ const otherParams = computed(() => ({
126
+ type: 'other' as const,
127
+ page_size: PAGE_SIZE,
128
+ q: searchDebounced.value || undefined,
129
+ }))
130
+
131
+ const { data: mainData, status: mainStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: mainParams })
132
+ const { data: documentationData, status: documentationStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: documentationParams, server: false })
133
+ const { data: updateData, status: updateStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: updateParams, server: false })
134
+ const { data: apiData, status: apiStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: apiParams, server: false })
135
+ const { data: codeData, status: codeStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: codeParams, server: false })
136
+ const { data: otherData, status: otherStatus } = await useFetch<PaginatedArray<Resource>>(url, { params: otherParams, server: false })
137
+
138
+ const rawResourcesByTypes = [
139
+ { data: mainData, status: mainStatus },
140
+ { data: documentationData, status: documentationStatus },
141
+ { data: updateData, status: updateStatus },
142
+ { data: apiData, status: apiStatus },
143
+ { data: codeData, status: codeStatus },
144
+ { data: otherData, status: otherStatus },
145
+ ]
146
+
147
+ // Evaluated once at setup (before any search) — never changes afterwards
148
+ const hasAnyResources = computed(() => {
149
+ return props.dataset.resources.total > 0
150
+ })
151
+
152
+ const extraResourcesByType: Ref<Resource[]>[] = RESOURCE_TYPE.map(() => ref<Resource[]>([]))
153
+ const pageByType: Ref<number>[] = RESOURCE_TYPE.map(() => ref(1))
154
+
155
+ watch(searchDebounced, () => {
156
+ for (let i = 0; i < RESOURCE_TYPE.length; i++) {
157
+ extraResourcesByType[i]!.value = []
158
+ pageByType[i]!.value = 1
159
+ }
160
+ })
161
+
162
+ const loadMore = async (type: ResourceType) => {
163
+ const index = RESOURCE_TYPE.indexOf(type)
164
+ if (index === -1) return
165
+ const pageRef = pageByType[index]!
166
+ const extraRef = extraResourcesByType[index]!
167
+ pageRef.value++
168
+
169
+ const { data } = await useFetch<PaginatedArray<Resource>>(url, {
170
+ params: {
171
+ type,
172
+ page_size: PAGE_SIZE,
173
+ page: pageRef.value,
174
+ q: searchDebounced.value || undefined,
175
+ },
176
+ })
177
+
178
+ if (data.value) {
179
+ extraRef.value = [...extraRef.value, ...data.value.data]
180
+ }
181
+ }
182
+
183
+ const allResources = computed<ResourceGroup[]>(() => {
184
+ return RESOURCE_TYPE
185
+ .map((type, index) => {
186
+ const rawData = rawResourcesByTypes[index]!
187
+ const extraData = extraResourcesByType[index]!
188
+ return {
189
+ type: type as ResourceType,
190
+ total: rawData.data.value?.total ?? 0,
191
+ items: [...(rawData.data.value?.data ?? []), ...extraData.value],
192
+ }
193
+ })
194
+ .filter(group => group.items.length > 0)
195
+ })
196
+
197
+ const flatResources = computed(() =>
198
+ allResources.value.flatMap(g => g.items),
199
+ )
200
+
201
+ // Fetch resource by ID if specified in URL (for SSR)
202
+ const initialResourceId = resourceIdQuery.value
203
+ const { data: fetchedResource } = initialResourceId
204
+ ? await useFetch<Resource>(`/api/1/datasets/${props.dataset.id}/resources/${initialResourceId}/`)
205
+ : { data: ref(null) }
206
+
207
+ // Initial selection (synchronous for SSR hydration)
208
+ function getInitialResource(): Resource | null {
209
+ const resourceId = resourceIdQuery.value
210
+ if (resourceId) {
211
+ // First check in already loaded resources
212
+ const existing = flatResources.value.find(r => r.id === resourceId)
213
+ if (existing) return existing
214
+ // Use fetched resource if available
215
+ if (fetchedResource.value) return fetchedResource.value
216
+ }
217
+ // Default to first resource
218
+ return flatResources.value[0] ?? null
219
+ }
220
+
221
+ const selectedResource = ref<Resource | null>(getInitialResource())
222
+
223
+ function updateSearch(newSearch: string) {
224
+ search.value = newSearch
225
+ flush()
226
+ }
227
+
228
+ const selectResource = (resource: Resource) => {
229
+ selectedResource.value = resource
230
+ router.replace({
231
+ query: { ...router.currentRoute.value.query, resource_id: resource.id },
232
+ })
233
+ }
234
+
235
+ // Update selection when resources change (e.g., after client-side fetch completes)
236
+ watch(flatResources, () => {
237
+ if (selectedResource.value) return
238
+ const firstResource = flatResources.value[0]
239
+ if (firstResource) {
240
+ selectedResource.value = firstResource
241
+ }
242
+ })
243
+ </script>
@@ -0,0 +1,116 @@
1
+ <template>
2
+ <aside
3
+ v-if="!collapsed"
4
+ class="w-72 shrink-0 pl-4"
5
+ >
6
+ <div class="flex items-center justify-between mb-3">
7
+ <h3 class="text-sm font-bold uppercase mb-0">
8
+ {{ t('Ressources') }}
9
+ </h3>
10
+ <button
11
+ type="button"
12
+ :title="t('Masquer le panneau')"
13
+ class="p-1 hover:bg-gray-100 rounded"
14
+ @click="$emit('update:collapsed', true)"
15
+ >
16
+ <RiArrowRightSLine class="size-5" />
17
+ </button>
18
+ </div>
19
+
20
+ <div class="mb-3">
21
+ <label
22
+ :for="searchId"
23
+ class="sr-only"
24
+ >{{ t('Rechercher') }}</label>
25
+ <input
26
+ :id="searchId"
27
+ :value="search"
28
+ type="search"
29
+ class="w-full border border-gray-default rounded px-2.5 py-1.5 text-sm"
30
+ :placeholder="t('Rechercher')"
31
+ @input="$emit('update:search', ($event.target as HTMLInputElement).value)"
32
+ >
33
+ </div>
34
+
35
+ <div class="space-y-4 overflow-y-auto">
36
+ <div
37
+ v-for="group in resources"
38
+ :key="group.type"
39
+ >
40
+ <div class="text-xs text-gray-plain font-bold uppercase mb-1.5 border-b border-gray-default pb-1">
41
+ {{ getResourceLabel(group.type, group.total) }}
42
+ </div>
43
+ <ul class="list-none p-0 m-0 space-y-0.5">
44
+ <li
45
+ v-for="resource in group.items"
46
+ :key="resource.id"
47
+ >
48
+ <button
49
+ type="button"
50
+ class="w-full text-left px-2 py-1.5 rounded text-sm flex items-center gap-1.5 hover:bg-gray-100"
51
+ :class="{
52
+ 'font-bold bg-blue-50': resource.id === selectedResourceId,
53
+ }"
54
+ @click="$emit('select', resource)"
55
+ >
56
+ <ResourceIcon
57
+ :resource
58
+ class="size-3.5 shrink-0"
59
+ />
60
+ <span class="truncate">{{ resource.title || t('Fichier sans nom') }}</span>
61
+ </button>
62
+ </li>
63
+ </ul>
64
+ <button
65
+ v-if="group.items.length < group.total"
66
+ type="button"
67
+ class="w-full text-left px-2 py-1.5 text-sm text-blue-default hover:underline"
68
+ @click="$emit('load-more', group.type)"
69
+ >
70
+ {{ t('Charger plus…') }}
71
+ </button>
72
+ </div>
73
+ </div>
74
+ </aside>
75
+
76
+ <div
77
+ v-else
78
+ class="shrink-0 flex items-start pt-1"
79
+ >
80
+ <button
81
+ type="button"
82
+ :title="t('Afficher le panneau des ressources')"
83
+ class="p-1 hover:bg-gray-100 rounded border border-gray-default"
84
+ @click="$emit('update:collapsed', false)"
85
+ >
86
+ <RiArrowLeftSLine class="size-5" />
87
+ </button>
88
+ </div>
89
+ </template>
90
+
91
+ <script setup lang="ts">
92
+ import { useId } from 'vue'
93
+ import { RiArrowRightSLine, RiArrowLeftSLine } from '@remixicon/vue'
94
+ import ResourceIcon from '../ResourceAccordion/ResourceIcon.vue'
95
+ import { getResourceLabel } from '../../functions/resources'
96
+ import { useTranslation } from '../../composables/useTranslation'
97
+ import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
98
+
99
+ const { t } = useTranslation()
100
+
101
+ defineProps<{
102
+ resources: ResourceGroup[]
103
+ selectedResourceId: string | null
104
+ collapsed: boolean
105
+ search: string
106
+ }>()
107
+
108
+ defineEmits<{
109
+ 'select': [resource: Resource]
110
+ 'load-more': [type: ResourceType]
111
+ 'update:collapsed': [value: boolean]
112
+ 'update:search': [value: string]
113
+ }>()
114
+
115
+ const searchId = useId()
116
+ </script>