Skip to content

Commit 192dec6

Browse files
committed
fix: current color mode
Saving the current `color mode` in a cookie to avoid `hydration issues`, as this way the `server` already knows the current `color mode`. It was necessary to use `useCookie()` to save the `color mode` instead of the `storage setting` of `nuxtjs/color-mode`, as the cookie value otherwise will be applied to the current `/path`. Resulting in different `color modes` for different `paths. See: nuxt-modules/color-mode#301
1 parent 733cc97 commit 192dec6

File tree

11 files changed

+162
-189
lines changed

11 files changed

+162
-189
lines changed

frontend/components/bc/BcThemeToggle.vue

Lines changed: 0 additions & 98 deletions
This file was deleted.

frontend/components/bc/footer/BcFooterMain.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ const { t: $t } = useTranslation()
136136
<span class="main-footer__meta_seperator">
137137
|
138138
</span>
139-
<BcThemeToggle />
139+
<ClientOnly>
140+
<BaseColormode />
141+
</ClientOnly>
140142
</p>
141143
</footer>
142144
</template>

frontend/composables/useBcCookie.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type CookieName
99
| 'bc-cookies-preference'
1010
| 'bc-user-dashboards'
1111
| 'bc-validator-dashboard-key'
12+
| 'theme'
1213

1314
// for now without the other `type overload` there is no way to use
1415
// `readonly` feature of `useCookieNuxt` (we might adapt this if needed)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<script setup lang="ts">
2+
const colorMode = useColorMode()
3+
const cookie = useBcCookie<'dark' | 'light'>('theme')
4+
colorMode.preference = cookie.value || 'dark'
5+
const handleUpdate = () => {
6+
cookie.value = colorMode.preference as 'dark' | 'light'
7+
}
8+
</script>
9+
10+
<template>
11+
<BaseSwitch
12+
v-model="colorMode.preference"
13+
class="w-fit"
14+
:values="[
15+
{
16+
label: $t('base.footer.color_mode.light'),
17+
key: 'light',
18+
},
19+
{
20+
label: $t('base.footer.color_mode.dark'),
21+
key: 'dark',
22+
},
23+
]"
24+
:class-list="{
25+
trackItem: 'p-md +py-md +px-lg border border-transparent rounded-4xl text-gray-600 dark:text-gray-400 has-checked:text-black has-checked:dark:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-brand-300',
26+
track: 'border border-gray-100 dark:border-gray-900 flex gap-md [&>*]:grow text-center bg-white dark:bg-gray-950 rounded-4xl shadow-[0_1px_0.5px_0_rgba(255,255,255,0.08)_inset,0-1px_0_0_rgba(255,255,255,0.18)_inset]',
27+
thumb: 'bg-gray-50 dark:bg-gray-800 rounded-4xl shadow-[0_2px_2px_0_rgba(0,0,0,0.25),_0_0.5px_0.5px_0_rgba(255,255,255,0.12)_inset]',
28+
}"
29+
screenreader-title="base.footer.color_mode.title"
30+
@update:model-value="handleUpdate"
31+
>
32+
<template #light=" { label } ">
33+
<BaseIcon name="sun" />
34+
<span class="sr-only">{{ label }}</span>
35+
</template>
36+
<template #dark=" { label } ">
37+
<BaseIcon name="moon" />
38+
<span class="sr-only">{{ label }}</span>
39+
</template>
40+
</BaseSwitch>
41+
</template>
42+
43+
<style scoped></style>

frontend/layers/base/app/components/BaseSwitch.vue

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,41 @@ const { values } = defineProps<{
1212
screenreaderTitle: TranslationInput,
1313
values: T,
1414
}>()
15-
const idFirstValue = useId()
16-
const idSecondValue = useId()
15+
const id = useId()
1716
const name = useId()
18-
const modelValue = defineModel<typeof values[number]['key']>()
17+
const modelValue = defineModel<typeof values[number]['key']>({ required: true })
18+
const defaultValue = modelValue.value
1919
const { t: $t } = useTranslation()
20-
const thumb = useTemplateRef('thumb')
20+
const thumbs = useTemplateRef('thumb')
2121
const track = useTemplateRef('track')
2222
2323
const moveThumb = () => {
24+
// console.log('👉', thumbs.value)
2425
const activeTrackItem = track.value?.querySelector(':has(input[type="radio"]:checked)')
26+
const thumb = thumbs.value?.[0]
2527
if (!activeTrackItem) return
26-
if (!thumb.value) return
28+
if (!thumb) return
2729
28-
const { x: initialX } = thumb.value.getBoundingClientRect()
30+
const { x: initialX } = thumb.getBoundingClientRect()
2931
const { x } = activeTrackItem.getBoundingClientRect()
3032
3133
// this should rather have been done via view transition api
3234
// but it currently lacks `firefox support`
3335
// and also there was a flickering issue
34-
const animation = thumb.value.animate([ {
36+
const animation = thumb.animate([ {
3537
transform: `translateX(${x - initialX}px)`,
3638
} ],
3739
{ duration: 180 },
3840
)
3941
return animation.finished.then(() => {
40-
activeTrackItem.appendChild(thumb.value!)
42+
if (!thumb) return
43+
activeTrackItem.appendChild(thumb)
4144
})
4245
}
46+
4347
watch(modelValue, () => {
4448
moveThumb()
45-
})
49+
}, { immediate: true })
4650
</script>
4751

4852
<template>
@@ -55,7 +59,7 @@ watch(modelValue, () => {
5559
:class="classList?.track"
5660
v-bind="$attrs"
5761
>
58-
<label
62+
<!-- <label
5963
:for="idFirstValue"
6064
:class="classList?.trackItem"
6165
class="relative"
@@ -104,6 +108,37 @@ watch(modelValue, () => {
104108
type="radio"
105109
:name
106110
>
111+
</label> -->
112+
<label
113+
v-for="(value, index) in values"
114+
:key="value.key"
115+
:for="`${id}-${index}`"
116+
:class="classList?.trackItem"
117+
class="relative"
118+
>
119+
<span class="relative z-10">
120+
<slot
121+
:name="value.key"
122+
:label="values[index]?.label"
123+
>
124+
{{ values[index]?.label }}
125+
</slot>
126+
</span>
127+
<input
128+
:id="`${id}-${index}`"
129+
v-model="modelValue"
130+
:value="values[index]?.key"
131+
type="radio"
132+
class="sr-only"
133+
:name
134+
>
135+
<span
136+
v-if="value.key === defaultValue"
137+
ref="thumb"
138+
class="absolute inset-[0] z-0"
139+
aria-hidden="true"
140+
:class="classList?.thumb"
141+
/>
107142
</label>
108143
</div>
109144
</fieldset>

frontend/layers/base/app/components/TheFooter.vue

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ const resourceLinks: Link[] = [
4040
href: 'https://status.beaconcha.in/', icon: 'circle-check-filled', text: $t('base.footer.resources.links.site_status'),
4141
},
4242
]
43-
const colorMode = useColorMode()
4443
</script>
4544

4645
<template>
@@ -169,35 +168,9 @@ const colorMode = useColorMode()
169168
/>
170169
</li>
171170
</ul>
172-
<BaseSwitch
173-
v-model="colorMode.value"
174-
class="w-fit -ml-[1px]"
175-
:values="[
176-
{
177-
label: $t('base.footer.color_mode.light'),
178-
key: 'light',
179-
},
180-
{
181-
label: $t('base.footer.color_mode.dark'),
182-
key: 'dark',
183-
},
184-
]"
185-
:class-list="{
186-
trackItem: 'p-md +py-md +px-lg border border-transparent rounded-4xl text-gray-600 dark:text-gray-400 has-checked:text-black has-checked:dark:text-white has-focus-visible:outline-2 has-focus-visible:outline-offset-2 has-focus-visible:outline-brand-300',
187-
track: 'border border-gray-100 dark:border-gray-900 flex gap-md [&>*]:grow text-center bg-white dark:bg-gray-950 rounded-4xl shadow-[0_1px_0.5px_0_rgba(255,255,255,0.08)_inset,0-1px_0_0_rgba(255,255,255,0.18)_inset]',
188-
thumb: 'bg-gray-50 dark:bg-gray-800 rounded-4xl shadow-[0_2px_2px_0_rgba(0,0,0,0.25),_0_0.5px_0.5px_0_rgba(255,255,255,0.12)_inset]',
189-
}"
190-
screenreader-title="base.footer.color_mode.title"
191-
>
192-
<template #first=" { label } ">
193-
<BaseIcon name="sun" />
194-
<span class="sr-only">{{ label }}</span>
195-
</template>
196-
<template #second=" { label } ">
197-
<BaseIcon name="moon" />
198-
<span class="sr-only">{{ label }}</span>
199-
</template>
200-
</BaseSwitch>
171+
<BaseColormode
172+
class="-ml-[1px]"
173+
/>
201174
</div>
202175
</div>
203176

frontend/layers/base/nuxt.config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export default defineNuxtConfig({
1010
/* eslint-disable perfectionist/sort-objects -- as there is a conflict with `nuxt specific eslint rules` */
1111
modules: [
1212
'@nuxt/icon',
13+
'@nuxtjs/color-mode',
1314
'@nuxtjs/i18n',
1415
'reka-ui/nuxt',
1516
],
@@ -22,6 +23,16 @@ export default defineNuxtConfig({
2223
scrollBehaviorType: 'smooth',
2324
},
2425
},
26+
colorMode: {
27+
fallback: 'dark',
28+
preference: 'dark',
29+
dataValue: 'theme',
30+
storageKey: 'theme',
31+
// currently cookie storage is only applying the theme on the current path
32+
// See open PR: https://github.com/nuxt-modules/color-mode/pull/301
33+
// storage: 'cookie',
34+
storage: 'localStorage',
35+
},
2536
vite: {
2637
plugins: [ tailwindcss() ],
2738
},

frontend/layers/base/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"dependencies": {
3+
"@nuxtjs/color-mode": "^3.5.2",
34
"@tailwindcss/vite": "^4.1.12",
45
"reka-ui": "^2.5.0",
56
"tailwindcss": "^4.1.12"

frontend/nuxt.config.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ export default defineNuxtConfig({
2828
/* eslint-disable perfectionist/sort-objects -- as there is a conflict with `nuxt specific eslint rules` */
2929
modules: [
3030
'@nuxtjs/i18n',
31-
'@nuxtjs/color-mode',
3231
[
3332
'@pinia/nuxt',
3433
{ storesDirs: [ './stores/**' ] },
@@ -40,12 +39,6 @@ export default defineNuxtConfig({
4039
],
4140
ssr: process.env.ENABLE_SSR !== 'FALSE',
4241
devtools: { enabled: true },
43-
colorMode: {
44-
fallback: 'dark',
45-
preference: 'dark',
46-
dataValue: 'theme',
47-
storageKey: 'theme',
48-
},
4942
runtimeConfig: {
5043
private: {
5144
apiServer: process.env.PRIVATE_API_SERVER,

0 commit comments

Comments
 (0)