@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
@@ -0,0 +1,334 @@
1
+ <template>
2
+ <div
3
+ class="fr-input-group"
4
+ :class="{ 'fr-input-group--error': errorText, 'fr-input-group--warning': !errorText && warningText, 'fr-input-group--valid': validText }"
5
+ >
6
+ <label
7
+ :for="id"
8
+ :title="explanation"
9
+ class="fr-label"
10
+ :class="{ 'sr-only': hideLabel }"
11
+ >
12
+ {{ label }}
13
+ <span
14
+ v-if="required"
15
+ class="text-new-primary"
16
+ >*</span>
17
+ <span
18
+ v-if="explanation"
19
+ class="fr-icon-information-line"
20
+ aria-hidden="true"
21
+ />
22
+ <span
23
+ v-if="hintText"
24
+ class="fr-hint-text"
25
+ >{{ hintText }}</span>
26
+ </label>
27
+ <Combobox
28
+ v-slot="{ open, activeOption }"
29
+ v-model="model"
30
+ :multiple
31
+ :by="compareTwoOptions"
32
+ :aria-describedby="ariaDescribedBy"
33
+ :disabled="loading"
34
+ nullable
35
+ >
36
+ <div class="relative mt-1">
37
+ <div
38
+ ref="floatingReference"
39
+ 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"
40
+ >
41
+ <ComboboxInput
42
+ :id
43
+ class="input shadow-input group-data-[input-color=blue]/form:shadow-input-blue !pr-10"
44
+ :class="showClearButton ? '!pr-[4.5rem]' : '!pr-10'"
45
+ :display-value="(option: unknown) => option ? displayValue(option as ModelType) : ''"
46
+ :placeholder
47
+ @change="query = $event.target.value"
48
+ />
49
+ <AnimatedLoader
50
+ v-if="loading"
51
+ class="absolute text-lg top-2 right-3 flex items-center justify-end hover:!bg-transparent"
52
+ />
53
+ <div
54
+ v-else
55
+ class="absolute inset-y-0 flex items-center justify-end pr-2"
56
+ :class="{
57
+ 'right-0': open,
58
+ 'inset-x-0': !open,
59
+ }"
60
+ >
61
+ <ComboboxButton
62
+ v-if="! open"
63
+ class="w-full h-full hover:!bg-transparent"
64
+ :data-testid="`searchable-select-${simpleSlug(label)}`"
65
+ />
66
+ <button
67
+ v-if="showClearButton"
68
+ type="button"
69
+ class="p-2"
70
+ :title="t('Effacer')"
71
+ @click.prevent="model = null"
72
+ >
73
+ <RiDeleteBinLine class="size-4 text-gray-800" />
74
+ </button>
75
+ <ComboboxButton class="p-2">
76
+ <RiArrowDownSLine class="size-4 text-gray-800" />
77
+ </ComboboxButton>
78
+ </div>
79
+ </div>
80
+ <TransitionRoot
81
+ leave="transition ease-in duration-100"
82
+ leave-from="opacity-100"
83
+ leave-to="opacity-0"
84
+ @after-leave="query = ''"
85
+ >
86
+ <ComboboxOptions
87
+ ref="popover"
88
+ :style="floatingStyles"
89
+ 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"
90
+ >
91
+ <div
92
+ v-if="!filteredAndGroupedOptions && query !== ''"
93
+ class="relative cursor-default select-none px-4 py-2 text-gray-700"
94
+ >
95
+ {{ t('Aucun résultat') }}
96
+ </div>
97
+
98
+ <div
99
+ v-for="(groupOptions, group) in filteredAndGroupedOptions"
100
+ :key="group"
101
+ >
102
+ <li
103
+ v-if="group"
104
+ class="relative select-none py-4 px-4 list-none bg-gray-100 uppercase text-gray-800 font-semibold text-xs"
105
+ >
106
+ {{ group }}
107
+ </li>
108
+ <ComboboxOption
109
+ v-for="option in groupOptions"
110
+ :key="getOptionId(toValue(option))"
111
+ v-slot="comboboxSlot"
112
+ as="template"
113
+ :value="option"
114
+ >
115
+ <li
116
+ class="relative cursor-default select-none py-2 pr-4 list-none flex items-center gap-2 text-gray-900"
117
+ :class="{
118
+ 'bg-gray-lower': isActive(activeOption, option),
119
+ 'pl-2': comboboxSlot.selected,
120
+ 'pl-6': !comboboxSlot.selected,
121
+ }"
122
+ >
123
+ <div
124
+ class="flex items-center justify-center aspect-square"
125
+ >
126
+ <RiCheckLine
127
+ v-if="comboboxSlot.selected"
128
+ class="size-4 text-new-primary"
129
+ />
130
+ </div>
131
+ <slot
132
+ name="option"
133
+ v-bind="{ option, active: isActive(activeOption, option) as boolean }"
134
+ >
135
+ {{ displayValue((multiple ? [option] : option) as ModelType) }}
136
+ </slot>
137
+ </li>
138
+ </ComboboxOption>
139
+ </div>
140
+ </ComboboxOptions>
141
+ </TransitionRoot>
142
+ </div>
143
+ </Combobox>
144
+ <p
145
+ v-if="errorText"
146
+ :id="errorTextId"
147
+ class="fr-error-text"
148
+ >
149
+ {{ errorText }}
150
+ </p>
151
+ </div>
152
+ </template>
153
+
154
+ <script setup lang="ts" generic="T extends string | number | object, Multiple extends true | false">
155
+ import { ref, computed, onMounted, useId, toValue, useTemplateRef } from 'vue'
156
+ import { useFloating, autoUpdate, autoPlacement } from '@floating-ui/vue'
157
+ import { watchDebounced } from '@vueuse/core'
158
+ import {
159
+ Combobox,
160
+ ComboboxInput,
161
+ ComboboxButton,
162
+ ComboboxOptions,
163
+ ComboboxOption,
164
+ TransitionRoot,
165
+ } from '@headlessui/vue'
166
+ import { RiArrowDownSLine, RiCheckLine, RiDeleteBinLine } from '@remixicon/vue'
167
+ import { useTranslation } from '../../composables/useTranslation'
168
+ import AnimatedLoader from '../AnimatedLoader.vue'
169
+
170
+ type ModelType = Multiple extends false ? T : Array<T>
171
+
172
+ const props = withDefaults(defineProps<{
173
+ validText?: string
174
+ errorText?: string | null
175
+ warningText?: string | null
176
+ hintText?: string
177
+ explanation?: string
178
+ label: string
179
+ placeholder?: string
180
+ loading?: boolean
181
+ hideLabel?: boolean
182
+
183
+ getOptionId?: (option: T) => string | number
184
+ groupBy?: (option: T) => string
185
+ displayValue?: (option: ModelType) => string
186
+ filter?: (option: T, query: string) => boolean
187
+ options?: Array<T>
188
+ suggest?: (query: string) => Promise<Array<T>>
189
+ allowNewOption?: (query: string) => T
190
+
191
+ required?: boolean
192
+ multiple: Multiple
193
+ }>(), {
194
+ required: false,
195
+ loading: false,
196
+ hideLabel: false,
197
+
198
+ displayValue: (): string => '',
199
+
200
+ groupBy: (): string => '',
201
+ getOptionId: (option: T): string | number => {
202
+ if (typeof option === 'string') return option
203
+ if (typeof option === 'number') return option
204
+ if (typeof option === 'object' && 'id' in option) return option.id as string
205
+
206
+ throw new Error('Please set getOptionId()')
207
+ },
208
+ filter: (option: T, query: string): boolean => {
209
+ const searchables = []
210
+
211
+ if (typeof option === 'string') searchables.push(option)
212
+ if (typeof option === 'number') searchables.push(option.toString())
213
+ if (typeof option === 'object') {
214
+ for (const value of Object.values(option)) {
215
+ if (typeof value !== 'string') continue
216
+
217
+ searchables.push(value)
218
+ }
219
+ }
220
+
221
+ for (const searchable of searchables) {
222
+ if (searchable.toLocaleLowerCase().includes(query.toLocaleLowerCase())) {
223
+ return true
224
+ }
225
+ }
226
+
227
+ return false
228
+ },
229
+ })
230
+
231
+ const model = defineModel<ModelType | null>()
232
+
233
+ const { t } = useTranslation()
234
+
235
+ const id = useId()
236
+ const errorTextId = useId()
237
+
238
+ const ariaDescribedBy = computed(() => {
239
+ if (props.errorText) return errorTextId
240
+ return ''
241
+ })
242
+
243
+ const showClearButton = computed(() => !props.required && !props.multiple && model.value)
244
+
245
+ const query = ref('')
246
+
247
+ const suggestedOptions = ref<Array<T> | null>(null)
248
+ const fetchSuggests = async () => {
249
+ if (!props.suggest) return
250
+
251
+ const savedQuery = query.value
252
+ const options = await props.suggest(query.value)
253
+ if (savedQuery === query.value) {
254
+ suggestedOptions.value = options
255
+ }
256
+ }
257
+
258
+ async function fetchSuggestsQuery(q: string) {
259
+ query.value = q
260
+ return fetchSuggests()
261
+ }
262
+
263
+ defineExpose({
264
+ fetchSuggestsQuery,
265
+ })
266
+
267
+ onMounted(async () => {
268
+ await fetchSuggests()
269
+ })
270
+
271
+ watchDebounced(query, async () => {
272
+ await fetchSuggests()
273
+ }, { debounce: 400, maxWait: 800 })
274
+
275
+ const filteredOptions = computed<Array<T>>(() => {
276
+ if (props.suggest) {
277
+ return suggestedOptions.value as Array<T>
278
+ }
279
+
280
+ if (!props.options) throw new Error('Please set options or suggest')
281
+
282
+ if (!query.value) return props.options
283
+ return props.options.filter(option => props.filter(option, query.value))
284
+ })
285
+ const filteredOptionsWithNewOption = computed(() => {
286
+ if (!props.allowNewOption || !query.value) return filteredOptions.value
287
+
288
+ const newOption = props.allowNewOption(query.value)
289
+ if (filteredOptions.value.find(option => compareTwoOptions(option, newOption))) return filteredOptions.value
290
+
291
+ return [
292
+ newOption,
293
+ ...filteredOptions.value,
294
+ ]
295
+ })
296
+
297
+ const filteredAndGroupedOptions = computed<Record<string, Array<T>>>(() => {
298
+ if (!filteredOptionsWithNewOption.value) return {}
299
+
300
+ const groups = {} as Record<string, Array<T>>
301
+ for (const option of filteredOptionsWithNewOption.value) {
302
+ const group = props.groupBy(option)
303
+ groups[group] = groups[group] || []
304
+ groups[group].push(option)
305
+ }
306
+
307
+ return groups
308
+ })
309
+
310
+ function compareTwoOptions(a: T | null, b: T | null) {
311
+ if (a === b) return true
312
+ if (!a || !b) return false
313
+
314
+ return props.getOptionId(a) === props.getOptionId(b)
315
+ }
316
+
317
+ function isActive(activeOption: T, currentOption: T) {
318
+ return activeOption ? props.getOptionId(activeOption) === props.getOptionId(currentOption) : false
319
+ }
320
+
321
+ function simpleSlug(text: string) {
322
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
323
+ }
324
+
325
+ const referenceRef = useTemplateRef('floatingReference')
326
+ const floatingRef = useTemplateRef<InstanceType<typeof ComboboxOptions>>('popover')
327
+ const { floatingStyles } = useFloating(referenceRef, floatingRef, {
328
+ middleware: [autoPlacement({
329
+ allowedPlacements: ['bottom-start', 'bottom', 'bottom-end'],
330
+ crossAxis: true,
331
+ })],
332
+ whileElementsMounted: autoUpdate,
333
+ })
334
+ </script>
@@ -0,0 +1,132 @@
1
+ <template>
2
+ <div
3
+ class="fr-select-group"
4
+ :class="selectGroupClass"
5
+ >
6
+ <label
7
+ v-if="!hideLabel"
8
+ class="fr-label"
9
+ :for="id"
10
+ >
11
+ {{ label }}
12
+ <span
13
+ v-if="required"
14
+ class="text-new-primary"
15
+ >*</span>
16
+ <span
17
+ v-if="hintText"
18
+ class="fr-hint-text"
19
+ >{{ hintText }}</span>
20
+ </label>
21
+ <select
22
+ :id="id"
23
+ v-model="model"
24
+ class="fr-select group-data-[input-color=blue]/form:!shadow-input-blue"
25
+ :class="{ 'fr-select--error': hasError, 'fr-select--valid': isValid }"
26
+ :aria-describedby="ariaDescribedBy"
27
+ :aria-label="hideLabel ? label : undefined"
28
+ :required="required"
29
+ :disabled="disabled"
30
+ >
31
+ <option
32
+ v-if="! hideNullOption"
33
+ :value="null"
34
+ disabled
35
+ hidden
36
+ >
37
+ {{ t('Sélectionner une option') }}
38
+ </option>
39
+ <option
40
+ v-for="option in options"
41
+ :key="String(option.value ?? option.label)"
42
+ :value="option.value"
43
+ :disabled="option.disabled"
44
+ :hidden="option.hidden"
45
+ >
46
+ {{ option.label }}
47
+ </option>
48
+ </select>
49
+ <p
50
+ v-if="isValid"
51
+ :id="validTextId"
52
+ class="fr-valid-text"
53
+ >
54
+ {{ validText }}
55
+ </p>
56
+ <p
57
+ v-else-if="hasError"
58
+ :id="errorTextId"
59
+ class="fr-error-text"
60
+ >
61
+ {{ errorText }}
62
+ </p>
63
+ </div>
64
+ </template>
65
+
66
+ <script setup lang="ts">
67
+ import { computed, useId } from 'vue'
68
+ import { useTranslation } from '../../composables/useTranslation'
69
+
70
+ export type SelectOption = {
71
+ label: string
72
+ value?: string | boolean | null | undefined
73
+ disabled?: boolean
74
+ hidden?: boolean
75
+ selected?: boolean
76
+ }
77
+
78
+ export type SelectGroupProps = {
79
+ disabled?: boolean
80
+ errorText?: string | null
81
+ hasError?: boolean
82
+ hintText?: string
83
+ isValid?: boolean
84
+ label: string
85
+ hideLabel?: boolean
86
+ modelValue?: string | boolean | null
87
+ options: Array<SelectOption>
88
+ required?: boolean
89
+ validText?: string
90
+ hideNullOption?: boolean
91
+ }
92
+
93
+ const model = defineModel<string | boolean | null>()
94
+
95
+ const props = withDefaults(defineProps<SelectGroupProps>(), {
96
+ disabled: false,
97
+ errorText: '',
98
+ hasError: false,
99
+ hintText: '',
100
+ isValid: false,
101
+ modelValue: undefined,
102
+ required: false,
103
+ validText: '',
104
+ hideLabel: false,
105
+ hideNullOption: false,
106
+ })
107
+
108
+ const { t } = useTranslation()
109
+
110
+ const id = useId()
111
+
112
+ const errorTextId = computed(() => id + '-desc-error')
113
+ const validTextId = computed(() => id + '-desc-valid')
114
+ const ariaDescribedBy = computed(() => {
115
+ let describedBy = ''
116
+ if (props.isValid) {
117
+ describedBy += ' ' + validTextId.value
118
+ }
119
+ else if (props.hasError) {
120
+ describedBy += ' ' + errorTextId.value
121
+ }
122
+ return describedBy
123
+ })
124
+
125
+ const selectGroupClass = computed(() => {
126
+ return {
127
+ 'fr-select-group--disabled': props.disabled,
128
+ 'fr-select-group--error': props.hasError,
129
+ 'fr-select-group--valid': props.isValid,
130
+ }
131
+ })
132
+ </script>
@@ -0,0 +1,38 @@
1
+ <template>
2
+ <SearchableSelect
3
+ v-model="model"
4
+ :options="[]"
5
+ :suggest="suggestTags"
6
+ :label="t('Mots clés')"
7
+ :placeholder="t('Tous les mots clés')"
8
+ :get-option-id="(tag: string) => tag"
9
+ :display-value="(tag: string) => tag"
10
+ :multiple="false"
11
+ />
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { ofetch } from 'ofetch'
16
+ import { useComponentsConfig } from '../../config'
17
+ import { useTranslation } from '../../composables/useTranslation'
18
+ import { useStringSelectSync } from '../../composables/useSelectModelSync'
19
+ import SearchableSelect from './SearchableSelect.vue'
20
+
21
+ const model = defineModel<string | null>({ default: null })
22
+ const id = defineModel<string | undefined>('id')
23
+
24
+ const config = useComponentsConfig()
25
+ const { t } = useTranslation()
26
+
27
+ useStringSelectSync({ model, id })
28
+
29
+ type TagSuggest = { text: string }
30
+
31
+ async function suggestTags(q: string) {
32
+ const tags = await ofetch<TagSuggest[]>('/api/1/tags/suggest/', {
33
+ baseURL: config.apiBase,
34
+ query: { q, size: 20 },
35
+ })
36
+ return tags.map(tag => tag.text)
37
+ }
38
+ </script>
@@ -0,0 +1,31 @@
1
+ <template>
2
+ <div
3
+ ref="containerRef"
4
+ class="h-[400px] w-[400px] fr-raw-link"
5
+ />
6
+ </template>
7
+
8
+ <script setup lang="ts">
9
+ import { ref, onMounted } from 'vue'
10
+ import type { GeoJsonObject } from 'geojson'
11
+
12
+ const props = defineProps<{
13
+ geojson: GeoJsonObject
14
+ }>()
15
+
16
+ const containerRef = ref<HTMLElement | null>(null)
17
+
18
+ onMounted(async () => {
19
+ await import('leaflet/dist/leaflet.css')
20
+ const L = await import('leaflet')
21
+
22
+ if (!containerRef.value) return
23
+ const map = L.map(containerRef.value)
24
+ L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
25
+ maxZoom: 19,
26
+ attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
27
+ }).addTo(map)
28
+ const geoJSON = L.geoJSON(props.geojson).addTo(map)
29
+ map.fitBounds(geoJSON.getBounds())
30
+ })
31
+ </script>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <AppLink
3
+ v-if="license && license.url"
4
+ :to="license.url"
5
+ class="px-1 py-[2px] font-mono bg-gray-some text-gray-medium rounded"
6
+ >
7
+ {{ license.title }}
8
+ </AppLink>
9
+ <span
10
+ v-else-if="license"
11
+ class="px-1 py-[2px] font-mono bg-gray-some text-gray-medium rounded"
12
+ >
13
+ {{ license.title }}
14
+ </span>
15
+ </template>
16
+
17
+ <script setup lang="ts">
18
+ import type { License } from '../types/licenses'
19
+ import AppLink from './AppLink.vue'
20
+
21
+ defineProps<{
22
+ license: License
23
+ }>()
24
+ </script>
@@ -1,7 +1,18 @@
1
1
  <template>
