@tekkare/romulus 0.1.50 → 0.1.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/module.mjs +2 -1
- package/dist/runtime/assets/styles/components.css +1 -1
- package/dist/runtime/assets/styles/tokens.css +1 -1
- package/dist/runtime/components/CardActions.vue +229 -0
- package/dist/runtime/components/ChartCard.vue +27 -77
- package/dist/runtime/components/Drawer.vue +25 -2
- package/dist/runtime/components/ExportPanel.vue +95 -25
- package/dist/runtime/components/FilterComplex.vue +135 -28
- package/dist/runtime/components/FilterDropdown.vue +53 -17
- package/dist/runtime/components/Modal.vue +29 -4
- package/dist/runtime/components/ResultCard.vue +44 -3
- package/dist/runtime/components/SidebarLeft.vue +21 -3
- package/dist/runtime/components/TableCard.vue +28 -2
- package/dist/runtime/components/ViewPill.vue +2 -2
- package/dist/runtime/composables/useChartTheme.d.ts +1 -1
- package/dist/runtime/composables/useChartTheme.js +7 -6
- package/dist/runtime/composables/useRmCardActions.d.ts +32 -0
- package/dist/runtime/composables/useRmCardActions.js +53 -0
- package/dist/runtime/composables/useRmDialog.d.ts +27 -0
- package/dist/runtime/composables/useRmDialog.js +64 -0
- package/dist/runtime/composables/useRmExport.js +32 -11
- package/dist/runtime/composables/useTheme.d.ts +6 -1
- package/dist/runtime/composables/useTheme.js +23 -1
- package/dist/runtime/plugins/a11yFilters.client.d.ts +2 -0
- package/dist/runtime/plugins/a11yFilters.client.js +30 -0
- package/dist/runtime/utils/cardData.d.ts +41 -0
- package/dist/runtime/utils/cardData.js +138 -0
- package/dist/runtime/utils/export.d.ts +17 -1
- package/package.json +1 -1
|
@@ -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>
|
|
@@ -139,7 +139,7 @@ onBeforeUnmount(() => {
|
|
|
139
139
|
:aria-expanded="open"
|
|
140
140
|
@click="open = !open"
|
|
141
141
|
>
|
|
142
|
-
<RmIcon name="BookmarkSimple"
|
|
142
|
+
<RmIcon name="BookmarkSimple" :size="16" class="rm-vpill__icon" />
|
|
143
143
|
<span class="rm-vpill__key">{{ t.key }}</span>
|
|
144
144
|
<span class="rm-vpill__value">{{ label }}</span>
|
|
145
145
|
<span v-if="dirty" class="rm-vpill__dot" :title="t.unsaved" aria-hidden="true" />
|
|
@@ -194,5 +194,5 @@ onBeforeUnmount(() => {
|
|
|
194
194
|
</template>
|
|
195
195
|
|
|
196
196
|
<style scoped>
|
|
197
|
-
.rm-vpill{position:relative}.rm-vpill,.rm-vpill__pill{align-items:center;display:flex}.rm-vpill__pill{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-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h,32px);padding:0 14px;white-space:nowrap}.rm-vpill__pill:hover{background:var(--bg-surface-3)}.rm-vpill__pill.is-open{border-color:var(--color-primary);color:var(--color-primary)}.rm-vpill__icon{color:
|
|
197
|
+
.rm-vpill{position:relative}.rm-vpill,.rm-vpill__pill{align-items:center;display:flex}.rm-vpill__pill{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-14);font-weight:var(--weight-medium);gap:6px;height:var(--rm-fbar-field-h,32px);padding:0 14px;white-space:nowrap}.rm-vpill__pill:hover{background:var(--bg-surface-3)}.rm-vpill__pill.is-open{border-color:var(--color-primary);color:var(--color-primary)}.rm-vpill__icon{color:currentColor;flex-shrink:0}.rm-vpill__key{color:var(--text-muted);font-weight:var(--weight-normal)}.rm-vpill__value{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-vpill__pill.has-unsaved{border-color:var(--color-warning)}.rm-vpill__dot{animation:rm-vpill-pulse 2s ease-out infinite;background:var(--color-warning);border-radius:50%;flex-shrink:0;height:8px;width:8px}@keyframes rm-vpill-pulse{0%{box-shadow:0 0 0 0 rgba(217,119,6,.6)}70%{box-shadow:0 0 0 6px rgba(217,119,6,0)}to{box-shadow:0 0 0 0 rgba(217,119,6,0)}}@media (prefers-reduced-motion:reduce){.rm-vpill__dot,.rm-vpill__unsaved-dot:before{animation:none}}.rm-vpill__caret{color:var(--text-muted);flex-shrink:0}.rm-vpill__menu{background:var(--bg-surface);border:1px solid var(--border-default);border-radius:8px;box-shadow:var(--shadow-lg);box-sizing:border-box;font-family:var(--font-sans);left:0;max-width:460px;min-width:320px;padding:6px;position:absolute;top:calc(100% + 6px);z-index:60}.rm-vpill__unsaved{align-items:center;background:var(--color-warning-bg);border-radius:var(--radius-md);color:var(--color-warning);display:flex;font-size:var(--font-12);gap:8px;padding:8px 12px}.rm-vpill__unsaved-dot{align-items:center;display:inline-flex;flex-shrink:0;height:14px;justify-content:center;width:14px}.rm-vpill__unsaved-dot:before{animation:rm-vpill-pulse 2s ease-out infinite;background:var(--color-warning);border-radius:50%;content:"";height:7px;width:7px}.rm-vpill__unsaved-label{flex:1;font-weight:var(--weight-semibold)}.rm-vpill__unsaved-cta{background:transparent;border:none;color:var(--color-warning);cursor:pointer;font-family:inherit;font-size:inherit;font-weight:var(--weight-bold);padding:0;text-decoration:underline;text-underline-offset:2px}.rm-vpill__unsaved-cta:hover{color:var(--text-primary)}.rm-vpill__divider{background:var(--border-soft);height:1px;margin:6px -6px}.rm-vpill__section{color:var(--text-muted);font-size:var(--font-10);font-weight:var(--weight-bold);letter-spacing:.08em;margin:0;padding:8px 12px 4px 34px;text-align:left;text-transform:uppercase}.rm-vpill__item{align-items:center;background:none;border:none;border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:flex;font-family:inherit;font-size:var(--font-13);gap:8px;padding:8px 12px;text-align:left;transition:background .1s,color .1s;width:100%}.rm-vpill__item:hover{background:var(--bg-hover);color:var(--text-primary)}.rm-vpill__item.is-active{background:var(--bg-hover);color:var(--color-secondary);font-weight:var(--weight-semibold)}.rm-vpill__name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rm-vpill__item--view .rm-vpill__check{color:var(--color-secondary);flex-shrink:0;opacity:0;transition:opacity .1s}.rm-vpill__item--view.is-active .rm-vpill__check{opacity:1}
|
|
198
198
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function useChartTheme(): {
|
|
2
2
|
night: import("vue").Ref<boolean, boolean>;
|
|
3
|
-
|
|
3
|
+
cvdSafe: import("vue").Ref<boolean, boolean>;
|
|
4
4
|
theme: import("vue").ComputedRef<import("../utils/chart").RmChartTheme>;
|
|
5
5
|
palette: import("vue").ComputedRef<import("../utils/chart").RmPalette>;
|
|
6
6
|
decal: import("vue").ComputedRef<boolean>;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
2
2
|
import { rmChartTheme, rmPalette } from "../utils/chart.js";
|
|
3
|
-
const CVD_CLASSES = ["cvd-deutera", "cvd-protan", "cvd-trita", "cvd-mono"];
|
|
4
3
|
export function useChartTheme() {
|
|
5
4
|
const night = ref(false);
|
|
6
|
-
const
|
|
5
|
+
const cvdSafe = ref(false);
|
|
6
|
+
const decalOn = ref(false);
|
|
7
7
|
let observer = null;
|
|
8
8
|
function read() {
|
|
9
9
|
if (typeof document === "undefined") return;
|
|
10
10
|
const list = document.body.classList;
|
|
11
11
|
night.value = list.contains("night");
|
|
12
|
-
|
|
12
|
+
cvdSafe.value = list.contains("pal-cvd-safe");
|
|
13
|
+
decalOn.value = list.contains("chart-decal");
|
|
13
14
|
}
|
|
14
15
|
onMounted(() => {
|
|
15
16
|
read();
|
|
@@ -21,7 +22,7 @@ export function useChartTheme() {
|
|
|
21
22
|
observer = null;
|
|
22
23
|
});
|
|
23
24
|
const theme = computed(() => rmChartTheme(night.value));
|
|
24
|
-
const palette = computed(() => rmPalette(
|
|
25
|
-
const decal = computed(() =>
|
|
26
|
-
return { night,
|
|
25
|
+
const palette = computed(() => rmPalette(cvdSafe.value ? "cvdSafe" : "default", night.value));
|
|
26
|
+
const decal = computed(() => decalOn.value || cvdSafe.value);
|
|
27
|
+
return { night, cvdSafe, theme, palette, decal };
|
|
27
28
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRmCardActions - ce que fait le menu en haut a droite d'une carte.
|
|
3
|
+
*
|
|
4
|
+
* Le menu est le meme sur toutes les cartes du portail, donc son travail vit
|
|
5
|
+
* dans le paquet : deux apps ne peuvent pas sortir le PNG d'un graphique avec
|
|
6
|
+
* deux en-tetes differents, ni nommer leurs fichiers autrement. Les fichiers
|
|
7
|
+
* sont produits par les generateurs du panneau d'export (`useRmExport`), pas
|
|
8
|
+
* par une seconde chaine parallele : la carte n'est qu'un perimetre plus
|
|
9
|
+
* etroit que la page.
|
|
10
|
+
*
|
|
11
|
+
* La donnee, elle, est relue dans le DOM de la carte (`rmCardSheets`) faute
|
|
12
|
+
* d'une declaration de la page : c'est ce qui rend le menu utilisable sur les
|
|
13
|
+
* cartes deja ecrites, sans les rouvrir une par une.
|
|
14
|
+
*/
|
|
15
|
+
import type { RmExportSheet } from '../utils/export.js';
|
|
16
|
+
/** Les actions qu'une carte propose. `link` copie, il ne telecharge pas. */
|
|
17
|
+
export type RmCardAction = 'png' | 'svg' | 'csv' | 'xlsx' | 'link';
|
|
18
|
+
export interface RmCardActionContext {
|
|
19
|
+
/** La carte visee. */
|
|
20
|
+
card: Element;
|
|
21
|
+
lang?: 'fr' | 'en';
|
|
22
|
+
/** Nom du fichier et de l'onglet. A defaut, le titre lu dans la carte. */
|
|
23
|
+
name?: string;
|
|
24
|
+
/** Lien copie par « Copier le lien direct ». A defaut, l'URL courante. */
|
|
25
|
+
link?: string;
|
|
26
|
+
/** Onglets fournis par la page, quand elle a mieux que le rendu a l'ecran. */
|
|
27
|
+
sheets?: RmExportSheet[];
|
|
28
|
+
}
|
|
29
|
+
export declare function useRmCardActions(): {
|
|
30
|
+
runCardAction: (action: RmCardAction, context: RmCardActionContext) => Promise<import("./useRmExport").RmExportResult>;
|
|
31
|
+
toggleCardFullscreen: (card: Element) => Promise<void>;
|
|
32
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { rmCardCharts, rmCardSheets, rmCardTitle } from "../utils/cardData.js";
|
|
2
|
+
import { useRmExport } from "./useRmExport.js";
|
|
3
|
+
const FILE_NAME_MAX = 60;
|
|
4
|
+
function fileNameOf(label) {
|
|
5
|
+
const flat = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6
|
+
if (!flat) return "export";
|
|
7
|
+
if (flat.length <= FILE_NAME_MAX) return flat;
|
|
8
|
+
const cut = flat.slice(0, FILE_NAME_MAX);
|
|
9
|
+
return cut.slice(0, cut.lastIndexOf("-") + 1 || FILE_NAME_MAX).replace(/-+$/, "");
|
|
10
|
+
}
|
|
11
|
+
function configOf(format, fileName) {
|
|
12
|
+
return {
|
|
13
|
+
format,
|
|
14
|
+
range: "all",
|
|
15
|
+
sections: ["table", "charts", "kpi", "sources"],
|
|
16
|
+
columns: [],
|
|
17
|
+
options: ["logo", "sources"],
|
|
18
|
+
fileName,
|
|
19
|
+
layout: "table",
|
|
20
|
+
schedule: "once",
|
|
21
|
+
frequency: "",
|
|
22
|
+
email: false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function absolute(link) {
|
|
26
|
+
if (!link) return void 0;
|
|
27
|
+
if (typeof window === "undefined") return link;
|
|
28
|
+
return new URL(link, window.location.href).href;
|
|
29
|
+
}
|
|
30
|
+
export function useRmCardActions() {
|
|
31
|
+
const { run } = useRmExport();
|
|
32
|
+
async function runCardAction(action, context) {
|
|
33
|
+
const { card, lang = "fr" } = context;
|
|
34
|
+
const title = context.name || rmCardTitle(card) || "Export";
|
|
35
|
+
const format = action === "link" ? "link" : action;
|
|
36
|
+
const sheets = action === "png" || action === "svg" ? [] : context.sheets?.length ? context.sheets : await rmCardSheets(card, title, lang);
|
|
37
|
+
return run(configOf(format, fileNameOf(title)), {
|
|
38
|
+
title,
|
|
39
|
+
sheets,
|
|
40
|
+
charts: rmCardCharts(card).map((el) => ({ title, el })),
|
|
41
|
+
permalink: absolute(context.link)
|
|
42
|
+
}, lang);
|
|
43
|
+
}
|
|
44
|
+
async function toggleCardFullscreen(card) {
|
|
45
|
+
if (typeof document === "undefined") return;
|
|
46
|
+
if (document.fullscreenElement) {
|
|
47
|
+
await document.exitFullscreen();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await card.requestFullscreen?.();
|
|
51
|
+
}
|
|
52
|
+
return { runCardAction, toggleCardFullscreen };
|
|
53
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comportement clavier commun aux surfaces bloquantes (modale, tiroir).
|
|
3
|
+
*
|
|
4
|
+
* La checklist d'accessibilite du design system demande trois choses d'une
|
|
5
|
+
* surface qui recouvre la page, et aucune ne s'obtient en CSS :
|
|
6
|
+
*
|
|
7
|
+
* 1. `Escape` la ferme. Sans ca, un utilisateur au clavier qui ouvre une
|
|
8
|
+
* modale par erreur doit retrouver la croix a la souris.
|
|
9
|
+
* 2. Le focus entre dedans a l'ouverture et n'en sort plus au `Tab`. Sinon le
|
|
10
|
+
* focus continue de parcourir la page derriere, invisible sous le voile :
|
|
11
|
+
* l'utilisateur tabule dans le vide.
|
|
12
|
+
* 3. Le focus revient a l'element qui a ouvert la surface a la fermeture,
|
|
13
|
+
* pour ne pas renvoyer au debut du document.
|
|
14
|
+
*
|
|
15
|
+
* `aria-modal="true"` dit aux lecteurs d'ecran d'ignorer le reste de la page ;
|
|
16
|
+
* il ne deplace pas le focus clavier, d'ou le piege ci-dessous.
|
|
17
|
+
*/
|
|
18
|
+
import { type Ref } from 'vue';
|
|
19
|
+
export interface RmDialogOptions {
|
|
20
|
+
/** Surface ouverte. */
|
|
21
|
+
open: Ref<boolean>;
|
|
22
|
+
/** Racine de la surface, portee par un `ref` de template. */
|
|
23
|
+
surface: Ref<HTMLElement | null>;
|
|
24
|
+
/** Appele par `Escape`. */
|
|
25
|
+
close: () => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function useRmDialog({ open, surface, close }: RmDialogOptions): void;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { nextTick, onBeforeUnmount, watch } from "vue";
|
|
2
|
+
const FOCUSABLE = [
|
|
3
|
+
"a[href]",
|
|
4
|
+
"button:not([disabled])",
|
|
5
|
+
"input:not([disabled])",
|
|
6
|
+
"select:not([disabled])",
|
|
7
|
+
"textarea:not([disabled])",
|
|
8
|
+
'[tabindex]:not([tabindex="-1"])'
|
|
9
|
+
].join(",");
|
|
10
|
+
function focusables(root) {
|
|
11
|
+
return Array.from(root.querySelectorAll(FOCUSABLE)).filter((el) => el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement);
|
|
12
|
+
}
|
|
13
|
+
export function useRmDialog({ open, surface, close }) {
|
|
14
|
+
let opener = null;
|
|
15
|
+
function onKeydown(event) {
|
|
16
|
+
if (event.key === "Escape") {
|
|
17
|
+
event.stopPropagation();
|
|
18
|
+
close();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (event.key !== "Tab") return;
|
|
22
|
+
const root = surface.value;
|
|
23
|
+
if (!root) return;
|
|
24
|
+
const items = focusables(root);
|
|
25
|
+
if (!items.length) {
|
|
26
|
+
event.preventDefault();
|
|
27
|
+
root.focus();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const first = items[0];
|
|
31
|
+
const last = items[items.length - 1];
|
|
32
|
+
const active = document.activeElement;
|
|
33
|
+
if (event.shiftKey && (active === first || active === root)) {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
last.focus();
|
|
36
|
+
} else if (!event.shiftKey && active === last) {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
first.focus();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function bind() {
|
|
42
|
+
document.addEventListener("keydown", onKeydown, true);
|
|
43
|
+
}
|
|
44
|
+
function unbind() {
|
|
45
|
+
document.removeEventListener("keydown", onKeydown, true);
|
|
46
|
+
}
|
|
47
|
+
watch(open, async (isOpen) => {
|
|
48
|
+
if (typeof document === "undefined") return;
|
|
49
|
+
if (isOpen) {
|
|
50
|
+
opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
51
|
+
bind();
|
|
52
|
+
await nextTick();
|
|
53
|
+
const root = surface.value;
|
|
54
|
+
if (!root) return;
|
|
55
|
+
const items = focusables(root);
|
|
56
|
+
(items[0] ?? root).focus();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
unbind();
|
|
60
|
+
if (opener?.isConnected) opener.focus();
|
|
61
|
+
opener = null;
|
|
62
|
+
});
|
|
63
|
+
onBeforeUnmount(unbind);
|
|
64
|
+
}
|
|
@@ -87,8 +87,22 @@ async function raster(markup, width, height, scale = 2) {
|
|
|
87
87
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
88
88
|
return canvas;
|
|
89
89
|
}
|
|
90
|
+
const PRINTABLE_SUBSTITUTES = [
|
|
91
|
+
[/€/g, "EUR"],
|
|
92
|
+
[/[’‘]/g, "'"],
|
|
93
|
+
[/[“”]/g, '"'],
|
|
94
|
+
[/[–—]/g, "-"],
|
|
95
|
+
[/…/g, "..."],
|
|
96
|
+
[/≥/g, ">="],
|
|
97
|
+
[/≤/g, "<="],
|
|
98
|
+
[/[ ]/g, " "]
|
|
99
|
+
];
|
|
90
100
|
function printable(text) {
|
|
91
|
-
|
|
101
|
+
const substituted = PRINTABLE_SUBSTITUTES.reduce(
|
|
102
|
+
(out, [pattern, replacement]) => out.replace(pattern, replacement),
|
|
103
|
+
text
|
|
104
|
+
);
|
|
105
|
+
return substituted.replace(/[^\u0000-\u00ff]/g, "").replace(/\s{2,}/g, " ").trim();
|
|
92
106
|
}
|
|
93
107
|
function cardsOf(payload, config, lang) {
|
|
94
108
|
if (payload.cards?.length) return payload.cards.slice(0, payload.cardLimit ?? 100);
|
|
@@ -355,17 +369,16 @@ export function useRmExport() {
|
|
|
355
369
|
doc.setTextColor(...ink);
|
|
356
370
|
doc.setFont("helvetica", "bold");
|
|
357
371
|
doc.setFontSize(10);
|
|
358
|
-
doc.text(lang === "en" ? "
|
|
372
|
+
doc.text(lang === "en" ? "Reading this view" : "Lecture de la vue", margin, y);
|
|
359
373
|
y += 5;
|
|
360
374
|
doc.setFont("helvetica", "normal");
|
|
361
375
|
doc.setFontSize(9);
|
|
362
376
|
doc.setTextColor(...grey);
|
|
363
|
-
payload.filters.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
y += 4;
|
|
377
|
+
const applied = payload.filters.map((filter) => `${filter.label} = ${filter.value}`).join(" ; ");
|
|
378
|
+
const legend = lang === "en" ? `This report reproduces the page as it was displayed, with the following filters: ${applied}.` : `Ce rapport reprend la page telle qu'elle \xE9tait affich\xE9e, avec les filtres suivants : ${applied}.`;
|
|
379
|
+
const lines = doc.splitTextToSize(printable(legend), inner);
|
|
380
|
+
doc.text(lines, margin, y);
|
|
381
|
+
y += lines.length * 4.5 + 4;
|
|
369
382
|
}
|
|
370
383
|
const charts = config.sections.includes("charts") ? payload.charts ?? [] : [];
|
|
371
384
|
for (const chart of charts) {
|
|
@@ -468,13 +481,13 @@ export function useRmExport() {
|
|
|
468
481
|
}
|
|
469
482
|
}
|
|
470
483
|
if (config.sections.includes("table")) {
|
|
471
|
-
const MAX_COLUMNS =
|
|
484
|
+
const MAX_COLUMNS = landscape ? 16 : 10;
|
|
472
485
|
payload.sheets.forEach((sheet) => {
|
|
473
486
|
const all = columnsOf(sheet, config.columns);
|
|
474
487
|
const columns = all.slice(0, MAX_COLUMNS);
|
|
475
488
|
const rows = rowsOf(sheet, payload.limit);
|
|
476
489
|
if (!config.sections.includes("cards")) rowCount += rows.length;
|
|
477
|
-
doc.addPage(
|
|
490
|
+
doc.addPage();
|
|
478
491
|
const wide = doc.internal.pageSize.getWidth() - margin * 2;
|
|
479
492
|
doc.setTextColor(...ink);
|
|
480
493
|
doc.setFont("helvetica", "bold");
|
|
@@ -496,7 +509,15 @@ export function useRmExport() {
|
|
|
496
509
|
startY: margin + 10,
|
|
497
510
|
head: [columns.map((column) => printable(column.column))],
|
|
498
511
|
body: rows.map((row) => columns.map((column) => printable(cellText(column.value(row), lang)))),
|
|
499
|
-
|
|
512
|
+
// Passe huit colonnes, la police et les marges se resserrent : c'est
|
|
513
|
+
// ce qui fait tenir un suivi mensuel complet sans casser les mots.
|
|
514
|
+
styles: {
|
|
515
|
+
fontSize: columns.length > 8 ? 5.5 : 6.5,
|
|
516
|
+
cellPadding: columns.length > 8 ? 1 : 1.4,
|
|
517
|
+
overflow: "linebreak",
|
|
518
|
+
textColor: ink,
|
|
519
|
+
minCellWidth: wide / columns.length / 2
|
|
520
|
+
},
|
|
500
521
|
headStyles: { fillColor: violet, textColor: [255, 255, 255], fontStyle: "bold", valign: "middle" },
|
|
501
522
|
alternateRowStyles: { fillColor: [250, 250, 253] },
|
|
502
523
|
margin: { left: margin, right: margin, top: margin, bottom: margin },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type RmCvdMode = 'none' | 'deutera' | 'protan' | '
|
|
1
|
+
export type RmCvdMode = 'none' | 'deutera' | 'protan' | 'tritan' | 'mono';
|
|
2
|
+
export type RmChartPalette = 'default' | 'cvdSafe';
|
|
2
3
|
export type RmTextScale = 'normal' | 'large' | 'xl';
|
|
3
4
|
export declare function useRomulusTheme(): {
|
|
4
5
|
night: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
@@ -7,11 +8,15 @@ export declare function useRomulusTheme(): {
|
|
|
7
8
|
compact: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
8
9
|
cvd: Readonly<import("vue").Ref<RmCvdMode, RmCvdMode>>;
|
|
9
10
|
textScale: Readonly<import("vue").Ref<RmTextScale, RmTextScale>>;
|
|
11
|
+
chartPalette: Readonly<import("vue").Ref<RmChartPalette, RmChartPalette>>;
|
|
12
|
+
chartDecal: Readonly<import("vue").Ref<boolean, boolean>>;
|
|
10
13
|
setNight: (value: boolean) => void;
|
|
11
14
|
setHighContrast: (value: boolean) => void;
|
|
12
15
|
setReduceMotion: (value: boolean) => void;
|
|
13
16
|
setCompact: (value: boolean) => void;
|
|
14
17
|
setCvd: (mode: RmCvdMode) => void;
|
|
15
18
|
setTextScale: (scale: RmTextScale) => void;
|
|
19
|
+
setChartPalette: (palette: RmChartPalette) => void;
|
|
20
|
+
setChartDecal: (value: boolean) => void;
|
|
16
21
|
reset: () => void;
|
|
17
22
|
};
|