Dark mode

This commit is contained in:
Simon Cambier 2022-02-21 22:52:29 +01:00
parent b480885e7a
commit 0cd8eaf580
13 changed files with 179 additions and 35 deletions

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" translate="no" class="dark h-full"> <html lang="en" translate="no" class="dark h-full bg-stone-900">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@ -8,7 +8,7 @@
<title>N0mbers</title> <title>N0mbers</title>
</head> </head>
<body class="bg-stone-200 text-stone-900 dark:bg-stone-900 dark:text-stone-200 h-full"> <body class="bg-stone-300 text-stone-900 dark:bg-stone-900 dark:text-stone-200 h-full relative transition-colors">
<div id="app" class="h-full"></div> <div id="app" class="h-full"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

View File

@ -1,19 +1,34 @@
<script setup lang="ts">
import AppHeader from './components/AppHeader.vue'
</script>
<template> <template>
<div class="container flex overflow-hidden flex-col px-2 mx-auto max-w-md h-full"> <div
<AppHeader /> class="container flex overflow-hidden relative flex-col mx-auto max-w-md h-full border border-cyan-500">
<!-- keep relative for transition --> <div class="px-2 w-full h-full">
<div class="relative"> <AppHeader class="" />
<RouterView v-slot="{ Component, route }"> <!-- keep relative for transition -->
<Transition :name="route.meta.transition"> <div class="relative">
<Component <RouterView v-slot="{ Component, route }">
:is="Component" <Transition :name="route.meta.transition">
:key="route.path" /> <Component
</Transition> :is="Component"
</RouterView> :key="route.path" />
</Transition>
</RouterView>
</div>
</div> </div>
<SideMenu />
</div> </div>
</template> </template>
<script setup lang="ts">
import { onMounted } from 'vue'
import { darkMode, isDarkModeDefault } from '@/composables/settings'
import AppHeader from './components/AppHeader.vue'
import SideMenu from './components/SideMenu.vue'
onMounted(() => {
darkMode.value = localStorage.getItem('n0_dark')
? localStorage.getItem('n0_dark') === 'true'
: isDarkModeDefault()
})
</script>

View File

@ -3,14 +3,17 @@
<RouterLink <RouterLink
:class="{ 'opacity-0': isHome }" :class="{ 'opacity-0': isHome }"
to="/" to="/"
class="text-xl text-stone-400 hover:text-cyan-500 transition-opacity duration-200"> class="text-xl hover:text-cyan-500 dark:text-stone-400 transition-opacity duration-200">
<IconHouse /> <IconHouse />
</RouterLink> </RouterLink>
<h1 class="py-2 font-mono text-3xl text-center"> <h1
class="z-20 py-2 font-mono text-3xl text-center bg-stone-300 dark:bg-stone-900">
N<span class="text-cyan-500">0</span>mbers N<span class="text-cyan-500">0</span>mbers
</h1> </h1>
<IconMenu <button @click="isSideMenuVisible = true">
class="text-xl text-stone-400 hover:text-cyan-500 transition-opacity" /> <IconMenu
class="text-xl hover:text-cyan-500 dark:text-stone-400 transition-opacity" />
</button>
</div> </div>
</template> </template>
@ -18,6 +21,7 @@
import { computed } from 'vue' import { computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { isSideMenuVisible } from '@/composables/side-menu'
import IconHouse from '~icons/ph/house' import IconHouse from '~icons/ph/house'
import IconMenu from '~icons/ph/list' import IconMenu from '~icons/ph/list'

View File

@ -0,0 +1,44 @@
<template>
<Transition name="menu">
<div
v-if="isSideMenuVisible"
class="flex absolute top-[1px] right-0 z-10 flex-col w-[50%] h-full bg-stone-300 dark:bg-stone-900 border-l border-l-cyan-500">
<div class="h-12">
<button @click="isSideMenuVisible = false">
<IconClose
class="absolute top-0 right-1 h-12 text-xl hover:text-cyan-500" />
</button>
</div>
<div class="flex-1 ">
<div class="h-12" />
<InputSwitch
id="toggleNight"
lbl-left="Day"
lbl-right="Night"
v-model="darkMode" />
</div>
</div>
</Transition>
</template>
<style scoped>
.menu-move,
.menu-enter-active,
.menu-leave-active {
transition: all 0.5s ease;
}
.menu-enter-from,
.menu-leave-to {
/* opacity: 0; */
transform: translateX(100%);
}
</style>
<script setup lang="ts">
import { darkMode } from '@/composables/settings'
import { isSideMenuVisible } from '@/composables/side-menu'
import IconClose from '~icons/ph/x'
import InputSwitch from './common/InputSwitch.vue'
</script>

View File

@ -0,0 +1,46 @@
<template>
<!-- Toggle A -->
<div class="flex justify-center items-center mb-12 w-full">
<label
:for="id"
class="flex items-center cursor-pointer">
<div>{{ lblLeft }}</div>
<!-- toggle -->
<div class="relative mx-2 ml-3">
<!-- input -->
<input
:checked="modelValue"
@change="e => emit('update:modelValue', (e?.target as any)?.checked)"
:id="id"
type="checkbox"
class="sr-only">
<!-- line -->
<div class="w-10 h-4 bg-cyan-500 rounded-full shadow-inner" />
<!-- dot -->
<div
class="absolute -top-1 -left-1 w-6 h-6 bg-stone-400 rounded-full shadow transition dot" />
</div>
<!-- label -->
<div>{{ lblRight }}</div>
</label>
</div>
</template>
<style scoped>
/* Toggle A */
input:checked ~ .dot {
transform: translateX(100%);
/* background-color: #48bb78; */
}
</style>
<script setup lang="ts">
defineProps<{
modelValue: boolean
id: string
lblLeft: string
lblRight: string
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
}>()
</script>

View File

@ -0,0 +1,20 @@
import { ref, watch } from 'vue'
export const darkMode = ref(true)
watch(darkMode, val => {
if (val) {
document.documentElement.classList.add('dark')
}
else {
document.documentElement.classList.remove('dark')
}
localStorage.setItem('n0_dark', val ? 'true' : 'false')
})
export function isDarkModeDefault(): boolean {
return (
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
)
}

View File

@ -0,0 +1,3 @@
import { ref } from 'vue'
export const isSideMenuVisible = ref(false)

View File

@ -142,10 +142,12 @@
opacity: 0; opacity: 0;
} }
@layer utils {
}
@layer components { @layer components {
.btn { .btn {
@apply w-fit p-2; @apply w-fit p-2;
@apply rounded border border-stone-600 transition-opacity; @apply rounded border border-stone-600 transition-opacity;
@apply hover:border-cyan-500 hover:text-cyan-500; @apply hover:border-cyan-500 hover:text-cyan-500;
} }
} }

