feat: add light/dark theme toggle with system preference detection

- Implement theme store with localStorage persistence
- Add ThemeToggle component with animated icon transitions
- Update color system with semantic tokens for light/dark modes
- Add blocking script in app.html to prevent FOUC
- Apply theme-aware styling across all public and admin pages
This commit is contained in:
2026-01-06 20:31:24 -06:00
parent 0149dc1df9
commit 5c4d3b6efa
26 changed files with 336 additions and 190 deletions
+71 -13
View File
@@ -1,9 +1,23 @@
@import "tailwindcss"; @import "tailwindcss";
/* Dark variant definition */
@variant dark (&:where(.dark, .dark *));
@theme { @theme {
/* Custom colors */ /* Custom colors */
--color-zinc-850: #1d1d20; --color-zinc-850: #1d1d20;
/* Semantic color tokens - Light mode defaults */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f4f4f5;
--color-surface: #ffffff;
--color-surface-secondary: #fafafa;
--color-border: #e4e4e7;
--color-border-subtle: #f4f4f5;
--color-text-primary: #18181b;
--color-text-secondary: #52525b;
--color-text-tertiary: #71717a;
/* Custom font sizes */ /* Custom font sizes */
--font-size-10xl: 10rem; --font-size-10xl: 10rem;
@@ -29,22 +43,22 @@
--animate-fade-left: fade-left 3s ease-in-out forwards; --animate-fade-left: fade-left 3s ease-in-out forwards;
--animate-fade-right: fade-right 3s ease-in-out forwards; --animate-fade-right: fade-right 3s ease-in-out forwards;
/* Admin colors - Geist-inspired semantic scale */ /* Admin colors - Light mode defaults */
--color-admin-bg: #0a0a0b; --color-admin-bg: #f9fafb;
--color-admin-bg-secondary: #18181b; --color-admin-bg-secondary: #ffffff;
--color-admin-surface: #27272a; --color-admin-surface: #ffffff;
--color-admin-surface-hover: #3f3f46; --color-admin-surface-hover: #f3f4f6;
--color-admin-border: #27272a; --color-admin-border: #e5e7eb;
--color-admin-border-hover: #3f3f46; --color-admin-border-hover: #d1d5db;
--color-admin-text: #fafafa; --color-admin-text: #111827;
--color-admin-text-secondary: #a1a1aa; --color-admin-text-secondary: #4b5563;
--color-admin-text-muted: #71717a; --color-admin-text-muted: #6b7280;
--color-admin-accent: #6366f1; --color-admin-accent: #6366f1;
--color-admin-accent-hover: #818cf8; --color-admin-accent-hover: #818cf8;
/* Legacy aliases for backward compatibility */ /* Legacy aliases for backward compatibility */
--color-admin-panel: #18181b; --color-admin-panel: #ffffff;
--color-admin-hover: #3f3f46; --color-admin-hover: #f3f4f6;
/* Status colors */ /* Status colors */
--color-status-active: #22c55e; --color-status-active: #22c55e;
@@ -56,6 +70,34 @@
--color-status-info: #06b6d4; --color-status-info: #06b6d4;
} }
/* Dark mode overrides */
.dark {
--color-bg-primary: #000000;
--color-bg-secondary: #09090b;
--color-surface: #18181b;
--color-surface-secondary: #27272a;
--color-border: #27272a;
--color-border-subtle: #18181b;
--color-text-primary: #fafafa;
--color-text-secondary: #d4d4d8;
--color-text-tertiary: #a1a1aa;
/* Admin colors - Dark mode overrides */
--color-admin-bg: #0a0a0b;
--color-admin-bg-secondary: #18181b;
--color-admin-surface: #27272a;
--color-admin-surface-hover: #3f3f46;
--color-admin-border: #27272a;
--color-admin-border-hover: #3f3f46;
--color-admin-text: #fafafa;
--color-admin-text-secondary: #a1a1aa;
--color-admin-text-muted: #71717a;
/* Legacy aliases */
--color-admin-panel: #18181b;
--color-admin-hover: #3f3f46;
}
@keyframes fade { @keyframes fade {
0% { 0% {
opacity: 0%; opacity: 0%;
@@ -126,11 +168,27 @@
html, html,
body { body {
@apply font-inter overflow-x-hidden text-white; @apply font-inter overflow-x-hidden;
color: var(--color-text-primary);
transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out;
} }
body { body {
@apply h-full; @apply h-full;
background-color: var(--color-bg-primary);
}
/* Smooth theme transitions for all elements */
*:not(canvas):not([class*="animate-"]) {
transition-property: background-color, border-color, color, fill, stroke;
transition-duration: 0.3s;
transition-timing-function: ease-in-out;
}
/* Elements with explicit transition classes should extend, not replace */
[class*="transition-colors"],
[class*="transition-all"] {
transition-duration: 0.3s !important;
} }
/* OverlayScrollbars theme customization */ /* OverlayScrollbars theme customization */
+9
View File
@@ -3,6 +3,15 @@
<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" />
<script>
(function() {
const stored = localStorage.getItem('theme');
const isDark = stored === 'dark' || (stored !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
}
})();
</script>
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
+11 -3
View File
@@ -2,23 +2,31 @@
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import Dots from "./Dots.svelte"; import Dots from "./Dots.svelte";
import ThemeToggle from "./ThemeToggle.svelte";
let { let {
class: className = "", class: className = "",
backgroundClass = "", backgroundClass = "",
bgColor = "bg-black", bgColor = "",
showThemeToggle = true,
children, children,
}: { }: {
class?: string; class?: string;
backgroundClass?: string; backgroundClass?: string;
bgColor?: string; bgColor?: string;
showThemeToggle?: boolean;
children?: Snippet; children?: Snippet;
} = $props(); } = $props();
</script> </script>
<div class={cn("pointer-events-none fixed inset-0 -z-20", bgColor)}></div> <div class={cn("pointer-events-none fixed inset-0 -z-20 bg-white dark:bg-black transition-colors duration-300", bgColor)}></div>
<Dots class={[backgroundClass]} /> <Dots class={[backgroundClass]} />
<main class={cn("relative min-h-screen text-zinc-50", className)}> {#if showThemeToggle}
<div class="fixed top-5 right-6 z-50">
<ThemeToggle />
</div>
{/if}
<main class={cn("relative min-h-screen text-zinc-900 dark:text-zinc-50 transition-colors duration-300", className)}>
{#if children} {#if children}
{@render children()} {@render children()}
{/if} {/if}
+10 -10
View File
@@ -39,22 +39,22 @@
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class={cn( class={cn(
"group flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3 transition-all hover:border-zinc-700 hover:bg-zinc-800/70", "group flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800/70",
className, className,
)} )}
> >
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<h3 <h3
class="truncate font-medium text-lg sm:text-base text-zinc-100 transition-colors group-hover:text-white" class="truncate font-medium text-lg sm:text-base text-zinc-900 dark:text-zinc-100 transition-colors group-hover:text-zinc-950 dark:group-hover:text-white"
> >
{project.name} {project.name}
</h3> </h3>
<span class="shrink-0 sm:text-[0.83rem] text-zinc-300"> <span class="shrink-0 sm:text-[0.83rem] text-zinc-600 dark:text-zinc-300">
{formatDate(project.updatedAt)} {formatDate(project.updatedAt)}
</span> </span>
</div> </div>
<p class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-400"> <p class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{project.shortDescription} {project.shortDescription}
</p> </p>
</div> </div>
@@ -63,7 +63,7 @@
{#each project.tags as tag (tag.name)} {#each project.tags as tag (tag.name)}
<!-- TODO: Add link to project search with tag filtering --> <!-- TODO: Add link to project search with tag filtering -->
<span <span
class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-300 border-l-3" class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-200/80 dark:bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-700 dark:text-zinc-300 border-l-3"
style="border-left-color: #{tag.color || '06b6d4'}" style="border-left-color: #{tag.color || '06b6d4'}"
> >
{#if tag.iconSvg} {#if tag.iconSvg}
@@ -80,22 +80,22 @@
{:else} {:else}
<div <div
class={cn( class={cn(
"flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-800 bg-zinc-900/50 p-3", "flex h-44 flex-col gap-2.5 rounded-lg border border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50 p-3",
className, className,
)} )}
> >
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div class="flex items-start justify-between gap-2"> <div class="flex items-start justify-between gap-2">
<h3 <h3
class="truncate font-medium text-lg sm:text-base text-zinc-100" class="truncate font-medium text-lg sm:text-base text-zinc-900 dark:text-zinc-100"
> >
{project.name} {project.name}
</h3> </h3>
<span class="shrink-0 sm:text-[0.83rem] text-zinc-300"> <span class="shrink-0 sm:text-[0.83rem] text-zinc-600 dark:text-zinc-300">
{formatDate(project.updatedAt)} {formatDate(project.updatedAt)}
</span> </span>
</div> </div>
<p class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-400"> <p class="line-clamp-3 sm:text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
{project.shortDescription} {project.shortDescription}
</p> </p>
</div> </div>
@@ -103,7 +103,7 @@
<div class="mt-auto flex flex-wrap gap-1"> <div class="mt-auto flex flex-wrap gap-1">
{#each project.tags as tag (tag.name)} {#each project.tags as tag (tag.name)}
<span <span
class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-300 border-l-3" class="inline-flex items-center gap-1.25 rounded-r-sm rounded-l-xs bg-zinc-200/80 dark:bg-zinc-700/50 px-2 sm:px-1.5 py-1 sm:py-0.75 text-sm sm:text-xs text-zinc-700 dark:text-zinc-300 border-l-3"
style="border-left-color: #{tag.color || '06b6d4'}" style="border-left-color: #{tag.color || '06b6d4'}"
> >
{#if tag.iconSvg} {#if tag.iconSvg}
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import { themeStore } from "$lib/stores/theme.svelte";
import IconSun from "~icons/lucide/sun";
import IconMoon from "~icons/lucide/moon";
</script>
<button
type="button"
onclick={() => themeStore.toggle()}
aria-label={themeStore.isDark ? "Switch to light mode" : "Switch to dark mode"}
class="relative size-9 rounded-md border border-zinc-300 dark:border-zinc-700 bg-zinc-100 dark:bg-zinc-900/50 hover:bg-zinc-200 dark:hover:bg-zinc-800/70 transition-all duration-200"
>
<div class="absolute inset-0 flex items-center justify-center">
<IconSun
class="size-5 text-zinc-600 dark:text-zinc-400 transition-all duration-300 {themeStore.isDark
? 'rotate-90 scale-0 opacity-0'
: 'rotate-0 scale-100 opacity-100'}"
/>
<IconMoon
class="absolute size-5 text-zinc-600 dark:text-zinc-400 transition-all duration-300 {themeStore.isDark
? 'rotate-0 scale-100 opacity-100'
: '-rotate-90 scale-0 opacity-0'}"
/>
</div>
</button>
+3 -3
View File
@@ -28,12 +28,12 @@
const variantStyles = { const variantStyles = {
primary: primary:
"bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:ring-indigo-500 shadow-sm hover:shadow", "bg-admin-accent text-white hover:bg-admin-accent-hover focus-visible:ring-admin-accent shadow-sm hover:shadow",
secondary: secondary:
"bg-transparent text-admin-text border border-zinc-700 hover:border-zinc-600 hover:bg-zinc-800/50 focus-visible:ring-zinc-500", "bg-transparent text-admin-text border border-admin-border hover:border-admin-border-hover hover:bg-admin-surface-hover/50 focus-visible:ring-admin-accent",
danger: danger:
"bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-500 shadow-sm hover:shadow", "bg-red-600 text-white hover:bg-red-500 focus-visible:ring-red-500 shadow-sm hover:shadow",
ghost: "text-admin-text hover:bg-zinc-800/50 focus-visible:ring-zinc-500", ghost: "text-admin-text hover:bg-admin-surface-hover focus-visible:ring-admin-accent",
}; };
const sizeStyles = { const sizeStyles = {
+11 -11
View File
@@ -77,7 +77,7 @@
<div class={cn("space-y-3", className)}> <div class={cn("space-y-3", className)}>
{#if label} {#if label}
<label class="block text-sm font-medium text-zinc-300">{label}</label> <label class="block text-sm font-medium text-admin-text">{label}</label>
{/if} {/if}
<!-- Preset Palette --> <!-- Preset Palette -->
@@ -88,8 +88,8 @@
class={cn( class={cn(
"size-8 rounded border-2 transition-all hover:scale-110", "size-8 rounded border-2 transition-all hover:scale-110",
selectedColor === preset.value selectedColor === preset.value
? "border-white ring-2 ring-white/20" ? "border-admin-accent ring-2 ring-admin-accent/20"
: "border-zinc-700 hover:border-zinc-500", : "border-admin-border hover:border-admin-border-hover",
)} )}
style="background-color: #{preset.value}" style="background-color: #{preset.value}"
title={preset.name} title={preset.name}
@@ -103,13 +103,13 @@
class={cn( class={cn(
"size-8 rounded border-2 transition-all hover:scale-110 flex items-center justify-center", "size-8 rounded border-2 transition-all hover:scale-110 flex items-center justify-center",
!selectedColor !selectedColor
? "border-white ring-2 ring-white/20 bg-zinc-800" ? "border-admin-accent ring-2 ring-admin-accent/20 bg-admin-surface-hover"
: "border-zinc-700 hover:border-zinc-500 bg-zinc-900", : "border-admin-border hover:border-admin-border-hover bg-admin-surface",
)} )}
title="No color" title="No color"
onclick={clearColor} onclick={clearColor}
> >
<span class="text-zinc-500 text-xs"></span> <span class="text-admin-text-muted text-xs"></span>
</button> </button>
</div> </div>
@@ -117,7 +117,7 @@
<div class="flex items-start gap-2"> <div class="flex items-start gap-2">
<div class="flex-1"> <div class="flex-1">
<div class="relative"> <div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-500" <span class="absolute left-3 top-1/2 -translate-y-1/2 text-admin-text-muted"
>#</span >#</span
> >
<input <input
@@ -127,11 +127,11 @@
placeholder="3b82f6" placeholder="3b82f6"
maxlength="6" maxlength="6"
class={cn( class={cn(
"w-full rounded-md border bg-zinc-900 px-3 py-2 pl-7 text-sm text-zinc-100", "w-full rounded-md border bg-admin-bg-secondary px-3 py-2 pl-7 text-sm text-admin-text",
"placeholder:text-zinc-600 focus:outline-none focus:ring-2", "placeholder:text-admin-text-muted focus:outline-none focus:ring-2",
validationError validationError
? "border-red-500 focus:ring-red-500/20" ? "border-red-500 focus:ring-red-500/20"
: "border-zinc-700 focus:border-zinc-600 focus:ring-zinc-500/20", : "border-admin-border focus:border-admin-border-hover focus:ring-admin-accent/20",
)} )}
/> />
</div> </div>
@@ -143,7 +143,7 @@
<!-- Color Preview --> <!-- Color Preview -->
{#if selectedColor && validateHexColor(selectedColor)} {#if selectedColor && validateHexColor(selectedColor)}
<div <div
class="size-10 shrink-0 rounded-md border-2 border-zinc-700" class="size-10 shrink-0 rounded-md border-2 border-admin-border"
style="background-color: #{selectedColor}" style="background-color: #{selectedColor}"
title="#{selectedColor}" title="#{selectedColor}"
/> />
+10 -10
View File
@@ -39,7 +39,7 @@
defer defer
style="max-height: {maxHeight}" style="max-height: {maxHeight}"
> >
<div class="divide-y divide-zinc-800/50 bg-zinc-950"> <div class="divide-y divide-admin-border/50 bg-admin-bg">
{#each events as event (event.id)} {#each events as event (event.id)}
{@const levelColors = { {@const levelColors = {
info: "text-cyan-500/60", info: "text-cyan-500/60",
@@ -51,7 +51,7 @@
warning: "WARN", warning: "WARN",
error: "ERR", error: "ERR",
}} }}
<div class="hover:bg-zinc-900/50 transition-colors"> <div class="hover:bg-admin-surface-hover/50 transition-colors">
<div class="px-4 py-1.5"> <div class="px-4 py-1.5">
<div class="flex items-center justify-between gap-4 text-xs"> <div class="flex items-center justify-between gap-4 text-xs">
<div class="flex items-center gap-2.5 flex-1 min-w-0"> <div class="flex items-center gap-2.5 flex-1 min-w-0">
@@ -60,23 +60,23 @@
> >
{levelLabels[event.level]} {levelLabels[event.level]}
</span> </span>
<span class="text-zinc-300 truncate"> <span class="text-admin-text truncate">
{event.message} {event.message}
</span> </span>
<span class="text-zinc-500 shrink-0"> <span class="text-admin-text-muted shrink-0">
target=<span class="text-zinc-400">{event.target}</span> target=<span class="text-admin-text-secondary">{event.target}</span>
</span> </span>
</div> </div>
<div class="flex items-center gap-3 shrink-0"> <div class="flex items-center gap-3 shrink-0">
{#if showMetadata && event.metadata} {#if showMetadata && event.metadata}
<button <button
class="text-[11px] text-indigo-400 hover:text-indigo-300 transition-colors" class="text-[11px] text-admin-accent hover:text-admin-accent-hover transition-colors"
onclick={() => toggleMetadata(event.id)} onclick={() => toggleMetadata(event.id)}
> >
{expandedEventId === event.id ? "hide" : "show"} {expandedEventId === event.id ? "hide" : "show"}
</button> </button>
{/if} {/if}
<span class="text-zinc-600 text-[11px] tabular-nums"> <span class="text-admin-text-muted text-[11px] tabular-nums">
{formatTimestamp(event.timestamp)} {formatTimestamp(event.timestamp)}
</span> </span>
</div> </div>
@@ -85,10 +85,10 @@
{#if showMetadata && expandedEventId === event.id && event.metadata} {#if showMetadata && expandedEventId === event.id && event.metadata}
<div class="px-4 pb-2"> <div class="px-4 pb-2">
<div <div
class="bg-zinc-900 border border-zinc-800 rounded p-3 text-[11px]" class="bg-admin-surface border border-admin-border rounded p-3 text-[11px]"
> >
<p class="text-zinc-500 mb-2 font-medium">Metadata:</p> <p class="text-admin-text-muted mb-2 font-medium">Metadata:</p>
<pre class="text-zinc-400 overflow-x-auto">{JSON.stringify( <pre class="text-admin-text-secondary overflow-x-auto">{JSON.stringify(
event.metadata, event.metadata,
null, null,
2, 2,
+10 -10
View File
@@ -211,7 +211,7 @@
<!-- eslint-disable-next-line svelte/no-at-html-tags --> <!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html selectedIconSvg} {@html selectedIconSvg}
{:else} {:else}
<div class="size-6 animate-pulse rounded bg-zinc-700"></div> <div class="size-6 animate-pulse rounded bg-admin-surface-hover"></div>
{/if} {/if}
</div> </div>
<div class="flex-1"> <div class="flex-1">
@@ -234,8 +234,8 @@
class={cn( class={cn(
"rounded-md px-3 py-1.5 text-sm font-medium transition-colors", "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
selectedCollection === "all" selectedCollection === "all"
? "bg-indigo-600 text-white" ? "bg-admin-accent text-white"
: "bg-admin-panel text-admin-text-muted hover:bg-admin-hover hover:text-admin-text", : "bg-admin-surface text-admin-text-muted hover:bg-admin-surface-hover hover:text-admin-text",
)} )}
onclick={() => (selectedCollection = "all")} onclick={() => (selectedCollection = "all")}
> >
@@ -247,8 +247,8 @@
class={cn( class={cn(
"rounded-md px-3 py-1.5 text-sm font-medium transition-colors", "rounded-md px-3 py-1.5 text-sm font-medium transition-colors",
selectedCollection === collection.id selectedCollection === collection.id
? "bg-indigo-600 text-white" ? "bg-admin-accent text-white"
: "bg-admin-panel text-admin-text-muted hover:bg-admin-hover hover:text-admin-text", : "bg-admin-surface text-admin-text-muted hover:bg-admin-surface-hover hover:text-admin-text",
)} )}
onclick={() => (selectedCollection = collection.id)} onclick={() => (selectedCollection = collection.id)}
> >
@@ -265,7 +265,7 @@
type="text" type="text"
bind:value={searchQuery} bind:value={searchQuery}
{placeholder} {placeholder}
class="w-full rounded-md border border-admin-border bg-admin-panel px-3 py-2 text-sm text-admin-text placeholder:text-admin-text-muted focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" class="w-full rounded-md border border-admin-border bg-admin-bg-secondary px-3 py-2 text-sm text-admin-text placeholder:text-admin-text-muted focus:border-admin-accent focus:outline-none focus:ring-1 focus:ring-admin-accent"
onfocus={handleInputFocus} onfocus={handleInputFocus}
onblur={handleInputBlur} onblur={handleInputBlur}
/> />
@@ -273,7 +273,7 @@
<!-- Search results dropdown --> <!-- Search results dropdown -->
{#if showDropdown && searchResults.length > 0} {#if showDropdown && searchResults.length > 0}
<div <div
class="absolute z-10 mt-1 max-h-96 w-full overflow-auto rounded-md border border-admin-border bg-admin-panel shadow-lg" class="absolute z-10 mt-1 max-h-96 w-full overflow-auto rounded-md border border-admin-border bg-admin-surface shadow-lg"
> >
<!-- Grid layout for icons --> <!-- Grid layout for icons -->
<div class="grid grid-cols-8 gap-1 p-2"> <div class="grid grid-cols-8 gap-1 p-2">
@@ -293,14 +293,14 @@
{@html cachedSvg} {@html cachedSvg}
{:else} {:else}
<div <div
class="size-full animate-pulse rounded bg-zinc-700" class="size-full animate-pulse rounded bg-admin-surface-hover"
></div> ></div>
{/if} {/if}
</div> </div>
<!-- Tooltip on hover --> <!-- Tooltip on hover -->
<div <div
class="pointer-events-none absolute -top-8 left-1/2 z-20 hidden -translate-x-1/2 whitespace-nowrap rounded bg-zinc-900 px-2 py-1 text-xs text-white group-hover:block" class="pointer-events-none absolute -top-8 left-1/2 z-20 hidden -translate-x-1/2 whitespace-nowrap rounded bg-admin-surface border border-admin-border px-2 py-1 text-xs text-admin-text group-hover:block"
> >
{result.name} {result.name}
</div> </div>
@@ -318,7 +318,7 @@
</div> </div>
{:else if showDropdown && searchQuery && !isLoading} {:else if showDropdown && searchQuery && !isLoading}
<div <div
class="absolute z-10 mt-1 w-full rounded-md border border-admin-border bg-admin-panel p-3 text-center text-sm text-admin-text-muted shadow-lg" class="absolute z-10 mt-1 w-full rounded-md border border-admin-border bg-admin-surface p-3 text-center text-sm text-admin-text-muted shadow-lg"
> >
No icons found for "{searchQuery}" No icons found for "{searchQuery}"
</div> </div>
+1 -1
View File
@@ -42,7 +42,7 @@
const inputId = `input-${Math.random().toString(36).substring(2, 11)}`; const inputId = `input-${Math.random().toString(36).substring(2, 11)}`;
const inputStyles = const inputStyles =
"block w-full rounded-md border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-200 placeholder:text-zinc-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-50 transition-colors"; "block w-full rounded-md border border-admin-border bg-admin-bg-secondary px-3 py-2 text-sm text-admin-text placeholder:text-admin-text-muted focus:border-admin-accent focus:outline-none focus:ring-1 focus:ring-admin-accent disabled:cursor-not-allowed disabled:opacity-50 transition-colors";
const errorStyles = $derived( const errorStyles = $derived(
error ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "", error ? "border-red-500 focus:border-red-500 focus:ring-red-500" : "",
+3 -3
View File
@@ -51,18 +51,18 @@
tabindex="-1" tabindex="-1"
> >
<div <div
class="relative w-full max-w-md rounded-xl bg-zinc-900 border border-zinc-800 p-8 shadow-xl shadow-black/50" class="relative w-full max-w-md rounded-xl bg-admin-surface border border-admin-border p-8 shadow-xl shadow-black/20 dark:shadow-black/50"
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
> >
{#if title} {#if title}
<h2 class="text-lg font-semibold text-zinc-50 mb-2"> <h2 class="text-lg font-semibold text-admin-text mb-2">
{title} {title}
</h2> </h2>
{/if} {/if}
{#if description} {#if description}
<p class="text-sm text-zinc-400 mb-4"> <p class="text-sm text-admin-text-secondary mb-4">
{description} {description}
</p> </p>
{/if} {/if}
+13 -11
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/stores"; import { page } from "$app/stores";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
import ThemeToggle from "$lib/components/ThemeToggle.svelte";
import IconLayoutDashboard from "~icons/lucide/layout-dashboard"; import IconLayoutDashboard from "~icons/lucide/layout-dashboard";
import IconFolder from "~icons/lucide/folder"; import IconFolder from "~icons/lucide/folder";
import IconTags from "~icons/lucide/tags"; import IconTags from "~icons/lucide/tags";
@@ -57,7 +58,7 @@
<!-- Mobile menu button --> <!-- Mobile menu button -->
<button <button
class="fixed top-4 right-4 z-50 lg:hidden rounded-md bg-zinc-900 p-2 text-zinc-200 border border-zinc-800" class="fixed top-4 right-4 z-50 lg:hidden rounded-md bg-admin-surface p-2 text-admin-text border border-admin-border"
onclick={() => (mobileMenuOpen = !mobileMenuOpen)} onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
aria-label="Toggle menu" aria-label="Toggle menu"
> >
@@ -71,17 +72,18 @@
<!-- Sidebar --> <!-- Sidebar -->
<aside <aside
class={cn( class={cn(
"fixed left-0 top-0 z-40 h-screen w-64 border-r border-zinc-800 bg-admin-bg transition-transform lg:translate-x-0", "fixed left-0 top-0 z-40 h-screen w-64 border-r border-admin-border bg-admin-bg transition-transform lg:translate-x-0",
mobileMenuOpen ? "translate-x-0" : "-translate-x-full", mobileMenuOpen ? "translate-x-0" : "-translate-x-full",
)} )}
> >
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<!-- Logo --> <!-- Logo -->
<div class="border-b border-zinc-800 px-4 py-5"> <div class="border-b border-admin-border px-4 py-5 flex items-center justify-between">
<h1 class="text-base font-semibold text-zinc-50"> <h1 class="text-base font-semibold text-admin-text">
xevion.dev xevion.dev
<span class="text-xs font-normal text-zinc-500 ml-1.5">Admin</span> <span class="text-xs font-normal text-admin-text-muted ml-1.5">Admin</span>
</h1> </h1>
<ThemeToggle />
</div> </div>
<!-- Navigation --> <!-- Navigation -->
@@ -92,14 +94,14 @@
class={cn( class={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-all relative", "flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-all relative",
isActive(item.href) isActive(item.href)
? "bg-zinc-800/50 text-zinc-50 before:absolute before:left-0 before:top-1 before:bottom-1 before:w-0.5 before:bg-indigo-500 before:rounded-r" ? "bg-admin-surface-hover text-admin-text before:absolute before:left-0 before:top-1 before:bottom-1 before:w-0.5 before:bg-admin-accent before:rounded-r"
: "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800/30", : "text-admin-text-muted hover:text-admin-text hover:bg-admin-surface-hover/50",
)} )}
> >
<item.icon class="w-4 h-4 flex-shrink-0" /> <item.icon class="w-4 h-4 flex-shrink-0" />
<span class="flex-1">{item.label}</span> <span class="flex-1">{item.label}</span>
{#if item.badge} {#if item.badge}
<span class="text-xs text-zinc-500"> <span class="text-xs text-admin-text-muted">
{item.badge} {item.badge}
</span> </span>
{/if} {/if}
@@ -108,17 +110,17 @@
</nav> </nav>
<!-- Bottom actions --> <!-- Bottom actions -->
<div class="space-y-0.5 border-t border-zinc-800 bg-zinc-900/50 p-3"> <div class="space-y-0.5 border-t border-admin-border bg-admin-surface/50 p-3">
<a <a
href="/" href="/"
class="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-zinc-400 transition-all hover:text-zinc-200 hover:bg-zinc-800/30" class="flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-admin-text-muted transition-all hover:text-admin-text hover:bg-admin-surface-hover/50"
> >
<IconArrowLeft class="w-4 h-4" /> <IconArrowLeft class="w-4 h-4" />
<span>Back to Site</span> <span>Back to Site</span>
</a> </a>
<button <button
onclick={handleLogout} onclick={handleLogout}
class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-zinc-400 transition-all hover:text-zinc-200 hover:bg-zinc-800/30" class="flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm font-medium text-admin-text-muted transition-all hover:text-admin-text hover:bg-admin-surface-hover/50"
> >
<IconLogOut class="w-4 h-4" /> <IconLogOut class="w-4 h-4" />
<span>Logout</span> <span>Logout</span>
@@ -70,7 +70,7 @@
<div class="relative"> <div class="relative">
<!-- Selected tags display --> <!-- Selected tags display -->
<div <div
class="min-h-[42px] w-full rounded-md border border-admin-border bg-admin-panel px-3 py-2" class="min-h-[42px] w-full rounded-md border border-admin-border bg-admin-bg-secondary px-3 py-2"
> >
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each selectedTags as tag (tag.id)} {#each selectedTags as tag (tag.id)}
@@ -106,12 +106,12 @@
<!-- Dropdown --> <!-- Dropdown -->
{#if dropdownOpen && filteredTags.length > 0} {#if dropdownOpen && filteredTags.length > 0}
<div <div
class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-admin-border bg-admin-panel py-1 shadow-lg" class="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md border border-admin-border bg-admin-surface py-1 shadow-lg"
> >
{#each filteredTags as tag (tag.id)} {#each filteredTags as tag (tag.id)}
<button <button
type="button" type="button"
class="w-full px-3 py-2 text-left text-sm text-admin-text hover:bg-admin-hover transition-colors" class="w-full px-3 py-2 text-left text-sm text-admin-text hover:bg-admin-surface-hover transition-colors"
onclick={() => addTag(tag.id)} onclick={() => addTag(tag.id)}
> >
{tag.name} {tag.name}
+42
View File
@@ -0,0 +1,42 @@
class ThemeStore {
isDark = $state<boolean>(true);
private initialized = false;
init() {
if (this.initialized || typeof window === "undefined") return;
this.initialized = true;
const stored = localStorage.getItem("theme");
if (stored === "light" || stored === "dark") {
this.isDark = stored === "dark";
} else {
this.isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
}
this.updateDOMClass();
}
toggle() {
this.isDark = !this.isDark;
localStorage.setItem("theme", this.isDark ? "dark" : "light");
this.updateDOMClass();
}
setTheme(isDark: boolean) {
this.isDark = isDark;
localStorage.setItem("theme", isDark ? "dark" : "light");
this.updateDOMClass();
}
private updateDOMClass() {
if (typeof document === "undefined") return;
if (this.isDark) {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
}
}
export const themeStore = new ThemeStore();
+3 -3
View File
@@ -24,12 +24,12 @@
<AppWrapper> <AppWrapper>
<div class="flex min-h-screen items-center justify-center"> <div class="flex min-h-screen items-center justify-center">
<div class="mx-4 max-w-2xl text-center"> <div class="mx-4 max-w-2xl text-center">
<h1 class="mb-4 font-hanken text-8xl text-zinc-200">{status}</h1> <h1 class="mb-4 font-hanken text-8xl text-text-secondary">{status}</h1>
<p class="mb-8 text-2xl text-zinc-400">{message}</p> <p class="mb-8 text-2xl text-text-tertiary">{message}</p>
{#if showHomeLink} {#if showHomeLink}
<a <a
href={resolve("/")} href={resolve("/")}
class="inline-block rounded-sm bg-zinc-900 px-4 py-2 text-zinc-100 transition-colors hover:bg-zinc-800" class="inline-block rounded-sm bg-surface px-4 py-2 text-text-primary transition-colors hover:bg-surface-hover"
> >
Return home Return home
</a> </a>
+4
View File
@@ -6,6 +6,7 @@
import "../app.css"; import "../app.css";
import { OverlayScrollbars } from "overlayscrollbars"; import { OverlayScrollbars } from "overlayscrollbars";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { themeStore } from "$lib/stores/theme.svelte";
let { children, data } = $props(); let { children, data } = $props();
@@ -20,6 +21,9 @@
const metadata = $derived(data?.metadata ?? defaultMetadata); const metadata = $derived(data?.metadata ?? defaultMetadata);
onMount(() => { onMount(() => {
// Initialize theme store
themeStore.init();
// Initialize overlay scrollbars on the body element // Initialize overlay scrollbars on the body element
const osInstance = OverlayScrollbars(document.body, { const osInstance = OverlayScrollbars(document.body, {
scrollbars: { scrollbars: {
+21 -23
View File
@@ -13,22 +13,20 @@
</script> </script>
<AppWrapper class="overflow-x-hidden font-schibsted"> <AppWrapper class="overflow-x-hidden font-schibsted">
<div class="flex w-full justify-end items-center pt-5 px-6 pb-9"></div> <div class="flex items-center flex-col pt-14">
<div class="flex items-center flex-col">
<div <div
class="max-w-2xl mx-4 border-b border-zinc-700 divide-y divide-zinc-700 sm:mx-6" class="max-w-2xl mx-4 border-b border-zinc-200 dark:border-zinc-700 divide-y divide-zinc-200 dark:divide-zinc-700 sm:mx-6"
> >
<div class="flex flex-col pb-4"> <div class="flex flex-col pb-4">
<span class="text-2xl font-bold text-white sm:text-3xl" <span class="text-2xl font-bold text-zinc-900 dark:text-white sm:text-3xl"
>Ryan Walters,</span >Ryan Walters,</span
> >
<span class="text-xl font-normal text-zinc-400 sm:text-2xl"> <span class="text-xl font-normal text-zinc-600 dark:text-zinc-400 sm:text-2xl">
Full-Stack Software Engineer Full-Stack Software Engineer
</span> </span>
</div> </div>
<div class="py-4 text-zinc-200"> <div class="py-4 text-zinc-700 dark:text-zinc-200">
<p class="sm:text-[0.95em]"> <p class="sm:text-[0.95em]">
A fanatical software engineer with expertise and passion for sound, A fanatical software engineer with expertise and passion for sound,
scalable and high-performance applications. I'm always working on scalable and high-performance applications. I'm always working on
@@ -38,43 +36,43 @@
</div> </div>
<div class="py-3"> <div class="py-3">
<span class="text-zinc-200">Connect with me</span> <span class="text-zinc-700 dark:text-zinc-200">Connect with me</span>
<div class="flex flex-wrap gap-2 pl-3 pt-3 pb-2"> <div class="flex flex-wrap gap-2 pl-3 pt-3 pb-2">
<a <a
href="https://github.com/Xevion" href="https://github.com/Xevion"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors" class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
> >
<IconSimpleIconsGithub class="size-4 text-zinc-300" /> <IconSimpleIconsGithub class="size-4 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-100">GitHub</span> <span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">GitHub</span>
</a> </a>
<a <a
href="https://linkedin.com/in/ryancwalters" href="https://linkedin.com/in/ryancwalters"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors" class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
> >
<IconSimpleIconsLinkedin class="size-4 text-zinc-300" /> <IconSimpleIconsLinkedin class="size-4 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-100">LinkedIn</span <span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">LinkedIn</span
> >
</a> </a>
<button <button
type="button" type="button"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors" class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
> >
<IconSimpleIconsDiscord class="size-4 text-zinc-300" /> <IconSimpleIconsDiscord class="size-4 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-100">Discord</span> <span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">Discord</span>
</button> </button>
<a <a
href="mailto:your.email@example.com" href="mailto:your.email@example.com"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors" class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
> >
<MaterialSymbolsMailRounded class="size-4.5 text-zinc-300" /> <MaterialSymbolsMailRounded class="size-4.5 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-100">Email</span> <span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">Email</span>
</a> </a>
<button <button
type="button" type="button"
class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-900 shadow-sm hover:bg-zinc-800 transition-colors" class="flex items-center gap-x-1.5 px-1.5 py-1 rounded-sm bg-zinc-100 dark:bg-zinc-900 shadow-sm hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors"
> >
<MaterialSymbolsVpnKey class="size-4.5 text-zinc-300" /> <MaterialSymbolsVpnKey class="size-4.5 text-zinc-600 dark:text-zinc-300" />
<span class="whitespace-nowrap text-sm text-zinc-100">PGP Key</span> <span class="whitespace-nowrap text-sm text-zinc-800 dark:text-zinc-100">PGP Key</span>
</button> </button>
</div> </div>
</div> </div>
+1 -1
View File
@@ -55,7 +55,7 @@
{@render children()} {@render children()}
{:else} {:else}
<!-- Admin layout with sidebar and dots shader --> <!-- Admin layout with sidebar and dots shader -->
<AppWrapper bgColor="bg-admin-bg"> <AppWrapper bgColor="bg-admin-bg" showThemeToggle={false}>
<Sidebar <Sidebar
projectCount={stats?.totalProjects ?? 0} projectCount={stats?.totalProjects ?? 0}
tagCount={stats?.totalTags ?? 0} tagCount={stats?.totalTags ?? 0}
+7 -7
View File
@@ -32,8 +32,8 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div> <div>
<h1 class="text-xl font-semibold text-zinc-50">Dashboard</h1> <h1 class="text-xl font-semibold text-admin-text">Dashboard</h1>
<p class="mt-1 text-sm text-zinc-500"> <p class="mt-1 text-sm text-admin-text-muted">
Overview of your portfolio and recent activity Overview of your portfolio and recent activity
</p> </p>
</div> </div>
@@ -58,22 +58,22 @@
<!-- Recent Events --> <!-- Recent Events -->
<div <div
class="rounded-xl border border-zinc-800 bg-zinc-900/50 overflow-hidden shadow-sm shadow-black/20" class="rounded-xl border border-admin-border bg-admin-surface/50 overflow-hidden shadow-sm shadow-black/10 dark:shadow-black/20"
> >
<div <div
class="flex items-center justify-between px-6 py-3.5 bg-zinc-800/30 border-b border-zinc-800" class="flex items-center justify-between px-6 py-3.5 bg-admin-surface-hover/30 border-b border-admin-border"
> >
<h2 class="text-sm font-medium text-zinc-300">Recent Events</h2> <h2 class="text-sm font-medium text-admin-text-secondary">Recent Events</h2>
<a <a
href={resolve("/admin/events")} href={resolve("/admin/events")}
class="text-sm text-indigo-400 hover:text-indigo-300 transition-colors" class="text-sm text-admin-accent hover:text-admin-accent-hover transition-colors"
> >
View all → View all →
</a> </a>
</div> </div>
{#if recentEvents.length === 0} {#if recentEvents.length === 0}
<p class="text-sm text-zinc-500 text-center py-8">No events yet</p> <p class="text-sm text-admin-text-muted text-center py-8">No events yet</p>
{:else} {:else}
<EventLog events={recentEvents} maxHeight="400px" /> <EventLog events={recentEvents} maxHeight="400px" />
{/if} {/if}
+10 -10
View File
@@ -46,17 +46,17 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div> <div>
<h1 class="text-xl font-semibold text-zinc-50">Event Log</h1> <h1 class="text-xl font-semibold text-admin-text">Event Log</h1>
<p class="mt-1 text-sm text-zinc-500"> <p class="mt-1 text-sm text-admin-text-muted">
System activity, errors, and sync operations System activity, errors, and sync operations
</p> </p>
</div> </div>
<!-- Filters --> <!-- Filters -->
<div <div
class="rounded-xl border border-zinc-800 bg-zinc-900 p-6 shadow-sm shadow-black/20" class="rounded-xl border border-admin-border bg-admin-surface p-6 shadow-sm shadow-black/10 dark:shadow-black/20"
> >
<h3 class="text-sm font-medium text-zinc-400 mb-4">Filters</h3> <h3 class="text-sm font-medium text-admin-text-secondary mb-4">Filters</h3>
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<Input <Input
label="Level" label="Level"
@@ -75,19 +75,19 @@
<!-- Events Log --> <!-- Events Log -->
{#if loading} {#if loading}
<div class="text-center py-12 text-zinc-500">Loading events...</div> <div class="text-center py-12 text-admin-text-muted">Loading events...</div>
{:else if events.length === 0} {:else if events.length === 0}
<div class="text-center py-12"> <div class="text-center py-12">
<p class="text-zinc-500">No events found</p> <p class="text-admin-text-muted">No events found</p>
</div> </div>
{:else} {:else}
<div <div
class="rounded-xl border border-zinc-800 bg-zinc-900/50 overflow-hidden shadow-sm shadow-black/20" class="rounded-xl border border-admin-border bg-admin-surface/50 overflow-hidden shadow-sm shadow-black/10 dark:shadow-black/20"
> >
<div class="px-6 py-3.5 bg-zinc-800/30 border-b border-zinc-800"> <div class="px-6 py-3.5 bg-admin-surface-hover/30 border-b border-admin-border">
<h2 class="text-sm font-medium text-zinc-300"> <h2 class="text-sm font-medium text-admin-text-secondary">
Event Log Event Log
<span class="text-zinc-500 font-normal ml-2"> <span class="text-admin-text-muted font-normal ml-2">
({events.length} event{events.length === 1 ? "" : "s"}) ({events.length} event{events.length === 1 ? "" : "s"})
</span> </span>
</h2> </h2>
+1 -1
View File
@@ -43,7 +43,7 @@
<div class="flex min-h-screen items-center justify-center px-4"> <div class="flex min-h-screen items-center justify-center px-4">
<div class="w-full max-w-md space-y-4"> <div class="w-full max-w-md space-y-4">
<!-- Login Form --> <!-- Login Form -->
<div class="rounded-lg bg-admin-panel p-8 shadow-2xl shadow-zinc-500/20"> <div class="rounded-lg bg-admin-surface border border-admin-border p-8 shadow-2xl shadow-black/10 dark:shadow-zinc-500/20">
<form onsubmit={handleSubmit} class="space-y-6"> <form onsubmit={handleSubmit} class="space-y-6">
<Input <Input
label="Username" label="Username"
+18 -18
View File
@@ -83,8 +83,8 @@
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="text-xl font-semibold text-zinc-50">Projects</h1> <h1 class="text-xl font-semibold text-admin-text">Projects</h1>
<p class="mt-1 text-sm text-zinc-500">Manage your project portfolio</p> <p class="mt-1 text-sm text-admin-text-muted">Manage your project portfolio</p>
</div> </div>
<Button variant="primary" href="/admin/projects/new"> <Button variant="primary" href="/admin/projects/new">
<IconPlus class="w-4 h-4 mr-2" /> <IconPlus class="w-4 h-4 mr-2" />
@@ -94,45 +94,45 @@
<!-- Projects Table --> <!-- Projects Table -->
{#if loading} {#if loading}
<div class="text-center py-12 text-zinc-500">Loading projects...</div> <div class="text-center py-12 text-admin-text-muted">Loading projects...</div>
{:else if projects.length === 0} {:else if projects.length === 0}
<div class="text-center py-12"> <div class="text-center py-12">
<p class="text-zinc-500 mb-4">No projects yet</p> <p class="text-admin-text-muted mb-4">No projects yet</p>
<Button variant="primary" href="/admin/projects/new" <Button variant="primary" href="/admin/projects/new"
>Create your first project</Button >Create your first project</Button
> >
</div> </div>
{:else} {:else}
<Table> <Table>
<thead class="bg-zinc-900/50"> <thead class="bg-admin-surface/50">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Name Name
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Status Status
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Tags Tags
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Updated Updated
</th> </th>
<th class="px-4 py-3 text-right text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-right text-xs font-medium text-admin-text-muted">
Actions Actions
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-zinc-800/50"> <tbody class="divide-y divide-admin-border/50">
{#each projects as project (project.id)} {#each projects as project (project.id)}
<tr class="hover:bg-zinc-800/30 transition-colors"> <tr class="hover:bg-admin-surface-hover/30 transition-colors">
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div> <div>
<div class="font-medium text-zinc-200"> <div class="font-medium text-admin-text">
{project.name} {project.name}
</div> </div>
<div class="text-xs text-zinc-500"> <div class="text-xs text-admin-text-muted">
{project.slug} {project.slug}
</div> </div>
</div> </div>
@@ -153,7 +153,7 @@
{/if} {/if}
</div> </div>
</td> </td>
<td class="px-4 py-3 text-zinc-500 text-sm"> <td class="px-4 py-3 text-admin-text-secondary text-sm">
{formatDate(project.updatedAt)} {formatDate(project.updatedAt)}
</td> </td>
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
@@ -192,9 +192,9 @@
oncancel={cancelDelete} oncancel={cancelDelete}
> >
{#if deleteTarget} {#if deleteTarget}
<div class="rounded-md bg-zinc-800/50 border border-zinc-700 p-3"> <div class="rounded-md bg-admin-surface-hover/50 border border-admin-border p-3">
<p class="font-medium text-zinc-200">{deleteTarget.name}</p> <p class="font-medium text-admin-text">{deleteTarget.name}</p>
<p class="text-sm text-zinc-500">{deleteTarget.slug}</p> <p class="text-sm text-admin-text-secondary">{deleteTarget.slug}</p>
</div> </div>
{/if} {/if}
</Modal> </Modal>
@@ -78,13 +78,13 @@
<p class="text-admin-text-muted mb-4">Project not found</p> <p class="text-admin-text-muted mb-4">Project not found</p>
<a <a
href={resolve("/admin/projects")} href={resolve("/admin/projects")}
class="text-blue-400 hover:text-blue-300" class="text-admin-accent hover:text-admin-accent-hover"
> >
← Back to projects ← Back to projects
</a> </a>
</div> </div>
{:else} {:else}
<div class="rounded-lg border border-admin-border bg-admin-panel p-6"> <div class="rounded-lg border border-admin-border bg-admin-surface p-6">
<ProjectForm <ProjectForm
{project} {project}
availableTags={tags} availableTags={tags}
@@ -57,7 +57,7 @@
{#if loading} {#if loading}
<div class="text-center py-12 text-admin-text-muted">Loading...</div> <div class="text-center py-12 text-admin-text-muted">Loading...</div>
{:else} {:else}
<div class="rounded-lg border border-admin-border bg-admin-panel p-6"> <div class="rounded-lg border border-admin-border bg-admin-surface p-6">
<ProjectForm <ProjectForm
availableTags={tags} availableTags={tags}
onsubmit={handleSubmit} onsubmit={handleSubmit}
@@ -111,25 +111,25 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Header --> <!-- Header -->
<div> <div>
<h1 class="text-xl font-semibold text-zinc-50">Settings</h1> <h1 class="text-xl font-semibold text-admin-text">Settings</h1>
<p class="mt-1 text-sm text-zinc-500"> <p class="mt-1 text-sm text-admin-text-muted">
Configure your site identity, social links, and admin preferences Configure your site identity, social links, and admin preferences
</p> </p>
</div> </div>
{#if loading} {#if loading}
<div class="text-center py-12 text-zinc-500">Loading settings...</div> <div class="text-center py-12 text-admin-text-muted">Loading settings...</div>
{:else if formData} {:else if formData}
<!-- Tabs --> <!-- Tabs -->
<div class="border-b border-zinc-800"> <div class="border-b border-admin-border">
<nav class="flex gap-6" aria-label="Settings tabs"> <nav class="flex gap-6" aria-label="Settings tabs">
<button <button
type="button" type="button"
class={cn( class={cn(
"pb-3 px-1 text-sm font-medium border-b-2 transition-colors", "pb-3 px-1 text-sm font-medium border-b-2 transition-colors",
activeTab === "identity" activeTab === "identity"
? "border-indigo-500 text-zinc-50" ? "border-admin-accent text-admin-text"
: "border-transparent text-zinc-400 hover:text-zinc-300 hover:border-zinc-700", : "border-transparent text-admin-text-muted hover:text-admin-text hover:border-admin-border-hover",
)} )}
onclick={() => navigateToTab("identity")} onclick={() => navigateToTab("identity")}
> >
@@ -140,8 +140,8 @@
class={cn( class={cn(
"pb-3 px-1 text-sm font-medium border-b-2 transition-colors", "pb-3 px-1 text-sm font-medium border-b-2 transition-colors",
activeTab === "social" activeTab === "social"
? "border-indigo-500 text-zinc-50" ? "border-admin-accent text-admin-text"
: "border-transparent text-zinc-400 hover:text-zinc-300 hover:border-zinc-700", : "border-transparent text-admin-text-muted hover:text-admin-text hover:border-admin-border-hover",
)} )}
onclick={() => navigateToTab("social")} onclick={() => navigateToTab("social")}
> >
@@ -152,8 +152,8 @@
class={cn( class={cn(
"pb-3 px-1 text-sm font-medium border-b-2 transition-colors", "pb-3 px-1 text-sm font-medium border-b-2 transition-colors",
activeTab === "admin" activeTab === "admin"
? "border-indigo-500 text-zinc-50" ? "border-admin-accent text-admin-text"
: "border-transparent text-zinc-400 hover:text-zinc-300 hover:border-zinc-700", : "border-transparent text-admin-text-muted hover:text-admin-text hover:border-admin-border-hover",
)} )}
onclick={() => navigateToTab("admin")} onclick={() => navigateToTab("admin")}
> >
@@ -164,11 +164,11 @@
<!-- Tab Content --> <!-- Tab Content -->
<div <div
class="rounded-xl border border-zinc-800 bg-zinc-900 p-6 shadow-sm shadow-black/20" class="rounded-xl border border-admin-border bg-admin-surface p-6 shadow-sm shadow-black/10 dark:shadow-black/20"
> >
{#if activeTab === "identity"} {#if activeTab === "identity"}
<div class="space-y-4"> <div class="space-y-4">
<h3 class="text-base font-medium text-zinc-200 mb-4"> <h3 class="text-base font-medium text-admin-text mb-4">
Site Identity Site Identity
</h3> </h3>
<Input <Input
@@ -204,8 +204,8 @@
</div> </div>
{:else if activeTab === "social"} {:else if activeTab === "social"}
<div class="space-y-4"> <div class="space-y-4">
<h3 class="text-base font-medium text-zinc-200 mb-4">Social Links</h3> <h3 class="text-base font-medium text-admin-text mb-4">Social Links</h3>
<p class="text-sm text-zinc-500 mb-4"> <p class="text-sm text-admin-text-muted mb-4">
Configure your social media presence on the index page Configure your social media presence on the index page
</p> </p>
@@ -213,23 +213,23 @@
{#each formData.socialLinks as link (link.id)} {#each formData.socialLinks as link (link.id)}
{@const Icon = getSocialIcon(link.platform)} {@const Icon = getSocialIcon(link.platform)}
<div <div
class="rounded-lg border border-zinc-800 bg-zinc-900/50 p-4 hover:border-zinc-700 transition-colors" class="rounded-lg border border-admin-border bg-admin-surface-hover/50 p-4 hover:border-admin-border-hover transition-colors"
> >
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<div class="mt-2"> <div class="mt-2">
<Icon class="w-5 h-5 text-zinc-400" /> <Icon class="w-5 h-5 text-admin-text-muted" />
</div> </div>
<div class="flex-1 space-y-3"> <div class="flex-1 space-y-3">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-sm font-medium text-zinc-200" <span class="text-sm font-medium text-admin-text"
>{link.label}</span >{link.label}</span
> >
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<span class="text-xs text-zinc-500">Visible</span> <span class="text-xs text-admin-text-muted">Visible</span>
<input <input
type="checkbox" type="checkbox"
bind:checked={link.visible} bind:checked={link.visible}
class="w-4 h-4 rounded border-zinc-700 bg-zinc-800 text-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-0 cursor-pointer" class="w-4 h-4 rounded border-admin-border bg-admin-bg-secondary text-admin-accent focus:ring-2 focus:ring-admin-accent focus:ring-offset-0 cursor-pointer"
/> />
</label> </label>
</div> </div>
@@ -246,7 +246,7 @@
</div> </div>
{:else if activeTab === "admin"} {:else if activeTab === "admin"}
<div class="space-y-4"> <div class="space-y-4">
<h3 class="text-base font-medium text-zinc-200 mb-4"> <h3 class="text-base font-medium text-admin-text mb-4">
Admin Preferences Admin Preferences
</h3> </h3>
<Input <Input
+26 -26
View File
@@ -160,8 +160,8 @@
<!-- Header --> <!-- Header -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<h1 class="text-xl font-semibold text-zinc-50">Tags</h1> <h1 class="text-xl font-semibold text-admin-text">Tags</h1>
<p class="mt-1 text-sm text-zinc-500"> <p class="mt-1 text-sm text-admin-text-muted">
Manage project tags and categories Manage project tags and categories
</p> </p>
</div> </div>
@@ -181,9 +181,9 @@
<!-- Create Form --> <!-- Create Form -->
{#if showCreateForm} {#if showCreateForm}
<div <div
class="rounded-xl border border-zinc-800 bg-zinc-900 p-6 shadow-sm shadow-black/20" class="rounded-xl border border-admin-border bg-admin-surface p-6 shadow-sm shadow-black/10 dark:shadow-black/20"
> >
<h3 class="text-base font-medium text-zinc-200 mb-4">Create New Tag</h3> <h3 class="text-base font-medium text-admin-text mb-4">Create New Tag</h3>
<div class="grid gap-4 md:grid-cols-2"> <div class="grid gap-4 md:grid-cols-2">
<Input <Input
label="Name" label="Name"
@@ -219,38 +219,38 @@
<!-- Tags Table --> <!-- Tags Table -->
{#if loading} {#if loading}
<div class="text-center py-12 text-zinc-500">Loading tags...</div> <div class="text-center py-12 text-admin-text-muted">Loading tags...</div>
{:else if tags.length === 0} {:else if tags.length === 0}
<div class="text-center py-12"> <div class="text-center py-12">
<p class="text-zinc-500 mb-4">No tags yet</p> <p class="text-admin-text-muted mb-4">No tags yet</p>
<Button variant="primary" onclick={() => (showCreateForm = true)}> <Button variant="primary" onclick={() => (showCreateForm = true)}>
Create your first tag Create your first tag
</Button> </Button>
</div> </div>
{:else} {:else}
<Table> <Table>
<thead class="bg-zinc-900/50"> <thead class="bg-admin-surface/50">
<tr> <tr>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Name Name
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Slug Slug
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Color Color
</th> </th>
<th class="px-4 py-3 text-left text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-left text-xs font-medium text-admin-text-muted">
Projects Projects
</th> </th>
<th class="px-4 py-3 text-right text-xs font-medium text-zinc-500"> <th class="px-4 py-3 text-right text-xs font-medium text-admin-text-muted">
Actions Actions
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-zinc-800/50"> <tbody class="divide-y divide-admin-border/50">
{#each tags as tag (tag.id)} {#each tags as tag (tag.id)}
<tr class="hover:bg-zinc-800/30 transition-colors"> <tr class="hover:bg-admin-surface-hover/30 transition-colors">
{#if editingId === tag.id} {#if editingId === tag.id}
<!-- Edit mode --> <!-- Edit mode -->
<td class="px-4 py-3"> <td class="px-4 py-3">
@@ -271,13 +271,13 @@
{#if editColor} {#if editColor}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="size-6 rounded border border-zinc-700" class="size-6 rounded border border-admin-border"
style="background-color: #{editColor}" style="background-color: #{editColor}"
/> />
<span class="text-xs text-zinc-500">#{editColor}</span> <span class="text-xs text-admin-text-muted">#{editColor}</span>
</div> </div>
{:else} {:else}
<span class="text-xs text-zinc-500">No color</span> <span class="text-xs text-admin-text-muted">No color</span>
{/if} {/if}
</td> </td>
<td class="px-4 py-3 text-admin-text"> <td class="px-4 py-3 text-admin-text">
@@ -305,26 +305,26 @@
</td> </td>
{:else} {:else}
<!-- View mode --> <!-- View mode -->
<td class="px-4 py-3 font-medium text-zinc-200"> <td class="px-4 py-3 font-medium text-admin-text">
{tag.name} {tag.name}
</td> </td>
<td class="px-4 py-3 text-zinc-500"> <td class="px-4 py-3 text-admin-text-secondary">
{tag.slug} {tag.slug}
</td> </td>
<td class="px-4 py-3"> <td class="px-4 py-3">
{#if tag.color} {#if tag.color}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div <div
class="size-6 rounded border border-zinc-700" class="size-6 rounded border border-admin-border"
style="background-color: #{tag.color}" style="background-color: #{tag.color}"
/> />
<span class="text-xs text-zinc-500">#{tag.color}</span> <span class="text-xs text-admin-text-muted">#{tag.color}</span>
</div> </div>
{:else} {:else}
<span class="text-xs text-zinc-500">No color</span> <span class="text-xs text-admin-text-muted">No color</span>
{/if} {/if}
</td> </td>
<td class="px-4 py-3 text-zinc-300"> <td class="px-4 py-3 text-admin-text">
{tag.projectCount} {tag.projectCount}
</td> </td>
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
@@ -364,9 +364,9 @@
oncancel={cancelDelete} oncancel={cancelDelete}
> >
{#if deleteTarget} {#if deleteTarget}
<div class="rounded-md bg-zinc-800/50 border border-zinc-700 p-3"> <div class="rounded-md bg-admin-surface-hover/50 border border-admin-border p-3">
<p class="font-medium text-zinc-200">{deleteTarget.name}</p> <p class="font-medium text-admin-text">{deleteTarget.name}</p>
<p class="text-sm text-zinc-500"> <p class="text-sm text-admin-text-secondary">
Used in {deleteTarget.projectCount} project{deleteTarget.projectCount === Used in {deleteTarget.projectCount} project{deleteTarget.projectCount ===
1 1
? "" ? ""