feat: implement light/dark theme toggle with CSS custom properties

- Add CSS custom properties for theme colors (:root and .dark)
- Create utility classes: bg-theme-*, text-theme-*, border-theme-*
- Update +layout.svelte main containers
- Update Sidenav with theme-aware navigation links
- Update TopNav header and action buttons
- Update ChatWindow main area and input section
- Update ChatInput with themed input container
- Update MessageItem with theme-aware message bubbles
- Update EmptyState with themed welcome cards

Theme colors automatically switch between light and dark mode
when clicking the theme toggle button in the top navigation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-01 04:46:31 +01:00
parent 243f00f85f
commit 8fa6fdec1f
8 changed files with 111 additions and 52 deletions

View File

@@ -2,10 +2,69 @@
@tailwind components;
@tailwind utilities;
/* Theme colors using CSS custom properties */
:root {
/* Light mode (default) */
--color-bg-primary: theme('colors.slate.50');
--color-bg-secondary: theme('colors.slate.100');
--color-bg-tertiary: theme('colors.slate.200');
--color-bg-sidenav: theme('colors.slate.100');
--color-bg-topnav: theme('colors.white');
--color-bg-input: theme('colors.white');
--color-bg-hover: theme('colors.slate.200');
--color-bg-message-user: theme('colors.slate.200');
--color-bg-message-assistant: transparent;
--color-text-primary: theme('colors.slate.900');
--color-text-secondary: theme('colors.slate.600');
--color-text-muted: theme('colors.slate.500');
--color-text-placeholder: theme('colors.slate.400');
--color-border: theme('colors.slate.300');
--color-border-subtle: theme('colors.slate.200');
}
.dark {
/* Dark mode */
--color-bg-primary: theme('colors.slate.900');
--color-bg-secondary: theme('colors.slate.800');
--color-bg-tertiary: theme('colors.slate.700');
--color-bg-sidenav: theme('colors.slate.950');
--color-bg-topnav: theme('colors.slate.900');
--color-bg-input: theme('colors.slate.800');
--color-bg-hover: theme('colors.slate.700');
--color-bg-message-user: theme('colors.slate.700');
--color-bg-message-assistant: transparent;
--color-text-primary: theme('colors.slate.100');
--color-text-secondary: theme('colors.slate.300');
--color-text-muted: theme('colors.slate.400');
--color-text-placeholder: theme('colors.slate.500');
--color-border: theme('colors.slate.700');
--color-border-subtle: theme('colors.slate.800');
}
/* Utility classes for theme colors */
.bg-theme-primary { background-color: var(--color-bg-primary); }
.bg-theme-secondary { background-color: var(--color-bg-secondary); }
.bg-theme-tertiary { background-color: var(--color-bg-tertiary); }
.bg-theme-sidenav { background-color: var(--color-bg-sidenav); }
.bg-theme-topnav { background-color: var(--color-bg-topnav); }
.bg-theme-input { background-color: var(--color-bg-input); }
.bg-theme-hover { background-color: var(--color-bg-hover); }
.bg-theme-message-user { background-color: var(--color-bg-message-user); }
.text-theme-primary { color: var(--color-text-primary); }
.text-theme-secondary { color: var(--color-text-secondary); }
.text-theme-muted { color: var(--color-text-muted); }
.text-theme-placeholder { color: var(--color-text-placeholder); }
.border-theme { border-color: var(--color-border); }
.border-theme-subtle { border-color: var(--color-border-subtle); }
.hover\:bg-theme-hover:hover { background-color: var(--color-bg-hover); }
.placeholder-theme-placeholder::placeholder { color: var(--color-text-placeholder); }
/* Base styles */
html,
body {
@apply h-full;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
/* Text selection styling for dark theme */

View File

@@ -258,8 +258,8 @@
<!-- Full-screen drag overlay - shown when dragging files anywhere on the page -->
{#if isDragOver}
<div class="pointer-events-none fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/80 backdrop-blur-sm">
<div class="flex flex-col items-center gap-3 rounded-2xl border-2 border-dashed border-violet-500 bg-slate-800/90 p-8 text-violet-300">
<div class="pointer-events-none fixed inset-0 z-[100] flex items-center justify-center bg-[var(--color-bg-primary)]/80 backdrop-blur-sm">
<div class="flex flex-col items-center gap-3 rounded-2xl border-2 border-dashed border-violet-500 bg-theme-secondary p-8 text-violet-600 dark:text-violet-300">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="h-12 w-12">
<path fill-rule="evenodd" d="M10.5 3.75a6 6 0 0 0-5.98 6.496A5.25 5.25 0 0 0 6.75 20.25H18a4.5 4.5 0 0 0 2.206-8.423 3.75 3.75 0 0 0-4.133-4.303A6.001 6.001 0 0 0 10.5 3.75Zm2.03 5.47a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72v4.19a.75.75 0 0 0 1.5 0v-4.19l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z" clip-rule="evenodd" />
</svg>
@@ -270,7 +270,7 @@
Drop files here
{/if}
</span>
<span class="text-sm text-slate-400">
<span class="text-sm text-theme-muted">
{#if isVisionModel}
Images, text files, and PDFs supported
{:else}
@@ -295,7 +295,7 @@
/>
<div
class="flex items-end gap-3 rounded-2xl border border-slate-700/50 bg-slate-800/80 p-3 backdrop-blur transition-all focus-within:border-slate-600 focus-within:bg-slate-800"
class="flex items-end gap-3 rounded-2xl border border-theme bg-theme-input p-3 backdrop-blur transition-all focus-within:border-theme-subtle"
>
<!-- Attachment indicators -->
{#if pendingImages.length > 0 || pendingAttachments.length > 0}
@@ -346,7 +346,7 @@
{placeholder}
{disabled}
rows="1"
class="max-h-[200px] min-h-[40px] flex-1 resize-none bg-transparent px-1 py-1.5 text-slate-100 placeholder-slate-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
class="max-h-[200px] min-h-[40px] flex-1 resize-none bg-transparent px-1 py-1.5 text-theme-primary placeholder-theme-placeholder focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
aria-label="Message input"
data-chat-input
></textarea>
@@ -379,7 +379,7 @@
disabled={!canSend}
class="flex h-9 w-9 items-center justify-center rounded-xl transition-colors focus:outline-none focus:ring-2 focus:ring-violet-500/50 {canSend
? 'bg-violet-500/20 text-violet-400 hover:bg-violet-500/30 hover:text-violet-300'
: 'text-slate-600 cursor-not-allowed'}"
: 'text-theme-muted cursor-not-allowed'}"
aria-label="Send message"
title="Send message"
>
@@ -397,19 +397,19 @@
</div>
<!-- Subtle helper text -->
<p class="text-center text-[11px] text-slate-600">
<kbd class="rounded bg-slate-800 px-1 py-0.5 font-mono">Enter</kbd> send
<span class="mx-1.5 text-slate-700">·</span>
<kbd class="rounded bg-slate-800 px-1 py-0.5 font-mono">Shift+Enter</kbd> new line
<span class="mx-1.5 text-slate-700">·</span>
<p class="text-center text-[11px] text-theme-muted">
<kbd class="rounded bg-theme-secondary px-1 py-0.5 font-mono">Enter</kbd> send
<span class="mx-1.5 opacity-50">·</span>
<kbd class="rounded bg-theme-secondary px-1 py-0.5 font-mono">Shift+Enter</kbd> new line
<span class="mx-1.5 opacity-50">·</span>
{#if isVisionModel}
<span class="text-violet-500/70">images</span>
<span class="mx-1 text-slate-700">+</span>
<span class="text-violet-500 dark:text-violet-500/70">images</span>
<span class="mx-1 opacity-50">+</span>
{/if}
<span class="text-slate-500">files supported</span>
<span class="text-theme-muted">files supported</span>
{#if showTokenCount}
<span class="mx-1.5 text-slate-700">·</span>
<span class="text-slate-500" title="{tokenEstimate.textTokens} text + {tokenEstimate.imageTokens} image tokens">
<span class="mx-1.5 opacity-50">·</span>
<span class="text-theme-muted" title="{tokenEstimate.textTokens} text + {tokenEstimate.imageTokens} image tokens">
~{formatTokenCount(tokenEstimate.totalTokens)} tokens
</span>
{/if}

View File

@@ -807,7 +807,7 @@
}
</script>
<div class="flex h-full flex-col bg-slate-900">
<div class="flex h-full flex-col bg-theme-primary">
{#if hasMessages}
<div class="flex-1 overflow-hidden">
<MessageList
@@ -825,9 +825,9 @@
<!-- Input area with subtle gradient fade -->
<div class="relative">
<!-- Gradient fade at top -->
<div class="pointer-events-none absolute -top-8 left-0 right-0 h-8 bg-gradient-to-t from-slate-900 to-transparent"></div>
<div class="pointer-events-none absolute -top-8 left-0 right-0 h-8 bg-gradient-to-t from-[var(--color-bg-primary)] to-transparent"></div>
<div class="border-t border-slate-800/50 bg-slate-900/95 backdrop-blur-sm">
<div class="border-t border-theme bg-theme-primary/95 backdrop-blur-sm">
<!-- Summary recommendation banner -->
<SummaryBanner onSummarize={handleSummarize} isLoading={isSummarizing} />
@@ -850,8 +850,8 @@
<button
type="button"
onclick={() => settingsState.togglePanel()}
class="flex items-center gap-1.5 rounded px-2 py-1 text-xs text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
class:bg-slate-800={settingsState.isPanelOpen}
class="flex items-center gap-1.5 rounded px-2 py-1 text-xs text-theme-muted transition-colors hover:bg-theme-hover hover:text-theme-primary"
class:bg-theme-secondary={settingsState.isPanelOpen}
class:text-sky-400={settingsState.isPanelOpen || settingsState.useCustomParameters}
aria-label="Toggle model parameters"
aria-expanded={settingsState.isPanelOpen}
@@ -881,7 +881,7 @@
<!-- Right side: Thinking mode toggle -->
{#if supportsThinking}
<label class="flex cursor-pointer items-center gap-2 text-xs text-slate-400">
<label class="flex cursor-pointer items-center gap-2 text-xs text-theme-muted">
<span class="flex items-center gap-1">
<span class="text-amber-400">🧠</span>
Thinking mode

View File

@@ -89,7 +89,7 @@
</div>
<!-- Welcome text -->
<h2 class="mb-2 text-xl font-medium text-slate-100">
<h2 class="mb-2 text-xl font-medium text-theme-primary">
{#if hasModel}
Start a conversation
{:else}
@@ -97,9 +97,9 @@
{/if}
</h2>
<p class="mb-8 max-w-md text-sm text-slate-500">
<p class="mb-8 max-w-md text-sm text-theme-muted">
{#if hasModel && selectedModel}
Chatting with <span class="font-medium text-slate-300">{selectedModel.name}</span>
Chatting with <span class="font-medium text-theme-secondary">{selectedModel.name}</span>
{:else}
Select a model from the sidebar to start chatting
{/if}
@@ -155,9 +155,9 @@
onclick={() => selectPrompt(props.type)}
class="flex items-start gap-3 rounded-xl border p-3 text-left transition-all {active
? 'border-violet-500/50 bg-violet-500/10'
: 'border-slate-800/50 bg-slate-800/30 hover:border-slate-700 hover:bg-slate-800/60'}"
: 'border-theme hover:border-theme-subtle bg-theme-secondary/30 hover:bg-theme-secondary/60'}"
>
<div class="flex-shrink-0 {active ? 'text-violet-400' : 'text-slate-500'}">
<div class="flex-shrink-0 {active ? 'text-violet-400' : 'text-theme-muted'}">
{#if props.icon === 'lightbulb'}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4">
<path d="M10 1a6 6 0 00-3.815 10.631C7.237 12.5 8 13.443 8 14.456v.644a.75.75 0 00.572.729 6.016 6.016 0 002.856 0A.75.75 0 0012 15.1v-.644c0-1.013.762-1.957 1.815-2.825A6 6 0 0010 1zM8.863 17.414a.75.75 0 00-.226 1.483 9.066 9.066 0 002.726 0 .75.75 0 00-.226-1.483 7.553 7.553 0 01-2.274 0z" />
@@ -177,8 +177,8 @@
{/if}
</div>
<div>
<h3 class="text-sm font-medium {active ? 'text-violet-200' : 'text-slate-200'}">{props.title}</h3>
<p class="text-xs {active ? 'text-violet-400/70' : 'text-slate-500'}">{props.description}</p>
<h3 class="text-sm font-medium {active ? 'text-violet-200 dark:text-violet-200' : 'text-theme-primary'}">{props.title}</h3>
<p class="text-xs {active ? 'text-violet-400/70' : 'text-theme-muted'}">{props.description}</p>
</div>
</button>
{/snippet}

View File

@@ -123,10 +123,10 @@
</div>
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center gap-2">
<span class="text-xs font-medium text-amber-400">Conversation Summary</span>
<span class="text-xs text-slate-500">Earlier messages compressed</span>
<span class="text-xs font-medium text-amber-500 dark:text-amber-400">Conversation Summary</span>
<span class="text-xs text-theme-muted">Earlier messages compressed</span>
</div>
<div class="prose prose-sm prose-invert max-w-none text-slate-300">
<div class="prose prose-sm dark:prose-invert max-w-none text-theme-secondary">
<MessageContent
content={node.message.content.replace('[Previous conversation summary]\n\n', '')}
{isStreaming}
@@ -185,9 +185,9 @@
<!-- Message bubble with branch navigator -->
<div
class="relative rounded-2xl px-4 py-3 {isUser
? 'bg-slate-700 text-slate-100'
? 'bg-theme-message-user text-theme-primary'
: isToolMessage
? 'bg-slate-800/50 border-l-2 border-teal-500/50'
? 'bg-theme-secondary border-l-2 border-teal-500/50'
: 'bg-transparent'}"
>
{#if isEditing}
@@ -275,7 +275,7 @@
<!-- Avatar for user -->
{#if isUser}
<div
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-slate-600 text-slate-300"
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-xl bg-theme-tertiary text-theme-secondary"
aria-hidden="true"
>
<svg

View File

@@ -29,7 +29,7 @@
<!-- Sidenav container -->
<aside
class="fixed left-0 top-0 z-50 flex h-full flex-col overflow-hidden bg-slate-950 transition-all duration-300 ease-in-out"
class="fixed left-0 top-0 z-50 flex h-full flex-col overflow-hidden bg-theme-sidenav transition-all duration-300 ease-in-out"
class:w-[280px]={uiState.sidenavOpen}
class:w-0={!uiState.sidenavOpen}
class:shadow-xl={uiState.sidenavOpen}
@@ -48,11 +48,11 @@
</div>
<!-- Footer / Navigation links -->
<div class="border-t border-slate-700/50 p-3 space-y-1">
<div class="border-t border-theme p-3 space-y-1">
<!-- Model Browser link -->
<a
href="/models"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/models') ? 'bg-cyan-900/30 text-cyan-400' : 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'}"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/models') ? 'bg-cyan-500/20 text-cyan-600 dark:bg-cyan-900/30 dark:text-cyan-400' : 'text-theme-muted hover:bg-theme-hover hover:text-theme-primary'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -74,7 +74,7 @@
<!-- Knowledge Base link -->
<a
href="/knowledge"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/knowledge') ? 'bg-blue-900/30 text-blue-400' : 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'}"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/knowledge') ? 'bg-blue-500/20 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400' : 'text-theme-muted hover:bg-theme-hover hover:text-theme-primary'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -96,7 +96,7 @@
<!-- Tools link -->
<a
href="/tools"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/tools') ? 'bg-emerald-900/30 text-emerald-400' : 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'}"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/tools') ? 'bg-emerald-500/20 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-400' : 'text-theme-muted hover:bg-theme-hover hover:text-theme-primary'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -118,7 +118,7 @@
<!-- Prompts link -->
<a
href="/prompts"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/prompts') ? 'bg-purple-900/30 text-purple-400' : 'text-slate-400 hover:bg-slate-800 hover:text-slate-200'}"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors {isActive('/prompts') ? 'bg-purple-500/20 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400' : 'text-theme-muted hover:bg-theme-hover hover:text-theme-primary'}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -141,7 +141,7 @@
<button
type="button"
onclick={() => (settingsOpen = true)}
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm text-theme-muted transition-colors hover:bg-theme-hover hover:text-theme-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -101,7 +101,7 @@
</script>
<header
class="flex h-16 items-center border-b border-slate-800 bg-slate-950/90 backdrop-blur-sm"
class="flex h-16 items-center border-b border-theme bg-theme-topnav backdrop-blur-sm"
>
<div class="flex h-full w-full items-center justify-between px-4">
<!-- Left section: Hamburger menu + Model select -->
@@ -110,7 +110,7 @@
<button
type="button"
onclick={() => uiState.toggleSidenav()}
class="rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
class="rounded-lg p-2 text-theme-muted transition-colors hover:bg-theme-hover hover:text-theme-primary"
aria-label={uiState.sidenavOpen ? 'Close sidebar' : 'Open sidebar'}
>
{#if uiState.sidenavOpen}
@@ -159,7 +159,7 @@
<div class="hidden flex-1 items-center justify-center gap-4 sm:flex">
<!-- Conversation title -->
{#if currentConversation}
<h1 class="max-w-[300px] truncate text-sm font-medium text-slate-300" title={currentConversation.title}>
<h1 class="max-w-[300px] truncate text-sm font-medium text-theme-secondary" title={currentConversation.title}>
{currentConversation.title}
</h1>
{/if}
@@ -173,7 +173,7 @@
<button
type="button"
onclick={() => uiState.toggleDarkMode()}
class="rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
class="rounded-lg p-2 text-theme-muted transition-colors hover:bg-theme-hover hover:text-theme-primary"
aria-label={uiState.darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
title={uiState.darkMode ? 'Light mode' : 'Dark mode'}
>
@@ -217,7 +217,7 @@
<button
type="button"
onclick={handleExport}
class="rounded-lg p-2 text-slate-400 transition-colors hover:bg-slate-800 hover:text-slate-200"
class="rounded-lg p-2 text-theme-muted transition-colors hover:bg-theme-hover hover:text-theme-primary"
aria-label="Export conversation"
title="Export"
>
@@ -241,7 +241,7 @@
<button
type="button"
onclick={handlePin}
class="rounded-lg p-2 transition-colors hover:bg-slate-800 {currentConversation?.isPinned ? 'text-emerald-500 hover:text-emerald-400' : 'text-slate-400 hover:text-slate-200'}"
class="rounded-lg p-2 transition-colors hover:bg-theme-hover {currentConversation?.isPinned ? 'text-emerald-500 hover:text-emerald-400' : 'text-theme-muted hover:text-theme-primary'}"
aria-label={currentConversation?.isPinned ? 'Unpin conversation' : 'Pin conversation'}
title={currentConversation?.isPinned ? 'Unpin' : 'Pin'}
>
@@ -265,7 +265,7 @@
<button
type="button"
onclick={handleArchive}
class="rounded-lg p-2 transition-colors hover:bg-slate-800 {currentConversation?.isArchived ? 'text-amber-500 hover:text-amber-400' : 'text-slate-400 hover:text-slate-200'}"
class="rounded-lg p-2 transition-colors hover:bg-theme-hover {currentConversation?.isArchived ? 'text-amber-500 hover:text-amber-400' : 'text-theme-muted hover:text-theme-primary'}"
aria-label={currentConversation?.isArchived ? 'Unarchive conversation' : 'Archive conversation'}
title={currentConversation?.isArchived ? 'Unarchive' : 'Archive'}
>
@@ -289,7 +289,7 @@
<button
type="button"
onclick={handleDeleteClick}
class="rounded-lg p-2 text-slate-400 transition-colors hover:bg-red-900/30 hover:text-red-400"
class="rounded-lg p-2 text-theme-muted transition-colors hover:bg-red-500/20 hover:text-red-500 dark:hover:bg-red-900/30 dark:hover:text-red-400"
aria-label="Delete conversation"
title="Delete"
>

View File

@@ -144,13 +144,13 @@
}
</script>
<div class="h-screen w-full overflow-hidden bg-slate-900">
<div class="h-screen w-full overflow-hidden bg-theme-primary">
<!-- Sidenav - fixed position -->
<Sidenav />
<!-- Main content wrapper - shifts right when sidenav is open on desktop -->
<div
class="flex h-full flex-col bg-slate-900 transition-[margin-left] duration-300 ease-in-out"
class="flex h-full flex-col bg-theme-primary transition-[margin-left] duration-300 ease-in-out"
style="margin-left: {!uiState.isMobile && uiState.sidenavOpen ? SIDENAV_WIDTH : 0}px"
>
<!-- Top navigation - fixed at top of content area -->