@datagouv/components-next 1.0.2-dev.231 → 1.0.2-dev.232
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.
- package/dist/{ImagePreview.client-H7jGf9xS.js → ImagePreview.client-CSRz7eUF.js} +2 -2
- package/dist/{JsonPreview.client-CQSCGT6V.js → JsonPreview.client-wT2hfYLv.js} +2 -2
- package/dist/{PdfPreview.client-Cz_jnvla.js → PdfPreview.client-DifxM8Th.js} +2 -2
- package/dist/{Pmtiles.client-CSoax_YA.js → Pmtiles.client-OyXiX_7g.js} +4 -4
- package/dist/{PreviewWrapper-DBlpLgs2.js → PreviewWrapper-D7yfQyyH.js} +1 -1
- package/dist/{XmlPreview.client-8vye8TAa.js → XmlPreview.client-BkPGZLn6.js} +2 -2
- package/dist/components-next.js +9356 -9191
- package/dist/components.css +1 -1
- package/dist/{france-Bu5x7PSp.js → france-BNatwStf.js} +1 -1
- package/dist/{resources-BBaRfEol.js → resources-kuI_FosB.js} +5 -5
- package/dist/{vue-D62CcCib.js → vue-D5eMxsCr.js} +78 -74
- package/package.json +1 -1
- package/src/components/ResourceExplorer/ResourceExplorer.vue +8 -2
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +140 -90
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +17 -2
- package/src/components/ResourceExplorer/ResourceSelector.vue +3 -0
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +3 -5
- package/src/components/ResourceIconBadge.vue +28 -0
- package/src/components/ResourceListItem.vue +83 -27
- package/src/components/TabularExplorer/TabularActiveFilters.vue +94 -54
- package/src/components/TabularExplorer/TabularExplorer.vue +58 -18
- package/src/components/TabularExplorer/TabularSearchInput.vue +40 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +14 -6
- package/src/components/TabularExplorer/TabularTable.vue +12 -5
- package/src/components/TabularExplorer/TabularToolbar.vue +7 -4
- package/src/components/TabularExplorer/useTabularContext.ts +10 -0
- package/src/composables/useResourceCapabilities.ts +5 -0
- package/src/main.ts +2 -0
|
@@ -14,8 +14,13 @@
|
|
|
14
14
|
class="flex"
|
|
15
15
|
:class="fullscreen ? 'min-h-0 flex-1 overflow-hidden' : 'overflow-hidden rounded border border-gray-default'"
|
|
16
16
|
>
|
|
17
|
-
<div
|
|
17
|
+
<div
|
|
18
|
+
class="hidden md:flex"
|
|
19
|
+
:class="resourceListExpanded ? 'flex-1' : ''"
|
|
20
|
+
>
|
|
18
21
|
<ResourceExplorerSidebar
|
|
22
|
+
v-model:expanded="resourceListExpanded"
|
|
23
|
+
:dataset
|
|
19
24
|
:groups
|
|
20
25
|
:selected-resource-id="selectedResource?.id ?? null"
|
|
21
26
|
:collapsed="sidebarCollapsed"
|
|
@@ -31,7 +36,7 @@
|
|
|
31
36
|
<div
|
|
32
37
|
id="resource-explorer-viewer"
|
|
33
38
|
class="flex-1 min-w-0"
|
|
34
|
-
:class="fullscreen ? 'flex flex-col' : ''"
|
|
39
|
+
:class="[fullscreen ? 'flex flex-col' : '', resourceListExpanded ? 'md:hidden' : '']"
|
|
35
40
|
role="region"
|
|
36
41
|
:aria-label="t('Détail de la ressource')"
|
|
37
42
|
>
|
|
@@ -154,6 +159,7 @@ const {
|
|
|
154
159
|
watch(selectedResource, resource => emit('select', resource), { immediate: true })
|
|
155
160
|
|
|
156
161
|
const sidebarCollapsed = ref(false)
|
|
162
|
+
const resourceListExpanded = ref(false)
|
|
157
163
|
|
|
158
164
|
const resourceTo = (resource: Resource): RouteLocationRaw => ({
|
|
159
165
|
query: { ...route.query, resource_id: resource.id },
|
|
@@ -1,134 +1,183 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
:
|
|
2
|
+
<!-- The resize grip sits outside the aside: it straddles the border, and the
|
|
3
|
+
aside's overflow-hidden would clip its outer half. -->
|
|
4
|
+
<div
|
|
5
|
+
class="relative flex"
|
|
6
|
+
:class="expanded ? 'flex-1' : 'shrink-0'"
|
|
7
7
|
>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
<!-- Expanded, the list takes the viewer's place (the explorer hides the viewer). -->
|
|
9
|
+
<aside
|
|
10
|
+
class="flex flex-col overflow-hidden bg-white"
|
|
11
|
+
:class="[
|
|
12
|
+
expanded ? 'flex-1' : 'shrink-0 border-r border-gray-default',
|
|
13
|
+
resizing ? '' : 'transition-[width] duration-200',
|
|
14
|
+
]"
|
|
15
|
+
:style="expanded ? undefined : { width: collapsed ? '48px' : `${width}px` }"
|
|
16
|
+
:aria-label="t('Ressources')"
|
|
11
17
|
>
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
class="
|
|
15
|
-
>{{ t('Ressources') }}</span>
|
|
16
|
-
<button
|
|
17
|
-
type="button"
|
|
18
|
-
:title="collapsed ? t('Afficher le panneau des ressources') : t('Masquer le panneau')"
|
|
19
|
-
class="flex size-6 items-center justify-center rounded text-gray-plain hover:bg-gray-100"
|
|
20
|
-
@click="$emit('update:collapsed', !collapsed)"
|
|
18
|
+
<div
|
|
19
|
+
class="flex h-14 items-center border-b border-gray-default bg-gray-some px-3"
|
|
20
|
+
:class="collapsed ? 'justify-center' : 'justify-between'"
|
|
21
21
|
>
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
class="
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
<span
|
|
23
|
+
v-if="!collapsed"
|
|
24
|
+
class="text-[14px] font-medium text-gray-title"
|
|
25
|
+
>{{ t('Ressources') }}</span>
|
|
26
|
+
<span class="flex items-center gap-1">
|
|
27
|
+
<button
|
|
28
|
+
v-if="!collapsed"
|
|
29
|
+
type="button"
|
|
30
|
+
class="inline-flex h-7 items-center gap-1 rounded px-2 text-[13px] font-medium text-gray-title underline-offset-4 hover:bg-gray-100 hover:underline"
|
|
31
|
+
@click="$emit('update:expanded', !expanded)"
|
|
32
|
+
>
|
|
33
|
+
<template v-if="expanded">
|
|
34
|
+
<RiArrowLeftLine
|
|
35
|
+
class="size-3.5"
|
|
36
|
+
aria-hidden="true"
|
|
37
|
+
/>
|
|
38
|
+
{{ t('Revenir à l’explorateur') }}
|
|
39
|
+
</template>
|
|
40
|
+
<template v-else>
|
|
41
|
+
{{ t('Tout afficher') }}
|
|
42
|
+
</template>
|
|
43
|
+
</button>
|
|
44
|
+
<button
|
|
45
|
+
v-if="!expanded"
|
|
46
|
+
type="button"
|
|
47
|
+
:title="collapsed ? t('Afficher le panneau des ressources') : t('Masquer le panneau')"
|
|
48
|
+
class="flex size-6 items-center justify-center rounded text-gray-plain hover:bg-gray-100"
|
|
49
|
+
@click="$emit('update:collapsed', !collapsed)"
|
|
50
|
+
>
|
|
51
|
+
<component
|
|
52
|
+
:is="collapsed ? RiSidebarUnfoldLine : RiSidebarFoldLine"
|
|
53
|
+
class="size-5"
|
|
54
|
+
/>
|
|
55
|
+
</button>
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
28
58
|
|
|
29
|
-
|
|
59
|
+
<!-- min-w keeps the content laid out at the sidebar's min width so it doesn't
|
|
30
60
|
reflow (badges wrapping, section titles on 2 lines) while the width animates
|
|
31
61
|
open from the collapsed state — the aside's overflow-hidden clips the rest. -->
|
|
32
|
-
|
|
62
|
+
<!-- Extra bottom padding so the last item can scroll clear of the browser's
|
|
33
63
|
hover URL bar (Firefox shows it bottom-left). -->
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
>
|
|
38
|
-
<label class="flex h-8 items-center gap-1 rounded border border-gray-default bg-gray-some px-2">
|
|
39
|
-
<RiSearchLine class="size-3.5 shrink-0 text-gray-medium" />
|
|
40
|
-
<span class="sr-only">{{ t('Rechercher') }}</span>
|
|
41
|
-
<input
|
|
42
|
-
:value="search"
|
|
43
|
-
type="search"
|
|
44
|
-
class="min-w-0 flex-1 bg-transparent text-[13px] outline-none placeholder:text-gray-medium"
|
|
45
|
-
:placeholder="t('Rechercher une ressource')"
|
|
46
|
-
@input="$emit('update:search', ($event.target as HTMLInputElement).value)"
|
|
47
|
-
>
|
|
48
|
-
</label>
|
|
49
|
-
|
|
50
|
-
<section
|
|
51
|
-
v-for="group in groups"
|
|
52
|
-
:key="group.type"
|
|
53
|
-
class="space-y-0.5"
|
|
64
|
+
<div
|
|
65
|
+
v-if="!collapsed"
|
|
66
|
+
class="flex min-h-0 min-w-[260px] flex-1 flex-col gap-3 overflow-y-auto p-2 pb-16"
|
|
54
67
|
>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
:
|
|
70
|
-
class="
|
|
71
|
-
@click="$emit('load-more', group.type)"
|
|
68
|
+
<label class="flex h-8 items-center gap-1 rounded border border-gray-default bg-gray-some px-2">
|
|
69
|
+
<RiSearchLine class="size-3.5 shrink-0 text-gray-medium" />
|
|
70
|
+
<span class="sr-only">{{ t('Rechercher') }}</span>
|
|
71
|
+
<input
|
|
72
|
+
:value="search"
|
|
73
|
+
type="search"
|
|
74
|
+
class="min-w-0 flex-1 bg-transparent text-[14px] outline-none placeholder:text-gray-medium"
|
|
75
|
+
:placeholder="t('Rechercher une ressource')"
|
|
76
|
+
@input="$emit('update:search', ($event.target as HTMLInputElement).value)"
|
|
77
|
+
>
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<section
|
|
81
|
+
v-for="group in groups"
|
|
82
|
+
:key="group.type"
|
|
83
|
+
class="space-y-0.5"
|
|
72
84
|
>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
<p class="px-1 py-2 text-[13px] font-medium leading-4 text-gray-medium">
|
|
86
|
+
{{ getResourceLabel(group.type, group.total) }}
|
|
87
|
+
</p>
|
|
88
|
+
<ResourceListItem
|
|
89
|
+
v-for="resource in group.items"
|
|
90
|
+
:key="resource.id"
|
|
91
|
+
:resource
|
|
92
|
+
:dataset
|
|
93
|
+
:to="resourceTo(resource)"
|
|
94
|
+
:replace
|
|
95
|
+
:selected="resource.id === selectedResourceId"
|
|
96
|
+
:expanded
|
|
97
|
+
@click="$emit('update:expanded', false)"
|
|
76
98
|
/>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
<button
|
|
100
|
+
v-if="group.items.length < group.total"
|
|
101
|
+
type="button"
|
|
102
|
+
:disabled="loadingType === group.type"
|
|
103
|
+
class="flex w-full items-center gap-1 px-1 py-1 text-left text-[14px] text-blue-default hover:underline disabled:cursor-default disabled:no-underline"
|
|
104
|
+
@click="$emit('load-more', group.type)"
|
|
105
|
+
>
|
|
106
|
+
<RiLoader5Line
|
|
107
|
+
v-if="loadingType === group.type"
|
|
108
|
+
class="size-3.5 shrink-0 animate-spin"
|
|
109
|
+
/>
|
|
110
|
+
{{ t('Charger plus…') }}
|
|
111
|
+
</button>
|
|
112
|
+
</section>
|
|
80
113
|
|
|
81
|
-
|
|
114
|
+
<!-- The search lives here, so its outcome does too: the viewer only says that
|
|
82
115
|
nothing is selected. -->
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
>
|
|
87
|
-
<p class="m-0 text-[13px] leading-snug text-gray-medium">
|
|
88
|
-
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
89
|
-
</p>
|
|
90
|
-
<BrandedButton
|
|
91
|
-
color="secondary"
|
|
92
|
-
size="xs"
|
|
93
|
-
class="w-full"
|
|
94
|
-
@click="$emit('update:search', '')"
|
|
116
|
+
<div
|
|
117
|
+
v-if="search && !groups.length"
|
|
118
|
+
class="flex flex-col gap-2 px-1 py-2"
|
|
95
119
|
>
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
<p class="m-0 text-[14px] leading-snug text-gray-medium">
|
|
121
|
+
{{ t('Pas de résultats pour « {q} »', { q: search }) }}
|
|
122
|
+
</p>
|
|
123
|
+
<BrandedButton
|
|
124
|
+
color="secondary"
|
|
125
|
+
size="xs"
|
|
126
|
+
class="w-full"
|
|
127
|
+
@click="$emit('update:search', '')"
|
|
128
|
+
>
|
|
129
|
+
{{ t('Réinitialiser la recherche') }}
|
|
130
|
+
</BrandedButton>
|
|
131
|
+
</div>
|
|
98
132
|
</div>
|
|
99
|
-
</
|
|
133
|
+
</aside>
|
|
100
134
|
|
|
101
135
|
<!-- Drag the right edge to resize the panel and read longer resource titles.
|
|
102
136
|
A separator rather than a button: it only answers to a drag, and a focusable
|
|
103
137
|
button would announce an action that no key can trigger. Keyboard users fold
|
|
104
138
|
the panel from the header instead. -->
|
|
105
139
|
<div
|
|
106
|
-
v-if="!collapsed"
|
|
140
|
+
v-if="!collapsed && !expanded"
|
|
107
141
|
role="separator"
|
|
108
142
|
aria-orientation="vertical"
|
|
109
143
|
:title="t('Glisser pour redimensionner')"
|
|
110
|
-
class="absolute right-
|
|
144
|
+
class="group absolute -right-1.5 top-0 z-20 flex h-full w-3 cursor-col-resize touch-none justify-center hover:bg-new-primary/10"
|
|
111
145
|
@mousedown="startResize"
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
>
|
|
147
|
+
<!-- Grip centered on the header so the resize affordance is visible at rest. -->
|
|
148
|
+
<span
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
class="mt-3 flex h-8 w-2 flex-col items-center justify-center gap-0.5 rounded-full border border-gray-default bg-white shadow-sm transition-colors group-hover:border-new-primary"
|
|
151
|
+
>
|
|
152
|
+
<span
|
|
153
|
+
v-for="dot in 3"
|
|
154
|
+
:key="dot"
|
|
155
|
+
class="size-0.5 rounded-full bg-gray-medium group-hover:bg-new-primary"
|
|
156
|
+
/>
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
114
160
|
</template>
|
|
115
161
|
|
|
116
162
|
<script setup lang="ts">
|
|
117
163
|
import type { RouteLocationRaw } from 'vue-router'
|
|
118
|
-
import { RiSidebarFoldLine, RiSidebarUnfoldLine, RiSearchLine, RiLoader5Line } from '@remixicon/vue'
|
|
164
|
+
import { RiArrowLeftLine, RiSidebarFoldLine, RiSidebarUnfoldLine, RiSearchLine, RiLoader5Line } from '@remixicon/vue'
|
|
119
165
|
import BrandedButton from '../BrandedButton.vue'
|
|
120
166
|
import ResourceListItem from '../ResourceListItem.vue'
|
|
121
167
|
import { getResourceLabel } from '../../functions/resources'
|
|
122
168
|
import { useTranslation } from '../../composables/useTranslation'
|
|
123
169
|
import { useResizable } from '../../composables/useResizable'
|
|
170
|
+
import type { DatasetV2 } from '../../types/datasets'
|
|
124
171
|
import type { Resource, ResourceGroup, ResourceType } from '../../types/resources'
|
|
125
172
|
|
|
126
173
|
const { t } = useTranslation()
|
|
127
174
|
|
|
128
175
|
defineProps<{
|
|
176
|
+
dataset: DatasetV2
|
|
129
177
|
groups: ResourceGroup[]
|
|
130
178
|
selectedResourceId: string | null
|
|
131
179
|
collapsed: boolean
|
|
180
|
+
expanded: boolean
|
|
132
181
|
search: string
|
|
133
182
|
loadingType: ResourceType | null
|
|
134
183
|
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
@@ -138,11 +187,12 @@ defineProps<{
|
|
|
138
187
|
defineEmits<{
|
|
139
188
|
'load-more': [type: ResourceType]
|
|
140
189
|
'update:collapsed': [value: boolean]
|
|
190
|
+
'update:expanded': [value: boolean]
|
|
141
191
|
'update:search': [value: string]
|
|
142
192
|
}>()
|
|
143
193
|
|
|
144
194
|
const { width, resizing, startResize } = useResizable({
|
|
145
|
-
initialWidth:
|
|
195
|
+
initialWidth: 340,
|
|
146
196
|
minWidth: 260,
|
|
147
197
|
maxWidth: 720,
|
|
148
198
|
})
|
|
@@ -63,8 +63,17 @@
|
|
|
63
63
|
v-if="previewKind === 'tabular'"
|
|
64
64
|
:timeout="200"
|
|
65
65
|
>
|
|
66
|
-
<TabularExplorer
|
|
66
|
+
<TabularExplorer
|
|
67
|
+
v-model:global-search="search"
|
|
68
|
+
:resource-id="resource.id"
|
|
69
|
+
>
|
|
67
70
|
<TabularToolbar class="shrink-0 border-b border-gray-default p-2" />
|
|
71
|
+
<div class="hidden shrink-0 md:block">
|
|
72
|
+
<TabularActiveFilters
|
|
73
|
+
with-clear
|
|
74
|
+
class="border-b border-gray-default p-2"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
68
77
|
<TabularTable :fill="fullscreen" />
|
|
69
78
|
<TabularMobileFilters />
|
|
70
79
|
</TabularExplorer>
|
|
@@ -161,7 +170,7 @@
|
|
|
161
170
|
</template>
|
|
162
171
|
|
|
163
172
|
<script setup lang="ts">
|
|
164
|
-
import { computed, defineAsyncComponent } from 'vue'
|
|
173
|
+
import { computed, defineAsyncComponent, ref } from 'vue'
|
|
165
174
|
import { useRoute, useRouter } from 'vue-router'
|
|
166
175
|
import PreviewUnavailable from '../ResourceAccordion/PreviewUnavailable.vue'
|
|
167
176
|
import MarkdownViewer from '../MarkdownViewer.vue'
|
|
@@ -171,6 +180,7 @@ import TabList from '../Tabs/TabList.vue'
|
|
|
171
180
|
import Tab from '../Tabs/Tab.vue'
|
|
172
181
|
import TabPanels from '../Tabs/TabPanels.vue'
|
|
173
182
|
import TabPanel from '../Tabs/TabPanel.vue'
|
|
183
|
+
import TabularActiveFilters from '../TabularExplorer/TabularActiveFilters.vue'
|
|
174
184
|
import TabularExplorer from '../TabularExplorer/TabularExplorer.vue'
|
|
175
185
|
import TabularToolbar from '../TabularExplorer/TabularToolbar.vue'
|
|
176
186
|
import TabularTable from '../TabularExplorer/TabularTable.vue'
|
|
@@ -251,6 +261,11 @@ if (hasTabularData.value) {
|
|
|
251
261
|
await provideTabularProfile(() => props.resource.id)
|
|
252
262
|
}
|
|
253
263
|
|
|
264
|
+
// Kept here as well as in the table: the viewer outlives the data tab, so the search
|
|
265
|
+
// survives a trip to another tab, and it resets with the resource since switching
|
|
266
|
+
// resource remounts the viewer.
|
|
267
|
+
const search = ref('')
|
|
268
|
+
|
|
254
269
|
// The active tab lives in the URL so a shared link opens on the same one. Read once
|
|
255
270
|
// at mount (TabGroup only takes an initial index), which is enough: switching resource
|
|
256
271
|
// remounts the viewer, and the tab is preserved when the new resource also has it.
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
>
|
|
22
22
|
<ResourceListItem
|
|
23
23
|
:resource="r"
|
|
24
|
+
:dataset
|
|
24
25
|
:to="resourceTo(r)"
|
|
25
26
|
:replace
|
|
26
27
|
:selected="r.id === selectedId"
|
|
@@ -38,9 +39,11 @@ import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
|
38
39
|
import { RiArrowDownSLine } from '@remixicon/vue'
|
|
39
40
|
import { useTranslation } from '../../composables/useTranslation'
|
|
40
41
|
import ResourceListItem from '../ResourceListItem.vue'
|
|
42
|
+
import type { Dataset, DatasetV2 } from '../../types/datasets'
|
|
41
43
|
import type { Resource } from '../../types/resources'
|
|
42
44
|
|
|
43
45
|
defineProps<{
|
|
46
|
+
dataset: Dataset | DatasetV2
|
|
44
47
|
resources: Resource[]
|
|
45
48
|
selectedId: string
|
|
46
49
|
resourceTo: (resource: Resource) => RouteLocationRaw
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<header class="flex h-14 shrink-0 items-center justify-between gap-2 border-b border-gray-default bg-gray-some px-3">
|
|
3
3
|
<div class="flex min-w-0 items-center gap-1.5 overflow-hidden text-[13px] text-gray-medium">
|
|
4
|
-
<
|
|
5
|
-
:resource
|
|
6
|
-
class="size-4 shrink-0"
|
|
7
|
-
/>
|
|
4
|
+
<ResourceIconBadge :resource />
|
|
8
5
|
<span
|
|
9
6
|
class="min-w-0 truncate font-medium text-gray-title"
|
|
10
7
|
:title="resource.title || t('Fichier sans nom')"
|
|
11
8
|
>{{ resource.title || t('Fichier sans nom') }}</span>
|
|
12
9
|
<ResourceSelector
|
|
13
10
|
v-if="resources && resources.length > 1 && resourceTo"
|
|
11
|
+
:dataset
|
|
14
12
|
:resources
|
|
15
13
|
:selected-id="resource.id"
|
|
16
14
|
:resource-to
|
|
@@ -87,7 +85,7 @@ import { computed } from 'vue'
|
|
|
87
85
|
import { RiDownloadLine, RiFullscreenLine } from '@remixicon/vue'
|
|
88
86
|
import BrandedButton from '../BrandedButton.vue'
|
|
89
87
|
import CopyButton from '../CopyButton.vue'
|
|
90
|
-
import
|
|
88
|
+
import ResourceIconBadge from '../ResourceIconBadge.vue'
|
|
91
89
|
import SchemaBadge from '../ResourceAccordion/SchemaBadge.vue'
|
|
92
90
|
import ResourceSelector from './ResourceSelector.vue'
|
|
93
91
|
import ResourceMainAction from './ResourceMainAction.vue'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
:class="[iconColor, '[&_svg]:fill-current']"
|
|
4
|
+
class="flex size-5 shrink-0 items-center justify-center rounded-[1px]"
|
|
5
|
+
>
|
|
6
|
+
<component
|
|
7
|
+
:is="iconComponent"
|
|
8
|
+
class="size-4"
|
|
9
|
+
aria-hidden="true"
|
|
10
|
+
/>
|
|
11
|
+
</span>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import File from './Icons/File.vue'
|
|
17
|
+
import { getResourceFormatIcon, getResourceIconColor } from '../functions/resources'
|
|
18
|
+
import type { Resource } from '../types/resources'
|
|
19
|
+
|
|
20
|
+
const props = defineProps<{
|
|
21
|
+
resource: Resource
|
|
22
|
+
}>()
|
|
23
|
+
|
|
24
|
+
// Render the icon directly (not via ResourceIcon which forces a gray color) so the
|
|
25
|
+
// colored badge can tint it through currentColor + [&_svg]:fill-current.
|
|
26
|
+
const iconComponent = computed(() => (props.resource.format ? getResourceFormatIcon(props.resource.format) : null) ?? File)
|
|
27
|
+
const iconColor = computed(() => getResourceIconColor(props.resource.format))
|
|
28
|
+
</script>
|
|
@@ -4,41 +4,82 @@
|
|
|
4
4
|
v-bind="$attrs"
|
|
5
5
|
:to
|
|
6
6
|
:replace
|
|
7
|
-
:class="
|
|
8
|
-
|
|
7
|
+
:class="[
|
|
8
|
+
selected ? '[&&]:!bg-gray-200' : '[&&]:hover:!bg-gray-100',
|
|
9
|
+
// Expanded, each row is its own grid: fixed tracks keep the extra columns
|
|
10
|
+
// aligned from one row to the next, where `auto` tracks would follow each
|
|
11
|
+
// row's content.
|
|
12
|
+
expanded ? 'grid-cols-[auto_minmax(0,1fr)_13rem_5rem_6rem_4rem_6.5rem]' : 'grid-cols-[auto_minmax(0,1fr)_auto_1.25rem]',
|
|
13
|
+
]"
|
|
14
|
+
class="grid h-8 w-full items-center gap-1 rounded px-1 py-1 text-left !bg-none !no-underline"
|
|
9
15
|
@pointerenter="openOnHover"
|
|
10
16
|
@pointerleave="closeTooltip"
|
|
11
|
-
@focus="show =
|
|
17
|
+
@focus="show = !expanded"
|
|
12
18
|
@blur="closeTooltip"
|
|
13
19
|
>
|
|
14
|
-
<
|
|
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>
|
|
20
|
+
<ResourceIconBadge :resource />
|
|
24
21
|
<div class="flex min-w-0 items-baseline gap-0.5 whitespace-nowrap leading-4">
|
|
25
22
|
<span
|
|
26
|
-
class="truncate text-[
|
|
23
|
+
class="truncate text-[14px]"
|
|
27
24
|
:class="selected ? 'font-extrabold text-gray-title' : 'font-medium text-gray-medium'"
|
|
28
25
|
>{{ resource.title || t('Fichier sans nom') }}</span>
|
|
29
|
-
<template v-if="humanFilesize">
|
|
30
|
-
<span class="shrink-0 text-[
|
|
31
|
-
<span class="shrink-0 text-[
|
|
26
|
+
<template v-if="humanFilesize && !expanded">
|
|
27
|
+
<span class="shrink-0 text-[14px] text-gray-medium">·</span>
|
|
28
|
+
<span class="shrink-0 text-[13px] text-gray-medium">{{ humanFilesize }}</span>
|
|
32
29
|
</template>
|
|
33
30
|
</div>
|
|
31
|
+
<template v-if="expanded">
|
|
32
|
+
<span class="truncate text-[13px] text-gray-medium">
|
|
33
|
+
<TranslationT keypath="Mis à jour le {date}">
|
|
34
|
+
<template #date>
|
|
35
|
+
<FormattedDate :date="resource.last_modified" />
|
|
36
|
+
</template>
|
|
37
|
+
</TranslationT>
|
|
38
|
+
</span>
|
|
39
|
+
<span class="text-right text-[13px] tabular-nums text-gray-medium">{{ humanFilesize }}</span>
|
|
40
|
+
</template>
|
|
34
41
|
<!-- Capped and truncated: an `auto` grid track floors at its content width, so an
|
|
35
42
|
unusually long format (`www:link-1.0-http--samples`) would otherwise squeeze
|
|
36
43
|
the title track to nothing and overflow the fixed-height row. -->
|
|
37
44
|
<span
|
|
38
45
|
v-if="resource.format"
|
|
39
|
-
class="max-w-24 truncate rounded bg-gray-lower px-1.5 py-0.5 text-[
|
|
46
|
+
class="max-w-24 justify-self-start truncate rounded bg-gray-lower px-1.5 py-0.5 text-[13px] uppercase leading-4 text-gray-medium"
|
|
40
47
|
:title="resource.format"
|
|
41
48
|
>{{ resource.format }}</span>
|
|
49
|
+
<!-- Holds the format track so the next columns stay in place. -->
|
|
50
|
+
<span v-else />
|
|
51
|
+
<span
|
|
52
|
+
v-if="expanded"
|
|
53
|
+
class="inline-flex items-center justify-end gap-1 text-[13px] tabular-nums text-gray-medium"
|
|
54
|
+
>
|
|
55
|
+
<RiDownloadLine
|
|
56
|
+
class="size-3"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
/>
|
|
59
|
+
{{ summarize(resource.metrics.views) }}
|
|
60
|
+
</span>
|
|
61
|
+
<!-- Compact, the eye is labelled in the hover card; expanded, the row has room
|
|
62
|
+
for the label and no hover card. -->
|
|
63
|
+
<span
|
|
64
|
+
v-if="expanded && hasPreview"
|
|
65
|
+
class="flex w-fit items-center gap-0.5 justify-self-end rounded bg-gray-lower px-1.5 py-0.5 text-[13px] leading-4 text-gray-medium"
|
|
66
|
+
>
|
|
67
|
+
<RiEyeLine
|
|
68
|
+
class="size-3.5"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
/>
|
|
71
|
+
{{ t('Explorable') }}
|
|
72
|
+
</span>
|
|
73
|
+
<span
|
|
74
|
+
v-else
|
|
75
|
+
class="flex items-center justify-center"
|
|
76
|
+
>
|
|
77
|
+
<RiEyeLine
|
|
78
|
+
v-if="hasPreview"
|
|
79
|
+
class="size-4 text-gray-medium"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
/>
|
|
82
|
+
</span>
|
|
42
83
|
</AppLink>
|
|
43
84
|
|
|
44
85
|
<!-- Hover card: the row truncates the title, so surface the full name plus the
|
|
@@ -84,6 +125,16 @@
|
|
|
84
125
|
{{ summarize(resource.metrics.views) }}
|
|
85
126
|
</span>
|
|
86
127
|
</div>
|
|
128
|
+
<span
|
|
129
|
+
v-if="hasPreview"
|
|
130
|
+
class="mt-1.5 flex w-fit items-center gap-0.5 rounded bg-gray-lower px-1.5 py-0.5 text-[12px] leading-4 text-gray-medium"
|
|
131
|
+
>
|
|
132
|
+
<RiEyeLine
|
|
133
|
+
class="size-3"
|
|
134
|
+
aria-hidden="true"
|
|
135
|
+
/>
|
|
136
|
+
{{ t('Explorable') }}
|
|
137
|
+
</span>
|
|
87
138
|
</div>
|
|
88
139
|
</Teleport>
|
|
89
140
|
</ClientOnly>
|
|
@@ -94,15 +145,17 @@ import { computed, ref, useTemplateRef } from 'vue'
|
|
|
94
145
|
import { autoUpdate, flip, offset, shift, useFloating } from '@floating-ui/vue'
|
|
95
146
|
import { useEventListener } from '@vueuse/core'
|
|
96
147
|
import type { RouteLocationRaw } from 'vue-router'
|
|
97
|
-
import { RiDownloadLine } from '@remixicon/vue'
|
|
148
|
+
import { RiDownloadLine, RiEyeLine } from '@remixicon/vue'
|
|
98
149
|
import AppLink from './AppLink.vue'
|
|
99
150
|
import ClientOnly from './ClientOnly.vue'
|
|
100
|
-
import File from './Icons/File.vue'
|
|
101
151
|
import FormattedDate from './FormattedDate.vue'
|
|
152
|
+
import ResourceIconBadge from './ResourceIconBadge.vue'
|
|
102
153
|
import TranslationT from './TranslationT.vue'
|
|
103
|
-
import {
|
|
154
|
+
import { getResourceFilesize } from '../functions/resources'
|
|
104
155
|
import { filesize, summarize } from '../functions/helpers'
|
|
156
|
+
import { useResourceCapabilities } from '../composables/useResourceCapabilities'
|
|
105
157
|
import { useTranslation } from '../composables/useTranslation'
|
|
158
|
+
import type { Dataset, DatasetV2 } from '../types/datasets'
|
|
106
159
|
import type { Resource } from '../types/resources'
|
|
107
160
|
|
|
108
161
|
// The hover card below is a second root node, so Vue drops fallthrough attributes
|
|
@@ -115,20 +168,22 @@ defineOptions({ inheritAttrs: false })
|
|
|
115
168
|
// truth for the selection.
|
|
116
169
|
const props = withDefaults(defineProps<{
|
|
117
170
|
resource: Resource
|
|
171
|
+
// Some previews (Data Fair, OpenAPI) depend on the dataset's organization.
|
|
172
|
+
dataset: Dataset | DatasetV2
|
|
118
173
|
to: RouteLocationRaw
|
|
119
174
|
selected?: boolean
|
|
120
175
|
replace?: boolean
|
|
176
|
+
// Full-width list: the row also shows the update date, size and downloads.
|
|
177
|
+
expanded?: boolean
|
|
121
178
|
}>(), {
|
|
122
179
|
selected: false,
|
|
123
180
|
replace: false,
|
|
181
|
+
expanded: false,
|
|
124
182
|
})
|
|
125
183
|
|
|
126
184
|
const { t } = useTranslation()
|
|
127
185
|
|
|
128
|
-
|
|
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))
|
|
186
|
+
const { hasPreview } = useResourceCapabilities(() => props.resource, () => props.dataset)
|
|
132
187
|
|
|
133
188
|
const humanFilesize = computed(() => {
|
|
134
189
|
const size = getResourceFilesize(props.resource)
|
|
@@ -151,9 +206,10 @@ const { floatingStyles } = useFloating(rowEl, card, {
|
|
|
151
206
|
|
|
152
207
|
// A tap fires a pointer enter too, so the card would flash on every touch selection
|
|
153
208
|
// in the mobile resource picker. Only a real pointer opens it — keyboard focus still
|
|
154
|
-
// does, through @focus.
|
|
209
|
+
// does, through @focus. The expanded row already shows everything the card holds,
|
|
210
|
+
// so it never opens there.
|
|
155
211
|
function openOnHover(event: PointerEvent) {
|
|
156
|
-
if (event.pointerType === 'mouse') show.value = true
|
|
212
|
+
if (event.pointerType === 'mouse' && !props.expanded) show.value = true
|
|
157
213
|
}
|
|
158
214
|
|
|
159
215
|
function closeTooltip() {
|