From 39ba8677e5a08d86dfef793623f0af94b5fcb661 Mon Sep 17 00:00:00 2001 From: Florian <52180080+florianjs@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:09:50 +0200 Subject: [PATCH 1/2] feat(ui): new design system and app shell - theme tokens as CSS variables: light, dark, or follow the system, chosen per user (cookie, applied during SSR so there is no flash) - meaningful colors: green for upload/seed, amber for download/leech, violet for freeleech; contrast inks keep existing controls legible in both themes - self-hosted IBM Plex Sans and Mono (Google Fonts was blocked by the CSP, so the app silently fell back to system fonts) - app shell: grouped sidebar (browse, community, account, staff), top bar with a transfer meter, freeleech pill and account menu (passkey copy, points, theme, language) - command palette (Cmd/Ctrl+K or /): search torrents and jump to pages - mobile navigation drawer, skip link, visible focus, reduced motion - dialog overlays stay dark in both themes; fix icons collapsed to zero width by an inline class Co-Authored-By: Claude Opus 5.5 --- app/app.vue | 5 + app/assets/css/main.css | 148 ++++- app/components/EditTorrentModal.vue | 2 +- app/components/ThemeSwitcher.vue | 30 + app/components/UploadTorrentModal.vue | 2 +- app/components/admin/Panic.vue | 4 +- app/components/admin/TorznabRateLimiting.vue | 2 +- app/components/admin/TorznabUsers.vue | 4 +- app/components/shell/AppSidebar.vue | 146 +++++ app/components/shell/CommandPalette.vue | 210 +++++++ app/components/shell/TransferMeter.vue | 64 ++ app/components/shell/UserMenu.vue | 161 +++++ app/composables/useTheme.ts | 36 ++ app/layouts/default.vue | 597 +++++-------------- app/pages/auth/register.vue | 6 +- app/pages/forum/index.vue | 6 +- app/pages/torrents/[hash].vue | 2 +- i18n/locales/en/app.json | 62 ++ i18n/locales/fr/app.json | 62 ++ package-lock.json | 20 + package.json | 2 + tailwind.config.ts | 53 +- 22 files changed, 1102 insertions(+), 522 deletions(-) create mode 100644 app/components/ThemeSwitcher.vue create mode 100644 app/components/shell/AppSidebar.vue create mode 100644 app/components/shell/CommandPalette.vue create mode 100644 app/components/shell/TransferMeter.vue create mode 100644 app/components/shell/UserMenu.vue create mode 100644 app/composables/useTheme.ts diff --git a/app/app.vue b/app/app.vue index f8eacfa7..33fb6efa 100644 --- a/app/app.vue +++ b/app/app.vue @@ -3,3 +3,8 @@ + + diff --git a/app/assets/css/main.css b/app/assets/css/main.css index 1a30b290..07016375 100644 --- a/app/assets/css/main.css +++ b/app/assets/css/main.css @@ -1,22 +1,94 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap'); +/* Self-hosted fonts (the CSP only allows same-origin fonts) */ +@import '@fontsource/ibm-plex-sans/400.css'; +@import '@fontsource/ibm-plex-sans/500.css'; +@import '@fontsource/ibm-plex-sans/600.css'; +@import '@fontsource/ibm-plex-mono/400.css'; +@import '@fontsource/ibm-plex-mono/500.css'; @tailwind base; @tailwind components; @tailwind utilities; +/* + * Theme tokens as RGB channels (see tailwind.config.ts). + * Light by default, dark when the system prefers it, and an explicit + * data-theme on (user choice) wins over the system. + */ @layer base { + :root { + color-scheme: light; + --bg: 243 244 246; + --surface: 255 255 255; + --raised: 247 248 250; + --hover: 236 238 241; + --line: 220 223 228; + --line-strong: 195 200 208; + --ink: 26 28 32; + --ink-2: 79 85 94; + --ink-3: 107 113 123; + --seed: 29 133 86; + --leech: 168 107 12; + --free: 91 71 214; + --danger: 194 58 51; + --contrast: 26 28 32; + --contrast-inv: 255 255 255; + } + + @media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + color-scheme: dark; + --bg: 23 24 27; + --surface: 31 33 37; + --raised: 40 42 47; + --hover: 46 48 54; + --line: 51 54 60; + --line-strong: 70 74 82; + --ink: 236 234 229; + --ink-2: 167 166 161; + --ink-3: 132 133 127; + --seed: 76 183 130; + --leech: 226 162 62; + --free: 155 138 247; + --danger: 230 98 91; + --contrast: 245 244 240; + --contrast-inv: 23 24 27; + } + } + + :root[data-theme='dark'] { + color-scheme: dark; + --bg: 23 24 27; + --surface: 31 33 37; + --raised: 40 42 47; + --hover: 46 48 54; + --line: 51 54 60; + --line-strong: 70 74 82; + --ink: 236 234 229; + --ink-2: 167 166 161; + --ink-3: 132 133 127; + --seed: 76 183 130; + --leech: 226 162 62; + --free: 155 138 247; + --danger: 230 98 91; + --contrast: 245 244 240; + --contrast-inv: 23 24 27; + } + html { @apply antialiased; + font-feature-settings: 'ss02'; } body { - @apply bg-bg-primary text-text-primary font-sans selection:bg-white selection:text-black; - background-image: radial-gradient( - circle at 2px 2px, - #1a1a1a 1px, - transparent 0 - ); - background-size: 40px 40px; + @apply bg-bg-primary text-text-primary font-sans text-sm leading-6; + } + + ::selection { + @apply bg-free/25 text-text-primary; + } + + :focus-visible { + @apply outline-none ring-2 ring-free/60 ring-offset-1 ring-offset-bg-primary; } /* Custom scrollbar */ @@ -25,7 +97,7 @@ } ::-webkit-scrollbar-track { - @apply bg-bg-secondary; + @apply bg-transparent; } ::-webkit-scrollbar-thumb { @@ -35,80 +107,98 @@ ::-webkit-scrollbar-thumb:hover { @apply bg-border-hover; } + + @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } + } } @layer components { - /* Card component */ + /* Panels: flat surface, one radius, no glow */ .card { - @apply bg-bg-secondary/80 backdrop-blur-sm border border-border rounded-md transition-all duration-200 hover:border-border-hover; + @apply bg-bg-secondary border border-border rounded-md; } .card-header { - @apply px-3 py-2 border-b border-border flex items-center justify-between; + @apply px-4 py-2.5 border-b border-border flex items-center justify-between gap-3; } .card-body { - @apply p-3; + @apply p-4; } - /* Button styles */ + /* Buttons */ .btn { - @apply px-3 py-1.5 rounded text-sm font-medium transition-all duration-200 active:scale-[0.98] disabled:opacity-50 disabled:pointer-events-none; + @apply inline-flex items-center justify-center gap-1.5 px-3 py-1.5 rounded text-sm font-medium transition-colors disabled:opacity-50 disabled:pointer-events-none; } .btn-primary { - @apply bg-white text-black hover:bg-gray-200 shadow-[0_0_15px_rgba(255,255,255,0.1)]; + @apply bg-white text-black hover:bg-white/85; } .btn-secondary { @apply bg-bg-tertiary text-text-primary border border-border hover:bg-bg-hover hover:border-border-hover; } - /* Input styles */ + .btn-ghost { + @apply text-text-secondary hover:text-text-primary hover:bg-bg-hover; + } + + /* Inputs */ .input { - @apply bg-bg-tertiary/50 border border-border rounded px-3 py-1.5 text-sm text-text-primary placeholder-text-muted focus:outline-none focus:border-white/30 focus:bg-bg-tertiary transition-all; + @apply bg-bg-tertiary border border-border rounded px-3 py-1.5 text-sm text-text-primary placeholder-text-muted transition-colors hover:border-border-hover focus:outline-none focus:border-free/60 focus:ring-2 focus:ring-free/20; } - /* Table styles */ + /* Tables */ .data-table { - @apply w-full text-xs border-separate border-spacing-0; + @apply w-full text-sm border-separate border-spacing-0; } .data-table th { - @apply text-left text-text-muted font-semibold px-3 py-2 border-b border-border bg-bg-secondary/50 uppercase tracking-wider; + @apply text-left text-xs font-medium text-text-muted px-3 py-2 border-b border-border; } .data-table td { - @apply px-3 py-2 border-b border-border/50; + @apply px-3 py-2 border-b border-border/60; } .data-table tr:last-child td { @apply border-b-0; } - .data-table tr:hover td { - @apply bg-white/[0.02]; + .data-table tbody tr:hover td { + @apply bg-bg-hover/60; } - /* Stat badge */ + /* Swarm counters */ .stat-badge { - @apply inline-flex items-center gap-1 px-1.5 py-0.5 rounded-sm text-[10px] font-mono font-bold uppercase; + @apply inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-mono tabular-nums; } .stat-seeders { - @apply text-success bg-success/5 border border-success/20; + @apply text-seed bg-seed/10; } .stat-leechers { - @apply text-warning bg-warning/5 border border-warning/20; + @apply text-leech bg-leech/10; } .btn-search { - @apply p-1.5 text-text-muted hover:text-white transition-colors; + @apply p-1.5 text-text-muted hover:text-text-primary transition-colors; } } @layer utilities { + /* Numbers that should line up: sizes, ratios, counts */ + .num { + @apply font-mono tabular-nums; + } + .truncate-hash { @apply font-mono text-2xs; max-width: 120px; diff --git a/app/components/EditTorrentModal.vue b/app/components/EditTorrentModal.vue index c4bc4ace..06277c88 100644 --- a/app/components/EditTorrentModal.vue +++ b/app/components/EditTorrentModal.vue @@ -2,7 +2,7 @@
+
+ +
+ + + diff --git a/app/components/UploadTorrentModal.vue b/app/components/UploadTorrentModal.vue index 5d328186..0d4a9513 100644 --- a/app/components/UploadTorrentModal.vue +++ b/app/components/UploadTorrentModal.vue @@ -2,7 +2,7 @@

- + {{ t('admin.torznabRateLimit.immediateNote') }}