diff --git a/extension/content/content.js b/extension/content/content.js index c7545f2..e0f9de9 100644 --- a/extension/content/content.js +++ b/extension/content/content.js @@ -7,7 +7,39 @@ let currentSelection = null; const OVERLAY_STYLES = ` - :host { all: initial; } + :host { + all: initial; + --bg-primary: #09090b; + --bg-secondary: #0f0f12; + --bg-tertiary: #18181b; + --bg-card: #09090b; + --bg-elevated: #18181b; + --bg-hover: #27272a; + + --text-primary: #e4e4e7; + --text-secondary: #a1a1aa; + --border: #27272a; + + --accent: #6366f1; + --accent-hover: #4f46e5; + } + + :host(.light) { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-card: #ffffff; + --bg-elevated: #f4f4f5; + --bg-hover: #e4e4e7; + + --text-primary: #18181b; + --text-secondary: #52525b; + --border: #e4e4e7; + + --accent: #4f46e5; + --accent-hover: #4338ca; + } + .margin-overlay { position: absolute; top: 0; @@ -20,8 +52,8 @@ .margin-popover { position: absolute; width: 320px; - background: #09090b; - border: 1px solid #27272a; + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 12px; padding: 0; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); @@ -30,7 +62,7 @@ pointer-events: auto; z-index: 2147483647; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - color: #e4e4e7; + color: var(--text-primary); opacity: 0; transform: scale(0.95); animation: popover-in 0.15s forwards; @@ -40,21 +72,22 @@ @keyframes popover-in { to { opacity: 1; transform: scale(1); } } .popover-header { padding: 12px 16px; - border-bottom: 1px solid #27272a; + border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; - background: #0f0f12; + background: var(--bg-secondary); border-radius: 12px 12px 0 0; font-weight: 600; font-size: 13px; + color: var(--text-primary); } .popover-scroll-area { overflow-y: auto; max-height: 400px; } .popover-item-block { - border-bottom: 1px solid #27272a; + border-bottom: 1px solid var(--border); margin-bottom: 0; animation: fade-in 0.2s; } @@ -68,36 +101,36 @@ gap: 8px; } .popover-avatar { - width: 24px; height: 24px; border-radius: 50%; background: #27272a; + width: 24px; height: 24px; border-radius: 50%; background: var(--bg-hover); display: flex; align-items: center; justify-content: center; - font-size: 10px; color: #a1a1aa; + font-size: 10px; color: var(--text-secondary); } - .popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; } - .popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; } - .popover-close:hover { color: #e4e4e7; } - .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; } + .popover-handle { font-size: 12px; font-weight: 600; color: var(--text-primary); } + .popover-close { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px; } + .popover-close:hover { color: var(--text-primary); } + .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: var(--text-primary); } .popover-quote { - margin-top: 8px; padding: 6px 10px; background: #18181b; - border-left: 2px solid #6366f1; border-radius: 4px; - font-size: 11px; color: #a1a1aa; font-style: italic; + margin-top: 8px; padding: 6px 10px; background: var(--bg-tertiary); + border-left: 2px solid var(--accent); border-radius: 4px; + font-size: 11px; color: var(--text-secondary); font-style: italic; } .popover-actions { padding: 8px 16px; display: flex; justify-content: flex-end; gap: 8px; } .btn-action { - background: none; border: 1px solid #27272a; border-radius: 4px; - padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; + background: none; border: 1px solid var(--border); border-radius: 4px; + padding: 4px 8px; color: var(--text-secondary); font-size: 11px; cursor: pointer; } - .btn-action:hover { background: #27272a; color: #e4e4e7; } + .btn-action:hover { background: var(--bg-hover); color: var(--text-primary); } .margin-selection-popup { position: fixed; display: flex; gap: 4px; padding: 6px; - background: #09090b; - border: 1px solid #27272a; + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 16px rgba(0,0,0,0.4); z-index: 2147483647; @@ -113,14 +146,14 @@ background: transparent; border: none; border-radius: 6px; - color: #e4e4e7; + color: var(--text-primary); font-size: 12px; font-weight: 500; cursor: pointer; transition: background 0.15s; } .selection-btn:hover { - background: #27272a; + background: var(--bg-hover); } .selection-btn svg { width: 14px; @@ -130,8 +163,8 @@ position: fixed; width: 340px; max-width: calc(100vw - 40px); - background: #09090b; - border: 1px solid #27272a; + background: var(--bg-card); + border: 1px solid var(--border); border-radius: 12px; padding: 16px; box-sizing: border-box; @@ -139,7 +172,7 @@ z-index: 2147483647; pointer-events: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - color: #e4e4e7; + color: var(--text-primary); animation: popover-in 0.15s forwards; overflow: hidden; } @@ -148,11 +181,11 @@ } .inline-compose-quote { padding: 8px 12px; - background: #18181b; - border-left: 3px solid #6366f1; + background: var(--bg-tertiary); + border-left: 3px solid var(--accent); border-radius: 4px; font-size: 12px; - color: #a1a1aa; + color: var(--text-secondary); font-style: italic; margin-bottom: 12px; max-height: 60px; @@ -163,10 +196,10 @@ width: 100%; min-height: 80px; padding: 10px 12px; - background: #18181b; - border: 1px solid #27272a; + background: var(--bg-elevated); + border: 1px solid var(--border); border-radius: 8px; - color: #e4e4e7; + color: var(--text-primary); font-family: inherit; font-size: 13px; resize: vertical; @@ -175,7 +208,7 @@ } .inline-compose-textarea:focus { outline: none; - border-color: #6366f1; + border-color: var(--accent); } .inline-compose-actions { display: flex; @@ -185,19 +218,19 @@ .btn-cancel { padding: 8px 16px; background: transparent; - border: 1px solid #27272a; + border: 1px solid var(--border); border-radius: 6px; - color: #a1a1aa; + color: var(--text-secondary); font-size: 13px; cursor: pointer; } .btn-cancel:hover { - background: #27272a; - color: #e4e4e7; + background: var(--bg-hover); + color: var(--text-primary); } .btn-submit { padding: 8px 16px; - background: #6366f1; + background: var(--accent); border: none; border-radius: 6px; color: white; @@ -206,26 +239,26 @@ cursor: pointer; } .btn-submit:hover { - background: #4f46e5; + background: var(--accent-hover); } .btn-submit:disabled { opacity: 0.5; cursor: not-allowed; } .reply-section { - border-top: 1px solid #27272a; + border-top: 1px solid var(--border); padding: 12px 16px; - background: #0f0f12; + background: var(--bg-secondary); border-radius: 0 0 12px 12px; } .reply-textarea { width: 100%; min-height: 60px; padding: 8px 10px; - background: #18181b; - border: 1px solid #27272a; + background: var(--bg-elevated); + border: 1px solid var(--border); border-radius: 6px; - color: #e4e4e7; + color: var(--text-primary); font-family: inherit; font-size: 12px; resize: none; @@ -233,11 +266,11 @@ } .reply-textarea:focus { outline: none; - border-color: #6366f1; + border-color: var(--accent); } .reply-submit { padding: 6px 12px; - background: #6366f1; + background: var(--accent); border: none; border-radius: 4px; color: white; @@ -251,7 +284,7 @@ } .reply-item { padding: 8px 0; - border-top: 1px solid #27272a; + border-top: 1px solid var(--border); } .reply-item:first-child { border-top: none; @@ -259,12 +292,12 @@ .reply-author { font-size: 11px; font-weight: 600; - color: #a1a1aa; + color: var(--text-secondary); margin-bottom: 4px; } .reply-text { font-size: 12px; - color: #e4e4e7; + color: var(--text-primary); line-height: 1.4; } `; @@ -422,6 +455,32 @@ } } + function applyTheme(theme) { + if (!sidebarHost) return; + sidebarHost.classList.remove("light", "dark"); + if (theme === "system" || !theme) { + if (window.matchMedia("(prefers-color-scheme: light)").matches) { + sidebarHost.classList.add("light"); + } + } else { + sidebarHost.classList.add(theme); + } + } + + window + .matchMedia("(prefers-color-scheme: light)") + .addEventListener("change", (e) => { + chrome.storage.local.get(["theme"], (result) => { + if (!result.theme || result.theme === "system") { + if (e.matches) { + sidebarHost?.classList.add("light"); + } else { + sidebarHost?.classList.remove("light"); + } + } + }); + }); + function initOverlay() { sidebarHost = document.createElement("div"); sidebarHost.id = "margin-overlay-host"; @@ -456,7 +515,8 @@ if (document.documentElement) observer.observe(document.documentElement); if (typeof chrome !== "undefined" && chrome.storage) { - chrome.storage.local.get(["showOverlay"], (result) => { + chrome.storage.local.get(["showOverlay", "theme"], (result) => { + applyTheme(result.theme); if (result.showOverlay === false) { sidebarHost.style.display = "none"; } else { @@ -469,6 +529,22 @@ document.addEventListener("mousemove", handleMouseMove); document.addEventListener("click", handleDocumentClick, true); + + chrome.storage.onChanged.addListener((changes, area) => { + if (area === "local") { + if (changes.theme) { + applyTheme(changes.theme.newValue); + } + if (changes.showOverlay) { + if (changes.showOverlay.newValue === false) { + sidebarHost.style.display = "none"; + } else { + sidebarHost.style.display = ""; + fetchAnnotations(); + } + } + } + }); } function showInlineComposeModal() { diff --git a/extension/popup/popup.css b/extension/popup/popup.css index 5b5e700..a134584 100644 --- a/extension/popup/popup.css +++ b/extension/popup/popup.css @@ -29,6 +29,79 @@ 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } +@media (prefers-color-scheme: light) { + :root { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-card: #ffffff; + --bg-elevated: #f4f4f5; + --bg-hover: #e4e4e7; + + --text-primary: #18181b; + --text-secondary: #52525b; + --text-tertiary: #71717a; + --border: #e4e4e7; + --border-hover: #d4d4d8; + + --accent: #4f46e5; + --accent-hover: #4338ca; + --accent-text: #4f46e5; + --accent-subtle: rgba(79, 70, 229, 0.1); + + --success: #059669; + --error: #dc2626; + --warning: #d97706; + } +} + +body.light { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-card: #ffffff; + --bg-elevated: #f4f4f5; + --bg-hover: #e4e4e7; + + --text-primary: #18181b; + --text-secondary: #52525b; + --text-tertiary: #71717a; + --border: #e4e4e7; + --border-hover: #d4d4d8; + + --accent: #4f46e5; + --accent-hover: #4338ca; + --accent-text: #4f46e5; + --accent-subtle: rgba(79, 70, 229, 0.1); + + --success: #059669; + --error: #dc2626; + --warning: #d97706; +} + +body.dark { + --bg-primary: #09090b; + --bg-secondary: #0f0f12; + --bg-tertiary: #18181b; + --bg-card: #09090b; + --bg-elevated: #18181b; + --bg-hover: #27272a; + + --text-primary: #e4e4e7; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + --border: #27272a; + --border-hover: #3f3f46; + + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-subtle: rgba(99, 102, 241, 0.1); + --accent-text: #818cf8; + --success: #10b981; + --error: #ef4444; + --warning: #f59e0b; +} + * { box-sizing: border-box; margin: 0; @@ -710,3 +783,35 @@ body { outline: none; border-color: var(--accent); } +.theme-toggle-group { + display: flex; + background: var(--bg-tertiary); + padding: 4px; + border-radius: var(--radius-md); + gap: 2px; + margin-top: 8px; +} + +.theme-btn { + flex: 1; + padding: 6px; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + font-weight: 500; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s ease; +} + +.theme-btn:hover { + color: var(--text-primary); + background: rgba(128, 128, 128, 0.1); +} + +.theme-btn.active { + background: var(--bg-card); + color: var(--text-primary); + box-shadow: var(--shadow-sm); +} diff --git a/extension/popup/popup.html b/extension/popup/popup.html index b344f51..3895207 100644 --- a/extension/popup/popup.html +++ b/extension/popup/popup.html @@ -247,6 +247,14 @@ />

Enter your backend URL

+
+ +
+ + + +
+
diff --git a/extension/popup/popup.js b/extension/popup/popup.js index 5d47e40..23bc6b3 100644 --- a/extension/popup/popup.js +++ b/extension/popup/popup.js @@ -40,6 +40,7 @@ document.addEventListener("DOMContentLoaded", async () => { collectionLoading: document.getElementById("collection-loading"), collectionsEmpty: document.getElementById("collections-empty"), overlayToggle: document.getElementById("overlay-toggle"), + themeBtns: document.querySelectorAll(".theme-btn"), }; let currentTab = null; @@ -48,7 +49,11 @@ document.addEventListener("DOMContentLoaded", async () => { let pendingSelector = null; // let _activeAnnotationUriForCollection = null; - const storage = await browserAPI.storage.local.get(["apiUrl", "showOverlay"]); + const storage = await browserAPI.storage.local.get([ + "apiUrl", + "showOverlay", + "theme", + ]); if (storage.apiUrl) { apiUrl = storage.apiUrl; } @@ -58,6 +63,10 @@ document.addEventListener("DOMContentLoaded", async () => { els.overlayToggle.checked = storage.showOverlay !== false; } + const currentTheme = storage.theme || "system"; + applyTheme(currentTheme); + updateThemeUI(currentTheme); + try { const [tab] = await browserAPI.tabs.query({ active: true, @@ -242,6 +251,15 @@ document.addEventListener("DOMContentLoaded", async () => { checkSession(); }); + els.themeBtns.forEach((btn) => { + btn.addEventListener("click", async () => { + const theme = btn.getAttribute("data-theme"); + await browserAPI.storage.local.set({ theme }); + applyTheme(theme); + updateThemeUI(theme); + }); + }); + els.closeCollectionSelector?.addEventListener("click", () => { views.collectionSelector.style.display = "none"; }); @@ -781,3 +799,20 @@ document.addEventListener("DOMContentLoaded", async () => { }); } }); + +function applyTheme(theme) { + document.body.classList.remove("light", "dark"); + if (theme === "system") return; + document.body.classList.add(theme); +} + +function updateThemeUI(theme) { + const btns = document.querySelectorAll(".theme-btn"); + btns.forEach((btn) => { + if (btn.getAttribute("data-theme") === theme) { + btn.classList.add("active"); + } else { + btn.classList.remove("active"); + } + }); +} diff --git a/extension/sidepanel/sidepanel.css b/extension/sidepanel/sidepanel.css index fb250e6..6c7c99f 100644 --- a/extension/sidepanel/sidepanel.css +++ b/extension/sidepanel/sidepanel.css @@ -31,6 +31,83 @@ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } +@media (prefers-color-scheme: light) { + :root { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-card: #ffffff; + --bg-hover: #e4e4e7; + --bg-elevated: #f4f4f5; + + --text-primary: #18181b; + --text-secondary: #52525b; + --text-tertiary: #71717a; + + --accent: #4f46e5; + --accent-hover: #4338ca; + --accent-subtle: rgba(79, 70, 229, 0.1); + --accent-text: #4f46e5; + + --border: #e4e4e7; + --border-hover: #d4d4d8; + + --success: #059669; + --error: #dc2626; + --warning: #d97706; + } +} + +body.light { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-card: #ffffff; + --bg-hover: #e4e4e7; + --bg-elevated: #f4f4f5; + + --text-primary: #18181b; + --text-secondary: #52525b; + --text-tertiary: #71717a; + + --accent: #4f46e5; + --accent-hover: #4338ca; + --accent-subtle: rgba(79, 70, 229, 0.1); + --accent-text: #4f46e5; + + --border: #e4e4e7; + --border-hover: #d4d4d8; + + --success: #059669; + --error: #dc2626; + --warning: #d97706; +} + +body.dark { + --bg-primary: #09090b; + --bg-secondary: #0f0f12; + --bg-tertiary: #18181b; + --bg-card: #09090b; + --bg-hover: #18181b; + --bg-elevated: #18181b; + + --text-primary: #e4e4e7; + --text-secondary: #a1a1aa; + --text-tertiary: #71717a; + + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-subtle: rgba(99, 102, 241, 0.1); + --accent-text: #818cf8; + + --border: #27272a; + --border-hover: #3f3f46; + + --success: #10b981; + --error: #ef4444; + --warning: #f59e0b; +} + * { margin: 0; padding: 0; @@ -929,3 +1006,35 @@ body { transform: translateX(20px); background-color: white; } +.theme-toggle-group { + display: flex; + background: var(--bg-tertiary); + padding: 4px; + border-radius: var(--radius-md); + gap: 2px; + margin-top: 8px; +} + +.theme-btn { + flex: 1; + padding: 6px; + border: none; + background: transparent; + color: var(--text-secondary); + font-size: 12px; + font-weight: 500; + border-radius: var(--radius-sm); + cursor: pointer; + transition: all 0.15s ease; +} + +.theme-btn:hover { + color: var(--text-primary); + background: rgba(128, 128, 128, 0.1); +} + +.theme-btn.active { + background: var(--bg-card); + color: var(--text-primary); + box-shadow: var(--shadow-sm); +} diff --git a/extension/sidepanel/sidepanel.html b/extension/sidepanel/sidepanel.html index c87f4f6..0e0ca68 100644 --- a/extension/sidepanel/sidepanel.html +++ b/extension/sidepanel/sidepanel.html @@ -279,6 +279,14 @@ />

Enter your Margin backend URL

+
+ +
+ + + +
+
diff --git a/extension/sidepanel/sidepanel.js b/extension/sidepanel/sidepanel.js index b02b436..2ca8d95 100644 --- a/extension/sidepanel/sidepanel.js +++ b/extension/sidepanel/sidepanel.js @@ -58,14 +58,36 @@ document.addEventListener("DOMContentLoaded", async () => { } chrome.storage.onChanged.addListener((changes, area) => { - if (area === "local" && changes.apiUrl) { - apiUrl = changes.apiUrl.newValue || ""; - - els.apiUrlInput.value = apiUrl; - checkSession(); + if (area === "local") { + if (changes.apiUrl) { + apiUrl = changes.apiUrl.newValue || ""; + els.apiUrlInput.value = apiUrl; + checkSession(); + } + if (changes.theme) { + const newTheme = changes.theme.newValue || "system"; + applyTheme(newTheme); + updateThemeUI(newTheme); + } } }); + chrome.storage.local.get(["theme"], (result) => { + const currentTheme = result.theme || "system"; + applyTheme(currentTheme); + updateThemeUI(currentTheme); + }); + + const themeBtns = document.querySelectorAll(".theme-btn"); + themeBtns.forEach((btn) => { + btn.addEventListener("click", () => { + const theme = btn.getAttribute("data-theme"); + chrome.storage.local.set({ theme }); + applyTheme(theme); + updateThemeUI(theme); + }); + }); + try { const [tab] = await chrome.tabs.query({ active: true, @@ -264,7 +286,10 @@ document.addEventListener("DOMContentLoaded", async () => { const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); const showOverlay = els.overlayToggle?.checked ?? true; - await chrome.storage.local.set({ apiUrl: newUrl, showOverlay }); + await chrome.storage.local.set({ + apiUrl: newUrl, + showOverlay, + }); if (newUrl) { apiUrl = newUrl; } @@ -911,4 +936,21 @@ document.addEventListener("DOMContentLoaded", async () => { }); }); } + + function applyTheme(theme) { + document.body.classList.remove("light", "dark"); + if (theme === "system") return; + document.body.classList.add(theme); + } + + function updateThemeUI(theme) { + const btns = document.querySelectorAll(".theme-btn"); + btns.forEach((btn) => { + if (btn.getAttribute("data-theme") === theme) { + btn.classList.add("active"); + } else { + btn.classList.remove("active"); + } + }); + } }); diff --git a/web/src/App.jsx b/web/src/App.jsx index 6db20ad..3670e73 100644 --- a/web/src/App.jsx +++ b/web/src/App.jsx @@ -18,6 +18,7 @@ import CollectionDetail from "./pages/CollectionDetail"; import Privacy from "./pages/Privacy"; import Terms from "./pages/Terms"; import ScrollToTop from "./components/ScrollToTop"; +import { ThemeProvider } from "./context/ThemeContext"; function AppContent() { const { user } = useAuth(); @@ -77,10 +78,12 @@ function AppContent() { export default function App() { return ( - - - } /> - - + + + + } /> + + + ); } diff --git a/web/src/components/RightSidebar.jsx b/web/src/components/RightSidebar.jsx index c10e8fc..9b6bcea 100644 --- a/web/src/components/RightSidebar.jsx +++ b/web/src/components/RightSidebar.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Link } from "react-router-dom"; -import { ExternalLink } from "lucide-react"; +import { ExternalLink, Sun, Moon, Monitor } from "lucide-react"; import { SiFirefox, SiGooglechrome, @@ -12,6 +12,7 @@ import { } from "react-icons/si"; import { FaEdge } from "react-icons/fa"; import { useAuth } from "../context/AuthContext"; +import { useTheme } from "../context/ThemeContext"; import { getTrendingTags } from "../api/client"; const isFirefox = @@ -58,6 +59,7 @@ function getExtensionInfo() { } export default function RightSidebar() { + const { theme, setTheme } = useTheme(); const { isAuthenticated } = useAuth(); const ext = getExtensionInfo(); const ExtIcon = ext.icon; @@ -196,9 +198,28 @@ export default function RightSidebar() {
- Privacy - · - Terms +
+ Privacy + · + Terms +
+
); diff --git a/web/src/context/ThemeContext.jsx b/web/src/context/ThemeContext.jsx new file mode 100644 index 0000000..44e1d8f --- /dev/null +++ b/web/src/context/ThemeContext.jsx @@ -0,0 +1,75 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +const ThemeContext = createContext({ + theme: "system", + setTheme: () => null, +}); + +export function ThemeProvider({ children }) { + const [theme, setTheme] = useState(() => { + return localStorage.getItem("theme") || "system"; + }); + + useEffect(() => { + localStorage.setItem("theme", theme); + + const root = window.document.documentElement; + root.classList.remove("light", "dark"); + + delete root.dataset.theme; + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + if (systemTheme === "light") { + root.dataset.theme = "light"; + } else { + root.dataset.theme = "dark"; + } + return; + } + + if (theme === "light") { + root.dataset.theme = "light"; + } + }, [theme]); + + useEffect(() => { + if (theme !== "system") return; + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleChange = () => { + const root = window.document.documentElement; + if (mediaQuery.matches) { + delete root.dataset.theme; + } else { + root.dataset.theme = "light"; + } + }; + + mediaQuery.addEventListener("change", handleChange); + return () => mediaQuery.removeEventListener("change", handleChange); + }, [theme]); + + const value = { + theme, + setTheme: (newTheme) => { + setTheme(newTheme); + }, + }; + + return ( + {children} + ); +} + +// eslint-disable-next-line react-refresh/only-export-components +export function useTheme() { + const context = useContext(ThemeContext); + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + return context; +} diff --git a/web/src/css/base.css b/web/src/css/base.css index 046cbfa..9e77eb6 100644 --- a/web/src/css/base.css +++ b/web/src/css/base.css @@ -32,6 +32,30 @@ "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; } +[data-theme="light"] { + --bg-primary: #ffffff; + --bg-secondary: #f4f4f5; + --bg-tertiary: #e4e4e7; + --bg-card: #ffffff; + --bg-elevated: #f4f4f5; + --text-primary: #18181b; + --text-secondary: #52525b; + --text-tertiary: #71717a; + --border: #e4e4e7; + --border-hover: #d4d4d8; + --accent: #4f46e5; + --accent-hover: #4338ca; + --accent-subtle: rgba(79, 70, 229, 0.1); + --accent-text: #4f46e5; + --success: #059669; + --error: #dc2626; + --warning: #d97706; + --info: #2563eb; + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --shadow-md: + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + * { margin: 0; padding: 0; diff --git a/web/src/css/layout.css b/web/src/css/layout.css index a4b2e71..dacb024 100644 --- a/web/src/css/layout.css +++ b/web/src/css/layout.css @@ -359,20 +359,48 @@ .right-footer { margin-top: auto; display: flex; - flex-wrap: wrap; - gap: 12px; - font-size: 0.75rem; + align-items: center; + justify-content: space-between; + padding-top: 16px; + border-top: 1px solid var(--border); +} + +.footer-links { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; color: var(--text-tertiary); } -.right-footer a { +.footer-links a { color: var(--text-tertiary); + text-decoration: none; } -.right-footer a:hover { +.footer-links a:hover { + text-decoration: underline; color: var(--text-secondary); } +.theme-toggle-mini { + background: none; + border: none; + cursor: pointer; + padding: 4px; + color: var(--text-tertiary); + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.theme-toggle-mini:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + .mobile-nav { display: none; position: fixed;