feat: add dark mode with flowbite

This commit is contained in:
2025-03-16 14:13:40 +01:00
parent e7e4749ecb
commit 2315c3297e
11 changed files with 479 additions and 308 deletions

View File

@@ -1 +1,35 @@
@import 'tailwindcss';
@plugin 'flowbite/plugin';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* https://www.tints.dev */
--color-primary-50: #e0faff;
--color-primary-100: #c7f6ff;
--color-primary-200: #8aebff;
--color-primary-300: #52e2ff;
--color-primary-400: #14d8ff;
--color-primary-500: #00b8db;
--color-primary-600: #0091ad;
--color-primary-700: #006f85;
--color-primary-800: #004857;
--color-primary-900: #00262e;
--color-primary-950: #001114;
--color-secondary-50: #e0fff5;
--color-secondary-100: #b3ffe5;
--color-secondary-200: #1affb2;
--color-secondary-300: #00eb9c;
--color-secondary-400: #00d68f;
--color-secondary-500: #00bc7d;
--color-secondary-600: #00a870;
--color-secondary-700: #009463;
--color-secondary-800: #007a52;
--color-secondary-900: #00573a;
--color-secondary-950: #00422c;
}
@source "../node_modules/flowbite-svelte/dist";

View File

@@ -6,7 +6,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<body
data-sveltekit-preload-data="hover"
class="bg-gray-100 text-black dark:bg-gray-900 dark:text-white"
>
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,17 +1,17 @@
<script lang="ts">
import { Toggle } from 'flowbite-svelte';
import JSConfetti from 'js-confetti';
import Cell from './Cell.svelte';
import ValueSelector from './ValueSelector.svelte';
import { Board, Difficulty } from './core/board';
import { onMount } from 'svelte';
import Cell from './Cell.svelte';
import GameCommands from './GameCommands.svelte';
import SumIndicator from './SumIndicator.svelte';
import Switch from './Switch.svelte';
import ValueSelector from './ValueSelector.svelte';
import { Board, Difficulty } from './core/board';
let selectedI: number | undefined = undefined;
let selectedJ: number | undefined = undefined;
let status: 'incomplete' | 'error' | 'won' = 'incomplete';
let valueMode: 'Write' | 'Annotation' = 'Write';
let isCompletion = true;
let lastMode: Difficulty;
const onSelect = (i: number, j: number) => () => {
@@ -26,7 +26,7 @@
const onSetValue = (number: number) => {
if (selectedI !== undefined && selectedJ !== undefined) {
if (valueMode === 'Write') {
if (isCompletion) {
board.grid[selectedI][selectedJ] = number;
if (board.isComplete()) {
status = board.isValid() ? 'won' : 'error';
@@ -71,28 +71,33 @@
});
</script>
<GameCommands {lastMode} onClick={onGameCommand} />
<p class="py-4 text-center">STATUS: {status}</p>
<div class="text-black dark:text-white">
<GameCommands {lastMode} onClick={onGameCommand} />
<p class="py-4 text-center">STATUS: {status}</p>
<div class="mx-auto grid max-w-xl grid-cols-[1fr_1fr_1fr_auto] gap-4">
{#each board.grid as row, i (row)}
{#each row as cell, j}
<Cell
value={cell}
annotations={board.annotations[i][j]}
onclick={onSelect(i, j)}
selected={selectedI === i && selectedJ === j}
disabled={!board.lockedCells[i][j]}
/>
<div class="grid grid-cols-[1fr_1fr_1fr_auto] gap-4">
{#each board.grid as row, i (row)}
{#each row as cell, j}
<Cell
value={cell}
annotations={board.annotations[i][j]}
onclick={onSelect(i, j)}
selected={selectedI === i && selectedJ === j}
disabled={!board.lockedCells[i][j]}
/>
{/each}
<SumIndicator value={board.horizontalSum[i]} />
{/each}
<SumIndicator value={board.horizontalSum[i]} />
{/each}
{#each board.verticalSum as s}
<SumIndicator value={s} />
{/each}
</div>
{#each board.verticalSum as s}
<SumIndicator value={s} />
{/each}
</div>
<div class="flex justify-center py-8">
<Switch bind:value={valueMode} design="multi" options={['Annotation', 'Write']} />
<div class="flex justify-center py-8">
<Toggle color="secondary" bind:checked={isCompletion}>
<svelte:fragment slot="offLabel">Annotation</svelte:fragment>
Completion
</Toggle>
</div>
<ValueSelector onclick={onSetValue} used={board.getPlacedNumbers()} />
</div>
<ValueSelector onclick={onSetValue} used={board.getPlacedNumbers()} />

View File

@@ -15,8 +15,9 @@
</script>
<button
class="aspect-square rounded-4xl border border-gray-200"
class:bg-cyan-500={selected}
class="aspect-square rounded-4xl border border-gray-200 {selected
? 'bg-primary-500 dark:bg-primary-500'
: ''} {disabled ? 'bg-gray-200 dark:bg-gray-400' : ''}"
class:bg-gray-200={disabled}
class:cursor-pointer={!disabled}
{onclick}
@@ -29,7 +30,7 @@
<div class="flex flex-wrap justify-center space-y-2 space-x-2 md:flex-col">
{#each annotations as nb (nb)}
<div
class="flex h-9 w-9 items-center justify-center rounded-full bg-emerald-300 p-1"
class="bg-secondary-300 flex h-9 w-9 items-center justify-center rounded-full p-1 text-black"
class:bg-white={selected}
>
{nb}

View File

@@ -5,8 +5,8 @@
<div class="flex justify-center space-x-2">
{#each ['reset', 'easy', 'medium', 'hard'] as mode (mode)}
<button
class="cursor-pointer rounded-full bg-cyan-500 px-4 py-2 text-sm text-white capitalize"
class:bg-cyan-800={lastMode === mode}
class="bg-primary-500 cursor-pointer rounded-full px-4 py-2 text-sm text-white capitalize"
class:bg-primary-800={lastMode === mode}
onclick={() => onClick(mode)}>{mode}</button
>
{/each}

10
src/lib/Navbar.svelte Normal file
View File

@@ -0,0 +1,10 @@
<script>
import { DarkMode } from 'flowbite-svelte';
</script>
<nav>
<h1 class="mb-5 grow text-center text-5xl font-medium">Fubuki</h1>
<div class="absolute top-0 right-0">
<DarkMode size="lg" />
</div>
</nav>

View File

@@ -1,263 +0,0 @@
<script lang="ts">
// copy from https://svelte.dev/playground/d65a4e9f0ae74d1eb1b08d13e428af32?version=5.23.0
// based on suggestions from:
// Inclusive Components by Heydon Pickering https://inclusive-components.design/toggle-button/
// On Designing and Building Toggle Switches by Sara Soueidan https://www.sarasoueidan.com/blog/toggle-switch-design/
// and this example by Scott O'hara https://codepen.io/scottohara/pen/zLZwNv
export let label = '';
export let design = 'inner label';
export let options: string[] = [];
export let fontSize = 16;
export let value = 'on';
let checked = true;
const uniqueID = Math.floor(Math.random() * 100);
function handleClick(event) {
const target = event.target;
const state = target.getAttribute('aria-checked');
checked = state === 'true' ? false : true;
value = checked === true ? 'on' : 'off';
}
</script>
{#if design == 'inner'}
<div class="s s--inner">
<span id={`switch-${uniqueID}`}>{label}</span>
<button
role="switch"
aria-checked={checked}
aria-labelledby={`switch-${uniqueID}`}
on:click={handleClick}
>
<span>on</span>
<span>off</span>
</button>
</div>
{:else if design == 'slider'}
<div class="s s--slider" style="font-size:{fontSize}px">
<span id={`switch-${uniqueID}`}>{label}</span>
<button
role="switch"
aria-checked={checked}
aria-labelledby={`switch-${uniqueID}`}
on:click={handleClick}
>
</button>
</div>
{:else}
<div class="s s--multi">
<div
role="radiogroup"
class="group-container"
aria-labelledby={`label-${uniqueID}`}
style="font-size:{fontSize}px"
id={`group-${uniqueID}`}
>
<div class="legend" id={`label-${uniqueID}`}>{label}</div>
{#each options as option}
<input type="radio" id={`${option}-${uniqueID}`} value={option} bind:group={value} />
<label for={`${option}-${uniqueID}`}>
{option}
</label>
{/each}
</div>
</div>
{/if}
<style lang="postcss">
@reference "tailwindcss";
:root {
--accent-color: theme(colors.emerald.300);
--gray: #ccc;
}
/* Inner Design Option */
.s--inner button {
padding: 0.5em;
background-color: #fff;
border: 1px solid var(--gray);
}
[role='switch'][aria-checked='true'] :first-child,
[role='switch'][aria-checked='false'] :last-child {
display: none;
color: #fff;
}
.s--inner button span {
user-select: none;
pointer-events: none;
padding: 0.25em;
}
.s--inner button:focus {
outline: var(--accent-color) solid 1px;
}
/* Slider Design Option */
.s--slider {
display: flex;
align-items: center;
}
.s--slider button {
width: 3em;
height: 1.6em;
position: relative;
margin: 0 0 0 0.5em;
background: var(--gray);
border: none;
}
.s--slider button::before {
content: '';
position: absolute;
width: 1.3em;
height: 1.3em;
background: #fff;
top: 0.13em;
right: 1.5em;
transition: transform 0.3s;
}
.s--slider button[aria-checked='true'] {
background-color: var(--accent-color);
}
.s--slider button[aria-checked='true']::before {
transform: translateX(1.3em);
transition: transform 0.3s;
}
.s--slider button:focus {
box-shadow: 0 0px 0px 1px var(--accent-color);
}
/* Multi Design Option */
/* Based on suggestions from Sara Soueidan https://www.sarasoueidan.com/blog/toggle-switch-design/
and this example from Scott O'hara https://codepen.io/scottohara/pen/zLZwNv */
.s--multi .group-container {
border: none;
padding: 0;
white-space: nowrap;
}
/* .s--multi legend {
font-size: 2px;
opacity: 0;
position: absolute;
} */
.s--multi label {
display: inline-block;
line-height: 1.6;
position: relative;
z-index: 2;
}
.s--multi input {
opacity: 0;
position: absolute;
}
.s--multi label:first-of-type {
padding-right: 5em;
}
.s--multi label:last-child {
margin-left: -5em;
padding-left: 5em;
}
.s--multi:focus-within label:first-of-type:after {
box-shadow: 0 0px 8px var(--accent-color);
border-radius: 1.5em;
}
/* making the switch UI. */
.s--multi label:first-of-type:before,
.s--multi label:first-of-type:after {
content: '';
height: 1.25em;
overflow: hidden;
pointer-events: none;
position: absolute;
vertical-align: middle;
}
.s--multi label:first-of-type:before {
border-radius: 100%;
z-index: 2;
position: absolute;
width: 1.2em;
height: 1.2em;
background: #fff;
top: 0.2em;
right: 1.2em;
transition: transform 0.3s;
}
.s--multi label:first-of-type:after {
background: var(--accent-color);
border-radius: 1em;
margin: 0 1em;
transition: background 0.2s ease-in-out;
width: 3em;
height: 1.6em;
}
.s--multi input:first-of-type:checked ~ label:first-of-type:after {
background: var(--gray);
}
.s--multi input:first-of-type:checked ~ label:first-of-type:before {
transform: translateX(-1.4em);
}
.s--multi input:last-of-type:checked ~ label:last-of-type {
z-index: 1;
}
.s--multi input:focus {
box-shadow: 0 0px 8px var(--accent-color);
border-radius: 1.5em;
}
/* gravy */
/* Inner Design Option */
[role='switch'][aria-checked='true'] :first-child,
[role='switch'][aria-checked='false'] :last-child {
border-radius: 0.25em;
background: var(--accent-color);
display: inline-block;
}
.s--inner button:focus {
box-shadow: 0 0px 8px var(--accent-color);
border-radius: 0.1em;
}
/* Slider Design Option */
.s--slider button {
border-radius: 1.5em;
}
.s--slider button::before {
border-radius: 100%;
}
.s--slider button:focus {
box-shadow: 0 0px 8px var(--accent-color);
border-radius: 1.5em;
}
</style>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { TrashBinOutline } from 'flowbite-svelte-icons';
const { onclick, used } = $props();
const numbers = Array.from({ length: 9 }, (_, i) => i + 1);
@@ -7,7 +8,7 @@
<div class="flex flex-wrap justify-center space-x-2">
{#each numbers as number (number)}
<button
class="mr-1 cursor-pointer rounded-full bg-emerald-300 px-4 py-2 disabled:bg-gray-200"
class="bg-secondary-400 mr-1 cursor-pointer rounded-full px-4 py-2 text-black disabled:bg-gray-200"
onclick={() => onclick(number)}
disabled={used.includes(number)}
>
@@ -15,9 +16,9 @@
</button>
{/each}
<button
class="mr-1 cursor-pointer rounded-full bg-emerald-300 px-4 py-2 disabled:bg-gray-200"
class="bg-secondary-400 mr-1 cursor-pointer rounded-full px-4 py-2 text-black disabled:bg-gray-200"
onclick={() => onclick()}
>
🗑️
<TrashBinOutline strokeWidth="1.5" />
</button>
</div>

View File

@@ -1,13 +1,17 @@
<script>
import Board from '$lib/Board.svelte';
import Navbar from '$lib/Navbar.svelte';
import { Card } from 'flowbite-svelte';
</script>
<svelte:head>
<title>FUBIKI</title>
</svelte:head>
<h1 class="mb-5 text-center text-6xl font-medium">Fubuki</h1>
<Navbar />
<div class="mx-2">
<Board />
<div class="flex justify-center">
<Card size="md" padding="xl">
<Board />
</Card>
</div>