refactor(theming): migrate remaining 12 ListViews to theme tokens

Replace raw white-alpha Tailwind utilities across the last 12 module
ListViews that were flagged by validate-theme-tokens: citycorners,
guides, inventory, memoro, picture, plants, playground, presi,
questions, times, uload, who. Also replace semantic color hex/names
(bg-yellow-500/20, bg-green-400, text-blue-400, bg-teal-600, etc.)
with success/warning/error/primary tokens.

Per-deck brand colors in who/ListView (#a855f7 purple/historical,
#ec4899 pink/women, #f59e0b amber/antiquity, #0ea5e9 blue/inventors)
stay as hex — those are domain semantics, not theme intent.

Wire validate:theme-tokens into validate:all so future regressions
fail the local pre-push gate. All 76 module ListViews now pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-22 15:29:43 +02:00
parent a2a43b1d5a
commit 86c205ffc5
13 changed files with 129 additions and 124 deletions

View file

@ -56,20 +56,21 @@
_siblingIds: locations.map((l) => l.id), _siblingIds: locations.map((l) => l.id),
_siblingKey: 'locationId', _siblingKey: 'locationId',
})} })}
class="flex w-full min-h-[44px] items-start gap-2 rounded-md px-2 py-2 transition-colors hover:bg-white/5 cursor-pointer text-left" class="flex w-full min-h-[44px] items-start gap-2 rounded-md px-2 py-2 transition-colors hover:bg-muted/50 cursor-pointer text-left"
> >
<div <div
class="mt-0.5 h-2.5 w-2.5 shrink-0 rounded-full" class="mt-0.5 h-2.5 w-2.5 shrink-0 rounded-full"
style="background: {CATEGORY_COLORS[location.category] ?? '#666'}" style="background: {CATEGORY_COLORS[location.category] ??
'hsl(var(--color-muted-foreground))'}"
></div> ></div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<p class="truncate text-sm text-white/80">{location.name}</p> <p class="truncate text-sm text-foreground">{location.name}</p>
{#if favoriteIds.has(location.id)} {#if favoriteIds.has(location.id)}
<span class="text-xs text-yellow-400">&#9733;</span> <span class="text-xs text-warning">&#9733;</span>
{/if} {/if}
</div> </div>
<p class="text-xs text-white/40"> <p class="text-xs text-muted-foreground">
{categoryLabels[location.category] ?? location.category} {categoryLabels[location.category] ?? location.category}
</p> </p>
</div> </div>

View file

@ -85,7 +85,7 @@
<input <input
bind:value={query} bind:value={query}
placeholder="Guides durchsuchen..." placeholder="Guides durchsuchen..."
class="rounded-md border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<!-- Category filter + create toggle --> <!-- Category filter + create toggle -->
@ -97,8 +97,8 @@
onclick={() => (activeCategory = cat.id)} onclick={() => (activeCategory = cat.id)}
class="rounded-full px-3 py-1 text-xs transition-colors class="rounded-full px-3 py-1 text-xs transition-colors
{activeCategory === cat.id {activeCategory === cat.id
? 'bg-white/15 text-white' ? 'bg-primary/20 text-primary'
: 'bg-white/5 text-white/50 hover:bg-white/10'}" : 'bg-muted/30 text-muted-foreground hover:bg-muted'}"
> >
{cat.label} {cat.label}
</button> </button>
@ -106,7 +106,7 @@
</div> </div>
<button <button
type="button" type="button"
class="shrink-0 text-xs text-white/50 transition-colors hover:text-white/80" class="shrink-0 text-xs text-muted-foreground transition-colors hover:text-foreground"
onclick={() => (creating = !creating)} onclick={() => (creating = !creating)}
> >
{creating ? 'Abbrechen' : '+ Neuer Guide'} {creating ? 'Abbrechen' : '+ Neuer Guide'}
@ -114,17 +114,17 @@
</div> </div>
{#if creating} {#if creating}
<form class="flex flex-col gap-2 rounded-lg bg-white/5 p-3" onsubmit={handleCreate}> <form class="flex flex-col gap-2 rounded-lg bg-muted/30 p-3" onsubmit={handleCreate}>
<input <input
type="text" type="text"
bind:value={newTitle} bind:value={newTitle}
placeholder="Guide-Titel" placeholder="Guide-Titel"
required required
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<select <select
bind:value={newCategory} bind:value={newCategory}
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground focus:border-ring focus:outline-none"
> >
{#each Object.entries(GUIDE_CATEGORIES) as [key, info] (key)} {#each Object.entries(GUIDE_CATEGORIES) as [key, info] (key)}
<option value={key}>{info.label}</option> <option value={key}>{info.label}</option>
@ -132,7 +132,7 @@
</select> </select>
<button <button
type="submit" type="submit"
class="rounded-md bg-teal-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-teal-700 disabled:cursor-not-allowed disabled:opacity-50" class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!newTitle.trim()} disabled={!newTitle.trim()}
> >
Guide erstellen Guide erstellen
@ -152,28 +152,28 @@
{@const progress = getStepProgress(run ?? null, totalSteps)} {@const progress = getStepProgress(run ?? null, totalSteps)}
<button <button
onclick={() => openGuide(guide)} onclick={() => openGuide(guide)}
class="mb-2 w-full rounded-md border border-white/5 bg-white/5 p-3 text-left transition-colors hover:bg-white/10" class="mb-2 w-full rounded-md border border-border bg-muted/30 p-3 text-left transition-colors hover:bg-muted"
> >
<div class="mb-1 flex items-center gap-2"> <div class="mb-1 flex items-center gap-2">
<span class="inline-block h-2 w-2 rounded-full {meta.color}"></span> <span class="inline-block h-2 w-2 rounded-full {meta.color}"></span>
<span class="text-xs text-white/40">{meta.label}</span> <span class="text-xs text-muted-foreground">{meta.label}</span>
<span class="ml-auto text-xs text-white/30">{guide.estimatedMinutes} min</span> <span class="ml-auto text-xs text-muted-foreground/70">{guide.estimatedMinutes} min</span>
</div> </div>
<h3 class="text-sm font-medium text-white/90">{guide.title}</h3> <h3 class="text-sm font-medium text-foreground">{guide.title}</h3>
<p class="mt-0.5 text-xs text-white/50">{guide.description}</p> <p class="mt-0.5 text-xs text-muted-foreground">{guide.description}</p>
<div class="mt-1.5 flex items-center gap-2"> <div class="mt-1.5 flex items-center gap-2">
<span class="text-[10px] uppercase tracking-wide text-white/30"> <span class="text-[10px] uppercase tracking-wide text-muted-foreground/70">
{DIFFICULTY_LABELS[guide.difficulty]} {DIFFICULTY_LABELS[guide.difficulty]}
</span> </span>
{#if run} {#if run}
{#if run.completedAt} {#if run.completedAt}
<span class="ml-auto text-[10px] font-medium text-green-400">Abgeschlossen</span> <span class="ml-auto text-[10px] font-medium text-success">Abgeschlossen</span>
{:else if totalSteps > 0} {:else if totalSteps > 0}
<div class="ml-auto flex items-center gap-1.5"> <div class="ml-auto flex items-center gap-1.5">
<div class="h-1 w-12 rounded-full bg-white/10"> <div class="h-1 w-12 rounded-full bg-muted">
<div class="h-full rounded-full bg-teal-400" style="width: {progress}%"></div> <div class="h-full rounded-full bg-primary" style="width: {progress}%"></div>
</div> </div>
<span class="text-[10px] text-white/30">{progress}%</span> <span class="text-[10px] text-muted-foreground/70">{progress}%</span>
</div> </div>
{/if} {/if}
{/if} {/if}

View file

@ -108,17 +108,17 @@
_siblingIds: collections.map((c) => c.id), _siblingIds: collections.map((c) => c.id),
_siblingKey: 'collectionId', _siblingKey: 'collectionId',
})} })}
class="mb-2 w-full rounded-md border border-white/10 px-3 py-2.5 text-left transition-colors hover:bg-white/5 min-h-[44px]" class="mb-2 w-full rounded-md border border-border px-3 py-2.5 text-left transition-colors hover:bg-muted/50 min-h-[44px]"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
{#if collection.icon} {#if collection.icon}
<span class="text-sm">{collection.icon}</span> <span class="text-sm">{collection.icon}</span>
{/if} {/if}
<p class="flex-1 truncate text-sm font-medium text-white/80">{collection.name}</p> <p class="flex-1 truncate text-sm font-medium text-foreground">{collection.name}</p>
<span class="text-xs text-white/40">{itemsInCollection(collection.id)}</span> <span class="text-xs text-muted-foreground">{itemsInCollection(collection.id)}</span>
</div> </div>
{#if collection.description} {#if collection.description}
<p class="mt-1 truncate text-xs text-white/30">{collection.description}</p> <p class="mt-1 truncate text-xs text-muted-foreground/70">{collection.description}</p>
{/if} {/if}
</button> </button>
{/snippet} {/snippet}

View file

@ -58,10 +58,10 @@
} }
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
pending: 'bg-yellow-500/20 text-yellow-300', pending: 'bg-warning/20 text-warning',
processing: 'bg-blue-500/20 text-blue-300', processing: 'bg-primary/20 text-primary',
completed: 'bg-green-500/20 text-green-300', completed: 'bg-success/20 text-success',
failed: 'bg-red-500/20 text-red-300', failed: 'bg-error/20 text-error',
}; };
</script> </script>
@ -80,26 +80,26 @@
_siblingIds: sorted.map((m) => m.id), _siblingIds: sorted.map((m) => m.id),
_siblingKey: 'memoId', _siblingKey: 'memoId',
})} })}
class="mb-2 w-full rounded-md border border-white/10 px-3 py-2.5 text-left transition-colors hover:bg-white/5 min-h-[44px]" class="mb-2 w-full rounded-md border border-border px-3 py-2.5 text-left transition-colors hover:bg-muted/50 min-h-[44px]"
> >
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
{#if memo.isPinned} {#if memo.isPinned}
<span class="text-xs text-white/30">&#128204;</span> <span class="text-xs text-muted-foreground/70">&#128204;</span>
{/if} {/if}
<p class="truncate text-sm font-medium text-white/80"> <p class="truncate text-sm font-medium text-foreground">
{memo.title || 'Unbenanntes Memo'} {memo.title || 'Unbenanntes Memo'}
</p> </p>
</div> </div>
{#if memo.intro} {#if memo.intro}
<p class="mt-0.5 truncate text-xs text-white/40">{memo.intro}</p> <p class="mt-0.5 truncate text-xs text-muted-foreground">{memo.intro}</p>
{/if} {/if}
</div> </div>
<div class="flex items-center gap-1.5 shrink-0"> <div class="flex items-center gap-1.5 shrink-0">
{#if memo.transcriptModel && memo.processingStatus === 'completed'} {#if memo.transcriptModel && memo.processingStatus === 'completed'}
<span <span
class="rounded px-1 py-0.5 text-[9px] bg-white/5 text-white/30" class="rounded px-1 py-0.5 text-[9px] bg-muted text-muted-foreground"
title="STT-Pipeline" title="STT-Pipeline"
> >
{memo.transcriptModel} {memo.transcriptModel}

View file

@ -242,7 +242,7 @@
{/snippet} {/snippet}
{#snippet item(image)} {#snippet item(image)}
<div class="group relative aspect-square overflow-hidden rounded-md bg-white/5"> <div class="group relative aspect-square overflow-hidden rounded-md bg-muted/50">
{#if image.publicUrl} {#if image.publicUrl}
<img <img
src={image.publicUrl} src={image.publicUrl}
@ -251,12 +251,12 @@
loading="lazy" loading="lazy"
/> />
{:else} {:else}
<div class="flex h-full items-center justify-center text-white/20 text-xs"> <div class="flex h-full items-center justify-center text-muted-foreground/60 text-xs">
{image.format ?? 'img'} {image.format ?? 'img'}
</div> </div>
{/if} {/if}
{#if image.isFavorite} {#if image.isFavorite}
<span class="absolute right-1 top-1 text-xs text-yellow-400">&#9733;</span> <span class="absolute right-1 top-1 text-xs text-warning">&#9733;</span>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}
@ -334,7 +334,7 @@
object-fit: cover; object-fit: cover;
} }
.upload-thumb.success { .upload-thumb.success {
outline: 2px solid #22c55e; outline: 2px solid hsl(var(--color-success));
outline-offset: -2px; outline-offset: -2px;
} }
.upload-thumb.error { .upload-thumb.error {

View file

@ -74,7 +74,7 @@
<div class="flex items-center justify-end"> <div class="flex items-center justify-end">
<button <button
type="button" type="button"
class="text-xs text-white/50 transition-colors hover:text-white/80" class="text-xs text-muted-foreground transition-colors hover:text-foreground"
onclick={() => (creating = !creating)} onclick={() => (creating = !creating)}
> >
{creating {creating
@ -84,13 +84,13 @@
</div> </div>
{#if creating} {#if creating}
<form class="flex flex-col gap-2 rounded-lg bg-white/5 p-3" onsubmit={handleCreate}> <form class="flex flex-col gap-2 rounded-lg bg-muted/30 p-3" onsubmit={handleCreate}>
<input <input
type="text" type="text"
bind:value={newName} bind:value={newName}
placeholder={$_('plants.create.namePlaceholder', { default: 'Name (z. B. Monstera)' })} placeholder={$_('plants.create.namePlaceholder', { default: 'Name (z. B. Monstera)' })}
required required
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<input <input
type="text" type="text"
@ -98,11 +98,11 @@
placeholder={$_('plants.create.scientificPlaceholder', { placeholder={$_('plants.create.scientificPlaceholder', {
default: 'Botanischer Name (optional)', default: 'Botanischer Name (optional)',
})} })}
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<button <button
type="submit" type="submit"
class="rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50" class="rounded-md bg-success px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-success/90 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!newName.trim()} disabled={!newName.trim()}
> >
{$_('plants.create.save', { default: 'Pflanze anlegen' })} {$_('plants.create.save', { default: 'Pflanze anlegen' })}
@ -114,12 +114,12 @@
{#snippet header()} {#snippet header()}
<span>{$_('plants.list.count', { values: { count: plants.length } })}</span> <span>{$_('plants.list.count', { values: { count: plants.length } })}</span>
{#if dueForWatering.length > 0} {#if dueForWatering.length > 0}
<span class="text-blue-400" <span class="text-primary"
>{$_('plants.list.dueWatering', { values: { count: dueForWatering.length } })}</span >{$_('plants.list.dueWatering', { values: { count: dueForWatering.length } })}</span
> >
{/if} {/if}
{#if needsAttention.length > 0} {#if needsAttention.length > 0}
<span class="text-amber-400" <span class="text-warning"
>{$_('plants.list.needsCare', { values: { count: needsAttention.length } })}</span >{$_('plants.list.needsCare', { values: { count: needsAttention.length } })}</span
> >
{/if} {/if}
@ -135,24 +135,24 @@
_siblingIds: plants.map((p) => p.id), _siblingIds: plants.map((p) => p.id),
_siblingKey: 'plantId', _siblingKey: 'plantId',
})} })}
class="mb-2 w-full rounded-md border border-white/10 px-3 py-2.5 text-left transition-colors hover:bg-white/5 min-h-[44px]" class="mb-2 w-full rounded-md border border-border px-3 py-2.5 text-left transition-colors hover:bg-muted/50 min-h-[44px]"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-sm"> <span class="text-sm">
{@html healthIcons[plant.healthStatus ?? 'healthy'] ?? '&#127793;'} {@html healthIcons[plant.healthStatus ?? 'healthy'] ?? '&#127793;'}
</span> </span>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-white/80">{plant.name}</p> <p class="truncate text-sm font-medium text-foreground">{plant.name}</p>
{#if plant.scientificName} {#if plant.scientificName}
<p class="truncate text-xs italic text-white/30">{plant.scientificName}</p> <p class="truncate text-xs italic text-muted-foreground/70">{plant.scientificName}</p>
{/if} {/if}
</div> </div>
{#if waterDue} {#if waterDue}
<span class="text-xs text-blue-400">&#128167;</span> <span class="text-xs text-primary">&#128167;</span>
{/if} {/if}
</div> </div>
{#if schedule} {#if schedule}
<p class="mt-1 text-xs text-white/30"> <p class="mt-1 text-xs text-muted-foreground/70">
{$_('plants.list.everyXDays', { values: { days: schedule.frequencyDays } })} {$_('plants.list.everyXDays', { values: { days: schedule.frequencyDays } })}
</p> </p>
{/if} {/if}

View file

@ -91,10 +91,10 @@
<!-- Model selector --> <!-- Model selector -->
<select <select
bind:value={selectedModel} bind:value={selectedModel}
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white/70 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground/90 focus:border-ring focus:outline-none"
> >
{#each modelOptions as model} {#each modelOptions as model}
<option value={model.id} class="bg-neutral-900">{model.label} ({model.provider})</option> <option value={model.id} class="bg-card">{model.label} ({model.provider})</option>
{/each} {/each}
</select> </select>
@ -103,21 +103,23 @@
{#each messages as msg} {#each messages as msg}
<div <div
class="mb-2 min-h-[44px] rounded-md px-3 py-2 {msg.role === 'user' class="mb-2 min-h-[44px] rounded-md px-3 py-2 {msg.role === 'user'
? 'bg-white/5' ? 'bg-muted/30'
: 'bg-blue-500/10'}" : 'bg-primary/10'}"
> >
<p class="text-[10px] text-white/30">{msg.role === 'user' ? 'Du' : modelLabel}</p> <p class="text-[10px] text-muted-foreground/70">
{msg.role === 'user' ? 'Du' : modelLabel}
</p>
{#if msg.content} {#if msg.content}
<p class="whitespace-pre-wrap text-sm text-white/70">{msg.content}</p> <p class="whitespace-pre-wrap text-sm text-foreground/90">{msg.content}</p>
{:else} {:else}
<p class="text-sm text-white/30"></p> <p class="text-sm text-muted-foreground/70"></p>
{/if} {/if}
</div> </div>
{/each} {/each}
{#if messages.length === 0} {#if messages.length === 0}
<div class="flex h-full items-center justify-center"> <div class="flex h-full items-center justify-center">
<p class="text-sm text-white/30">Schreib einen Prompt...</p> <p class="text-sm text-muted-foreground/70">Schreib einen Prompt...</p>
</div> </div>
{/if} {/if}
</div> </div>
@ -133,20 +135,20 @@
<input <input
bind:value={prompt} bind:value={prompt}
placeholder="Prompt eingeben..." placeholder="Prompt eingeben..."
class="flex-1 rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="flex-1 rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
{#if isLoading} {#if isLoading}
<button <button
type="button" type="button"
onclick={stop} onclick={stop}
class="rounded-md bg-red-500/20 px-3 py-1.5 text-sm text-red-200 transition-colors hover:bg-red-500/30" class="rounded-md bg-error/20 px-3 py-1.5 text-sm text-error transition-colors hover:bg-error/30"
>Stop</button >Stop</button
> >
{:else} {:else}
<button <button
type="submit" type="submit"
disabled={!prompt.trim()} disabled={!prompt.trim()}
class="rounded-md bg-white/10 px-3 py-1.5 text-sm text-white/70 transition-colors hover:bg-white/15 disabled:opacity-50" class="rounded-md bg-muted px-3 py-1.5 text-sm text-foreground/80 transition-colors hover:bg-muted/80 disabled:opacity-50"
>&#9654;</button >&#9654;</button
> >
{/if} {/if}

View file

@ -57,10 +57,10 @@
<BaseListView items={decks} getKey={(d) => d.id} emptyTitle="Keine Präsentationen"> <BaseListView items={decks} getKey={(d) => d.id} emptyTitle="Keine Präsentationen">
{#snippet toolbar()} {#snippet toolbar()}
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xs text-white/40">{decks.length} Präsentationen</span> <span class="text-xs text-muted-foreground">{decks.length} Präsentationen</span>
<button <button
type="button" type="button"
class="text-xs text-white/50 transition-colors hover:text-white/80" class="text-xs text-muted-foreground transition-colors hover:text-foreground"
onclick={() => (creating = !creating)} onclick={() => (creating = !creating)}
> >
{creating ? 'Abbrechen' : '+ Neue Präsentation'} {creating ? 'Abbrechen' : '+ Neue Präsentation'}
@ -68,23 +68,23 @@
</div> </div>
{#if creating} {#if creating}
<form class="flex flex-col gap-2 rounded-lg bg-white/5 p-3" onsubmit={handleCreate}> <form class="flex flex-col gap-2 rounded-lg bg-muted/30 p-3" onsubmit={handleCreate}>
<input <input
type="text" type="text"
bind:value={newTitle} bind:value={newTitle}
placeholder="Titel (z. B. Q2 Review)" placeholder="Titel (z. B. Q2 Review)"
required required
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<input <input
type="text" type="text"
bind:value={newDescription} bind:value={newDescription}
placeholder="Beschreibung (optional)" placeholder="Beschreibung (optional)"
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<button <button
type="submit" type="submit"
class="rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50" class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!newTitle.trim()} disabled={!newTitle.trim()}
> >
Präsentation erstellen Präsentation erstellen
@ -105,17 +105,17 @@
_siblingIds: decks.map((d) => d.id), _siblingIds: decks.map((d) => d.id),
_siblingKey: 'deckId', _siblingKey: 'deckId',
})} })}
class="mb-2 w-full rounded-md border border-white/10 px-3 py-2.5 text-left transition-colors hover:bg-white/5 min-h-[44px]" class="mb-2 w-full rounded-md border border-border px-3 py-2.5 text-left transition-colors hover:bg-muted/50 min-h-[44px]"
> >
<p class="truncate text-sm font-medium text-white/80">{deck.title}</p> <p class="truncate text-sm font-medium text-foreground">{deck.title}</p>
<div class="mt-1 flex items-center gap-2 text-xs text-white/40"> <div class="mt-1 flex items-center gap-2 text-xs text-muted-foreground">
<span>{slideCount(deck.id)} Folien</span> <span>{slideCount(deck.id)} Folien</span>
{#if deck.isPublic} {#if deck.isPublic}
<span class="rounded bg-white/10 px-1.5 py-0.5 text-[10px]">Öffentlich</span> <span class="rounded bg-muted px-1.5 py-0.5 text-[10px]">Öffentlich</span>
{/if} {/if}
</div> </div>
{#if deck.description} {#if deck.description}
<p class="mt-1 truncate text-xs text-white/30">{deck.description}</p> <p class="mt-1 truncate text-xs text-muted-foreground/70">{deck.description}</p>
{/if} {/if}
</button> </button>
{/snippet} {/snippet}

View file

@ -59,10 +59,10 @@
const collections = $derived(collectionsQuery.value); const collections = $derived(collectionsQuery.value);
const statusColors: Record<string, string> = { const statusColors: Record<string, string> = {
open: 'bg-blue-500/20 text-blue-300', open: 'bg-primary/20 text-primary',
researching: 'bg-amber-500/20 text-amber-300', researching: 'bg-warning/20 text-warning',
answered: 'bg-green-500/20 text-green-300', answered: 'bg-success/20 text-success',
archived: 'bg-white/10 text-white/40', archived: 'bg-muted text-muted-foreground',
}; };
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
@ -82,12 +82,12 @@
<BaseListView items={sorted} getKey={(q) => q.id} emptyTitle="Keine offenen Fragen"> <BaseListView items={sorted} getKey={(q) => q.id} emptyTitle="Keine offenen Fragen">
{#snippet toolbar()} {#snippet toolbar()}
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-xs text-white/40" <span class="text-xs text-muted-foreground"
>{questions.length} Fragen · {collections.length} Sammlungen</span >{questions.length} Fragen · {collections.length} Sammlungen</span
> >
<button <button
type="button" type="button"
class="text-xs text-white/50 transition-colors hover:text-white/80" class="text-xs text-muted-foreground transition-colors hover:text-foreground"
onclick={() => (creating = !creating)} onclick={() => (creating = !creating)}
> >
{creating ? 'Abbrechen' : '+ Neue Frage'} {creating ? 'Abbrechen' : '+ Neue Frage'}
@ -95,23 +95,23 @@
</div> </div>
{#if creating} {#if creating}
<form class="flex flex-col gap-2 rounded-lg bg-white/5 p-3" onsubmit={handleCreate}> <form class="flex flex-col gap-2 rounded-lg bg-muted/30 p-3" onsubmit={handleCreate}>
<input <input
type="text" type="text"
bind:value={newTitle} bind:value={newTitle}
placeholder="Was möchtest du herausfinden?" placeholder="Was möchtest du herausfinden?"
required required
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<input <input
type="text" type="text"
bind:value={newDescription} bind:value={newDescription}
placeholder="Kontext / Details (optional)" placeholder="Kontext / Details (optional)"
class="rounded-md border border-white/10 bg-white/5 px-3 py-1.5 text-sm text-white placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="rounded-md border border-border bg-muted/30 px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
<button <button
type="submit" type="submit"
class="rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50" class="rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
disabled={!newTitle.trim()} disabled={!newTitle.trim()}
> >
Frage stellen Frage stellen
@ -133,10 +133,10 @@
_siblingIds: sorted.map((q) => q.id), _siblingIds: sorted.map((q) => q.id),
_siblingKey: 'questionId', _siblingKey: 'questionId',
})} })}
class="mb-2 w-full text-left rounded-md border border-white/10 px-3 py-2.5 transition-colors hover:bg-white/5 cursor-pointer min-h-[44px]" class="mb-2 w-full text-left rounded-md border border-border px-3 py-2.5 transition-colors hover:bg-muted/50 cursor-pointer min-h-[44px]"
> >
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<p class="text-sm font-medium text-white/80">{question.title}</p> <p class="text-sm font-medium text-foreground">{question.title}</p>
<span <span
class="shrink-0 rounded px-1.5 py-0.5 text-[10px] {statusColors[question.status] ?? ''}" class="shrink-0 rounded px-1.5 py-0.5 text-[10px] {statusColors[question.status] ?? ''}"
> >
@ -144,12 +144,14 @@
</span> </span>
</div> </div>
{#if question.description} {#if question.description}
<p class="mt-1 truncate text-xs text-white/30">{question.description}</p> <p class="mt-1 truncate text-xs text-muted-foreground/70">{question.description}</p>
{/if} {/if}
{#if question.tags.length > 0} {#if question.tags.length > 0}
<div class="mt-1 flex gap-1"> <div class="mt-1 flex gap-1">
{#each question.tags.slice(0, 3) as tag} {#each question.tags.slice(0, 3) as tag}
<span class="rounded bg-white/5 px-1.5 py-0.5 text-[10px] text-white/40">{tag}</span> <span class="rounded bg-muted/50 px-1.5 py-0.5 text-[10px] text-muted-foreground"
>{tag}</span
>
{/each} {/each}
</div> </div>
{/if} {/if}

View file

@ -101,8 +101,8 @@
<button <button
onclick={handleStartStop} onclick={handleStartStop}
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors {timerStore.isRunning class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors {timerStore.isRunning
? 'bg-red-500/80 text-white hover:bg-red-500' ? 'bg-error/80 text-white hover:bg-error'
: 'bg-white/10 text-white/50 hover:bg-green-500/80 hover:text-white'}" : 'bg-muted text-muted-foreground hover:bg-success/80 hover:text-white'}"
> >
{#if timerStore.isRunning} {#if timerStore.isRunning}
<Stop size={14} weight="fill" /> <Stop size={14} weight="fill" />
@ -115,12 +115,12 @@
value={description} value={description}
oninput={(e) => handleDescriptionInput((e.target as HTMLInputElement).value)} oninput={(e) => handleDescriptionInput((e.target as HTMLInputElement).value)}
placeholder="Was trackst du?" placeholder="Was trackst du?"
class="min-w-0 flex-1 rounded-md border border-white/10 bg-white/5 px-2.5 py-1.5 text-xs text-white/90 placeholder:text-white/30 focus:border-white/20 focus:outline-none" class="min-w-0 flex-1 rounded-md border border-border bg-muted/30 px-2.5 py-1.5 text-xs text-foreground placeholder:text-muted-foreground/60 focus:border-ring focus:outline-none"
/> />
{#if timerStore.isRunning} {#if timerStore.isRunning}
<div class="flex h-7 items-center gap-1.5 rounded-full bg-green-500/10 px-2.5"> <div class="flex h-7 items-center gap-1.5 rounded-full bg-success/10 px-2.5">
<div class="h-1.5 w-1.5 animate-pulse rounded-full bg-green-400"></div> <div class="h-1.5 w-1.5 animate-pulse rounded-full bg-success"></div>
<span class="font-mono text-xs text-green-400"> <span class="font-mono text-xs text-success">
{formatDuration(timerStore.elapsedSeconds)} {formatDuration(timerStore.elapsedSeconds)}
</span> </span>
</div> </div>
@ -132,21 +132,21 @@
<span class="flex-1" <span class="flex-1"
>Heute: {todayEntries.length} Eintr{todayEntries.length === 1 ? 'ag' : 'age'}</span >Heute: {todayEntries.length} Eintr{todayEntries.length === 1 ? 'ag' : 'age'}</span
> >
<span class="font-medium text-white/60">{fmtCompact(totalToday)}</span> <span class="font-medium text-foreground/80">{fmtCompact(totalToday)}</span>
{/snippet} {/snippet}
{#snippet item(entry)} {#snippet item(entry)}
<button <button
onclick={() => navigate('detail', { entryId: entry.id })} onclick={() => navigate('detail', { entryId: entry.id })}
class="mb-1 w-full min-h-[44px] rounded-md px-3 py-2 text-left transition-colors hover:bg-white/5" class="mb-1 w-full min-h-[44px] rounded-md px-3 py-2 text-left transition-colors hover:bg-muted/50"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="truncate text-sm text-white/80"> <p class="truncate text-sm text-foreground">
{entry.description || 'Ohne Beschreibung'} {entry.description || 'Ohne Beschreibung'}
</p> </p>
<span class="shrink-0 text-xs text-white/50">{fmtCompact(entry.duration)}</span> <span class="shrink-0 text-xs text-muted-foreground">{fmtCompact(entry.duration)}</span>
</div> </div>
<p class="text-xs text-white/30">{projectName(entry.projectId)}</p> <p class="text-xs text-muted-foreground/70">{projectName(entry.projectId)}</p>
</button> </button>
{/snippet} {/snippet}
</BaseListView> </BaseListView>

View file

@ -137,17 +137,17 @@
_siblingIds: sorted.map((l) => l.id), _siblingIds: sorted.map((l) => l.id),
_siblingKey: 'linkId', _siblingKey: 'linkId',
})} })}
class="mb-1 w-full min-h-[44px] text-left rounded-md px-3 py-2 transition-colors hover:bg-white/5 cursor-pointer" class="mb-1 w-full min-h-[44px] text-left rounded-md px-3 py-2 transition-colors hover:bg-muted/50 cursor-pointer"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p class="truncate text-sm font-medium text-white/80"> <p class="truncate text-sm font-medium text-foreground">
{link.title || link.shortCode} {link.title || link.shortCode}
</p> </p>
<span class="shrink-0 text-xs text-white/40">{link.clickCount}</span> <span class="shrink-0 text-xs text-muted-foreground">{link.clickCount}</span>
</div> </div>
<p class="truncate text-xs text-white/30">{hostname(link.originalUrl)}</p> <p class="truncate text-xs text-muted-foreground/70">{hostname(link.originalUrl)}</p>
{#if link.customCode} {#if link.customCode}
<p class="text-xs text-blue-400/60">/{link.customCode}</p> <p class="text-xs text-primary/70">/{link.customCode}</p>
{/if} {/if}
</button> </button>
{/snippet} {/snippet}

View file

@ -119,25 +119,25 @@
<div class="flex h-full flex-col gap-6 p-3 sm:p-4"> <div class="flex h-full flex-col gap-6 p-3 sm:p-4">
<!-- Header --> <!-- Header -->
<div> <div>
<h1 class="text-2xl font-bold text-white/90">Who?</h1> <h1 class="text-2xl font-bold text-foreground">Who?</h1>
<p class="mt-1 text-sm text-white/60"> <p class="mt-1 text-sm text-muted-foreground">
Errate die historische Persönlichkeit. Eine KI verkörpert sie ohne den Namen zu verraten. Errate die historische Persönlichkeit. Eine KI verkörpert sie ohne den Namen zu verraten.
</p> </p>
</div> </div>
<!-- Deck picker --> <!-- Deck picker -->
<section> <section>
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-white/50"> <h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Neues Spiel starten Neues Spiel starten
</h2> </h2>
{#if loadingDecks} {#if loadingDecks}
<div class="grid gap-3 sm:grid-cols-2"> <div class="grid gap-3 sm:grid-cols-2">
{#each Array(4) as _, i (i)} {#each Array(4) as _, i (i)}
<div class="h-24 animate-pulse rounded-lg bg-white/5"></div> <div class="h-24 animate-pulse rounded-lg bg-muted/30"></div>
{/each} {/each}
</div> </div>
{:else if decks.length === 0} {:else if decks.length === 0}
<p class="text-sm text-white/40">Keine Decks verfügbar.</p> <p class="text-sm text-muted-foreground">Keine Decks verfügbar.</p>
{:else} {:else}
<div class="grid gap-3 sm:grid-cols-2"> <div class="grid gap-3 sm:grid-cols-2">
{#each decks as deck (deck.id)} {#each decks as deck (deck.id)}
@ -145,23 +145,23 @@
type="button" type="button"
onclick={() => startGame(deck.id)} onclick={() => startGame(deck.id)}
disabled={starting !== null} disabled={starting !== null}
class="group flex flex-col items-start gap-2 rounded-lg border border-white/10 bg-white/[0.02] p-4 text-left transition hover:border-white/20 hover:bg-white/[0.05] disabled:cursor-wait disabled:opacity-50" class="group flex flex-col items-start gap-2 rounded-lg border border-border bg-muted/20 p-4 text-left transition hover:border-border-strong hover:bg-muted/40 disabled:cursor-wait disabled:opacity-50"
style="border-left: 3px solid {deckColor(deck.id)}" style="border-left: 3px solid {deckColor(deck.id)}"
> >
<div class="flex w-full items-center justify-between"> <div class="flex w-full items-center justify-between">
<span class="text-base font-medium text-white/90">{deck.name.de}</span> <span class="text-base font-medium text-foreground">{deck.name.de}</span>
<span <span
class="rounded-full bg-white/5 px-2 py-0.5 text-[10px] uppercase tracking-wide text-white/50" class="rounded-full bg-muted px-2 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground"
> >
{difficultyLabel(deck.difficulty)} {difficultyLabel(deck.difficulty)}
</span> </span>
</div> </div>
<p class="text-xs text-white/60">{deck.description.de}</p> <p class="text-xs text-muted-foreground">{deck.description.de}</p>
<p class="text-[11px] text-white/40"> <p class="text-[11px] text-muted-foreground/70">
{deck.characterCount} Personen · {deck.categories.join(', ')} {deck.characterCount} Personen · {deck.categories.join(', ')}
</p> </p>
{#if starting === deck.id} {#if starting === deck.id}
<p class="text-xs text-white/70">Starte…</p> <p class="text-xs text-foreground/80">Starte…</p>
{/if} {/if}
</button> </button>
{/each} {/each}
@ -172,30 +172,30 @@
<!-- Past games --> <!-- Past games -->
{#if games.length > 0} {#if games.length > 0}
<section> <section>
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-white/50"> <h2 class="mb-3 text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Vergangene Spiele Vergangene Spiele
</h2> </h2>
<ul class="divide-y divide-white/5 rounded-lg border border-white/10 bg-white/[0.02]"> <ul class="divide-y divide-border rounded-lg border border-border bg-muted/20">
{#each games as game (game.id)} {#each games as game (game.id)}
<li class="flex items-center gap-3 px-3 py-2.5"> <li class="flex items-center gap-3 px-3 py-2.5">
<span class="text-lg">{statusEmoji(game.status)}</span> <span class="text-lg">{statusEmoji(game.status)}</span>
<button type="button" class="flex-1 text-left" onclick={() => openGame(game.id)}> <button type="button" class="flex-1 text-left" onclick={() => openGame(game.id)}>
<div class="text-sm text-white/90"> <div class="text-sm text-foreground">
{#if game.revealedName} {#if game.revealedName}
<span class="font-medium">{game.revealedName}</span> <span class="font-medium">{game.revealedName}</span>
{:else if game.status === 'playing'} {:else if game.status === 'playing'}
<span class="text-white/60">Laufendes Spiel</span> <span class="text-muted-foreground">Laufendes Spiel</span>
{:else} {:else}
<span class="text-white/60">Aufgegeben</span> <span class="text-muted-foreground">Aufgegeben</span>
{/if} {/if}
</div> </div>
<div class="text-[11px] text-white/40"> <div class="text-[11px] text-muted-foreground/70">
{game.deckId} · {game.messageCount} Nachrichten · {gameStatusLabel(game.status)} {game.deckId} · {game.messageCount} Nachrichten · {gameStatusLabel(game.status)}
</div> </div>
</button> </button>
<button <button
type="button" type="button"
class="rounded p-1 text-white/30 hover:bg-white/5 hover:text-white/60" class="rounded p-1 text-muted-foreground/70 hover:bg-muted hover:text-foreground"
onclick={() => deleteGame(game.id)} onclick={() => deleteGame(game.id)}
title="Löschen" title="Löschen"
> >
@ -208,7 +208,7 @@
{/if} {/if}
{#if error} {#if error}
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-300"> <div class="rounded-lg border border-error/30 bg-error/10 p-3 text-sm text-error">
{error} {error}
</div> </div>
{/if} {/if}

View file

@ -22,7 +22,7 @@
"validate:turbo": "node scripts/validate-no-recursive-turbo.mjs", "validate:turbo": "node scripts/validate-no-recursive-turbo.mjs",
"validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs", "validate:pg-schema": "node scripts/validate-pg-schema-isolation.mjs",
"validate:theme-tokens": "node scripts/validate-theme-tokens.mjs", "validate:theme-tokens": "node scripts/validate-theme-tokens.mjs",
"validate:all": "pnpm run validate:turbo && pnpm run validate:pg-schema && pnpm run check:crypto", "validate:all": "pnpm run validate:turbo && pnpm run validate:pg-schema && pnpm run validate:theme-tokens && pnpm run check:crypto",
"check:crypto": "node scripts/audit-crypto-registry.mjs", "check:crypto": "node scripts/audit-crypto-registry.mjs",
"check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed", "check:crypto:seed": "node scripts/audit-crypto-registry.mjs --seed",
"audit:deps": "node scripts/audit-workspace-deps.mjs", "audit:deps": "node scripts/audit-workspace-deps.mjs",