Dark mode
This commit is contained in:
parent
b480885e7a
commit
0cd8eaf580
|
@ -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>
|
||||||
|
|
27
src/App.vue
27
src/App.vue
|
@ -1,10 +1,8 @@
|
||||||
<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">
|
||||||
|
<div class="px-2 w-full h-full">
|
||||||
|
<AppHeader class="" />
|
||||||
<!-- keep relative for transition -->
|
<!-- keep relative for transition -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<RouterView v-slot="{ Component, route }">
|
<RouterView v-slot="{ Component, route }">
|
||||||
|
@ -16,4 +14,21 @@ import AppHeader from './components/AppHeader.vue'
|
||||||
</RouterView>
|
</RouterView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<SideMenu />
|
||||||
|
</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>
|
||||||
|
|
|
@ -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>
|
||||||
|
<button @click="isSideMenuVisible = true">
|
||||||
<IconMenu
|
<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>
|
</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'
|
||||||
|
|
||||||
|
|
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;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@layer utils {
|
||||||
|
}
|
||||||
@layer components {
|
@layer components {
|
||||||
.btn {
|
.btn {
|
||||||
@apply w-fit p-2;
|
@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",
|
"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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: '/',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user