@datagouv/components-next 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/{Control-ZFh5ta_U.js → Control-b-43UT2B.js} +10 -10
- package/dist/{Datafair.client-rf4T1IkA.js → Datafair.client-BrjISlaZ.js} +4 -4
- package/dist/{Event-DSQcW7OF.js → Event-py_AAwJY.js} +18 -18
- package/dist/{Image-BijNEG0p.js → Image-BHQYZWzH.js} +23 -23
- package/dist/{JsonPreview.client-dzar6iuh.js → JsonPreview.client-DivFAnbN.js} +5 -5
- package/dist/{Map-BUtPf5GN.js → Map-CtP7leHl.js} +1313 -1173
- package/dist/{MapContainer.client-D-MoRNhG.js → MapContainer.client-Cq8tNLZZ.js} +17 -17
- package/dist/{OSM-D4MTdBtk.js → OSM-gdDSIsDA.js} +15 -23
- package/dist/{PdfPreview.client-DoDYLmJD.js → PdfPreview.client-BNBgLsK2.js} +6 -10
- package/dist/Pmtiles.client-CQi5Rw8W.js +26123 -0
- package/dist/{PreviewWrapper.vue_vue_type_script_setup_true_lang-BRNYswg3.js → PreviewWrapper.vue_vue_type_script_setup_true_lang-C3dDzrIr.js} +2 -2
- package/dist/{ScaleLine-hJQIqcZm.js → ScaleLine-hG-FhNqy.js} +2 -2
- package/dist/{Tile-Dcl7oIVu.js → Tile-D6rFonpT.js} +163 -166
- package/dist/{TileImage-BJeHipMX.js → TileImage-cfrEgz5y.js} +131 -119
- package/dist/{View-xp_P_OHw.js → View-vp7vDb9q.js} +169 -119
- package/dist/{XmlPreview.client-cOhwff6P.js → XmlPreview.client-XCsNvnsZ.js} +5 -5
- package/dist/{common-BjQlan3k.js → common-C8efyyuc.js} +49 -49
- package/dist/components-next.css +1 -52
- package/dist/components-next.js +157 -153
- package/dist/components.css +2 -2
- package/dist/{index-NofRBuyf.js → index-CXUamc8Z.js} +6382 -6057
- package/dist/{leaflet-src-7m1mB8LI.js → leaflet-src-BRKv9Ksv.js} +162 -164
- package/dist/{main-Iz1ZCL6k.js → main-DKu5SOAl.js} +20671 -21654
- package/dist/{proj-CsNo9yH1.js → proj-CsjVeojN.js} +322 -306
- package/dist/{text-clamp.esm-B5kW_XMt.js → text-clamp.esm-DVseBqZZ.js} +4 -4
- package/dist/{tilecoord-A0fLnBZr.js → tilecoord-CPcQJnrF.js} +270 -261
- 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-BIaEIFYG.js} +1 -1
- package/package.json +34 -34
- package/src/components/ResourceAccordion/Pmtiles.client.vue +3 -1
- package/src/components/ResourceAccordion/ResourceAccordion.vue +2 -2
- package/src/components/ResourceExplorer/ResourceExplorer.vue +8 -9
- package/src/components/ResourceExplorer/ResourceExplorerViewer.vue +4 -1
- package/src/components/Search/GlobalSearch.vue +16 -2
- package/src/components/TabularExplorer/TabularCellPopover.vue +2 -2
- package/src/components/TabularExplorer/TabularExplorer.vue +212 -74
- package/src/components/TabularExplorer/TabularFilterContent.vue +15 -13
- package/src/components/TabularExplorer/TabularFilterPopover.vue +21 -26
- package/src/components/TabularExplorer/types.ts +9 -9
- package/src/composables/useResourceCapabilities.ts +1 -1
- package/src/composables/useSearchFilter.ts +7 -5
- package/src/functions/api.ts +13 -7
- package/src/functions/resources.ts +5 -0
- package/src/functions/tabular.ts +103 -1
- package/src/types/resources.ts +0 -1
- package/dist/Pmtiles.client-Dzm01Zfm.js +0 -25185
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
:aria-label="t('Chargement de l\'aperçu…')"
|
|
20
20
|
role="status"
|
|
21
21
|
>
|
|
22
|
-
<div
|
|
22
|
+
<div>
|
|
23
23
|
<div class="flex items-center justify-end gap-4 py-3">
|
|
24
24
|
<div class="h-4 w-20 bg-gray-200" />
|
|
25
25
|
<div class="h-4 w-20 bg-gray-200" />
|
|
@@ -58,15 +58,19 @@
|
|
|
58
58
|
</div>
|
|
59
59
|
|
|
60
60
|
<template v-else-if="tableData && profileData">
|
|
61
|
-
<!-- Toolbar
|
|
62
|
-
|
|
61
|
+
<!-- Toolbar & active filters inherit the host's 1rem horizontal padding
|
|
62
|
+
(px-4 on ResourceExplorerViewer's TabPanel, .container on
|
|
63
|
+
ExploreResourceView), so they line up with the resource title /
|
|
64
|
+
download button above. Only the table itself goes edge-to-edge
|
|
65
|
+
(via -mx-4 below). -->
|
|
66
|
+
<div>
|
|
63
67
|
<div class="flex items-center py-3 gap-2">
|
|
64
68
|
<!-- Mobile: filter & sort button -->
|
|
65
69
|
<BrandedButton
|
|
66
70
|
class="md:hidden"
|
|
67
71
|
color="tertiary"
|
|
68
72
|
size="2xs"
|
|
69
|
-
:icon="
|
|
73
|
+
:icon="RiFilterLine"
|
|
70
74
|
keep-margins-even-without-borders
|
|
71
75
|
@click="mobileFilterOpen = true"
|
|
72
76
|
>
|
|
@@ -105,33 +109,51 @@
|
|
|
105
109
|
class="bg-white border border-gray-default rounded shadow-lg w-72 absolute z-[800]"
|
|
106
110
|
:style="columnFloatingStyles"
|
|
107
111
|
>
|
|
108
|
-
<div class="
|
|
109
|
-
<span class="text-xs font-medium text-gray-title">
|
|
112
|
+
<div class="px-3 py-2 border-b border-gray-default space-y-1.5">
|
|
113
|
+
<span class="block text-xs font-medium text-gray-title">
|
|
110
114
|
{{ visibleColumns.size }} {{ t('sur') }} {{ allColumns.length }} {{ t('colonnes visibles') }}
|
|
111
115
|
</span>
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
<div class="flex items-center gap-1">
|
|
117
|
+
<BrandedButton
|
|
118
|
+
v-if="hiddenCount"
|
|
119
|
+
color="tertiary"
|
|
120
|
+
size="2xs"
|
|
121
|
+
@click="showAllColumns"
|
|
122
|
+
>
|
|
123
|
+
{{ t('Tout afficher') }}
|
|
124
|
+
</BrandedButton>
|
|
125
|
+
<BrandedButton
|
|
126
|
+
v-if="visibleColumns.size > 0"
|
|
127
|
+
color="tertiary"
|
|
128
|
+
size="2xs"
|
|
129
|
+
@click="hideAllColumns"
|
|
130
|
+
>
|
|
131
|
+
{{ t('Tout masquer') }}
|
|
132
|
+
</BrandedButton>
|
|
133
|
+
</div>
|
|
120
134
|
</div>
|
|
121
135
|
<div class="max-h-64 overflow-auto p-1">
|
|
122
|
-
<
|
|
136
|
+
<div
|
|
123
137
|
v-for="col in allColumns"
|
|
124
138
|
:key="col"
|
|
125
|
-
class="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-50
|
|
139
|
+
class="group/col flex items-center gap-2 px-2 py-1.5 rounded hover:bg-gray-50 text-xs"
|
|
126
140
|
>
|
|
127
|
-
<
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
<label class="flex items-center gap-2 flex-1 min-w-0 cursor-pointer">
|
|
142
|
+
<input
|
|
143
|
+
type="checkbox"
|
|
144
|
+
:checked="visibleColumns.has(col)"
|
|
145
|
+
class="size-3.5 accent-new-primary"
|
|
146
|
+
@change="toggleColumn(col)"
|
|
147
|
+
>
|
|
148
|
+
<span class="truncate">{{ col }}</span>
|
|
149
|
+
</label>
|
|
150
|
+
<button
|
|
151
|
+
class="shrink-0 px-1 text-new-primary hover:underline hidden group-hover/col:block group-focus-within/col:block"
|
|
152
|
+
@click="selectOnlyColumn(col)"
|
|
132
153
|
>
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
{{ t('Uniquement') }}
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
135
157
|
</div>
|
|
136
158
|
</PopoverPanel>
|
|
137
159
|
</Teleport>
|
|
@@ -150,20 +172,23 @@
|
|
|
150
172
|
</div>
|
|
151
173
|
</div>
|
|
152
174
|
|
|
153
|
-
<!-- Active filters -->
|
|
175
|
+
<!-- Active filters & sort -->
|
|
154
176
|
<div
|
|
155
|
-
v-if="activeFilters.length > 0"
|
|
177
|
+
v-if="activeFilters.length > 0 || sort"
|
|
156
178
|
class="bg-gray-some border border-gray-default rounded-xl px-3 py-2.5 mb-3 space-y-2.5"
|
|
157
179
|
>
|
|
158
180
|
<!-- Header -->
|
|
159
181
|
<div class="flex items-center justify-between">
|
|
160
182
|
<div class="flex items-center gap-2">
|
|
161
|
-
<
|
|
183
|
+
<RiFilterLine
|
|
162
184
|
class="size-3.5"
|
|
163
185
|
aria-hidden="true"
|
|
164
186
|
/>
|
|
165
|
-
<span class="text-xs text-gray-plain">{{ t('Filtres actifs') }}</span>
|
|
166
|
-
<span
|
|
187
|
+
<span class="text-xs text-gray-plain">{{ activeFilters.length > 0 ? t('Filtres actifs') : t('Tri actif') }}</span>
|
|
188
|
+
<span
|
|
189
|
+
v-if="activeFilters.length > 0"
|
|
190
|
+
class="inline-flex items-center justify-center rounded-full bg-new-primary/10 text-new-primary text-xs tabular-nums min-w-5 h-5 px-1.5"
|
|
191
|
+
>
|
|
167
192
|
{{ activeFilters.length }}
|
|
168
193
|
</span>
|
|
169
194
|
</div>
|
|
@@ -171,20 +196,45 @@
|
|
|
171
196
|
color="tertiary"
|
|
172
197
|
size="2xs"
|
|
173
198
|
:icon="RiCloseLine"
|
|
174
|
-
@click="clearAllFilters"
|
|
199
|
+
@click="clearAllFilters(); sort = null"
|
|
175
200
|
>
|
|
176
201
|
{{ t('Tout effacer') }}
|
|
177
202
|
</BrandedButton>
|
|
178
203
|
</div>
|
|
179
204
|
<!-- Chips -->
|
|
180
205
|
<div class="flex flex-wrap gap-1.5">
|
|
206
|
+
<!-- Sort chip -->
|
|
207
|
+
<span
|
|
208
|
+
v-if="sort"
|
|
209
|
+
class="inline-flex items-center gap-1.5 bg-white border border-gray-silver rounded-lg pl-2 pr-1 py-1 text-xs"
|
|
210
|
+
>
|
|
211
|
+
<component
|
|
212
|
+
:is="sort.direction === 'asc' ? RiArrowUpLine : RiArrowDownLine"
|
|
213
|
+
class="size-3 text-new-primary"
|
|
214
|
+
aria-hidden="true"
|
|
215
|
+
/>
|
|
216
|
+
<span class="text-gray-plain">{{ t('Tri') }}</span>
|
|
217
|
+
<span class="text-gray-title">{{ sort.column }}</span>
|
|
218
|
+
<span class="text-gray-plain">{{ sort.direction === 'asc' ? t('croissant') : t('décroissant') }}</span>
|
|
219
|
+
<button
|
|
220
|
+
class="rounded p-0.5 text-gray-low hover:text-gray-plain hover:bg-gray-100"
|
|
221
|
+
@click="sort = null"
|
|
222
|
+
>
|
|
223
|
+
<RiCloseLine
|
|
224
|
+
class="size-3"
|
|
225
|
+
aria-hidden="true"
|
|
226
|
+
/>
|
|
227
|
+
<span class="sr-only">{{ t('Supprimer le tri') }}</span>
|
|
228
|
+
</button>
|
|
229
|
+
</span>
|
|
230
|
+
<!-- Filter chips -->
|
|
181
231
|
<span
|
|
182
232
|
v-for="af in activeFilters"
|
|
183
233
|
:key="af.column"
|
|
184
234
|
class="inline-flex items-center gap-1.5 bg-white border border-gray-silver rounded-lg pl-2 pr-1 py-1 text-xs"
|
|
185
235
|
>
|
|
186
236
|
<component
|
|
187
|
-
:is="
|
|
237
|
+
:is="getColumnDisplay(af.column).icon"
|
|
188
238
|
class="size-3 text-gray-title"
|
|
189
239
|
aria-hidden="true"
|
|
190
240
|
/>
|
|
@@ -204,15 +254,46 @@
|
|
|
204
254
|
</div>
|
|
205
255
|
</div>
|
|
206
256
|
</div>
|
|
207
|
-
<!-- /
|
|
257
|
+
<!-- /toolbar & active filters -->
|
|
258
|
+
|
|
259
|
+
<!-- No column selected -->
|
|
260
|
+
<div
|
|
261
|
+
v-if="displayedColumns.length === 0"
|
|
262
|
+
class="flex flex-col items-center py-12"
|
|
263
|
+
>
|
|
264
|
+
<img
|
|
265
|
+
:src="noColumnsSrc"
|
|
266
|
+
class="h-32"
|
|
267
|
+
alt=""
|
|
268
|
+
>
|
|
269
|
+
<p class="font-bold text-gray-title my-3">
|
|
270
|
+
{{ t('Aucune colonne sélectionnée') }}
|
|
271
|
+
</p>
|
|
272
|
+
<BrandedButton
|
|
273
|
+
color="secondary"
|
|
274
|
+
size="xs"
|
|
275
|
+
@click="showAllColumns"
|
|
276
|
+
>
|
|
277
|
+
{{ t('Afficher toutes les colonnes') }}
|
|
278
|
+
</BrandedButton>
|
|
279
|
+
</div>
|
|
208
280
|
|
|
209
281
|
<!-- Desktop: scrollable table -->
|
|
210
|
-
<!--
|
|
211
|
-
|
|
212
|
-
|
|
282
|
+
<!-- The table goes edge-to-edge while the toolbar / active-filter rows
|
|
283
|
+
above keep the host's horizontal padding:
|
|
284
|
+
- default: `-mx-4` cancels the host's 1rem padding (px-4 on
|
|
285
|
+
ResourceExplorerViewer's TabPanel), so the table reaches the
|
|
286
|
+
host box edge.
|
|
287
|
+
- `fullBleed`: the table breaks out of the (max-width) container to
|
|
288
|
+
span almost the whole viewport (explore page), keeping a 2rem
|
|
289
|
+
gutter on each side so it doesn't touch the screen edges. Centered
|
|
290
|
+
on the viewport via the classic `left-1/2` + `-translate-x-1/2`
|
|
291
|
+
trick (works because the container is itself centered). -->
|
|
213
292
|
<div
|
|
293
|
+
v-if="displayedColumns.length > 0"
|
|
214
294
|
ref="scrollContainer"
|
|
215
|
-
class="hidden md:block overflow-auto max-h-[70vh]
|
|
295
|
+
class="hidden md:block overflow-auto max-h-[70vh]"
|
|
296
|
+
:class="fullBleed ? 'relative left-1/2 w-[calc(100vw_-_4rem)] -translate-x-1/2' : '-mx-4'"
|
|
216
297
|
>
|
|
217
298
|
<table class="text-sm border-collapse">
|
|
218
299
|
<thead class="sticky top-0 bg-white z-10 shadow-[inset_0_-1px_0_0_#E5E5E5]">
|
|
@@ -251,14 +332,14 @@
|
|
|
251
332
|
:boolean-counts="getColumnType(col) === 'boolean' ? getBooleanCounts(col) : undefined"
|
|
252
333
|
/>
|
|
253
334
|
</div>
|
|
254
|
-
<!-- Column type -->
|
|
255
|
-
<span class="font-mono text-xs text-gray-plain -mt-0.5 inline-flex items-center gap-1">
|
|
335
|
+
<!-- Column type (semantic format when available) -->
|
|
336
|
+
<span class="font-mono text-xs text-gray-plain -mt-0.5 inline-flex items-center gap-1 max-w-full">
|
|
256
337
|
<component
|
|
257
|
-
:is="
|
|
258
|
-
class="size-3"
|
|
338
|
+
:is="getColumnDisplay(col).icon"
|
|
339
|
+
class="size-3 shrink-0"
|
|
259
340
|
aria-hidden="true"
|
|
260
341
|
/>
|
|
261
|
-
<span class="mt-px">{{
|
|
342
|
+
<span class="mt-px truncate">{{ getColumnDisplay(col).label }}</span>
|
|
262
343
|
</span>
|
|
263
344
|
<!-- Resize handle: wide hit zone, thin visible bar -->
|
|
264
345
|
<div
|
|
@@ -326,7 +407,10 @@
|
|
|
326
407
|
/>
|
|
327
408
|
|
|
328
409
|
<!-- Mobile: card layout -->
|
|
329
|
-
<div
|
|
410
|
+
<div
|
|
411
|
+
v-if="displayedColumns.length > 0"
|
|
412
|
+
class="md:hidden space-y-2 px-1"
|
|
413
|
+
>
|
|
330
414
|
<div
|
|
331
415
|
v-if="allRows.length === 0"
|
|
332
416
|
class="py-16 text-center"
|
|
@@ -352,7 +436,7 @@
|
|
|
352
436
|
>
|
|
353
437
|
<div class="flex items-center gap-1 min-w-0">
|
|
354
438
|
<component
|
|
355
|
-
:is="
|
|
439
|
+
:is="getColumnDisplay(col).icon"
|
|
356
440
|
class="size-3 text-gray-low shrink-0"
|
|
357
441
|
aria-hidden="true"
|
|
358
442
|
/>
|
|
@@ -444,7 +528,7 @@
|
|
|
444
528
|
@click="toggleMobileFilterColumn(col)"
|
|
445
529
|
>
|
|
446
530
|
<component
|
|
447
|
-
:is="
|
|
531
|
+
:is="getColumnDisplay(col).icon"
|
|
448
532
|
class="size-3.5 text-gray-low shrink-0"
|
|
449
533
|
aria-hidden="true"
|
|
450
534
|
/>
|
|
@@ -521,7 +605,7 @@ import {
|
|
|
521
605
|
RiArrowDownSLine,
|
|
522
606
|
RiArrowUpLine,
|
|
523
607
|
RiArrowDownLine,
|
|
524
|
-
|
|
608
|
+
RiFilterLine,
|
|
525
609
|
RiCloseLine,
|
|
526
610
|
RiSearchLine,
|
|
527
611
|
} from '@remixicon/vue'
|
|
@@ -529,7 +613,7 @@ import { useFetch } from '../../functions/api'
|
|
|
529
613
|
import { useComponentsConfig } from '../../config'
|
|
530
614
|
import { useTranslation } from '../../composables/useTranslation'
|
|
531
615
|
import { injectTabularProfile } from '../../composables/useTabularProfile'
|
|
532
|
-
import { buildTypeConfig, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy } from '../../functions/tabular'
|
|
616
|
+
import { buildTypeConfig, buildFormatConfig, humanizeFormat, GENERIC_FORMATS, hasFilterForColumn as _hasFilterForColumn, isTruthy, isFalsy } from '../../functions/tabular'
|
|
533
617
|
import ClientOnly from '../ClientOnly.vue'
|
|
534
618
|
import SimpleBanner from '../SimpleBanner.vue'
|
|
535
619
|
import BrandedButton from '../BrandedButton.vue'
|
|
@@ -540,9 +624,14 @@ import type { CellInfo } from './TabularCellPopover.vue'
|
|
|
540
624
|
import TabularFilterContent from './TabularFilterContent.vue'
|
|
541
625
|
import TabularFilterPopover from './TabularFilterPopover.vue'
|
|
542
626
|
import type { TabularDataResponse, TabularRow, ColumnType, SortConfig, ColumnFilters, BadgeStyle } from './types'
|
|
627
|
+
import noColumnsSrc from '../../../assets/illustrations/_table.svg?url'
|
|
543
628
|
|
|
544
629
|
const props = defineProps<{
|
|
545
630
|
resourceId: string
|
|
631
|
+
// When true, the table breaks out to the full viewport width instead of just
|
|
632
|
+
// the host's padding. Used on the standalone explore page where the table
|
|
633
|
+
// should span the whole screen while the toolbar stays inside the container.
|
|
634
|
+
fullBleed?: boolean
|
|
546
635
|
}>()
|
|
547
636
|
|
|
548
637
|
const { t } = useTranslation()
|
|
@@ -674,7 +763,7 @@ const hiddenCount = computed(() =>
|
|
|
674
763
|
function toggleColumn(col: string) {
|
|
675
764
|
const next = new Set(visibleColumns.value)
|
|
676
765
|
if (next.has(col)) {
|
|
677
|
-
|
|
766
|
+
next.delete(col)
|
|
678
767
|
}
|
|
679
768
|
else {
|
|
680
769
|
next.add(col)
|
|
@@ -686,65 +775,95 @@ function showAllColumns() {
|
|
|
686
775
|
visibleColumns.value = new Set(allColumns.value)
|
|
687
776
|
}
|
|
688
777
|
|
|
778
|
+
function hideAllColumns() {
|
|
779
|
+
visibleColumns.value = new Set()
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function selectOnlyColumn(col: string) {
|
|
783
|
+
visibleColumns.value = new Set([col])
|
|
784
|
+
}
|
|
785
|
+
|
|
689
786
|
// Column widths
|
|
690
787
|
//
|
|
691
788
|
// Strategy:
|
|
692
|
-
// -
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
// -
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
-
//
|
|
699
|
-
//
|
|
789
|
+
// - `naturalWidths` holds each column's content-driven width, measured ONCE
|
|
790
|
+
// from the DOM while the <th> are still auto-sized. It's the stable base and
|
|
791
|
+
// never re-measured (re-measuring would require unlocking the widths first).
|
|
792
|
+
// - `columnWidths` is the applied result: `naturalWidths` distributed to fill
|
|
793
|
+
// the container. It's recomputed from the base whenever the set of displayed
|
|
794
|
+
// columns changes (toggling, isolating or hiding columns via the menu), so
|
|
795
|
+
// reducing to a few columns re-stretches them instead of leaving blank space.
|
|
796
|
+
// Pagination / filtering / sorting don't change the column set, so they keep
|
|
797
|
+
// the locked widths and never trigger a layout shift.
|
|
798
|
+
// - Manual resize writes directly to `columnWidths` during the drag, then stores
|
|
799
|
+
// the final width back into `naturalWidths` so it survives later redistribution.
|
|
700
800
|
const columnWidths = ref<Record<string, number>>({})
|
|
801
|
+
const naturalWidths = ref<Record<string, number>>({})
|
|
701
802
|
const resizing = ref<{ column: string, startX: number, startWidth: number } | null>(null)
|
|
702
803
|
|
|
703
|
-
|
|
804
|
+
// Measure content-driven widths once, while the <th> are still auto-sized.
|
|
805
|
+
function measureNaturalWidths() {
|
|
704
806
|
const container = scrollContainerRef.value
|
|
705
807
|
if (!container) return
|
|
706
808
|
const ths = container.querySelectorAll('th')
|
|
707
809
|
if (!ths || ths.length === 0) return
|
|
708
810
|
|
|
709
|
-
const
|
|
710
|
-
let naturalSum = 0
|
|
811
|
+
const widths: Record<string, number> = {}
|
|
711
812
|
ths.forEach((th, i) => {
|
|
712
813
|
const col = displayedColumns.value[i]
|
|
713
|
-
if (
|
|
714
|
-
const w = th.offsetWidth
|
|
715
|
-
naturalWidths[col] = w
|
|
716
|
-
naturalSum += w
|
|
814
|
+
if (col) widths[col] = th.offsetWidth
|
|
717
815
|
})
|
|
718
|
-
|
|
816
|
+
naturalWidths.value = widths
|
|
817
|
+
}
|
|
719
818
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
819
|
+
// Distribute the natural widths of the currently displayed columns to fill the
|
|
820
|
+
// container (inflating proportionally when they don't naturally cover it).
|
|
821
|
+
function redistributeColumnWidths() {
|
|
822
|
+
const container = scrollContainerRef.value
|
|
823
|
+
if (!container) return
|
|
824
|
+
const cols = displayedColumns.value
|
|
825
|
+
let naturalSum = 0
|
|
826
|
+
for (const col of cols) naturalSum += naturalWidths.value[col] ?? 0
|
|
827
|
+
if (naturalSum === 0) return
|
|
723
828
|
|
|
829
|
+
const ratio = naturalSum < container.clientWidth ? container.clientWidth / naturalSum : 1
|
|
724
830
|
const widths: Record<string, number> = {}
|
|
725
|
-
for (const [col
|
|
726
|
-
widths[col] = Math.floor(w * ratio)
|
|
727
|
-
}
|
|
831
|
+
for (const col of cols) widths[col] = Math.floor((naturalWidths.value[col] ?? 0) * ratio)
|
|
728
832
|
columnWidths.value = widths
|
|
729
833
|
}
|
|
730
834
|
|
|
731
|
-
//
|
|
732
|
-
//
|
|
733
|
-
// (pagination, filters) keep the locked widths and avoid layout shifts.
|
|
835
|
+
// Measure once both data and profile are ready (so the <thead> is rendered with
|
|
836
|
+
// content-driven natural widths), then distribute.
|
|
734
837
|
watch(
|
|
735
838
|
() => [tableData.value !== null, displayedColumns.value.length] as const,
|
|
736
839
|
async ([dataReady, colCount]) => {
|
|
737
840
|
if (!dataReady || colCount === 0) return
|
|
738
|
-
if (Object.keys(
|
|
841
|
+
if (Object.keys(naturalWidths.value).length > 0) return
|
|
739
842
|
await nextTick()
|
|
740
|
-
|
|
843
|
+
measureNaturalWidths()
|
|
844
|
+
redistributeColumnWidths()
|
|
741
845
|
},
|
|
742
846
|
{ immediate: true },
|
|
743
847
|
)
|
|
744
848
|
|
|
849
|
+
// Re-stretch the remaining columns whenever the displayed set changes (columns
|
|
850
|
+
// toggled / hidden / isolated from the menu). No DOM measurement needed — we
|
|
851
|
+
// redistribute the already-measured natural widths over the new container space.
|
|
852
|
+
watch(displayedColumns, async () => {
|
|
853
|
+
if (Object.keys(naturalWidths.value).length === 0) return
|
|
854
|
+
// Wait for the table to (re)mount before reading the container: the desktop
|
|
855
|
+
// table is `v-if`-ed out when no column is displayed, so going from 0 → N
|
|
856
|
+
// columns means `scrollContainer` only exists after the next render.
|
|
857
|
+
await nextTick()
|
|
858
|
+
redistributeColumnWidths()
|
|
859
|
+
})
|
|
860
|
+
|
|
745
861
|
function startResize(col: string, e: MouseEvent) {
|
|
746
862
|
// Fallback in case the user grabs the handle before the initial measure ran.
|
|
747
|
-
if (!Object.keys(
|
|
863
|
+
if (!Object.keys(naturalWidths.value).length) {
|
|
864
|
+
measureNaturalWidths()
|
|
865
|
+
redistributeColumnWidths()
|
|
866
|
+
}
|
|
748
867
|
resizing.value = { column: col, startX: e.clientX, startWidth: columnWidths.value[col] ?? 100 }
|
|
749
868
|
// Disable smooth scroll during resize
|
|
750
869
|
if (scrollContainerRef.value) scrollContainerRef.value.style.scrollBehavior = 'auto'
|
|
@@ -772,6 +891,13 @@ function onResize(e: MouseEvent) {
|
|
|
772
891
|
|
|
773
892
|
function stopResize() {
|
|
774
893
|
if (scrollContainerRef.value) scrollContainerRef.value.style.scrollBehavior = ''
|
|
894
|
+
// Persist the manual width as the column's new base so it survives later
|
|
895
|
+
// redistribution when other columns are toggled.
|
|
896
|
+
if (resizing.value) {
|
|
897
|
+
const col = resizing.value.column
|
|
898
|
+
const width = columnWidths.value[col]
|
|
899
|
+
if (width != null) naturalWidths.value = { ...naturalWidths.value, [col]: width }
|
|
900
|
+
}
|
|
775
901
|
resizing.value = null
|
|
776
902
|
document.removeEventListener('mousemove', onResize)
|
|
777
903
|
document.removeEventListener('mouseup', stopResize)
|
|
@@ -914,11 +1040,23 @@ function getNullPercent(col: string) {
|
|
|
914
1040
|
}
|
|
915
1041
|
|
|
916
1042
|
const typeConfig = buildTypeConfig(t)
|
|
1043
|
+
const formatConfig = buildFormatConfig(t)
|
|
917
1044
|
|
|
918
1045
|
function getTypeConfig(col: string) {
|
|
919
1046
|
return typeConfig[getColumnType(col)]
|
|
920
1047
|
}
|
|
921
1048
|
|
|
1049
|
+
// Header display: prefer the rich semantic format detected by csv-detective
|
|
1050
|
+
// (code INSEE, UAI, département, SIREN, email…) over the generic display type.
|
|
1051
|
+
// Tolerant: generic formats fall back to the type config, and unknown semantic
|
|
1052
|
+
// formats are humanized rather than dropped.
|
|
1053
|
+
function getColumnDisplay(col: string) {
|
|
1054
|
+
const format = profileData.value?.profile?.columns?.[col]?.format
|
|
1055
|
+
const fallback = getTypeConfig(col)
|
|
1056
|
+
if (!format || GENERIC_FORMATS.has(format)) return fallback
|
|
1057
|
+
return formatConfig[format] ?? { icon: fallback.icon, label: humanizeFormat(format) }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
922
1060
|
const BADGE_PALETTE = [
|
|
923
1061
|
{ bg: '#E6EEFE', text: '#3558A2' }, // blue-cumulus
|
|
924
1062
|
{ bg: '#E8E3DB', text: '#6A6156' }, // beige-gris-galet
|
|
@@ -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"
|
|
@@ -1,25 +1,26 @@
|
|
|
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="p-0.5 rounded focus:outline-none"
|
|
7
|
+
<PopoverButton
|
|
8
|
+
class="p-0.5 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-new-primary"
|
|
8
9
|
:class="hasColumnFilter ? 'bg-primary text-white' : 'hover:bg-gray-100'"
|
|
9
|
-
@click.stop="togglePopover"
|
|
10
10
|
>
|
|
11
|
-
<
|
|
11
|
+
<RiFilterLine
|
|
12
12
|
class="size-3.5"
|
|
13
13
|
aria-hidden="true"
|
|
14
14
|
/>
|
|
15
15
|
<span class="sr-only">{{ t('Filtrer') }} {{ column }}</span>
|
|
16
|
-
</
|
|
16
|
+
</PopoverButton>
|
|
17
17
|
|
|
18
18
|
<ClientOnly>
|
|
19
19
|
<Teleport to="#tooltips">
|
|
20
|
-
<
|
|
21
|
-
v-show="
|
|
20
|
+
<PopoverPanel
|
|
21
|
+
v-show="open"
|
|
22
22
|
ref="panel"
|
|
23
|
+
static
|
|
23
24
|
class="bg-white border border-black/10 rounded-lg shadow-md w-64 absolute z-[800]"
|
|
24
25
|
:style="floatingStyles"
|
|
25
26
|
>
|
|
@@ -50,17 +51,17 @@
|
|
|
50
51
|
:category-badge-styles="categoryBadgeStyles"
|
|
51
52
|
:boolean-counts="booleanCounts"
|
|
52
53
|
/>
|
|
53
|
-
</
|
|
54
|
+
</PopoverPanel>
|
|
54
55
|
</Teleport>
|
|
55
56
|
</ClientOnly>
|
|
56
|
-
</
|
|
57
|
+
</Popover>
|
|
57
58
|
</template>
|
|
58
59
|
|
|
59
60
|
<script setup lang="ts">
|
|
60
|
-
import { computed,
|
|
61
|
+
import { computed, useTemplateRef } from 'vue'
|
|
61
62
|
import { flip, shift, autoUpdate, useFloating } from '@floating-ui/vue'
|
|
62
|
-
import {
|
|
63
|
-
import {
|
|
63
|
+
import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue'
|
|
64
|
+
import { RiFilterLine, RiCloseLine } from '@remixicon/vue'
|
|
64
65
|
import { useTranslation } from '../../composables/useTranslation'
|
|
65
66
|
import { hasFilterForColumn as _hasFilterForColumn } from '../../functions/tabular'
|
|
66
67
|
import BrandedButton from '../BrandedButton.vue'
|
|
@@ -83,18 +84,12 @@ const filters = defineModel<Record<string, ColumnFilters>>('filters', { default:
|
|
|
83
84
|
|
|
84
85
|
const { t } = useTranslation()
|
|
85
86
|
|
|
86
|
-
|
|
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,
|