@tekkare/romulus 0.1.57 → 0.1.59
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/ChaptersNav.vue +146 -0
- package/dist/runtime/components/Checkbox.vue +69 -0
- package/dist/runtime/components/CircleAmount.vue +27 -0
- package/dist/runtime/components/Divider.vue +33 -0
- package/dist/runtime/components/Emoji.vue +52 -0
- package/dist/runtime/components/FilterBar.vue +1 -1
- package/dist/runtime/components/LangSelect.vue +103 -0
- package/dist/runtime/components/Switch.vue +63 -0
- package/dist/runtime/components/Tooltip.vue +67 -0
- package/dist/runtime/components/TrendIndicator.vue +49 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmChaptersNav - sommaire d'une page longue, qui suit la lecture.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-chapters-nav` d'Auriga : liste collante a gauche, trait de
|
|
5
|
+
couleur qui se deplace sur le chapitre en cours. Trois choses changent.
|
|
6
|
+
|
|
7
|
+
Le chapitre courant vient d'un `IntersectionObserver` et non d'un ecouteur de
|
|
8
|
+
defilement qui relisait `getBoundingClientRect()` de chaque ancre a chaque
|
|
9
|
+
pixel. Sur une page a quinze chapitres, ce calcul tournait des centaines de
|
|
10
|
+
fois par seconde.
|
|
11
|
+
|
|
12
|
+
Le trait se cale sur la position reelle de l'entree active, mesuree dans le
|
|
13
|
+
DOM. Auriga la calculait en dur (`(22 + 16) * index`) : un intitule qui
|
|
14
|
+
passait sur deux lignes decalait le trait de toutes les entrees suivantes.
|
|
15
|
+
|
|
16
|
+
Le decalage du haut est une prop. Auriga allait lire `#arg_header` dans la
|
|
17
|
+
page, ce qui liait le composant a un identifiant d'application.
|
|
18
|
+
-->
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
21
|
+
|
|
22
|
+
export interface RmChapter {
|
|
23
|
+
/** Intitule affiche. */
|
|
24
|
+
label: string
|
|
25
|
+
/** `id` de la section visee dans la page. */
|
|
26
|
+
value: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<{
|
|
30
|
+
chapters: RmChapter[]
|
|
31
|
+
/** Titre du sommaire. Absent, la liste sort seule. */
|
|
32
|
+
title?: string
|
|
33
|
+
/**
|
|
34
|
+
* Hauteur du chrome fixe en haut de page, en pixels : ce dont il faut
|
|
35
|
+
* remonter pour qu'un chapitre ne se cale pas derriere l'en-tete.
|
|
36
|
+
*/
|
|
37
|
+
offset?: number
|
|
38
|
+
}>(), {
|
|
39
|
+
title: '',
|
|
40
|
+
offset: 0,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{ select: [value: string] }>()
|
|
44
|
+
|
|
45
|
+
const activeValue = defineModel<string>({ default: '' })
|
|
46
|
+
|
|
47
|
+
const listRef = ref<HTMLElement>()
|
|
48
|
+
const itemRefs = ref<HTMLElement[]>([])
|
|
49
|
+
const barTop = ref(0)
|
|
50
|
+
const barHeight = ref(0)
|
|
51
|
+
|
|
52
|
+
const activeIndex = computed(() =>
|
|
53
|
+
props.chapters.findIndex(chapter => chapter.value === activeValue.value),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
/** Cale le trait sur l'entree active, telle qu'elle est reellement posee. */
|
|
57
|
+
async function placeBar() {
|
|
58
|
+
await nextTick()
|
|
59
|
+
const item = itemRefs.value[activeIndex.value]
|
|
60
|
+
if (!item || !listRef.value) return
|
|
61
|
+
barTop.value = item.offsetTop
|
|
62
|
+
barHeight.value = item.offsetHeight
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
watch(activeValue, placeBar)
|
|
66
|
+
watch(() => props.chapters, placeBar, { deep: true })
|
|
67
|
+
|
|
68
|
+
let observer: IntersectionObserver | undefined
|
|
69
|
+
|
|
70
|
+
function observe() {
|
|
71
|
+
observer?.disconnect()
|
|
72
|
+
if (typeof IntersectionObserver === 'undefined') return
|
|
73
|
+
|
|
74
|
+
observer = new IntersectionObserver(
|
|
75
|
+
(entries) => {
|
|
76
|
+
// La section la plus haute encore visible fait le chapitre courant :
|
|
77
|
+
// pendant un defilement rapide plusieurs sections se croisent, et la
|
|
78
|
+
// derniere entree recue n'est pas celle qu'on lit.
|
|
79
|
+
const visible = entries
|
|
80
|
+
.filter(entry => entry.isIntersecting)
|
|
81
|
+
.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)[0]
|
|
82
|
+
if (visible) activeValue.value = visible.target.id
|
|
83
|
+
},
|
|
84
|
+
// La ligne de declenchement est juste sous le chrome fixe.
|
|
85
|
+
{ rootMargin: `-${props.offset}px 0px -60% 0px`, threshold: 0 },
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
for (const chapter of props.chapters) {
|
|
89
|
+
const section = document.getElementById(chapter.value)
|
|
90
|
+
if (section) observer.observe(section)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function select(chapter: RmChapter) {
|
|
95
|
+
activeValue.value = chapter.value
|
|
96
|
+
emit('select', chapter.value)
|
|
97
|
+
const section = document.getElementById(chapter.value)
|
|
98
|
+
if (!section) return
|
|
99
|
+
window.scrollTo({
|
|
100
|
+
top: section.getBoundingClientRect().top + window.scrollY - props.offset,
|
|
101
|
+
behavior: 'smooth',
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
onMounted(() => {
|
|
106
|
+
observe()
|
|
107
|
+
placeBar()
|
|
108
|
+
})
|
|
109
|
+
onBeforeUnmount(() => observer?.disconnect())
|
|
110
|
+
watch(() => props.chapters, observe, { deep: true })
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<template>
|
|
114
|
+
<nav class="rm-chapters" :aria-label="title || undefined">
|
|
115
|
+
<p v-if="title" class="rm-chapters__title">{{ title }}</p>
|
|
116
|
+
|
|
117
|
+
<ul ref="listRef" class="rm-chapters__list">
|
|
118
|
+
<li
|
|
119
|
+
v-for="(chapter, index) in chapters"
|
|
120
|
+
:key="chapter.value"
|
|
121
|
+
ref="itemRefs"
|
|
122
|
+
class="rm-chapters__item"
|
|
123
|
+
>
|
|
124
|
+
<button
|
|
125
|
+
type="button"
|
|
126
|
+
class="rm-chapters__link"
|
|
127
|
+
:class="{ 'is-active': index === activeIndex }"
|
|
128
|
+
:aria-current="index === activeIndex ? 'true' : undefined"
|
|
129
|
+
@click="select(chapter)"
|
|
130
|
+
>
|
|
131
|
+
{{ chapter.label }}
|
|
132
|
+
</button>
|
|
133
|
+
</li>
|
|
134
|
+
|
|
135
|
+
<span
|
|
136
|
+
v-show="activeIndex >= 0"
|
|
137
|
+
class="rm-chapters__bar"
|
|
138
|
+
:style="{ transform: `translateY(${barTop}px)`, height: `${barHeight}px` }"
|
|
139
|
+
/>
|
|
140
|
+
</ul>
|
|
141
|
+
</nav>
|
|
142
|
+
</template>
|
|
143
|
+
|
|
144
|
+
<style scoped>
|
|
145
|
+
.rm-chapters{align-self:stretch;flex:0 0 auto;width:auto}.rm-chapters__title{color:var(--text-primary);font-size:var(--font-14,14px);font-weight:700;line-height:1.6;margin-bottom:8px}.rm-chapters__list{border-left:2px solid var(--border-default);display:flex;flex-direction:column;gap:16px;list-style:none;margin:0;padding:0 0 0 8px;position:relative}.rm-chapters__item{align-items:center;display:flex;padding:0}.rm-chapters__link{background:none;border:none;color:var(--text-primary);cursor:pointer;font-size:var(--font-14,14px);font-weight:400;line-height:1.6;padding:0;text-align:left}.rm-chapters__link.is-active,.rm-chapters__link:hover{color:var(--color-primary)}.rm-chapters__bar{background:var(--color-primary);left:-2px;position:absolute;top:0;transition:transform var(--duration-base,.3s) var(--ease-in-out,ease);width:2px}
|
|
146
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmCheckbox - case a cocher encadree, avec son intitule.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-checkbox` d'Auriga : meme boite bordee, meme icone carree qui
|
|
5
|
+
se remplit, meme bulle d'aide optionnelle a droite. Trois choses changent.
|
|
6
|
+
|
|
7
|
+
C'est un vrai `<input type="checkbox">` visuellement masque, dans un
|
|
8
|
+
`<label>`. Auriga posait un `<div tabindex="0">` avec un `@click` : la barre
|
|
9
|
+
d'espace ne cochait rien et le lecteur d'ecran n'annoncait ni role ni etat.
|
|
10
|
+
|
|
11
|
+
L'etat est un `v-model` plutot qu'une copie interne de `defaultSelect` :
|
|
12
|
+
une case dont le parent refuse le changement revenait, chez Auriga, dans
|
|
13
|
+
l'etat coche malgre le refus.
|
|
14
|
+
|
|
15
|
+
Le masque `paint(squircle)` saute. Il demandait un worklet CSS charge a la
|
|
16
|
+
main, ne marchait que sur Chrome, et un `border-radius` donne le meme angle.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
withDefaults(defineProps<{
|
|
20
|
+
/** Intitule pose a cote de la case. */
|
|
21
|
+
label: string
|
|
22
|
+
/** Intitule du champ, au-dessus de la boite. Absent, rien ne sort. */
|
|
23
|
+
fieldLabel?: string
|
|
24
|
+
/** Bulle d'aide, sortie par une icone a droite. */
|
|
25
|
+
tooltip?: string
|
|
26
|
+
tooltipDir?: 'top' | 'right' | 'bottom' | 'left'
|
|
27
|
+
disabled?: boolean
|
|
28
|
+
}>(), {
|
|
29
|
+
fieldLabel: '',
|
|
30
|
+
tooltip: '',
|
|
31
|
+
tooltipDir: 'top',
|
|
32
|
+
disabled: false,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const checked = defineModel<boolean>({ default: false })
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div class="rm-checkbox">
|
|
40
|
+
<p v-if="fieldLabel" class="rm-checkbox__field-label">{{ fieldLabel }}</p>
|
|
41
|
+
|
|
42
|
+
<div class="rm-checkbox__wrap">
|
|
43
|
+
<label class="rm-checkbox__box" :class="{ 'is-disabled': disabled }">
|
|
44
|
+
<input
|
|
45
|
+
v-model="checked"
|
|
46
|
+
type="checkbox"
|
|
47
|
+
class="rm-checkbox__input"
|
|
48
|
+
:disabled="disabled"
|
|
49
|
+
>
|
|
50
|
+
<span class="rm-checkbox__icon" :class="{ 'is-checked': checked }">
|
|
51
|
+
<RmIcon
|
|
52
|
+
:name="checked ? 'CheckSquare' : 'Square'"
|
|
53
|
+
:weight="checked ? 'fill' : 'regular'"
|
|
54
|
+
:size="20"
|
|
55
|
+
/>
|
|
56
|
+
</span>
|
|
57
|
+
<span class="rm-checkbox__label">{{ label }}</span>
|
|
58
|
+
</label>
|
|
59
|
+
|
|
60
|
+
<RmTooltip v-if="tooltip" :text="tooltip" :dir="tooltipDir" class="rm-checkbox__help">
|
|
61
|
+
<RmIcon name="Question" :size="16" />
|
|
62
|
+
</RmTooltip>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<style scoped>
|
|
68
|
+
.rm-checkbox__field-label{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:600;margin-bottom:6px}.rm-checkbox__wrap{position:relative}.rm-checkbox__box,.rm-checkbox__wrap{align-items:center;display:flex;gap:8px}.rm-checkbox__box{background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;flex:1;font-size:var(--font-14,14px);font-weight:600;min-height:40px;padding:0 12px;transition:border-color var(--duration-fast,.15s) var(--ease-out,ease)}.rm-checkbox__box:hover:not(.is-disabled){border-color:var(--color-primary)}.rm-checkbox__box:focus-within{border-color:var(--color-primary);box-shadow:0 0 0 4px rgba(var(--brand-rgb),.1)}.rm-checkbox__box.is-disabled{color:var(--text-muted);cursor:not-allowed}.rm-checkbox__input{height:1px;opacity:0;pointer-events:none;position:absolute;width:1px}.rm-checkbox__icon{color:var(--text-muted);display:flex;flex-shrink:0}.rm-checkbox__icon.is-checked{color:var(--color-primary)}.rm-checkbox__box.is-disabled .rm-checkbox__icon{color:var(--border-default)}.rm-checkbox__label{min-width:0}.rm-checkbox__help{color:var(--text-muted)}
|
|
69
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmCircleAmount - compte pose a cote d'un intitule (« Filtres 3 »).
|
|
3
|
+
|
|
4
|
+
Reprend `arg-circle-amount` d'Auriga, meme pastille bordee. Ce n'est pas un
|
|
5
|
+
RmBadge : le badge nomme un statut et porte toujours du texte, celui-ci ne
|
|
6
|
+
porte qu'un nombre et disparait quand il vaut zero, parce qu'un « 0 » affiche
|
|
7
|
+
se lit comme un filtre pose sur rien.
|
|
8
|
+
-->
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
|
|
12
|
+
const props = withDefaults(defineProps<{
|
|
13
|
+
amount: number | string
|
|
14
|
+
/** Garde la pastille visible a zero, pour un compteur qui doit tenir sa place. */
|
|
15
|
+
showZero?: boolean
|
|
16
|
+
}>(), { showZero: false })
|
|
17
|
+
|
|
18
|
+
const visible = computed(() => props.showZero || Number(props.amount) !== 0)
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<span v-if="visible" class="rm-circle-amount">{{ amount }}</span>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<style scoped>
|
|
26
|
+
.rm-circle-amount{align-items:center;border:1px solid var(--border-default);border-radius:var(--radius-pill,999px);color:var(--text-primary);display:inline-flex;font-size:var(--font-12,12px);font-weight:700;justify-content:center;line-height:normal;padding:2px 8px}
|
|
27
|
+
</style>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmDivider - trait de separation.
|
|
3
|
+
|
|
4
|
+
Remplace `arg-line-break` d'Auriga, qui posait un `<hr>` et rien d'autre.
|
|
5
|
+
Deux ajouts : le sens vertical, pour separer deux blocs cote a cote sans
|
|
6
|
+
bricoler une bordure, et `decorative` qui retire le trait de l'arbre
|
|
7
|
+
d'accessibilite quand il ne separe rien de reel (un `<hr>` est annonce comme
|
|
8
|
+
une rupture thematique par un lecteur d'ecran, ce qui n'a aucun sens quand il
|
|
9
|
+
ne sert qu'a poser une ligne dans une carte).
|
|
10
|
+
-->
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
withDefaults(defineProps<{
|
|
13
|
+
vertical?: boolean
|
|
14
|
+
/** Trait purement graphique : il sort de l'arbre d'accessibilite. */
|
|
15
|
+
decorative?: boolean
|
|
16
|
+
}>(), {
|
|
17
|
+
vertical: false,
|
|
18
|
+
decorative: true,
|
|
19
|
+
})
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<hr
|
|
24
|
+
class="rm-divider"
|
|
25
|
+
:class="{ 'rm-divider--vertical': vertical }"
|
|
26
|
+
:role="decorative ? 'none' : 'separator'"
|
|
27
|
+
:aria-orientation="!decorative && vertical ? 'vertical' : undefined"
|
|
28
|
+
>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.rm-divider{background:var(--border-default);border:0;height:1px;margin:0}.rm-divider--vertical{align-self:stretch;height:auto;width:1px}
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmEmoji - un emoji a la bonne taille, nomme plutot que colle dans le code.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-emoji` d'Auriga et ses trois entrees. Deux choses changent.
|
|
5
|
+
|
|
6
|
+
Le rendu passe par du texte, pas par `v-html` : le composant d'Auriga
|
|
7
|
+
injectait une entite HTML, ce qui ouvre la porte a n'importe quel balisage
|
|
8
|
+
des que le nom vient d'une donnee.
|
|
9
|
+
|
|
10
|
+
Un nom inconnu ne rend plus le vide : la valeur passe telle quelle, si bien
|
|
11
|
+
qu'un emoji absent de la table s'ecrit directement sans toucher au paquet.
|
|
12
|
+
|
|
13
|
+
L'emoji porte `role="img"` et un intitule, sinon un lecteur d'ecran lit le
|
|
14
|
+
nom Unicode complet au milieu d'une phrase.
|
|
15
|
+
-->
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { computed } from 'vue'
|
|
18
|
+
|
|
19
|
+
/** Les emojis nommes du design system. Un nom absent passe tel quel. */
|
|
20
|
+
const EMOJIS: Record<string, string> = {
|
|
21
|
+
Lightbulb: '💡',
|
|
22
|
+
Pin: '📍',
|
|
23
|
+
Television: '📺',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<{
|
|
27
|
+
/** Nom de la table, ou l'emoji lui-meme. */
|
|
28
|
+
name: string
|
|
29
|
+
size?: number
|
|
30
|
+
/** Intitule lu a voix haute. Absent, l'emoji est purement decoratif. */
|
|
31
|
+
alt?: string
|
|
32
|
+
}>(), {
|
|
33
|
+
size: 20,
|
|
34
|
+
alt: '',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const glyph = computed(() => EMOJIS[props.name] ?? props.name)
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<template>
|
|
41
|
+
<span
|
|
42
|
+
class="rm-emoji"
|
|
43
|
+
:style="{ fontSize: `${size}px` }"
|
|
44
|
+
:role="alt ? 'img' : undefined"
|
|
45
|
+
:aria-label="alt || undefined"
|
|
46
|
+
:aria-hidden="alt ? undefined : 'true'"
|
|
47
|
+
>{{ glyph }}</span>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
.rm-emoji{display:inline-block;line-height:1}
|
|
52
|
+
</style>
|
|
@@ -260,5 +260,5 @@ onBeforeUnmount(() => {
|
|
|
260
260
|
</template>
|
|
261
261
|
|
|
262
262
|
<style scoped>
|
|
263
|
-
.rm-fbar{margin-inline:calc((min(var(--oip-content-max, 100%), 100%) - var(--oip-main-w, 100%))/2);position:sticky;width:var(--oip-main-w,100%);z-index:91;--rm-fbar-field-h:32px;backdrop-filter:blur(12px);background:rgba(245,245,249,.8);border-bottom:1px solid var(--border-default);display:flex;flex-direction:column;font-family:var(--font-sans)}.rm-fbar__views{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__views:empty{display:none}.rm-fbar__row{align-items:flex-start;display:flex;gap:12px;padding-block:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__row:not(:has(~.rm-fbar__rail-row)){border-bottom:1px solid var(--border-default)}.rm-fbar__toggle :deep(p),.rm-fbar__toggle :deep(span){font-size:inherit;font-weight:inherit;line-height:inherit;margin:0}.rm-fbar__toggle{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;display:flex;flex:0 0 auto;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h);justify-content:center;padding:0 14px}.rm-fbar__toggle:hover{background:var(--bg-surface-3)}.rm-fbar__toggle.is-active,.rm-fbar__toggle.is-active:hover{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-fbar__toggle.is-active .rm-fbar__badge{background:var(--color-text-inverse);color:var(--color-primary)}.rm-fbar__badge{align-items:center;background:var(--color-primary);border-radius:16px;color:var(--color-text-inverse);display:inline-flex;font-size:var(--font-11);font-weight:var(--weight-bold);justify-content:center;min-width:18px;padding:0 5px}.rm-fbar__view{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__advanced{align-items:center;background:none;border:none;border-left:1px solid var(--border-default);color:var(--color-primary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-13);font-weight:var(--weight-medium);height:var(--rm-fbar-field-h);padding:0 0 0 12px;position:absolute;right:14px;text-decoration-line:underline;top:0;white-space:nowrap;z-index:2}.rm-fbar__advanced.is-active,.rm-fbar__advanced:hover{color:var(--color-primary-hover,var(--color-primary));text-decoration-thickness:2px}.rm-fbar__rail-row{display:flex;padding-bottom:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__rail-row{border-bottom:1px solid var(--border-default)}.rm-fbar__rail{align-items:center;display:flex;flex:0 1 auto;min-width:0;overflow-x:auto;scrollbar-width:thin}.rm-fbar__rail:empty{display:none}.rm-fbar__search{align-items:center;display:flex;flex:1 1 auto;gap:8px;min-width:0;position:relative}.rm-fbar__search:empty{display:none}.rm-fbar__search :deep(.search-bar-wrapper){flex:1 1 auto;min-width:0}.rm-fbar__search :deep(.search-bar){display:flex;width:100%}.rm-fbar__search :deep(.filters_input_search){border-radius:var(--radius-pill);box-sizing:border-box;height:auto;min-height:var(--rm-fbar-field-h);padding-right:calc(var(--rm-fbar-advanced-w, 0px) + 12px);width:100%}.rm-fbar__search :deep(.research_tags:not(:has(*))){display:none}.rm-fbar__search :deep(.select_shortcut){box-sizing:border-box;font-size:var(--font-11);height:24px;padding-block:0}.rm-fbar__search :deep(.select_shortcut:hover){background:var(--bg-surface-3)!important;border-color:var(--border-default)!important;color:inherit!important}.rm-fbar__search :deep(.select_shortcut:hover svg){color:inherit!important}.rm-fbar__search :deep(.remove_search_section){border-left:none;border-radius:0 var(--radius-pill) var(--radius-pill) 0}.rm-fbar__search :deep(.remove_search_section:hover){background:transparent!important}.rm-fbar__search :deep(.remove_search_section:hover svg){color:var(--text-primary)!important}.rm-fbar__search :deep(.filters_input_search.no_bottom){border-bottom:1.5px solid var(--color-primary)!important;border-radius:var(--radius-pill)}.rm-fbar__search :deep(.search_suggestions){border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-lg);left:0;padding:6px;right:0;top:calc(100% + 6px);width:auto}.rm-fbar__search :deep(.sugg_container>*){border-radius:var(--radius-lg)}.rm-fbar__search :deep(.sugg_container>.selected){background:color-mix(in srgb,var(--color-primary) 6%,transparent)!important}.rm-fbar__panel{--rm-filter-col:224px;--rm-fbar-panel-field-h:40px;align-items:end;display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(0,var(--rm-filter-col)));justify-content:center;padding-block:16px 20px;padding-inline:56px}.rm-fbar__panel--wide{align-items:stretch;display:flex;flex-direction:column;gap:12px}.rm-fbar__filters{display:contents}.rm-fbar__filters :deep(.filter-chip),.rm-fbar__filters :deep(.filter-complex__trigger),.rm-fbar__filters :deep(.rm-number__box){border-radius:var(--radius-lg);height:var(--rm-fbar-panel-field-h);width:100%}.rm-fbar__panel-head{align-items:baseline;display:flex;gap:16px;grid-column:1/-1;width:100%}.rm-fbar__customize{grid-column:1/-1}.rm-fbar__panel :deep(.filters_row_break){grid-column:1/-1}.rm-fbar__panel-title{font-size:var(--font-18);font-weight:800;margin:0}.rm-fbar__action{background:none;border:none;cursor:pointer;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);padding:0;text-decoration-line:underline}.rm-fbar__action--save{color:var(--color-primary)}.rm-fbar__action--clear{color:var(--text-secondary)}
|
|
263
|
+
.rm-fbar{margin-inline:calc((min(var(--oip-content-max, 100%), 100%) - var(--oip-main-w, 100%))/2);position:sticky;width:var(--oip-main-w,100%);z-index:91;--rm-fbar-field-h:32px;backdrop-filter:blur(12px);background:rgba(245,245,249,.8);border-bottom:1px solid var(--border-default);display:flex;flex-direction:column;font-family:var(--font-sans)}.rm-fbar__views{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__views:empty{display:none}.rm-fbar__row{align-items:flex-start;display:flex;gap:12px;padding-block:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__row:not(:has(~.rm-fbar__rail-row)){border-bottom:1px solid var(--border-default)}.rm-fbar__toggle :deep(p),.rm-fbar__toggle :deep(span){font-size:inherit;font-weight:inherit;line-height:inherit;margin:0}.rm-fbar__toggle{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-pill);color:var(--text-primary);cursor:pointer;display:flex;flex:0 0 auto;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h);justify-content:center;padding:0 14px}.rm-fbar__toggle:hover{background:var(--bg-surface-3)}.rm-fbar__toggle.is-active,.rm-fbar__toggle.is-active:hover{background:var(--color-primary);border-color:var(--color-primary);color:var(--color-text-inverse)}.rm-fbar__toggle.is-active .rm-fbar__badge{background:var(--color-text-inverse);color:var(--color-primary)}.rm-fbar__badge{align-items:center;background:var(--color-primary);border-radius:16px;color:var(--color-text-inverse);display:inline-flex;font-size:var(--font-11);font-weight:var(--weight-bold);justify-content:center;min-width:18px;padding:0 5px}.rm-fbar__view{align-items:center;display:flex;flex:0 0 auto}.rm-fbar__advanced{align-items:center;background:none;border:none;border-left:1px solid var(--border-default);color:var(--color-primary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-13);font-weight:var(--weight-medium);height:var(--rm-fbar-field-h);padding:0 0 0 12px;position:absolute;right:14px;text-decoration-line:underline;top:0;white-space:nowrap;z-index:2}.rm-fbar__advanced.is-active,.rm-fbar__advanced:hover{color:var(--color-primary-hover,var(--color-primary));text-decoration-thickness:2px}.rm-fbar__rail-row{display:flex;padding-bottom:8px;padding-inline:max(32px,(100% - var(--oip-content-max,100%))/2 + 32px)}.rm-fbar--open .rm-fbar__rail-row{border-bottom:1px solid var(--border-default)}.rm-fbar__rail{align-items:center;display:flex;flex:0 1 auto;min-width:0;overflow-x:auto;scrollbar-width:thin}.rm-fbar__rail:empty{display:none}.rm-fbar__search{align-items:center;display:flex;flex:1 1 auto;gap:8px;min-width:0;position:relative}.rm-fbar__search:empty{display:none}.rm-fbar__search :deep(.search-bar-wrapper){flex:1 1 auto;min-width:0}.rm-fbar__search :deep(.search-bar){background:var(--bg-surface);box-sizing:border-box;display:flex;height:var(--rm-fbar-field-h);width:100%}.rm-fbar__search :deep(.filters_input_search){border-radius:var(--radius-pill);box-sizing:border-box;height:auto;min-height:var(--rm-fbar-field-h);padding-right:calc(var(--rm-fbar-advanced-w, 0px) + 12px);width:100%}.rm-fbar__search :deep(.research_tags:not(:has(*))){display:none}.rm-fbar__search :deep(.select_shortcut){box-sizing:border-box;font-size:var(--font-11);height:24px;padding-block:0}.rm-fbar__search :deep(.select_shortcut:hover){background:var(--bg-surface-3)!important;border-color:var(--border-default)!important;color:inherit!important}.rm-fbar__search :deep(.select_shortcut:hover svg){color:inherit!important}.rm-fbar__search :deep(.remove_search_section){border-left:none;border-radius:0 var(--radius-pill) var(--radius-pill) 0}.rm-fbar__search :deep(.remove_search_section:hover){background:transparent!important}.rm-fbar__search :deep(.remove_search_section:hover svg){color:var(--text-primary)!important}.rm-fbar__search :deep(.filters_input_search.no_bottom){border-bottom:1.5px solid var(--color-primary)!important;border-radius:var(--radius-pill)}.rm-fbar__search :deep(.search_suggestions){border:1px solid var(--border-default);border-radius:var(--radius-2xl);box-shadow:var(--shadow-lg);left:0;padding:6px;right:0;top:calc(100% + 6px);width:auto}.rm-fbar__search :deep(.sugg_container>*){border-radius:var(--radius-lg)}.rm-fbar__search :deep(.sugg_container>.selected){background:color-mix(in srgb,var(--color-primary) 6%,transparent)!important}.rm-fbar__panel{--rm-filter-col:224px;--rm-fbar-panel-field-h:40px;align-items:end;display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(0,var(--rm-filter-col)));justify-content:center;padding-block:16px 20px;padding-inline:56px}.rm-fbar__panel--wide{align-items:stretch;display:flex;flex-direction:column;gap:12px}.rm-fbar__filters{display:contents}.rm-fbar__filters :deep(.filter-chip),.rm-fbar__filters :deep(.filter-complex__trigger),.rm-fbar__filters :deep(.rm-number__box){border-radius:var(--radius-lg);height:var(--rm-fbar-panel-field-h);width:100%}.rm-fbar__panel-head{align-items:baseline;display:flex;gap:16px;grid-column:1/-1;width:100%}.rm-fbar__customize{grid-column:1/-1}.rm-fbar__panel :deep(.filters_row_break){grid-column:1/-1}.rm-fbar__panel-title{font-size:var(--font-18);font-weight:800;margin:0}.rm-fbar__action{background:none;border:none;cursor:pointer;font-family:inherit;font-size:var(--font-14);font-weight:var(--weight-medium);padding:0;text-decoration-line:underline}.rm-fbar__action--save{color:var(--color-primary)}.rm-fbar__action--clear{color:var(--text-secondary)}
|
|
264
264
|
</style>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmLangSelect - choix de la langue, drapeau puis nom.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-lang-select` d'Auriga : la langue courante s'affiche, le clic
|
|
5
|
+
deroule les autres. Trois choses changent.
|
|
6
|
+
|
|
7
|
+
C'est un `<button aria-expanded>` suivi d'une liste `role="listbox"`, et
|
|
8
|
+
Echap referme. Auriga posait un `<div @click>` sans role ni etat, injoignable
|
|
9
|
+
au clavier.
|
|
10
|
+
|
|
11
|
+
La langue courante est un `v-model` sur l'iso, plus une copie interne : une
|
|
12
|
+
langue changee par la route ou par un autre menu se refletait mal.
|
|
13
|
+
|
|
14
|
+
`collapsed` remplace le `sidebarOpen` a trois etats (`true`, `false`, `null`)
|
|
15
|
+
d'Auriga, qui melangeait « barre repliee » et « pas dans une barre » dans la
|
|
16
|
+
meme valeur.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
|
20
|
+
|
|
21
|
+
export interface RmLang {
|
|
22
|
+
/** Code ISO du pays, pour le drapeau (`fr`, `gb`...). */
|
|
23
|
+
iso: string
|
|
24
|
+
/** Nom affiche. */
|
|
25
|
+
title: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const props = withDefaults(defineProps<{
|
|
29
|
+
langs: RmLang[]
|
|
30
|
+
/** Barre laterale repliee : le drapeau seul, sans le nom. */
|
|
31
|
+
collapsed?: boolean
|
|
32
|
+
/** Intitule du bouton pour les technologies d'assistance. */
|
|
33
|
+
label?: string
|
|
34
|
+
}>(), {
|
|
35
|
+
collapsed: false,
|
|
36
|
+
label: 'Langue',
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const selected = defineModel<string>({ default: '' })
|
|
40
|
+
|
|
41
|
+
const isOpen = ref(false)
|
|
42
|
+
const wrapperRef = ref<HTMLElement>()
|
|
43
|
+
|
|
44
|
+
const current = computed(() => props.langs.find(lang => lang.iso === selected.value))
|
|
45
|
+
const others = computed(() => props.langs.filter(lang => lang.iso !== selected.value))
|
|
46
|
+
|
|
47
|
+
function pick(lang: RmLang) {
|
|
48
|
+
selected.value = lang.iso
|
|
49
|
+
isOpen.value = false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onClickOutside(event: Event) {
|
|
53
|
+
if (wrapperRef.value && !wrapperRef.value.contains(event.target as Node)) isOpen.value = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function onKeydown(event: KeyboardEvent) {
|
|
57
|
+
if (event.key === 'Escape' && isOpen.value) isOpen.value = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onMounted(() => {
|
|
61
|
+
document.addEventListener('click', onClickOutside)
|
|
62
|
+
document.addEventListener('keydown', onKeydown)
|
|
63
|
+
})
|
|
64
|
+
onUnmounted(() => {
|
|
65
|
+
document.removeEventListener('click', onClickOutside)
|
|
66
|
+
document.removeEventListener('keydown', onKeydown)
|
|
67
|
+
})
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<template>
|
|
71
|
+
<div v-if="current" ref="wrapperRef" class="rm-lang">
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
class="rm-lang__current"
|
|
75
|
+
:aria-label="label"
|
|
76
|
+
:aria-expanded="isOpen"
|
|
77
|
+
@click="isOpen = !isOpen"
|
|
78
|
+
>
|
|
79
|
+
<RmFlag :code="current.iso" :height="18" :title="current.title" />
|
|
80
|
+
<span v-if="!collapsed" class="rm-lang__name">{{ current.title }}</span>
|
|
81
|
+
<RmIcon
|
|
82
|
+
v-if="!collapsed"
|
|
83
|
+
name="CaretDown"
|
|
84
|
+
:size="14"
|
|
85
|
+
class="rm-lang__caret"
|
|
86
|
+
:class="{ 'is-open': isOpen }"
|
|
87
|
+
/>
|
|
88
|
+
</button>
|
|
89
|
+
|
|
90
|
+
<ul v-if="isOpen && others.length > 0" class="rm-lang__list" role="listbox" :aria-label="label">
|
|
91
|
+
<li v-for="lang in others" :key="lang.iso" role="option" :aria-selected="false">
|
|
92
|
+
<button type="button" class="rm-lang__option" @click="pick(lang)">
|
|
93
|
+
<RmFlag :code="lang.iso" :height="18" :title="lang.title" />
|
|
94
|
+
<span v-if="!collapsed">{{ lang.title }}</span>
|
|
95
|
+
</button>
|
|
96
|
+
</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<style scoped>
|
|
102
|
+
.rm-lang{position:relative}.rm-lang__current,.rm-lang__option{align-items:center;background:none;border:none;border-radius:var(--radius-md,8px);color:var(--text-primary);cursor:pointer;display:flex;font-size:var(--font-14,14px);gap:8px;padding:6px 8px;width:100%}.rm-lang__current:hover,.rm-lang__option:hover{background:var(--bg-hover)}.rm-lang__name{flex:1;text-align:left}.rm-lang__caret{color:var(--text-muted);transition:transform var(--duration-fast,.15s) var(--ease-in-out,ease)}.rm-lang__caret.is-open{transform:rotate(180deg)}.rm-lang__list{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md,8px);bottom:calc(100% + 4px);box-shadow:0 4px 16px rgba(var(--shadow-rgb),.1);left:0;list-style:none;margin:0;min-width:100%;padding:4px;position:absolute;z-index:50}
|
|
103
|
+
</style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmSwitch - bascule entre deux etats nommes.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-switch` d'Auriga : les deux intitules encadrent la bascule, et
|
|
5
|
+
celui qui vaut est le seul en couleur pleine. Ce n'est pas une case a cocher
|
|
6
|
+
deguisee : les deux etats ont un nom, aucun n'est « rien ».
|
|
7
|
+
|
|
8
|
+
Trois choses changent. C'est un `<button role="switch">` porteur de
|
|
9
|
+
`aria-checked` : le `<div tabindex="0">` d'Auriga n'annoncait ni role ni
|
|
10
|
+
etat, et ne repondait pas a la barre d'espace.
|
|
11
|
+
|
|
12
|
+
L'etat est un `v-model` plutot qu'une copie interne : la bascule d'Auriga
|
|
13
|
+
avancait toute seule, meme quand le parent refusait le changement.
|
|
14
|
+
|
|
15
|
+
Le masque `paint(squircle)` saute, avec le worklet CSS qu'il fallait charger
|
|
16
|
+
a la main et qui ne peignait que sur Chrome. Un `border-radius` suffit.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
withDefaults(defineProps<{
|
|
20
|
+
/** Intitule de l'etat actif, a gauche. */
|
|
21
|
+
activeText: string
|
|
22
|
+
/** Intitule de l'etat inactif, a droite. */
|
|
23
|
+
inactiveText: string
|
|
24
|
+
/** Intitule du champ, au-dessus. Absent, rien ne sort. */
|
|
25
|
+
label?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
}>(), {
|
|
28
|
+
label: '',
|
|
29
|
+
disabled: false,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const active = defineModel<boolean>({ default: true })
|
|
33
|
+
|
|
34
|
+
function toggle() {
|
|
35
|
+
active.value = !active.value
|
|
36
|
+
}
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="rm-switch">
|
|
41
|
+
<p v-if="label" class="rm-switch__label">{{ label }}</p>
|
|
42
|
+
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
role="switch"
|
|
46
|
+
class="rm-switch__control"
|
|
47
|
+
:aria-checked="active"
|
|
48
|
+
:aria-label="label || undefined"
|
|
49
|
+
:disabled="disabled"
|
|
50
|
+
@click="toggle"
|
|
51
|
+
>
|
|
52
|
+
<span class="rm-switch__text" :class="{ 'is-on': active }">{{ activeText }}</span>
|
|
53
|
+
<span class="rm-switch__track">
|
|
54
|
+
<span class="rm-switch__knob" :class="{ 'is-right': !active }" />
|
|
55
|
+
</span>
|
|
56
|
+
<span class="rm-switch__text" :class="{ 'is-on': !active }">{{ inactiveText }}</span>
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</template>
|
|
60
|
+
|
|
61
|
+
<style scoped>
|
|
62
|
+
.rm-switch__label{color:var(--text-secondary);font-size:var(--font-12,12px);font-weight:600;margin-bottom:6px}.rm-switch__control{align-items:center;background:var(--bg-surface);border:1.5px solid var(--border-default);border-radius:var(--radius-md,8px);cursor:pointer;display:flex;gap:10px;padding:10px 16px;width:-moz-fit-content;width:fit-content}.rm-switch__control:focus-visible{border-color:var(--color-primary);box-shadow:0 0 0 4px rgba(var(--brand-rgb),.1);outline:none}.rm-switch__control:disabled{cursor:not-allowed;opacity:.6}.rm-switch__text{color:var(--text-muted);font-size:var(--font-14,14px);font-weight:600;line-height:16px;transition:color var(--duration-fast,.15s) var(--ease-out,ease)}.rm-switch__text.is-on{color:var(--text-primary)}.rm-switch__track{background:var(--color-primary);border-radius:var(--radius-pill,999px);flex-shrink:0;height:18px;position:relative;width:36px}.rm-switch__knob{background:var(--r-color-white);border-radius:50%;height:14px;left:2px;position:absolute;top:50%;transform:translateY(-50%);transition:left var(--duration-fast,.15s) var(--ease-out,ease);width:14px}.rm-switch__knob.is-right{left:20px}
|
|
63
|
+
</style>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmTooltip - bulle d'aide au survol d'un element.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-tooltip` d'Auriga : meme bulle sombre, memes quatre directions.
|
|
5
|
+
Trois choses changent.
|
|
6
|
+
|
|
7
|
+
La bulle est un vrai `role="tooltip"` relie au contenu par
|
|
8
|
+
`aria-describedby` : au clavier et au lecteur d'ecran, une bulle qui n'existe
|
|
9
|
+
qu'au `:hover` CSS n'est jamais lue. Elle sort donc aussi au focus.
|
|
10
|
+
|
|
11
|
+
Elle se centre par `translateX(-50%)` au lieu du `margin-left: -72px` en dur
|
|
12
|
+
d'Auriga, qui decalait la bulle des que le texte n'atteignait pas la largeur
|
|
13
|
+
maximale.
|
|
14
|
+
|
|
15
|
+
Le texte passe par un slot nomme plutot que par une prop : un lien ou une
|
|
16
|
+
mise en forme dans une bulle n'obligent plus a sortir du composant.
|
|
17
|
+
-->
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
import { useId } from 'vue'
|
|
20
|
+
|
|
21
|
+
withDefaults(defineProps<{
|
|
22
|
+
/** Texte de la bulle. Ignore si le slot `content` est fourni. */
|
|
23
|
+
text?: string
|
|
24
|
+
/** Cote ou la bulle sort, par rapport a l'element survole. */
|
|
25
|
+
dir?: 'top' | 'right' | 'bottom' | 'left'
|
|
26
|
+
/**
|
|
27
|
+
* Neutralise la bulle sans retirer le composant : un intitule tronque n'a
|
|
28
|
+
* besoin de son texte complet que lorsqu'il est effectivement coupe.
|
|
29
|
+
*/
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Laisse la bulle s'etendre sur toute sa hauteur. Par defaut elle s'arrete a
|
|
33
|
+
* quatre lignes : une bulle plus haute recouvre ce qu'on est en train de
|
|
34
|
+
* lire.
|
|
35
|
+
*/
|
|
36
|
+
full?: boolean
|
|
37
|
+
}>(), {
|
|
38
|
+
text: '',
|
|
39
|
+
dir: 'top',
|
|
40
|
+
disabled: false,
|
|
41
|
+
full: false,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const id = `rm-tooltip-${useId()}`
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<template>
|
|
48
|
+
<!-- `aria-describedby` est pose en permanence : un lecteur d'ecran lit la
|
|
49
|
+
bulle a l'arrivee sur l'element, sans attendre un survol qu'il ne fait
|
|
50
|
+
pas. C'est le CSS seul qui la montre a l'oeil. -->
|
|
51
|
+
<span class="rm-tooltip" :aria-describedby="disabled ? undefined : id">
|
|
52
|
+
<slot />
|
|
53
|
+
<span
|
|
54
|
+
v-if="!disabled"
|
|
55
|
+
:id="id"
|
|
56
|
+
role="tooltip"
|
|
57
|
+
class="rm-tooltip__bubble"
|
|
58
|
+
:class="[`rm-tooltip__bubble--${dir}`, { 'rm-tooltip__bubble--full': full }]"
|
|
59
|
+
>
|
|
60
|
+
<slot name="content">{{ text }}</slot>
|
|
61
|
+
</span>
|
|
62
|
+
</span>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<style scoped>
|
|
66
|
+
.rm-tooltip{display:inline-flex;position:relative;width:-moz-fit-content;width:fit-content}.rm-tooltip__bubble{background:var(--r-color-gray-900);border-radius:var(--radius-sm,4px);color:var(--r-color-white);display:-webkit-box;font-size:var(--font-13,13px);font-weight:400;-webkit-line-clamp:4;line-height:1.6;max-width:220px;padding:4px 12px;position:absolute;text-align:left;white-space:pre-line;width:-moz-max-content;width:max-content;z-index:60;-webkit-box-orient:vertical;opacity:0;overflow:hidden;transition:opacity var(--duration-fast,.15s) var(--ease-out,ease);visibility:hidden}.rm-tooltip__bubble--full{display:block;overflow:visible}.rm-tooltip:focus-within .rm-tooltip__bubble,.rm-tooltip:hover .rm-tooltip__bubble{opacity:1;visibility:visible}.rm-tooltip__bubble--top{bottom:calc(100% + 6px);left:50%;transform:translateX(-50%)}.rm-tooltip__bubble--bottom{left:50%;top:calc(100% + 6px);transform:translateX(-50%)}.rm-tooltip__bubble--right{left:calc(100% + 8px);top:50%;transform:translateY(-50%)}.rm-tooltip__bubble--left{right:calc(100% + 8px);top:50%;transform:translateY(-50%)}
|
|
67
|
+
</style>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
RmTrendIndicator - pastille de tendance, une fleche montante ou descendante.
|
|
3
|
+
|
|
4
|
+
Reprend `arg-trend-indicator` d'Auriga : meme pastille ronde a fond teinte,
|
|
5
|
+
memes fleches diagonales. Deux choses changent.
|
|
6
|
+
|
|
7
|
+
Le sens n'est jamais porte par la seule couleur : la fleche pointe en haut ou
|
|
8
|
+
en bas, et la pastille porte un `title` lisible par un lecteur d'ecran. Chez
|
|
9
|
+
Auriga, un daltonien lisait deux ronds identiques.
|
|
10
|
+
|
|
11
|
+
`semantic` remplace `isGreenAndRed`, nomme a l'envers de ce qu'il fait : une
|
|
12
|
+
hausse n'est bonne que dans certains tableaux (des ventes, pas des ruptures),
|
|
13
|
+
d'ou la teinte neutre par defaut et le vert/rouge sur demande.
|
|
14
|
+
-->
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { computed } from 'vue'
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(defineProps<{
|
|
19
|
+
/** Sens de la tendance. */
|
|
20
|
+
up?: boolean
|
|
21
|
+
/** Vert pour une hausse, rouge pour une baisse. Sinon, teinte de marque. */
|
|
22
|
+
semantic?: boolean
|
|
23
|
+
/** Lu par les technologies d'assistance. */
|
|
24
|
+
title?: string
|
|
25
|
+
}>(), {
|
|
26
|
+
up: true,
|
|
27
|
+
semantic: false,
|
|
28
|
+
title: '',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const color = computed(() => {
|
|
32
|
+
if (!props.semantic) return props.up ? 'var(--color-primary)' : 'var(--color-secondary)'
|
|
33
|
+
return props.up ? 'var(--color-success)' : 'var(--color-danger)'
|
|
34
|
+
})
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<template>
|
|
38
|
+
<span
|
|
39
|
+
class="rm-trend"
|
|
40
|
+
:class="[up ? 'rm-trend--up' : 'rm-trend--down', { 'rm-trend--semantic': semantic }]"
|
|
41
|
+
:title="title || undefined"
|
|
42
|
+
>
|
|
43
|
+
<RmIcon :name="up ? 'ArrowUpRight' : 'ArrowDownRight'" :size="16" :color="color" />
|
|
44
|
+
</span>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<style scoped>
|
|
48
|
+
.rm-trend{align-items:center;border-radius:50%;display:inline-flex;height:24px;justify-content:center;width:24px}.rm-trend--up{background:rgba(var(--brand-rgb),.1)}.rm-trend--down{background:color-mix(in srgb,var(--color-secondary) 12%,transparent)}.rm-trend--semantic.rm-trend--up{background:var(--color-success-bg)}.rm-trend--semantic.rm-trend--down{background:var(--color-danger-bg)}
|
|
49
|
+
</style>
|