feat: remove dark mode support

Remove ThemeToggle component, theme store, dark: Tailwind variants,
and dark color values from NOTE_COLORS. Simplify color logic to use
a single bg property. Delete dark mode e2e tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-05 23:07:18 +01:00
co-authored by Claude Opus 4.6
parent 6f3f7c7e03
commit c619515ca6
22 changed files with 95 additions and 200 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#202124" />
<meta name="theme-color" content="#ffffff" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🥨</text></svg>"
+2 -2
View File
@@ -91,7 +91,7 @@
value={item.text}
oninput={(e) => updateText(index, (e.target as HTMLInputElement).value)}
onkeydown={(e) => handleKeydown(e, index)}
class="flex-1 bg-transparent text-sm outline-none {item.checked ? 'text-gray-400 line-through' : 'text-gray-800 dark:text-gray-200'}"
class="flex-1 bg-transparent text-sm outline-none {item.checked ? 'text-gray-400 line-through' : 'text-gray-800'}"
placeholder="List item"
data-testid="checklist-input"
/>
@@ -109,7 +109,7 @@
{/each}
<button
onclick={() => addItem(items.length - 1)}
class="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
class="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700"
data-testid="checklist-add"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+2 -3
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { NOTE_COLORS, COLOR_OPTIONS } from '$lib/utils/colors.js';
import { effectiveTheme } from '$lib/stores/theme.js';
import type { NoteColor } from '$lib/types/index.js';
interface Props {
@@ -16,8 +15,8 @@
{@const colors = NOTE_COLORS[option.value]}
<button
onclick={() => onSelect(option.value)}
class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {selected === option.value ? 'border-gray-800 dark:border-white' : 'border-transparent'}"
style="background-color: {$effectiveTheme === 'dark' ? colors.dark : colors.light}"
class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {selected === option.value ? 'border-gray-800' : 'border-transparent'}"
style="background-color: {colors.bg}"
title={option.label}
data-testid="color-{option.value}"
></button>
+17 -17
View File
@@ -80,15 +80,15 @@
const chevronSize = 14;
function btnClass(active: boolean = false, disabled: boolean = false): string {
return `rounded p-1.5 text-gray-600 hover:bg-black/10 dark:text-gray-400 dark:hover:bg-white/10 ${active ? 'bg-black/10 dark:bg-white/10' : ''} ${disabled ? 'opacity-30' : ''}`;
return `rounded p-1.5 text-gray-600 hover:bg-black/10 ${active ? 'bg-black/10' : ''} ${disabled ? 'opacity-30' : ''}`;
}
function dropdownBtnClass(active: boolean = false): string {
return `flex items-center gap-0.5 rounded p-1.5 text-gray-600 hover:bg-black/10 dark:text-gray-400 dark:hover:bg-white/10 ${active ? 'bg-black/10 dark:bg-white/10' : ''}`;
return `flex items-center gap-0.5 rounded p-1.5 text-gray-600 hover:bg-black/10 ${active ? 'bg-black/10' : ''}`;
}
function dropdownItemClass(active: boolean = false): string {
return `flex w-full items-center gap-2 rounded px-3 py-1.5 text-sm text-gray-700 hover:bg-black/5 dark:text-gray-300 dark:hover:bg-white/10 ${active ? 'bg-black/5 dark:bg-white/10' : ''}`;
return `flex w-full items-center gap-2 rounded px-3 py-1.5 text-sm text-gray-700 hover:bg-black/5 ${active ? 'bg-black/5' : ''}`;
}
function handlePointerDown(event: PointerEvent) {
@@ -109,7 +109,7 @@
<svelte:document onpointerdown={handlePointerDown} onkeydown={handleKeydown} />
<div
class="flex flex-wrap items-center gap-0.5 border-b border-gray-200/50 px-2 py-1 dark:border-gray-700/50"
class="flex flex-wrap items-center gap-0.5 border-b border-gray-200/50 px-2 py-1"
data-testid="formatting-toolbar"
>
<!-- History -->
@@ -132,7 +132,7 @@
<Redo2 size={iconSize} />
</button>
<div class="mx-1 h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
<div class="mx-1 h-4 w-px bg-gray-300"></div>
<!-- Heading dropdown -->
<div class="relative" data-dropdown="heading">
@@ -146,7 +146,7 @@
<ChevronDown size={chevronSize} />
</button>
{#if openDropdown === 'heading'}
<div class="absolute left-0 top-full z-50 mt-1 min-w-[150px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800">
<div class="absolute left-0 top-full z-50 mt-1 min-w-[150px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
<button
onclick={() => { editor?.chain().focus().toggleHeading({ level: 1 }).run(); closeDropdowns(); }}
class={dropdownItemClass(editor?.isActive('heading', { level: 1 }) ?? false)}
@@ -187,7 +187,7 @@
<ChevronDown size={chevronSize} />
</button>
{#if openDropdown === 'list'}
<div class="absolute left-0 top-full z-50 mt-1 min-w-[170px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800">
<div class="absolute left-0 top-full z-50 mt-1 min-w-[170px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
<button
onclick={() => { editor?.chain().focus().toggleBulletList().run(); closeDropdowns(); }}
class={dropdownItemClass(editor?.isActive('bulletList') ?? false)}
@@ -225,7 +225,7 @@
<CodeXml size={iconSize} />
</button>
<div class="mx-1 h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
<div class="mx-1 h-4 w-px bg-gray-300"></div>
<!-- Inline -->
<button
@@ -280,7 +280,7 @@
<Link size={iconSize} />
</button>
{#if openDropdown === 'link'}
<div class="absolute left-1/2 top-full z-50 mt-1 -translate-x-1/2 rounded-lg border border-gray-200 bg-white px-2 py-1.5 shadow-lg dark:border-gray-700 dark:bg-gray-800">
<div class="absolute left-1/2 top-full z-50 mt-1 -translate-x-1/2 rounded-lg border border-gray-200 bg-white px-2 py-1.5 shadow-lg">
<form
class="flex items-center gap-1"
onsubmit={(e) => { e.preventDefault(); applyLink(); }}
@@ -290,23 +290,23 @@
bind:value={linkUrl}
type="url"
placeholder="Paste a link..."
class="w-44 bg-transparent px-1.5 py-1 text-sm text-gray-700 outline-none placeholder:text-gray-400 dark:text-gray-300 dark:placeholder:text-gray-500"
class="w-44 bg-transparent px-1.5 py-1 text-sm text-gray-700 outline-none placeholder:text-gray-400"
data-testid="format-link-input"
/>
<button
type="submit"
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-300"
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-gray-600"
title="Apply link"
data-testid="format-link-apply"
>
<CornerDownLeft size={16} />
</button>
<div class="mx-0.5 h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
<div class="mx-0.5 h-4 w-px bg-gray-300"></div>
<button
type="button"
onclick={openLink}
disabled={!editor?.getAttributes('link').href}
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-gray-600 disabled:opacity-30 dark:hover:bg-white/10 dark:hover:text-gray-300"
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-gray-600 disabled:opacity-30"
title="Open link"
data-testid="format-link-open"
>
@@ -316,7 +316,7 @@
type="button"
onclick={removeLink}
disabled={!editor?.isActive('link')}
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-red-500 disabled:opacity-30 dark:hover:bg-white/10 dark:hover:text-red-400"
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-red-500 disabled:opacity-30"
title="Remove link"
data-testid="format-unlink"
>
@@ -327,7 +327,7 @@
{/if}
</div>
<div class="mx-1 h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
<div class="mx-1 h-4 w-px bg-gray-300"></div>
<!-- Align dropdown -->
<div class="relative" data-dropdown="align">
@@ -341,7 +341,7 @@
<ChevronDown size={chevronSize} />
</button>
{#if openDropdown === 'align'}
<div class="absolute left-0 top-full z-50 mt-1 min-w-[150px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800">
<div class="absolute left-0 top-full z-50 mt-1 min-w-[150px] rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
<button
onclick={() => { editor?.chain().focus().setTextAlign('left').run(); closeDropdowns(); }}
class={dropdownItemClass(editor?.isActive({ textAlign: 'left' }) ?? false)}
@@ -378,7 +378,7 @@
{/if}
</div>
<div class="mx-1 h-4 w-px bg-gray-300 dark:bg-gray-600"></div>
<div class="mx-1 h-4 w-px bg-gray-300"></div>
<!-- Insert -->
<button
+1 -1
View File
@@ -80,7 +80,7 @@
<!-- Upload area -->
<div
class="rounded-lg border-2 border-dashed p-4 text-center transition-colors {dragOver ? 'border-amber-500 bg-amber-50 dark:bg-amber-900/20' : 'border-gray-300 dark:border-gray-600'}"
class="rounded-lg border-2 border-dashed p-4 text-center transition-colors {dragOver ? 'border-amber-500 bg-amber-50' : 'border-gray-300'}"
ondrop={handleDrop}
ondragover={handleDragOver}
ondragleave={() => (dragOver = false)}
+4 -6
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import SearchBar from '../SearchBar.svelte';
import ThemeToggle from '../ThemeToggle.svelte';
import SyncIndicator from '../SyncIndicator.svelte';
interface Props {
@@ -10,20 +9,20 @@
let { onMenuToggle }: Props = $props();
</script>
<header class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-gray-200 bg-white px-4 shadow-sm dark:border-gray-700 dark:bg-gray-900">
<header class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-gray-200 bg-white px-4 shadow-sm">
<button
onclick={onMenuToggle}
class="rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
class="rounded-full p-2 hover:bg-gray-100"
aria-label="Toggle sidebar"
>
<svg class="h-6 w-6 text-gray-600 dark:text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-6 w-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div class="flex items-center gap-2">
<span class="text-2xl">🥨</span>
<h1 class="text-xl font-semibold text-gray-800 dark:text-gray-100">Crumbs</h1>
<h1 class="text-xl font-semibold text-gray-800">Crumbs</h1>
</div>
<div class="mx-4 flex-1">
@@ -32,6 +31,5 @@
<div class="flex items-center gap-2">
<SyncIndicator />
<ThemeToggle />
</div>
</header>
+7 -7
View File
@@ -20,14 +20,14 @@
</script>
<aside
class="fixed left-0 top-16 z-20 h-[calc(100vh-4rem)] w-64 transform border-r border-gray-200 bg-white transition-transform duration-200 dark:border-gray-700 dark:bg-gray-900 {open ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0"
class="fixed left-0 top-16 z-20 h-[calc(100vh-4rem)] w-64 transform border-r border-gray-200 bg-white transition-transform duration-200 {open ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0"
>
<nav class="p-2">
<ul class="space-y-1">
<li>
<button
onclick={() => setFilter('all')}
class="flex w-full items-center gap-3 rounded-full px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'all' && !$selectedTag ? 'bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200' : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}"
class="flex w-full items-center gap-3 rounded-full px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'all' && !$selectedTag ? 'bg-amber-100 text-amber-900' : 'text-gray-700 hover:bg-gray-100'}"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
@@ -38,7 +38,7 @@
<li>
<button
onclick={() => setFilter('archived')}
class="flex w-full items-center gap-3 rounded-full px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'archived' ? 'bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200' : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}"
class="flex w-full items-center gap-3 rounded-full px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'archived' ? 'bg-amber-100 text-amber-900' : 'text-gray-700 hover:bg-gray-100'}"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
@@ -49,7 +49,7 @@
<li>
<button
onclick={() => setFilter('trashed')}
class="flex w-full items-center gap-3 rounded-full px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'trashed' ? 'bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200' : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}"
class="flex w-full items-center gap-3 rounded-full px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'trashed' ? 'bg-amber-100 text-amber-900' : 'text-gray-700 hover:bg-gray-100'}"
>
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
@@ -60,14 +60,14 @@
</ul>
{#if $allTags.length > 0}
<div class="mt-6 border-t border-gray-200 pt-4 dark:border-gray-700">
<h3 class="px-6 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Tags</h3>
<div class="mt-6 border-t border-gray-200 pt-4">
<h3 class="px-6 text-xs font-semibold uppercase tracking-wider text-gray-500">Tags</h3>
<ul class="mt-2 space-y-1">
{#each $allTags as tag}
<li>
<button
onclick={() => selectTag($selectedTag === tag ? null : tag)}
class="flex w-full items-center gap-3 rounded-full px-6 py-2 text-left text-sm transition-colors {$selectedTag === tag ? 'bg-amber-100 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200' : 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800'}"
class="flex w-full items-center gap-3 rounded-full px-6 py-2 text-left text-sm transition-colors {$selectedTag === tag ? 'bg-amber-100 text-amber-900' : 'text-gray-700 hover:bg-gray-100'}"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
+1 -1
View File
@@ -32,7 +32,7 @@
<div class="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-2">
{#each $toasts as toast (toast.id)}
<div
class="flex items-center gap-3 rounded-lg px-4 py-3 shadow-lg {toast.type === 'error' ? 'bg-red-600 text-white' : toast.type === 'success' ? 'bg-green-600 text-white' : 'bg-gray-800 text-white dark:bg-gray-200 dark:text-gray-900'}"
class="flex items-center gap-3 rounded-lg px-4 py-3 shadow-lg {toast.type === 'error' ? 'bg-red-600 text-white' : toast.type === 'success' ? 'bg-green-600 text-white' : 'bg-gray-800 text-white'}"
>
<span class="text-sm">{toast.text}</span>
{#if toast.action}
+12 -14
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import { NOTE_COLORS } from '$lib/utils/colors.js';
import { effectiveTheme } from '$lib/stores/theme.js';
import { renderMarkdown } from '$lib/utils/markdown.js';
import { togglePin, trashNote, archiveNote, unarchiveNote, restoreNote, deleteNote, currentFilter } from '$lib/stores/notes.js';
import type { Note } from '$lib/types/index.js';
@@ -19,8 +18,7 @@
$effect(() => {
const colors = NOTE_COLORS[note.color];
const bg = $effectiveTheme === 'dark' ? colors.dark : colors.light;
cardStyle = `background-color: ${bg}`;
cardStyle = `background-color: ${colors.bg}`;
});
let cardStyle = $state('');
@@ -45,7 +43,7 @@
</script>
<article
class="group relative cursor-pointer rounded-lg border border-gray-200 p-4 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700"
class="group relative cursor-pointer rounded-lg border border-gray-200 p-4 shadow-sm transition-shadow hover:shadow-md"
style={cardStyle}
onclick={() => onEdit(note)}
onkeydown={(e) => e.key === 'Enter' && onEdit(note)}
@@ -55,13 +53,13 @@
data-note-id={note.id}
>
{#if note.title}
<h3 class="mb-2 text-sm font-semibold text-gray-900 dark:text-gray-100">{note.title}</h3>
<h3 class="mb-2 text-sm font-semibold text-gray-900">{note.title}</h3>
{/if}
{#if note.checklistMode && checklistItems.length > 0}
<ul class="space-y-1" data-testid="note-checklist-preview">
{#each checklistItems.slice(0, 8) as item}
<li class="flex items-center gap-2 text-sm {item.checked ? 'text-gray-400 line-through' : 'text-gray-700 dark:text-gray-300'}">
<li class="flex items-center gap-2 text-sm {item.checked ? 'text-gray-400 line-through' : 'text-gray-700'}">
<input type="checkbox" checked={item.checked} disabled class="h-3.5 w-3.5 rounded border-gray-300 text-amber-600" />
<span class="truncate">{item.text}</span>
</li>
@@ -71,7 +69,7 @@
{/if}
</ul>
{:else if note.content}
<div class="prose prose-sm dark:prose-invert line-clamp-6 max-w-none text-sm text-gray-700 dark:text-gray-300" data-testid="note-content-preview">
<div class="prose prose-sm line-clamp-6 max-w-none text-sm text-gray-700" data-testid="note-content-preview">
{@html renderedContent}
</div>
{/if}
@@ -79,7 +77,7 @@
{#if note.tags && note.tags.length > 0}
<div class="mt-2 flex flex-wrap gap-1">
{#each note.tags as tag}
<span class="rounded-full bg-gray-200/60 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700/60 dark:text-gray-400">
<span class="rounded-full bg-gray-200/60 px-2 py-0.5 text-xs text-gray-600">
#{tag}
</span>
{/each}
@@ -91,7 +89,7 @@
{#if $currentFilter === 'trashed'}
<button
onclick={stop(() => restoreNote(note.id))}
class="rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-1.5 hover:bg-black/10"
title="Restore"
data-testid="restore-btn"
>
@@ -101,7 +99,7 @@
</button>
<button
onclick={stop(() => deleteNote(note.id))}
class="rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-1.5 hover:bg-black/10"
title="Delete forever"
data-testid="delete-forever-btn"
>
@@ -112,7 +110,7 @@
{:else}
<button
onclick={stop(() => togglePin(note.id, note.pinned))}
class="rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-1.5 hover:bg-black/10"
title={note.pinned ? 'Unpin' : 'Pin'}
data-testid="pin-btn"
>
@@ -123,7 +121,7 @@
{#if $currentFilter === 'archived'}
<button
onclick={stop(() => unarchiveNote(note.id))}
class="rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-1.5 hover:bg-black/10"
title="Unarchive"
data-testid="unarchive-btn"
>
@@ -134,7 +132,7 @@
{:else}
<button
onclick={stop(() => archiveNote(note.id))}
class="rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-1.5 hover:bg-black/10"
title="Archive"
data-testid="archive-btn"
>
@@ -145,7 +143,7 @@
{/if}
<button
onclick={stop(() => trashNote(note.id))}
class="rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-1.5 hover:bg-black/10"
title="Delete"
data-testid="trash-btn"
>
+11 -12
View File
@@ -5,7 +5,6 @@
import TiptapEditor from './TiptapEditor.svelte';
import { updateNote, createNote } from '$lib/stores/notes.js';
import { NOTE_COLORS } from '$lib/utils/colors.js';
import { effectiveTheme } from '$lib/stores/theme.js';
import type { Editor } from '@tiptap/core';
import type { Note, NoteColor } from '$lib/types/index.js';
@@ -34,7 +33,7 @@
let bgStyle = $state('');
$effect(() => {
const colors = NOTE_COLORS[color];
bgStyle = `background-color: ${$effectiveTheme === 'dark' ? colors.dark : colors.light}`;
bgStyle = `background-color: ${colors.bg}`;
});
async function save() {
@@ -79,7 +78,7 @@
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="mx-4 w-full max-w-xl rounded-lg border border-gray-200 shadow-xl dark:border-gray-700"
class="mx-4 w-full max-w-xl rounded-lg border border-gray-200 shadow-xl"
style={bgStyle}
onclick={(e) => e.stopPropagation()}
onkeydown={(e) => { e.stopPropagation(); handleKeydown(e); }}
@@ -90,7 +89,7 @@
type="text"
placeholder="Title"
bind:value={title}
class="w-full bg-transparent px-4 pt-4 text-lg font-semibold text-gray-900 outline-none placeholder:text-gray-500 dark:text-gray-100"
class="w-full bg-transparent px-4 pt-4 text-lg font-semibold text-gray-900 outline-none placeholder:text-gray-500"
data-testid="note-title-input"
/>
@@ -104,7 +103,7 @@
bind:this={textareaEl}
placeholder="Take a note..."
bind:value={content}
class="min-h-[300px] w-full resize-none bg-transparent px-4 py-2 text-sm text-gray-800 outline-none placeholder:text-gray-500 dark:text-gray-200"
class="min-h-[300px] w-full resize-none bg-transparent px-4 py-2 text-sm text-gray-800 outline-none placeholder:text-gray-500"
rows="12"
data-testid="note-content-input"
></textarea>
@@ -124,22 +123,22 @@
{/if}
<!-- Toolbar -->
<div class="flex items-center justify-between border-t border-gray-200/50 px-2 py-2 dark:border-gray-700/50">
<div class="flex items-center justify-between border-t border-gray-200/50 px-2 py-2">
<div class="flex items-center gap-1">
<!-- Color picker toggle -->
<div class="relative">
<button
onclick={() => (showColorPicker = !showColorPicker)}
class="rounded-full p-2 hover:bg-black/10 dark:hover:bg-white/10"
class="rounded-full p-2 hover:bg-black/10"
title="Background color"
data-testid="color-picker-toggle"
>
<svg class="h-5 w-5 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-5 w-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
</button>
{#if showColorPicker}
<div class="absolute left-0 top-full mt-2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg dark:border-gray-700 dark:bg-gray-800">
<div class="absolute left-0 top-full mt-2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg">
<ColorPicker selected={color} onSelect={handleColorSelect} />
</div>
{/if}
@@ -148,7 +147,7 @@
<!-- Checklist mode toggle -->
<button
onclick={() => (checklistMode = !checklistMode)}
class="rounded-full p-2 hover:bg-black/10 dark:hover:bg-white/10 {checklistMode ? 'text-amber-600' : ''}"
class="rounded-full p-2 hover:bg-black/10 {checklistMode ? 'text-amber-600' : ''}"
title="Checklist mode"
data-testid="checklist-toggle"
>
@@ -160,7 +159,7 @@
<!-- Raw markdown mode toggle -->
<button
onclick={toggleMarkdownMode}
class="rounded-full p-2 hover:bg-black/10 dark:hover:bg-white/10 {rawMarkdownMode ? 'text-amber-600' : ''}"
class="rounded-full p-2 hover:bg-black/10 {rawMarkdownMode ? 'text-amber-600' : ''}"
title="Markdown mode"
data-testid="markdown-toggle"
>
@@ -172,7 +171,7 @@
<button
onclick={save}
class="rounded px-4 py-1 text-sm font-medium text-gray-700 hover:bg-black/10 dark:text-gray-300 dark:hover:bg-white/10"
class="rounded px-4 py-1 text-sm font-medium text-gray-700 hover:bg-black/10"
data-testid="close-editor-btn"
>
Close
+1 -1
View File
@@ -13,7 +13,7 @@
{#if notes.length > 0}
{#if label}
<p class="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">{label}</p>
<p class="mb-2 px-2 text-xs font-semibold uppercase tracking-wider text-gray-500">{label}</p>
{/if}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4" data-testid="note-grid">
{#each notes as note (note.id)}
+3 -3
View File
@@ -37,7 +37,7 @@
</script>
<div class="relative max-w-2xl">
<div class="flex items-center rounded-lg bg-gray-100 px-4 py-2 dark:bg-gray-800">
<div class="flex items-center rounded-lg bg-gray-100 px-4 py-2">
<svg class="mr-3 h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
@@ -46,11 +46,11 @@
placeholder="Search notes..."
bind:value={query}
oninput={handleSearch}
class="w-full bg-transparent text-sm text-gray-800 outline-none placeholder:text-gray-500 dark:text-gray-200"
class="w-full bg-transparent text-sm text-gray-800 outline-none placeholder:text-gray-500"
data-testid="search-input"
/>
{#if query}
<button onclick={clearSearch} class="ml-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" aria-label="Clear search">
<button onclick={clearSearch} class="ml-2 text-gray-500 hover:text-gray-700" aria-label="Clear search">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
+1 -1
View File
@@ -11,7 +11,7 @@
{#each $allTags as tag}
<button
onclick={() => toggleTag(tag)}
class="rounded-full px-3 py-1 text-xs font-medium transition-colors {$selectedTag === tag ? 'bg-amber-200 text-amber-900 dark:bg-amber-800 dark:text-amber-100' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700'}"
class="rounded-full px-3 py-1 text-xs font-medium transition-colors {$selectedTag === tag ? 'bg-amber-200 text-amber-900' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}"
data-testid="tag-chip"
>
#{tag}
-20
View File
@@ -1,20 +0,0 @@
<script lang="ts">
import { effectiveTheme, toggleTheme } from '$lib/stores/theme.js';
</script>
<button
onclick={toggleTheme}
class="rounded-full p-2 hover:bg-gray-100 dark:hover:bg-gray-800"
aria-label="Toggle dark mode"
data-testid="theme-toggle"
>
{#if $effectiveTheme === 'dark'}
<svg class="h-5 w-5 text-gray-300" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
{:else}
<svg class="h-5 w-5 text-gray-600" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
{/if}
</button>
+1 -1
View File
@@ -58,7 +58,7 @@
<div
bind:this={element}
class="tiptap-wrapper prose prose-sm dark:prose-invert min-h-[300px] max-w-none px-4 py-2 text-gray-800 dark:text-gray-200"
class="tiptap-wrapper prose prose-sm min-h-[300px] max-w-none px-4 py-2 text-gray-800"
data-testid="tiptap-editor"
></div>
-46
View File
@@ -1,46 +0,0 @@
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
type Theme = 'light' | 'dark' | 'system';
function getInitialTheme(): Theme {
if (!browser) return 'system';
return (localStorage.getItem('theme') as Theme) || 'system';
}
function getEffectiveTheme(theme: Theme): 'light' | 'dark' {
if (theme !== 'system') return theme;
if (!browser) return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
export const themePreference = writable<Theme>(getInitialTheme());
export const effectiveTheme = writable<'light' | 'dark'>('light');
if (browser) {
themePreference.subscribe((theme) => {
localStorage.setItem('theme', theme);
const effective = getEffectiveTheme(theme);
effectiveTheme.set(effective);
document.documentElement.classList.toggle('dark', effective === 'dark');
});
// Listen for system theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
themePreference.update((current) => {
if (current === 'system') {
const effective = getEffectiveTheme('system');
effectiveTheme.set(effective);
document.documentElement.classList.toggle('dark', effective === 'dark');
}
return current;
});
});
}
export function toggleTheme() {
themePreference.update((current) => {
const effective = getEffectiveTheme(current);
return effective === 'dark' ? 'light' : 'dark';
});
}
+13 -13
View File
@@ -1,18 +1,18 @@
import type { NoteColor } from '$lib/types/index.js';
export const NOTE_COLORS: Record<NoteColor, { light: string; dark: string; label: string }> = {
default: { light: '#ffffff', dark: '#202124', label: 'Default' },
coral: { light: '#faafa8', dark: '#77172e', label: 'Coral' },
peach: { light: '#f39f76', dark: '#692b17', label: 'Peach' },
sand: { light: '#fff8b8', dark: '#7c4a03', label: 'Sand' },
mint: { light: '#e2f6d3', dark: '#264d3b', label: 'Mint' },
sage: { light: '#b4ddd3', dark: '#0c625d', label: 'Sage' },
fog: { light: '#d4e4ed', dark: '#256377', label: 'Fog' },
storm: { light: '#aeccdc', dark: '#284255', label: 'Storm' },
dusk: { light: '#d3bfdb', dark: '#472e5b', label: 'Dusk' },
blossom: { light: '#f6e2dd', dark: '#6c394f', label: 'Blossom' },
clay: { light: '#e9e3d4', dark: '#4b443a', label: 'Clay' },
chalk: { light: '#efeff1', dark: '#232427', label: 'Chalk' }
export const NOTE_COLORS: Record<NoteColor, { bg: string; label: string }> = {
default: { bg: '#ffffff', label: 'Default' },
coral: { bg: '#faafa8', label: 'Coral' },
peach: { bg: '#f39f76', label: 'Peach' },
sand: { bg: '#fff8b8', label: 'Sand' },
mint: { bg: '#e2f6d3', label: 'Mint' },
sage: { bg: '#b4ddd3', label: 'Sage' },
fog: { bg: '#d4e4ed', label: 'Fog' },
storm: { bg: '#aeccdc', label: 'Storm' },
dusk: { bg: '#d3bfdb', label: 'Dusk' },
blossom: { bg: '#f6e2dd', label: 'Blossom' },
clay: { bg: '#e9e3d4', label: 'Clay' },
chalk: { bg: '#efeff1', label: 'Chalk' }
};
export const COLOR_OPTIONS = Object.entries(NOTE_COLORS).map(([value, { label }]) => ({
+1 -2
View File
@@ -4,7 +4,6 @@
import Sidebar from '$lib/components/Layout/Sidebar.svelte';
import Toast from '$lib/components/Layout/Toast.svelte';
import { loadNotes } from '$lib/stores/notes.js';
import '$lib/stores/theme.js';
import { onMount } from 'svelte';
let { children } = $props();
@@ -15,7 +14,7 @@
});
</script>
<div class="min-h-screen bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">
<div class="min-h-screen bg-white text-gray-900">
<Header onMenuToggle={() => (sidebarOpen = !sidebarOpen)} />
<Sidebar open={sidebarOpen} />
+4 -4
View File
@@ -26,7 +26,7 @@
<div class="mx-auto mb-6 max-w-xl">
<button
onclick={() => (showNewNote = true)}
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-left text-sm text-gray-500 shadow-sm transition-shadow hover:shadow-md dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400"
class="w-full rounded-lg border border-gray-300 px-4 py-3 text-left text-sm text-gray-500 shadow-sm transition-shadow hover:shadow-md"
data-testid="new-note-btn"
>
Take a note...
@@ -39,9 +39,9 @@
</div>
{#if $currentFilter === 'archived'}
<h2 class="mb-4 text-lg font-medium text-gray-600 dark:text-gray-400">Archive</h2>
<h2 class="mb-4 text-lg font-medium text-gray-600">Archive</h2>
{:else if $currentFilter === 'trashed'}
<h2 class="mb-4 text-lg font-medium text-gray-600 dark:text-gray-400">Trash</h2>
<h2 class="mb-4 text-lg font-medium text-gray-600">Trash</h2>
{/if}
{#if $pinnedNotes.length > 0}
@@ -57,7 +57,7 @@
/>
{#if $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
<div class="flex flex-col items-center justify-center py-20 text-gray-400 dark:text-gray-600">
<div class="flex flex-col items-center justify-center py-20 text-gray-400">
<svg class="mb-4 h-24 w-24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
+6 -6
View File
@@ -35,17 +35,17 @@
<title>Login - Crumbs</title>
</svelte:head>
<div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div class="w-full max-w-sm rounded-lg bg-white p-8 shadow-md dark:bg-gray-800">
<div class="flex min-h-screen items-center justify-center bg-gray-50">
<div class="w-full max-w-sm rounded-lg bg-white p-8 shadow-md">
<div class="mb-6 text-center">
<span class="text-5xl">🥨</span>
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100">Crumbs</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Enter your password to continue</p>
<h1 class="text-2xl font-bold text-gray-800">Crumbs</h1>
<p class="mt-1 text-sm text-gray-500">Enter your password to continue</p>
</div>
<form onsubmit={handleLogin}>
{#if errorMsg}
<div class="mb-4 rounded bg-red-50 p-3 text-sm text-red-600 dark:bg-red-900/30 dark:text-red-400" data-testid="error-message">
<div class="mb-4 rounded bg-red-50 p-3 text-sm text-red-600" data-testid="error-message">
{errorMsg}
</div>
{/if}
@@ -54,7 +54,7 @@
type="password"
bind:value={password}
placeholder="Password"
class="mb-4 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
class="mb-4 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500"
data-testid="password-input"
required
/>
+7 -7
View File
@@ -47,17 +47,17 @@
<title>Setup - Crumbs</title>
</svelte:head>
<div class="flex min-h-screen items-center justify-center bg-gray-50 dark:bg-gray-900">
<div class="w-full max-w-sm rounded-lg bg-white p-8 shadow-md dark:bg-gray-800">
<div class="flex min-h-screen items-center justify-center bg-gray-50">
<div class="w-full max-w-sm rounded-lg bg-white p-8 shadow-md">
<div class="mb-6 text-center">
<span class="text-5xl">🥨</span>
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100">Welcome to Crumbs</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set up your password to get started</p>
<h1 class="text-2xl font-bold text-gray-800">Welcome to Crumbs</h1>
<p class="mt-1 text-sm text-gray-500">Set up your password to get started</p>
</div>
<form onsubmit={handleSetup}>
{#if errorMsg}
<div class="mb-4 rounded bg-red-50 p-3 text-sm text-red-600 dark:bg-red-900/30 dark:text-red-400" data-testid="error-message">
<div class="mb-4 rounded bg-red-50 p-3 text-sm text-red-600" data-testid="error-message">
{errorMsg}
</div>
{/if}
@@ -66,7 +66,7 @@
type="password"
bind:value={password}
placeholder="Password (min 8 characters)"
class="mb-3 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
class="mb-3 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500"
data-testid="password-input"
required
/>
@@ -75,7 +75,7 @@
type="password"
bind:value={confirmPassword}
placeholder="Confirm password"
class="mb-4 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100"
class="mb-4 w-full rounded-lg border border-gray-300 px-4 py-3 text-sm outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500"
data-testid="confirm-password-input"
required
/>
-32
View File
@@ -1,32 +0,0 @@
import { test, expect } from './helpers/fixtures.js';
test.describe('Dark Mode', () => {
test('Scenario: Dark mode can be toggled on and off', async ({ authenticatedPage: page }) => {
// When the user enables dark mode
await page.getByTestId('theme-toggle').click();
// Then the application is displayed in dark mode
const isDark = await page.evaluate(() =>
document.documentElement.classList.contains('dark')
);
expect(isDark).toBe(true);
// When the user disables dark mode
await page.getByTestId('theme-toggle').click();
// Then the application is displayed in light mode
const isLight = await page.evaluate(() =>
!document.documentElement.classList.contains('dark')
);
expect(isLight).toBe(true);
});
test('Scenario: Theme preference is remembered across page loads', async ({ authenticatedPage: page }) => {
// When the user enables dark mode
await page.getByTestId('theme-toggle').click();
// Then the preference is persisted
const theme = await page.evaluate(() => localStorage.getItem('theme'));
expect(theme).toBeTruthy();
});
});