@tekkare/romulus 0.1.66 → 0.1.68
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/runtime/components/AccordionMenu.vue +229 -0
- package/dist/runtime/components/AppSidebar.vue +126 -0
- package/dist/runtime/components/FilterTabs.vue +71 -0
- package/dist/runtime/components/MenuItem.vue +78 -0
- package/dist/runtime/components/MenuLink.vue +68 -0
- package/dist/runtime/components/ProductCard.vue +71 -0
- package/dist/runtime/components/SectionCard.vue +57 -0
- package/dist/runtime/components/SubMenu.vue +69 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmAccordionMenu - menu de barre laterale en sections depliables.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-accordion-menu` d'Auriga a l'identique : sections repliables,
|
|
5
|
+
sous-sections sur fond voile, pastille de donnee a droite des entrees, et
|
|
6
|
+
`singleOpen` qui referme les autres sections quand on en ouvre une.
|
|
7
|
+
|
|
8
|
+
Trois choses changent. Les en-tetes de section sont des `<button
|
|
9
|
+
aria-expanded>` : les `<div @click>` d'Auriga n'etaient ni atteignables au
|
|
10
|
+
clavier ni annonces comme repliables.
|
|
11
|
+
|
|
12
|
+
La pastille de donnee porte un intitule lu a voix haute. En couleur seule,
|
|
13
|
+
elle ne disait rien a qui ne distingue pas le vert du gris.
|
|
14
|
+
|
|
15
|
+
Les couleurs en dur (`#ececf2`, `#1a1a2e`, `#2017ad`) passent aux tokens.
|
|
16
|
+
-->
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { reactive, watch } from 'vue'
|
|
19
|
+
|
|
20
|
+
export interface RmAccordionItem {
|
|
21
|
+
label: string
|
|
22
|
+
/** Icone Phosphor sans le prefixe "Ph". */
|
|
23
|
+
icon?: string
|
|
24
|
+
/** Cible de navigation. Sans elle, l'entree emet `navigate` et rien d'autre. */
|
|
25
|
+
to?: string
|
|
26
|
+
/**
|
|
27
|
+
* Pastille a droite : verte si la page porte des donnees, grise sinon.
|
|
28
|
+
* Absente, aucune pastille.
|
|
29
|
+
*/
|
|
30
|
+
haveData?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RmAccordionSubSection {
|
|
34
|
+
label: string
|
|
35
|
+
items: RmAccordionItem[]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RmAccordionSection {
|
|
39
|
+
title: string
|
|
40
|
+
items?: RmAccordionItem[]
|
|
41
|
+
subSections?: RmAccordionSubSection[]
|
|
42
|
+
/** Section ouverte au montage. Sans precision, seule la premiere l'est. */
|
|
43
|
+
defaultOpen?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const props = withDefaults(defineProps<{
|
|
47
|
+
menu: RmAccordionSection[]
|
|
48
|
+
/** Ouvrir une section referme les autres. */
|
|
49
|
+
singleOpen?: boolean
|
|
50
|
+
/** Intitules des pastilles de donnee, lus a voix haute. */
|
|
51
|
+
dataLabel?: string
|
|
52
|
+
noDataLabel?: string
|
|
53
|
+
}>(), {
|
|
54
|
+
singleOpen: true,
|
|
55
|
+
dataLabel: 'Données disponibles',
|
|
56
|
+
noDataLabel: 'Aucune donnée',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const emit = defineEmits<{ navigate: [item: RmAccordionItem] }>()
|
|
60
|
+
|
|
61
|
+
const openSections = reactive<Record<number, boolean>>({})
|
|
62
|
+
const openSubSections = reactive<Record<string, boolean>>({})
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Les sous-sections partent ouvertes : elles ne portent que quelques entrees,
|
|
66
|
+
* les replier ferait un second geste pour rien.
|
|
67
|
+
*/
|
|
68
|
+
function initSections(menu: RmAccordionSection[]) {
|
|
69
|
+
for (const key of Object.keys(openSections)) delete openSections[Number(key)]
|
|
70
|
+
for (const key of Object.keys(openSubSections)) delete openSubSections[key]
|
|
71
|
+
|
|
72
|
+
menu.forEach((section, index) => {
|
|
73
|
+
openSections[index] = section.defaultOpen ?? index === 0
|
|
74
|
+
section.subSections?.forEach((_sub, subIndex) => {
|
|
75
|
+
openSubSections[`${index}-${subIndex}`] = true
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
initSections(props.menu)
|
|
81
|
+
watch(() => props.menu, menu => initSections(menu), { deep: true })
|
|
82
|
+
|
|
83
|
+
function toggleSection(index: number) {
|
|
84
|
+
if (!props.singleOpen) {
|
|
85
|
+
openSections[index] = !openSections[index]
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
const wasOpen = openSections[index]
|
|
89
|
+
for (const key of Object.keys(openSections)) openSections[Number(key)] = false
|
|
90
|
+
openSections[index] = !wasOpen
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function toggleSub(sectionIndex: number, subIndex: number) {
|
|
94
|
+
const key = `${sectionIndex}-${subIndex}`
|
|
95
|
+
openSubSections[key] = !openSubSections[key]
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<template>
|
|
100
|
+
<div class="rm-accordion">
|
|
101
|
+
<div v-for="(section, s) in menu" :key="s" class="rm-accordion__section">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="rm-accordion__head"
|
|
105
|
+
:aria-expanded="!!openSections[s]"
|
|
106
|
+
@click="toggleSection(s)"
|
|
107
|
+
>
|
|
108
|
+
<span class="rm-accordion__title">{{ section.title }}</span>
|
|
109
|
+
<span class="rm-accordion__caret" :class="{ 'is-open': openSections[s] }">
|
|
110
|
+
<RmIcon name="CaretDown" :size="16" />
|
|
111
|
+
</span>
|
|
112
|
+
</button>
|
|
113
|
+
|
|
114
|
+
<div class="rm-accordion__body" :class="{ 'is-open': openSections[s] }">
|
|
115
|
+
<!-- Entrees posees directement sous la section -->
|
|
116
|
+
<template v-for="(item, i) in section.items" :key="`i-${i}`">
|
|
117
|
+
<NuxtLink
|
|
118
|
+
v-if="item.to"
|
|
119
|
+
:to="item.to"
|
|
120
|
+
class="rm-accordion__item"
|
|
121
|
+
active-class="is-active"
|
|
122
|
+
@click="emit('navigate', item)"
|
|
123
|
+
>
|
|
124
|
+
<span class="rm-accordion__item-left">
|
|
125
|
+
<RmIcon v-if="item.icon" :name="item.icon" :size="20" />
|
|
126
|
+
<span>{{ item.label }}</span>
|
|
127
|
+
</span>
|
|
128
|
+
<span
|
|
129
|
+
v-if="item.haveData !== undefined"
|
|
130
|
+
class="rm-accordion__dot"
|
|
131
|
+
:class="item.haveData ? 'has-data' : 'no-data'"
|
|
132
|
+
:title="item.haveData ? dataLabel : noDataLabel"
|
|
133
|
+
:aria-label="item.haveData ? dataLabel : noDataLabel"
|
|
134
|
+
role="img"
|
|
135
|
+
/>
|
|
136
|
+
</NuxtLink>
|
|
137
|
+
<button
|
|
138
|
+
v-else
|
|
139
|
+
type="button"
|
|
140
|
+
class="rm-accordion__item"
|
|
141
|
+
@click="emit('navigate', item)"
|
|
142
|
+
>
|
|
143
|
+
<span class="rm-accordion__item-left">
|
|
144
|
+
<RmIcon v-if="item.icon" :name="item.icon" :size="20" />
|
|
145
|
+
<span>{{ item.label }}</span>
|
|
146
|
+
</span>
|
|
147
|
+
<span
|
|
148
|
+
v-if="item.haveData !== undefined"
|
|
149
|
+
class="rm-accordion__dot"
|
|
150
|
+
:class="item.haveData ? 'has-data' : 'no-data'"
|
|
151
|
+
:title="item.haveData ? dataLabel : noDataLabel"
|
|
152
|
+
:aria-label="item.haveData ? dataLabel : noDataLabel"
|
|
153
|
+
role="img"
|
|
154
|
+
/>
|
|
155
|
+
</button>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<!-- Sous-sections -->
|
|
159
|
+
<div v-for="(sub, sb) in section.subSections" :key="`s-${sb}`" class="rm-accordion__sub">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="rm-accordion__sub-head"
|
|
163
|
+
:aria-expanded="!!openSubSections[`${s}-${sb}`]"
|
|
164
|
+
@click="toggleSub(s, sb)"
|
|
165
|
+
>
|
|
166
|
+
<span class="rm-accordion__sub-label">{{ sub.label }}</span>
|
|
167
|
+
<span
|
|
168
|
+
class="rm-accordion__caret rm-accordion__caret--sub"
|
|
169
|
+
:class="{ 'is-open': openSubSections[`${s}-${sb}`] }"
|
|
170
|
+
>
|
|
171
|
+
<RmIcon name="CaretDown" :size="12" />
|
|
172
|
+
</span>
|
|
173
|
+
</button>
|
|
174
|
+
|
|
175
|
+
<div
|
|
176
|
+
class="rm-accordion__sub-body"
|
|
177
|
+
:class="{ 'is-open': openSubSections[`${s}-${sb}`] }"
|
|
178
|
+
>
|
|
179
|
+
<template v-for="(item, i) in sub.items" :key="`si-${i}`">
|
|
180
|
+
<NuxtLink
|
|
181
|
+
v-if="item.to"
|
|
182
|
+
:to="item.to"
|
|
183
|
+
class="rm-accordion__item"
|
|
184
|
+
active-class="is-active"
|
|
185
|
+
@click="emit('navigate', item)"
|
|
186
|
+
>
|
|
187
|
+
<span class="rm-accordion__item-left">
|
|
188
|
+
<RmIcon v-if="item.icon" :name="item.icon" :size="20" />
|
|
189
|
+
<span>{{ item.label }}</span>
|
|
190
|
+
</span>
|
|
191
|
+
<span
|
|
192
|
+
v-if="item.haveData !== undefined"
|
|
193
|
+
class="rm-accordion__dot"
|
|
194
|
+
:class="item.haveData ? 'has-data' : 'no-data'"
|
|
195
|
+
:title="item.haveData ? dataLabel : noDataLabel"
|
|
196
|
+
:aria-label="item.haveData ? dataLabel : noDataLabel"
|
|
197
|
+
role="img"
|
|
198
|
+
/>
|
|
199
|
+
</NuxtLink>
|
|
200
|
+
<button
|
|
201
|
+
v-else
|
|
202
|
+
type="button"
|
|
203
|
+
class="rm-accordion__item"
|
|
204
|
+
@click="emit('navigate', item)"
|
|
205
|
+
>
|
|
206
|
+
<span class="rm-accordion__item-left">
|
|
207
|
+
<RmIcon v-if="item.icon" :name="item.icon" :size="20" />
|
|
208
|
+
<span>{{ item.label }}</span>
|
|
209
|
+
</span>
|
|
210
|
+
<span
|
|
211
|
+
v-if="item.haveData !== undefined"
|
|
212
|
+
class="rm-accordion__dot"
|
|
213
|
+
:class="item.haveData ? 'has-data' : 'no-data'"
|
|
214
|
+
:title="item.haveData ? dataLabel : noDataLabel"
|
|
215
|
+
:aria-label="item.haveData ? dataLabel : noDataLabel"
|
|
216
|
+
role="img"
|
|
217
|
+
/>
|
|
218
|
+
</button>
|
|
219
|
+
</template>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</template>
|
|
226
|
+
|
|
227
|
+
<style scoped>
|
|
228
|
+
.rm-accordion{display:flex;flex-direction:column;width:100%}.rm-accordion__section{border-bottom:1px solid var(--border-default);margin-bottom:4px;padding-bottom:8px}.rm-accordion__section:last-child{border-bottom:none}.rm-accordion__head,.rm-accordion__sub-head{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);cursor:pointer;display:flex;font-family:inherit;justify-content:space-between;transition:background-color .2s ease;width:100%}.rm-accordion__head{padding:8px}.rm-accordion__head:hover,.rm-accordion__sub-head:hover{background:var(--bg-hover)}.rm-accordion__title{color:var(--text-primary);font-size:var(--font-13,13px);font-weight:700;line-height:normal}.rm-accordion__caret{align-items:center;color:var(--text-muted);display:flex;transition:transform .2s ease}.rm-accordion__caret.is-open{transform:rotate(180deg)}.rm-accordion__body{max-height:0;overflow:hidden;padding-left:12px;padding-right:8px;transition:max-height .3s ease}.rm-accordion__body.is-open{margin-top:4px;max-height:2000px}.rm-accordion__item{align-items:center;align-self:stretch;background:none;border:none;border-radius:var(--radius-sm,4px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:400;gap:8px;justify-content:space-between;line-height:1.6;padding:8px;text-align:left;text-decoration:none;width:100%}.rm-accordion__item:hover:not(.is-active){background:var(--bg-hover)}.rm-accordion__item.is-active{background:rgba(var(--brand-rgb),.1)}.rm-accordion__item :deep(svg){color:var(--text-secondary)}.rm-accordion__item.is-active :deep(svg),.rm-accordion__item:hover:not(.is-active) :deep(svg){color:var(--color-primary)}.rm-accordion__item-left{align-items:center;display:flex;gap:8px}.rm-accordion__dot{border-radius:50%;flex-shrink:0;height:10px;width:10px}.rm-accordion__dot.has-data{background:var(--color-success)}.rm-accordion__dot.no-data{background:var(--r-color-gray-300)}.rm-accordion__sub{margin-bottom:8px}.rm-accordion__sub-head{padding:4px 8px}.rm-accordion__sub-label{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:700}.rm-accordion__sub-body{background:var(--bg-surface-2);border-radius:var(--radius-md,8px);margin-top:4px;max-height:0;overflow:hidden;padding:0 8px;transition:max-height .2s ease,padding .2s ease}.rm-accordion__sub-body.is-open{max-height:300px;padding:8px}.rm-accordion__head:focus-visible,.rm-accordion__item:focus-visible,.rm-accordion__sub-head:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}
|
|
229
|
+
</style>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmAppSidebar - barre laterale repliable, composee en slots.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-sidebar` d'Auriga : 200px depliee, 54px repliee, bouton retour
|
|
5
|
+
a l'accueil, titre de l'app, corps defilant (slot `menu`) et pied colle en bas
|
|
6
|
+
(slot `footer`). Se replie seule sous 1000px de large.
|
|
7
|
+
|
|
8
|
+
Laquelle des barres du paquet prendre :
|
|
9
|
+
- celle-ci quand la page compose son menu en balises (`RmMenuItem`,
|
|
10
|
+
`RmSubMenu`) et doit glisser ses propres blocs entre les entrees ;
|
|
11
|
+
- `RmSidebar` quand le menu tient dans une simple liste d'entrees ;
|
|
12
|
+
- `RmSidebarLeft` pour le chrome du portail, decrit par une prop `sections` ;
|
|
13
|
+
- `RmSidebarRight` pour le rail d'icones.
|
|
14
|
+
|
|
15
|
+
Trois choses changent par rapport a Auriga. Les libelles du chrome sont dans
|
|
16
|
+
le composant plutot que charges depuis des JSON de locale : un composant qui
|
|
17
|
+
va chercher un fichier au montage affiche une bulle vide le temps du premier
|
|
18
|
+
rendu.
|
|
19
|
+
|
|
20
|
+
Le retour a l'accueil est un vrai `<a href>` : le `window.location.href`
|
|
21
|
+
d'Auriga privait du clic milieu et du survol qui montre la cible.
|
|
22
|
+
|
|
23
|
+
L'etat replie est un `v-model`, si bien qu'une page peut le poser elle-meme.
|
|
24
|
+
-->
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
import { computed, onMounted, onUnmounted } from 'vue'
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<{
|
|
29
|
+
/** Nom de l'app, en titre. Absent, le slot `title` prend la place. */
|
|
30
|
+
name?: string
|
|
31
|
+
/** Code ISO 3166-1 alpha-2 pose devant le nom. */
|
|
32
|
+
appIso?: string
|
|
33
|
+
/** Cible du bouton accueil. `null` retire le bouton. */
|
|
34
|
+
homeLink?: string | null
|
|
35
|
+
lang?: 'fr' | 'en'
|
|
36
|
+
/** Largeur en dessous de laquelle la barre se replie seule. */
|
|
37
|
+
breakpoint?: number
|
|
38
|
+
}>(), {
|
|
39
|
+
name: '',
|
|
40
|
+
appIso: undefined,
|
|
41
|
+
homeLink: '/',
|
|
42
|
+
lang: 'fr',
|
|
43
|
+
breakpoint: 1000,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const emit = defineEmits<{ sidebarChange: [open: boolean] }>()
|
|
47
|
+
|
|
48
|
+
const open = defineModel<boolean>('open', { default: true })
|
|
49
|
+
|
|
50
|
+
const WORDS = {
|
|
51
|
+
fr: { home: 'Accueil', toggle: 'Replier ou déplier la barre' },
|
|
52
|
+
en: { home: 'Home', toggle: 'Collapse or expand the sidebar' },
|
|
53
|
+
} as const
|
|
54
|
+
|
|
55
|
+
const t = computed(() => WORDS[props.lang])
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Sous le seuil, la barre depliee mange la moitie de l'ecran : elle se replie
|
|
59
|
+
* seule, et se laisse rouvrir a la main ensuite.
|
|
60
|
+
*/
|
|
61
|
+
function handleResize() {
|
|
62
|
+
const next = window.innerWidth >= props.breakpoint
|
|
63
|
+
if (next === open.value) return
|
|
64
|
+
open.value = next
|
|
65
|
+
emit('sidebarChange', next)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toggle() {
|
|
69
|
+
open.value = !open.value
|
|
70
|
+
emit('sidebarChange', open.value)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onMounted(() => {
|
|
74
|
+
window.addEventListener('resize', handleResize)
|
|
75
|
+
handleResize()
|
|
76
|
+
})
|
|
77
|
+
onUnmounted(() => window.removeEventListener('resize', handleResize))
|
|
78
|
+
</script>
|
|
79
|
+
|
|
80
|
+
<template>
|
|
81
|
+
<div id="rm-app-sidebar" class="rm-asb" :class="{ 'rm-asb--open': open }">
|
|
82
|
+
<div
|
|
83
|
+
class="rm-asb__head"
|
|
84
|
+
:class="[
|
|
85
|
+
open ? 'rm-asb__head--full' : 'rm-asb__head--small',
|
|
86
|
+
{ 'rm-asb__head--end': open && homeLink === null },
|
|
87
|
+
]"
|
|
88
|
+
>
|
|
89
|
+
<a v-if="homeLink !== null" :href="homeLink" class="rm-asb__home">
|
|
90
|
+
<RmTooltip :text="t.home" :disabled="open" dir="right" class="rm-asb__mark">
|
|
91
|
+
<RmIcon name="House" :size="20" color="var(--color-primary)" />
|
|
92
|
+
</RmTooltip>
|
|
93
|
+
<span v-if="open">{{ t.home }}</span>
|
|
94
|
+
</a>
|
|
95
|
+
|
|
96
|
+
<button type="button" class="rm-asb__toggle" :aria-expanded="open" @click="toggle">
|
|
97
|
+
<RmTooltip :text="t.toggle" :disabled="open" dir="right" class="rm-asb__mark">
|
|
98
|
+
<RmIcon name="Sidebar" :size="20" color="var(--color-primary)" />
|
|
99
|
+
</RmTooltip>
|
|
100
|
+
<span class="rm-sr-only">{{ t.toggle }}</span>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<h1 v-show="open" class="rm-asb__title">
|
|
105
|
+
<slot v-if="!name" name="title" />
|
|
106
|
+
<span v-else class="rm-asb__name">
|
|
107
|
+
<RmFlag v-if="appIso" :code="appIso" :height="16" />
|
|
108
|
+
<span>{{ name }}</span>
|
|
109
|
+
</span>
|
|
110
|
+
</h1>
|
|
111
|
+
|
|
112
|
+
<div class="rm-asb__body">
|
|
113
|
+
<div class="rm-asb__content">
|
|
114
|
+
<slot name="menu" />
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div class="rm-asb__footer">
|
|
119
|
+
<slot name="footer" />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</template>
|
|
123
|
+
|
|
124
|
+
<style scoped>
|
|
125
|
+
.rm-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-asb{align-items:flex-start;background:var(--bg-surface-2);border-right:1px solid var(--border-default);box-sizing:border-box;display:flex;flex-direction:column;gap:16px;height:100vh;left:0;padding:16px 8px;position:fixed;top:0;transition:width .3s ease;width:54px;z-index:98}.rm-asb--open{width:200px}.rm-asb__head{align-items:center;display:flex}.rm-asb__head--full{align-self:stretch;justify-content:space-between}.rm-asb__head--end{justify-content:flex-end}.rm-asb__head--small{flex-direction:column;gap:4px}.rm-asb__home,.rm-asb__toggle{align-items:center;background:transparent;border:none;border-radius:var(--radius-sm,4px);color:var(--color-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);gap:6px;height:32px;line-height:1.6;padding:6px;text-decoration:none}.rm-asb__toggle{align-self:flex-end;justify-content:center}.rm-asb__home:hover,.rm-asb__toggle:hover{background:var(--bg-hover)}.rm-asb__home:focus-visible,.rm-asb__toggle:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-asb__mark{display:flex}.rm-asb__title{align-self:stretch;font-size:20px;font-weight:900;line-height:normal;margin:0 0 16px;padding:0 8px}.rm-asb__name,.rm-asb__title{align-items:center;display:flex;gap:8px}.rm-asb__body{gap:16px;overflow:hidden;width:100%}.rm-asb__body,.rm-asb__content{display:flex;flex:1;flex-direction:column;min-height:0}.rm-asb__content{gap:8px;overflow-y:auto;scrollbar-width:thin}.rm-asb__content::-webkit-scrollbar{height:5px;width:5px}.rm-asb__content::-webkit-scrollbar-thumb{background:var(--text-muted);border-radius:var(--radius-xl,12px)}.rm-asb__footer{width:100%}
|
|
126
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmFilterTabs - rail d'onglets pose sur un fond voile, un seul actif.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-filter-tabs` d'Auriga a l'identique : pastille blanche ombree
|
|
5
|
+
sur l'onglet actif, fond voile pour le rail.
|
|
6
|
+
|
|
7
|
+
Ce n'est pas `RmSegmented`, qui sert une echelle ordonnee (SMR, phases) et
|
|
8
|
+
colle ses segments : ici les onglets sont des vues qui n'ont pas d'ordre, et
|
|
9
|
+
la pastille se pose sous celui qu'on lit.
|
|
10
|
+
|
|
11
|
+
Deux choses changent. Les onglets sont des `<button>` dans un
|
|
12
|
+
`role="tablist"` : les `<div @click>` d'Auriga n'etaient ni atteignables au
|
|
13
|
+
clavier ni annonces comme des onglets.
|
|
14
|
+
|
|
15
|
+
L'onglet actif est un `v-model` plutot qu'un etat interne synchronise depuis
|
|
16
|
+
`index_menu` au montage : la valeur d'Auriga ne suivait plus la prop des que
|
|
17
|
+
la page la changeait elle-meme, par exemple au retour d'une route.
|
|
18
|
+
-->
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed } from 'vue'
|
|
21
|
+
|
|
22
|
+
export interface RmFilterTab {
|
|
23
|
+
/** Intitule affiche. */
|
|
24
|
+
name: string
|
|
25
|
+
/** Valeur portee par le modele. Absente, c'est l'index. */
|
|
26
|
+
value?: string | number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<{
|
|
30
|
+
tabs: RmFilterTab[]
|
|
31
|
+
/** Intitule du rail pour les technologies d'assistance. */
|
|
32
|
+
ariaLabel?: string
|
|
33
|
+
}>(), {
|
|
34
|
+
ariaLabel: undefined,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** Valeur de l'onglet actif : sa `value`, ou son index si elle manque. */
|
|
38
|
+
const selected = defineModel<string | number>({ default: 0 })
|
|
39
|
+
|
|
40
|
+
function valueOf(tab: RmFilterTab, index: number) {
|
|
41
|
+
return tab.value ?? index
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const activeIndex = computed(() => {
|
|
45
|
+
const found = props.tabs.findIndex((tab, i) => valueOf(tab, i) === selected.value)
|
|
46
|
+
// Une valeur inconnue ne doit pas laisser le rail sans onglet actif : un
|
|
47
|
+
// rail ou rien n'est pose se lit comme un chargement en cours.
|
|
48
|
+
return found === -1 ? 0 : found
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div class="rm-filter-tabs" role="tablist" :aria-label="ariaLabel">
|
|
54
|
+
<button
|
|
55
|
+
v-for="(tab, index) in tabs"
|
|
56
|
+
:key="index"
|
|
57
|
+
type="button"
|
|
58
|
+
role="tab"
|
|
59
|
+
class="rm-filter-tabs__tab"
|
|
60
|
+
:class="{ 'is-active': index === activeIndex }"
|
|
61
|
+
:aria-selected="index === activeIndex"
|
|
62
|
+
@click="selected = valueOf(tab, index)"
|
|
63
|
+
>
|
|
64
|
+
{{ tab.name }}
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<style scoped>
|
|
70
|
+
.rm-filter-tabs{align-items:center;background:var(--bg-surface-2);border-radius:var(--radius-xl,12px);display:flex;padding:4px;width:-moz-fit-content;width:fit-content}.rm-filter-tabs__tab{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);color:var(--text-secondary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:6px;justify-content:center;line-height:normal;padding:8px 24px}.rm-filter-tabs__tab.is-active{background:var(--bg-surface);box-shadow:0 0 8px 0 rgba(var(--shadow-rgb,62 63 94),.12);color:var(--text-primary)}.rm-filter-tabs__tab:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmMenuItem - entree de navigation d'une barre laterale.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-menu-item` d'Auriga : icone ou drapeau, intitule qui disparait
|
|
5
|
+
quand la barre est repliee, bulle a droite qui prend le relais dans ce cas,
|
|
6
|
+
et squelette scintillant pendant le chargement.
|
|
7
|
+
|
|
8
|
+
A poser dans `RmSidebar`, qui transmet son etat replie. Pour un menu a
|
|
9
|
+
sections depliables, c'est `RmAccordionMenu`.
|
|
10
|
+
|
|
11
|
+
Deux choses changent. L'entree en chargement porte `aria-busy` et un intitule
|
|
12
|
+
masque : chez Auriga, c'etait une barre grise muette, que le lecteur d'ecran
|
|
13
|
+
sautait sans rien dire.
|
|
14
|
+
|
|
15
|
+
Le drapeau passe par `RmFlag` plutot que par l'emoji, qui ne se ligature pas
|
|
16
|
+
sous Windows.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
withDefaults(defineProps<{
|
|
20
|
+
to: string
|
|
21
|
+
label: string
|
|
22
|
+
/** Icone Phosphor sans le prefixe "Ph". A defaut, `countryIso`. */
|
|
23
|
+
icon?: string
|
|
24
|
+
/** Code ISO 3166-1 alpha-2, pour une entree qui designe un pays. */
|
|
25
|
+
countryIso?: string
|
|
26
|
+
/** Barre depliee : l'intitule sort et la bulle se tait. */
|
|
27
|
+
sidebarOpen?: boolean
|
|
28
|
+
newTab?: boolean
|
|
29
|
+
/** Entree pas encore prete : squelette a la place du contenu. */
|
|
30
|
+
loading?: boolean
|
|
31
|
+
}>(), {
|
|
32
|
+
icon: undefined,
|
|
33
|
+
countryIso: undefined,
|
|
34
|
+
sidebarOpen: true,
|
|
35
|
+
newTab: false,
|
|
36
|
+
loading: false,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const emit = defineEmits<{ clickLink: [payload: { label: string, to: string }] }>()
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<NuxtLink
|
|
44
|
+
:to="to"
|
|
45
|
+
class="rm-menu-item"
|
|
46
|
+
:class="{ 'rm-menu-item--loading': loading }"
|
|
47
|
+
active-class="is-active"
|
|
48
|
+
:target="newTab ? '_blank' : '_self'"
|
|
49
|
+
:aria-busy="loading ? 'true' : undefined"
|
|
50
|
+
@click="emit('clickLink', { label, to })"
|
|
51
|
+
>
|
|
52
|
+
<template v-if="loading">
|
|
53
|
+
<span class="rm-menu-item__skeleton" aria-hidden="true" />
|
|
54
|
+
<span class="rm-sr-only">{{ label }}</span>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<template v-else>
|
|
58
|
+
<!-- Repliee, la barre ne montre que l'icone : la bulle porte alors le
|
|
59
|
+
nom, sinon l'entree n'est identifiable qu'a l'essai. -->
|
|
60
|
+
<RmTooltip
|
|
61
|
+
:text="label"
|
|
62
|
+
:disabled="sidebarOpen"
|
|
63
|
+
dir="right"
|
|
64
|
+
class="rm-menu-item__mark"
|
|
65
|
+
:class="{ 'rm-menu-item__mark--grow': !sidebarOpen }"
|
|
66
|
+
>
|
|
67
|
+
<RmIcon v-if="icon" :name="icon" :size="20" />
|
|
68
|
+
<RmFlag v-else-if="countryIso" :code="countryIso" :height="16" />
|
|
69
|
+
</RmTooltip>
|
|
70
|
+
|
|
71
|
+
<span v-show="sidebarOpen" class="rm-menu-item__label">{{ label }}</span>
|
|
72
|
+
</template>
|
|
73
|
+
</NuxtLink>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<style scoped>
|
|
77
|
+
.rm-sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.rm-menu-item{align-items:center;align-self:stretch;border-radius:var(--radius-sm,4px);color:var(--text-primary);display:flex;font-size:var(--font-14,14px);gap:8px;line-height:1.6;padding:8px;text-decoration:none}.rm-menu-item--loading{height:36px;padding:0}.rm-menu-item :deep(svg){color:var(--text-secondary)}.rm-menu-item:hover:not(.is-active){background:var(--bg-hover)}.rm-menu-item.is-active{background:rgba(var(--brand-rgb),.1)}.rm-menu-item.is-active :deep(svg),.rm-menu-item:hover:not(.is-active) :deep(svg){color:var(--color-primary)}.rm-menu-item:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-menu-item__mark{display:flex}.rm-menu-item__mark--grow{flex:1 1 100%}.rm-menu-item__label{min-width:0}.rm-menu-item__skeleton{background:var(--bg-surface-2);border-radius:var(--radius-sm,4px);height:100%;overflow:hidden;position:relative;width:100%}.rm-menu-item__skeleton:after{animation:rm-menu-item-shimmer 1.5s infinite;background:linear-gradient(90deg,rgba(var(--brand-rgb),0) 0,rgba(var(--brand-rgb),.08) 50%,rgba(var(--brand-rgb),0) 100%);content:"";height:100%;left:-100%;position:absolute;top:0;width:100%}@keyframes rm-menu-item-shimmer{to{left:100%}}@media (prefers-reduced-motion:reduce){.rm-menu-item__skeleton:after{animation:none}}
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmMenuLink - lien secondaire d'une barre laterale, ou action.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-menu-link` d'Auriga : meme rangee que `RmMenuItem` mais sans
|
|
5
|
+
etat actif, pour ce qui vit au pied de la barre (aide, contact, deconnexion).
|
|
6
|
+
`action` en fait un bouton qui emet `click` au lieu de naviguer.
|
|
7
|
+
|
|
8
|
+
Une chose change : la branche action est un vrai `<button>`. Le `<div @click>`
|
|
9
|
+
d'Auriga n'etait ni atteignable au clavier ni annonce comme actionnable, ce
|
|
10
|
+
qui coupait la deconnexion a qui navigue sans souris.
|
|
11
|
+
-->
|
|
12
|
+
<script setup lang="ts">
|
|
13
|
+
withDefaults(defineProps<{
|
|
14
|
+
label: string
|
|
15
|
+
/** Icone Phosphor sans le prefixe "Ph". */
|
|
16
|
+
icon: string
|
|
17
|
+
to?: string
|
|
18
|
+
/** Barre depliee : l'intitule sort et la bulle se tait. */
|
|
19
|
+
sidebarOpen?: boolean
|
|
20
|
+
/** Bouton d'action plutot que lien de navigation. */
|
|
21
|
+
action?: boolean
|
|
22
|
+
newTab?: boolean
|
|
23
|
+
}>(), {
|
|
24
|
+
to: '/',
|
|
25
|
+
sidebarOpen: true,
|
|
26
|
+
action: false,
|
|
27
|
+
newTab: false,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{ click: [] }>()
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<NuxtLink
|
|
35
|
+
v-if="!action"
|
|
36
|
+
:to="to"
|
|
37
|
+
class="rm-menu-link"
|
|
38
|
+
:target="newTab ? '_blank' : '_self'"
|
|
39
|
+
>
|
|
40
|
+
<RmTooltip
|
|
41
|
+
:text="label"
|
|
42
|
+
:disabled="sidebarOpen"
|
|
43
|
+
dir="right"
|
|
44
|
+
class="rm-menu-link__mark"
|
|
45
|
+
:class="{ 'rm-menu-link__mark--grow': !sidebarOpen }"
|
|
46
|
+
>
|
|
47
|
+
<RmIcon :name="icon" :size="20" />
|
|
48
|
+
</RmTooltip>
|
|
49
|
+
<span v-show="sidebarOpen">{{ label }}</span>
|
|
50
|
+
</NuxtLink>
|
|
51
|
+
|
|
52
|
+
<button v-else type="button" class="rm-menu-link" @click="emit('click')">
|
|
53
|
+
<RmTooltip
|
|
54
|
+
:text="label"
|
|
55
|
+
:disabled="sidebarOpen"
|
|
56
|
+
dir="right"
|
|
57
|
+
class="rm-menu-link__mark"
|
|
58
|
+
:class="{ 'rm-menu-link__mark--grow': !sidebarOpen }"
|
|
59
|
+
>
|
|
60
|
+
<RmIcon :name="icon" :size="20" />
|
|
61
|
+
</RmTooltip>
|
|
62
|
+
<span v-show="sidebarOpen">{{ label }}</span>
|
|
63
|
+
</button>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped>
|
|
67
|
+
.rm-menu-link{align-items:center;align-self:stretch;background:none;border:none;border-radius:var(--radius-sm,4px);color:var(--text-primary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-14,14px);gap:8px;line-height:1.6;margin:8px 0;padding:8px;text-align:left;text-decoration:none;text-transform:capitalize;width:100%}.rm-menu-link :deep(svg){color:var(--text-secondary)}.rm-menu-link:hover{background:var(--bg-hover)}.rm-menu-link:hover :deep(svg){color:var(--color-primary)}.rm-menu-link:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-menu-link__mark{display:flex}.rm-menu-link__mark--grow{flex:1 1 100%}
|
|
68
|
+
</style>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmProductCard - banniere de produit, titre et texte sur une image de fond.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-product-card` d'Auriga : le fond est voile d'un degrade blanc a
|
|
5
|
+
gauche pour que le texte reste lisible, la teinte du titre suit le produit, et
|
|
6
|
+
le bord porte la meme teinte a 10 %.
|
|
7
|
+
|
|
8
|
+
Deux choses changent.
|
|
9
|
+
|
|
10
|
+
L'image de fond est une **prop**, plus trois PNG embarques dans le paquet.
|
|
11
|
+
Ceux d'Auriga pesaient nean de 2 a 4,6 Mo piece : un paquet de composants qui
|
|
12
|
+
transporte neuf megaoctets d'images les fait telecharger a toutes les apps,
|
|
13
|
+
y compris celles qui n'affichent jamais cette banniere.
|
|
14
|
+
|
|
15
|
+
Les titres et textes par defaut sautent. Auriga en portait trois jeux ecrits
|
|
16
|
+
en dur, dont un faux texte (« Spider totalus peruvian-night-powder »), qui
|
|
17
|
+
n'ont rien a faire dans un systeme de design : le contenu vient de l'appelant,
|
|
18
|
+
par prop ou par slot.
|
|
19
|
+
-->
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
withDefaults(defineProps<{
|
|
22
|
+
/** Teinte du titre et du bord. */
|
|
23
|
+
accent?: 'healthcare' | 'discovery' | 'databank' | 'pharma'
|
|
24
|
+
/** Image de fond, voilee d'un degrade blanc. Absente, fond uni. */
|
|
25
|
+
background?: string
|
|
26
|
+
/** Titre, si le slot `title` n'est pas fourni. */
|
|
27
|
+
title?: string
|
|
28
|
+
/** Texte, si le slot `text` n'est pas fourni. */
|
|
29
|
+
text?: string
|
|
30
|
+
/**
|
|
31
|
+
* Pose le contenu sur un panneau translucide flou plutot qu'a meme le fond.
|
|
32
|
+
* A utiliser quand l'image est chargee : le texte y tient sans que le
|
|
33
|
+
* degrade ait a manger la moitie de l'image.
|
|
34
|
+
*/
|
|
35
|
+
overlay?: boolean
|
|
36
|
+
}>(), {
|
|
37
|
+
accent: 'healthcare',
|
|
38
|
+
background: '',
|
|
39
|
+
title: '',
|
|
40
|
+
text: '',
|
|
41
|
+
overlay: false,
|
|
42
|
+
})
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<div
|
|
47
|
+
class="rm-product-card"
|
|
48
|
+
:class="[`rm-product-card--${accent}`, { 'rm-product-card--padded': !overlay }]"
|
|
49
|
+
:style="background
|
|
50
|
+
? { backgroundImage: `linear-gradient(90deg, rgba(255,255,255,.9) 41%, rgba(255,255,255,0) 100%), url('${background}')` }
|
|
51
|
+
: undefined"
|
|
52
|
+
>
|
|
53
|
+
<div class="rm-product-card__inner" :class="{ 'rm-product-card__inner--overlay': overlay }">
|
|
54
|
+
<div class="rm-product-card__title">
|
|
55
|
+
<slot name="title">{{ title }}</slot>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="rm-product-card__text">
|
|
59
|
+
<slot name="text">{{ text }}</slot>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div v-if="$slots.actions" class="rm-product-card__actions">
|
|
63
|
+
<slot name="actions" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<style scoped>
|
|
70
|
+
.rm-product-card{align-self:stretch;background-color:var(--bg-surface);background-position:50%;background-repeat:no-repeat;background-size:cover;border-radius:var(--radius-xl,12px);box-shadow:0 6px 8px 0 rgba(var(--shadow-rgb,30 41 59),.1);display:flex;flex-direction:column;position:relative}.rm-product-card--padded{padding:40px}.rm-product-card--healthcare{border:1px solid rgba(var(--healthcare-rgb),.1)}.rm-product-card--discovery{border:1px solid rgba(var(--discovery-rgb),.1)}.rm-product-card--databank{border:1px solid rgba(var(--databank-rgb),.1)}.rm-product-card--pharma{border:1px solid rgba(var(--pharma-rgb),.1)}.rm-product-card__inner{align-items:flex-start;align-self:stretch;display:flex;flex-direction:column;gap:24px}.rm-product-card__inner--overlay{backdrop-filter:blur(5px);background:hsla(0,0%,100%,.7);border-radius:var(--radius-md,8px);gap:16px;margin:24px;padding:24px;width:calc(100% - 48px)}.rm-product-card__title{font-size:26px;font-weight:900;line-height:normal}.rm-product-card--healthcare .rm-product-card__title{color:var(--color-primary)}.rm-product-card--discovery .rm-product-card__title{color:var(--color-discovery)}.rm-product-card--databank .rm-product-card__title{color:var(--color-databank)}.rm-product-card--pharma .rm-product-card__title{color:var(--color-pharma)}.rm-product-card__text{color:var(--text-primary);max-width:550px}.rm-product-card__actions{align-items:center;display:flex;gap:16px}
|
|
71
|
+
</style>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmSectionCard - carte a bandeau, en tete ou en pied.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-section-card` d'Auriga a l'identique : un bandeau voile colle au
|
|
5
|
+
corps blanc, en haut (`position="top"`) ou en bas (`position="bottom"`), et
|
|
6
|
+
les arrondis qui suivent le bandeau pour que la carte reste d'un bloc.
|
|
7
|
+
|
|
8
|
+
Ce n'est pas `RmCard`, qui est une surface nue : ici le bandeau est un
|
|
9
|
+
registre a part, plus petit et plus sourd que le corps, ou l'on pose un pays,
|
|
10
|
+
un statut, une date. Le corps garde le fond plein.
|
|
11
|
+
|
|
12
|
+
`action` marque la carte comme cliquable : le survol sort le bord de marque.
|
|
13
|
+
Sans lui, la carte ne bouge pas, et rien ne laisse croire qu'on peut cliquer.
|
|
14
|
+
|
|
15
|
+
Les couleurs en dur d'Auriga (`#dbdef0`, `#fdfdff`) passent aux tokens.
|
|
16
|
+
-->
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
withDefaults(defineProps<{
|
|
19
|
+
/**
|
|
20
|
+
* Cote du bandeau. `top` rend le slot `header`, `bottom` le slot `footer`,
|
|
21
|
+
* `none` ne rend que le corps.
|
|
22
|
+
*/
|
|
23
|
+
position?: 'top' | 'bottom' | 'none'
|
|
24
|
+
/** Carte cliquable : le bord se colore au survol. */
|
|
25
|
+
action?: boolean
|
|
26
|
+
}>(), {
|
|
27
|
+
position: 'top',
|
|
28
|
+
action: false,
|
|
29
|
+
})
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div class="rm-section-card" :class="{ 'rm-section-card--action': action }">
|
|
34
|
+
<div v-if="position === 'top'" class="rm-section-card__band rm-section-card__band--top">
|
|
35
|
+
<slot name="header" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
class="rm-section-card__body"
|
|
40
|
+
:class="{
|
|
41
|
+
'rm-section-card__body--under-top': position === 'top',
|
|
42
|
+
'rm-section-card__body--over-bottom': position === 'bottom',
|
|
43
|
+
'rm-section-card__body--alone': position === 'none',
|
|
44
|
+
}"
|
|
45
|
+
>
|
|
46
|
+
<slot />
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div v-if="position === 'bottom'" class="rm-section-card__band rm-section-card__band--bottom">
|
|
50
|
+
<slot name="footer" />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
54
|
+
|
|
55
|
+
<style scoped>
|
|
56
|
+
.rm-section-card{align-items:flex-start;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-xl,12px);box-shadow:var(--r-shadow-sm);display:flex;flex-direction:column}.rm-section-card--action:hover{border-color:var(--color-primary);box-shadow:var(--r-shadow-md)}.rm-section-card__band{align-self:stretch;background:var(--bg-surface-2);color:var(--text-secondary);font-size:var(--font-12,12px);line-height:normal;padding:8px 16px}.rm-section-card__band--top{border-bottom:1px solid var(--border-default);border-top-left-radius:var(--radius-xl,12px);border-top-right-radius:var(--radius-xl,12px)}.rm-section-card__band--bottom{border-bottom-left-radius:var(--radius-xl,12px);border-bottom-right-radius:var(--radius-xl,12px);border-top:1px solid var(--border-default)}.rm-section-card__body{align-self:stretch;background:var(--bg-surface);padding:24px}.rm-section-card__body--under-top{border-bottom-left-radius:var(--radius-xl,12px);border-bottom-right-radius:var(--radius-xl,12px)}.rm-section-card__body--over-bottom{border-top-left-radius:var(--radius-xl,12px);border-top-right-radius:var(--radius-xl,12px)}.rm-section-card__body--alone{border-radius:var(--radius-xl,12px)}
|
|
57
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmSubMenu - groupe repliable dans une barre laterale.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-sub-menu` d'Auriga : un intitule, un chevron qui pivote, et les
|
|
5
|
+
entrees en dessous. Repliee, la barre ne garde que le chevron, la bulle porte
|
|
6
|
+
alors le nom du groupe.
|
|
7
|
+
|
|
8
|
+
A poser dans `RmSidebar`, autour de `RmMenuItem`. Pour un menu entier decrit
|
|
9
|
+
en donnees plutot qu'en balises, c'est `RmAccordionMenu`.
|
|
10
|
+
|
|
11
|
+
Deux choses changent : le bouton porte `aria-expanded`, et l'ouverture est un
|
|
12
|
+
`v-model` optionnel. Chez Auriga l'etat etait interne et lu une seule fois au
|
|
13
|
+
montage : une page qui voulait ouvrir un groupe apres coup ne pouvait pas.
|
|
14
|
+
-->
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
withDefaults(defineProps<{
|
|
17
|
+
label: string
|
|
18
|
+
/** Icone Phosphor devant l'intitule. A defaut, `flagIso`. */
|
|
19
|
+
icon?: string
|
|
20
|
+
/** Code ISO 3166-1 alpha-2, pour un groupe qui designe un pays. */
|
|
21
|
+
flagIso?: string
|
|
22
|
+
/** Barre depliee : l'intitule sort et la bulle se tait. */
|
|
23
|
+
sidebarOpen?: boolean
|
|
24
|
+
}>(), {
|
|
25
|
+
icon: undefined,
|
|
26
|
+
flagIso: undefined,
|
|
27
|
+
sidebarOpen: true,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
/** Groupe ouvert. Ouvert par defaut : un menu tout replie cache la navigation. */
|
|
31
|
+
const open = defineModel<boolean>('open', { default: true })
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div class="rm-submenu" :class="{ 'rm-submenu--collapsed': !sidebarOpen }">
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
class="rm-submenu__head"
|
|
39
|
+
:aria-expanded="open"
|
|
40
|
+
@click="open = !open"
|
|
41
|
+
>
|
|
42
|
+
<span v-if="sidebarOpen" class="rm-submenu__title">
|
|
43
|
+
<RmIcon v-if="icon" :name="icon" :size="12" :class="{ 'is-open': open }" />
|
|
44
|
+
<RmFlag v-else-if="flagIso" :code="flagIso" :height="12" />
|
|
45
|
+
<span>{{ label }}</span>
|
|
46
|
+
</span>
|
|
47
|
+
|
|
48
|
+
<RmTooltip
|
|
49
|
+
:text="label"
|
|
50
|
+
:disabled="sidebarOpen"
|
|
51
|
+
dir="right"
|
|
52
|
+
class="rm-submenu__caret-slot"
|
|
53
|
+
:class="{ 'rm-submenu__caret-slot--grow': !sidebarOpen }"
|
|
54
|
+
>
|
|
55
|
+
<span class="rm-submenu__caret" :class="{ 'is-open': open }">
|
|
56
|
+
<RmIcon name="CaretDown" :size="12" />
|
|
57
|
+
</span>
|
|
58
|
+
</RmTooltip>
|
|
59
|
+
</button>
|
|
60
|
+
|
|
61
|
+
<div v-show="open" class="rm-submenu__body">
|
|
62
|
+
<slot />
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style scoped>
|
|
68
|
+
.rm-submenu{align-items:flex-start;flex-direction:column;gap:8px}.rm-submenu,.rm-submenu__head{align-self:stretch;display:flex;width:100%}.rm-submenu__head{align-items:center;background:none;border:none;color:var(--text-primary);cursor:pointer;font-family:inherit;font-size:var(--font-14,14px);font-weight:700;gap:10px;justify-content:space-between;line-height:normal;padding:3px 8px 6px}.rm-submenu__head:focus-visible{outline:2px solid var(--color-primary);outline-offset:-2px}.rm-submenu__title{align-items:center;display:flex;gap:4px}.rm-submenu__title :deep(svg){color:var(--text-secondary)}.rm-submenu__title :deep(svg.is-open){color:var(--color-primary)}.rm-submenu__caret-slot{display:flex}.rm-submenu__caret-slot--grow{flex:1 1 100%}.rm-submenu__caret{color:var(--text-secondary);display:flex;transition:transform .3s ease}.rm-submenu__caret.is-open{color:var(--color-primary);transform:rotate(180deg)}.rm-submenu__body{display:flex;flex-direction:column;gap:8px;width:100%}
|
|
69
|
+
</style>
|