@datagouv/components-next 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/{Control-ZFh5ta_U.js → Control-B9WQxlpJ.js} +1 -1
- package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-D_6yzJUG.js} +9 -9
- package/dist/{Event-DSQcW7OF.js → Event-iXDrp-fy.js} +11 -11
- package/dist/{Image-BijNEG0p.js → Image-mNMBOofs.js} +5 -5
- package/dist/ImagePreview.client-B0iPhFm3.js +36 -0
- package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DSl1kiwC.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-BfqKMw0S.js} +1306 -1166
- package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-BSfpk_rT.js} +15 -15
- package/dist/{OSM-D4MTdBtk.js → OSM-CcrUhItr.js} +15 -23
- package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BANFeM6K.js} +441 -434
- package/dist/Pmtiles.client-DH9ZrsZR.js +26333 -0
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-I8EZCgzp.js +60 -0
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-VGQ7nkMS.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-CSpn0Ca8.js} +153 -156
- package/dist/{TileImage-BJeHipMX.js → TileImage-CpbaEFwd.js} +143 -131
- package/dist/{View-xp_P_OHw.js → View-BYBpt-AU.js} +167 -117
- package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-KCB5mwVf.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-BoeTUnDZ.js} +26 -26
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +201 -170
- package/dist/components.css +2 -2
- package/dist/{index-NofRBuyf.js → index-DHWzX5vK.js} +5999 -5472
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-pVQfonBI.js} +162 -164
- package/dist/{main-Iz1ZCL6k.js → main-BDcYkhOc.js} +36671 -29402
- package/dist/{proj-CsNo9yH1.js → proj-DxGbobp7.js} +312 -296
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-DvX7WWiA.js} +264 -255
- package/dist/{vue3-json-viewer-BXwup7nO.js → vue3-json-viewer-CJLQFzXZ.js} +10 -16
- package/dist/{vue3-xml-viewer.common-tVI9uXUz.js → vue3-xml-viewer.common-Bdcn54ST.js} +1 -1
- package/package.json +40 -39
- package/src/components/ActivityList/ActivityList.vue +21 -12
- package/src/components/AppLink.vue +3 -0
- package/src/components/BannerAction.vue +6 -2
- package/src/components/Chart/ChartViewer.vue +111 -17
- package/src/components/Chart/ChartViewerWrapper.vue +28 -16
- package/src/components/ChartCard.vue +112 -0
- package/src/components/CopyButton.vue +6 -2
- package/src/components/DataserviceCard.vue +23 -6
- package/src/components/DataserviceQuality.vue +82 -0
- package/src/components/DataserviceQualityInline.vue +31 -0
- package/src/components/DataserviceQualityTooltipContent.vue +79 -0
- package/src/components/DatasetCard.vue +17 -6
- package/src/components/DatasetInformation/DatasetTemporalitySection.vue +7 -4
- package/src/components/DatasetQuality.vue +11 -11
- package/src/components/DatasetQualityInline.vue +2 -2
- package/src/components/DatasetQualityTooltipContent.vue +1 -1
- package/src/components/DiscussionMessageCard.vue +14 -15
- package/src/components/Form/SchemaSelect.vue +2 -0
- package/src/components/FormattedDate.vue +69 -0
- package/src/components/LoadingBlock.vue +4 -1
- package/src/components/ObjectCard.vue +4 -1
- package/src/components/ObjectCardOwner.vue +13 -1
- package/src/components/Pagination.vue +18 -58
- package/src/components/Placeholder.vue +3 -2
- package/src/components/PostCard.vue +13 -15
- package/src/components/ResourceAccordion/Datafair.client.vue +1 -1
- package/src/components/ResourceAccordion/Downloads.vue +19 -7
- package/src/components/ResourceAccordion/ImagePreview.client.vue +48 -0
- package/src/components/ResourceAccordion/MapContainer.client.vue +1 -8
- package/src/components/ResourceAccordion/Metadata.vue +21 -5
- package/src/components/ResourceAccordion/PdfPreview.client.vue +36 -12
- package/src/components/ResourceAccordion/PdfSkeleton.vue +44 -0
- package/src/components/ResourceAccordion/Pmtiles.client.vue +30 -19
- package/src/components/ResourceAccordion/Preview.vue +8 -4
- package/src/components/ResourceAccordion/PreviewWrapper.vue +17 -10
- package/src/components/ResourceAccordion/ResourceAccordion.vue +31 -11
- package/src/components/ResourceExplorer/ResourceExplorer.vue +105 -210
- package/src/components/ResourceExplorer/ResourceExplorerHeader.vue +79 -0
- package/src/components/ResourceExplorer/ResourceExplorerSidebar.vue +104 -71
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +140 -184
- package/src/components/ResourceExplorer/ResourceMainAction.vue +240 -0
- package/src/components/ResourceExplorer/ResourceSelector.vue +24 -86
- package/src/components/ResourceExplorer/ResourceViewerHeader.vue +124 -0
- package/src/components/ResourceExplorer/ResourceViewerSkeleton.vue +96 -0
- package/src/components/ResourceListItem.vue +168 -0
- package/src/components/ReuseCard.vue +18 -6
- package/src/components/ReuseDetails.vue +2 -2
- package/src/components/ReuseHorizontalCard.vue +4 -3
- package/src/components/SchemaCard.vue +10 -1
- package/src/components/Search/Filter/AccessTypeFilter.vue +2 -2
- package/src/components/Search/GlobalSearch.vue +28 -10
- package/src/components/Search/SearchInput.vue +1 -1
- package/src/components/Search/Sidemenu.vue +4 -3
- package/src/components/SmallChart.vue +11 -10
- package/src/components/StatBox.vue +13 -5
- package/src/components/TabularExplorer/TabularActiveFilters.vue +79 -0
- package/src/components/TabularExplorer/TabularCell.vue +7 -8
- package/src/components/TabularExplorer/TabularCellPopover.vue +21 -5
- package/src/components/TabularExplorer/TabularColumnsMenu.vue +112 -0
- package/src/components/TabularExplorer/TabularDateField.vue +41 -0
- package/src/components/TabularExplorer/TabularDateFilter.vue +403 -0
- package/src/components/TabularExplorer/TabularExplorer.vue +127 -780
- package/src/components/TabularExplorer/TabularFilterContent.vue +48 -38
- package/src/components/TabularExplorer/TabularFilterPopover.vue +27 -32
- package/src/components/TabularExplorer/TabularMobileFilterButton.vue +21 -0
- package/src/components/TabularExplorer/TabularMobileFilters.vue +150 -0
- package/src/components/TabularExplorer/TabularRowsInfo.vue +31 -0
- package/src/components/TabularExplorer/TabularSkeleton.vue +92 -0
- package/src/components/TabularExplorer/TabularTable.vue +484 -0
- package/src/components/TabularExplorer/TabularToolbar.vue +25 -0
- package/src/components/TabularExplorer/types.ts +40 -28
- package/src/components/TabularExplorer/useColumnMetadata.ts +98 -0
- package/src/components/TabularExplorer/useTabularContext.ts +82 -0
- package/src/components/TopicCard.vue +15 -14
- package/src/composables/useActiveDescendant.ts +5 -0
- package/src/composables/useDatasetResources.ts +164 -0
- package/src/composables/useHasTabularData.ts +2 -1
- package/src/composables/useResizable.ts +46 -0
- package/src/composables/useResourceCapabilities.ts +48 -40
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/composables/useTranslation.ts +24 -9
- package/src/config.ts +1 -0
- package/src/functions/api.ts +13 -7
- package/src/functions/charts.ts +32 -1
- package/src/functions/dates.ts +26 -7
- package/src/functions/helpers.ts +15 -0
- package/src/functions/metrics.ts +57 -56
- package/src/functions/owned.ts +21 -0
- package/src/functions/paginate.ts +46 -0
- package/src/functions/resourceCapabilities.ts +34 -0
- package/src/functions/resources.ts +88 -0
- package/src/functions/schemas.ts +27 -0
- package/src/functions/scroll.ts +20 -0
- package/src/functions/tabular.ts +222 -5
- package/src/functions/tabularApi.ts +1 -43
- package/src/main.ts +32 -6
- package/src/types/dataservices.ts +15 -0
- package/src/types/organizations.ts +10 -0
- package/src/types/resources.ts +0 -1
- package/src/types/reuses.ts +12 -1
- package/src/types/search.ts +2 -2
- package/src/types/visualizations.ts +5 -0
- package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
- package/dist/PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js +0 -61
- /package/src/components/{DatasetQualityItem.vue → QualityItem.vue} +0 -0
- /package/src/components/{DatasetQualityItemWarning.vue → QualityItemWarning.vue} +0 -0
- /package/src/components/{DatasetQualityScore.vue → QualityScore.vue} +0 -0
|
@@ -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="
|
|
29
|
+
class="px-3 py-2 border-b border-black/10 space-y-1.5"
|
|
30
30
|
>
|
|
31
|
-
<
|
|
32
|
-
<span class="
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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"
|
|
@@ -59,8 +61,18 @@
|
|
|
59
61
|
</div>
|
|
60
62
|
</div>
|
|
61
63
|
|
|
64
|
+
<!-- Date filter -->
|
|
65
|
+
<TabularDateFilter
|
|
66
|
+
v-if="columnType === 'date'"
|
|
67
|
+
v-model:filters="filters"
|
|
68
|
+
:column="column"
|
|
69
|
+
/>
|
|
70
|
+
|
|
62
71
|
<!-- Search (contains) -->
|
|
63
|
-
<div
|
|
72
|
+
<div
|
|
73
|
+
v-if="columnType !== 'boolean' && columnType !== 'date'"
|
|
74
|
+
class="px-3 py-2 border-b border-black/10"
|
|
75
|
+
>
|
|
64
76
|
<div class="relative">
|
|
65
77
|
<RiSearchLine
|
|
66
78
|
class="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-gray-medium"
|
|
@@ -68,7 +80,7 @@
|
|
|
68
80
|
/>
|
|
69
81
|
<input
|
|
70
82
|
v-model="search"
|
|
71
|
-
type="text"
|
|
83
|
+
:type="isNumeric ? 'number' : 'text'"
|
|
72
84
|
class="w-full h-8 text-sm border border-transparent rounded-lg py-1 pl-8 pr-3 bg-[#f3f3f5] focus:outline-none focus:border-new-primary"
|
|
73
85
|
:placeholder="t('Rechercher...')"
|
|
74
86
|
>
|
|
@@ -98,14 +110,7 @@
|
|
|
98
110
|
/>
|
|
99
111
|
</span>
|
|
100
112
|
<span class="flex-1 truncate text-left text-xs">
|
|
101
|
-
|
|
102
|
-
v-if="categoryBadgeStyles?.[top.value]"
|
|
103
|
-
class="inline-block rounded font-medium px-2 py-0.5 text-xs"
|
|
104
|
-
:style="{ backgroundColor: categoryBadgeStyles![top.value]!.backgroundColor, color: categoryBadgeStyles![top.value]!.color }"
|
|
105
|
-
>{{ top.value }}</span>
|
|
106
|
-
<template v-else>
|
|
107
|
-
{{ top.value ?? 'null' }}
|
|
108
|
-
</template>
|
|
113
|
+
{{ top.value ?? 'null' }}
|
|
109
114
|
</span>
|
|
110
115
|
<span class="font-mono text-xs text-gray-low tabular-nums shrink-0">{{ top.count }}</span>
|
|
111
116
|
</button>
|
|
@@ -152,14 +157,14 @@
|
|
|
152
157
|
|
|
153
158
|
<!-- Number range -->
|
|
154
159
|
<form
|
|
155
|
-
v-if="columnType === 'number' && columnProfile"
|
|
160
|
+
v-if="(columnType === 'number' || columnType === 'year') && columnProfile"
|
|
156
161
|
class="px-3 py-2 border-b border-black/10 space-y-2"
|
|
157
162
|
@submit.prevent="applyRange"
|
|
158
163
|
>
|
|
159
164
|
<div class="flex items-center gap-2 text-xs text-gray-plain">
|
|
160
|
-
<span class="tabular-nums">{{ formatNumber(profileMin) }}</span>
|
|
165
|
+
<span class="tabular-nums">{{ columnType === 'year' ? profileMin : formatNumber(profileMin) }}</span>
|
|
161
166
|
<span class="text-gray-medium">—</span>
|
|
162
|
-
<span class="tabular-nums">{{ formatNumber(profileMax) }}</span>
|
|
167
|
+
<span class="tabular-nums">{{ columnType === 'year' ? profileMax : formatNumber(profileMax) }}</span>
|
|
163
168
|
</div>
|
|
164
169
|
<div class="flex items-center gap-2">
|
|
165
170
|
<input
|
|
@@ -182,14 +187,7 @@
|
|
|
182
187
|
:max="profileMax"
|
|
183
188
|
>
|
|
184
189
|
</div>
|
|
185
|
-
<div class="flex items-center gap-2">
|
|
186
|
-
<BrandedButton
|
|
187
|
-
color="primary"
|
|
188
|
-
size="2xs"
|
|
189
|
-
type="submit"
|
|
190
|
-
>
|
|
191
|
-
{{ t('Appliquer') }}
|
|
192
|
-
</BrandedButton>
|
|
190
|
+
<div class="flex items-center justify-end gap-2">
|
|
193
191
|
<BrandedButton
|
|
194
192
|
color="tertiary"
|
|
195
193
|
size="2xs"
|
|
@@ -199,6 +197,13 @@
|
|
|
199
197
|
>
|
|
200
198
|
{{ t('Effacer') }}
|
|
201
199
|
</BrandedButton>
|
|
200
|
+
<BrandedButton
|
|
201
|
+
color="primary"
|
|
202
|
+
size="2xs"
|
|
203
|
+
type="submit"
|
|
204
|
+
>
|
|
205
|
+
{{ t('Appliquer') }}
|
|
206
|
+
</BrandedButton>
|
|
202
207
|
</div>
|
|
203
208
|
</form>
|
|
204
209
|
</div>
|
|
@@ -217,7 +222,8 @@ import { useTranslation } from '../../composables/useTranslation'
|
|
|
217
222
|
import { useFormatTabular } from '../../functions/tabular'
|
|
218
223
|
import BrandedButton from '../BrandedButton.vue'
|
|
219
224
|
import ProgressBar from '../ProgressBar.vue'
|
|
220
|
-
import
|
|
225
|
+
import TabularDateFilter from './TabularDateFilter.vue'
|
|
226
|
+
import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig, SortDirection } from './types'
|
|
221
227
|
|
|
222
228
|
const props = defineProps<{
|
|
223
229
|
column: string
|
|
@@ -225,7 +231,6 @@ const props = defineProps<{
|
|
|
225
231
|
columnProfile: TabularColumnProfile | null
|
|
226
232
|
nullPercent: string
|
|
227
233
|
totalLines: number
|
|
228
|
-
categoryBadgeStyles?: Record<string, BadgeStyle>
|
|
229
234
|
booleanCounts?: { trueCount: number, falseCount: number }
|
|
230
235
|
}>()
|
|
231
236
|
|
|
@@ -237,13 +242,18 @@ const { formatNumber } = useFormatTabular()
|
|
|
237
242
|
|
|
238
243
|
const search = ref('')
|
|
239
244
|
|
|
245
|
+
// Numbers and years are matched exactly (the API has no `contains` for them),
|
|
246
|
+
// so the field offers a number input instead of a text search.
|
|
247
|
+
const isNumeric = computed(() => props.columnType === 'number' || props.columnType === 'year')
|
|
248
|
+
|
|
240
249
|
watchDebounced(search, (q) => {
|
|
241
250
|
const existing = filters.value[props.column] ?? {}
|
|
251
|
+
const operator = isNumeric.value ? 'exact' : 'contains'
|
|
242
252
|
if (q) {
|
|
243
|
-
filters.value = { ...filters.value, [props.column]: { ...existing,
|
|
253
|
+
filters.value = { ...filters.value, [props.column]: { ...existing, [operator]: q } }
|
|
244
254
|
}
|
|
245
255
|
else {
|
|
246
|
-
const {
|
|
256
|
+
const { [operator]: _, ...rest } = existing
|
|
247
257
|
filters.value = { ...filters.value, [props.column]: rest }
|
|
248
258
|
}
|
|
249
259
|
}, { debounce: 300 })
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<Popover
|
|
3
3
|
ref="anchor"
|
|
4
|
+
v-slot="{ open }"
|
|
4
5
|
class="relative shrink-0"
|
|
5
6
|
>
|
|
6
|
-
<
|
|
7
|
-
class="
|
|
8
|
-
:class="
|
|
9
|
-
@click.stop="togglePopover"
|
|
7
|
+
<PopoverButton
|
|
8
|
+
class="flex size-5 items-center justify-center rounded transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
|
|
9
|
+
:class="open ? 'bg-new-primary/10' : 'hover:bg-gray-100'"
|
|
10
10
|
>
|
|
11
|
-
<
|
|
12
|
-
class="size-
|
|
11
|
+
<RiFilterLine
|
|
12
|
+
class="size-4"
|
|
13
|
+
:class="open || hasColumnFilter ? 'text-new-primary' : 'text-gray-low'"
|
|
13
14
|
aria-hidden="true"
|
|
14
15
|
/>
|
|
15
16
|
<span class="sr-only">{{ t('Filtrer') }} {{ column }}</span>
|
|
16
|
-
</
|
|
17
|
+
</PopoverButton>
|
|
17
18
|
|
|
18
19
|
<ClientOnly>
|
|
19
20
|
<Teleport to="#tooltips">
|
|
20
|
-
<
|
|
21
|
-
v-show="
|
|
21
|
+
<PopoverPanel
|
|
22
|
+
v-show="open"
|
|
22
23
|
ref="panel"
|
|
23
|
-
|
|
24
|
+
static
|
|
25
|
+
class="bg-white border border-black/10 rounded-lg shadow-md w-80 absolute z-[800]"
|
|
24
26
|
:style="floatingStyles"
|
|
27
|
+
:data-testid="`column-filter-${column}`"
|
|
25
28
|
>
|
|
26
29
|
<!-- Title -->
|
|
27
30
|
<div class="flex items-center justify-between px-3 py-2 border-b border-black/10">
|
|
@@ -47,26 +50,25 @@
|
|
|
47
50
|
:column-profile="columnProfile"
|
|
48
51
|
:null-percent="nullPercent"
|
|
49
52
|
:total-lines="totalLines"
|
|
50
|
-
:category-badge-styles="categoryBadgeStyles"
|
|
51
53
|
:boolean-counts="booleanCounts"
|
|
52
54
|
/>
|
|
53
|
-
</
|
|
55
|
+
</PopoverPanel>
|
|
54
56
|
</Teleport>
|
|
55
57
|
</ClientOnly>
|
|
56
|
-
</
|
|
58
|
+
</Popover>
|
|
57
59
|
</template>
|
|
58
60
|
|
|
59
61
|
<script setup lang="ts">
|
|
60
|
-
import { computed,
|
|
62
|
+
import { computed, useTemplateRef } from 'vue'
|
|
61
63
|
import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
64
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
65
|
+
import { RiFilterLine, RiCloseLine } from '@remixicon/vue'
|
|
64
66
|
import { useTranslation } from '../../composables/useTranslation'
|
|
65
67
|
import { hasFilterForColumn as _hasFilterForColumn } from '../../functions/tabular'
|
|
66
68
|
import BrandedButton from '../BrandedButton.vue'
|
|
67
69
|
import ClientOnly from '../ClientOnly.vue'
|
|
68
70
|
import TabularFilterContent from './TabularFilterContent.vue'
|
|
69
|
-
import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig
|
|
71
|
+
import type { TabularColumnProfile, ColumnType, ColumnFilters, SortConfig } from './types'
|
|
70
72
|
|
|
71
73
|
const props = defineProps<{
|
|
72
74
|
column: string
|
|
@@ -74,7 +76,6 @@ const props = defineProps<{
|
|
|
74
76
|
columnProfile: TabularColumnProfile | null
|
|
75
77
|
nullPercent: string
|
|
76
78
|
totalLines: number
|
|
77
|
-
categoryBadgeStyles?: Record<string, BadgeStyle>
|
|
78
79
|
booleanCounts?: { trueCount: number, falseCount: number }
|
|
79
80
|
}>()
|
|
80
81
|
|
|
@@ -83,18 +84,12 @@ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default:
|
|
|
83
84
|
|
|
84
85
|
const { t } = useTranslation()
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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(
|
|
101
|
+
const { floatingStyles } = useFloating(anchorEl, panelEl, {
|
|
107
102
|
placement: 'bottom-start',
|
|
108
103
|
middleware: [flip(), shift()],
|
|
109
104
|
whileElementsMounted: autoUpdate,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<BrandedButton
|
|
3
|
+
color="tertiary"
|
|
4
|
+
size="2xs"
|
|
5
|
+
:icon="RiFilterLine"
|
|
6
|
+
keep-margins-even-without-borders
|
|
7
|
+
@click="mobileFilterOpen = true"
|
|
8
|
+
>
|
|
9
|
+
{{ t('Filtres & tri') }}
|
|
10
|
+
</BrandedButton>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import { RiFilterLine } from '@remixicon/vue'
|
|
15
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
16
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
17
|
+
import { useTabularContext } from './useTabularContext'
|
|
18
|
+
|
|
19
|
+
const { t } = useTranslation()
|
|
20
|
+
const { mobileFilterOpen } = useTabularContext()
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<TransitionRoot
|
|
3
|
+
:show="mobileFilterOpen"
|
|
4
|
+
as="template"
|
|
5
|
+
>
|
|
6
|
+
<Dialog
|
|
7
|
+
class="relative z-[900]"
|
|
8
|
+
@close="mobileFilterOpen = false"
|
|
9
|
+
>
|
|
10
|
+
<!-- Backdrop -->
|
|
11
|
+
<TransitionChild
|
|
12
|
+
as="template"
|
|
13
|
+
enter="ease-out duration-300"
|
|
14
|
+
enter-from="opacity-0"
|
|
15
|
+
enter-to="opacity-100"
|
|
16
|
+
leave="ease-in duration-200"
|
|
17
|
+
leave-from="opacity-100"
|
|
18
|
+
leave-to="opacity-0"
|
|
19
|
+
>
|
|
20
|
+
<div class="fixed inset-0 bg-black/30" />
|
|
21
|
+
</TransitionChild>
|
|
22
|
+
<!-- Panel -->
|
|
23
|
+
<TransitionChild
|
|
24
|
+
as="template"
|
|
25
|
+
enter="ease-out duration-300"
|
|
26
|
+
enter-from="translate-y-full"
|
|
27
|
+
enter-to="translate-y-0"
|
|
28
|
+
leave="ease-in duration-200"
|
|
29
|
+
leave-from="translate-y-0"
|
|
30
|
+
leave-to="translate-y-full"
|
|
31
|
+
>
|
|
32
|
+
<div class="fixed inset-x-0 bottom-0 max-h-[80vh] flex flex-col bg-white rounded-t-2xl shadow-xl">
|
|
33
|
+
<DialogPanel class="flex flex-col max-h-[80vh]">
|
|
34
|
+
<div class="px-4 pt-4 pb-2 border-b border-gray-default">
|
|
35
|
+
<DialogTitle class="text-sm font-bold">
|
|
36
|
+
{{ t('Filtres & tri par colonne') }}
|
|
37
|
+
</DialogTitle>
|
|
38
|
+
<p class="text-xs text-gray-low">
|
|
39
|
+
{{ allColumns.length }} {{ t('colonnes') }} · {{ activeFilters.length }} {{ t('filtre') }}{{ activeFilters.length !== 1 ? 's' : '' }}
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
<!-- Column list -->
|
|
43
|
+
<div class="flex-1 overflow-y-auto">
|
|
44
|
+
<div
|
|
45
|
+
v-for="col in allColumns"
|
|
46
|
+
:key="col"
|
|
47
|
+
class="border-b border-gray-default last:border-b-0"
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
class="flex items-center gap-2 w-full px-3 py-2.5 text-left"
|
|
51
|
+
:class="hasFilterForColumn(col) ? 'bg-new-primary/5' : ''"
|
|
52
|
+
@click="toggleColumn(col)"
|
|
53
|
+
>
|
|
54
|
+
<component
|
|
55
|
+
:is="getColumnDisplay(col).icon"
|
|
56
|
+
class="size-3.5 text-gray-low shrink-0"
|
|
57
|
+
aria-hidden="true"
|
|
58
|
+
/>
|
|
59
|
+
<span class="flex-1 text-sm text-gray-title truncate">{{ col }}</span>
|
|
60
|
+
<RiArrowUpLine
|
|
61
|
+
v-if="sort?.column === col && sort.direction === 'asc'"
|
|
62
|
+
class="size-3 text-new-primary shrink-0"
|
|
63
|
+
aria-hidden="true"
|
|
64
|
+
/>
|
|
65
|
+
<RiArrowDownLine
|
|
66
|
+
v-if="sort?.column === col && sort.direction === 'desc'"
|
|
67
|
+
class="size-3 text-new-primary shrink-0"
|
|
68
|
+
aria-hidden="true"
|
|
69
|
+
/>
|
|
70
|
+
<span
|
|
71
|
+
v-if="hasFilterForColumn(col)"
|
|
72
|
+
class="size-2 rounded-full bg-new-primary shrink-0"
|
|
73
|
+
/>
|
|
74
|
+
<RiArrowDownSLine
|
|
75
|
+
class="size-3.5 text-gray-low shrink-0 transition-transform"
|
|
76
|
+
:class="{ 'rotate-180': expandedCol === col }"
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
/>
|
|
79
|
+
</button>
|
|
80
|
+
<div
|
|
81
|
+
v-if="expandedCol === col"
|
|
82
|
+
class="pb-1"
|
|
83
|
+
>
|
|
84
|
+
<TabularFilterContent
|
|
85
|
+
v-model:sort="sort"
|
|
86
|
+
v-model:filters="filters"
|
|
87
|
+
:column="col"
|
|
88
|
+
:column-type="getColumnType(col)"
|
|
89
|
+
:column-profile="getColumnProfile(col)"
|
|
90
|
+
:null-percent="getNullPercent(col)"
|
|
91
|
+
:total-lines="totalLines"
|
|
92
|
+
:boolean-counts="getColumnType(col) === 'boolean' ? getBooleanCounts(col) : undefined"
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<!-- Reset all -->
|
|
98
|
+
<div
|
|
99
|
+
v-if="activeFilters.length > 0 || sort"
|
|
100
|
+
class="border-t border-gray-default px-4 py-3"
|
|
101
|
+
>
|
|
102
|
+
<BrandedButton
|
|
103
|
+
color="secondary"
|
|
104
|
+
size="xs"
|
|
105
|
+
class="w-full"
|
|
106
|
+
@click="clearAllFilters(); sort = null; mobileFilterOpen = false"
|
|
107
|
+
>
|
|
108
|
+
{{ t('Tout réinitialiser') }}
|
|
109
|
+
</BrandedButton>
|
|
110
|
+
</div>
|
|
111
|
+
</DialogPanel>
|
|
112
|
+
</div>
|
|
113
|
+
</TransitionChild>
|
|
114
|
+
</Dialog>
|
|
115
|
+
</TransitionRoot>
|
|
116
|
+
</template>
|
|
117
|
+
|
|
118
|
+
<script setup lang="ts">
|
|
119
|
+
import { ref } from 'vue'
|
|
120
|
+
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
|
|
121
|
+
import { RiArrowDownLine, RiArrowDownSLine, RiArrowUpLine } from '@remixicon/vue'
|
|
122
|
+
import BrandedButton from '../BrandedButton.vue'
|
|
123
|
+
import TabularFilterContent from './TabularFilterContent.vue'
|
|
124
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
125
|
+
import { useTabularContext } from './useTabularContext'
|
|
126
|
+
|
|
127
|
+
const { t } = useTranslation()
|
|
128
|
+
const {
|
|
129
|
+
mobileFilterOpen,
|
|
130
|
+
allColumns,
|
|
131
|
+
activeFilters,
|
|
132
|
+
sort,
|
|
133
|
+
filters,
|
|
134
|
+
hasFilterForColumn,
|
|
135
|
+
clearAllFilters,
|
|
136
|
+
totalLines,
|
|
137
|
+
getColumnDisplay,
|
|
138
|
+
getColumnType,
|
|
139
|
+
getColumnProfile,
|
|
140
|
+
getNullPercent,
|
|
141
|
+
getBooleanCounts,
|
|
142
|
+
} = useTabularContext()
|
|
143
|
+
|
|
144
|
+
// The expanded column is local to the sheet.
|
|
145
|
+
const expandedCol = ref<string | null>(null)
|
|
146
|
+
|
|
147
|
+
function toggleColumn(col: string) {
|
|
148
|
+
expandedCol.value = expandedCol.value === col ? null : col
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span class="flex items-center gap-1.5 text-xs text-gray-plain">
|
|
3
|
+
<RiLoader5Line
|
|
4
|
+
v-if="isRefreshing"
|
|
5
|
+
class="size-3 text-new-primary animate-spin"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
/>
|
|
8
|
+
<RiLayoutRowLine
|
|
9
|
+
v-else
|
|
10
|
+
class="size-3 text-mention-grey"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
/>
|
|
13
|
+
<span class="font-bold hidden md:inline">{{ t('Lignes') }}</span>
|
|
14
|
+
<!-- The count stays in place while refreshing: the spinner already says it is
|
|
15
|
+
stale, and swapping it for a word of another width would shift the whole
|
|
16
|
+
toolbar on every search. -->
|
|
17
|
+
<span
|
|
18
|
+
data-testid="row-count"
|
|
19
|
+
class="font-mono tabular-nums"
|
|
20
|
+
>{{ (tableData?.meta.total ?? 0).toLocaleString() }}/{{ totalLines.toLocaleString() }}</span>
|
|
21
|
+
</span>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup lang="ts">
|
|
25
|
+
import { RiLayoutRowLine, RiLoader5Line } from '@remixicon/vue'
|
|
26
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
27
|
+
import { useTabularContext } from './useTabularContext'
|
|
28
|
+
|
|
29
|
+
const { t } = useTranslation()
|
|
30
|
+
const { tableData, totalLines, isRefreshing } = useTabularContext()
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<!-- Toolbar row — mirrors TabularToolbar: the mobile filter button on the left (it
|
|
3
|
+
only shows below md), and the columns menu (h-6 button, which drives the row
|
|
4
|
+
height) plus the rows info on the right. The active-filters area stays empty
|
|
5
|
+
until the user filters, so nothing stands in for it. -->
|
|
6
|
+
<div class="animate-pulse-placeholder flex shrink-0 items-center gap-2 border-b border-gray-default p-2">
|
|
7
|
+
<div class="h-6 w-24 rounded bg-gray-200 md:hidden" />
|
|
8
|
+
<div class="flex flex-1 items-center justify-end gap-4">
|
|
9
|
+
<div class="h-6 w-24 rounded bg-gray-200" />
|
|
10
|
+
<div class="h-5 w-24 rounded bg-gray-200" />
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<!-- Table — mirrors TabularTable: tall header cells (name + type line) then rows.
|
|
15
|
+
Desktop only, like the real one. -->
|
|
16
|
+
<div
|
|
17
|
+
class="animate-pulse-placeholder hidden overflow-hidden md:block"
|
|
18
|
+
:class="fill ? 'min-h-0 flex-1' : 'max-h-[70vh]'"
|
|
19
|
+
role="status"
|
|
20
|
+
:aria-label="t('Chargement de l\'aperçu…')"
|
|
21
|
+
>
|
|
22
|
+
<table class="w-full table-fixed text-sm border-collapse">
|
|
23
|
+
<thead class="bg-gray-some shadow-[inset_0_-1px_0_0_#E5E5E5]">
|
|
24
|
+
<tr class="border-b border-gray-default">
|
|
25
|
+
<th
|
|
26
|
+
v-for="i in 6"
|
|
27
|
+
:key="i"
|
|
28
|
+
class="h-12 px-3 text-left align-middle border-r border-gray-default last:border-r-0"
|
|
29
|
+
>
|
|
30
|
+
<div class="flex items-center justify-between gap-1">
|
|
31
|
+
<div class="flex items-center gap-1">
|
|
32
|
+
<div class="size-4 shrink-0 rounded bg-gray-200" />
|
|
33
|
+
<div class="h-3.5 w-20 rounded bg-gray-200" />
|
|
34
|
+
</div>
|
|
35
|
+
<div class="size-4 shrink-0 rounded bg-gray-200" />
|
|
36
|
+
</div>
|
|
37
|
+
</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
<tr
|
|
42
|
+
v-for="row in 16"
|
|
43
|
+
:key="row"
|
|
44
|
+
class="h-10 border-b border-gray-default even:bg-gray-lowest-2"
|
|
45
|
+
>
|
|
46
|
+
<td
|
|
47
|
+
v-for="col in 6"
|
|
48
|
+
:key="col"
|
|
49
|
+
class="px-2 align-middle border-r border-gray-default last:border-r-0"
|
|
50
|
+
>
|
|
51
|
+
<div class="h-3.5 w-3/4 rounded bg-gray-200" />
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
</tbody>
|
|
55
|
+
</table>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Mobile — the real table becomes a list of cards below md, each showing four
|
|
59
|
+
fields until it is expanded. -->
|
|
60
|
+
<div
|
|
61
|
+
class="animate-pulse-placeholder space-y-2 px-1 md:hidden"
|
|
62
|
+
role="status"
|
|
63
|
+
:aria-label="t('Chargement de l\'aperçu…')"
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
v-for="card in 4"
|
|
67
|
+
:key="card"
|
|
68
|
+
class="space-y-2 rounded-lg border border-gray-default p-3"
|
|
69
|
+
:class="card % 2 === 0 ? 'bg-gray-lowest-2' : 'bg-white'"
|
|
70
|
+
>
|
|
71
|
+
<div
|
|
72
|
+
v-for="field in 4"
|
|
73
|
+
:key="field"
|
|
74
|
+
class="flex flex-col gap-0.5"
|
|
75
|
+
>
|
|
76
|
+
<div class="h-3 w-24 rounded bg-gray-200" />
|
|
77
|
+
<div class="h-4 w-2/3 rounded bg-gray-200" />
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<script setup lang="ts">
|
|
84
|
+
import { useTranslation } from '../../composables/useTranslation'
|
|
85
|
+
|
|
86
|
+
defineProps<{
|
|
87
|
+
// Fill the available height (fullscreen) instead of the default capped height.
|
|
88
|
+
fill?: boolean
|
|
89
|
+
}>()
|
|
90
|
+
|
|
91
|
+
const { t } = useTranslation()
|
|
92
|
+
</script>
|