feat(nexus): design system phase 1 tokens and inter font
First phase of the DESIGN.md (ClickHouse-inspired) migration. Rewrites
the foundation CSS variables and theme machinery; downstream phases
(status/role dictionaries, raw utility sweep) still pending.
index.css
- Full rewrite of @theme inline block. Dark (.dark) and light (:root)
token sets per MIGRATION-PLAN sections 3 and 5:
* Dark: pure black canvas (#000000), Neon Volt primary (#faff69),
Forest Green secondary (#166534), charcoal border (rgba(65,65,65,
0.8)), near-black cards (#141414), silver muted (#a0a0a0).
* Light: near-white canvas (#fafafa), Forest primary, Volt
downgraded to dark olive (#4f5100) for border/active use only,
silver inverted to #6b6b6b. Accessibility fallback, not brand.
- Added --warning (#f59e0b / #b45309), --success, and direct brand
token refs (--volt, --volt-pale, --volt-border, --forest, --near-
black, --hover-gray, --silver, --charcoal-border) exposed as
Tailwind utilities via --color-* mirrors.
- Added --destructive: #ef4444 (#dc2626 in light).
- Radius scale collapsed to 4px sharp / 8px comfortable / 9999px pill.
- Deleted .theme-tokyo-night.dark block entirely (was dead code —
ThemeContext never applied the class).
- Rewrote hljs syntax highlighting: one dark block under .dark .hljs
using volt for keywords, pale volt for strings, silver for
comments; one light block under .hljs using forest/dark-olive/
silver. Replaced all three Catppuccin + Tokyo Night hljs rule sets.
- Rewrote scrollbar rules to use var(--muted) / var(--charcoal-
border) / var(--hover-gray) instead of hardcoded oklch values.
- Added @font-face declarations for Inter (normal + italic) from the
self-hosted woff2 files at /fonts/InterVariable*.woff2. font-weight
100-900 range unlocks weight 900 for DESIGN.md hero moments from
a single variable font.
- Set --font-sans to Inter-first stack; body rule pulls the token.
ThemeContext.tsx
- Simplified to binary Theme = "light" | "dark". Dropped "custom"
theme type, PaletteRole interface, ROLE_TO_TOKEN map, and the
/api/nexus/settings custom-theme hydration effect.
- applyTheme() now just toggles .dark on <html> and sets
colorScheme. applyCustomTheme() left as a deprecated no-op (no
external callers but keeping the export avoids churn).
- Legacy localStorage values (catppuccin-mocha, tokyo-night, custom,
catppuccin-latte) coerced to "dark" on read so existing users
don't see a crash after the migration.
- Default theme: "dark".
Layout.tsx
- Dropped THEME_META import and the THEME_CYCLE map. Theme toggle
is now a binary sun/moon flip via toggleTheme().
index.html
- Added <link rel="preload" href="/fonts/InterVariable.woff2"
as="font" type="font/woff2" crossorigin>.
- Set inline style="background:#000000; color-scheme:dark;" on
<html> so the pre-React paint is already dark — no white flash.
- Boot script coerces legacy localStorage theme values and persists
"light" or "dark" only.
ui/public/fonts/
- Added InterVariable.woff2 (344 KB) and InterVariable-Italic.woff2
(379 KB), both Inter v4.x from rsms.me/inter (the canonical
upstream). Self-hosted for LAN/offline reliability.
Not changed:
- lib/status-colors.ts, lib/agent-role-colors.ts — next phase
- Any component files — phase 3
- MIGRATION-PLAN.md — will be updated with resolved decisions later
Expected visual state: pages using theme tokens (bg-background,
text-muted-foreground, border-border, ~1,250 instances) immediately
render with the new palette. Pages using raw Tailwind utilities
(bg-red-500, text-amber-600, ~274 instances) still show old colors
until phase 3 sweep.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab45bc063d
commit
e49144a4e8
6 changed files with 283 additions and 311 deletions
|
|
@ -1,9 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<html lang="en" class="dark" style="background:#000000; color-scheme:dark;">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#1e1e2e" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
|
|
@ -18,23 +18,25 @@
|
|||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<!-- PAPERCLIP_FAVICON_END -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="preload" as="font" type="font/woff2" href="/fonts/InterVariable.woff2" crossorigin />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<script>
|
||||
(() => {
|
||||
const key = "paperclip.theme";
|
||||
const VALID = ["catppuccin-mocha", "tokyo-night", "catppuccin-latte"];
|
||||
const LEGACY_DARK = ["catppuccin-mocha", "tokyo-night", "custom"];
|
||||
try {
|
||||
const stored = window.localStorage.getItem(key);
|
||||
const theme = VALID.includes(stored) ? stored : "catppuccin-mocha";
|
||||
const isDark = theme !== "catppuccin-latte";
|
||||
let stored = window.localStorage.getItem(key);
|
||||
if (stored && stored !== "light" && stored !== "dark") {
|
||||
// Coerce legacy values. Latte was the old light default; everything else -> dark.
|
||||
stored = stored === "catppuccin-latte" ? "light" : LEGACY_DARK.includes(stored) ? "dark" : "dark";
|
||||
try { window.localStorage.setItem(key, stored); } catch {}
|
||||
}
|
||||
const theme = stored === "light" ? "light" : "dark";
|
||||
const isDark = theme === "dark";
|
||||
document.documentElement.classList.toggle("dark", isDark);
|
||||
document.documentElement.classList.toggle("theme-tokyo-night", theme === "tokyo-night");
|
||||
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
const bg = { "catppuccin-mocha": "#1e1e2e", "tokyo-night": "#1a1b26", "catppuccin-latte": "#eff1f5" };
|
||||
meta.setAttribute("content", bg[theme] || "#1e1e2e");
|
||||
}
|
||||
if (meta) meta.setAttribute("content", isDark ? "#000000" : "#fafafa");
|
||||
} catch {
|
||||
document.documentElement.classList.add("dark");
|
||||
document.documentElement.style.colorScheme = "dark";
|
||||
|
|
|
|||
BIN
ui/public/fonts/InterVariable-Italic.woff2
Normal file
BIN
ui/public/fonts/InterVariable-Italic.woff2
Normal file
Binary file not shown.
BIN
ui/public/fonts/InterVariable.woff2
Normal file
BIN
ui/public/fonts/InterVariable.woff2
Normal file
Binary file not shown.
|
|
@ -23,7 +23,7 @@ import { usePanel } from "../context/PanelContext";
|
|||
import { useChatPanel } from "../context/ChatPanelContext";
|
||||
import { useCompany } from "../context/CompanyContext";
|
||||
import { useSidebar } from "../context/SidebarContext";
|
||||
import { useTheme, THEME_META } from "../context/ThemeContext";
|
||||
import { useTheme } from "../context/ThemeContext";
|
||||
import { useKeyboardShortcuts } from "../hooks/useKeyboardShortcuts";
|
||||
import { useCompanyPageMemory } from "../hooks/useCompanyPageMemory";
|
||||
import { healthApi } from "../api/health";
|
||||
|
|
@ -64,12 +64,8 @@ export function Layout() {
|
|||
setSelectedCompanyId,
|
||||
} = useCompany();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const THEME_CYCLE: Record<string, string> = {
|
||||
"catppuccin-mocha": "Tokyo Night",
|
||||
"tokyo-night": "Catppuccin Latte",
|
||||
"catppuccin-latte": "Catppuccin Mocha",
|
||||
};
|
||||
const nextThemeLabel = THEME_CYCLE[theme] ?? "next theme";
|
||||
const isDarkTheme = theme === "dark";
|
||||
const nextThemeLabel = isDarkTheme ? "Light" : "Dark";
|
||||
const { companyPrefix } = useParams<{ companyPrefix: string }>();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
|
@ -78,7 +74,6 @@ export function Layout() {
|
|||
const lastMainScrollTop = useRef(0);
|
||||
const [mobileNavVisible, setMobileNavVisible] = useState(true);
|
||||
const [instanceSettingsTarget, setInstanceSettingsTarget] = useState<string>(() => readRememberedInstanceSettingsPath());
|
||||
const isDarkTheme = THEME_META[theme].dark;
|
||||
const matchedCompany = useMemo(() => {
|
||||
if (!companyPrefix) return null;
|
||||
const requestedPrefix = companyPrefix.toUpperCase();
|
||||
|
|
|
|||
|
|
@ -8,64 +8,52 @@ import {
|
|||
type ReactNode,
|
||||
} from "react";
|
||||
|
||||
export interface PaletteRole {
|
||||
name: string;
|
||||
dark: { oklch: string; hex: string; wcagAA: boolean };
|
||||
light: { oklch: string; hex: string; wcagAA: boolean };
|
||||
}
|
||||
|
||||
export type Theme = "light" | "dark" | "custom";
|
||||
export type Theme = "light" | "dark";
|
||||
|
||||
/** Metadata for each theme — used by Layout, MarkdownBody, InstanceGeneralSettings */
|
||||
export const THEME_META: Record<Theme, { label: string; dark: boolean; bg: string; primary: string }> = {
|
||||
dark: { label: "Dark", dark: true, bg: "#18181b", primary: "#a78bfa" },
|
||||
light: { label: "Light", dark: false, bg: "#ffffff", primary: "#7c3aed" },
|
||||
custom: { label: "Custom", dark: true, bg: "#18181b", primary: "#a78bfa" },
|
||||
dark: { label: "Dark", dark: true, bg: "#000000", primary: "#faff69" },
|
||||
light: { label: "Light", dark: false, bg: "#fafafa", primary: "#166534" },
|
||||
};
|
||||
|
||||
/** Ordered list of themes for display in theme selector UI */
|
||||
export const ORDERED_THEMES: Theme[] = ["dark", "light", "custom"];
|
||||
export const ORDERED_THEMES: Theme[] = ["dark", "light"];
|
||||
|
||||
interface ThemeContextValue {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
toggleTheme: () => void;
|
||||
applyCustomTheme: (palette: PaletteRole[], variant: "dark" | "light") => void;
|
||||
/** @deprecated Custom themes are no longer supported. No-op retained for backwards compat. */
|
||||
applyCustomTheme: (...args: unknown[]) => void;
|
||||
}
|
||||
|
||||
const THEME_STORAGE_KEY = "paperclip.theme";
|
||||
const DARK_THEME_COLOR = "#18181b";
|
||||
const LIGHT_THEME_COLOR = "#ffffff";
|
||||
const DARK_THEME_COLOR = "#000000";
|
||||
const LIGHT_THEME_COLOR = "#fafafa";
|
||||
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
|
||||
|
||||
const ROLE_TO_TOKEN: Record<string, string> = {
|
||||
background: "--background",
|
||||
surface: "--card",
|
||||
overlay: "--secondary",
|
||||
text: "--foreground",
|
||||
"accent-1": "--primary",
|
||||
"accent-2": "--accent",
|
||||
"accent-3": "--muted",
|
||||
};
|
||||
function coerceStoredTheme(raw: string | null): Theme {
|
||||
if (raw === "light") return "light";
|
||||
if (raw === "dark") return "dark";
|
||||
// Legacy values: "catppuccin-latte" was the old light; anything else defaulted to dark.
|
||||
if (raw === "catppuccin-latte") return "light";
|
||||
return "dark";
|
||||
}
|
||||
|
||||
function resolveInitialTheme(): Theme {
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (stored === "dark" || stored === "light" || stored === "custom") return stored;
|
||||
if (stored !== null) return coerceStoredTheme(stored);
|
||||
}
|
||||
if (typeof document !== "undefined") {
|
||||
return document.documentElement.classList.contains("dark") ? "dark" : "light";
|
||||
return document.documentElement.classList.contains("dark") ? "dark" : "dark";
|
||||
}
|
||||
return "dark";
|
||||
}
|
||||
|
||||
function isDarkTheme(theme: Theme): boolean {
|
||||
return theme === "dark" || theme === "custom";
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
if (typeof document === "undefined") return;
|
||||
const dark = isDarkTheme(theme);
|
||||
const dark = theme === "dark";
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle("dark", dark);
|
||||
root.style.colorScheme = dark ? "dark" : "light";
|
||||
|
|
@ -78,61 +66,18 @@ function applyTheme(theme: Theme) {
|
|||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setThemeState] = useState<Theme>(() => resolveInitialTheme());
|
||||
|
||||
// On mount: if stored theme is "custom", fetch nexus settings to restore customTheme palette
|
||||
useEffect(() => {
|
||||
if (theme !== "custom") return;
|
||||
if (typeof document === "undefined") return;
|
||||
fetch("/api/nexus/settings", { credentials: "include" })
|
||||
.then((res) => res.ok ? res.json() : null)
|
||||
.then((data: { customTheme?: { palette?: PaletteRole[] } } | null) => {
|
||||
const palette = data?.customTheme?.palette;
|
||||
if (!palette || !Array.isArray(palette)) return;
|
||||
const root = document.documentElement;
|
||||
palette.forEach((role) => {
|
||||
const tokenName = ROLE_TO_TOKEN[role.name];
|
||||
if (!tokenName) return;
|
||||
// Restore dark variant by default for custom theme
|
||||
root.style.setProperty(tokenName, role.dark.hex);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// silently ignore - custom theme tokens will simply not be restored on this mount
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // only on mount
|
||||
|
||||
const setTheme = useCallback((nextTheme: Theme) => {
|
||||
setThemeState(nextTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = useCallback(() => {
|
||||
setThemeState((current) => {
|
||||
if (isDarkTheme(current)) return "light";
|
||||
return "dark";
|
||||
});
|
||||
setThemeState((current) => (current === "dark" ? "light" : "dark"));
|
||||
}, []);
|
||||
|
||||
const applyCustomTheme = useCallback(
|
||||
(palette: PaletteRole[], paletteVariant: "dark" | "light") => {
|
||||
if (typeof document === "undefined") return;
|
||||
const root = document.documentElement;
|
||||
|
||||
palette.forEach((role) => {
|
||||
const tokenName = ROLE_TO_TOKEN[role.name];
|
||||
if (!tokenName) return;
|
||||
const value = paletteVariant === "dark" ? role.dark.hex : role.light.hex;
|
||||
root.style.setProperty(tokenName, value);
|
||||
});
|
||||
|
||||
setThemeState("custom");
|
||||
try {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, "custom");
|
||||
} catch (_e) {
|
||||
// ignore write failures
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
// Deprecated no-op kept so any stale imports don't break the build.
|
||||
const applyCustomTheme = useCallback((..._args: unknown[]) => {
|
||||
void _args;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
applyTheme(theme);
|
||||
|
|
|
|||
458
ui/src/index.css
458
ui/src/index.css
|
|
@ -3,6 +3,24 @@
|
|||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* -- Fonts ------------------------------------------------------------- */
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/fonts/InterVariable.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/fonts/InterVariable-Italic.woff2") format("woff2");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
|
|
@ -20,6 +38,10 @@
|
|||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-warning: var(--warning);
|
||||
--color-warning-foreground: var(--warning-foreground);
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
|
|
@ -36,178 +58,199 @@
|
|||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0px;
|
||||
--radius-xl: 0px;
|
||||
--color-volt: var(--volt);
|
||||
--color-volt-pale: var(--volt-pale);
|
||||
--color-volt-border: var(--volt-border);
|
||||
--color-forest: var(--forest);
|
||||
--color-forest-dark: var(--forest-dark);
|
||||
--color-near-black: var(--near-black);
|
||||
--color-hover-gray: var(--hover-gray);
|
||||
--color-silver: var(--silver);
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 4px;
|
||||
--radius-lg: 8px;
|
||||
--radius-xl: 8px;
|
||||
--radius-full: 9999px;
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
--font-mono: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||
--font-display: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* -- Light mode (default :root) ---------------------------------------- */
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--radius: 0;
|
||||
--background: #eff1f5;
|
||||
--foreground: #4c4f69;
|
||||
--card: #e6e9ef;
|
||||
--card-foreground: #4c4f69;
|
||||
--popover: #e6e9ef;
|
||||
--popover-foreground: #4c4f69;
|
||||
--primary: #1e66f5;
|
||||
--primary-foreground: #eff1f5;
|
||||
--secondary: #ccd0da;
|
||||
--secondary-foreground: #4c4f69;
|
||||
--muted: #ccd0da;
|
||||
--muted-foreground: #9ca0b0;
|
||||
--accent: #bcc0cc;
|
||||
--accent-foreground: #4c4f69;
|
||||
--destructive: #d20f39;
|
||||
--destructive-foreground: #eff1f5;
|
||||
--border: #ccd0da;
|
||||
--input: #ccd0da;
|
||||
--ring: #1e66f5;
|
||||
--chart-1: #1e66f5;
|
||||
--chart-2: #40a02b;
|
||||
--chart-3: #8839ef;
|
||||
--chart-4: #df8e1d;
|
||||
--chart-5: #d20f39;
|
||||
--sidebar: #e6e9ef;
|
||||
--sidebar-foreground: #4c4f69;
|
||||
--sidebar-primary: #1e66f5;
|
||||
--sidebar-primary-foreground: #eff1f5;
|
||||
--sidebar-accent: #ccd0da;
|
||||
--sidebar-accent-foreground: #4c4f69;
|
||||
--sidebar-border: #ccd0da;
|
||||
--sidebar-ring: #1e66f5;
|
||||
--radius: 8px;
|
||||
--background: #fafafa;
|
||||
--foreground: #0a0a0a;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #0a0a0a;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #0a0a0a;
|
||||
--primary: #166534;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f1f5f1;
|
||||
--secondary-foreground: #0a0a0a;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #6b6b6b;
|
||||
--accent: #fafff0;
|
||||
--accent-foreground: #4f5100;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #ffffff;
|
||||
--warning: #b45309;
|
||||
--warning-foreground: #ffffff;
|
||||
--success: #166534;
|
||||
--success-foreground: #ffffff;
|
||||
--border: rgba(20, 20, 20, 0.12);
|
||||
--input: #ffffff;
|
||||
--ring: #166534;
|
||||
--chart-1: #166534;
|
||||
--chart-2: #4f5100;
|
||||
--chart-3: #6b6b6b;
|
||||
--chart-4: #8a8c00;
|
||||
--chart-5: #dc2626;
|
||||
--sidebar: #ffffff;
|
||||
--sidebar-foreground: #6b6b6b;
|
||||
--sidebar-primary: #166534;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #f4f4f5;
|
||||
--sidebar-accent-foreground: #0a0a0a;
|
||||
--sidebar-border: rgba(20, 20, 20, 0.12);
|
||||
--sidebar-ring: #166534;
|
||||
/* Brand direct refs */
|
||||
--volt: #4f5100;
|
||||
--volt-pale: #8a8c00;
|
||||
--volt-border: #4f5100;
|
||||
--forest: #166534;
|
||||
--forest-dark: #14572f;
|
||||
--near-black: #0a0a0a;
|
||||
--hover-gray: #e4e4e7;
|
||||
--silver: #6b6b6b;
|
||||
--charcoal-border: rgba(20, 20, 20, 0.12);
|
||||
--charcoal-divider: rgba(20, 20, 20, 0.08);
|
||||
}
|
||||
|
||||
/* -- Dark mode --------------------------------------------------------- */
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--background: #1e1e2e;
|
||||
--foreground: #cdd6f4;
|
||||
--card: #181825;
|
||||
--card-foreground: #cdd6f4;
|
||||
--popover: #181825;
|
||||
--popover-foreground: #cdd6f4;
|
||||
--primary: #89b4fa;
|
||||
--primary-foreground: #1e1e2e;
|
||||
--secondary: #313244;
|
||||
--secondary-foreground: #cdd6f4;
|
||||
--muted: #313244;
|
||||
--muted-foreground: #6c7086;
|
||||
--accent: #45475a;
|
||||
--accent-foreground: #cdd6f4;
|
||||
--destructive: #f38ba8;
|
||||
--destructive-foreground: #1e1e2e;
|
||||
--border: #313244;
|
||||
--input: #313244;
|
||||
--ring: #89b4fa;
|
||||
--chart-1: #89b4fa;
|
||||
--chart-2: #a6e3a1;
|
||||
--chart-3: #cba6f7;
|
||||
--chart-4: #f9e2af;
|
||||
--chart-5: #f38ba8;
|
||||
--sidebar: #181825;
|
||||
--sidebar-foreground: #cdd6f4;
|
||||
--sidebar-primary: #89b4fa;
|
||||
--sidebar-primary-foreground: #1e1e2e;
|
||||
--sidebar-accent: #313244;
|
||||
--sidebar-accent-foreground: #cdd6f4;
|
||||
--sidebar-border: #313244;
|
||||
--sidebar-ring: #89b4fa;
|
||||
}
|
||||
|
||||
.theme-tokyo-night.dark {
|
||||
--background: #1a1b26;
|
||||
--foreground: #c0caf5;
|
||||
--card: #16161e;
|
||||
--card-foreground: #c0caf5;
|
||||
--popover: #16161e;
|
||||
--popover-foreground: #c0caf5;
|
||||
--primary: #7aa2f7;
|
||||
--primary-foreground: #1a1b26;
|
||||
--secondary: #292e42;
|
||||
--secondary-foreground: #c0caf5;
|
||||
--muted: #292e42;
|
||||
--muted-foreground: #565f89;
|
||||
--accent: #3b4261;
|
||||
--accent-foreground: #c0caf5;
|
||||
--destructive: #f7768e;
|
||||
--destructive-foreground: #1a1b26;
|
||||
--border: #292e42;
|
||||
--input: #292e42;
|
||||
--ring: #7aa2f7;
|
||||
--chart-1: #7aa2f7;
|
||||
--chart-2: #9ece6a;
|
||||
--chart-3: #bb9af7;
|
||||
--chart-4: #e0af68;
|
||||
--chart-5: #f7768e;
|
||||
--sidebar: #16161e;
|
||||
--sidebar-foreground: #c0caf5;
|
||||
--sidebar-primary: #7aa2f7;
|
||||
--sidebar-primary-foreground: #1a1b26;
|
||||
--sidebar-accent: #292e42;
|
||||
--sidebar-accent-foreground: #c0caf5;
|
||||
--sidebar-border: #292e42;
|
||||
--sidebar-ring: #7aa2f7;
|
||||
--radius: 8px;
|
||||
--background: #000000;
|
||||
--foreground: #ffffff;
|
||||
--card: #141414;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #141414;
|
||||
--popover-foreground: #ffffff;
|
||||
--primary: #faff69;
|
||||
--primary-foreground: #151515;
|
||||
--secondary: #166534;
|
||||
--secondary-foreground: #ffffff;
|
||||
--muted: #141414;
|
||||
--muted-foreground: #a0a0a0;
|
||||
--accent: #141414;
|
||||
--accent-foreground: #faff69;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #ffffff;
|
||||
--warning: #f59e0b;
|
||||
--warning-foreground: #151515;
|
||||
--success: #166534;
|
||||
--success-foreground: #ffffff;
|
||||
--border: rgba(65, 65, 65, 0.8);
|
||||
--input: #141414;
|
||||
--ring: #faff69;
|
||||
--chart-1: #faff69;
|
||||
--chart-2: #166534;
|
||||
--chart-3: #a0a0a0;
|
||||
--chart-4: #f4f692;
|
||||
--chart-5: #ef4444;
|
||||
--sidebar: #000000;
|
||||
--sidebar-foreground: #a0a0a0;
|
||||
--sidebar-primary: #faff69;
|
||||
--sidebar-primary-foreground: #151515;
|
||||
--sidebar-accent: #141414;
|
||||
--sidebar-accent-foreground: #ffffff;
|
||||
--sidebar-border: rgba(65, 65, 65, 0.8);
|
||||
--sidebar-ring: #faff69;
|
||||
/* Brand direct refs */
|
||||
--volt: #faff69;
|
||||
--volt-pale: #f4f692;
|
||||
--volt-border: #4f5100;
|
||||
--forest: #166534;
|
||||
--forest-dark: #14572f;
|
||||
--near-black: #141414;
|
||||
--hover-gray: #3a3a3a;
|
||||
--silver: #a0a0a0;
|
||||
--charcoal-border: rgba(65, 65, 65, 0.8);
|
||||
--charcoal-divider: #343434;
|
||||
}
|
||||
|
||||
/* -- highlight.js syntax theme overrides (chat code blocks) ------------- */
|
||||
|
||||
/* Base hljs reset -- ensure code blocks use our themed variables */
|
||||
/* Base hljs surface — themed via CSS vars */
|
||||
.hljs {
|
||||
background: var(--code-block-bg, hsl(var(--card))) !important;
|
||||
color: var(--code-block-fg, hsl(var(--card-foreground))) !important;
|
||||
background: var(--card) !important;
|
||||
color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
/* Catppuccin Mocha (default dark) */
|
||||
.dark .hljs { --code-block-bg: #1e1e2e; --code-block-fg: #cdd6f4; }
|
||||
.dark .hljs-keyword { color: #cba6f7; }
|
||||
.dark .hljs-string { color: #a6e3a1; }
|
||||
.dark .hljs-number { color: #fab387; }
|
||||
.dark .hljs-comment { color: #6c7086; font-style: italic; }
|
||||
.dark .hljs-function { color: #89b4fa; }
|
||||
.dark .hljs-title { color: #89b4fa; }
|
||||
.dark .hljs-built_in { color: #f38ba8; }
|
||||
.dark .hljs-type { color: #f9e2af; }
|
||||
.dark .hljs-attr { color: #89dceb; }
|
||||
.dark .hljs-variable { color: #cdd6f4; }
|
||||
.dark .hljs-literal { color: #fab387; }
|
||||
.dark .hljs-meta { color: #f5e0dc; }
|
||||
.dark .hljs-selector-class { color: #89dceb; }
|
||||
.dark .hljs-selector-tag { color: #cba6f7; }
|
||||
/* Light mode hljs (default) */
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-literal,
|
||||
.hljs-number {
|
||||
color: #166534;
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-attr {
|
||||
color: #4f5100;
|
||||
}
|
||||
.hljs-function,
|
||||
.hljs-title,
|
||||
.hljs-built_in,
|
||||
.hljs-variable,
|
||||
.hljs-type,
|
||||
.hljs-meta {
|
||||
color: #0a0a0a;
|
||||
}
|
||||
.hljs-comment {
|
||||
color: #6b6b6b;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-deletion,
|
||||
.hljs-selector-class {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Tokyo Night */
|
||||
.theme-tokyo-night .hljs { --code-block-bg: #1a1b26; --code-block-fg: #a9b1d6; }
|
||||
.theme-tokyo-night .hljs-keyword { color: #bb9af7; }
|
||||
.theme-tokyo-night .hljs-string { color: #9ece6a; }
|
||||
.theme-tokyo-night .hljs-number { color: #ff9e64; }
|
||||
.theme-tokyo-night .hljs-comment { color: #565f89; font-style: italic; }
|
||||
.theme-tokyo-night .hljs-function { color: #7aa2f7; }
|
||||
.theme-tokyo-night .hljs-title { color: #7aa2f7; }
|
||||
.theme-tokyo-night .hljs-built_in { color: #f7768e; }
|
||||
.theme-tokyo-night .hljs-type { color: #e0af68; }
|
||||
.theme-tokyo-night .hljs-attr { color: #73daca; }
|
||||
.theme-tokyo-night .hljs-variable { color: #a9b1d6; }
|
||||
.theme-tokyo-night .hljs-literal { color: #ff9e64; }
|
||||
.theme-tokyo-night .hljs-meta { color: #c0caf5; }
|
||||
.theme-tokyo-night .hljs-selector-class { color: #73daca; }
|
||||
.theme-tokyo-night .hljs-selector-tag { color: #bb9af7; }
|
||||
|
||||
/* Catppuccin Latte (light) */
|
||||
:root .hljs { --code-block-bg: #eff1f5; --code-block-fg: #4c4f69; }
|
||||
:root .hljs-keyword { color: #8839ef; }
|
||||
:root .hljs-string { color: #40a02b; }
|
||||
:root .hljs-number { color: #fe640b; }
|
||||
:root .hljs-comment { color: #9ca0b0; font-style: italic; }
|
||||
:root .hljs-function { color: #1e66f5; }
|
||||
:root .hljs-title { color: #1e66f5; }
|
||||
:root .hljs-built_in { color: #d20f39; }
|
||||
:root .hljs-type { color: #df8e1d; }
|
||||
:root .hljs-attr { color: #179299; }
|
||||
:root .hljs-variable { color: #4c4f69; }
|
||||
:root .hljs-literal { color: #fe640b; }
|
||||
:root .hljs-meta { color: #dc8a78; }
|
||||
:root .hljs-selector-class { color: #179299; }
|
||||
:root .hljs-selector-tag { color: #8839ef; }
|
||||
/* Dark mode hljs — overrides the light defaults */
|
||||
.dark .hljs {
|
||||
background: #141414 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
.dark .hljs-keyword,
|
||||
.dark .hljs-selector-tag,
|
||||
.dark .hljs-literal,
|
||||
.dark .hljs-number,
|
||||
.dark .hljs-meta {
|
||||
color: #faff69;
|
||||
}
|
||||
.dark .hljs-string,
|
||||
.dark .hljs-attr,
|
||||
.dark .hljs-type {
|
||||
color: #f4f692;
|
||||
}
|
||||
.dark .hljs-function,
|
||||
.dark .hljs-title,
|
||||
.dark .hljs-built_in,
|
||||
.dark .hljs-variable,
|
||||
.dark .hljs-selector-class {
|
||||
color: #ffffff;
|
||||
}
|
||||
.dark .hljs-comment {
|
||||
color: #a0a0a0;
|
||||
font-style: italic;
|
||||
}
|
||||
.dark .hljs-deletion {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
|
|
@ -219,6 +262,7 @@
|
|||
}
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
font-family: var(--font-sans);
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -259,23 +303,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Dark mode scrollbars */
|
||||
.dark *::-webkit-scrollbar {
|
||||
/* Scrollbars — themed via CSS vars so they follow light/dark. */
|
||||
*::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.dark *::-webkit-scrollbar-track {
|
||||
background: oklch(0.205 0 0);
|
||||
*::-webkit-scrollbar-track {
|
||||
background: var(--muted);
|
||||
}
|
||||
|
||||
.dark *::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.4 0 0);
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background: var(--charcoal-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dark *::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.5 0 0);
|
||||
*::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--hover-gray);
|
||||
}
|
||||
|
||||
/* Auto-hide scrollbar: always reserves space, thumb visible only on hover */
|
||||
|
|
@ -289,25 +333,14 @@
|
|||
.scrollbar-auto-hide::-webkit-scrollbar-thumb {
|
||||
background: transparent !important;
|
||||
}
|
||||
/* Light mode scrollbar on hover */
|
||||
.scrollbar-auto-hide:hover::-webkit-scrollbar-track {
|
||||
background: oklch(0.92 0 0) !important;
|
||||
background: var(--muted) !important;
|
||||
}
|
||||
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.7 0 0) !important;
|
||||
background: var(--charcoal-border) !important;
|
||||
}
|
||||
.scrollbar-auto-hide:hover::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.6 0 0) !important;
|
||||
}
|
||||
/* Dark mode scrollbar on hover */
|
||||
.dark .scrollbar-auto-hide:hover::-webkit-scrollbar-track {
|
||||
background: oklch(0.205 0 0) !important;
|
||||
}
|
||||
.dark .scrollbar-auto-hide:hover::-webkit-scrollbar-thumb {
|
||||
background: oklch(0.4 0 0) !important;
|
||||
}
|
||||
.dark .scrollbar-auto-hide:hover::-webkit-scrollbar-thumb:hover {
|
||||
background: oklch(0.5 0 0) !important;
|
||||
background: var(--hover-gray) !important;
|
||||
}
|
||||
|
||||
/* Expandable dialog transition for max-width changes */
|
||||
|
|
@ -449,14 +482,14 @@
|
|||
}
|
||||
|
||||
.paperclip-mdxeditor-content a:not(.paperclip-mention-chip):not(.paperclip-project-mention-chip) {
|
||||
color: color-mix(in oklab, var(--foreground) 76%, #0969da 24%);
|
||||
color: color-mix(in oklab, var(--foreground) 76%, var(--primary) 24%);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dark .paperclip-mdxeditor-content a:not(.paperclip-mention-chip):not(.paperclip-project-mention-chip) {
|
||||
color: color-mix(in oklab, var(--foreground) 80%, #58a6ff 20%);
|
||||
color: var(--volt);
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor-content a.paperclip-mention-chip,
|
||||
|
|
@ -564,57 +597,56 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
}
|
||||
|
||||
.paperclip-mdxeditor-content code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor-content pre {
|
||||
margin: 0.4rem 0;
|
||||
padding: 0;
|
||||
border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: calc(var(--radius) - 3px);
|
||||
background: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
background: var(--card);
|
||||
color: var(--card-foreground);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* Dark theme for CodeMirror code blocks inside the MDXEditor.
|
||||
Overrides the default cm6-theme-basic-light that MDXEditor bundles. */
|
||||
/* CodeMirror code blocks inside the MDXEditor — themed via CSS vars. */
|
||||
.paperclip-mdxeditor .cm-editor {
|
||||
background-color: #1e1e2e !important;
|
||||
color: #cdd6f4 !important;
|
||||
background-color: var(--card) !important;
|
||||
color: var(--card-foreground) !important;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-gutters {
|
||||
background-color: #181825 !important;
|
||||
color: #585b70 !important;
|
||||
border-right: 1px solid #313244 !important;
|
||||
background-color: var(--muted) !important;
|
||||
color: var(--muted-foreground) !important;
|
||||
border-right: 1px solid var(--border) !important;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-activeLineGutter {
|
||||
background-color: #1e1e2e !important;
|
||||
background-color: var(--card) !important;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-activeLine {
|
||||
background-color: color-mix(in oklab, #cdd6f4 5%, transparent) !important;
|
||||
background-color: color-mix(in oklab, var(--foreground) 5%, transparent) !important;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-cursor,
|
||||
.paperclip-mdxeditor .cm-dropCursor {
|
||||
border-left-color: #cdd6f4 !important;
|
||||
border-left-color: var(--foreground) !important;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-selectionBackground {
|
||||
background-color: color-mix(in oklab, #89b4fa 25%, transparent) !important;
|
||||
background-color: color-mix(in oklab, var(--primary) 25%, transparent) !important;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-focused .cm-selectionBackground {
|
||||
background-color: color-mix(in oklab, #89b4fa 30%, transparent) !important;
|
||||
background-color: color-mix(in oklab, var(--primary) 30%, transparent) !important;
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor .cm-content {
|
||||
caret-color: #cdd6f4;
|
||||
caret-color: var(--foreground);
|
||||
}
|
||||
|
||||
/* MDXEditor code block language selector – show on hover only */
|
||||
|
|
@ -634,9 +666,9 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
|
||||
.paperclip-mdxeditor-content [class*="_codeMirrorToolbar_"] select,
|
||||
.paperclip-mdxeditor-content [class*="_codeBlockToolbar_"] select {
|
||||
background-color: #313244;
|
||||
color: #cdd6f4;
|
||||
border-color: #45475a;
|
||||
background-color: var(--muted);
|
||||
color: var(--foreground);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.paperclip-mdxeditor-content [class*="_codeMirrorWrapper_"]:hover [class*="_codeMirrorToolbar_"],
|
||||
|
|
@ -646,21 +678,19 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Rendered markdown code blocks & inline code (prose/MarkdownBody context).
|
||||
Dark theme code blocks with compact sizing.
|
||||
Override prose CSS variables so prose-invert can't revert to defaults. */
|
||||
/* Rendered markdown code blocks & inline code (prose/MarkdownBody context). */
|
||||
.paperclip-markdown {
|
||||
--tw-prose-pre-bg: #1e1e2e;
|
||||
--tw-prose-pre-code: #cdd6f4;
|
||||
--tw-prose-invert-pre-bg: #1e1e2e;
|
||||
--tw-prose-invert-pre-code: #cdd6f4;
|
||||
--tw-prose-pre-bg: var(--card);
|
||||
--tw-prose-pre-code: var(--card-foreground);
|
||||
--tw-prose-invert-pre-bg: var(--card);
|
||||
--tw-prose-invert-pre-code: var(--card-foreground);
|
||||
}
|
||||
|
||||
.paperclip-markdown pre {
|
||||
border: 1px solid color-mix(in oklab, var(--foreground) 12%, transparent) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: calc(var(--radius) - 3px) !important;
|
||||
background-color: #1e1e2e !important;
|
||||
color: #cdd6f4 !important;
|
||||
background-color: var(--card) !important;
|
||||
color: var(--card-foreground) !important;
|
||||
padding: 0.5rem 0.65rem !important;
|
||||
margin: 0.4rem 0 !important;
|
||||
font-size: 1em !important;
|
||||
|
|
@ -669,7 +699,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
}
|
||||
|
||||
.paperclip-markdown code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
|
@ -694,7 +724,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
}
|
||||
|
||||
.dark .prose :not(pre) > code {
|
||||
background-color: #ffffff0f;
|
||||
background-color: color-mix(in oklab, var(--foreground) 6%, transparent);
|
||||
}
|
||||
|
||||
.paperclip-markdown {
|
||||
|
|
@ -776,7 +806,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
}
|
||||
|
||||
.paperclip-markdown a {
|
||||
color: color-mix(in oklab, var(--foreground) 76%, #0969da 24%);
|
||||
color: color-mix(in oklab, var(--foreground) 76%, var(--primary) 24%);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.15em;
|
||||
cursor: pointer;
|
||||
|
|
@ -787,7 +817,7 @@ a.paperclip-mention-chip[data-mention-kind="agent"]::before {
|
|||
}
|
||||
|
||||
.dark .paperclip-markdown a {
|
||||
color: color-mix(in oklab, var(--foreground) 80%, #58a6ff 20%);
|
||||
color: var(--volt);
|
||||
}
|
||||
|
||||
.paperclip-markdown blockquote {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue