@tekkare/romulus 0.1.51 → 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/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/package.json +1 -1
|
@@ -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>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, ref, toRef } from 'vue'
|
|
3
3
|
import { PhX } from '@phosphor-icons/vue'
|
|
4
|
+
import { useRmDialog } from '../composables/useRmDialog'
|
|
4
5
|
|
|
5
6
|
export interface RmModalProps {
|
|
6
7
|
/** Whether the modal is open */
|
|
@@ -9,11 +10,23 @@ export interface RmModalProps {
|
|
|
9
10
|
size?: 'sm' | 'md' | 'lg' | 'xl'
|
|
10
11
|
/** Mode: fixed (size is fixed) or free (adapts to content) */
|
|
11
12
|
mode?: 'fixed' | 'free'
|
|
13
|
+
/**
|
|
14
|
+
* Nom accessible de la modale. A renseigner des que l'en-tete est purement
|
|
15
|
+
* visuel : sans lui un lecteur d'ecran annonce « dialogue » sans dire lequel.
|
|
16
|
+
*/
|
|
17
|
+
label?: string
|
|
18
|
+
/** Id de l'element qui porte le titre visible, quand l'en-tete en a un. */
|
|
19
|
+
labelledBy?: string
|
|
20
|
+
/** Libelle du bouton de fermeture, pour les lecteurs d'ecran. */
|
|
21
|
+
closeLabel?: string
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
const props = withDefaults(defineProps<RmModalProps>(), {
|
|
15
25
|
size: 'md',
|
|
16
26
|
mode: 'fixed',
|
|
27
|
+
label: undefined,
|
|
28
|
+
labelledBy: undefined,
|
|
29
|
+
closeLabel: 'Fermer',
|
|
17
30
|
})
|
|
18
31
|
|
|
19
32
|
const emit = defineEmits<{
|
|
@@ -24,6 +37,9 @@ function close() {
|
|
|
24
37
|
emit('update:modelValue', false)
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
const surface = ref<HTMLElement | null>(null)
|
|
41
|
+
useRmDialog({ open: toRef(props, 'modelValue'), surface, close })
|
|
42
|
+
|
|
27
43
|
const sizeMap: Record<string, { width: string; height: string }> = {
|
|
28
44
|
sm: { width: '480px', height: '360px' },
|
|
29
45
|
md: { width: '640px', height: '500px' },
|
|
@@ -48,12 +64,21 @@ const modalStyle = computed(() => {
|
|
|
48
64
|
class="rm-modal-overlay"
|
|
49
65
|
@click.self="close"
|
|
50
66
|
>
|
|
51
|
-
<div
|
|
67
|
+
<div
|
|
68
|
+
ref="surface"
|
|
69
|
+
class="rm-modal"
|
|
70
|
+
:style="modalStyle"
|
|
71
|
+
role="dialog"
|
|
72
|
+
aria-modal="true"
|
|
73
|
+
:aria-label="labelledBy ? undefined : label"
|
|
74
|
+
:aria-labelledby="labelledBy"
|
|
75
|
+
tabindex="-1"
|
|
76
|
+
>
|
|
52
77
|
<div class="rm-modal__header">
|
|
53
78
|
<div class="rm-modal__header-content">
|
|
54
79
|
<slot name="header" />
|
|
55
80
|
</div>
|
|
56
|
-
<button class="rm-modal__close" @click="close">
|
|
81
|
+
<button class="rm-modal__close" type="button" :aria-label="closeLabel" @click="close">
|
|
57
82
|
<PhX :size="20" />
|
|
58
83
|
</button>
|
|
59
84
|
</div>
|
|
@@ -76,5 +101,5 @@ const modalStyle = computed(() => {
|
|
|
76
101
|
</template>
|
|
77
102
|
|
|
78
103
|
<style scoped>
|
|
79
|
-
.rm-modal-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(0,0,0,.4);display:flex;inset:0;justify-content:center;position:fixed;z-index:100}.rm-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.15);display:flex;flex-direction:column;max-height:90vh;max-width:95vw}.rm-modal__header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between;padding:20px 24px 0}.rm-modal__header-content{flex:1;min-width:0}.rm-modal__close{align-items:center;background:none;border:none;border-radius:var(--r-radius-md);color:var(--r-color-gray-400);cursor:pointer;display:flex;flex-shrink:0;height:32px;justify-content:center;transition:all var(--r-transition-fast);width:32px}.rm-modal__close:hover{background:var(--r-color-gray-50);color:var(--r-color-gray-700)}.rm-modal__tabs{padding:0}.rm-modal__body{flex:1;overflow-y:auto;padding:24px}.rm-modal__footer{border-top:1px solid var(--r-color-gray-200);padding:16px 24px}.rm-modal-enter-active,.rm-modal-leave-active{transition:opacity .2s ease}.rm-modal-enter-active .rm-modal,.rm-modal-leave-active .rm-modal{transition:transform .2s ease}.rm-modal-enter-from,.rm-modal-leave-to{opacity:0}.rm-modal-enter-from .rm-modal,.rm-modal-leave-to .rm-modal{transform:scale(.95)}
|
|
104
|
+
.rm-modal-overlay{align-items:center;backdrop-filter:blur(2px);background:rgba(0,0,0,.4);display:flex;inset:0;justify-content:center;position:fixed;z-index:100}.rm-modal{background:#fff;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.15);display:flex;flex-direction:column;max-height:90vh;max-width:95vw}.rm-modal__header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between;padding:20px 24px 0}.rm-modal__header-content{flex:1;min-width:0}.rm-modal__close{align-items:center;background:none;border:none;border-radius:var(--r-radius-md);color:var(--r-color-gray-400);cursor:pointer;display:flex;flex-shrink:0;height:32px;justify-content:center;transition:all var(--r-transition-fast);width:32px}.rm-modal__close:hover{background:var(--r-color-gray-50);color:var(--r-color-gray-700)}.rm-modal__tabs{padding:0}.rm-modal__body{flex:1;overflow-y:auto;padding:24px}.rm-modal__footer{border-top:1px solid var(--r-color-gray-200);padding:16px 24px}.rm-modal-enter-active,.rm-modal-leave-active{transition:opacity .2s ease}.rm-modal-enter-active .rm-modal,.rm-modal-leave-active .rm-modal{transition:transform .2s ease}.rm-modal-enter-from,.rm-modal-leave-to{opacity:0}.rm-modal-enter-from .rm-modal,.rm-modal-leave-to .rm-modal{transform:scale(.95)}.rm-modal-leave-active,.rm-modal-leave-to{pointer-events:none}
|
|
80
105
|
</style>
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
.rm-rcard__desc description 14px, gris
|
|
31
31
|
-->
|
|
32
32
|
<script setup lang="ts">
|
|
33
|
+
import type { RmExportSheet } from '../utils/export'
|
|
34
|
+
|
|
33
35
|
withDefaults(
|
|
34
36
|
defineProps<{
|
|
35
37
|
/** Le chiffre du compteur. Sans valeur, la colonne de gauche est omise. */
|
|
@@ -42,8 +44,38 @@ withDefaults(
|
|
|
42
44
|
moreLabel?: string
|
|
43
45
|
/** Curseur main et surbrillance au survol : la carte est un lien. */
|
|
44
46
|
clickable?: boolean
|
|
47
|
+
/**
|
|
48
|
+
* Menu d'actions en haut a droite (exporter, partager). Les items visuels
|
|
49
|
+
* (PNG, SVG, plein ecran) n'y sont pas : une carte de resultat ne dessine
|
|
50
|
+
* rien, elle decrit une ligne.
|
|
51
|
+
*/
|
|
52
|
+
actions?: boolean
|
|
53
|
+
/**
|
|
54
|
+
* Lien copie par « Copier le lien direct ». Pour une carte de resultat,
|
|
55
|
+
* c'est l'adresse de la FICHE, pas celle de la liste : on partage un
|
|
56
|
+
* produit, pas une recherche. A defaut, l'URL courante.
|
|
57
|
+
*/
|
|
58
|
+
link?: string
|
|
59
|
+
/** Langue du menu d'actions. Le contenu projete garde la sienne. */
|
|
60
|
+
lang?: 'fr' | 'en'
|
|
61
|
+
/**
|
|
62
|
+
* Onglets d'export fournis par la page, quand elle a mieux que le rendu de
|
|
63
|
+
* la carte (les cinq avis derriere le compteur). A defaut, les champs lus
|
|
64
|
+
* dans la carte.
|
|
65
|
+
*/
|
|
66
|
+
sheets?: RmExportSheet[]
|
|
45
67
|
}>(),
|
|
46
|
-
{
|
|
68
|
+
{
|
|
69
|
+
count: null,
|
|
70
|
+
countLabel: '',
|
|
71
|
+
countExtra: '',
|
|
72
|
+
moreLabel: '',
|
|
73
|
+
clickable: false,
|
|
74
|
+
actions: true,
|
|
75
|
+
link: undefined,
|
|
76
|
+
lang: 'fr',
|
|
77
|
+
sheets: undefined,
|
|
78
|
+
},
|
|
47
79
|
)
|
|
48
80
|
</script>
|
|
49
81
|
|
|
@@ -54,7 +86,16 @@ withDefaults(
|
|
|
54
86
|
<h3 class="rm-rcard__title"><slot name="title" /></h3>
|
|
55
87
|
<div v-if="$slots.subtitle" class="rm-rcard__subtitle"><slot name="subtitle" /></div>
|
|
56
88
|
</div>
|
|
57
|
-
<div v-if="$slots.actions" class="rm-rcard__actions"
|
|
89
|
+
<div v-if="actions || $slots.actions" class="rm-rcard__actions">
|
|
90
|
+
<slot name="actions" />
|
|
91
|
+
<RmCardActions
|
|
92
|
+
v-if="actions"
|
|
93
|
+
:lang="lang"
|
|
94
|
+
:link="link"
|
|
95
|
+
:sheets="sheets"
|
|
96
|
+
:visual="false"
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
58
99
|
</div>
|
|
59
100
|
|
|
60
101
|
<div class="rm-rcard__body">
|
|
@@ -76,5 +117,5 @@ withDefaults(
|
|
|
76
117
|
</template>
|
|
77
118
|
|
|
78
119
|
<style scoped>
|
|
79
|
-
.rm-rcard{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);color:var(--text-primary);display:block;font-family:var(--font-sans);overflow:hidden;transition:border-color var(--duration-fast) linear,box-shadow var(--duration-fast) linear;width:100%}.rm-rcard--clickable{cursor:pointer}.rm-rcard--clickable:hover{border-color:color-mix(in srgb,var(--color-primary) 35%,var(--border-default));box-shadow:var(--shadow-md)}.rm-rcard__head{align-items:flex-start;display:flex;gap:16px;padding:20px 24px 0}.rm-rcard__identity{flex:1 1 auto;min-width:0}.rm-rcard__title{font-size:var(--font-18);font-weight:800;letter-spacing:.01em;margin:0}.rm-rcard__subtitle{color:var(--text-muted);font-size:var(--font-14);margin-top:4px;overflow-wrap:anywhere}.rm-rcard__actions{flex:0 0 auto}.rm-rcard__body{align-items:stretch;display:flex;gap:24px;padding:16px 24px 20px}.rm-rcard__count{align-items:center;border-right:1px solid var(--border-default);display:flex;flex:0 0 128px;flex-direction:column;gap:2px;justify-content:center;padding-right:24px;text-align:center}.rm-rcard__count-value{color:var(--color-primary);font-size:var(--font-32);font-weight:var(--weight-bold);line-height:1.1;margin:0}.rm-rcard__count-label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-rcard__count-extra{color:var(--color-primary);font-size:var(--font-13);line-height:1.3;margin:6px 0 0}.rm-rcard__main{display:flex;flex:1 1 auto;flex-direction:column;gap:6px;min-width:0}.rm-rcard__meta{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.rm-rcard__more{color:var(--color-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:4px 0 0}.rm-rcard__footer{background:var(--bg-surface-2);border-top:1px solid var(--border-default);font-size:var(--font-13);padding:12px 24px}.rm-rcard :deep(.rm-rcard__label){align-items:center;color:var(--text-secondary);display:inline-flex;font-size:var(--font-12);font-weight:var(--weight-bold);gap:4px;letter-spacing:.06em;text-transform:uppercase}.rm-rcard :deep(.rm-rcard__ref){background:var(--bg-surface-3);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:Courier New,monospace;font-size:var(--font-12);padding:2px 8px}.rm-rcard :deep(.rm-rcard__lead){font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-rcard :deep(.rm-rcard__text){font-size:15px;margin:0}.rm-rcard :deep(.rm-rcard__desc){color:var(--text-secondary);font-size:var(--font-14);line-height:1.5;margin:0}.rm-rcard :deep(.rm-rcard__clamp){display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
120
|
+
.rm-rcard{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);color:var(--text-primary);display:block;font-family:var(--font-sans);overflow:hidden;transition:border-color var(--duration-fast) linear,box-shadow var(--duration-fast) linear;width:100%}.rm-rcard--clickable{cursor:pointer}.rm-rcard--clickable:hover{border-color:color-mix(in srgb,var(--color-primary) 35%,var(--border-default));box-shadow:var(--shadow-md)}.rm-rcard__head{align-items:flex-start;display:flex;gap:16px;padding:20px 24px 0}.rm-rcard__identity{flex:1 1 auto;min-width:0}.rm-rcard__title{font-size:var(--font-18);font-weight:800;letter-spacing:.01em;margin:0}.rm-rcard__subtitle{color:var(--text-muted);font-size:var(--font-14);margin-top:4px;overflow-wrap:anywhere}.rm-rcard__actions{align-items:center;display:flex;flex:0 0 auto;gap:8px}.rm-rcard__body{align-items:stretch;display:flex;gap:24px;padding:16px 24px 20px}.rm-rcard__count{align-items:center;border-right:1px solid var(--border-default);display:flex;flex:0 0 128px;flex-direction:column;gap:2px;justify-content:center;padding-right:24px;text-align:center}.rm-rcard__count-value{color:var(--color-primary);font-size:var(--font-32);font-weight:var(--weight-bold);line-height:1.1;margin:0}.rm-rcard__count-label{color:var(--text-muted);font-size:var(--font-12);font-weight:var(--weight-semibold);letter-spacing:.08em;margin:0;text-transform:uppercase}.rm-rcard__count-extra{color:var(--color-primary);font-size:var(--font-13);line-height:1.3;margin:6px 0 0}.rm-rcard__main{display:flex;flex:1 1 auto;flex-direction:column;gap:6px;min-width:0}.rm-rcard__meta{align-items:center;display:flex;flex-wrap:wrap;gap:8px}.rm-rcard__more{color:var(--color-primary);font-size:var(--font-14);font-weight:var(--weight-semibold);margin:4px 0 0}.rm-rcard__footer{background:var(--bg-surface-2);border-top:1px solid var(--border-default);font-size:var(--font-13);padding:12px 24px}.rm-rcard :deep(.rm-rcard__label){align-items:center;color:var(--text-secondary);display:inline-flex;font-size:var(--font-12);font-weight:var(--weight-bold);gap:4px;letter-spacing:.06em;text-transform:uppercase}.rm-rcard :deep(.rm-rcard__ref){background:var(--bg-surface-3);border-radius:var(--radius-sm);color:var(--text-secondary);font-family:Courier New,monospace;font-size:var(--font-12);padding:2px 8px}.rm-rcard :deep(.rm-rcard__lead){font-size:15px;font-weight:var(--weight-bold);margin:0}.rm-rcard :deep(.rm-rcard__text){font-size:15px;margin:0}.rm-rcard :deep(.rm-rcard__desc){color:var(--text-secondary);font-size:var(--font-14);line-height:1.5;margin:0}.rm-rcard :deep(.rm-rcard__clamp){display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
80
121
|
</style>
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
avec une boite d'icone de 22px.
|
|
9
9
|
|
|
10
10
|
Un ecart assume avec oip7 : le libelle de section est un bouton, et les
|
|
11
|
-
groupes forment un accordeon exclusif
|
|
11
|
+
groupes forment un accordeon exclusif — barre depliee seulement, puisque
|
|
12
|
+
replie il n'y a plus de libelle a plier. La barre du portail liste dix entrees,
|
|
12
13
|
celle d'une app metier en liste vingt et plus ; toutes deployees, la
|
|
13
14
|
navigation devient un long defilement. Le style du libelle, lui, ne change
|
|
14
15
|
pas. Un groupe sans `label` reste toujours ouvert, comme le premier groupe
|
|
@@ -121,7 +122,21 @@ watch(() => sectionOf(props.activePath), (key) => {
|
|
|
121
122
|
})
|
|
122
123
|
|
|
123
124
|
const isCollapsible = (s: RmNavSection) => !!s.label && s.collapsible !== false
|
|
124
|
-
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Groupe deploye.
|
|
128
|
+
*
|
|
129
|
+
* Repliee, la barre n'a plus de libelle a plier : il cede la place au filet
|
|
130
|
+
* (cf. .rm-sb__rule dans le gabarit). L'accordeon continuait pourtant de
|
|
131
|
+
* fermer les autres groupes, dont les entrees quittaient le DOM : le rail ne
|
|
132
|
+
* montrait que le groupe de la page courante, quelques filets nus en dessous,
|
|
133
|
+
* et aucun moyen d'atteindre le reste de la navigation ni de le deplier. Tous
|
|
134
|
+
* les groupes sont donc ouverts en mode replie — ce que l'accordeon evite est
|
|
135
|
+
* un long defilement de groupes libelles, or une entree repliee ne fait
|
|
136
|
+
* qu'une pastille de 26px.
|
|
137
|
+
*/
|
|
138
|
+
const isOpen = (s: RmNavSection) =>
|
|
139
|
+
!isCollapsible(s) || props.collapsed || openSection.value === s.key
|
|
125
140
|
const isActive = (item: RmNavItem) => item.active ?? (!!item.to && item.to === props.activePath)
|
|
126
141
|
</script>
|
|
127
142
|
|
|
@@ -165,7 +180,10 @@ const isActive = (item: RmNavItem) => item.active ?? (!!item.to && item.to === p
|
|
|
165
180
|
<div v-for="section in sections" :key="section.key" class="rm-sb__group">
|
|
166
181
|
<template v-if="section.label">
|
|
167
182
|
<!-- Replie, le libelle cede la place au filet d'oip7 : trois lettres
|
|
168
|
-
capitales dans un rail de 64px seraient illisibles.
|
|
183
|
+
capitales dans un rail de 64px seraient illisibles. Le groupe
|
|
184
|
+
reste ouvert (cf. isOpen) : sans libelle il n'y a rien a plier,
|
|
185
|
+
et le fermer rendait ses pages inatteignables. Chaque entree
|
|
186
|
+
porte son `title`, le survol donne le nom. -->
|
|
169
187
|
<div v-if="collapsed" class="rm-sb__rule" aria-hidden="true" />
|
|
170
188
|
<button
|
|
171
189
|
v-else-if="isCollapsible(section)"
|
|
@@ -8,15 +8,41 @@
|
|
|
8
8
|
Slots : #header (titre, source, compte), default (le tableau et sa
|
|
9
9
|
pagination).
|
|
10
10
|
-->
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import type { RmExportSheet } from '../utils/export'
|
|
13
|
+
|
|
14
|
+
withDefaults(
|
|
15
|
+
defineProps<{
|
|
16
|
+
/**
|
|
17
|
+
* Menu d'actions en haut a droite (exporter, partager). Les items visuels
|
|
18
|
+
* ne s'affichent que si la carte porte un graphe : un tableau seul n'a ni
|
|
19
|
+
* PNG ni plein ecran a offrir.
|
|
20
|
+
*/
|
|
21
|
+
actions?: boolean
|
|
22
|
+
/** Lien copie par « Copier le lien direct ». A defaut, l'URL courante. */
|
|
23
|
+
link?: string
|
|
24
|
+
/** Langue du menu d'actions. Le contenu projete garde la sienne. */
|
|
25
|
+
lang?: 'fr' | 'en'
|
|
26
|
+
/**
|
|
27
|
+
* Onglets d'export fournis par la page, quand elle a mieux que le rendu :
|
|
28
|
+
* la donnee complete plutot que la page affichee du tableau.
|
|
29
|
+
*/
|
|
30
|
+
sheets?: RmExportSheet[]
|
|
31
|
+
}>(),
|
|
32
|
+
{ actions: true, link: undefined, lang: 'fr', sheets: undefined },
|
|
33
|
+
)
|
|
34
|
+
</script>
|
|
35
|
+
|
|
11
36
|
<template>
|
|
12
37
|
<section class="rm-table-card">
|
|
13
|
-
<div v-if="$slots.header" class="rm-table-card__header">
|
|
38
|
+
<div v-if="$slots.header || actions" class="rm-table-card__header">
|
|
14
39
|
<slot name="header" />
|
|
40
|
+
<RmCardActions v-if="actions" :lang="lang" :link="link" :sheets="sheets" />
|
|
15
41
|
</div>
|
|
16
42
|
<slot />
|
|
17
43
|
</section>
|
|
18
44
|
</template>
|
|
19
45
|
|
|
20
46
|
<style scoped>
|
|
21
|
-
.rm-table-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-xs);box-sizing:border-box;color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);overflow:hidden;padding:0;width:100%}.rm-table-card__header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between;padding:24px}.rm-table-card__header :deep(.rm-table-card__title){font-size:var(--font-18);font-weight:800;margin:0}.rm-table-card__header :deep(.rm-table-card__caption){color:var(--text-secondary);font-size:var(--font-12);margin:0;white-space:nowrap}.rm-table-card :deep(.rm-table-wrapper){background:transparent;border:none;border-radius:0}.rm-table-card__header+:deep(.rm-table-wrapper){border-top:1px solid var(--border-default)}.rm-table-card>:deep(:not(.rm-table-wrapper):not(.rm-table-card__header)){padding:16px 24px}
|
|
47
|
+
.rm-table-card{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-xs);box-sizing:border-box;color:var(--text-primary);display:flex;flex-direction:column;font-family:var(--font-sans);overflow:hidden;padding:0;width:100%}.rm-table-card__header{align-items:flex-start;display:flex;gap:16px;justify-content:space-between;padding:24px}.rm-table-card__header>:deep(.rm-card-actions){margin-left:auto}.rm-table-card__header :deep(.rm-table-card__title){font-size:var(--font-18);font-weight:800;margin:0}.rm-table-card__header :deep(.rm-table-card__caption){color:var(--text-secondary);font-size:var(--font-12);margin:0;white-space:nowrap}.rm-table-card :deep(.rm-table-wrapper){background:transparent;border:none;border-radius:0}.rm-table-card__header+:deep(.rm-table-wrapper){border-top:1px solid var(--border-default)}.rm-table-card>:deep(:not(.rm-table-wrapper):not(.rm-table-card__header)){padding:16px 24px}
|
|
22
48
|
</style>
|