@tekkare/romulus 0.1.50 → 0.1.52
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/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/CardActions.vue +229 -0
- package/dist/runtime/components/ChartCard.vue +27 -77
- package/dist/runtime/components/Drawer.vue +25 -2
- package/dist/runtime/components/ExportPanel.vue +95 -25
- package/dist/runtime/components/FilterComplex.vue +135 -28
- package/dist/runtime/components/FilterDropdown.vue +53 -17
- package/dist/runtime/components/Modal.vue +29 -4
- package/dist/runtime/components/ResultCard.vue +44 -3
- package/dist/runtime/components/SidebarLeft.vue +21 -3
- package/dist/runtime/components/TableCard.vue +28 -2
- package/dist/runtime/components/ViewPill.vue +2 -2
- package/dist/runtime/composables/useChartTheme.d.ts +1 -1
- package/dist/runtime/composables/useChartTheme.js +7 -6
- package/dist/runtime/composables/useRmCardActions.d.ts +32 -0
- package/dist/runtime/composables/useRmCardActions.js +53 -0
- package/dist/runtime/composables/useRmDialog.d.ts +27 -0
- package/dist/runtime/composables/useRmDialog.js +64 -0
- package/dist/runtime/composables/useRmExport.js +32 -11
- package/dist/runtime/composables/useTheme.d.ts +6 -1
- package/dist/runtime/composables/useTheme.js +23 -1
- package/dist/runtime/plugins/a11yFilters.client.d.ts +2 -0
- package/dist/runtime/plugins/a11yFilters.client.js +30 -0
- package/dist/runtime/utils/cardData.d.ts +41 -0
- package/dist/runtime/utils/cardData.js +138 -0
- package/dist/runtime/utils/export.d.ts +17 -1
- package/package.json +1 -1
|
@@ -84,7 +84,15 @@ const props = withDefaults(
|
|
|
84
84
|
},
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
const emit = defineEmits<{
|
|
87
|
+
const emit = defineEmits<{
|
|
88
|
+
export: [config: RmExportConfig]
|
|
89
|
+
/** Relancer un export conserve, par son identifiant cote app. */
|
|
90
|
+
rerun: [id: string]
|
|
91
|
+
/** Ouvrir l'ecran d'origine d'un export tire d'ailleurs. */
|
|
92
|
+
open: [id: string]
|
|
93
|
+
/** Retirer un export de la liste. */
|
|
94
|
+
forget: [id: string]
|
|
95
|
+
}>()
|
|
88
96
|
|
|
89
97
|
const LABELS = {
|
|
90
98
|
fr: {
|
|
@@ -138,6 +146,10 @@ const LABELS = {
|
|
|
138
146
|
recentTitle: 'Exports récents',
|
|
139
147
|
recentEmpty: 'Aucun export lancé depuis ce navigateur.',
|
|
140
148
|
recentHint: 'Historique local à ce navigateur : les exports ne sont pas conservés sur nos serveurs.',
|
|
149
|
+
savedHint: 'Un export conservé garde ses filtres, pas son fichier : le rejouer repose ces filtres et régénère le document avec la donnée du jour.',
|
|
150
|
+
rerun: 'Rejouer',
|
|
151
|
+
openPage: 'Ouvrir l\'écran',
|
|
152
|
+
forget: 'Supprimer',
|
|
141
153
|
permalinkHint: 'Le lien reprend la vue courante, filtres compris. Rien à configurer.',
|
|
142
154
|
nothing: 'Aucun contenu disponible pour ce format sur cette page.',
|
|
143
155
|
},
|
|
@@ -192,6 +204,10 @@ const LABELS = {
|
|
|
192
204
|
recentTitle: 'Recent exports',
|
|
193
205
|
recentEmpty: 'No export run from this browser yet.',
|
|
194
206
|
recentHint: 'History is local to this browser: exports are not kept on our servers.',
|
|
207
|
+
savedHint: 'A kept export stores its filters, not its file: running it again re-applies those filters and rebuilds the document from today\'s data.',
|
|
208
|
+
rerun: 'Run again',
|
|
209
|
+
openPage: 'Open screen',
|
|
210
|
+
forget: 'Delete',
|
|
195
211
|
permalinkHint: 'The link points at the current view, filters included. Nothing to configure.',
|
|
196
212
|
nothing: 'No content available for this format on this page.',
|
|
197
213
|
},
|
|
@@ -238,6 +254,12 @@ watch(
|
|
|
238
254
|
)
|
|
239
255
|
|
|
240
256
|
const step = ref(1)
|
|
257
|
+
/**
|
|
258
|
+
* Onglet affiche. « Mes exports » n'est pas une etape du wizard : c'est la
|
|
259
|
+
* liste de ce qui a deja ete tire, et elle vit a cote plutot qu'en dessous
|
|
260
|
+
* pour ne pas allonger un panneau deja long de quatre etapes.
|
|
261
|
+
*/
|
|
262
|
+
const tab = ref<'export' | 'mine'>('export')
|
|
241
263
|
const mode = ref<'entry' | 'wizard'>(props.templates.length ? 'entry' : 'wizard')
|
|
242
264
|
const templateKey = ref<string | null>(null)
|
|
243
265
|
|
|
@@ -360,24 +382,89 @@ const recentDate = (at: string | Date) =>
|
|
|
360
382
|
<template>
|
|
361
383
|
<div class="rm-export">
|
|
362
384
|
<div class="rm-export__tabs" role="tablist">
|
|
363
|
-
<button
|
|
385
|
+
<button
|
|
386
|
+
class="rm-export__tab"
|
|
387
|
+
:class="{ 'rm-export__tab--active': tab === 'export' }"
|
|
388
|
+
type="button"
|
|
389
|
+
role="tab"
|
|
390
|
+
@click="tab = 'export'"
|
|
391
|
+
>
|
|
364
392
|
<RmIcon name="PlusCircle" :size="16" /> {{ t.tabExport }}
|
|
365
393
|
</button>
|
|
366
394
|
<button
|
|
367
395
|
class="rm-export__tab"
|
|
396
|
+
:class="{ 'rm-export__tab--active': tab === 'mine' }"
|
|
368
397
|
type="button"
|
|
369
398
|
role="tab"
|
|
370
|
-
|
|
371
|
-
:title="t.schedulingSoon"
|
|
399
|
+
@click="tab = 'mine'"
|
|
372
400
|
>
|
|
373
401
|
<RmIcon name="ClockClockwise" :size="16" /> {{ t.tabMine }}
|
|
402
|
+
<span v-if="recent.length" class="rm-export__tab-count">{{ recent.length }}</span>
|
|
374
403
|
</button>
|
|
375
404
|
</div>
|
|
376
405
|
|
|
377
|
-
<p class="rm-export__lead">{{ t.lead }}</p>
|
|
406
|
+
<p v-if="tab === 'export'" class="rm-export__lead">{{ t.lead }}</p>
|
|
378
407
|
|
|
379
408
|
<!-- Entree : modeles pre-remplis, ou le wizard a la main -->
|
|
380
|
-
<div v-if="
|
|
409
|
+
<div v-if="tab === 'mine'" class="rm-export__history">
|
|
410
|
+
<p class="rm-export__eyebrow">
|
|
411
|
+
<RmIcon name="DownloadSimple" :size="14" /> {{ t.recentTitle }}
|
|
412
|
+
</p>
|
|
413
|
+
<p v-if="!recent.length" class="rm-export__hint">{{ t.recentEmpty }}</p>
|
|
414
|
+
<div
|
|
415
|
+
v-for="(item, index) in recent"
|
|
416
|
+
:key="item.id ?? `${item.name}-${index}`"
|
|
417
|
+
class="rm-export__history-row"
|
|
418
|
+
>
|
|
419
|
+
<RmIcon name="File" :size="16" />
|
|
420
|
+
<span class="rm-export__history-body">
|
|
421
|
+
<span class="rm-export__history-name">{{ item.name }}</span>
|
|
422
|
+
<span class="rm-export__history-meta">
|
|
423
|
+
<template v-if="item.pageLabel">{{ item.pageLabel }} · </template>
|
|
424
|
+
<template v-if="item.rows">{{ item.rows.toLocaleString(lang === 'en' ? 'en-US' : 'fr-FR') }} {{ t.rows }} · </template>
|
|
425
|
+
<template v-if="item.size">{{ item.size }} · </template>
|
|
426
|
+
{{ recentDate(item.at) }}
|
|
427
|
+
</span>
|
|
428
|
+
</span>
|
|
429
|
+
<!-- Un export conserve se rejoue ; venu d'un autre ecran, il s'y ouvre,
|
|
430
|
+
parce que ses filtres ne veulent rien dire sur celui-ci. -->
|
|
431
|
+
<template v-if="item.id">
|
|
432
|
+
<RmButton
|
|
433
|
+
v-if="item.runnable !== false"
|
|
434
|
+
variant="secondary"
|
|
435
|
+
size="sm"
|
|
436
|
+
prefix-icon="ArrowClockwise"
|
|
437
|
+
:disabled="busy"
|
|
438
|
+
:title="t.rerun"
|
|
439
|
+
@click="emit('rerun', item.id)"
|
|
440
|
+
>
|
|
441
|
+
{{ t.rerun }}
|
|
442
|
+
</RmButton>
|
|
443
|
+
<RmButton
|
|
444
|
+
v-else
|
|
445
|
+
variant="secondary"
|
|
446
|
+
size="sm"
|
|
447
|
+
prefix-icon="ArrowSquareOut"
|
|
448
|
+
:title="t.openPage"
|
|
449
|
+
@click="emit('open', item.id)"
|
|
450
|
+
>
|
|
451
|
+
{{ t.openPage }}
|
|
452
|
+
</RmButton>
|
|
453
|
+
<RmButton
|
|
454
|
+
variant="ghost"
|
|
455
|
+
size="sm"
|
|
456
|
+
prefix-icon="Trash"
|
|
457
|
+
:title="t.forget"
|
|
458
|
+
@click="emit('forget', item.id)"
|
|
459
|
+
/>
|
|
460
|
+
</template>
|
|
461
|
+
</div>
|
|
462
|
+
<p v-if="recent.length" class="rm-export__hint">
|
|
463
|
+
{{ recent.some(item => item.id) ? t.savedHint : t.recentHint }}
|
|
464
|
+
</p>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div v-else-if="mode === 'entry'" class="rm-export__entry">
|
|
381
468
|
<p class="rm-export__eyebrow">{{ t.entryTitle }}</p>
|
|
382
469
|
<div class="rm-export__templates">
|
|
383
470
|
<button
|
|
@@ -409,7 +496,7 @@ const recentDate = (at: string | Date) =>
|
|
|
409
496
|
</div>
|
|
410
497
|
|
|
411
498
|
<!-- Wizard -->
|
|
412
|
-
<div v-else class="rm-export__wizard">
|
|
499
|
+
<div v-else-if="tab === 'export'" class="rm-export__wizard">
|
|
413
500
|
<div v-if="templates.length" class="rm-export__wizard-head">
|
|
414
501
|
<button class="rm-export__back" type="button" @click="mode = 'entry'">
|
|
415
502
|
<RmIcon name="ArrowLeft" :size="14" /> {{ t.backToEntry }}
|
|
@@ -722,26 +809,9 @@ const recentDate = (at: string | Date) =>
|
|
|
722
809
|
</div>
|
|
723
810
|
</div>
|
|
724
811
|
|
|
725
|
-
<!-- Exports recents -->
|
|
726
|
-
<div class="rm-export__history">
|
|
727
|
-
<p class="rm-export__eyebrow">
|
|
728
|
-
<RmIcon name="DownloadSimple" :size="14" /> {{ t.recentTitle }}
|
|
729
|
-
</p>
|
|
730
|
-
<p v-if="!recent.length" class="rm-export__hint">{{ t.recentEmpty }}</p>
|
|
731
|
-
<div v-for="(item, index) in recent" :key="`${item.name}-${index}`" class="rm-export__history-row">
|
|
732
|
-
<RmIcon name="File" :size="16" />
|
|
733
|
-
<span class="rm-export__history-name">{{ item.name }}</span>
|
|
734
|
-
<span class="rm-export__history-meta">
|
|
735
|
-
<template v-if="item.rows">{{ item.rows.toLocaleString(lang === 'en' ? 'en-US' : 'fr-FR') }} {{ t.rows }} · </template>
|
|
736
|
-
<template v-if="item.size">{{ item.size }} · </template>
|
|
737
|
-
{{ recentDate(item.at) }}
|
|
738
|
-
</span>
|
|
739
|
-
</div>
|
|
740
|
-
<p v-if="recent.length" class="rm-export__hint">{{ t.recentHint }}</p>
|
|
741
|
-
</div>
|
|
742
812
|
</div>
|
|
743
813
|
</template>
|
|
744
814
|
|
|
745
815
|
<style scoped>
|
|
746
|
-
.rm-export{display:flex;flex-direction:column}.rm-export__tabs{border-bottom:1px solid var(--border-default);display:flex;margin-bottom:var(--space-16)}.rm-export__tab{align-items:center;background:none;border:none;border-bottom:3px solid transparent;color:var(--text-muted);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-14);font-weight:600;gap:var(--space-6);padding:var(--space-12) var(--space-16)}.rm-export__tab--active{border-bottom-color:var(--color-primary);color:var(--color-secondary)}.rm-export__tab:disabled{cursor:not-allowed;opacity:.5}.rm-export__desc,.rm-export__lead{color:var(--text-secondary);font-size:12.5px;line-height:1.55;margin:0 0 var(--space-16)}.rm-export__eyebrow{align-items:center;color:var(--text-muted);display:flex;font-size:10.5px;font-weight:700;gap:var(--space-6);letter-spacing:.1em;margin:0 0 var(--space-8);text-transform:uppercase}.rm-export__hint{color:var(--text-muted);display:block;font-size:11.5px;line-height:1.45;margin:var(--space-8) 0 0}.rm-export__notice{color:var(--color-success)}.rm-export__error,.rm-export__notice{font-size:12px;margin:var(--space-8) 0 0}.rm-export__error{color:var(--color-danger)}.rm-export__templates{display:grid;gap:var(--space-8)}.rm-export__template{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);cursor:pointer;display:flex;font-family:inherit;gap:var(--space-12);padding:var(--space-12) var(--space-16);text-align:left;transition:border-color var(--duration-fast),background var(--duration-fast),transform var(--duration-fast)}.rm-export__template:hover{background:var(--bg-hover);border-color:var(--color-secondary);transform:translateY(-1px)}.rm-export__template--custom{background:transparent;border-style:dashed}.rm-export__template-body{display:flex;flex-direction:column;min-width:0}.rm-export__template-name{color:var(--text-primary);font-size:13.5px;font-weight:700;line-height:1.2}.rm-export__template-meta{color:var(--text-muted);font-size:11px;line-height:1.35;margin-top:3px}.rm-export__wizard{background:var(--bg-surface-2);border:1px solid var(--border-default);border-radius:var(--radius-2xl);padding:var(--space-16) var(--space-20)}.rm-export__wizard-head{align-items:center;border-bottom:1px solid var(--border-soft);display:flex;gap:var(--space-12);margin-bottom:var(--space-16);padding-bottom:var(--space-12)}.rm-export__back{align-items:center;background:none;border:none;color:var(--color-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:12px;font-weight:600;gap:var(--space-4);padding:0}.rm-export__back:hover{text-decoration:underline;text-underline-offset:3px}.rm-export__stepper{align-items:center;display:flex;gap:var(--space-4);margin-bottom:var(--space-20)}.rm-export__step{background:none;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:inherit;gap:var(--space-6);padding:var(--space-4) var(--space-8)}.rm-export__step,.rm-export__step-num{align-items:center;display:inline-flex}.rm-export__step-num{background:var(--bg-surface-3);border:1.5px solid var(--border-default);border-radius:50%;color:var(--text-muted);font-size:12px;font-weight:700;height:22px;justify-content:center;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast);width:22px}.rm-export__step-label{color:var(--text-muted);font-size:12px;font-weight:600}.rm-export__step:hover .rm-export__step-label{color:var(--text-primary)}.rm-export__step--active .rm-export__step-num{background:var(--color-primary);border-color:var(--color-primary);box-shadow:var(--shadow-focus);color:#fff}.rm-export__step--active .rm-export__step-label{color:var(--color-secondary)}.rm-export__step--done .rm-export__step-num{background:var(--color-secondary);border-color:var(--color-secondary);color:#fff}.rm-export__step--done .rm-export__step-label{color:var(--text-secondary)}.rm-export__step-divider{background:var(--border-default);border-radius:var(--radius-pill);flex:1;height:1.5px}.rm-export__panel{animation:rm-export-in var(--duration-base) var(--ease-out)}@keyframes rm-export-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.rm-export__formats{display:grid;gap:var(--space-8)}.rm-export__format{align-items:flex-start;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-xl);cursor:pointer;display:flex;gap:var(--space-12);padding:var(--space-12) var(--space-16);transition:border-color var(--duration-fast),background var(--duration-fast),transform var(--duration-fast)}.rm-export__format:hover{border-color:var(--color-secondary);transform:translateY(-1px)}.rm-export__format--active{background:var(--bg-hover);border-color:var(--color-primary);box-shadow:var(--shadow-focus)}.rm-export__format--active .rm-export__format-name{color:var(--color-primary)}.rm-export__format--off{cursor:not-allowed;opacity:.55}.rm-export__format--off:hover{border-color:var(--border-default);transform:none}.rm-export__format input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:50%;cursor:inherit;flex-shrink:0;height:18px;margin:1px 0 0;position:relative;width:18px}.rm-export__format input[type=radio]:checked{border-color:var(--color-primary)}.rm-export__format input[type=radio]:checked:after{background:var(--color-primary);border-radius:50%;content:"";inset:3px;position:absolute}.rm-export__format-text{display:flex;flex-direction:column;min-width:0}.rm-export__format-name{color:var(--text-primary);font-size:14px;font-weight:700;line-height:1.2}.rm-export__format-desc{color:var(--text-muted);font-size:11.5px;line-height:1.45;margin-top:3px}.rm-export__format-meta{border-top:1px solid var(--border-soft);color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;margin-top:var(--space-8);padding-top:var(--space-8);text-transform:uppercase}.rm-export__row{margin-bottom:var(--space-20)}.rm-export__row:last-child{margin-bottom:0}.rm-export__label{color:var(--text-secondary);display:block;font-size:11px;font-weight:700;letter-spacing:.06em;margin-bottom:var(--space-8);text-transform:uppercase}.rm-export__sub{margin-top:var(--space-12)}.rm-export__input{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;font-size:13px;padding:var(--space-8) var(--space-12);width:100%}.rm-export__input:focus{border-color:var(--color-primary);box-shadow:var(--shadow-focus);outline:none}.rm-export__checks{display:grid;gap:var(--space-6)}.rm-export__check{align-items:center;color:var(--text-primary);cursor:pointer;display:flex;font-size:12.5px;gap:var(--space-8)}.rm-export__check--off{cursor:not-allowed;opacity:.5}.rm-export__check input[type=checkbox]{accent-color:var(--color-primary);cursor:inherit;height:15px;width:15px}.rm-export__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-export__chip{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill);cursor:pointer;display:inline-flex;font-family:inherit;font-size:11.5px;font-weight:600;gap:var(--space-4);padding:3px var(--space-8)}.rm-export__chip--on{background:var(--bg-hover);border-color:var(--color-primary);color:var(--color-primary)}.rm-export__chip--off{background:var(--bg-surface);color:var(--text-muted)}.rm-export__chip--off:hover{border-color:var(--color-secondary);color:var(--color-secondary)}.rm-export__recap{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);margin-top:var(--space-16);padding:var(--space-16)}.rm-export__recap-row{align-items:flex-start;border-bottom:1px solid var(--border-soft);color:var(--color-secondary);display:flex;gap:var(--space-12);padding:var(--space-8) 0}.rm-export__recap-row:last-child{border-bottom:none}.rm-export__recap-row>div{flex:1;min-width:0}.rm-export__recap-k{color:var(--text-muted);font-size:10.5px;font-weight:700;letter-spacing:.08em;margin-bottom:2px;text-transform:uppercase}.rm-export__recap-v{color:var(--text-primary);font-size:13px;font-weight:600;word-break:break-word}.rm-export__footer{align-items:center;border-top:1px solid var(--border-default);display:flex;gap:var(--space-8);margin-top:var(--space-20);padding-top:var(--space-16)}.rm-export__progress{color:var(--text-muted);flex:1;font-size:12px;text-align:center}.rm-export__progress b{color:var(--text-primary)}.rm-export__history{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);
|
|
816
|
+
.rm-export{display:flex;flex-direction:column}.rm-export__tabs{border-bottom:1px solid var(--border-default);display:flex;margin-bottom:var(--space-16)}.rm-export__tab{align-items:center;background:none;border:none;border-bottom:3px solid transparent;color:var(--text-muted);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-14);font-weight:600;gap:var(--space-6);padding:var(--space-12) var(--space-16)}.rm-export__tab--active{border-bottom-color:var(--color-primary);color:var(--color-secondary)}.rm-export__tab:disabled{cursor:not-allowed;opacity:.5}.rm-export__desc,.rm-export__lead{color:var(--text-secondary);font-size:12.5px;line-height:1.55;margin:0 0 var(--space-16)}.rm-export__eyebrow{align-items:center;color:var(--text-muted);display:flex;font-size:10.5px;font-weight:700;gap:var(--space-6);letter-spacing:.1em;margin:0 0 var(--space-8);text-transform:uppercase}.rm-export__hint{color:var(--text-muted);display:block;font-size:11.5px;line-height:1.45;margin:var(--space-8) 0 0}.rm-export__notice{color:var(--color-success)}.rm-export__error,.rm-export__notice{font-size:12px;margin:var(--space-8) 0 0}.rm-export__error{color:var(--color-danger)}.rm-export__templates{display:grid;gap:var(--space-8)}.rm-export__template{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);cursor:pointer;display:flex;font-family:inherit;gap:var(--space-12);padding:var(--space-12) var(--space-16);text-align:left;transition:border-color var(--duration-fast),background var(--duration-fast),transform var(--duration-fast)}.rm-export__template:hover{background:var(--bg-hover);border-color:var(--color-secondary);transform:translateY(-1px)}.rm-export__template--custom{background:transparent;border-style:dashed}.rm-export__template-body{display:flex;flex-direction:column;min-width:0}.rm-export__template-name{color:var(--text-primary);font-size:13.5px;font-weight:700;line-height:1.2}.rm-export__template-meta{color:var(--text-muted);font-size:11px;line-height:1.35;margin-top:3px}.rm-export__wizard{background:var(--bg-surface-2);border:1px solid var(--border-default);border-radius:var(--radius-2xl);padding:var(--space-16) var(--space-20)}.rm-export__wizard-head{align-items:center;border-bottom:1px solid var(--border-soft);display:flex;gap:var(--space-12);margin-bottom:var(--space-16);padding-bottom:var(--space-12)}.rm-export__back{align-items:center;background:none;border:none;color:var(--color-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:12px;font-weight:600;gap:var(--space-4);padding:0}.rm-export__back:hover{text-decoration:underline;text-underline-offset:3px}.rm-export__stepper{align-items:center;display:flex;gap:var(--space-4);margin-bottom:var(--space-20)}.rm-export__step{background:none;border:none;border-radius:var(--radius-pill);cursor:pointer;font-family:inherit;gap:var(--space-6);padding:var(--space-4) var(--space-8)}.rm-export__step,.rm-export__step-num{align-items:center;display:inline-flex}.rm-export__step-num{background:var(--bg-surface-3);border:1.5px solid var(--border-default);border-radius:50%;color:var(--text-muted);font-size:12px;font-weight:700;height:22px;justify-content:center;transition:background var(--duration-fast),color var(--duration-fast),border-color var(--duration-fast);width:22px}.rm-export__step-label{color:var(--text-muted);font-size:12px;font-weight:600}.rm-export__step:hover .rm-export__step-label{color:var(--text-primary)}.rm-export__step--active .rm-export__step-num{background:var(--color-primary);border-color:var(--color-primary);box-shadow:var(--shadow-focus);color:#fff}.rm-export__step--active .rm-export__step-label{color:var(--color-secondary)}.rm-export__step--done .rm-export__step-num{background:var(--color-secondary);border-color:var(--color-secondary);color:#fff}.rm-export__step--done .rm-export__step-label{color:var(--text-secondary)}.rm-export__step-divider{background:var(--border-default);border-radius:var(--radius-pill);flex:1;height:1.5px}.rm-export__panel{animation:rm-export-in var(--duration-base) var(--ease-out)}@keyframes rm-export-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.rm-export__formats{display:grid;gap:var(--space-8)}.rm-export__format{align-items:flex-start;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-xl);cursor:pointer;display:flex;gap:var(--space-12);padding:var(--space-12) var(--space-16);transition:border-color var(--duration-fast),background var(--duration-fast),transform var(--duration-fast)}.rm-export__format:hover{border-color:var(--color-secondary);transform:translateY(-1px)}.rm-export__format--active{background:var(--bg-hover);border-color:var(--color-primary);box-shadow:var(--shadow-focus)}.rm-export__format--active .rm-export__format-name{color:var(--color-primary)}.rm-export__format--off{cursor:not-allowed;opacity:.55}.rm-export__format--off:hover{border-color:var(--border-default);transform:none}.rm-export__format input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:50%;cursor:inherit;flex-shrink:0;height:18px;margin:1px 0 0;position:relative;width:18px}.rm-export__format input[type=radio]:checked{border-color:var(--color-primary)}.rm-export__format input[type=radio]:checked:after{background:var(--color-primary);border-radius:50%;content:"";inset:3px;position:absolute}.rm-export__format-text{display:flex;flex-direction:column;min-width:0}.rm-export__format-name{color:var(--text-primary);font-size:14px;font-weight:700;line-height:1.2}.rm-export__format-desc{color:var(--text-muted);font-size:11.5px;line-height:1.45;margin-top:3px}.rm-export__format-meta{border-top:1px solid var(--border-soft);color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;margin-top:var(--space-8);padding-top:var(--space-8);text-transform:uppercase}.rm-export__row{margin-bottom:var(--space-20)}.rm-export__row:last-child{margin-bottom:0}.rm-export__label{color:var(--text-secondary);display:block;font-size:11px;font-weight:700;letter-spacing:.06em;margin-bottom:var(--space-8);text-transform:uppercase}.rm-export__sub{margin-top:var(--space-12)}.rm-export__input{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-primary);font-family:inherit;font-size:13px;padding:var(--space-8) var(--space-12);width:100%}.rm-export__input:focus{border-color:var(--color-primary);box-shadow:var(--shadow-focus);outline:none}.rm-export__checks{display:grid;gap:var(--space-6)}.rm-export__check{align-items:center;color:var(--text-primary);cursor:pointer;display:flex;font-size:12.5px;gap:var(--space-8)}.rm-export__check--off{cursor:not-allowed;opacity:.5}.rm-export__check input[type=checkbox]{accent-color:var(--color-primary);cursor:inherit;height:15px;width:15px}.rm-export__chips{display:flex;flex-wrap:wrap;gap:var(--space-6)}.rm-export__chip{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill);cursor:pointer;display:inline-flex;font-family:inherit;font-size:11.5px;font-weight:600;gap:var(--space-4);padding:3px var(--space-8)}.rm-export__chip--on{background:var(--bg-hover);border-color:var(--color-primary);color:var(--color-primary)}.rm-export__chip--off{background:var(--bg-surface);color:var(--text-muted)}.rm-export__chip--off:hover{border-color:var(--color-secondary);color:var(--color-secondary)}.rm-export__recap{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);margin-top:var(--space-16);padding:var(--space-16)}.rm-export__recap-row{align-items:flex-start;border-bottom:1px solid var(--border-soft);color:var(--color-secondary);display:flex;gap:var(--space-12);padding:var(--space-8) 0}.rm-export__recap-row:last-child{border-bottom:none}.rm-export__recap-row>div{flex:1;min-width:0}.rm-export__recap-k{color:var(--text-muted);font-size:10.5px;font-weight:700;letter-spacing:.08em;margin-bottom:2px;text-transform:uppercase}.rm-export__recap-v{color:var(--text-primary);font-size:13px;font-weight:600;word-break:break-word}.rm-export__footer{align-items:center;border-top:1px solid var(--border-default);display:flex;gap:var(--space-8);margin-top:var(--space-20);padding-top:var(--space-16)}.rm-export__progress{color:var(--text-muted);flex:1;font-size:12px;text-align:center}.rm-export__progress b{color:var(--text-primary)}.rm-export__history{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl);padding:var(--space-16)}.rm-export__history-row{align-items:center;color:var(--color-primary);display:flex;gap:var(--space-8);padding:var(--space-6) 0}.rm-export__history-body{display:flex;flex:1;flex-direction:column;min-width:0}.rm-export__history-name{color:var(--text-primary);font-size:12.5px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-export__history-meta{color:var(--text-muted);font-size:11px}.rm-export__tab-count{background:var(--bg-surface-3);border-radius:var(--radius-pill);color:var(--text-secondary);font-size:10.5px;padding:1px var(--space-6)}
|
|
747
817
|
</style>
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmFilterComplex - filtre a selection multiple sur un referentiel long.
|
|
3
|
+
|
|
4
|
+
Deux colonnes, cote a cote : a gauche ce qu'on peut ajouter, a droite ce qui
|
|
5
|
+
est deja retenu. Une liste a cases a cocher obligerait a retrouver ses choix
|
|
6
|
+
au milieu de deux mille lignes ; ici la selection est toujours lisible d'un
|
|
7
|
+
coup d'oeil, et se defait la ou elle s'affiche.
|
|
8
|
+
|
|
9
|
+
La colonne de gauche peut ouvrir sur des selections toutes faites
|
|
10
|
+
(`presets`) : sur un referentiel connu, « Big 5 Europe » vaut cinq clics et
|
|
11
|
+
evite une erreur de composition.
|
|
12
|
+
|
|
13
|
+
La pastille, elle, nomme la selection plutot que de la compter : « Pays :
|
|
14
|
+
France, UK +3 » se lit sans ouvrir le panneau, « Pays (5) » non.
|
|
15
|
+
-->
|
|
1
16
|
<script setup lang="ts">
|
|
2
17
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
|
3
18
|
import {
|
|
@@ -14,10 +29,21 @@ export interface FilterComplexOption {
|
|
|
14
29
|
value: string
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
/** Une selection toute faite, proposee en tete de la colonne d'ajout. */
|
|
33
|
+
export interface FilterComplexPreset {
|
|
34
|
+
label: string
|
|
35
|
+
values: string[]
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
const props = withDefaults(defineProps<{
|
|
18
39
|
icon?: Component
|
|
19
40
|
label: string
|
|
20
41
|
options: FilterComplexOption[]
|
|
42
|
+
/**
|
|
43
|
+
* Selections rapides. Vides, la section ne sort pas : un intitule
|
|
44
|
+
* « Selection rapide » suivi de rien se lit comme un chargement en cours.
|
|
45
|
+
*/
|
|
46
|
+
presets?: FilterComplexPreset[]
|
|
21
47
|
/** Langue du chrome du panneau : titres, recherches, actions. */
|
|
22
48
|
lang?: 'fr' | 'en'
|
|
23
49
|
/**
|
|
@@ -29,6 +55,7 @@ const props = withDefaults(defineProps<{
|
|
|
29
55
|
remote?: boolean
|
|
30
56
|
}>(), {
|
|
31
57
|
icon: undefined,
|
|
58
|
+
presets: () => [],
|
|
32
59
|
lang: 'en',
|
|
33
60
|
remote: false,
|
|
34
61
|
})
|
|
@@ -47,22 +74,26 @@ const delSearch = ref('')
|
|
|
47
74
|
/** Le chrome du panneau, dans les deux langues. */
|
|
48
75
|
const WORDS = {
|
|
49
76
|
fr: {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
77
|
+
quick: 'Sélection rapide',
|
|
78
|
+
custom: 'Sélection custom',
|
|
79
|
+
customAfterPresets: 'Ou sélection custom',
|
|
80
|
+
searchAdd: 'Rechercher...',
|
|
81
|
+
addAll: 'Tout sélectionner',
|
|
82
|
+
current: 'Sélection actuelle',
|
|
83
|
+
clear: 'Tout effacer',
|
|
84
|
+
searchSelection: 'Filtrer la sélection...',
|
|
56
85
|
noResult: 'Aucun résultat',
|
|
57
86
|
noSelection: 'Aucune sélection',
|
|
58
87
|
},
|
|
59
88
|
en: {
|
|
89
|
+
quick: 'Quick selection',
|
|
60
90
|
custom: 'Custom selection',
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
customAfterPresets: 'Or custom selection',
|
|
92
|
+
searchAdd: 'Search...',
|
|
93
|
+
addAll: 'Select all',
|
|
63
94
|
current: 'Current selection',
|
|
64
|
-
clear: 'Clear',
|
|
65
|
-
searchSelection: '
|
|
95
|
+
clear: 'Clear all',
|
|
96
|
+
searchSelection: 'Filter selection...',
|
|
66
97
|
noResult: 'No results',
|
|
67
98
|
noSelection: 'No selection',
|
|
68
99
|
},
|
|
@@ -100,6 +131,52 @@ const filteredSelected = computed(() => {
|
|
|
100
131
|
return selectedOptions.value.filter(o => o.label.toLowerCase().includes(q))
|
|
101
132
|
})
|
|
102
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Resume porte par la pastille : les premiers noms, puis le compte de ce qui
|
|
136
|
+
* reste. La pastille occupe une cellule de la barre de filtres, donc une
|
|
137
|
+
* largeur fixe : on s'arrete des que les noms la depassent, plutot que d'en
|
|
138
|
+
* poser deux et de couper le second au milieu (« DOLIPRANE, EFF... »). Deux
|
|
139
|
+
* noms courts tiennent, un nom long passe seul.
|
|
140
|
+
*/
|
|
141
|
+
const SUMMARY_CHARS = 16
|
|
142
|
+
const SUMMARY_NAMES = 2
|
|
143
|
+
|
|
144
|
+
const summaryNames = computed(() => {
|
|
145
|
+
const names: string[] = []
|
|
146
|
+
let width = 0
|
|
147
|
+
|
|
148
|
+
for (const option of selectedOptions.value.slice(0, SUMMARY_NAMES)) {
|
|
149
|
+
const next = width + option.label.length + (names.length ? 2 : 0)
|
|
150
|
+
// Le premier nom passe quoi qu'il arrive : une pastille qui ne nommerait
|
|
151
|
+
// que « +5 » ne dirait plus sur quoi la page est filtree.
|
|
152
|
+
if (names.length && next > SUMMARY_CHARS) break
|
|
153
|
+
names.push(option.label)
|
|
154
|
+
width = next
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return names
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const summary = computed(() => summaryNames.value.join(', '))
|
|
161
|
+
const overflow = computed(() => Math.max(0, selected.value.length - summaryNames.value.length))
|
|
162
|
+
|
|
163
|
+
/** Une selection rapide est active quand elle decrit exactement l'ecran. */
|
|
164
|
+
function isPresetActive(preset: FilterComplexPreset) {
|
|
165
|
+
return (
|
|
166
|
+
preset.values.length === selected.value.length &&
|
|
167
|
+
preset.values.every(value => selected.value.includes(value))
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Une selection rapide remplace la selection : elle nomme un ensemble
|
|
173
|
+
* (« Big 5 Europe »), elle ne s'ajoute pas au precedent, sinon le libelle de
|
|
174
|
+
* la pastille ne decrirait plus ce qui est retenu.
|
|
175
|
+
*/
|
|
176
|
+
function applyPreset(preset: FilterComplexPreset) {
|
|
177
|
+
selected.value = isPresetActive(preset) ? [] : [...preset.values]
|
|
178
|
+
}
|
|
179
|
+
|
|
103
180
|
function addOption(value: string) {
|
|
104
181
|
selected.value = [...selected.value, value]
|
|
105
182
|
}
|
|
@@ -144,10 +221,9 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
144
221
|
@click="toggle"
|
|
145
222
|
>
|
|
146
223
|
<component :is="icon" v-if="icon" :size="16" class="filter-complex__icon" />
|
|
147
|
-
<span class="filter-complex__label">{{ label }}
|
|
148
|
-
<span v-if="isActive" class="filter-
|
|
149
|
-
|
|
150
|
-
</span>
|
|
224
|
+
<span class="filter-complex__label">{{ label }}<template v-if="isActive"> :</template></span>
|
|
225
|
+
<span v-if="isActive" class="filter-complex__summary">{{ summary }}</span>
|
|
226
|
+
<span v-if="overflow > 0" class="filter-complex__badge">+{{ overflow }}</span>
|
|
151
227
|
<PhCaretDown
|
|
152
228
|
:size="12"
|
|
153
229
|
class="filter-complex__caret"
|
|
@@ -159,7 +235,35 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
159
235
|
<div v-if="isOpen" class="filter-complex__panel" @click.stop>
|
|
160
236
|
<!-- Left: Add -->
|
|
161
237
|
<div class="filter-complex__left">
|
|
162
|
-
<
|
|
238
|
+
<template v-if="presets.length">
|
|
239
|
+
<h3 class="filter-complex__title">{{ t.quick }}</h3>
|
|
240
|
+
<div class="filter-complex__presets">
|
|
241
|
+
<button
|
|
242
|
+
v-for="preset in presets"
|
|
243
|
+
:key="preset.label"
|
|
244
|
+
type="button"
|
|
245
|
+
class="filter-complex__preset"
|
|
246
|
+
:class="{ 'is-active': isPresetActive(preset) }"
|
|
247
|
+
@click="applyPreset(preset)"
|
|
248
|
+
>
|
|
249
|
+
{{ preset.label }}
|
|
250
|
+
</button>
|
|
251
|
+
</div>
|
|
252
|
+
</template>
|
|
253
|
+
|
|
254
|
+
<div class="filter-complex__head">
|
|
255
|
+
<h3 class="filter-complex__title">
|
|
256
|
+
{{ presets.length ? t.customAfterPresets : t.custom }}
|
|
257
|
+
</h3>
|
|
258
|
+
<button
|
|
259
|
+
v-if="filteredAvailable.length > 0"
|
|
260
|
+
type="button"
|
|
261
|
+
class="filter-complex__action filter-complex__action--add"
|
|
262
|
+
@click="addAll"
|
|
263
|
+
>
|
|
264
|
+
{{ t.addAll }}
|
|
265
|
+
</button>
|
|
266
|
+
</div>
|
|
163
267
|
|
|
164
268
|
<div class="filter-complex__search">
|
|
165
269
|
<PhMagnifyingGlass :size="14" color="var(--text-muted)" />
|
|
@@ -177,14 +281,6 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
177
281
|
/>
|
|
178
282
|
</div>
|
|
179
283
|
|
|
180
|
-
<button
|
|
181
|
-
v-if="filteredAvailable.length > 0"
|
|
182
|
-
class="filter-complex__action filter-complex__action--add"
|
|
183
|
-
@click="addAll"
|
|
184
|
-
>
|
|
185
|
-
{{ t.addAll }} ({{ filteredAvailable.length }})
|
|
186
|
-
</button>
|
|
187
|
-
|
|
188
284
|
<div class="filter-complex__list">
|
|
189
285
|
<button
|
|
190
286
|
v-for="option in filteredAvailable"
|
|
@@ -192,8 +288,12 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
192
288
|
class="filter-complex__item"
|
|
193
289
|
@click="addOption(option.value)"
|
|
194
290
|
>
|
|
291
|
+
<PhPlusCircle
|
|
292
|
+
:size="16"
|
|
293
|
+
weight="fill"
|
|
294
|
+
class="filter-complex__item-icon filter-complex__item-icon--add"
|
|
295
|
+
/>
|
|
195
296
|
<span class="filter-complex__item-label">{{ option.label }}</span>
|
|
196
|
-
<PhPlusCircle :size="16" class="filter-complex__item-icon filter-complex__item-icon--add" />
|
|
197
297
|
</button>
|
|
198
298
|
<p v-if="filteredAvailable.length === 0" class="filter-complex__empty">
|
|
199
299
|
{{ t.noResult }}
|
|
@@ -203,10 +303,13 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
203
303
|
|
|
204
304
|
<!-- Right: Selection -->
|
|
205
305
|
<div class="filter-complex__right">
|
|
206
|
-
<div class="filter-
|
|
207
|
-
<h3 class="filter-complex__title">
|
|
306
|
+
<div class="filter-complex__head">
|
|
307
|
+
<h3 class="filter-complex__title">
|
|
308
|
+
{{ t.current }} ({{ selected.length }})
|
|
309
|
+
</h3>
|
|
208
310
|
<button
|
|
209
311
|
v-if="selected.length > 0"
|
|
312
|
+
type="button"
|
|
210
313
|
class="filter-complex__action filter-complex__action--clear"
|
|
211
314
|
@click="clearAll"
|
|
212
315
|
>
|
|
@@ -237,8 +340,12 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
237
340
|
class="filter-complex__item"
|
|
238
341
|
@click="removeOption(option.value)"
|
|
239
342
|
>
|
|
343
|
+
<PhMinusCircle
|
|
344
|
+
:size="16"
|
|
345
|
+
weight="fill"
|
|
346
|
+
class="filter-complex__item-icon filter-complex__item-icon--remove"
|
|
347
|
+
/>
|
|
240
348
|
<span class="filter-complex__item-label">{{ option.label }}</span>
|
|
241
|
-
<PhMinusCircle :size="16" class="filter-complex__item-icon filter-complex__item-icon--remove" />
|
|
242
349
|
</button>
|
|
243
350
|
<p v-if="selected.length === 0" class="filter-complex__empty">
|
|
244
351
|
{{ t.noSelection }}
|
|
@@ -251,5 +358,5 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
251
358
|
</template>
|
|
252
359
|
|
|
253
360
|
<style scoped>
|
|
254
|
-
.filter-complex{position:relative}.filter-complex__trigger{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:
|
|
361
|
+
.filter-complex{position:relative}.filter-complex__trigger{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);box-sizing:border-box;color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h,32px);max-width:320px;padding:0 12px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap}.filter-complex__trigger:hover{background:var(--bg-surface-3)}.filter-complex__icon{color:var(--color-primary);flex-shrink:0}.filter-complex__trigger--active{border-color:var(--color-primary)}.filter-complex__label{color:var(--text-muted);flex-shrink:0;font-weight:var(--weight-normal)}.filter-complex__summary{font-weight:var(--weight-semibold);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.filter-complex__badge{align-items:center;background-color:color-mix(in srgb,var(--color-primary) 10%,transparent);border-radius:var(--radius-pill);color:var(--color-primary);display:inline-flex;flex-shrink:0;font-size:var(--font-12);font-weight:var(--weight-semibold);height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 6px}.filter-complex__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-complex__caret--open{transform:rotate(180deg)}.filter-complex__panel{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl,12px);box-shadow:0 4px 24px rgba(var(--shadow-rgb),.1),0 2px 8px rgba(var(--shadow-rgb),.05);display:flex;left:0;max-height:460px;max-width:calc(100vw - 32px);overflow:hidden;position:absolute;top:calc(100% + 6px);width:720px;z-index:50}.filter-complex__left,.filter-complex__right{display:flex;flex-direction:column;gap:10px;overflow:hidden;padding:20px}.filter-complex__left{flex:1 1 50%}.filter-complex__right{border-left:1px solid var(--border-default);flex:1 1 50%}.filter-complex__head{align-items:center;display:flex;gap:12px;justify-content:space-between}.filter-complex__title{color:var(--text-primary);font-size:var(--font-14);font-weight:var(--weight-bold);margin:0}.filter-complex__presets{display:flex;flex-wrap:wrap;gap:8px}.filter-complex__preset{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;font-family:var(--font-sans);font-size:var(--font-12);font-weight:var(--weight-medium);height:26px;padding:0 12px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap}.filter-complex__preset:hover{border-color:var(--color-primary);color:var(--color-primary)}.filter-complex__preset.is-active{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.filter-complex__search{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);display:flex;gap:8px;padding:7px 10px}.filter-complex__search:focus-within{border-color:var(--color-primary)}.filter-complex__search-input{background:transparent;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-14);outline:none}.filter-complex__search-input::-moz-placeholder{color:var(--text-muted)}.filter-complex__search-input::placeholder{color:var(--text-muted)}.filter-complex__search-clear{color:var(--text-muted);cursor:pointer;flex-shrink:0}.filter-complex__search-clear:hover{color:var(--text-secondary)}.filter-complex__action{background:none;border:none;cursor:pointer;font-family:var(--font-sans);font-size:var(--font-12);font-weight:var(--weight-medium);padding:0;text-decoration:underline;white-space:nowrap}.filter-complex__action--add{color:var(--color-primary)}.filter-complex__action--add:hover{color:var(--color-primary-hover)}.filter-complex__action--clear{color:var(--color-danger)}.filter-complex__list{display:flex;flex:1;flex-direction:column;overflow-y:auto}.filter-complex__list::-webkit-scrollbar{width:4px}.filter-complex__list::-webkit-scrollbar-thumb{background:var(--border-default);border-radius:100px}.filter-complex__list::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}.filter-complex__item{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);gap:10px;padding:7px 8px;text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out)}.filter-complex__item:hover{background-color:var(--bg-surface-3)}.filter-complex__item-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.filter-complex__item-icon{flex-shrink:0}.filter-complex__item-icon--add{color:var(--color-primary)}.filter-complex__item-icon--remove{color:var(--color-danger)}.filter-complex__empty{color:var(--text-muted);font-size:var(--font-14);font-style:italic;padding:8px}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
|
|
255
362
|
</style>
|
|
@@ -6,8 +6,20 @@ import type { Component } from 'vue'
|
|
|
6
6
|
export interface FilterDropdownOption {
|
|
7
7
|
label: string
|
|
8
8
|
value: string
|
|
9
|
+
/**
|
|
10
|
+
* Famille a laquelle l'option appartient (« Mensuel », « Trimestriel »).
|
|
11
|
+
* Les options d'une meme famille se suivent sous un intitule commun : une
|
|
12
|
+
* liste de douze periodes melangees oblige a lire chaque ligne pour trouver
|
|
13
|
+
* la maille voulue. Sans `group`, la liste reste plate.
|
|
14
|
+
*/
|
|
15
|
+
group?: string
|
|
9
16
|
}
|
|
10
17
|
|
|
18
|
+
/** Une ligne de la liste : un intitule de famille, ou une option. */
|
|
19
|
+
type DropdownRow =
|
|
20
|
+
| { kind: 'header', key: string, label: string }
|
|
21
|
+
| { kind: 'option', key: string, option: FilterDropdownOption }
|
|
22
|
+
|
|
11
23
|
const props = withDefaults(defineProps<{
|
|
12
24
|
icon?: Component
|
|
13
25
|
label: string
|
|
@@ -69,6 +81,26 @@ const filteredOptions = computed(() => {
|
|
|
69
81
|
return props.options.filter(o => o.label.toLowerCase().includes(needle))
|
|
70
82
|
})
|
|
71
83
|
|
|
84
|
+
/**
|
|
85
|
+
* La liste affichee : chaque changement de famille pose son intitule. Le
|
|
86
|
+
* regroupement suit l'ordre donne par l'appelant, il ne trie pas : c'est lui
|
|
87
|
+
* qui sait dans quel ordre ses mailles se lisent.
|
|
88
|
+
*/
|
|
89
|
+
const rows = computed<DropdownRow[]>(() => {
|
|
90
|
+
const out: DropdownRow[] = []
|
|
91
|
+
let current: string | undefined
|
|
92
|
+
|
|
93
|
+
filteredOptions.value.forEach((option) => {
|
|
94
|
+
if (option.group && option.group !== current) {
|
|
95
|
+
out.push({ kind: 'header', key: `group:${option.group}`, label: option.group })
|
|
96
|
+
}
|
|
97
|
+
current = option.group
|
|
98
|
+
out.push({ kind: 'option', key: option.value, option })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return out
|
|
102
|
+
})
|
|
103
|
+
|
|
72
104
|
function toggle() {
|
|
73
105
|
isOpen.value = !isOpen.value
|
|
74
106
|
}
|
|
@@ -168,23 +200,27 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
168
200
|
<p v-if="hasSearch && filteredOptions.length === 0" class="filter-chip__no-match">
|
|
169
201
|
Aucun résultat
|
|
170
202
|
</p>
|
|
171
|
-
<
|
|
172
|
-
v-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
multiple ? 'filter-chip__checkbox' : 'filter-chip__radio',
|
|
181
|
-
isSelected(option.value) && (multiple ? 'filter-chip__checkbox--checked' : 'filter-chip__radio--checked'),
|
|
182
|
-
]"
|
|
203
|
+
<template v-for="row in rows" :key="row.key">
|
|
204
|
+
<p v-if="row.kind === 'header'" class="filter-chip__section">
|
|
205
|
+
{{ row.label }}
|
|
206
|
+
</p>
|
|
207
|
+
<button
|
|
208
|
+
v-else
|
|
209
|
+
class="filter-chip__option"
|
|
210
|
+
:class="{ 'filter-chip__option--selected': isSelected(row.option.value) }"
|
|
211
|
+
@click="toggleOption(row.option.value)"
|
|
183
212
|
>
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
213
|
+
<span
|
|
214
|
+
:class="[
|
|
215
|
+
multiple ? 'filter-chip__checkbox' : 'filter-chip__radio',
|
|
216
|
+
isSelected(row.option.value) && (multiple ? 'filter-chip__checkbox--checked' : 'filter-chip__radio--checked'),
|
|
217
|
+
]"
|
|
218
|
+
>
|
|
219
|
+
<PhCheck v-if="multiple && isSelected(row.option.value)" :size="10" weight="bold" />
|
|
220
|
+
</span>
|
|
221
|
+
<span>{{ row.option.label }}</span>
|
|
222
|
+
</button>
|
|
223
|
+
</template>
|
|
188
224
|
</div>
|
|
189
225
|
</div>
|
|
190
226
|
</Transition>
|
|
@@ -192,5 +228,5 @@ onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
192
228
|
</template>
|
|
193
229
|
|
|
194
230
|
<style scoped>
|
|
195
|
-
.filter-chip-wrapper{position:relative}.filter-chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:8px;height:32px;padding:8px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--border-default)}.filter-chip__icon{color:var(--color-healthcare);flex-shrink:0}.filter-chip--active{border-color:var(--color-healthcare);box-shadow:0 0 8px 0 rgba(var(--healthcare-rgb),.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;flex-shrink:0;font-size:11px;font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:var(--color-text-inverse);border:1px solid var(--border-default);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:300px;max-width:420px;min-width:380px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);width:-moz-max-content;width:max-content;z-index:50}.filter-chip__search{align-items:center;background-color:var(--color-text-inverse);border-bottom:1px solid var(--border-soft);color:var(--text-muted);display:flex;gap:6px;margin:-4px -4px 4px;padding:8px;position:sticky;top:-4px;z-index:1}.filter-chip__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-14);min-width:0;outline:none}.filter-chip__search input::-webkit-search-cancel-button{cursor:pointer}.filter-chip__no-match{
|
|
231
|
+
.filter-chip-wrapper{position:relative}.filter-chip{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:6px;box-sizing:border-box;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:var(--weight-medium);gap:8px;height:32px;padding:8px;transition:all var(--duration-fast) var(--ease-in-out);white-space:nowrap;width:193px}.filter-chip:hover{border-color:var(--border-default)}.filter-chip__icon{color:var(--color-healthcare);flex-shrink:0}.filter-chip--active{border-color:var(--color-healthcare);box-shadow:0 0 8px 0 rgba(var(--healthcare-rgb),.1)}.filter-chip__label{flex:1;min-width:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap}.filter-chip__badge{align-items:center;background-color:var(--color-healthcare);border-radius:100px;color:var(--color-text-inverse);display:inline-flex;flex-shrink:0;font-size:11px;font-weight:600;height:18px;justify-content:center;line-height:1;min-width:18px;padding:0 5px}.filter-chip__caret{color:var(--text-muted);flex-shrink:0;transition:transform var(--duration-fast) var(--ease-in-out)}.filter-chip__caret--open{transform:rotate(180deg)}.filter-chip__dropdown{background-color:var(--color-text-inverse);border:1px solid var(--border-default);border-radius:6px;box-shadow:0 4px 12px rgba(var(--shadow-rgb),.08),0 2px 4px rgba(var(--shadow-rgb),.04);left:0;max-height:300px;max-width:420px;min-width:380px;overflow-y:auto;padding:4px;position:absolute;top:calc(100% + 4px);width:-moz-max-content;width:max-content;z-index:50}.filter-chip__search{align-items:center;background-color:var(--color-text-inverse);border-bottom:1px solid var(--border-soft);color:var(--text-muted);display:flex;gap:6px;margin:-4px -4px 4px;padding:8px;position:sticky;top:-4px;z-index:1}.filter-chip__search input{background:none;border:none;color:var(--text-primary);flex:1;font-family:var(--font-sans);font-size:var(--font-14);min-width:0;outline:none}.filter-chip__search input::-webkit-search-cancel-button{cursor:pointer}.filter-chip__no-match{font-size:var(--font-14);margin:0;padding:10px 8px}.filter-chip__no-match,.filter-chip__section{color:var(--text-muted);font-family:var(--font-sans)}.filter-chip__section{border-top:1px solid var(--border-default);font-size:var(--font-12);font-weight:var(--weight-bold);letter-spacing:.04em;margin:8px 0 2px;padding:8px 8px 0;text-transform:uppercase}.filter-chip__section:first-child{border-top:none;margin-top:0;padding-top:0}.filter-chip__option{align-items:center;background:none;border:none;border-radius:4px;color:var(--text-secondary);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);gap:8px;padding:6px 8px;text-align:left;transition:background-color var(--duration-fast) var(--ease-in-out);width:100%}.filter-chip__option:hover{background-color:var(--bg-surface-2)}.filter-chip__option--selected{color:var(--text-primary);font-weight:var(--weight-medium)}.filter-chip__checkbox{align-items:center;border:1.5px solid var(--border-default);border-radius:3px;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__checkbox--checked{background-color:var(--color-healthcare);border-color:var(--color-healthcare);color:var(--color-text-inverse)}.filter-chip__radio{align-items:center;border:1.5px solid var(--border-default);border-radius:100%;display:flex;flex-shrink:0;height:16px;justify-content:center;transition:all var(--duration-fast) var(--ease-in-out);width:16px}.filter-chip__radio--checked{border-color:var(--color-healthcare)}.filter-chip__radio--checked:after{background-color:var(--color-healthcare);border-radius:100%;content:"";height:8px;width:8px}.filter-chip__select-all{align-items:center;background:none;border:none;border-radius:4px;color:var(--color-healthcare);cursor:pointer;display:flex;font-family:var(--font-sans);font-size:var(--font-14);font-weight:500;gap:8px;padding:6px 8px;text-align:left;width:100%}.filter-chip__select-all:hover{background-color:rgba(var(--healthcare-rgb),.04)}.filter-chip__options-group{border-left:1px solid var(--border-default);margin-left:14px;padding-left:6px}.dropdown-enter-active,.dropdown-leave-active{transition:opacity var(--duration-fast) var(--ease-in-out),transform var(--duration-fast) var(--ease-in-out)}.dropdown-enter-from,.dropdown-leave-to{opacity:0;transform:translateY(-4px)}
|
|
196
232
|
</style>
|