View File

@ -1,3 +1,13 @@
{ {
"playAgain": "New number",
"easy": "easy",
"dailyGame": "Daily challenge\n",
"endGame.failureLabel": "Too bad...<br>Another game?",
"endGame.victoryLabel": "Well done!",
"hard": "hard",
"impossible": "☠",
"medium": "medium",
"randomGame": "Random number",
"startGame": "Start",
"dailyDescription": "The daily challenge changes every day at midnight, and is common to all players."
} }

View File

@ -1,6 +1,6 @@
{ {
"playAgain": "Nouveau nombre", "playAgain": "Nouveau nombre",
"endGame.victoryLabel": "Le compte est bon !", "endGame.victoryLabel": "Bien joué !",
"endGame.failureLabel": "Dommage...<br>Une autre partie ?", "endGame.failureLabel": "Dommage...<br>Une autre partie ?",
"startGame": "Démarrer", "startGame": "Démarrer",
"easy": "facile", "easy": "facile",
@ -8,5 +8,8 @@
"hard": "difficile", "hard": "difficile",
"impossible": "☠", "impossible": "☠",
"dailyGame": "Défi quotidien", "dailyGame": "Défi quotidien",
"randomGame": "Nombre au hasard" "randomGame": "Nombre au hasard",
"gameDescription": "Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en approcher le plus possible.",
"dailyDescription": "Le défi quotidien change chaque jour à minuit, et est commun à tous les joueurs.",
"randomDescription": "Une partie au hasard, pour le plaisir."
} }

View File

@ -1,9 +1,9 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',

View File

@ -12,8 +12,7 @@
class="text-center" class="text-center"
v-if="gameState <= GameState.Waiting"> v-if="gameState <= GameState.Waiting">
<div v-if="isDaily"> <div v-if="isDaily">
Le défi quotidien vous propose un résultat différent chaque jour, {{ t('dailyDescription') }}<br>
commun à tous les joueurs.<br>
</div> </div>
<button <button

View File

@ -2,8 +2,7 @@
<div class="flex flex-col grow p-1 w-full text-center"> <div class="flex flex-col grow p-1 w-full text-center">
<div class="flex flex-col grow gap-16 justify-center md:mt-16"> <div class="flex flex-col grow gap-16 justify-center md:mt-16">
<div> <div>
Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en {{ t('gameDescription') }}<br>
approcher le plus possible.<br>
<br> <br>
</div> </div>
<!-- Daily --> <!-- Daily -->
@ -13,9 +12,8 @@
class="text-2xl btn"> class="text-2xl btn">
{{ t('dailyGame') }} {{ t('dailyGame') }}
</RouterLink> </RouterLink>
<span> <span v-html="t('dailyDescription')">
Un nombre à atteindre, unique pour la journée,<br>commun à tous les
joueurs
</span> </span>
</div> </div>
@ -26,7 +24,7 @@
class="text-2xl btn"> class="text-2xl btn">
{{ t('randomGame') }} {{ t('randomGame') }}
</RouterLink> </RouterLink>
Une partie au hasard, pour le plaisir {{ t('randomDescription') }}
</div> </div>
</div> </div>
</div> </div>