diff --git a/app/assets/stylesheets/base/root.css b/app/assets/stylesheets/base/root.css index e08e820..8ce0eac 100644 --- a/app/assets/stylesheets/base/root.css +++ b/app/assets/stylesheets/base/root.css @@ -2,6 +2,7 @@ body { font-family: var(--font-sans); color: var(--color-text-secondary); background-color: var(--color-bg-page); + transition: background-color var(--transition-normal), color var(--transition-normal); } .container { diff --git a/app/assets/stylesheets/colors.css b/app/assets/stylesheets/colors.css index e8596a3..56b01a2 100644 --- a/app/assets/stylesheets/colors.css +++ b/app/assets/stylesheets/colors.css @@ -1,60 +1,178 @@ +:root { + color-scheme: light; + /* Indigo-based palette */ + --color-primary-50: #eef2ff; + --color-primary-100: #e0e7ff; + --color-primary-200: #c7d2fe; + --color-primary-300: #a5b4fc; + --color-primary-500: #6366f1; + --color-primary-600: #4f46e5; + --color-primary-700: #4338ca; + --color-primary-900: #312e81; -:root { - /* https://paletton.com/#uid=63S0u0kaVz84jP27qHbeJtFiHpX */ - --color-primary-1: #C6CBE1; - --color-primary-2: #A3ABCD; - --color-primary-3: #8089B6; - --color-primary-4: #696CA1; - --color-primary-5: #45528D; - - --color-secondary1-1: #CFC6E1; - --color-secondary1-2: #B0A2CD; - --color-secondary1-3: #917EB6; - --color-secondary1-4: #755FA2; - --color-secondary1-5: #5B438D; - - --color-secondary2-1: #BFD4DB; - --color-secondary2-2: #98B9C4; - --color-secondary2-3: #719BA9; - --color-secondary2-4: #528394; - --color-secondary2-5: #396F81; - - /* Color Palette - Grays */ + --color-primary-1: #f5f7ff; + --color-primary-2: #e7ebff; + --color-primary-3: #d5dcff; + --color-primary-4: #b7c4fb; + --color-primary-5: #8c99e6; + + --color-secondary1-1: #f3e8ff; + --color-secondary1-2: #e9d5ff; + --color-secondary1-3: #d8b4fe; + --color-secondary1-4: #c084fc; + + --color-secondary2-1: #dbeafe; + --color-secondary2-2: #bfdbfe; + --color-secondary2-3: #93c5fd; + --color-secondary2-4: #60a5fa; + --color-secondary2-5: #3b82f6; + + --color-gray-25: #f8fafc; --color-gray-50: #f8fafc; - --color-gray-100: #f1f5f9; - --color-gray-200: #e2e8f0; - --color-gray-300: #d1d5db; - --color-gray-400: #9ca3af; + --color-gray-100: #e2e8f0; + --color-gray-300: #cbd5e1; --color-gray-500: #64748b; --color-gray-600: #475569; - --color-gray-700: #374151; + --color-gray-700: #334155; --color-gray-800: #1e293b; --color-gray-900: #0f172a; + --color-text-primary: #0f172a; + --color-text-secondary: #1f2937; + --color-text-muted: #6b7280; + --color-text-link: var(--color-primary-700); + --color-text-link-hover: var(--color-primary-900); + --color-text-contrast: #f8fafc; + --color-text-button: #0f172a; + --color-bg-page: var(--color-primary-1); - --color-bg-input: var(--color-primary-1); - --color-bg-container: var(--color-primary-3); - --color-bg-hover: var(--color-primary-4); - --color-bg-sidebar: var(--color-primary-3); + --color-bg-input: #f8fafc; + --color-bg-container: #ffffff; + --color-bg-card: #ffffff; + --color-bg-hover: var(--color-primary-2); + --color-bg-sidebar: #f3f4ff; --color-bg-sidebar-header: var(--color-primary-2); --color-bg-table-header: var(--color-primary-2); - --color-bg-nav: var(--color-primary-4); - --color-border: var(--color-primary-5); - --color-bg-button: var(--color-secondary2-2); - --color-bg-button-hover: var(--color-secondary2-1); + --color-bg-nav: #0f172a; + --color-bg-button: var(--color-primary-2); + --color-bg-button-hover: var(--color-primary-3); + --color-bg-icon: var(--color-secondary1-2); + --color-bg-icon-status: var(--color-secondary2-2); + --color-bg-hoverbox: #eef2ff; + --color-bg-contributor: var(--color-secondary1-2); + --color-bg-committer: var(--color-secondary2-2); + --color-bg-activity-team: var(--color-secondary2-2); + --color-bg-activity-user: var(--color-secondary2-1); + --color-border: #cdd5e7; + --color-border-focus: var(--color-primary-500); --color-border-contributor: var(--color-secondary2-2); --color-border-new: var(--color-secondary1-1); --color-border-aware: var(--color-secondary1-2); --color-border-reading: var(--color-secondary1-3); - --color-border-read: var(--color-secondary1-4); + --color-border-read: var(--color-secondary2-3); - --color-bg-icon: var(--color-secondary1-2); - --color-bg-icon-status: var(--color-secondary2-2); - --color-bg-hoverbox: var(--color-secondary2-1); - --color-bg-contributor: var(--color-secondary1-2); - --color-bg-committer: var(--color-secondary1-1); - --color-bg-activity-team: var(--color-secondary2-2); - --color-bg-activity-user: var(--color-secondary2-1); + --color-warning-bg: #fef3c7; + --color-warning: #f59e0b; + --color-warning-text: #92400e; + --color-info: #38bdf8; + --color-info-soft: #e0f2fe; + --color-danger: #ef4444; + --color-danger-soft: #fee2e2; + --color-purple: #7c3aed; + --color-success: #16a34a; + --color-success-soft: #dcfce7; + --color-note-soft: #fef9c3; + + --shadow-lg: 0 16px 40px rgba(15, 23, 42, 0.12); +} + +:root[data-theme="dark"] { + color-scheme: dark; + + --color-primary-50: #161b33; + --color-primary-100: #1d2440; + --color-primary-200: #222c52; + --color-primary-300: #2c3669; + --color-primary-500: #6f7ee8; + --color-primary-600: #8b9cf5; + --color-primary-700: #c7d2fe; + --color-primary-900: #e0e7ff; + + --color-primary-1: #0b1220; + --color-primary-2: #0f172a; + --color-primary-3: #131f35; + --color-primary-4: #1b2743; + --color-primary-5: #223055; + + --color-secondary1-1: #2b2244; + --color-secondary1-2: #372b59; + --color-secondary1-3: #45356f; + --color-secondary1-4: #564086; + + --color-secondary2-1: #16263c; + --color-secondary2-2: #1d2f4d; + --color-secondary2-3: #254165; + --color-secondary2-4: #2f4f7a; + --color-secondary2-5: #3a5f93; + + --color-gray-25: #0b1220; + --color-gray-50: #0f172a; + --color-gray-100: #111827; + --color-gray-300: #1f2937; + --color-gray-500: #94a3b8; + --color-gray-600: #cbd5e1; + --color-gray-700: #e2e8f0; + --color-gray-800: #e5e7eb; + --color-gray-900: #f8fafc; + + --color-text-primary: #e2e8f0; + --color-text-secondary: #cbd5e1; + --color-text-muted: #94a3b8; + --color-text-link: #c7d2fe; + --color-text-link-hover: #e0e7ff; + --color-text-contrast: #f8fafc; + --color-text-button: #e2e8f0; + + --color-bg-page: var(--color-primary-1); + --color-bg-input: #111827; + --color-bg-container: var(--color-primary-2); + --color-bg-card: #111827; + --color-bg-hover: #1f2937; + --color-bg-sidebar: var(--color-primary-2); + --color-bg-sidebar-header: #111827; + --color-bg-table-header: #111827; + --color-bg-nav: #0b1220; + --color-bg-button: #223055; + --color-bg-button-hover: #2e3b66; + --color-bg-icon: #1f2937; + --color-bg-icon-status: #2b375c; + --color-bg-hoverbox: #1a2337; + --color-bg-contributor: #1f2545; + --color-bg-committer: #1c2f4f; + --color-bg-activity-team: #18253d; + --color-bg-activity-user: #142235; + + --color-border: #1f2937; + --color-border-focus: #818cf8; + --color-border-contributor: #2b375c; + --color-border-new: #3a5f93; + --color-border-aware: #564086; + --color-border-reading: #254165; + --color-border-read: #2f4f7a; + + --color-warning-bg: #3a2c17; + --color-warning: #f59e0b; + --color-warning-text: #fcd34d; + --color-info: #38bdf8; + --color-info-soft: #10253b; + --color-danger: #ef4444; + --color-danger-soft: #3a1f24; + --color-purple: #c084fc; + --color-success: #22c55e; + --color-success-soft: #1b2b1e; + --color-note-soft: #3a3216; + + --shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.45); } diff --git a/app/assets/stylesheets/components/activities.css b/app/assets/stylesheets/components/activities.css index 3a75871..45831cd 100644 --- a/app/assets/stylesheets/components/activities.css +++ b/app/assets/stylesheets/components/activities.css @@ -19,8 +19,8 @@ } .activity-card.is-unread { - border-color: #bfdbfe; - background: #eff6ff; + border-color: var(--color-primary-200); + background: var(--color-primary-50); } .activity-meta { @@ -39,8 +39,8 @@ .activity-unread-badge { padding: 2px 8px; - background: #2563eb; - color: white; + background: var(--color-primary-600); + color: var(--color-text-contrast); border-radius: 999px; font-size: var(--font-size-xs); } diff --git a/app/assets/stylesheets/components/form.css b/app/assets/stylesheets/components/form.css index 2fbf7e7..c5f1e52 100644 --- a/app/assets/stylesheets/components/form.css +++ b/app/assets/stylesheets/components/form.css @@ -4,7 +4,7 @@ input { width: 100%; padding: var(--spacing-4); margin: var(--spacing-4) 0; - border: var(--border-width) solid var(--color-gray-300); + border: var(--border-width) solid var(--color-border); border-radius: var(--border-radius-md); font-size: var(--font-size-base); display: block; diff --git a/app/assets/stylesheets/components/messages.css b/app/assets/stylesheets/components/messages.css index e1062f3..479f543 100644 --- a/app/assets/stylesheets/components/messages.css +++ b/app/assets/stylesheets/components/messages.css @@ -51,7 +51,7 @@ height: 42px; border-radius: 50%; border: var(--border-width) solid var(--color-border); - background: white; + background: var(--color-bg-card); } .author-details { @@ -93,7 +93,7 @@ transition: background-color 0.3s ease, color 0.3s ease; &.is-read { - background: var(--color-gray-25, #f8f8f8); + background: var(--color-bg-hover); } } @@ -103,7 +103,7 @@ & .button-secondary { padding: var(--spacing-2) var(--spacing-4); border: var(--border-width) solid var(--color-border); - background: white; + background: var(--color-bg-card); border-radius: var(--border-radius-sm); cursor: pointer; } @@ -151,7 +151,7 @@ } & .message-diff { - background: var(--color-gray-100); + background: var(--color-bg-hover); border: var(--border-width) solid var(--color-border); border-radius: var(--border-radius-sm); padding: var(--spacing-3); @@ -217,7 +217,7 @@ .attachment { padding: var(--spacing-2) var(--spacing-3); - background: var(--color-gray-100); + background: var(--color-bg-hover); border-radius: var(--border-radius-sm); margin-bottom: var(--spacing-2); } @@ -257,7 +257,7 @@ .import-log { margin-top: var(--spacing-2); padding: var(--spacing-2); - background: var(--color-gray-100); + background: var(--color-bg-hover); border-radius: var(--border-radius-sm); font-size: var(--font-size-xs); color: var(--color-gray-500); diff --git a/app/assets/stylesheets/components/navigation.css b/app/assets/stylesheets/components/navigation.css index ae8e789..e3b5619 100644 --- a/app/assets/stylesheets/components/navigation.css +++ b/app/assets/stylesheets/components/navigation.css @@ -18,6 +18,7 @@ justify-content: space-between; align-items: center; min-height: var(--nav-height); + gap: var(--spacing-4); } .nav-brand { @@ -28,7 +29,7 @@ .brand-link { font-size: var(--font-size-2xl); font-weight: var(--font-weight-bold); - color: #000; + color: var(--color-text-contrast); text-decoration: none; margin-bottom: -2px; } @@ -36,6 +37,7 @@ .tagline { font-size: var(--font-size-xs); font-weight: var(--font-weight-normal); + color: var(--color-text-muted); } .nav-links { @@ -43,14 +45,39 @@ gap: var(--spacing-6); } +.nav-right { + display: flex; + align-items: center; + gap: var(--spacing-3); +} + +.nav-auth { + display: flex; + align-items: center; + gap: var(--spacing-3); +} + +.nav-auth form { + margin: 0; +} + +.nav-user { + color: var(--color-text-contrast); + font-weight: var(--font-weight-medium); +} + .nav-link { background-color: var(--color-bg-button); - color: #000; + color: var(--color-text-button); text-decoration: none; font-weight: var(--font-weight-medium); padding: var(--spacing-2) var(--spacing-4); border-radius: var(--border-radius-md); transition: all var(--transition-fast); + border: var(--border-width) solid transparent; + display: inline-flex; + align-items: center; + gap: var(--spacing-2); &:hover { background-color: var(--color-bg-button-hover); @@ -70,8 +97,20 @@ min-width: 22px; padding: 2px 8px; border-radius: 999px; - background: #ef4444; - color: white; + background: var(--color-danger); + color: var(--color-text-contrast); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); } + +.theme-toggle { + border-color: var(--color-border); + background: var(--color-bg-card); + color: var(--color-text-secondary); + cursor: pointer; + box-shadow: var(--shadow-sm); + + &:hover { + background: var(--color-bg-hover); + } +} diff --git a/app/assets/stylesheets/components/notes.css b/app/assets/stylesheets/components/notes.css index ccb0417..bd249db 100644 --- a/app/assets/stylesheets/components/notes.css +++ b/app/assets/stylesheets/components/notes.css @@ -44,7 +44,7 @@ border: var(--border-width) solid var(--color-border); border-radius: var(--border-radius-md); padding: var(--spacing-3); - background: white; + background: var(--color-bg-card); } .note-meta { diff --git a/app/assets/stylesheets/components/topics.css b/app/assets/stylesheets/components/topics.css index 86a8dfb..c0bae08 100644 --- a/app/assets/stylesheets/components/topics.css +++ b/app/assets/stylesheets/components/topics.css @@ -69,7 +69,7 @@ & td { padding: var(--spacing-2); - border-bottom: 1px #000 solid; + border-bottom: var(--border-width) solid var(--color-border); vertical-align: top; } @@ -113,7 +113,7 @@ padding: 2px 4px; border-radius: 999px; background: var(--color-bg-icon); - color: #000; + color: var(--color-text-primary); font-size: var(--font-size-xs); font-weight: var(--font-weight-bold); line-height: 1; @@ -192,7 +192,7 @@ } .topic-row.topic-read { - color: #444; + color: var(--color-text-secondary); } .topic-icon-hover { @@ -292,31 +292,31 @@ width: 28px; height: 28px; border-radius: 999px; - background: #f3f4f6; + background: var(--color-gray-50); border: 2px solid transparent; color: inherit; } .is-new .status-shape { - border-color: #ef4444; - background: #fee2e2; + border-color: var(--color-danger); + background: var(--color-danger-soft); } .is-aware .status-shape { - border-color: #f59e0b; - background: #fef3c7; + border-color: var(--color-warning); + background: var(--color-warning-bg); border-radius: 999px; } .is-reading .status-shape { - border-color: #38bdf8; - background: #e0f2fe; + border-color: var(--color-info); + background: var(--color-info-soft); border-radius: 4px; } .is-read .status-shape { - border-color: #1d4ed8; - background: #e0e7ff; + border-color: var(--color-primary-600); + background: var(--color-primary-100); border-radius: 4px; } @@ -335,11 +335,11 @@ } .team-reader-avatar.is-read { - border-color: #1d4ed8; + border-color: var(--color-primary-600); } .team-reader-avatar.is-reading { - border-color: #38bdf8; + border-color: var(--color-info); } .note-status { @@ -362,8 +362,8 @@ min-width: 22px; padding: 2px 6px; border-radius: 10px; - background: #fef3c7; - color: #b45309; + background: var(--color-note-soft); + color: var(--color-warning-text); font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); } @@ -391,19 +391,20 @@ .mark-aware-button { padding: var(--spacing-2) var(--spacing-4); border: var(--border-width) solid var(--color-border); - background: white; + background: var(--color-bg-card); border-radius: var(--border-radius-sm); cursor: pointer; font-weight: var(--font-weight-medium); + color: var(--color-text-primary); &.secondary { - background: var(--color-gray-50); + background: var(--color-bg-hover); } } .global-warning { - background: #fff4e5; - color: #9a4d00; + background: var(--color-warning-bg); + color: var(--color-warning-text); padding: var(--spacing-2) var(--spacing-4); text-align: center; font-weight: var(--font-weight-semibold); @@ -413,14 +414,14 @@ margin-top: 6px; width: 120px; height: 6px; - background: var(--color-gray-100); + background: var(--color-bg-hover); border-radius: 999px; overflow: hidden; } .status-progress-bar { height: 100%; - background: linear-gradient(90deg, #a5b4fc, #6366f1); + background: linear-gradient(90deg, var(--color-primary-300), var(--color-primary-600)); } .tag-filter-banner { @@ -459,7 +460,7 @@ text-decoration: none; color: var(--color-text-primary); font-weight: var(--font-weight-medium); - background: white; + background: var(--color-bg-card); } .toggle-btn.active { diff --git a/app/assets/stylesheets/layouts/topic-view.css b/app/assets/stylesheets/layouts/topic-view.css index 52b14e1..9a4d809 100644 --- a/app/assets/stylesheets/layouts/topic-view.css +++ b/app/assets/stylesheets/layouts/topic-view.css @@ -63,7 +63,7 @@ &.active { background: var(--color-primary-500); - color: white; + color: var(--color-text-contrast); } } @@ -254,7 +254,7 @@ border-radius: 50%; border: var(--border-width) solid var(--color-border); object-fit: cover; - background: white; + background: var(--color-bg-card); } .outline-number { diff --git a/app/assets/stylesheets/variables.css b/app/assets/stylesheets/variables.css index 59d4686..1079de0 100644 --- a/app/assets/stylesheets/variables.css +++ b/app/assets/stylesheets/variables.css @@ -24,6 +24,7 @@ --font-size-3xl: 2rem; --font-size-4xl: 2.5rem; + --font-family-base: var(--font-sans); --font-weight-normal: 400; --font-weight-medium: 500; --font-weight-semibold: 600; diff --git a/app/javascript/controllers/theme_controller.js b/app/javascript/controllers/theme_controller.js new file mode 100644 index 0000000..50971b3 --- /dev/null +++ b/app/javascript/controllers/theme_controller.js @@ -0,0 +1,55 @@ +import { Controller } from "@hotwired/stimulus" + +const STORAGE_KEY = "hackorum-theme" +const DEFAULT_THEME = "light" + +export default class extends Controller { + static targets = ["icon", "label"] + + connect() { + this.applyInitialTheme() + } + + toggle(event) { + event.preventDefault() + const nextTheme = this.currentTheme === "dark" ? "light" : "dark" + this.setTheme(nextTheme) + } + + applyInitialTheme() { + const storedTheme = window.localStorage.getItem(STORAGE_KEY) + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches + const initialTheme = storedTheme || (prefersDark ? "dark" : DEFAULT_THEME) + + this.setTheme(initialTheme, { persist: false }) + } + + setTheme(theme, { persist = true } = {}) { + const normalizedTheme = theme === "dark" ? "dark" : "light" + document.documentElement.dataset.theme = normalizedTheme + + if (persist) { + window.localStorage.setItem(STORAGE_KEY, normalizedTheme) + } + + this.updateToggle(normalizedTheme) + } + + updateToggle(theme) { + if (this.hasLabelTarget) { + this.labelTarget.textContent = theme === "dark" ? "Dark" : "Light" + } + + if (this.hasIconTarget) { + this.iconTarget.classList.remove("fa-moon", "fa-sun") + this.iconTarget.classList.add(theme === "dark" ? "fa-sun" : "fa-moon") + } + + this.element.setAttribute("aria-pressed", theme === "dark") + this.element.setAttribute("title", `Switch to ${theme === "dark" ? "light" : "dark"} mode`) + } + + get currentTheme() { + return document.documentElement.dataset.theme || DEFAULT_THEME + } +} diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim index 0a0cbbc..eded62b 100644 --- a/app/views/layouts/application.html.slim +++ b/app/views/layouts/application.html.slim @@ -1,5 +1,5 @@ doctype html -html +html data-theme="light" head title = content_for(:title) || "Hackorum" @@ -12,6 +12,15 @@ html link[rel="icon" href="/icon.png" type="image/png"] link[rel="icon" href="/icon.svg" type="image/svg+xml"] link[rel="apple-touch-icon" href="/icon.png"] + script + | + (function() { + var storageKey = 'hackorum-theme'; + var stored = window.localStorage.getItem(storageKey); + var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + var theme = stored || (prefersDark ? 'dark' : 'light'); + document.documentElement.dataset.theme = theme; + })(); = stylesheet_link_tag :app, "data-turbo-track": "reload" = javascript_importmap_tags script[src="https://kit.fontawesome.com/92fb6aa8ba.js"] @@ -20,19 +29,23 @@ html .global-warning span Please set a username in Settings. nav.main-navigation - .nav-container - .nav-brand - = link_to "Hackorum", root_path, class: "brand-link" - span.tagline PostgreSQL Hackers Archive - .nav-links - = link_to "Topics", topics_path, class: "nav-link" - = link_to "Search", search_topics_path, class: "nav-link" - - if user_signed_in? - - unread = activity_unread_count - = link_to activities_path, class: "nav-link nav-link-activity" do - | Activity - - if unread.positive? - span.nav-badge = unread + .nav-container + .nav-brand + = link_to "Hackorum", root_path, class: "brand-link" + span.tagline PostgreSQL Hackers Archive + .nav-links + = link_to "Topics", topics_path, class: "nav-link" + = link_to "Search", search_topics_path, class: "nav-link" + - if user_signed_in? + - unread = activity_unread_count + = link_to activities_path, class: "nav-link nav-link-activity" do + | Activity + - if unread.positive? + span.nav-badge = unread + .nav-right + button.nav-link.theme-toggle type="button" aria-label="Toggle theme" data-controller="theme" data-action="click->theme#toggle" + i.fas.fa-moon data-theme-target="icon" + span data-theme-target="label" Theme .nav-auth - if user_signed_in? - if current_user&.primary_alias diff --git a/app/views/teams/index.html.slim b/app/views/teams/index.html.slim index 1722f66..9102fc6 100644 --- a/app/views/teams/index.html.slim +++ b/app/views/teams/index.html.slim @@ -11,7 +11,7 @@ - @your_teams.each do |team| li = link_to team.name, team_path(team) - span.team-meta = "#{team.team_members.count} member(s)" + span.team-meta = " - #{team.team_members.count} member(s)" - else p You are not in any teams yet. @@ -30,6 +30,6 @@ - @all_teams.each do |team| li = link_to team.name, team_path(team) - span.team-meta = "#{team.team_members.count} member(s)" + span.team-meta = " - #{team.team_members.count} member(s)" - else p No teams yet.