2
2
  <div class="relative">
3
3
  <div :class="{ 'opacity-50 min-h-64': loading }">
4
- <slot />
4
+ <slot
5
+ v-if="!loading && !hasError && data"
6
+ :data="data"
7
+ />
8
+ <slot
9
+ v-else-if="hasError"
10
+ name="error"
11
+ >
12
+ <div class="text-center py-8 text-gray-500">
13
+ Une erreur est survenue lors du chargement.
14
+ </div>
15
+ </slot>
5
16
  </div>
6
17
  <div
7
18
  v-if="loading"
@@ -12,15 +23,25 @@
12
23
  </div>
13
24
  </template>
14
25
 
15
- <script setup lang="ts">
26
+ <script setup lang="ts" generic="T">
16
27
  import { computed } from 'vue'
17
28
  import AnimatedLoader from './AnimatedLoader.vue'
18
29
 
19
30
  const props = defineProps<{
20
31
  status: string
32
+ data: T | null | undefined
33
+ }>()
34
+
35
+ defineSlots<{
36
+ default: (props: { data: NonNullable<T> }) => unknown
37
+ error: () => unknown
21
38
  }>()
22
39
 
23
40
  const loading = computed(() => {
24
41
  return props.status === 'idle' || props.status === 'pending'
25
42
  })
