Dark mode
This commit is contained in:
parent
b480885e7a
commit
0cd8eaf580
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no" class="dark h-full">
|
||||
<html lang="en" translate="no" class="dark h-full bg-stone-900">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
@ -8,7 +8,7 @@
|
|||
<title>N0mbers</title>
|
||||
</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>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
|
27
src/App.vue
27
src/App.vue
|
@ -1,10 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import AppHeader from './components/AppHeader.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container flex overflow-hidden flex-col px-2 mx-auto max-w-md h-full">
|
||||
<AppHeader />
|
||||
<div
|
||||
class="container flex overflow-hidden relative flex-col mx-auto max-w-md h-full border border-cyan-500">
|
||||
<div class="px-2 w-full h-full">
|
||||
<AppHeader class="" />
|
||||
<!-- keep relative for transition -->
|
||||
<div class="relative">
|
||||
<RouterView v-slot="{ Component, route }">
|
||||
|
@ -16,4 +14,21 @@ import AppHeader from './components/AppHeader.vue'
|
|||
</RouterView>
|
||||
</div>
|
||||
</div>
|
||||
<SideMenu />
|
||||
</div>
|
||||
</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>
|
||||
|
|
|
@ -3,14 +3,17 @@
|
|||
<RouterLink
|
||||
:class="{ 'opacity-0': isHome }"
|
||||
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 />
|
||||
</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
|
||||
</h1>
|
||||
<button @click="isSideMenuVisible = true">
|
||||
<IconMenu
|
||||
class="text-xl text-stone-400 hover:text-cyan-500 transition-opacity" />
|
||||
class="text-xl hover:text-cyan-500 dark:text-stone-400 transition-opacity" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -18,6 +21,7 @@
|
|||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { isSideMenuVisible } from '@/composables/side-menu'
|
||||
import IconHouse from '~icons/ph/house'
|
||||
import IconMenu from '~icons/ph/list'
|
||||
|
||||
|
|
44
src/components/SideMenu.vue
Normal file
44
src/components/SideMenu.vue
Normal 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>
|
46
src/components/common/InputSwitch.vue
Normal file
46
src/components/common/InputSwitch.vue
Normal 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>
|
20
src/composables/settings.ts
Normal file
20
src/composables/settings.ts
Normal 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
|
||||
)
|
||||
}
|
3
src/composables/side-menu.ts
Normal file
3
src/composables/side-menu.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { ref } from 'vue'
|
||||
|
||||
export const isSideMenuVisible = ref(false)
|
|
@ -142,6 +142,8 @@
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
@layer utils {
|
||||
}
|
||||
@layer components {
|
||||
.btn {
|
||||
@apply w-fit p-2;
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"playAgain": "Nouveau nombre",
|
||||
"endGame.victoryLabel": "Le compte est bon !",
|
||||
"endGame.victoryLabel": "Bien joué !",
|
||||
"endGame.failureLabel": "Dommage...<br>Une autre partie ?",
|
||||
"startGame": "Démarrer",
|
||||
"easy": "facile",
|
||||
|
@ -8,5 +8,8 @@
|
|||
"hard": "difficile",
|
||||
"impossible": "☠",
|
||||
"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."
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
|
||||
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
|
|
|
@ -12,8 +12,7 @@
|
|||
class="text-center"
|
||||
v-if="gameState <= GameState.Waiting">
|
||||
<div v-if="isDaily">
|
||||
Le défi quotidien vous propose un résultat différent chaque jour,
|
||||
commun à tous les joueurs.<br>
|
||||
{{ t('dailyDescription') }}<br>
|
||||
</div>
|
||||
|
||||
<button
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
<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>
|
||||
Combinez les nombres imposés afin d'atteindre le résultat, ou de vous en
|
||||
approcher le plus possible.<br>
|
||||
{{ t('gameDescription') }}<br>
|
||||
<br>
|
||||
</div>
|
||||
<!-- Daily -->
|
||||
|
@ -13,9 +12,8 @@
|
|||
class="text-2xl btn">
|
||||
{{ t('dailyGame') }}
|
||||
</RouterLink>
|
||||
<span>
|
||||
Un nombre à atteindre, unique pour la journée,<br>commun à tous les
|
||||
joueurs
|
||||
<span v-html="t('dailyDescription')">
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -26,7 +24,7 @@
|
|||
class="text-2xl btn">
|
||||
{{ t('randomGame') }}
|
||||
</RouterLink>
|
||||
Une partie au hasard, pour le plaisir
|
||||
{{ t('randomDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user