@tekkare/romulus 0.1.89 → 0.1.90

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 CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.16.0"
6
6
  },
7
- "version": "0.1.88",
7
+ "version": "0.1.89",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "0.8.4",
10
10
  "unbuild": "2.0.0"
@@ -0,0 +1,194 @@
1
+ <!--
2
+ RmA11yPanel - les reglages d'accessibilite, en rangees.
3
+
4
+ Porte le panneau « Accessibilite » du design system (`reference.html`) :
5
+ une rangee par reglage, chacune avec son icone, son intitule, une phrase
6
+ qui dit ce qu'elle change, et sa commande. La phrase n'est pas decorative :
7
+ « Simuler le daltonisme » et « Palette des charts » se ressemblent au point
8
+ de se confondre, et rien dans leur nom ne dit que l'un est un outil de test
9
+ et l'autre un reglage de confort.
10
+
11
+ Le panneau ne conserve aucun etat. Tout vit dans les classes de `body`,
12
+ posees par `useRomulusTheme`, et c'est ce qui permet a une preference
13
+ restauree au demarrage, ou a une bascule declenchee ailleurs (la bascule
14
+ jour / nuit du rail), d'etre reprise sans rien a synchroniser.
15
+
16
+ Trois reglages absents du panneau, alors que le composable les expose :
17
+
18
+ - La taille du texte et la densite d'affichage, tant que le CSS ne les
19
+ suit pas hors des composants Romulus. Les poser ferait une commande qui
20
+ change trois titres et laisse le reste de l'ecran en place, ce qui se lit
21
+ comme une panne.
22
+ - Le mode jour / nuit, qui a sa bascule a demeure (`RmThemeToggle`) :
23
+ l'enterrer dans un panneau qu'il faut ouvrir serait un recul.
24
+ -->
25
+ <script setup lang="ts">
26
+ import { computed } from 'vue'
27
+ import type { RmCvdMode } from '../composables/useTheme'
28
+ import { useRomulusTheme } from '../composables/useTheme'
29
+
30
+ const props = withDefaults(defineProps<{
31
+ lang?: 'fr' | 'en'
32
+ /** Chapeau du panneau. Vide, rien ne sort. */
33
+ description?: string
34
+ }>(), {
35
+ lang: 'fr',
36
+ description: undefined,
37
+ })
38
+
39
+ const {
40
+ cvd, chartPalette, chartDecal, highContrast, reduceMotion,
41
+ setCvd, setChartPalette, setChartDecal, setHighContrast, setReduceMotion, reset,
42
+ } = useRomulusTheme()
43
+
44
+ const WORDS = {
45
+ fr: {
46
+ intro: "Adaptez l'affichage à votre besoin. Les changements s'appliquent en temps réel à tout le dashboard et sont mémorisés pour vos prochaines sessions.",
47
+ cvd: 'Simuler le daltonisme',
48
+ cvdDesc: 'Affiche les charts à travers un filtre simulant la perception d\'un daltonien. Outil de test pour vérifier que vos visualisations restent lisibles.',
49
+ cvdNone: 'Aucun',
50
+ palette: 'Palette des charts',
51
+ paletteDesc: 'Default = palette Tekkare violet. CVD-safe = Okabe-Ito, scientifiquement validée pour les 3 types de daltonisme. Les charts se rechargent automatiquement avec la nouvelle palette.',
52
+ paletteDefault: 'Default Tekkare',
53
+ paletteCvd: 'CVD-safe Okabe-Ito',
54
+ decal: 'Patterns sur charts',
55
+ decalDesc: 'Ajoute un motif (hachures, points, lignes) sur chaque série, en plus de la couleur. Permet de distinguer les séries même en noir et blanc ou en daltonisme total.',
56
+ contrast: 'Contraste élevé',
57
+ contrastDesc: 'Renforce les couleurs de texte, épaissit les bordures, supprime les transparences.',
58
+ motion: 'Réduire les animations',
59
+ motionDesc: 'Désactive toutes les transitions et animations (troubles vestibulaires, ou simple préférence).',
60
+ reset: 'Réinitialiser tout',
61
+ },
62
+ en: {
63
+ intro: 'Adjust the display to suit you. Changes apply across the dashboard right away and are remembered for your next sessions.',
64
+ cvd: 'Simulate colour blindness',
65
+ cvdDesc: 'Shows charts through a filter simulating colour-blind perception. A test tool to check your visualisations stay readable.',
66
+ cvdNone: 'None',
67
+ palette: 'Chart palette',
68
+ paletteDesc: 'Default = Tekkare purple palette. CVD-safe = Okabe-Ito, scientifically validated for the 3 types of colour blindness. Charts reload automatically with the new palette.',
69
+ paletteDefault: 'Default Tekkare',
70
+ paletteCvd: 'CVD-safe Okabe-Ito',
71
+ decal: 'Chart patterns',
72
+ decalDesc: 'Adds a pattern (hatching, dots, lines) to each series, on top of colour. Series stay distinct in black and white, or with total colour blindness.',
73
+ contrast: 'High contrast',
74
+ contrastDesc: 'Strengthens text colours, thickens borders, removes transparency.',
75
+ motion: 'Reduce animations',
76
+ motionDesc: 'Turns off every transition and animation (vestibular disorders, or plain preference).',
77
+ reset: 'Reset everything',
78
+ },
79
+ } as const
80
+
81
+ const T = computed(() => WORDS[props.lang] ?? WORDS.fr)
82
+
83
+ /* Les quatre simulations gardent leur nom court, le meme en francais et en
84
+ anglais : ce sont les noms des deficiences, pas des libelles a traduire. */
85
+ const cvdOptions = computed(() => [
86
+ { value: 'none', label: T.value.cvdNone },
87
+ { value: 'deutera', label: 'Deutéra' },
88
+ { value: 'protan', label: 'Protan' },
89
+ { value: 'tritan', label: 'Tritan' },
90
+ { value: 'mono', label: 'Mono' },
91
+ ])
92
+
93
+ const paletteOptions = computed(() => [
94
+ { value: 'default', label: T.value.paletteDefault },
95
+ { value: 'cvdSafe', label: T.value.paletteCvd },
96
+ ])
97
+
98
+ /*
99
+ Les refs du composable sont en lecture seule : la commande passe par le
100
+ setter, seul endroit qui pose la classe et enregistre la preference. Les
101
+ proxys inscriptibles ci-dessous laissent quand meme utiliser `v-model`, qui
102
+ est la forme attendue des composants de saisie.
103
+ */
104
+ const cvdModel = computed({
105
+ get: () => cvd.value as string,
106
+ set: (value: string) => setCvd(value as RmCvdMode),
107
+ })
108
+
109
+ const paletteModel = computed({
110
+ get: () => chartPalette.value as string,
111
+ set: (value: string) => setChartPalette(value === 'cvdSafe' ? 'cvdSafe' : 'default'),
112
+ })
113
+
114
+ const decalModel = computed({
115
+ get: () => chartDecal.value,
116
+ set: setChartDecal,
117
+ })
118
+
119
+ const contrastModel = computed({
120
+ get: () => highContrast.value,
121
+ set: setHighContrast,
122
+ })
123
+
124
+ const motionModel = computed({
125
+ get: () => reduceMotion.value,
126
+ set: setReduceMotion,
127
+ })
128
+ </script>
129
+
130
+ <template>
131
+ <div class="rm-a11y">
132
+ <p class="rm-a11y__intro">{{ description ?? T.intro }}</p>
133
+
134
+ <div class="rm-a11y__row">
135
+ <div class="rm-a11y__head">
136
+ <span class="rm-a11y__label"><i class="ph ph-eye" />{{ T.cvd }}</span>
137
+ </div>
138
+ <p class="rm-a11y__desc">{{ T.cvdDesc }}</p>
139
+ <RmSegmented
140
+ v-model="cvdModel"
141
+ :options="cvdOptions"
142
+ :aria-label="T.cvd"
143
+ variant="soft"
144
+ block
145
+ />
146
+ </div>
147
+
148
+ <div class="rm-a11y__row">
149
+ <div class="rm-a11y__head">
150
+ <span class="rm-a11y__label"><i class="ph ph-palette" />{{ T.palette }}</span>
151
+ </div>
152
+ <p class="rm-a11y__desc">{{ T.paletteDesc }}</p>
153
+ <RmSegmented
154
+ v-model="paletteModel"
155
+ :options="paletteOptions"
156
+ :aria-label="T.palette"
157
+ variant="soft"
158
+ block
159
+ />
160
+ </div>
161
+
162
+ <div class="rm-a11y__row">
163
+ <div class="rm-a11y__head">
164
+ <span class="rm-a11y__label"><i class="ph ph-squares-four" />{{ T.decal }}</span>
165
+ <RmToggle v-model="decalModel" :label="T.decal" />
166
+ </div>
167
+ <p class="rm-a11y__desc rm-a11y__desc--last">{{ T.decalDesc }}</p>
168
+ </div>
169
+
170
+ <div class="rm-a11y__row">
171
+ <div class="rm-a11y__head">
172
+ <span class="rm-a11y__label"><i class="ph ph-circle-half" />{{ T.contrast }}</span>
173
+ <RmToggle v-model="contrastModel" :label="T.contrast" />
174
+ </div>
175
+ <p class="rm-a11y__desc rm-a11y__desc--last">{{ T.contrastDesc }}</p>
176
+ </div>
177
+
178
+ <div class="rm-a11y__row">
179
+ <div class="rm-a11y__head">
180
+ <span class="rm-a11y__label"><i class="ph ph-pause" />{{ T.motion }}</span>
181
+ <RmToggle v-model="motionModel" :label="T.motion" />
182
+ </div>
183
+ <p class="rm-a11y__desc rm-a11y__desc--last">{{ T.motionDesc }}</p>
184
+ </div>
185
+
186
+ <button class="rm-a11y__reset" type="button" @click="reset()">
187
+ <i class="ph ph-arrow-counter-clockwise" />{{ T.reset }}
188
+ </button>
189
+ </div>
190
+ </template>
191
+
192
+ <style scoped>
193
+ .rm-a11y{font-family:var(--font-sans);width:100%}.rm-a11y__intro{color:var(--text-secondary);font-size:var(--font-13);line-height:1.5;margin:0 0 18px}.rm-a11y__row{border-bottom:1px solid var(--border-soft);padding:14px 0}.rm-a11y__row:last-of-type{border-bottom:none}.rm-a11y__head{align-items:center;display:flex;gap:12px;justify-content:space-between;margin-bottom:8px}.rm-a11y__label{align-items:center;color:var(--text-primary);display:inline-flex;font-size:var(--font-13);font-weight:600;gap:8px}.rm-a11y__label i{color:var(--color-primary);font-size:16px}.rm-a11y__desc{color:var(--text-muted);font-size:var(--font-12);line-height:1.5;margin:0 0 10px}.rm-a11y__desc--last{margin-bottom:0}.rm-a11y__reset{align-items:center;background:var(--bg-surface);border:1px solid var(--border-default);border-radius:var(--radius-md);color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-size:var(--font-13);font-weight:600;gap:6px;justify-content:center;margin-top:14px;padding:10px 14px;transition:background var(--duration-fast) var(--ease-in-out),color var(--duration-fast) var(--ease-in-out),border-color var(--duration-fast) var(--ease-in-out);width:100%}.rm-a11y__reset:hover{background:var(--color-danger-bg);border-color:var(--color-danger);color:var(--color-danger)}.rm-a11y__reset:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}
194
+ </style>
@@ -43,8 +43,14 @@ const props = withDefaults(defineProps<{
43
43
  * surface, bord arrondi complet, hauteur de rangee) : c'est la forme a
44
44
  * utiliser quand le rail se pose SUR la barre, a cote de « Filtres », et non
45
45
  * dans le panneau. `default` garde le rail voile des panneaux et des cartes.
46
+ *
47
+ * `soft` inverse le pave : creux clair, pave de surface, libelle actif en
48
+ * violet au lieu de blanc sur violet. C'est la forme des reglages, la ou le
49
+ * rail occupe toute la largeur d'un panneau et se repete d'une rangee a
50
+ * l'autre : un pave plein par rangee ferait une colonne de blocs colores,
51
+ * et l'oeil ne saurait plus lequel est le reglage qu'on vient de changer.
46
52
  */
47
- variant?: 'default' | 'pill'
53
+ variant?: 'default' | 'pill' | 'soft'
48
54
  ariaLabel?: string
49
55
  }>(), {
50
56
  size: 'md',
@@ -168,5 +174,5 @@ watch(() => [model.value, items.value.length, props.block], remeasure)
168
174
  </template>
169
175
 
170
176
  <style scoped>
171
- .rm-seg{background:color-mix(in srgb,var(--color-primary) 5%,var(--bg-surface));border:1px solid var(--border-default);border-radius:var(--radius-lg);display:inline-flex;flex-wrap:wrap;font-family:var(--font-sans);gap:2px;max-width:100%;padding:3px;position:relative}.rm-seg--block{display:flex;width:100%}.rm-seg--block .rm-seg__option{flex:1}.rm-seg__thumb{background:var(--color-primary);border-radius:var(--radius-md);left:0;position:absolute;top:0;transition:transform var(--duration-base) var(--ease-in-out),width var(--duration-base) var(--ease-in-out),height var(--duration-base) var(--ease-in-out);z-index:0}.rm-seg__option{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:var(--radius-md);box-shadow:none;color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-weight:500;gap:6px;justify-content:center;margin:0;position:relative;transition:color var(--duration-fast) var(--ease-in-out);white-space:nowrap;z-index:1}.rm-seg--md .rm-seg__option{font-size:14px;line-height:20px;padding:5px 12px}.rm-seg--sm .rm-seg__option{font-size:13px;line-height:18px;padding:3px 10px}.rm-seg__option i{font-size:16px;line-height:1}.rm-seg__option:hover:not(:disabled):not(.rm-seg__option--active){color:var(--color-primary)}.rm-seg__option--active{color:#fff;font-weight:600}.rm-seg__option:disabled{color:var(--text-muted);cursor:not-allowed;opacity:.6}.rm-seg__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:1px}.rm-seg--pill{background:var(--bg-surface);border-radius:var(--radius-pill);flex-wrap:nowrap;gap:0;overflow-x:auto;padding:2px;scrollbar-width:none}.rm-seg--pill::-webkit-scrollbar{display:none}.rm-seg--pill .rm-seg__option,.rm-seg--pill .rm-seg__thumb{border-radius:var(--radius-pill)}.rm-seg--pill .rm-seg__option{font-size:var(--font-14);height:26px;line-height:26px;padding:0 14px}.rm-seg--pill.rm-seg--sm .rm-seg__option{font-size:var(--font-13);height:24px;line-height:24px;padding:0 12px}.rm-seg--pill .rm-seg__option--sep:before{left:0}.rm-seg__option--sep:before{background:var(--border-default);content:"";height:60%;left:-2px;position:absolute;top:20%;width:1px}@media (prefers-reduced-motion:reduce){.rm-seg__thumb{transition:none}}
177
+ .rm-seg{background:color-mix(in srgb,var(--color-primary) 5%,var(--bg-surface));border:1px solid var(--border-default);border-radius:var(--radius-lg);display:inline-flex;flex-wrap:wrap;font-family:var(--font-sans);gap:2px;max-width:100%;padding:3px;position:relative}.rm-seg--block{display:flex;width:100%}.rm-seg--block .rm-seg__option{flex:1}.rm-seg__thumb{background:var(--color-primary);border-radius:var(--radius-md);left:0;position:absolute;top:0;transition:transform var(--duration-base) var(--ease-in-out),width var(--duration-base) var(--ease-in-out),height var(--duration-base) var(--ease-in-out);z-index:0}.rm-seg__option{align-items:center;-moz-appearance:none;appearance:none;-webkit-appearance:none;background:none;border:none;border-radius:var(--radius-md);box-shadow:none;color:var(--text-secondary);cursor:pointer;display:inline-flex;font-family:inherit;font-weight:500;gap:6px;justify-content:center;margin:0;position:relative;transition:color var(--duration-fast) var(--ease-in-out);white-space:nowrap;z-index:1}.rm-seg--md .rm-seg__option{font-size:14px;line-height:20px;padding:5px 12px}.rm-seg--sm .rm-seg__option{font-size:13px;line-height:18px;padding:3px 10px}.rm-seg__option i{font-size:16px;line-height:1}.rm-seg__option:hover:not(:disabled):not(.rm-seg__option--active){color:var(--color-primary)}.rm-seg__option--active{color:#fff;font-weight:600}.rm-seg__option:disabled{color:var(--text-muted);cursor:not-allowed;opacity:.6}.rm-seg__option:focus-visible{outline:2px solid var(--color-primary);outline-offset:1px}.rm-seg--pill{background:var(--bg-surface);border-radius:var(--radius-pill);flex-wrap:nowrap;gap:0;overflow-x:auto;padding:2px;scrollbar-width:none}.rm-seg--pill::-webkit-scrollbar{display:none}.rm-seg--pill .rm-seg__option,.rm-seg--pill .rm-seg__thumb{border-radius:var(--radius-pill)}.rm-seg--pill .rm-seg__option{font-size:var(--font-14);height:26px;line-height:26px;padding:0 14px}.rm-seg--pill.rm-seg--sm .rm-seg__option{font-size:var(--font-13);height:24px;line-height:24px;padding:0 12px}.rm-seg--soft{background:var(--bg-surface-3);border-radius:var(--radius-pill);box-shadow:inset 0 1px 2px rgba(30,41,59,.04);gap:0;padding:3px}.rm-seg--soft .rm-seg__thumb{background:var(--bg-surface);border-radius:var(--radius-pill);box-shadow:var(--shadow-sm)}.rm-seg--soft .rm-seg__option{border-radius:var(--radius-pill);color:var(--text-muted);font-size:var(--font-12);padding:7px 10px}.rm-seg--soft .rm-seg__option:hover:not(:disabled):not(.rm-seg__option--active){color:var(--text-primary)}.rm-seg--soft .rm-seg__option--active{color:var(--color-secondary);font-weight:600}.rm-seg--soft .rm-seg__option--sep:before{display:none}.rm-seg--pill .rm-seg__option--sep:before{left:0}.rm-seg__option--sep:before{background:var(--border-default);content:"";height:60%;left:-2px;position:absolute;top:20%;width:1px}@media (prefers-reduced-motion:reduce){.rm-seg__thumb{transition:none}}
172
178
  </style>
@@ -0,0 +1,47 @@
1
+ <!--
2
+ RmToggle - bascule simple, allumee ou eteinte.
3
+
4
+ Reprend le `.a11y-switch` du design system : une pastille de 40x22 posee au
5
+ bout d'une rangee de reglage, dont le libelle est le texte de la rangee.
6
+
7
+ A ne pas confondre avec `RmSwitch`, qui encadre la bascule de deux intitules
8
+ parce que ses deux etats ont un nom (« Volumes » / « Valeurs »). Ici un seul
9
+ etat est nomme, l'autre est son absence : « Contraste eleve » est actif ou
10
+ ne l'est pas, et lui inventer un intitule contraire (« Contraste normal »)
11
+ ferait lire deux reglages la ou il n'y en a qu'un.
12
+
13
+ Le libelle n'est pas rendu par le composant : dans le panneau il vit dans
14
+ l'en-tete de la rangee, avec son icone, et le rappeler ici le dirait deux
15
+ fois a un lecteur d'ecran. `label` sert donc de nom accessible seul, et il
16
+ est obligatoire : une bascule sans nom ne s'annonce pas.
17
+ -->
18
+ <script setup lang="ts">
19
+ withDefaults(defineProps<{
20
+ /** Nom accessible de la bascule. Non affiche. */
21
+ label: string
22
+ disabled?: boolean
23
+ }>(), {
24
+ disabled: false,
25
+ })
26
+
27
+ const on = defineModel<boolean>({ default: false })
28
+ </script>
29
+
30
+ <template>
31
+ <button
32
+ class="rm-toggle"
33
+ :class="{ 'rm-toggle--on': on }"
34
+ type="button"
35
+ role="switch"
36
+ :aria-checked="on"
37
+ :aria-label="label"
38
+ :disabled="disabled"
39
+ @click="on = !on"
40
+ >
41
+ <span class="rm-toggle__knob" aria-hidden="true" />
42
+ </button>
43
+ </template>
44
+
45
+ <style scoped>
46
+ .rm-toggle{background:var(--border-default);border:none;border-radius:var(--radius-pill);cursor:pointer;flex-shrink:0;height:22px;padding:0;position:relative;transition:background var(--duration-fast) var(--ease-in-out);width:40px}.rm-toggle--on{background:var(--color-primary)}.rm-toggle:disabled{cursor:not-allowed;opacity:.6}.rm-toggle:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}.rm-toggle__knob{background:var(--bg-surface);border-radius:50%;box-shadow:0 1px 3px rgba(0,0,0,.15);height:18px;left:2px;position:absolute;top:2px;transition:transform var(--duration-fast) var(--ease-in-out);width:18px}.rm-toggle--on .rm-toggle__knob{transform:translateX(18px)}@media (prefers-reduced-motion:reduce){.rm-toggle,.rm-toggle__knob{transition:none}}
47
+ </style>
@@ -2,10 +2,19 @@ export type RmCvdMode = 'none' | 'deutera' | 'protan' | 'tritan' | 'mono';
2
2
  export type RmChartPalette = 'default' | 'cvdSafe';
3
3
  export type RmTextScale = 'normal' | 'large' | 'xl';
4
4
  /**
5
- * Mode nuit enregistre au dernier passage. Lu par le plugin de demarrage, qui
6
- * pose la classe avant le premier rendu.
5
+ * Pose sur `body` les classes enregistrees au dernier passage.
6
+ *
7
+ * Appelee par le plugin de demarrage, avant le premier rendu. Le composable
8
+ * ne peut pas s'en charger : il ne lit `body` qu'au montage d'un composant,
9
+ * et l'ecran s'afficherait donc en clair, en taille standard, sans contraste
10
+ * renforce, avant de basculer — exactement le saut que ces reglages sont la
11
+ * pour eviter, et le plus penible pour qui a besoin du contraste.
12
+ *
13
+ * Ne passe pas par les setters a dessein : la garde de transitions n'a rien a
14
+ * neutraliser avant le premier rendu, et reecrire les preferences qu'on vient
15
+ * de lire n'apporte rien.
7
16
  */
8
- export declare function rmSavedNight(): boolean;
17
+ export declare function rmApplySavedPrefs(): void;
9
18
  export declare function useRomulusTheme(): {
10
19
  night: Readonly<import("vue").Ref<boolean, boolean>>;
11
20
  highContrast: Readonly<import("vue").Ref<boolean, boolean>>;
@@ -24,11 +24,23 @@ function writePref(key, value) {
24
24
  } catch {
25
25
  }
26
26
  }
27
- export function rmSavedNight() {
28
- return readPrefs().night === true;
29
- }
30
27
  const PALETTE_CLASS = "pal-cvd-safe";
31
28
  const DECAL_CLASS = "chart-decal";
29
+ export function rmApplySavedPrefs() {
30
+ const prefs = readPrefs();
31
+ const { classList } = document.body;
32
+ const add = (value, allowed) => {
33
+ if (typeof value === "string" && allowed.includes(value)) classList.add(value);
34
+ };
35
+ classList.toggle("night", prefs.night === true);
36
+ classList.toggle("high-contrast", prefs["high-contrast"] === true);
37
+ classList.toggle("reduce-motion", prefs["reduce-motion"] === true);
38
+ classList.toggle(DECAL_CLASS, prefs.decal === true);
39
+ classList.toggle(PALETTE_CLASS, prefs.palette === "pal-cvdSafe");
40
+ classList.toggle("density-compact", prefs.density === "density-compact");
41
+ add(prefs.text, Object.values(TEXT_CLASSES));
42
+ add(prefs.cvd, Object.values(CVD_CLASSES));
43
+ }
32
44
  export function useRomulusTheme() {
33
45
  const night = ref(false);
34
46
  const highContrast = ref(false);
@@ -47,34 +59,41 @@ export function useRomulusTheme() {
47
59
  const setHighContrast = (value) => apply(() => {
48
60
  highContrast.value = value;
49
61
  document.body.classList.toggle("high-contrast", value);
62
+ writePref("high-contrast", value);
50
63
  });
51
64
  const setReduceMotion = (value) => apply(() => {
52
65
  reduceMotion.value = value;
53
66
  document.body.classList.toggle("reduce-motion", value);
67
+ writePref("reduce-motion", value);
54
68
  });
55
69
  const setCompact = (value) => apply(() => {
56
70
  compact.value = value;
57
71
  document.body.classList.toggle("density-compact", value);
72
+ writePref("density", value ? "density-compact" : "density-default");
58
73
  });
59
74
  const setCvd = (mode) => apply(() => {
60
75
  cvd.value = mode;
61
76
  const { body } = document;
62
77
  Object.values(CVD_CLASSES).forEach((c) => body.classList.remove(c));
63
78
  if (mode !== "none") body.classList.add(CVD_CLASSES[mode]);
79
+ writePref("cvd", mode === "none" ? "cvd-none" : CVD_CLASSES[mode]);
64
80
  });
65
81
  const setTextScale = (scale) => apply(() => {
66
82
  textScale.value = scale;
67
83
  const { body } = document;
68
84
  Object.values(TEXT_CLASSES).forEach((c) => body.classList.remove(c));
69
85
  if (scale !== "normal") body.classList.add(TEXT_CLASSES[scale]);
86
+ writePref("text", scale === "normal" ? "text-default" : TEXT_CLASSES[scale]);
70
87
  });
71
88
  const setChartPalette = (palette) => apply(() => {
72
89
  chartPalette.value = palette;
73
90
  document.body.classList.toggle(PALETTE_CLASS, palette === "cvdSafe");
91
+ writePref("palette", palette === "cvdSafe" ? "pal-cvdSafe" : "pal-default");
74
92
  });
75
93
  const setChartDecal = (value) => apply(() => {
76
94
  chartDecal.value = value;
77
95
  document.body.classList.toggle(DECAL_CLASS, value);
96
+ writePref("decal", value);
78
97
  });
79
98
  const reset = () => apply(() => {
80
99
  const { body } = document;
@@ -86,7 +105,6 @@ export function useRomulusTheme() {
86
105
  textScale.value = "normal";
87
106
  chartPalette.value = "default";
88
107
  chartDecal.value = false;
89
- writePref("night", false);
90
108
  body.classList.remove(
91
109
  "night",
92
110
  "high-contrast",
@@ -97,6 +115,10 @@ export function useRomulusTheme() {
97
115
  ...Object.values(CVD_CLASSES),
98
116
  ...Object.values(TEXT_CLASSES)
99
117
  );
118
+ try {
119
+ localStorage.removeItem(STORE_KEY);
120
+ } catch {
121
+ }
100
122
  });
101
123
  onMounted(() => {
102
124
  const list = document.body.classList;
@@ -1,5 +1,5 @@
1
1
  import { defineNuxtPlugin } from "#app";
2
- import { rmSavedNight } from "../composables/useTheme.js";
2
+ import { rmApplySavedPrefs } from "../composables/useTheme.js";
3
3
  export default defineNuxtPlugin(() => {
4
- if (rmSavedNight()) document.body.classList.add("night");
4
+ rmApplySavedPrefs();
5
5
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tekkare/romulus",
3
- "version": "0.1.89",
3
+ "version": "0.1.90",
4
4
  "description": "Romulus Design System - Nuxt module by Tekkare",
5
5
  "license": "MIT",
6
6
  "type": "module",