diff --git a/frontend/src/app.css b/frontend/src/app.css index 7668dd3..55d72f7 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -1,80 +1,105 @@ @import 'tailwindcss'; @theme { - --color-background: #fafafa; - --color-foreground: #18181b; - --color-muted: #f4f4f5; - --color-muted-foreground: #71717a; - --color-border: #e4e4e7; - --color-primary: #18181b; - --color-primary-foreground: #fafafa; - --color-accent: #f4f4f5; - --color-accent-foreground: #18181b; + --color-background: #f8f9fb; + --color-foreground: #1a1a2e; + --color-muted: #eef0f4; + --color-muted-foreground: #6b7280; + --color-border: #e2e4ea; + --color-primary: #0d9488; + --color-primary-foreground: #ffffff; + --color-accent: #eef0f4; + --color-accent-foreground: #1a1a2e; --color-destructive: #ef4444; --color-card: #ffffff; - --color-card-foreground: #18181b; - --color-success-bg: #f0fdf4; - --color-success-fg: #15803d; - --color-warning-bg: #fefce8; - --color-warning-fg: #a16207; + --color-card-foreground: #1a1a2e; + --color-success-bg: #ecfdf5; + --color-success-fg: #059669; + --color-warning-bg: #fffbeb; + --color-warning-fg: #d97706; --color-error-bg: #fef2f2; --color-error-fg: #dc2626; - --font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif; - --radius: 0.5rem; + --color-glow: rgba(13, 148, 136, 0.15); + --color-glow-strong: rgba(13, 148, 136, 0.3); + --color-sidebar: #ffffff; + --color-sidebar-active: rgba(13, 148, 136, 0.08); + --font-sans: 'DM Sans', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + --radius: 0.625rem; } /* Dark theme overrides */ [data-theme="dark"] { - --color-background: #09090b; - --color-foreground: #fafafa; - --color-muted: #27272a; - --color-muted-foreground: #a1a1aa; - --color-border: #3f3f46; - --color-primary: #3f3f46; - --color-primary-foreground: #fafafa; - --color-accent: #27272a; - --color-accent-foreground: #fafafa; + --color-background: #0c0e14; + --color-foreground: #e4e6ed; + --color-muted: #1a1d28; + --color-muted-foreground: #8b8fa4; + --color-border: #252836; + --color-primary: #14b8a6; + --color-primary-foreground: #0c0e14; + --color-accent: #1a1d28; + --color-accent-foreground: #e4e6ed; --color-destructive: #f87171; - --color-card: #18181b; - --color-card-foreground: #fafafa; + --color-card: #13151e; + --color-card-foreground: #e4e6ed; --color-success-bg: #052e16; - --color-success-fg: #4ade80; + --color-success-fg: #34d399; --color-warning-bg: #422006; - --color-warning-fg: #facc15; + --color-warning-fg: #fbbf24; --color-error-bg: #450a0a; --color-error-fg: #f87171; + --color-glow: rgba(20, 184, 166, 0.12); + --color-glow-strong: rgba(20, 184, 166, 0.25); + --color-sidebar: #10121a; + --color-sidebar-active: rgba(20, 184, 166, 0.1); } body { font-family: var(--font-sans); background-color: var(--color-background); color: var(--color-foreground); - transition: background-color 0.2s, color 0.2s; + transition: background-color 0.3s ease, color 0.3s ease; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -/* Ensure all form controls respect the theme */ +/* Subtle background pattern */ +body::before { + content: ''; + position: fixed; + inset: 0; + z-index: -1; + opacity: 0.4; + background-image: radial-gradient(circle at 1px 1px, var(--color-border) 0.5px, transparent 0); + background-size: 32px 32px; + pointer-events: none; +} + +/* Form controls */ input, select, textarea { color: var(--color-foreground); background-color: var(--color-background); border-color: var(--color-border); + font-family: var(--font-sans); + transition: border-color 0.2s ease, box-shadow 0.2s ease; } -/* Global focus-visible styles for accessibility */ input:focus-visible, select:focus-visible, textarea:focus-visible { - outline: 2px solid var(--color-primary); - outline-offset: 2px; + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px var(--color-glow), 0 0 12px var(--color-glow); } button:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; - border-radius: 0.25rem; + border-radius: 0.375rem; } a:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; - border-radius: 0.25rem; + border-radius: 0.375rem; } /* Override browser autofill styles in dark mode */ @@ -82,16 +107,82 @@ a:focus-visible { [data-theme="dark"] input:-webkit-autofill:hover, [data-theme="dark"] input:-webkit-autofill:focus, [data-theme="dark"] select:-webkit-autofill { - -webkit-box-shadow: 0 0 0 1000px #18181b inset !important; - -webkit-text-fill-color: #fafafa !important; - caret-color: #fafafa; + -webkit-box-shadow: 0 0 0 1000px #13151e inset !important; + -webkit-text-fill-color: #e4e6ed !important; + caret-color: #e4e6ed; } -/* Dark mode color-scheme for native controls (scrollbars, checkboxes) */ -[data-theme="dark"] { - color-scheme: dark; +/* Color scheme for native controls */ +[data-theme="dark"] { color-scheme: dark; } +[data-theme="light"] { color-scheme: light; } + +/* Scrollbar styling */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: var(--color-muted-foreground); } + +/* Stagger animation utility */ +@keyframes fadeSlideIn { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } } -[data-theme="light"] { - color-scheme: light; +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes pulseGlow { + 0%, 100% { box-shadow: 0 0 4px var(--color-glow); } + 50% { box-shadow: 0 0 16px var(--color-glow-strong); } +} + +@keyframes countUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes gradientShift { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} + +.animate-fade-slide-in { + animation: fadeSlideIn 0.4s ease-out both; +} + +.animate-shimmer { + background: linear-gradient(90deg, var(--color-muted) 25%, var(--color-border) 50%, var(--color-muted) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; +} + +.animate-pulse-glow { + animation: pulseGlow 2s ease-in-out infinite; +} + +.animate-count-up { + animation: countUp 0.5s ease-out both; +} + +/* Stagger children utility — add .stagger-children to parent */ +.stagger-children > * { + animation: fadeSlideIn 0.4s ease-out both; +} +.stagger-children > *:nth-child(1) { animation-delay: 0ms; } +.stagger-children > *:nth-child(2) { animation-delay: 60ms; } +.stagger-children > *:nth-child(3) { animation-delay: 120ms; } +.stagger-children > *:nth-child(4) { animation-delay: 180ms; } +.stagger-children > *:nth-child(5) { animation-delay: 240ms; } +.stagger-children > *:nth-child(6) { animation-delay: 300ms; } +.stagger-children > *:nth-child(7) { animation-delay: 360ms; } +.stagger-children > *:nth-child(8) { animation-delay: 420ms; } +.stagger-children > *:nth-child(9) { animation-delay: 480ms; } +.stagger-children > *:nth-child(10) { animation-delay: 540ms; } + +/* Mono text utility */ +.font-mono { + font-family: var(--font-mono); } diff --git a/frontend/src/app.html b/frontend/src/app.html index e73d6fb..86ed7f5 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -4,6 +4,9 @@ + + + Immich Watcher %sveltekit.head% diff --git a/frontend/src/lib/components/Card.svelte b/frontend/src/lib/components/Card.svelte index f66f700..48c4455 100644 --- a/frontend/src/lib/components/Card.svelte +++ b/frontend/src/lib/components/Card.svelte @@ -6,6 +6,22 @@ }>(); -
+
{@render children()}
+ + diff --git a/frontend/src/lib/components/ConfirmModal.svelte b/frontend/src/lib/components/ConfirmModal.svelte index 1dbdb02..fe8863e 100644 --- a/frontend/src/lib/components/ConfirmModal.svelte +++ b/frontend/src/lib/components/ConfirmModal.svelte @@ -1,5 +1,6 @@ -

{message}

+
+
+ +
+

{message}

+
+ + diff --git a/frontend/src/lib/components/IconButton.svelte b/frontend/src/lib/components/IconButton.svelte index bc11362..3022975 100644 --- a/frontend/src/lib/components/IconButton.svelte +++ b/frontend/src/lib/components/IconButton.svelte @@ -10,17 +10,56 @@ size?: number; class?: string; }>(); - - const variantClasses = { - default: 'text-[var(--color-muted-foreground)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-muted)]', - danger: 'text-[var(--color-muted-foreground)] hover:text-[var(--color-destructive)] hover:bg-[var(--color-error-bg)]', - success: 'text-[var(--color-muted-foreground)] hover:text-[var(--color-success-fg)] hover:bg-[var(--color-success-bg)]', - }; + + diff --git a/frontend/src/lib/components/Loading.svelte b/frontend/src/lib/components/Loading.svelte index 47ab4f5..831bc61 100644 --- a/frontend/src/lib/components/Loading.svelte +++ b/frontend/src/lib/components/Loading.svelte @@ -2,8 +2,23 @@ let { lines = 3 } = $props<{ lines?: number }>(); -
- {#each Array(lines) as _} -
+
+ {#each Array(lines) as _, i} +
{/each}
+ + diff --git a/frontend/src/lib/components/Modal.svelte b/frontend/src/lib/components/Modal.svelte index f6f9c42..44be199 100644 --- a/frontend/src/lib/components/Modal.svelte +++ b/frontend/src/lib/components/Modal.svelte @@ -1,4 +1,7 @@ -
-
+
+

{title}

{#if description} -

{description}

+

{description}

{/if}
{#if children} - {@render children()} +
+ {@render children()} +
{/if}
diff --git a/frontend/src/lib/components/Snackbar.svelte b/frontend/src/lib/components/Snackbar.svelte index 4fbd70c..2b91892 100644 --- a/frontend/src/lib/components/Snackbar.svelte +++ b/frontend/src/lib/components/Snackbar.svelte @@ -21,15 +21,8 @@ warning: 'mdiAlert', }; - const borderColorMap: Record = { - success: '#22c55e', - error: '#ef4444', - info: '#3b82f6', - warning: '#f59e0b', - }; - - const iconColorMap: Record = { - success: '#22c55e', + const accentMap: Record = { + success: '#059669', error: '#ef4444', info: '#3b82f6', warning: '#f59e0b', @@ -38,38 +31,32 @@ {#if snacks.length > 0}
{#each snacks as snack (snack.id)}
- +
-

{snack.message}

+

{snack.message}

{#if snack.detail} - {#if expandedIds.has(snack.id)} -
{snack.detail}
+
{snack.detail}
{/if} {/if}
-
{/each} @@ -85,4 +72,78 @@ bottom: 1.5rem; } } + + .snack-item { + pointer-events: auto; + display: flex; + align-items: flex-start; + gap: 0.625rem; + padding: 0.75rem 1rem; + border-radius: 0.75rem; + border-left: 3px solid var(--snack-accent); + background: var(--color-card); + border-top: 1px solid var(--color-border); + border-right: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(255, 255, 255, 0.03) inset; + backdrop-filter: blur(12px); + } + + :global([data-theme="dark"]) .snack-item { + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 16px color-mix(in srgb, var(--snack-accent) 10%, transparent); + } + + .snack-icon { + flex-shrink: 0; + margin-top: 1px; + } + + .snack-message { + font-size: 0.8rem; + line-height: 1.4; + margin: 0; + color: var(--color-foreground); + } + + .snack-detail-toggle { + font-size: 0.7rem; + color: var(--color-muted-foreground); + background: none; + border: none; + padding: 0; + margin-top: 0.25rem; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + } + + .snack-detail-toggle:hover { + color: var(--color-foreground); + } + + .snack-detail { + font-size: 0.7rem; + font-family: var(--font-mono); + color: var(--color-muted-foreground); + margin: 0.25rem 0 0; + white-space: pre-wrap; + word-break: break-word; + } + + .snack-close { + flex-shrink: 0; + background: none; + border: none; + color: var(--color-muted-foreground); + cursor: pointer; + padding: 0.125rem; + border-radius: 0.25rem; + transition: all 0.15s ease; + line-height: 1; + } + + .snack-close:hover { + color: var(--color-foreground); + background: var(--color-muted); + } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 1378827..f2b2680 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -79,101 +79,146 @@ localStorage.setItem('sidebar_collapsed', String(collapsed)); } } + + function isActive(href: string): boolean { + return page.url.pathname === href; + } {#if isAuthPage} {@render children()} {:else if auth.loading}
-

{t('common.loading')}

+
+
+

{t('common.loading')}

+
{:else if auth.user}
-