mirror of
https://github.com/bretzel-app/crumbs.git
synced 2026-10-12 12:06:57 +02:00
style: redesign UI with parchment theme and JetBrains Mono font
Replace the default dark/white theme with a warm parchment aesthetic using CSS custom properties, retro pixel overlay, and JetBrains Mono monospace font across all components. Bump version to 0.1.7. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a9779c93aa
commit
0894c10c95
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "crumbs",
|
"name": "crumbs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
+52
@@ -1,2 +1,54 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap');
|
||||||
|
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-base: #f0e6d3;
|
||||||
|
--bg-surface: #faf5eb;
|
||||||
|
--primary: #C8860A;
|
||||||
|
--primary-hover: #E8A020;
|
||||||
|
--text: #1a1a2e;
|
||||||
|
--text-muted: #6b6272;
|
||||||
|
--border: #1a1a2e;
|
||||||
|
--border-subtle: #d4cabb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.03;
|
||||||
|
background-image: repeating-conic-gradient(#1a1a2e 0% 25%, transparent 0% 50%);
|
||||||
|
background-size: 4px 4px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prose overrides for parchment theme */
|
||||||
|
.prose {
|
||||||
|
--tw-prose-body: var(--text);
|
||||||
|
--tw-prose-headings: var(--text);
|
||||||
|
--tw-prose-links: var(--primary);
|
||||||
|
--tw-prose-code: var(--text);
|
||||||
|
--tw-prose-pre-bg: var(--bg-surface);
|
||||||
|
--tw-prose-pre-code: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(code):not(:where([class~="not-prose"] *)) {
|
||||||
|
background-color: var(--bg-surface);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0.125rem 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(a):not(:where([class~="not-prose"] *)) {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose :where(a):not(:where([class~="not-prose"] *)):hover {
|
||||||
|
color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -3,7 +3,9 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#f0e6d3" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
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>"
|
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>"
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={item.checked}
|
checked={item.checked}
|
||||||
onchange={() => toggleItem(index)}
|
onchange={() => toggleItem(index)}
|
||||||
class="h-4 w-4 rounded border-gray-300 text-amber-600 focus:ring-amber-500"
|
class="h-4 w-4 rounded border-[var(--border-subtle)] text-[var(--primary)] focus:ring-[var(--primary)]"
|
||||||
data-testid="checklist-checkbox"
|
data-testid="checklist-checkbox"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
value={item.text}
|
value={item.text}
|
||||||
oninput={(e) => updateText(index, (e.target as HTMLInputElement).value)}
|
oninput={(e) => updateText(index, (e.target as HTMLInputElement).value)}
|
||||||
onkeydown={(e) => handleKeydown(e, index)}
|
onkeydown={(e) => handleKeydown(e, index)}
|
||||||
class="flex-1 bg-transparent text-sm outline-none {item.checked ? 'text-gray-400 line-through' : 'text-gray-800'}"
|
class="flex-1 bg-transparent text-sm outline-none {item.checked ? 'text-[var(--text-muted)] line-through' : 'text-[var(--text)]'}"
|
||||||
placeholder="List item"
|
placeholder="List item"
|
||||||
data-testid="checklist-input"
|
data-testid="checklist-input"
|
||||||
/>
|
/>
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
aria-label="Remove item"
|
aria-label="Remove item"
|
||||||
data-testid="checklist-remove"
|
data-testid="checklist-remove"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4 text-[var(--text-muted)]" 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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
<button
|
<button
|
||||||
onclick={() => addItem(items.length - 1)}
|
onclick={() => addItem(items.length - 1)}
|
||||||
class="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-700"
|
class="flex items-center gap-2 text-sm text-[var(--text-muted)] hover:text-[var(--text)]"
|
||||||
data-testid="checklist-add"
|
data-testid="checklist-add"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{@const colors = NOTE_COLORS[option.value]}
|
{@const colors = NOTE_COLORS[option.value]}
|
||||||
<button
|
<button
|
||||||
onclick={() => onSelect(option.value)}
|
onclick={() => onSelect(option.value)}
|
||||||
class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {selected === option.value ? 'border-gray-800' : 'border-transparent'}"
|
class="h-8 w-8 rounded-full border-2 transition-transform hover:scale-110 {selected === option.value ? 'border-[var(--text)]' : 'border-transparent'}"
|
||||||
style="background-color: {colors.bg}"
|
style="background-color: {colors.bg}"
|
||||||
title={option.label}
|
title={option.label}
|
||||||
data-testid="color-{option.value}"
|
data-testid="color-{option.value}"
|
||||||
|
|||||||
@@ -80,15 +80,15 @@
|
|||||||
const chevronSize = 14;
|
const chevronSize = 14;
|
||||||
|
|
||||||
function btnClass(active: boolean = false, disabled: boolean = false): string {
|
function btnClass(active: boolean = false, disabled: boolean = false): string {
|
||||||
return `rounded p-1.5 text-gray-600 hover:bg-black/10 ${active ? 'bg-black/10' : ''} ${disabled ? 'opacity-30' : ''}`;
|
return `rounded p-1.5 text-[var(--text)] hover:bg-[var(--border)]/10 ${active ? 'text-[var(--primary)] bg-[var(--border)]/10' : ''} ${disabled ? 'opacity-30' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropdownBtnClass(active: boolean = false): string {
|
function dropdownBtnClass(active: boolean = false): string {
|
||||||
return `flex items-center gap-0.5 rounded p-1.5 text-gray-600 hover:bg-black/10 ${active ? 'bg-black/10' : ''}`;
|
return `flex items-center gap-0.5 rounded p-1.5 text-[var(--text)] hover:bg-[var(--border)]/10 ${active ? 'text-[var(--primary)] bg-[var(--border)]/10' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropdownItemClass(active: boolean = false): string {
|
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 ${active ? 'bg-black/5' : ''}`;
|
return `flex w-full items-center gap-2 rounded px-3 py-1.5 text-sm text-[var(--text)] hover:bg-[var(--border)]/5 ${active ? 'bg-[var(--border)]/5' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerDown(event: PointerEvent) {
|
function handlePointerDown(event: PointerEvent) {
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
<svelte:document onpointerdown={handlePointerDown} onkeydown={handleKeydown} />
|
<svelte:document onpointerdown={handlePointerDown} onkeydown={handleKeydown} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap items-center gap-0.5 border-b border-gray-200/50 px-2 py-1"
|
class="flex flex-wrap items-center gap-0.5 border-b border-[var(--border-subtle)] px-2 py-1"
|
||||||
data-testid="formatting-toolbar"
|
data-testid="formatting-toolbar"
|
||||||
>
|
>
|
||||||
<!-- History -->
|
<!-- History -->
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<Redo2 size={iconSize} />
|
<Redo2 size={iconSize} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="mx-1 h-4 w-px bg-gray-300"></div>
|
<div class="mx-1 h-4 w-px bg-[var(--border-subtle)]"></div>
|
||||||
|
|
||||||
<!-- Heading dropdown -->
|
<!-- Heading dropdown -->
|
||||||
<div class="relative" data-dropdown="heading">
|
<div class="relative" data-dropdown="heading">
|
||||||
@@ -146,13 +146,13 @@
|
|||||||
<ChevronDown size={chevronSize} />
|
<ChevronDown size={chevronSize} />
|
||||||
</button>
|
</button>
|
||||||
{#if openDropdown === 'heading'}
|
{#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">
|
<div class="absolute left-0 top-full z-50 mt-1 min-w-[150px] rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] py-1">
|
||||||
<button
|
<button
|
||||||
onclick={() => { editor?.chain().focus().toggleHeading({ level: 1 }).run(); closeDropdowns(); }}
|
onclick={() => { editor?.chain().focus().toggleHeading({ level: 1 }).run(); closeDropdowns(); }}
|
||||||
class={dropdownItemClass(editor?.isActive('heading', { level: 1 }) ?? false)}
|
class={dropdownItemClass(editor?.isActive('heading', { level: 1 }) ?? false)}
|
||||||
data-testid="format-h1"
|
data-testid="format-h1"
|
||||||
>
|
>
|
||||||
<span class="w-6 text-xs font-semibold text-gray-400">H1</span>
|
<span class="w-6 text-xs font-semibold text-[var(--text-muted)]">H1</span>
|
||||||
<span class="font-semibold">Heading 1</span>
|
<span class="font-semibold">Heading 1</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
class={dropdownItemClass(editor?.isActive('heading', { level: 2 }) ?? false)}
|
class={dropdownItemClass(editor?.isActive('heading', { level: 2 }) ?? false)}
|
||||||
data-testid="format-h2"
|
data-testid="format-h2"
|
||||||
>
|
>
|
||||||
<span class="w-6 text-xs font-semibold text-gray-400">H2</span>
|
<span class="w-6 text-xs font-semibold text-[var(--text-muted)]">H2</span>
|
||||||
<span>Heading 2</span>
|
<span>Heading 2</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
class={dropdownItemClass(editor?.isActive('heading', { level: 3 }) ?? false)}
|
class={dropdownItemClass(editor?.isActive('heading', { level: 3 }) ?? false)}
|
||||||
data-testid="format-h3"
|
data-testid="format-h3"
|
||||||
>
|
>
|
||||||
<span class="w-6 text-xs font-semibold text-gray-400">H3</span>
|
<span class="w-6 text-xs font-semibold text-[var(--text-muted)]">H3</span>
|
||||||
<span>Heading 3</span>
|
<span>Heading 3</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
<ChevronDown size={chevronSize} />
|
<ChevronDown size={chevronSize} />
|
||||||
</button>
|
</button>
|
||||||
{#if openDropdown === 'list'}
|
{#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">
|
<div class="absolute left-0 top-full z-50 mt-1 min-w-[170px] rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] py-1">
|
||||||
<button
|
<button
|
||||||
onclick={() => { editor?.chain().focus().toggleBulletList().run(); closeDropdowns(); }}
|
onclick={() => { editor?.chain().focus().toggleBulletList().run(); closeDropdowns(); }}
|
||||||
class={dropdownItemClass(editor?.isActive('bulletList') ?? false)}
|
class={dropdownItemClass(editor?.isActive('bulletList') ?? false)}
|
||||||
@@ -225,7 +225,7 @@
|
|||||||
<CodeXml size={iconSize} />
|
<CodeXml size={iconSize} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="mx-1 h-4 w-px bg-gray-300"></div>
|
<div class="mx-1 h-4 w-px bg-[var(--border-subtle)]"></div>
|
||||||
|
|
||||||
<!-- Inline -->
|
<!-- Inline -->
|
||||||
<button
|
<button
|
||||||
@@ -280,7 +280,7 @@
|
|||||||
<Link size={iconSize} />
|
<Link size={iconSize} />
|
||||||
</button>
|
</button>
|
||||||
{#if openDropdown === 'link'}
|
{#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">
|
<div class="absolute left-1/2 top-full z-50 mt-1 -translate-x-1/2 rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] px-2 py-1.5">
|
||||||
<form
|
<form
|
||||||
class="flex items-center gap-1"
|
class="flex items-center gap-1"
|
||||||
onsubmit={(e) => { e.preventDefault(); applyLink(); }}
|
onsubmit={(e) => { e.preventDefault(); applyLink(); }}
|
||||||
@@ -290,23 +290,23 @@
|
|||||||
bind:value={linkUrl}
|
bind:value={linkUrl}
|
||||||
type="url"
|
type="url"
|
||||||
placeholder="Paste a link..."
|
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"
|
class="w-44 bg-transparent px-1.5 py-1 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)]"
|
||||||
data-testid="format-link-input"
|
data-testid="format-link-input"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-gray-600"
|
class="rounded p-1 text-[var(--text-muted)] hover:bg-[var(--border)]/5 hover:text-[var(--text)]"
|
||||||
title="Apply link"
|
title="Apply link"
|
||||||
data-testid="format-link-apply"
|
data-testid="format-link-apply"
|
||||||
>
|
>
|
||||||
<CornerDownLeft size={16} />
|
<CornerDownLeft size={16} />
|
||||||
</button>
|
</button>
|
||||||
<div class="mx-0.5 h-4 w-px bg-gray-300"></div>
|
<div class="mx-0.5 h-4 w-px bg-[var(--border-subtle)]"></div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={openLink}
|
onclick={openLink}
|
||||||
disabled={!editor?.getAttributes('link').href}
|
disabled={!editor?.getAttributes('link').href}
|
||||||
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-gray-600 disabled:opacity-30"
|
class="rounded p-1 text-[var(--text-muted)] hover:bg-[var(--border)]/5 hover:text-[var(--text)] disabled:opacity-30"
|
||||||
title="Open link"
|
title="Open link"
|
||||||
data-testid="format-link-open"
|
data-testid="format-link-open"
|
||||||
>
|
>
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
onclick={removeLink}
|
onclick={removeLink}
|
||||||
disabled={!editor?.isActive('link')}
|
disabled={!editor?.isActive('link')}
|
||||||
class="rounded p-1 text-gray-400 hover:bg-black/5 hover:text-red-500 disabled:opacity-30"
|
class="rounded p-1 text-[var(--text-muted)] hover:bg-[var(--border)]/5 hover:text-red-500 disabled:opacity-30"
|
||||||
title="Remove link"
|
title="Remove link"
|
||||||
data-testid="format-unlink"
|
data-testid="format-unlink"
|
||||||
>
|
>
|
||||||
@@ -327,7 +327,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-1 h-4 w-px bg-gray-300"></div>
|
<div class="mx-1 h-4 w-px bg-[var(--border-subtle)]"></div>
|
||||||
|
|
||||||
<!-- Align dropdown -->
|
<!-- Align dropdown -->
|
||||||
<div class="relative" data-dropdown="align">
|
<div class="relative" data-dropdown="align">
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
<ChevronDown size={chevronSize} />
|
<ChevronDown size={chevronSize} />
|
||||||
</button>
|
</button>
|
||||||
{#if openDropdown === 'align'}
|
{#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">
|
<div class="absolute left-0 top-full z-50 mt-1 min-w-[150px] rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] py-1">
|
||||||
<button
|
<button
|
||||||
onclick={() => { editor?.chain().focus().setTextAlign('left').run(); closeDropdowns(); }}
|
onclick={() => { editor?.chain().focus().setTextAlign('left').run(); closeDropdowns(); }}
|
||||||
class={dropdownItemClass(editor?.isActive({ textAlign: 'left' }) ?? false)}
|
class={dropdownItemClass(editor?.isActive({ textAlign: 'left' }) ?? false)}
|
||||||
@@ -378,7 +378,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-1 h-4 w-px bg-gray-300"></div>
|
<div class="mx-1 h-4 w-px bg-[var(--border-subtle)]"></div>
|
||||||
|
|
||||||
<!-- Insert -->
|
<!-- Insert -->
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<img
|
<img
|
||||||
src="/api/notes/{noteId}/attachments/{attachment.id}"
|
src="/api/notes/{noteId}/attachments/{attachment.id}"
|
||||||
alt={attachment.filename}
|
alt={attachment.filename}
|
||||||
class="h-20 w-20 rounded object-cover"
|
class="h-20 w-20 rounded-sm object-cover"
|
||||||
data-testid="attachment-thumbnail"
|
data-testid="attachment-thumbnail"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
<!-- Upload area -->
|
<!-- Upload area -->
|
||||||
<div
|
<div
|
||||||
class="rounded-lg border-2 border-dashed p-4 text-center transition-colors {dragOver ? 'border-amber-500 bg-amber-50' : 'border-gray-300'}"
|
class="rounded-sm border-2 border-dashed p-4 text-center transition-colors {dragOver ? 'border-[var(--primary)] bg-[var(--primary)]/5' : 'border-[var(--border-subtle)]'}"
|
||||||
ondrop={handleDrop}
|
ondrop={handleDrop}
|
||||||
ondragover={handleDragOver}
|
ondragover={handleDragOver}
|
||||||
ondragleave={() => (dragOver = false)}
|
ondragleave={() => (dragOver = false)}
|
||||||
@@ -89,10 +89,10 @@
|
|||||||
data-testid="upload-dropzone"
|
data-testid="upload-dropzone"
|
||||||
>
|
>
|
||||||
{#if uploading}
|
{#if uploading}
|
||||||
<p class="text-sm text-gray-500">Uploading...</p>
|
<p class="text-sm text-[var(--text-muted)]">Uploading...</p>
|
||||||
{:else}
|
{:else}
|
||||||
<label class="cursor-pointer">
|
<label class="cursor-pointer">
|
||||||
<span class="text-sm text-gray-500">Drop images here or <span class="text-amber-600 underline">browse</span></span>
|
<span class="text-sm text-[var(--text-muted)]">Drop images here or <span class="text-[var(--primary)] underline">browse</span></span>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
|
|||||||
@@ -9,20 +9,20 @@
|
|||||||
let { onMenuToggle }: Props = $props();
|
let { onMenuToggle }: Props = $props();
|
||||||
</script>
|
</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">
|
<header class="sticky top-0 z-30 flex h-16 items-center gap-4 border-b border-[var(--border)] bg-[var(--bg-surface)] px-4">
|
||||||
<button
|
<button
|
||||||
onclick={onMenuToggle}
|
onclick={onMenuToggle}
|
||||||
class="rounded-full p-2 hover:bg-gray-100"
|
class="rounded-sm p-2 hover:bg-[var(--border)]/10"
|
||||||
aria-label="Toggle sidebar"
|
aria-label="Toggle sidebar"
|
||||||
>
|
>
|
||||||
<svg class="h-6 w-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-6 w-6 text-[var(--text)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-2xl">🥨</span>
|
<span class="text-2xl">🥨</span>
|
||||||
<h1 class="text-xl font-semibold text-gray-800">Crumbs</h1>
|
<h1 class="font-['Press_Start_2P'] text-lg text-[var(--primary)]">Crumbs</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-4 flex-1">
|
<div class="mx-4 flex-1">
|
||||||
|
|||||||
@@ -21,14 +21,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<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 {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-[var(--border)] bg-[var(--bg-surface)] transition-transform duration-200 {open ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0"
|
||||||
>
|
>
|
||||||
<nav class="p-2">
|
<nav class="p-2">
|
||||||
<ul class="space-y-1">
|
<ul class="space-y-1">
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onclick={() => setFilter('all')}
|
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' : 'text-gray-700 hover:bg-gray-100'}"
|
class="flex w-full items-center gap-3 rounded-sm px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'all' && !$selectedTag ? 'bg-[var(--primary)]/15 text-[var(--primary)]' : 'text-[var(--text)] hover:bg-[var(--bg-base)]'}"
|
||||||
>
|
>
|
||||||
<StickyNote size={20} />
|
<StickyNote size={20} />
|
||||||
Crumbs
|
Crumbs
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onclick={() => setFilter('archived')}
|
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' : 'text-gray-700 hover:bg-gray-100'}"
|
class="flex w-full items-center gap-3 rounded-sm px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'archived' ? 'bg-[var(--primary)]/15 text-[var(--primary)]' : 'text-[var(--text)] hover:bg-[var(--bg-base)]'}"
|
||||||
>
|
>
|
||||||
<Archive size={20} />
|
<Archive size={20} />
|
||||||
Archive
|
Archive
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onclick={() => setFilter('trashed')}
|
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' : 'text-gray-700 hover:bg-gray-100'}"
|
class="flex w-full items-center gap-3 rounded-sm px-6 py-3 text-left text-sm transition-colors {$currentFilter === 'trashed' ? 'bg-[var(--primary)]/15 text-[var(--primary)]' : 'text-[var(--text)] hover:bg-[var(--bg-base)]'}"
|
||||||
>
|
>
|
||||||
<Trash2 size={20} />
|
<Trash2 size={20} />
|
||||||
Trash
|
Trash
|
||||||
@@ -55,14 +55,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{#if $allTags.length > 0}
|
{#if $allTags.length > 0}
|
||||||
<div class="mt-6 border-t border-gray-200 pt-4">
|
<div class="mt-6 border-t border-[var(--border-subtle)] pt-4">
|
||||||
<h3 class="px-6 text-xs font-semibold uppercase tracking-wider text-gray-500">Tags</h3>
|
<h3 class="px-6 text-xs font-semibold uppercase tracking-wider text-[var(--text-muted)]">Tags</h3>
|
||||||
<ul class="mt-2 space-y-1">
|
<ul class="mt-2 space-y-1">
|
||||||
{#each $allTags as tag}
|
{#each $allTags as tag}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onclick={() => selectTag($selectedTag === tag ? null : tag)}
|
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' : 'text-gray-700 hover:bg-gray-100'}"
|
class="flex w-full items-center gap-3 rounded-sm px-6 py-2 text-left text-sm transition-colors {$selectedTag === tag ? 'bg-[var(--primary)]/15 text-[var(--primary)]' : 'text-[var(--text)] hover:bg-[var(--bg-base)]'}"
|
||||||
>
|
>
|
||||||
<Tag size={16} />
|
<Tag size={16} />
|
||||||
#{tag}
|
#{tag}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<div class="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 flex-col gap-2">
|
<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)}
|
{#each $toasts as toast (toast.id)}
|
||||||
<div
|
<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'}"
|
class="flex items-center gap-3 rounded-sm border border-[var(--border)] px-4 py-3 {toast.type === 'error' ? 'bg-red-600 text-white' : toast.type === 'success' ? 'bg-green-600 text-white' : 'bg-[var(--text)] text-[var(--bg-base)]'}"
|
||||||
>
|
>
|
||||||
<span class="text-sm">{toast.text}</span>
|
<span class="text-sm">{toast.text}</span>
|
||||||
{#if toast.action}
|
{#if toast.action}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<article
|
<article
|
||||||
class="group relative cursor-pointer rounded-lg border border-gray-200 p-4 shadow-sm transition-shadow hover:shadow-md"
|
class="group relative cursor-pointer rounded-sm border border-[var(--border)] p-4 transition-colors hover:border-[var(--primary)]"
|
||||||
style={cardStyle}
|
style={cardStyle}
|
||||||
onclick={() => onEdit(note)}
|
onclick={() => onEdit(note)}
|
||||||
onkeydown={(e) => e.key === 'Enter' && onEdit(note)}
|
onkeydown={(e) => e.key === 'Enter' && onEdit(note)}
|
||||||
@@ -53,23 +53,23 @@
|
|||||||
data-note-id={note.id}
|
data-note-id={note.id}
|
||||||
>
|
>
|
||||||
{#if note.title}
|
{#if note.title}
|
||||||
<h3 class="mb-2 text-sm font-semibold text-gray-900">{note.title}</h3>
|
<h3 class="mb-2 text-sm font-semibold text-[var(--text)]">{note.title}</h3>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if note.checklistMode && checklistItems.length > 0}
|
{#if note.checklistMode && checklistItems.length > 0}
|
||||||
<ul class="space-y-1" data-testid="note-checklist-preview">
|
<ul class="space-y-1" data-testid="note-checklist-preview">
|
||||||
{#each checklistItems.slice(0, 8) as item}
|
{#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'}">
|
<li class="flex items-center gap-2 text-sm {item.checked ? 'text-[var(--text-muted)] line-through' : 'text-[var(--text)]'}">
|
||||||
<input type="checkbox" checked={item.checked} disabled class="h-3.5 w-3.5 rounded border-gray-300 text-amber-600" />
|
<input type="checkbox" checked={item.checked} disabled class="h-3.5 w-3.5 rounded border-[var(--border-subtle)] text-[var(--primary)]" />
|
||||||
<span class="truncate">{item.text}</span>
|
<span class="truncate">{item.text}</span>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
{#if checklistItems.length > 8}
|
{#if checklistItems.length > 8}
|
||||||
<li class="text-xs text-gray-400">+{checklistItems.length - 8} more</li>
|
<li class="text-xs text-[var(--text-muted)]">+{checklistItems.length - 8} more</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
{:else if note.content}
|
{:else if note.content}
|
||||||
<div class="prose prose-sm line-clamp-6 max-w-none text-sm text-gray-700" data-testid="note-content-preview">
|
<div class="prose prose-sm line-clamp-6 max-w-none text-sm text-[var(--text-muted)]" data-testid="note-content-preview">
|
||||||
{@html renderedContent}
|
{@html renderedContent}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
{#if note.tags && note.tags.length > 0}
|
{#if note.tags && note.tags.length > 0}
|
||||||
<div class="mt-2 flex flex-wrap gap-1">
|
<div class="mt-2 flex flex-wrap gap-1">
|
||||||
{#each note.tags as tag}
|
{#each note.tags as tag}
|
||||||
<span class="rounded-full bg-gray-200/60 px-2 py-0.5 text-xs text-gray-600">
|
<span class="rounded-sm bg-[var(--border)]/10 px-2 py-0.5 text-xs text-[var(--text-muted)]">
|
||||||
#{tag}
|
#{tag}
|
||||||
</span>
|
</span>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
{#if $currentFilter === 'trashed'}
|
{#if $currentFilter === 'trashed'}
|
||||||
<button
|
<button
|
||||||
onclick={stop(() => restoreNote(note.id))}
|
onclick={stop(() => restoreNote(note.id))}
|
||||||
class="rounded-full p-1.5 hover:bg-black/10"
|
class="rounded-sm p-1.5 hover:bg-[var(--border)]/10"
|
||||||
title="Restore"
|
title="Restore"
|
||||||
data-testid="restore-btn"
|
data-testid="restore-btn"
|
||||||
>
|
>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={stop(() => deleteNote(note.id))}
|
onclick={stop(() => deleteNote(note.id))}
|
||||||
class="rounded-full p-1.5 hover:bg-black/10"
|
class="rounded-sm p-1.5 hover:bg-[var(--border)]/10"
|
||||||
title="Delete forever"
|
title="Delete forever"
|
||||||
data-testid="delete-forever-btn"
|
data-testid="delete-forever-btn"
|
||||||
>
|
>
|
||||||
@@ -110,18 +110,18 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
onclick={stop(() => togglePin(note.id, note.pinned))}
|
onclick={stop(() => togglePin(note.id, note.pinned))}
|
||||||
class="rounded-full p-1.5 hover:bg-black/10"
|
class="rounded-sm p-1.5 hover:bg-[var(--border)]/10"
|
||||||
title={note.pinned ? 'Unpin' : 'Pin'}
|
title={note.pinned ? 'Unpin' : 'Pin'}
|
||||||
data-testid="pin-btn"
|
data-testid="pin-btn"
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4 {note.pinned ? 'text-amber-600' : ''}" fill={note.pinned ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4 {note.pinned ? 'text-[var(--primary)]' : ''}" fill={note.pinned ? 'currentColor' : 'none'} stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if $currentFilter === 'archived'}
|
{#if $currentFilter === 'archived'}
|
||||||
<button
|
<button
|
||||||
onclick={stop(() => unarchiveNote(note.id))}
|
onclick={stop(() => unarchiveNote(note.id))}
|
||||||
class="rounded-full p-1.5 hover:bg-black/10"
|
class="rounded-sm p-1.5 hover:bg-[var(--border)]/10"
|
||||||
title="Unarchive"
|
title="Unarchive"
|
||||||
data-testid="unarchive-btn"
|
data-testid="unarchive-btn"
|
||||||
>
|
>
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
onclick={stop(() => archiveNote(note.id))}
|
onclick={stop(() => archiveNote(note.id))}
|
||||||
class="rounded-full p-1.5 hover:bg-black/10"
|
class="rounded-sm p-1.5 hover:bg-[var(--border)]/10"
|
||||||
title="Archive"
|
title="Archive"
|
||||||
data-testid="archive-btn"
|
data-testid="archive-btn"
|
||||||
>
|
>
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
onclick={stop(() => trashNote(note.id))}
|
onclick={stop(() => trashNote(note.id))}
|
||||||
class="rounded-full p-1.5 hover:bg-black/10"
|
class="rounded-sm p-1.5 hover:bg-[var(--border)]/10"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
data-testid="trash-btn"
|
data-testid="trash-btn"
|
||||||
>
|
>
|
||||||
@@ -157,7 +157,7 @@
|
|||||||
<!-- Pin indicator -->
|
<!-- Pin indicator -->
|
||||||
{#if note.pinned}
|
{#if note.pinned}
|
||||||
<div class="absolute right-2 top-2">
|
<div class="absolute right-2 top-2">
|
||||||
<svg class="h-4 w-4 text-amber-600" fill="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4 text-[var(--primary)]" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
<path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
>
|
>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="mx-4 w-full max-w-xl rounded-lg border border-gray-200 shadow-xl"
|
class="mx-4 w-full max-w-xl rounded-sm border border-[var(--border)]"
|
||||||
style={bgStyle}
|
style={bgStyle}
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
onkeydown={(e) => { e.stopPropagation(); handleKeydown(e); }}
|
onkeydown={(e) => { e.stopPropagation(); handleKeydown(e); }}
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Title"
|
placeholder="Title"
|
||||||
bind:value={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"
|
class="w-full bg-transparent px-4 pt-4 text-lg font-semibold text-[var(--text)] outline-none placeholder:text-[var(--text-muted)]"
|
||||||
data-testid="note-title-input"
|
data-testid="note-title-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
bind:this={textareaEl}
|
bind:this={textareaEl}
|
||||||
placeholder="Add a crumb..."
|
placeholder="Add a crumb..."
|
||||||
bind:value={content}
|
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"
|
class="min-h-[300px] w-full resize-none bg-transparent px-4 py-2 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)]"
|
||||||
rows="12"
|
rows="12"
|
||||||
data-testid="note-content-input"
|
data-testid="note-content-input"
|
||||||
></textarea>
|
></textarea>
|
||||||
@@ -123,22 +123,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div class="flex items-center justify-between border-t border-gray-200/50 px-2 py-2">
|
<div class="flex items-center justify-between border-t border-[var(--border-subtle)] px-2 py-2">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<!-- Color picker toggle -->
|
<!-- Color picker toggle -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
onclick={() => (showColorPicker = !showColorPicker)}
|
onclick={() => (showColorPicker = !showColorPicker)}
|
||||||
class="rounded-full p-2 hover:bg-black/10"
|
class="rounded-sm p-2 hover:bg-[var(--border)]/10"
|
||||||
title="Background color"
|
title="Background color"
|
||||||
data-testid="color-picker-toggle"
|
data-testid="color-picker-toggle"
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-5 w-5 text-[var(--text-muted)]" 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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if showColorPicker}
|
{#if showColorPicker}
|
||||||
<div class="absolute left-0 top-full mt-2 rounded-lg border border-gray-200 bg-white p-2 shadow-lg">
|
<div class="absolute left-0 top-full mt-2 rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-2">
|
||||||
<ColorPicker selected={color} onSelect={handleColorSelect} />
|
<ColorPicker selected={color} onSelect={handleColorSelect} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
<!-- Checklist mode toggle -->
|
<!-- Checklist mode toggle -->
|
||||||
<button
|
<button
|
||||||
onclick={() => (checklistMode = !checklistMode)}
|
onclick={() => (checklistMode = !checklistMode)}
|
||||||
class="rounded-full p-2 hover:bg-black/10 {checklistMode ? 'text-amber-600' : ''}"
|
class="rounded-sm p-2 hover:bg-[var(--border)]/10 {checklistMode ? 'text-[var(--primary)]' : ''}"
|
||||||
title="Checklist mode"
|
title="Checklist mode"
|
||||||
data-testid="checklist-toggle"
|
data-testid="checklist-toggle"
|
||||||
>
|
>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
<!-- Raw markdown mode toggle -->
|
<!-- Raw markdown mode toggle -->
|
||||||
<button
|
<button
|
||||||
onclick={toggleMarkdownMode}
|
onclick={toggleMarkdownMode}
|
||||||
class="rounded-full p-2 hover:bg-black/10 {rawMarkdownMode ? 'text-amber-600' : ''}"
|
class="rounded-sm p-2 hover:bg-[var(--border)]/10 {rawMarkdownMode ? 'text-[var(--primary)]' : ''}"
|
||||||
title="Markdown mode"
|
title="Markdown mode"
|
||||||
data-testid="markdown-toggle"
|
data-testid="markdown-toggle"
|
||||||
>
|
>
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onclick={save}
|
onclick={save}
|
||||||
class="rounded px-4 py-1 text-sm font-medium text-gray-700 hover:bg-black/10"
|
class="rounded-sm px-4 py-1 text-sm font-medium text-[var(--text)] hover:bg-[var(--border)]/10"
|
||||||
data-testid="close-editor-btn"
|
data-testid="close-editor-btn"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative max-w-2xl">
|
<div class="relative max-w-2xl">
|
||||||
<div class="flex items-center rounded-lg bg-gray-100 px-4 py-2">
|
<div class="flex items-center rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-2 focus-within:border-[var(--primary)]">
|
||||||
<svg class="mr-3 h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="mr-3 h-5 w-5 text-[var(--text-muted)]" 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" />
|
<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>
|
</svg>
|
||||||
<input
|
<input
|
||||||
@@ -46,11 +46,11 @@
|
|||||||
placeholder="Search crumbs..."
|
placeholder="Search crumbs..."
|
||||||
bind:value={query}
|
bind:value={query}
|
||||||
oninput={handleSearch}
|
oninput={handleSearch}
|
||||||
class="w-full bg-transparent text-sm text-gray-800 outline-none placeholder:text-gray-500"
|
class="w-full bg-transparent text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)]"
|
||||||
data-testid="search-input"
|
data-testid="search-input"
|
||||||
/>
|
/>
|
||||||
{#if query}
|
{#if query}
|
||||||
<button onclick={clearSearch} class="ml-2 text-gray-500 hover:text-gray-700" aria-label="Clear search">
|
<button onclick={clearSearch} class="ml-2 text-[var(--text-muted)] hover:text-[var(--text)]" aria-label="Clear search">
|
||||||
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{#each $allTags as tag}
|
{#each $allTags as tag}
|
||||||
<button
|
<button
|
||||||
onclick={() => toggleTag(tag)}
|
onclick={() => toggleTag(tag)}
|
||||||
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'}"
|
class="rounded-sm px-3 py-1 text-xs font-medium transition-colors {$selectedTag === tag ? 'bg-[var(--primary)] text-white' : 'border border-[var(--border-subtle)] bg-[var(--bg-surface)] text-[var(--text-muted)] hover:border-[var(--primary)]'}"
|
||||||
data-testid="tag-chip"
|
data-testid="tag-chip"
|
||||||
>
|
>
|
||||||
#{tag}
|
#{tag}
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
class="tiptap-wrapper prose prose-sm min-h-[300px] max-w-none px-4 py-2 text-gray-800"
|
class="tiptap-wrapper prose prose-sm min-h-[300px] max-w-none px-4 py-2 text-[var(--text)]"
|
||||||
data-testid="tiptap-editor"
|
data-testid="tiptap-editor"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
.tiptap-wrapper :global(.tiptap p.is-editor-empty:first-child::before) {
|
.tiptap-wrapper :global(.tiptap p.is-editor-empty:first-child::before) {
|
||||||
content: attr(data-placeholder);
|
content: attr(data-placeholder);
|
||||||
float: left;
|
float: left;
|
||||||
color: #9ca3af;
|
color: var(--text-muted);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { NoteColor } from '$lib/types/index.js';
|
import type { NoteColor } from '$lib/types/index.js';
|
||||||
|
|
||||||
export const NOTE_COLORS: Record<NoteColor, { bg: string; label: string }> = {
|
export const NOTE_COLORS: Record<NoteColor, { bg: string; label: string }> = {
|
||||||
default: { bg: '#ffffff', label: 'Default' },
|
default: { bg: '#faf5eb', label: 'Default' },
|
||||||
coral: { bg: '#faafa8', label: 'Coral' },
|
coral: { bg: '#faafa8', label: 'Coral' },
|
||||||
peach: { bg: '#f39f76', label: 'Peach' },
|
peach: { bg: '#f39f76', label: 'Peach' },
|
||||||
sand: { bg: '#fff8b8', label: 'Sand' },
|
sand: { bg: '#fff8b8', label: 'Sand' },
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex min-h-screen flex-col bg-white text-gray-900">
|
<div class="flex min-h-screen flex-col bg-[var(--bg-base)] text-[var(--text)]">
|
||||||
<Header onMenuToggle={() => (sidebarOpen = !sidebarOpen)} />
|
<Header onMenuToggle={() => (sidebarOpen = !sidebarOpen)} />
|
||||||
<Sidebar open={sidebarOpen} />
|
<Sidebar open={sidebarOpen} />
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer class="pb-4 pt-8 text-center text-xs text-gray-400 lg:ml-64">
|
<footer class="pb-4 pt-8 text-center text-xs text-[var(--text-muted)] lg:ml-64">
|
||||||
Crumbs by Bretzel v{data.appVersion} — made with 🥨 in Strasbourg
|
Crumbs by Bretzel v{data.appVersion} — made with 🥨 in Strasbourg
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<div class="mx-auto mb-6 max-w-xl">
|
<div class="mx-auto mb-6 max-w-xl">
|
||||||
<button
|
<button
|
||||||
onclick={() => (showNewNote = true)}
|
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"
|
class="w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-surface)] px-4 py-3 text-left text-sm text-[var(--text-muted)] transition-colors hover:border-[var(--primary)]"
|
||||||
data-testid="new-note-btn"
|
data-testid="new-note-btn"
|
||||||
>
|
>
|
||||||
Add a crumb...
|
Add a crumb...
|
||||||
@@ -39,9 +39,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $currentFilter === 'archived'}
|
{#if $currentFilter === 'archived'}
|
||||||
<h2 class="mb-4 text-lg font-medium text-gray-600">Archive</h2>
|
<h2 class="mb-4 text-lg font-medium text-[var(--text-muted)]">Archive</h2>
|
||||||
{:else if $currentFilter === 'trashed'}
|
{:else if $currentFilter === 'trashed'}
|
||||||
<h2 class="mb-4 text-lg font-medium text-gray-600">Trash</h2>
|
<h2 class="mb-4 text-lg font-medium text-[var(--text-muted)]">Trash</h2>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $pinnedNotes.length > 0}
|
{#if $pinnedNotes.length > 0}
|
||||||
@@ -57,11 +57,9 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{#if $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
|
{#if $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
|
||||||
<div class="flex flex-col items-center justify-center py-20 text-gray-400">
|
<div class="flex flex-col items-center justify-center py-20 text-[var(--text-muted)]">
|
||||||
<svg class="mb-4 h-24 w-24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<span class="mb-4 text-7xl">🥨</span>
|
||||||
<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" />
|
<p class="font-['Press_Start_2P'] text-xs">
|
||||||
</svg>
|
|
||||||
<p class="text-lg">
|
|
||||||
{#if $currentFilter === 'trashed'}
|
{#if $currentFilter === 'trashed'}
|
||||||
No crumbs in trash
|
No crumbs in trash
|
||||||
{:else if $currentFilter === 'archived'}
|
{:else if $currentFilter === 'archived'}
|
||||||
@@ -69,7 +67,7 @@
|
|||||||
{:else if $selectedTag}
|
{:else if $selectedTag}
|
||||||
No crumbs with tag #{$selectedTag}
|
No crumbs with tag #{$selectedTag}
|
||||||
{:else}
|
{:else}
|
||||||
Your crumbs will appear here
|
No crumbs yet
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,17 +35,17 @@
|
|||||||
<title>Login - Crumbs</title>
|
<title>Login - Crumbs</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
<div class="flex min-h-screen items-center justify-center bg-[var(--bg-base)]">
|
||||||
<div class="w-full max-w-sm rounded-lg bg-white p-8 shadow-md">
|
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8">
|
||||||
<div class="mb-6 text-center">
|
<div class="mb-6 text-center">
|
||||||
<span class="text-5xl">🥨</span>
|
<span class="text-5xl">🥨</span>
|
||||||
<h1 class="text-2xl font-bold text-gray-800">Crumbs</h1>
|
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Crumbs</h1>
|
||||||
<p class="mt-1 text-sm text-gray-500">Enter your password to continue</p>
|
<p class="mt-2 text-sm text-[var(--text-muted)]">Enter your password to continue</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onsubmit={handleLogin}>
|
<form onsubmit={handleLogin}>
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div class="mb-4 rounded bg-red-50 p-3 text-sm text-red-600" data-testid="error-message">
|
<div class="mb-4 rounded-sm border border-red-300 bg-red-50 p-3 text-sm text-red-600" data-testid="error-message">
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
placeholder="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"
|
class="mb-4 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
|
||||||
data-testid="password-input"
|
data-testid="password-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="w-full rounded-lg bg-amber-500 py-3 text-sm font-medium text-white transition-colors hover:bg-amber-600 disabled:opacity-50"
|
class="w-full rounded-sm bg-[var(--primary)] py-3 text-sm font-medium text-white transition-colors hover:bg-[var(--primary-hover)] disabled:opacity-50"
|
||||||
data-testid="login-btn"
|
data-testid="login-btn"
|
||||||
>
|
>
|
||||||
{loading ? 'Signing in...' : 'Sign in'}
|
{loading ? 'Signing in...' : 'Sign in'}
|
||||||
|
|||||||
@@ -47,17 +47,17 @@
|
|||||||
<title>Setup - Crumbs</title>
|
<title>Setup - Crumbs</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="flex min-h-screen items-center justify-center bg-gray-50">
|
<div class="flex min-h-screen items-center justify-center bg-[var(--bg-base)]">
|
||||||
<div class="w-full max-w-sm rounded-lg bg-white p-8 shadow-md">
|
<div class="w-full max-w-sm rounded-sm border border-[var(--border)] bg-[var(--bg-surface)] p-8">
|
||||||
<div class="mb-6 text-center">
|
<div class="mb-6 text-center">
|
||||||
<span class="text-5xl">🥨</span>
|
<span class="text-5xl">🥨</span>
|
||||||
<h1 class="text-2xl font-bold text-gray-800">Welcome to Crumbs</h1>
|
<h1 class="font-['Press_Start_2P'] text-xl text-[var(--primary)]">Welcome to Crumbs</h1>
|
||||||
<p class="mt-1 text-sm text-gray-500">Set up your password to get started</p>
|
<p class="mt-2 text-sm text-[var(--text-muted)]">Set up your password to get started</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onsubmit={handleSetup}>
|
<form onsubmit={handleSetup}>
|
||||||
{#if errorMsg}
|
{#if errorMsg}
|
||||||
<div class="mb-4 rounded bg-red-50 p-3 text-sm text-red-600" data-testid="error-message">
|
<div class="mb-4 rounded-sm border border-red-300 bg-red-50 p-3 text-sm text-red-600" data-testid="error-message">
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
bind:value={password}
|
bind:value={password}
|
||||||
placeholder="Password (min 8 characters)"
|
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"
|
class="mb-3 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
|
||||||
data-testid="password-input"
|
data-testid="password-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
bind:value={confirmPassword}
|
bind:value={confirmPassword}
|
||||||
placeholder="Confirm password"
|
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"
|
class="mb-4 w-full rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-base)] px-4 py-3 text-sm text-[var(--text)] outline-none placeholder:text-[var(--text-muted)] focus:border-[var(--primary)]"
|
||||||
data-testid="confirm-password-input"
|
data-testid="confirm-password-input"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="w-full rounded-lg bg-amber-500 py-3 text-sm font-medium text-white transition-colors hover:bg-amber-600 disabled:opacity-50"
|
class="w-full rounded-sm bg-[var(--primary)] py-3 text-sm font-medium text-white transition-colors hover:bg-[var(--primary-hover)] disabled:opacity-50"
|
||||||
data-testid="setup-btn"
|
data-testid="setup-btn"
|
||||||
>
|
>
|
||||||
{loading ? 'Setting up...' : 'Set password & start'}
|
{loading ? 'Setting up...' : 'Set password & start'}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ test.describe('Notes CRUD', () => {
|
|||||||
test('Scenario: Fresh account shows an empty notes list', async ({ authenticatedPage: page }) => {
|
test('Scenario: Fresh account shows an empty notes list', async ({ authenticatedPage: page }) => {
|
||||||
// Given the user has no notes
|
// Given the user has no notes
|
||||||
// Then the empty state message is displayed
|
// Then the empty state message is displayed
|
||||||
await expect(page.getByText('Your crumbs will appear here')).toBeVisible();
|
await expect(page.getByText('No crumbs yet')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Scenario: New note appears in the notes list after creation', async ({ authenticatedPage: page }) => {
|
test('Scenario: New note appears in the notes list after creation', async ({ authenticatedPage: page }) => {
|
||||||
|
|||||||
+2
-2
@@ -15,8 +15,8 @@ export default defineConfig({
|
|||||||
description: 'A self-hostable, offline-first note-taking app by Bretzel',
|
description: 'A self-hostable, offline-first note-taking app by Bretzel',
|
||||||
start_url: '/',
|
start_url: '/',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#202124',
|
background_color: '#f0e6d3',
|
||||||
theme_color: '#f59e0b',
|
theme_color: '#C8860A',
|
||||||
icons: [
|
icons: [
|
||||||
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
|
{ src: 'pwa-192x192.png', sizes: '192x192', type: 'image/png' },
|
||||||
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
|
{ src: 'pwa-512x512.png', sizes: '512x512', type: 'image/png' },
|
||||||
|
|||||||
Reference in New Issue
Block a user