43
+
44
+ const hasError = computed(() => {
45
+ return props.status === 'error'
46
+ })
26
47
  </script>
@@ -2,7 +2,7 @@
2
2
  <template>
3
3
  <div
4
4
  :class="size === 'sm' ? markdownSmClasses : markdownClasses"
5
- v-html="formatMarkdown(content, minHeading)"
5
+ v-html="formatMarkdown(content, { minDepth: minHeading, noHeadings })"
6
6
  />
7
7
  </template>
8
8
 
@@ -12,9 +12,11 @@ import { formatMarkdown, markdownClasses, markdownSmClasses } from '../functions
12
12
  withDefaults(defineProps<{
13
13
  content: string
14
14
  minHeading?: 1 | 2 | 3 | 4 | 5 | 6
15
+ noHeadings?: boolean
15
16
  size?: 'sm' | 'md'
16
17
  }>(), {
17
18
  minHeading: 3,
18
19
  size: 'sm',
20
+ noHeadings: false,
19
21
  })
20
22
  </script>
@@ -0,0 +1,42 @@
1
+ <template>
2
+ <article
3
+ class="p-4 border bg-white border-gray-default relative hover:bg-gray-some fr-enlarge-link"
4
+ :class="articleClass"
5
+ >
6
+ <slot name="badge" />
7
+ <div class="flex flex-wrap md:flex-nowrap gap-4 items-start">
8
+ <div
9
+ v-if="$slots.media"
10
+ class="flex-none"
11
+ >
12
+ <div
13
+ class="flex justify-center items-center border border-gray-lower bg-[#fff] rounded-md overflow-hidden"
14
+ :class="mediaContainerClass"
15
+ >
16
+ <slot name="media" />
17
+ </div>
18
+ </div>
19
+ <div class="flex-1 overflow-hidden">
20
+ <slot />
21
+ </div>
22
+ </div>
23
+ </article>
24
+ </template>
25
+
26
+ <script setup lang="ts">
27
+ import { computed } from 'vue'
28
+
29
+ const props = withDefaults(defineProps<{
30
+ articleClass?: string | string[] | Record<string, boolean>
31
+ mediaSize?: 'sm' | 'lg'
32
+ }>(), {
33
+ mediaSize: 'sm',
34
+ })
35
+
36
+ const mediaContainerClass = computed(() => {
37
+ if (props.mediaSize === 'lg') {
38
+ return 'w-[240px] h-[160px]'
39
+ }
40
+ return 'p-2'
41
+ })
42
+ </script>
@@ -0,0 +1,22 @@
1
+ <template>
2
+ <div class="absolute top-0 left-4 fr-grid-row fr-grid-row--middle fr-mt-n3v">
3
+ <p class="fr-badge fr-badge--sm fr-badge--mention-grey text-gray-medium">
4
+ <component
5
+ :is="icon"
6
+ aria-hidden="true"
7
+ class="size-4 mr-1"
8
+ />
9
+ <slot />
10
+ </p>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ // Only one ObjectCardBadge should be rendered at a time (use v-if/v-else-if),
16
+ // because the absolute positioning will cause multiple badges to overlap.
17
+ import type { Component } from 'vue'
18
+
19
+ defineProps<{
20
+ icon: Component
21
+ }>()
22
+ </script>