initial colors and theme improvements
This commit is contained in:
@@ -203,6 +203,19 @@ tasks:
|
||||
cmds:
|
||||
- task: lint:eslint
|
||||
- task: lint:dpdm
|
||||
- task: lint:colors
|
||||
|
||||
lint:colors:
|
||||
desc: "Enforce theme tokens — colours in core/theme route through the palette"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/lint/theme-lint.mjs
|
||||
|
||||
contrast:
|
||||
desc: "Report low-contrast theme token pairs (warning only, never blocks)"
|
||||
deps: [install]
|
||||
cmds:
|
||||
- node editor/scripts/lint/theme-lint.mjs contrast
|
||||
|
||||
lint:eslint:
|
||||
desc: "Run ESLint linting"
|
||||
|
||||
@@ -155,6 +155,8 @@ The project structure is defined in `engine/pyproject.toml`. Any new dependencie
|
||||
|
||||
For a broader explanation of the frontend layering and override architecture, read @frontend/editor/DeveloperGuide.md
|
||||
|
||||
Before touching colours or theming (tokens, dark mode, accent colours), read @frontend/editor/src/core/theme/README.md — it explains the palette/`--c-*` token system and the rule that literal colours live only in `primitives.css`.
|
||||
|
||||
```typescript
|
||||
// ✅ CORRECT - Use @app/* for all imports
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Custom toolbar control: an accent-colour swatch picker next to the theme
|
||||
// toggle. Picking a colour sets the accent for the CURRENT mode (light/dark) —
|
||||
// it writes the `accentLight` / `accentDark` global, which preview.tsx feeds
|
||||
// through deriveAccessiblePrimary to tint the canvas exactly like the editor.
|
||||
import React from "react";
|
||||
import { addons, types, useGlobals } from "storybook/manager-api";
|
||||
import { IconButton, WithTooltip } from "storybook/internal/components";
|
||||
// Single source of truth for the 3×5 accent grid. Imported by relative path
|
||||
// (not @core) because the Storybook manager bundle doesn't resolve app aliases;
|
||||
// constants/theme.ts has no imports of its own, so this stays cheap to bundle.
|
||||
import { THEME_ACCENT_PRESETS as PRESETS } from "../editor/src/core/constants/theme";
|
||||
|
||||
function AccentPicker() {
|
||||
const [globals, updateGlobals] = useGlobals();
|
||||
const scheme = globals.theme === "dark" ? "dark" : "light";
|
||||
const key = scheme === "dark" ? "accentDark" : "accentLight";
|
||||
const current = (globals[key] as string) || "#3b82f6";
|
||||
|
||||
return (
|
||||
<WithTooltip
|
||||
placement="bottom"
|
||||
trigger="click"
|
||||
closeOnOutsideClick
|
||||
tooltip={({ onHide }: { onHide: () => void }) => (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(5, 22px)",
|
||||
gap: 6,
|
||||
padding: 10,
|
||||
}}
|
||||
>
|
||||
{PRESETS.map((color) => {
|
||||
const selected = color.toLowerCase() === current.toLowerCase();
|
||||
return (
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
title={color}
|
||||
aria-label={color}
|
||||
onClick={() => {
|
||||
updateGlobals({ [key]: color });
|
||||
onHide();
|
||||
}}
|
||||
style={{
|
||||
width: 22,
|
||||
height: 22,
|
||||
padding: 0,
|
||||
borderRadius: 5,
|
||||
background: color,
|
||||
cursor: "pointer",
|
||||
border: selected ? "2px solid #fff" : "2px solid transparent",
|
||||
boxShadow: "inset 0 0 0 1px rgba(0, 0, 0, 0.25)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<IconButton key="accent" title={`Accent colour (${scheme} mode)`}>
|
||||
<span
|
||||
style={{
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: 4,
|
||||
background: current,
|
||||
boxShadow: "inset 0 0 0 1px rgba(0, 0, 0, 0.35)",
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</WithTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
addons.register("stirling/accent", () => {
|
||||
addons.add("stirling/accent-toolbar", {
|
||||
type: types.TOOL,
|
||||
title: "Accent colour",
|
||||
match: ({ viewMode }) => viewMode === "story" || viewMode === "docs",
|
||||
render: () => <AccentPicker />,
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { LinkProvider, type LinkState } from "@portal/contexts/LinkContext";
|
||||
import { ThemeProvider, useTheme } from "@portal/contexts/ThemeContext";
|
||||
import { UIProvider } from "@portal/contexts/UIContext";
|
||||
import { SuiProvider } from "@portal/theme/SuiProvider";
|
||||
import { deriveAccessiblePrimary } from "@core/utils/customPrimary";
|
||||
import { handlers } from "@portal/mocks/handlers";
|
||||
import { configureSupabase } from "@proprietary/auth/supabase/supabaseClient";
|
||||
import i18next from "i18next";
|
||||
@@ -29,6 +30,7 @@ import enTranslationToml from "../editor/public/locales/en-US/translation.toml?r
|
||||
|
||||
import "@mantine/core/styles.css";
|
||||
import "@core/tokens/tokens.css";
|
||||
import "@core/theme/index.css";
|
||||
import "@core/tokens/base.css";
|
||||
|
||||
// Storybook-only: init react-i18next with the real English resources parsed from
|
||||
@@ -124,6 +126,33 @@ function ThemeBridge({
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tints the canvas from the toolbar accent picker (see .storybook/manager.tsx),
|
||||
* exactly like the editor: always `data-app-theme="custom"`, with
|
||||
* --user-primary/-on/-accent-fg derived from the active mode's accent global.
|
||||
*/
|
||||
function AccentInjector({
|
||||
scheme,
|
||||
accent,
|
||||
}: {
|
||||
scheme: "light" | "dark";
|
||||
accent: string;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-app-theme", "custom");
|
||||
root.setAttribute("data-mantine-color-scheme", scheme);
|
||||
const { primary, onPrimary, accentForeground } = deriveAccessiblePrimary(
|
||||
accent,
|
||||
scheme,
|
||||
);
|
||||
root.style.setProperty("--user-primary", primary);
|
||||
root.style.setProperty("--user-primary-on", onPrimary);
|
||||
root.style.setProperty("--user-accent-fg", accentForeground);
|
||||
}, [scheme, accent]);
|
||||
return null;
|
||||
}
|
||||
|
||||
const withProviders: Decorator = (Story, context) => {
|
||||
const tier = (context.globals.tier as Tier) ?? "pro";
|
||||
const linkState =
|
||||
@@ -135,9 +164,15 @@ const withProviders: Decorator = (Story, context) => {
|
||||
// anything that isn't "dark" as light — matching the addon's own
|
||||
// `selected || defaultTheme` fallback where defaultTheme is light.
|
||||
const colorScheme = context.globals.theme === "dark" ? "dark" : "light";
|
||||
// Each mode has its own accent (set by the toolbar accent picker).
|
||||
const accent =
|
||||
(colorScheme === "dark"
|
||||
? (context.globals.accentDark as string)
|
||||
: (context.globals.accentLight as string)) ?? "#3b82f6";
|
||||
return (
|
||||
<MemoryRouter initialEntries={["/"]}>
|
||||
<ThemeProvider>
|
||||
<AccentInjector scheme={colorScheme} accent={accent} />
|
||||
<ThemeBridge theme={colorScheme}>
|
||||
<SuiProvider colorScheme={colorScheme}>
|
||||
{/* LinkProvider must wrap TierProvider: TierContext derives its tier
|
||||
@@ -182,6 +217,10 @@ const preview: Preview = {
|
||||
},
|
||||
},
|
||||
globalTypes: {
|
||||
// Per-mode accent colours, driven by the custom toolbar picker in
|
||||
// .storybook/manager.tsx (no built-in toolbar UI of their own).
|
||||
accentLight: { name: "Accent (light)", defaultValue: "#3b82f6" },
|
||||
accentDark: { name: "Accent (dark)", defaultValue: "#3b82f6" },
|
||||
tier: {
|
||||
name: "Tier",
|
||||
description: "Subscription tier — drives useTier() everywhere",
|
||||
|
||||
@@ -14,6 +14,51 @@
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
|
||||
<title>Stirling PDF</title>
|
||||
|
||||
<!-- Apply theme + accent before first paint (no FOUC); mirrors ThemeProvider, which then refines it. -->
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var p = JSON.parse(
|
||||
localStorage.getItem("stirlingpdf_preferences") || "{}",
|
||||
);
|
||||
var mode = p.theme || "system";
|
||||
var scheme =
|
||||
mode === "light"
|
||||
? "light"
|
||||
: mode === "dark"
|
||||
? "dark"
|
||||
: window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
var stored = scheme === "dark" ? p.darkPrimary : p.lightPrimary;
|
||||
// "default" (or unset) = neutral surfaces + blue buttons; a hex tints.
|
||||
var isDefault = !stored || stored === "default";
|
||||
var accent = isDefault ? "#3b82f6" : stored;
|
||||
var r = document.documentElement;
|
||||
r.setAttribute("data-theme", scheme);
|
||||
r.setAttribute("data-app-theme", "custom");
|
||||
r.setAttribute("data-accent", isDefault ? "default" : "custom");
|
||||
r.setAttribute("data-mantine-color-scheme", scheme);
|
||||
r.style.setProperty("--user-primary", accent);
|
||||
r.style.setProperty("--user-accent-fg", accent);
|
||||
var h = accent.replace("#", "");
|
||||
if (h.length === 3) h = h.replace(/./g, "$&$&");
|
||||
var lum =
|
||||
(0.2126 * parseInt(h.slice(0, 2), 16) +
|
||||
0.7152 * parseInt(h.slice(2, 4), 16) +
|
||||
0.0722 * parseInt(h.slice(4, 6), 16)) /
|
||||
255;
|
||||
r.style.setProperty(
|
||||
"--user-primary-on",
|
||||
lum > 0.7 ? "#000000" : "#ffffff",
|
||||
);
|
||||
} catch (e) {
|
||||
/* first visit / unavailable storage: ThemeProvider handles it */
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
@@ -8980,9 +8980,18 @@ languageDescription = "Choose the display language"
|
||||
logout = "Log out"
|
||||
setAsDefault = "Set as Default"
|
||||
theme = "Theme"
|
||||
themeAccent = "Accent colour"
|
||||
themeAccentCustom = "Custom colour"
|
||||
themeAccentCustomHint = "Custom (defaults recommended)"
|
||||
themeAccentDark = "Dark mode accent colour"
|
||||
themeAccentDefault = "Default"
|
||||
themeAccentDefaultHint = "Default theme (recommended)"
|
||||
themeAccentDescription = "Buttons, links and highlights follow it, and it subtly tints the app. Light and dark each have their own — System uses whichever is active."
|
||||
themeAccentLight = "Light mode accent colour"
|
||||
themeDark = "Dark"
|
||||
themeDescription = "Choose light, dark, or follow your system"
|
||||
themeDescription = "Choose light, dark, or follow your system so it switches automatically."
|
||||
themeLight = "Light"
|
||||
themeReset = "Restore theme to default"
|
||||
themeSystem = "System"
|
||||
title = "General"
|
||||
user = "User"
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env node
|
||||
// Theme colour lint — guards the theme SYSTEM (core/theme/). Two modes:
|
||||
//
|
||||
// node theme-lint.mjs enforce: literal colours live ONLY in
|
||||
// primitives.css; colors/compat/dimensions must
|
||||
// reference tokens; no duplicate primitives.
|
||||
// (blocking)
|
||||
// node theme-lint.mjs contrast warn-only WCAG contrast report (never blocks)
|
||||
//
|
||||
// Scope is deliberately just core/theme/ — the palette + token layer this PR
|
||||
// owns, which is clean, so no baseline file is needed. Enforcing "no hardcoded
|
||||
// colours" across the whole app (260+ existing sites) is a separate migration.
|
||||
//
|
||||
// Structural black / white / transparent (shadows, scrims) are always allowed.
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import { relative, join } from "node:path";
|
||||
|
||||
const THEME = join(process.cwd(), "editor/src/core/theme");
|
||||
const PRIMITIVES = "editor/src/core/theme/primitives.css";
|
||||
|
||||
// ── colour helpers ─────────────────────────────────────────────────────────
|
||||
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const FUNC_RE = /\b(?:rgba?|hsla?)\(\s*[^)]*\)/g;
|
||||
const NAMED_RE =
|
||||
/\b(?:white|black|red|green|blue|orange|yellow|purple|gray|grey|silver|transparent)\b/g;
|
||||
|
||||
function expandHex(hex) {
|
||||
let h = hex.slice(1).toLowerCase();
|
||||
if (h.length === 3) h = [...h].map((c) => c + c).join("");
|
||||
if (h.length === 4) h = [...h].map((c) => c + c).join("");
|
||||
return "#" + h;
|
||||
}
|
||||
function normalizeColor(raw) {
|
||||
const s = raw.trim().toLowerCase();
|
||||
if (s.startsWith("#")) return expandHex(s);
|
||||
const nums = s.match(/[\d.]+%?/g);
|
||||
if (!nums) return s;
|
||||
return `${s.startsWith("hsl") ? "hsl" : "rgb"}(${nums.join(",")})`;
|
||||
}
|
||||
function isStructuralColor(norm) {
|
||||
return (
|
||||
norm === "transparent" ||
|
||||
/^#000000(00)?$/.test(norm) ||
|
||||
/^#ffffff(ff)?$/.test(norm) ||
|
||||
/^rgb\(0,0,0[,)]/.test(norm) ||
|
||||
/^rgb\(255,255,255[,)]/.test(norm)
|
||||
);
|
||||
}
|
||||
function isStructuralName(name) {
|
||||
return /^(?:white|black|transparent)$/i.test(name.trim());
|
||||
}
|
||||
function walk(dir, exts, out = []) {
|
||||
for (const name of readdirSync(dir)) {
|
||||
const p = join(dir, name);
|
||||
if (statSync(p).isDirectory()) walk(p, exts, out);
|
||||
else if (exts.some((e) => name.endsWith(e))) out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function stripComments(text) {
|
||||
return text.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "));
|
||||
}
|
||||
|
||||
// ── enforce: literals only in primitives.css, no duplicate primitives ────────
|
||||
function check() {
|
||||
const violations = [];
|
||||
const primitiveValues = new Map();
|
||||
const lineOf = (text, index) => text.slice(0, index).split("\n").length;
|
||||
|
||||
for (const file of walk(THEME, [".css"])) {
|
||||
const rel = relative(process.cwd(), file);
|
||||
const isPrimitives = rel === PRIMITIVES;
|
||||
const text = stripComments(readFileSync(file, "utf8"));
|
||||
|
||||
for (const re of [HEX_RE, FUNC_RE]) {
|
||||
re.lastIndex = 0;
|
||||
let m;
|
||||
while ((m = re.exec(text)) !== null) {
|
||||
const norm = normalizeColor(m[0]);
|
||||
if (isStructuralColor(norm)) continue;
|
||||
if (isPrimitives) {
|
||||
if (primitiveValues.has(norm)) {
|
||||
violations.push({ file: rel, line: lineOf(text, m.index), msg: `duplicate primitive value ${norm} (also ${primitiveValues.get(norm)})` });
|
||||
} else {
|
||||
primitiveValues.set(norm, m[0]);
|
||||
}
|
||||
} else {
|
||||
violations.push({ file: rel, line: lineOf(text, m.index), msg: `raw colour ${m[0]} — define it in primitives.css and use var()` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Named colours, only in value position.
|
||||
if (!isPrimitives) {
|
||||
text.split("\n").forEach((line, i) => {
|
||||
const colon = line.indexOf(":");
|
||||
if (colon < 0 || /[{}]/.test(line)) return;
|
||||
// Property must be a lone identifier — a custom prop (--x) OR a standard
|
||||
// property (color, border) — so `color: red` is checked, not just tokens,
|
||||
// while selectors (`.foo:hover`) with a colon are skipped.
|
||||
if (!/^\s*(?:--)?[a-z][a-z0-9-]*\s*$/i.test(line.slice(0, colon))) return;
|
||||
const value = line
|
||||
.slice(colon + 1)
|
||||
.replace(/--[a-z0-9-]+/gi, " ")
|
||||
.replace(/url\([^)]*\)/g, " ")
|
||||
.replace(/["'][^"']*["']/g, " ");
|
||||
for (const nm of value.match(NAMED_RE) || []) {
|
||||
if (isStructuralName(nm)) continue;
|
||||
violations.push({ file: rel, line: i + 1, msg: `named colour "${nm}" — define it in primitives.css and use var()` });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
// ── contrast report (warn-only): resolve --c-* per theme, check legibility ───
|
||||
function reportContrast() {
|
||||
const primitivesCss = readFileSync(join(THEME, "primitives.css"), "utf8");
|
||||
const colorsCss = readFileSync(join(THEME, "colors.css"), "utf8");
|
||||
const primitives = {};
|
||||
for (const m of primitivesCss.matchAll(/(--p-[a-z0-9-]+)\s*:\s*(#[0-9a-fA-F]{3,8})\s*;/g)) primitives[m[1]] = m[2];
|
||||
const blocks = [];
|
||||
for (const m of colorsCss.matchAll(/([^{}]+)\{([^}]*)\}/g)) {
|
||||
const decls = {};
|
||||
for (const d of m[2].matchAll(/(--c-[a-z0-9-]+)\s*:\s*([^;]+);/g)) decls[d[1]] = d[2].trim();
|
||||
if (Object.keys(decls).length) blocks.push({ selector: m[1].trim(), decls });
|
||||
}
|
||||
// The editor always renders data-app-theme="custom"; the accent (--user-*) is
|
||||
// injected at runtime, so seed the DEFAULT blue to resolve the custom tint
|
||||
// statically. Themes = the ones that actually render: editor custom light/dark
|
||||
// (data-app-theme="custom") and the portal's neutral dark (data-theme="dark").
|
||||
const SEED = {
|
||||
"--user-primary": "#3b82f6",
|
||||
"--user-primary-on": "#ffffff",
|
||||
"--user-accent-fg": "#3b82f6",
|
||||
};
|
||||
const pick = (re) => blocks.filter((b) => re.test(b.selector));
|
||||
const lightBase = pick(/:root/);
|
||||
const customBase = blocks.filter(
|
||||
(b) => /app-theme="custom"/.test(b.selector) && !/color-scheme="dark"/.test(b.selector),
|
||||
);
|
||||
const customDark = pick(/app-theme="custom"\]\[data-mantine-color-scheme="dark"/);
|
||||
const midnight = pick(/data-theme="dark"/);
|
||||
const themes = {
|
||||
"editor light": [...lightBase, ...customBase],
|
||||
"editor dark": [...lightBase, ...customBase, ...customDark],
|
||||
"portal dark": [...lightBase, ...midnight],
|
||||
};
|
||||
const flatten = (list) => Object.assign({ ...SEED }, ...list.map((b) => b.decls));
|
||||
const hexToRgb = (h) => {
|
||||
h = h.replace("#", "");
|
||||
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
||||
return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), a: 1 };
|
||||
};
|
||||
const over = (f, b) => ({ r: f.r * f.a + b.r * (1 - f.a), g: f.g * f.a + b.g * (1 - f.a), b: f.b * f.a + b.b * (1 - f.a), a: 1 });
|
||||
const mix = (a, b, p) => ({ r: (a.r * p + b.r * (100 - p)) / 100, g: (a.g * p + b.g * (100 - p)) / 100, b: (a.b * p + b.b * (100 - p)) / 100, a: 1 });
|
||||
// Resolve any token value: hex, rgb(a), var(--x[, fallback]) (--p-* → palette,
|
||||
// else the theme map/seed), or color-mix(in srgb, A n%, B|transparent).
|
||||
function resolveValue(v, t, seen) {
|
||||
if (v == null) return null;
|
||||
v = v.trim();
|
||||
let m;
|
||||
if (v.startsWith("#")) return hexToRgb(v);
|
||||
if ((m = v.match(/^rgba?\(([^)]+)\)$/))) {
|
||||
const n = m[1].split(/[,/\s]+/).map(Number).filter((x) => !Number.isNaN(x));
|
||||
return { r: n[0], g: n[1], b: n[2], a: n[3] ?? 1 };
|
||||
}
|
||||
if ((m = v.match(/^var\(\s*(--[a-z0-9-]+)\s*(?:,\s*([\s\S]+))?\)$/))) {
|
||||
return resolveVar(m[1], m[2], t, seen);
|
||||
}
|
||||
if ((m = v.match(/^color-mix\(in srgb,\s*(.+?)\s+(\d+)%\s*,\s*(.+)\)$/))) {
|
||||
const a = resolveValue(m[1], t, seen);
|
||||
if (!a) return null;
|
||||
if (m[3].trim() === "transparent") return { ...a, a: +m[2] / 100 };
|
||||
const b = resolveValue(m[3].trim(), t, seen);
|
||||
return b ? mix(a, b, +m[2]) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function resolveVar(name, fallback, t, seen) {
|
||||
if (name.startsWith("--p-")) {
|
||||
return primitives[name] ? hexToRgb(primitives[name]) : fallback ? resolveValue(fallback, t, seen) : null;
|
||||
}
|
||||
if (!seen.has(name) && t[name] !== undefined) {
|
||||
const next = new Set(seen).add(name);
|
||||
const r = resolveValue(t[name], t, next);
|
||||
if (r) return r;
|
||||
}
|
||||
return fallback ? resolveValue(fallback, t, seen) : null;
|
||||
}
|
||||
const resolve = (token, t) => resolveValue(t[token] ?? null, t, new Set([token]));
|
||||
const lum = ({ r, g, b }) => {
|
||||
const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
||||
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
||||
};
|
||||
const contrast = (t1, t2, t) => {
|
||||
const surface = resolve("--c-surface", t);
|
||||
let a = resolve(t1, t);
|
||||
let b = resolve(t2, t);
|
||||
if (!a || !b || !surface) return null;
|
||||
if (a.a < 1) a = over(a, surface);
|
||||
if (b.a < 1) b = over(b, surface);
|
||||
const [hi, lo] = [lum(a), lum(b)].sort((x, y) => y - x);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
};
|
||||
const PAIRS = [
|
||||
["--c-text", "--c-surface", 4.5],
|
||||
["--c-text-muted", "--c-surface", 4.5],
|
||||
["--c-text-subtle", "--c-surface", 4.5],
|
||||
["--c-text", "--c-bg", 4.5],
|
||||
["--c-text-on-primary", "--c-primary", 3.0],
|
||||
];
|
||||
let warnings = 0;
|
||||
console.log("contrast report (warning-only, default accent):\n");
|
||||
for (const name of Object.keys(themes)) {
|
||||
const t = flatten(themes[name]);
|
||||
console.log(` ${name}`);
|
||||
for (const [t1, t2, floor] of PAIRS) {
|
||||
const r = contrast(t1, t2, t);
|
||||
if (r == null) { console.log(` ? ${t1} on ${t2} (unresolved)`); continue; }
|
||||
if (r < floor) warnings++;
|
||||
console.log(` ${r < floor ? "⚠ " : " "}${r.toFixed(2).padStart(5)} (floor ${floor}) ${t1} on ${t2}`);
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
console.log(warnings ? `⚠ ${warnings} pair(s) below floor — review, not blocking.` : "✓ all pairs clear their floor.");
|
||||
}
|
||||
|
||||
// ── CLI ──────────────────────────────────────────────────────────────────────
|
||||
if (process.argv.includes("contrast")) {
|
||||
reportContrast();
|
||||
process.exit(0); // never blocks
|
||||
}
|
||||
|
||||
const violations = check();
|
||||
if (violations.length) {
|
||||
console.error(`\n✖ theme-lint: ${violations.length} raw/duplicate colour(s) in core/theme/:\n`);
|
||||
for (const v of violations) console.error(` ${v.file}:${v.line} ${v.msg}`);
|
||||
console.error(`\nDefine every colour once in core/theme/primitives.css and reference it with var(--p-…).\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("✓ theme-lint: core/theme colours all route through the primitive palette");
|
||||
@@ -332,7 +332,7 @@
|
||||
max-width: 260px;
|
||||
height: calc(310px - 0.5rem);
|
||||
margin: 0.5rem auto 0;
|
||||
background: var(--file-card-bg);
|
||||
background: var(--c-bg);
|
||||
border: 1.5px solid var(--border-default);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-md);
|
||||
|
||||
@@ -34,12 +34,7 @@ export function LandingActions({
|
||||
<Button
|
||||
className="landing-btn-primary"
|
||||
leftSection={
|
||||
<LocalIcon
|
||||
icon={icons.uploadIconName}
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
style={{ color: "white" }}
|
||||
/>
|
||||
<LocalIcon icon={icons.uploadIconName} width="1rem" height="1rem" />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -53,12 +48,7 @@ export function LandingActions({
|
||||
variant="secondary"
|
||||
className="landing-btn-secondary"
|
||||
leftSection={
|
||||
<LocalIcon
|
||||
icon="add"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
className="text-[var(--accent-interactive)]"
|
||||
/>
|
||||
<LocalIcon icon="add" width="1rem" height="1rem" />
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
/* ── Action buttons ──────────────────────────────────────── */
|
||||
.landing-btn-primary {
|
||||
background: var(--landing-hero-gradient) !important;
|
||||
color: #ffffff !important;
|
||||
color: var(--c-text-on-primary) !important;
|
||||
border: none !important;
|
||||
border-radius: 0.75rem !important;
|
||||
font-weight: 600 !important;
|
||||
|
||||
@@ -17,9 +17,13 @@ import {
|
||||
type ThemeMode,
|
||||
getSystemTheme,
|
||||
resolveColorScheme,
|
||||
DEFAULT_ACCENT,
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
} from "@app/constants/theme";
|
||||
import { deriveAccessiblePrimary } from "@app/utils/customPrimary";
|
||||
// SUI shared design-system tokens (used by @app/ui); key on `data-theme`.
|
||||
import "@app/tokens/tokens.css";
|
||||
import "@app/theme/index.css";
|
||||
|
||||
interface ThemeContextType {
|
||||
themeMode: ThemeMode;
|
||||
@@ -68,13 +72,33 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
return () => media.removeEventListener("change", update);
|
||||
}, [themeMode]);
|
||||
|
||||
// The theme preference resolved to a concrete light/dark scheme.
|
||||
// The mode resolved to a concrete light/dark base.
|
||||
const colorScheme = resolveColorScheme(themeMode, systemScheme);
|
||||
// Each base has its own accent; "system" picks by the resolved scheme.
|
||||
const primaryInput =
|
||||
colorScheme === "dark"
|
||||
? preferences.darkPrimary
|
||||
: preferences.lightPrimary;
|
||||
|
||||
// Mantine drives `data-mantine-color-scheme`; mirror it to `data-theme` for SUI tokens.
|
||||
// A colour tints surfaces; the "default" sentinel means neutral surfaces + blue buttons (derive from blue, flag data-accent="default" so colors.css skips the tint).
|
||||
const isDefaultAccent = primaryInput === DEFAULT_ACCENT;
|
||||
const accentColor = isDefaultAccent ? DEFAULT_ACCENT_COLOR : primaryInput;
|
||||
|
||||
// Mirror the scheme to data-theme (SUI) and set data-app-theme="custom" so colors.css derives from --user-primary.
|
||||
useIsomorphicEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", colorScheme);
|
||||
}, [colorScheme]);
|
||||
const root = document.documentElement;
|
||||
root.setAttribute("data-theme", colorScheme);
|
||||
root.setAttribute("data-app-theme", "custom");
|
||||
root.setAttribute("data-accent", isDefaultAccent ? "default" : "custom");
|
||||
// Clamp/contrast-pick the accent so it can't collapse into the base and stays legible.
|
||||
const { primary, onPrimary, accentForeground } = deriveAccessiblePrimary(
|
||||
accentColor,
|
||||
colorScheme,
|
||||
);
|
||||
root.style.setProperty("--user-primary", primary);
|
||||
root.style.setProperty("--user-primary-on", onPrimary);
|
||||
root.style.setProperty("--user-accent-fg", accentForeground);
|
||||
}, [colorScheme, accentColor, isDefaultAccent]);
|
||||
|
||||
const value = useMemo<ThemeContextType>(
|
||||
() => ({ themeMode, setTheme }),
|
||||
|
||||
+279
-10
@@ -11,20 +11,29 @@ import {
|
||||
Group,
|
||||
Anchor,
|
||||
Badge,
|
||||
Popover,
|
||||
} from "@mantine/core";
|
||||
import { Button } from "@app/ui/Button";
|
||||
import { ActionIcon } from "@app/ui/ActionIcon";
|
||||
import { ColorInput } from "@app/ui/ColorInput";
|
||||
import { SegmentedControl } from "@app/ui/SegmentedControl";
|
||||
import { clampAccentChoice } from "@app/utils/customPrimary";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePreferences } from "@app/contexts/PreferencesContext";
|
||||
import { useAppConfig } from "@app/contexts/AppConfigContext";
|
||||
import { useTheme } from "@app/components/shared/ThemeProvider";
|
||||
import LanguageSelector from "@app/components/shared/LanguageSelector";
|
||||
import type { ThemeMode } from "@app/constants/theme";
|
||||
import {
|
||||
THEME_ACCENT_PRESETS,
|
||||
DEFAULT_ACCENT,
|
||||
DEFAULT_ACCENT_COLOR,
|
||||
type ThemeMode,
|
||||
} from "@app/constants/theme";
|
||||
import type { ToolPanelMode } from "@app/constants/toolPanel";
|
||||
import type {
|
||||
StartupView,
|
||||
ViewerZoomSetting,
|
||||
import {
|
||||
DEFAULT_PREFERENCES,
|
||||
type StartupView,
|
||||
type ViewerZoomSetting,
|
||||
} from "@app/services/preferencesService";
|
||||
import { Z_INDEX_OVER_CONFIG_MODAL } from "@app/styles/zIndex";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
@@ -75,6 +84,200 @@ interface GeneralSectionProps {
|
||||
desktopUpdateMode?: DesktopUpdateModeControl;
|
||||
}
|
||||
|
||||
/** Accent-colour picker: a swatch trigger opening a 3×5 grid — a "Default" icon cell + 14 accents — plus a "Custom" picker below. */
|
||||
function AccentSwatchDropdown({
|
||||
value,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
ariaLabel: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [opened, setOpened] = useState(false);
|
||||
const isDefault = value === DEFAULT_ACCENT;
|
||||
// Resolve the "default" sentinel to the blue it maps to so the ColorInput shows a real colour.
|
||||
const asHex = (v: string) => (v === DEFAULT_ACCENT ? DEFAULT_ACCENT_COLOR : v);
|
||||
// Live picker value; committed to preferences only on drag-end (below).
|
||||
const [draft, setDraft] = useState(() => asHex(value));
|
||||
useEffect(() => setDraft(asHex(value)), [value]);
|
||||
return (
|
||||
<Popover
|
||||
opened={opened}
|
||||
onChange={setOpened}
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
zIndex={Z_INDEX_OVER_CONFIG_MODAL}
|
||||
shadow="md"
|
||||
radius="md"
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
{/* eslint-disable-next-line no-restricted-syntax -- swatch-trigger control, not a text button; the shared Button's variants/padding don't fit */}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
aria-haspopup="listbox"
|
||||
onClick={() => setOpened((o) => !o)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "0.5rem",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid var(--c-border)",
|
||||
background: "var(--c-input-bg)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{isDefault ? (
|
||||
// Default is not a colour — show the icon chip so the trigger reads as unset, not a hue.
|
||||
<span
|
||||
style={{
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
borderRadius: "5px",
|
||||
background: "var(--c-surface-raised)",
|
||||
boxShadow: "inset 0 0 0 1px var(--c-border)",
|
||||
}}
|
||||
>
|
||||
<LocalIcon
|
||||
icon="star-rounded"
|
||||
width="0.875rem"
|
||||
height="0.875rem"
|
||||
style={{ color: "var(--c-text-muted)" }}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
style={{
|
||||
width: "1.25rem",
|
||||
height: "1.25rem",
|
||||
borderRadius: "5px",
|
||||
background: value,
|
||||
boxShadow: "inset 0 0 0 1px rgba(0, 0, 0, 0.15)",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<LocalIcon
|
||||
icon="expand-more-rounded"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
style={{ color: "var(--c-text-subtle)" }}
|
||||
/>
|
||||
</button>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown p="xs">
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(5, 1.75rem)",
|
||||
gap: "0.6875rem",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{/* Default — an icon chip for the neutral theme (surfaces untinted, blue buttons); stores the sentinel, a separate state from the colour swatches. */}
|
||||
{/* eslint-disable-next-line no-restricted-syntax -- an icon chip, not a text button; the shared Button's variants don't fit */}
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={isDefault}
|
||||
aria-label={t("settings.general.themeAccentDefault", "Default")}
|
||||
title={t(
|
||||
"settings.general.themeAccentDefaultHint",
|
||||
"Default theme (recommended)",
|
||||
)}
|
||||
onClick={() => {
|
||||
onChange(DEFAULT_ACCENT);
|
||||
setOpened(false);
|
||||
}}
|
||||
style={{
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
width: "1.75rem",
|
||||
height: "1.75rem",
|
||||
padding: 0,
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
background: "var(--c-surface-raised)",
|
||||
border: "1px solid var(--c-border)",
|
||||
outline: isDefault ? "2px solid var(--c-text)" : "none",
|
||||
outlineOffset: "2px",
|
||||
}}
|
||||
>
|
||||
<LocalIcon
|
||||
icon="star-rounded"
|
||||
width="1.125rem"
|
||||
height="1.125rem"
|
||||
style={{ color: "var(--c-text-muted)" }}
|
||||
/>
|
||||
</button>
|
||||
{THEME_ACCENT_PRESETS.map((color) => {
|
||||
const selected = value.toLowerCase() === color.toLowerCase();
|
||||
return (
|
||||
// eslint-disable-next-line no-restricted-syntax -- a colour swatch, not a text button; the shared Button's variants don't fit
|
||||
<button
|
||||
key={color}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={selected}
|
||||
aria-label={color}
|
||||
onClick={() => {
|
||||
onChange(color);
|
||||
setOpened(false);
|
||||
}}
|
||||
style={{
|
||||
width: "1.75rem",
|
||||
height: "1.75rem",
|
||||
padding: 0,
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
background: color,
|
||||
cursor: "pointer",
|
||||
outline: selected ? "2px solid var(--c-text)" : "none",
|
||||
outlineOffset: "2px",
|
||||
boxShadow: "inset 0 0 0 1px rgba(0, 0, 0, 0.12)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Custom colour — below the presets; our ColorInput, clamped off white/grey/black. */}
|
||||
<div
|
||||
style={{
|
||||
marginTop: "0.5rem",
|
||||
paddingTop: "0.5rem",
|
||||
borderTop: "1px solid var(--c-border-subtle)",
|
||||
}}
|
||||
>
|
||||
<Text size="xs" c="dimmed" mb={6}>
|
||||
{t(
|
||||
"settings.general.themeAccentCustomHint",
|
||||
"Custom (defaults recommended)",
|
||||
)}
|
||||
</Text>
|
||||
<ColorInput
|
||||
inputSize="sm"
|
||||
format="hex"
|
||||
withPicker
|
||||
value={draft}
|
||||
onChange={setDraft}
|
||||
onChangeEnd={onChange}
|
||||
clampValue={clampAccentChoice}
|
||||
aria-label={t("settings.general.themeAccentCustom", "Custom colour")}
|
||||
popoverProps={{ withinPortal: false }}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
hideTitle = false,
|
||||
hideUpdateSection = false,
|
||||
@@ -482,7 +685,7 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t(
|
||||
"settings.general.themeDescription",
|
||||
"Choose light, dark, or follow your system",
|
||||
"Choose light, dark, or follow your system so it switches automatically.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
@@ -505,29 +708,95 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.language", "Language")}
|
||||
{t("settings.general.themeAccent", "Accent colour")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t(
|
||||
"settings.general.languageDescription",
|
||||
"Choose the display language",
|
||||
"settings.general.themeAccentDescription",
|
||||
"Buttons, links and highlights follow it, and it subtly tints the app. Light and dark each have their own — System uses whichever is active.",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<LanguageSelector position="bottom-end" offset={6} />
|
||||
<Stack gap="md">
|
||||
<Group gap="sm" wrap="nowrap" align="flex-start" justify="flex-end">
|
||||
<Text size="sm" c="dimmed" style={{ width: "3rem", paddingTop: 4 }}>
|
||||
{t("settings.general.themeLight", "Light")}
|
||||
</Text>
|
||||
<AccentSwatchDropdown
|
||||
value={preferences.lightPrimary}
|
||||
onChange={(val) => updatePreference("lightPrimary", val)}
|
||||
ariaLabel={t(
|
||||
"settings.general.themeAccentLight",
|
||||
"Light mode accent colour",
|
||||
)}
|
||||
/>
|
||||
</Group>
|
||||
<Group gap="sm" wrap="nowrap" align="flex-start" justify="flex-end">
|
||||
<Text size="sm" c="dimmed" style={{ width: "3rem", paddingTop: 4 }}>
|
||||
{t("settings.general.themeDark", "Dark")}
|
||||
</Text>
|
||||
<AccentSwatchDropdown
|
||||
value={preferences.darkPrimary}
|
||||
onChange={(val) => updatePreference("darkPrimary", val)}
|
||||
ariaLabel={t(
|
||||
"settings.general.themeAccentDark",
|
||||
"Dark mode accent colour",
|
||||
)}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</div>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="quiet"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setTheme(DEFAULT_PREFERENCES.theme);
|
||||
updatePreference("lightPrimary", DEFAULT_PREFERENCES.lightPrimary);
|
||||
updatePreference("darkPrimary", DEFAULT_PREFERENCES.darkPrimary);
|
||||
}}
|
||||
>
|
||||
{t("settings.general.themeReset", "Restore theme to default")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Language */}
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Text fw={500} size="sm">
|
||||
{t("settings.general.language", "Language")}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{t(
|
||||
"settings.general.languageDescription",
|
||||
"Choose the display language",
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<LanguageSelector position="bottom-end" offset={6} />
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Stack gap="md">
|
||||
<div
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
// Theme constants and utilities
|
||||
|
||||
// Stored theme preference. "system" follows the OS.
|
||||
export type ThemeMode = "light" | "dark" | "system";
|
||||
|
||||
// The concrete scheme applied to the UI.
|
||||
// The concrete light/dark base applied to Mantine + the neutral ramp.
|
||||
export type ColorScheme = "light" | "dark";
|
||||
|
||||
// lightPrimary/darkPrimary hold either a colour (tints surfaces) or this sentinel (neutral surfaces + blue buttons; ThemeProvider maps it to DEFAULT_ACCENT_COLOR and flags data-accent="default").
|
||||
export const DEFAULT_ACCENT = "default";
|
||||
export const DEFAULT_ACCENT_COLOR = "#3b82f6"; // blue-500 — buttons in the default theme
|
||||
|
||||
// 14 curated mid-tone accents filling the 3×5 grid alongside the "Default" cell. Literal hex: the picker stores the value verbatim and deriveAccessiblePrimary parses it.
|
||||
export const THEME_ACCENT_PRESETS = [
|
||||
// blues → purples (one purple trimmed so the grid leaves room for Default)
|
||||
"#3b82f6", // blue
|
||||
"#6366f1", // indigo
|
||||
"#8b5cf6", // violet
|
||||
"#d946ef", // fuchsia
|
||||
// pinks → warm
|
||||
"#ec4899", // pink
|
||||
"#f43f5e", // rose
|
||||
"#ef4444", // red
|
||||
"#f97316", // orange
|
||||
"#f59e0b", // amber
|
||||
// yellow → greens → cyan
|
||||
"#eab308", // yellow
|
||||
"#84cc16", // lime
|
||||
"#22c55e", // green
|
||||
"#14b8a6", // teal
|
||||
"#06b6d4", // cyan
|
||||
];
|
||||
|
||||
// Detect the OS theme preference. Never throws: if the environment can't be
|
||||
// read (no window, no matchMedia, or it errors), defaults to "light".
|
||||
export function getSystemTheme(): ColorScheme {
|
||||
@@ -24,12 +48,13 @@ export function getSystemTheme(): ColorScheme {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a theme preference to a concrete light/dark scheme.
|
||||
// Falls back to systemScheme for unrecognised values (e.g. stale "rainbow").
|
||||
// Resolve the theme MODE to the concrete light/dark base Mantine uses.
|
||||
// "system" follows the OS; anything unrecognised falls back to it too.
|
||||
export function resolveColorScheme(
|
||||
mode: ThemeMode,
|
||||
systemScheme: ColorScheme,
|
||||
): ColorScheme {
|
||||
if (mode === "light" || mode === "dark") return mode;
|
||||
if (mode === "light") return "light";
|
||||
if (mode === "dark") return "dark";
|
||||
return systemScheme;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
type ToolPanelMode,
|
||||
DEFAULT_TOOL_PANEL_MODE,
|
||||
} from "@app/constants/toolPanel";
|
||||
import { type ThemeMode } from "@app/constants/theme";
|
||||
import { type ThemeMode, DEFAULT_ACCENT } from "@app/constants/theme";
|
||||
|
||||
export type LogoVariant = "modern" | "classic";
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface UserPreferences {
|
||||
// SaaS-only: team lead's post-login landing app (processor vs editor).
|
||||
loginLandingView: LoginLandingView;
|
||||
theme: ThemeMode;
|
||||
lightPrimary: string;
|
||||
darkPrimary: string;
|
||||
toolPanelModePromptSeen: boolean;
|
||||
hasSelectedToolPanelMode: boolean;
|
||||
showLegacyToolDescriptions: boolean;
|
||||
@@ -54,6 +56,8 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
|
||||
defaultViewerZoom: "auto",
|
||||
loginLandingView: "processor",
|
||||
theme: "system",
|
||||
lightPrimary: DEFAULT_ACCENT,
|
||||
darkPrimary: DEFAULT_ACCENT,
|
||||
toolPanelModePromptSeen: false,
|
||||
hasSelectedToolPanelMode: false,
|
||||
showLegacyToolDescriptions: false,
|
||||
@@ -71,6 +75,51 @@ const STORAGE_KEY = "stirlingpdf_preferences";
|
||||
class PreferencesService {
|
||||
private serverDefaults: Partial<UserPreferences> = {};
|
||||
|
||||
constructor() {
|
||||
this.migrateLegacyTheme();
|
||||
}
|
||||
|
||||
// One-time migration from the old theme model (theme: light|dark|midnight|
|
||||
// custom|system + customThemeBase + customThemePrimary) to the new one
|
||||
// (theme: light|dark|system + lightPrimary + darkPrimary). Runs once and
|
||||
// rewrites storage; a no-op for already-migrated or empty preferences.
|
||||
private migrateLegacyTheme(): void {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (!stored) return;
|
||||
const p = JSON.parse(stored) as Record<string, unknown>;
|
||||
const hadLegacy =
|
||||
"customThemeBase" in p ||
|
||||
"customThemePrimary" in p ||
|
||||
p.theme === "custom" ||
|
||||
p.theme === "midnight";
|
||||
if (!hadLegacy) return;
|
||||
|
||||
const legacyBase = p.customThemeBase === "light" ? "light" : "dark";
|
||||
const legacyPrimary =
|
||||
typeof p.customThemePrimary === "string"
|
||||
? p.customThemePrimary
|
||||
: DEFAULT_ACCENT;
|
||||
|
||||
// midnight → dark; custom → whichever base it was sitting on.
|
||||
if (p.theme === "midnight") p.theme = "dark";
|
||||
else if (p.theme === "custom") p.theme = legacyBase;
|
||||
|
||||
// Seed the customised side from the old primary; the other side gets the default (neutral) theme.
|
||||
if (p.lightPrimary === undefined) {
|
||||
p.lightPrimary = legacyBase === "light" ? legacyPrimary : DEFAULT_ACCENT;
|
||||
}
|
||||
if (p.darkPrimary === undefined) {
|
||||
p.darkPrimary = legacyBase === "dark" ? legacyPrimary : DEFAULT_ACCENT;
|
||||
}
|
||||
delete p.customThemeBase;
|
||||
delete p.customThemePrimary;
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(p));
|
||||
} catch (error) {
|
||||
console.error("Error migrating theme preferences:", error);
|
||||
}
|
||||
}
|
||||
|
||||
setServerDefaults(defaults: Partial<UserPreferences>): void {
|
||||
this.serverDefaults = defaults;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,26 @@
|
||||
:root {
|
||||
/* Compare highlight colors (same in light/dark) */
|
||||
--spdf-compare-removed-bg: rgba(255, 107, 107, 0.45); /* #ff6b6b @ 0.45 */
|
||||
--spdf-compare-added-bg: rgba(81, 207, 102, 0.35); /* #51cf66 @ 0.35 */
|
||||
--spdf-compare-removed-bg: color-mix(in srgb, var(--p-red-400) 45%, transparent); /* #ff6b6b @ 0.45 */
|
||||
--spdf-compare-added-bg: color-mix(in srgb, var(--p-green-500) 35%, transparent); /* #51cf66 @ 0.35 */
|
||||
|
||||
/* Badge colors for dropdowns */
|
||||
--spdf-compare-removed-badge-bg: rgba(255, 59, 48, 0.15);
|
||||
--spdf-compare-removed-badge-fg: #b91c1c;
|
||||
--spdf-compare-added-badge-bg: rgba(52, 199, 89, 0.18);
|
||||
--spdf-compare-added-badge-fg: #1b5e20;
|
||||
--spdf-compare-removed-badge-bg: color-mix(in srgb, var(--p-red-500) 15%, transparent);
|
||||
--spdf-compare-removed-badge-fg: var(--p-red-600);
|
||||
--spdf-compare-added-badge-bg: color-mix(in srgb, var(--p-green-500) 18%, transparent);
|
||||
--spdf-compare-added-badge-fg: var(--p-green-700);
|
||||
|
||||
/* Inline highlights in summary */
|
||||
--spdf-compare-inline-removed-bg: rgba(255, 59, 48, 0.25);
|
||||
--spdf-compare-inline-added-bg: rgba(52, 199, 89, 0.25);
|
||||
--spdf-compare-inline-removed-bg: color-mix(in srgb, var(--p-red-500) 25%, transparent);
|
||||
--spdf-compare-inline-added-bg: color-mix(in srgb, var(--p-green-500) 25%, transparent);
|
||||
|
||||
/* File Manager active file colors */
|
||||
--file-active-bg: rgba(
|
||||
59,
|
||||
130,
|
||||
246,
|
||||
0.1
|
||||
); /* Transparent blue for active file background */
|
||||
--file-active-badge-bg: rgba(
|
||||
34,
|
||||
197,
|
||||
94,
|
||||
0.15
|
||||
); /* Transparent green for active badge */
|
||||
--file-active-badge-fg: #15803d; /* Green text for active badge */
|
||||
--file-active-badge-fg: var(--p-green-700); /* Green text for active badge */
|
||||
--file-active-badge-border: rgba(
|
||||
34,
|
||||
197,
|
||||
@@ -42,13 +36,6 @@
|
||||
--fullscreen-anim-duration-in: 0.28s;
|
||||
--fullscreen-anim-duration-out: 0.22s;
|
||||
|
||||
/* Z-index constants (added in this PR) */
|
||||
--z-over-fullscreen-surface: 1400;
|
||||
--z-fullscreen-surface: 1200;
|
||||
--z-fullscreen-favorite-star: 2;
|
||||
--z-fullscreen-icon-svg: 1;
|
||||
--z-toolpicker-star: 1;
|
||||
|
||||
/* Standard gray scale */
|
||||
--gray-50: 249 250 251;
|
||||
--gray-100: 243 244 246;
|
||||
@@ -67,108 +54,91 @@
|
||||
--border: 229 231 235;
|
||||
|
||||
/* Colors for Mantine integration */
|
||||
--color-primary-50: #eff6ff;
|
||||
--color-primary-100: #dbeafe;
|
||||
--color-primary-200: #bfdbfe;
|
||||
--color-primary-300: #93c5fd;
|
||||
--color-primary-400: #60a5fa;
|
||||
--color-primary-500: #3b82f6;
|
||||
--color-primary-600: #2563eb;
|
||||
--color-primary-700: #1d4ed8;
|
||||
--color-primary-800: #1e40af;
|
||||
--color-primary-900: #1e3a8a;
|
||||
--color-primary-50: var(--p-blue-400);
|
||||
--color-primary-100: var(--p-blue-400);
|
||||
--color-primary-200: var(--p-blue-400);
|
||||
--color-primary-300: var(--p-blue-400);
|
||||
--color-primary-400: var(--p-blue-400);
|
||||
--color-primary-500: var(--p-blue-500);
|
||||
--color-primary-600: var(--p-blue-600);
|
||||
--color-primary-700: var(--p-blue-700);
|
||||
--color-primary-800: var(--p-blue-700);
|
||||
--color-primary-900: var(--p-blue-700);
|
||||
|
||||
/* Success (green) */
|
||||
--color-green-50: #f0fdf4;
|
||||
--color-green-100: #dcfce7;
|
||||
--color-green-200: #bbf7d0;
|
||||
--color-green-300: #86efac;
|
||||
--color-green-400: #4ade80;
|
||||
--color-green-500: #22c55e;
|
||||
--color-green-600: #16a34a;
|
||||
--color-green-700: #15803d;
|
||||
--color-green-800: #166534;
|
||||
--color-green-900: #14532d;
|
||||
--color-green-50: var(--p-green-500);
|
||||
--color-green-100: var(--p-green-500);
|
||||
--color-green-200: var(--p-green-500);
|
||||
--color-green-300: var(--p-green-500);
|
||||
--color-green-400: var(--p-green-500);
|
||||
--color-green-500: var(--p-green-500);
|
||||
--color-green-600: var(--p-green-600);
|
||||
--color-green-700: var(--p-green-700);
|
||||
--color-green-800: var(--p-green-700);
|
||||
--color-green-900: var(--p-green-700);
|
||||
|
||||
/* Warning (yellow) */
|
||||
--color-yellow-50: #fefce8;
|
||||
--color-yellow-100: #fef9c3;
|
||||
--color-yellow-200: #fef08a;
|
||||
--color-yellow-300: #fde047;
|
||||
--color-yellow-400: #facc15;
|
||||
--color-yellow-50: var(--p-amber-400);
|
||||
--color-yellow-100: var(--p-amber-400);
|
||||
--color-yellow-200: var(--p-amber-400);
|
||||
--color-yellow-300: var(--p-amber-400);
|
||||
--color-yellow-400: var(--p-amber-400);
|
||||
|
||||
/* Category colors - consistent across light and dark modes */
|
||||
--category-color-removal: #ef4444; /* Red for removal tools */
|
||||
--category-color-security: #f59e0b; /* Orange for security tools */
|
||||
--category-color-formatting: #8b5cf6; /* Purple for formatting tools */
|
||||
--category-color-extraction: #06b6d4; /* Cyan for extraction tools */
|
||||
--category-color-signing: #10b981; /* Green for signing tools */
|
||||
--category-color-general: #3b82f6; /* Blue for general tools */
|
||||
--category-color-verification: #f97316; /* Orange for verification tools */
|
||||
--category-color-automation: #ec4899; /* Pink for automation tools */
|
||||
--category-color-developer: #6b7280; /* Gray for developer tools */
|
||||
--category-color-default: #6b7280; /* Default gray */
|
||||
--category-color-removal: var(--p-red-500); /* Red for removal tools */
|
||||
--category-color-security: var(--p-amber-500); /* Orange for security tools */
|
||||
--category-color-formatting: var(--p-blue-400); /* Purple for formatting tools */
|
||||
--category-color-extraction: var(--p-blue-500); /* Cyan for extraction tools */
|
||||
--category-color-signing: var(--p-green-500); /* Green for signing tools */
|
||||
--category-color-general: var(--p-blue-500); /* Blue for general tools */
|
||||
--category-color-verification: var(--p-amber-600); /* Orange for verification tools */
|
||||
--category-color-automation: var(--p-red-400); /* Pink for automation tools */
|
||||
--category-color-developer: var(--p-gray-500); /* Gray for developer tools */
|
||||
--category-color-default: var(--p-gray-500); /* Default gray */
|
||||
|
||||
/* Special section colors - consistent across light and dark modes */
|
||||
--special-color-favorites: #ffc107; /* Yellow/gold for favorites */
|
||||
--special-color-recommended: #1bb1d4; /* Cyan for recommended */
|
||||
--color-yellow-500: #eab308;
|
||||
--color-yellow-600: #ca8a04;
|
||||
--color-yellow-700: #a16207;
|
||||
--color-yellow-800: #854d0e;
|
||||
--color-yellow-900: #713f12;
|
||||
--special-color-favorites: var(--p-amber-400); /* Yellow/gold for favorites */
|
||||
--special-color-recommended: var(--p-blue-500); /* Cyan for recommended */
|
||||
--color-yellow-500: var(--p-amber-500);
|
||||
--color-yellow-600: var(--p-amber-600);
|
||||
--color-yellow-700: var(--p-amber-600);
|
||||
--color-yellow-800: var(--p-amber-600);
|
||||
--color-yellow-900: var(--p-amber-600);
|
||||
|
||||
--color-red-50: #fef2f2;
|
||||
--color-red-100: #fee2e2;
|
||||
--color-red-200: #fecaca;
|
||||
--color-red-300: #fca5a5;
|
||||
--color-red-400: #f87171;
|
||||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
--color-red-700: #b91c1c;
|
||||
--color-red-800: #991b1b;
|
||||
--color-red-900: #7f1d1d;
|
||||
--color-red-50: var(--p-red-400);
|
||||
--color-red-100: var(--p-red-400);
|
||||
--color-red-200: var(--p-red-400);
|
||||
--color-red-300: var(--p-red-400);
|
||||
--color-red-400: var(--p-red-400);
|
||||
--color-red-500: var(--p-red-500);
|
||||
--color-red-600: var(--p-red-600);
|
||||
--color-red-700: var(--p-red-600);
|
||||
--color-red-800: var(--p-red-600);
|
||||
--color-red-900: var(--p-red-600);
|
||||
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
--color-gray-400: #9ca3af;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
--color-gray-50: var(--p-gray-50);
|
||||
--color-gray-100: var(--p-gray-100);
|
||||
--color-gray-200: var(--p-gray-200);
|
||||
--color-gray-300: var(--p-gray-300);
|
||||
--color-gray-400: var(--p-gray-400);
|
||||
--color-gray-500: var(--p-gray-500);
|
||||
--color-gray-600: var(--p-gray-600);
|
||||
--color-gray-700: var(--p-gray-700);
|
||||
--color-gray-800: var(--p-gray-800);
|
||||
--color-gray-900: var(--p-gray-900);
|
||||
|
||||
/* Categorical accents (sidebar groups, file-type icons, badges). One hue per
|
||||
token; the dark block overrides each with a LIGHTER shade for contrast on
|
||||
dark backgrounds. JS consumers go through utils/accentColors.ts. */
|
||||
--accent-green: #16a34a;
|
||||
--accent-blue: #2563eb;
|
||||
--accent-red: #dc2626;
|
||||
--accent-orange: #ea580c;
|
||||
--accent-purple: #9333ea;
|
||||
--accent-teal: #0d9488;
|
||||
--accent-pink: #db2777;
|
||||
--accent-amber: #d97706;
|
||||
--accent-indigo: #4f46e5;
|
||||
--accent-cyan: #0891b2;
|
||||
--accent-violet: #7c3aed;
|
||||
--accent-gray: #6b7280;
|
||||
|
||||
/* Spacing system */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
|
||||
/* Radius system */
|
||||
--radius-xs: 2px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--accent-orange: var(--p-amber-600);
|
||||
--accent-purple: var(--p-blue-500);
|
||||
--accent-teal: var(--p-blue-700);
|
||||
--accent-pink: var(--p-red-500);
|
||||
--accent-amber: var(--p-amber-600);
|
||||
--accent-indigo: var(--p-blue-600);
|
||||
--accent-cyan: var(--p-blue-600);
|
||||
--accent-violet: var(--p-blue-600);
|
||||
--accent-gray: var(--p-gray-500);
|
||||
|
||||
/* Shadow system */
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
@@ -177,201 +147,73 @@
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Font weights */
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* Light theme semantic colors */
|
||||
--bg-surface: #ffffff;
|
||||
--bg-raised: #f9fafb;
|
||||
--bg-muted: #f3f4f6;
|
||||
--bg-background: #f9fafb;
|
||||
--bg-toolbar: #ffffff;
|
||||
--bg-file-manager: #f5f6f8;
|
||||
--bg-file-list: #ffffff;
|
||||
--btn-open-file: #0a8bff;
|
||||
--text-primary: #111827;
|
||||
--text-secondary: #4b5563;
|
||||
--text-muted: #6b7280;
|
||||
/* Always-dark text (for use on light backgrounds like alerts) - does not change in dark mode */
|
||||
--text-always-dark: #1f2937;
|
||||
--text-always-dark-muted: #6b7280;
|
||||
--border-subtle: #e5e7eb;
|
||||
--border-default: #e2e8f0;
|
||||
--border-strong: #9ca3af;
|
||||
--border-hover: #9ca3af;
|
||||
--hover-bg: #f9fafb;
|
||||
--active-bg: #f3f4f6;
|
||||
--automation-entry-hover-bg: var(--color-gray-100);
|
||||
--text-always-dark: var(--p-gray-800);
|
||||
--text-always-dark-muted: var(--p-gray-500);
|
||||
|
||||
/* Icon colors for light mode */
|
||||
--icon-user-bg: #9ca3af;
|
||||
--icon-user-color: #ffffff;
|
||||
--icon-notifications-bg: #9ca3af;
|
||||
--icon-notifications-color: #ffffff;
|
||||
--icon-tools-bg: #1e88e5;
|
||||
--icon-tools-bg: var(--p-blue-500);
|
||||
--icon-tools-color: #ffffff;
|
||||
--icon-read-bg: #4caf50;
|
||||
--icon-read-color: #ffffff;
|
||||
--icon-sign-bg: #3ba99c;
|
||||
--icon-sign-color: #ffffff;
|
||||
--icon-automate-bg: #a576e3;
|
||||
--icon-automate-color: #ffffff;
|
||||
--icon-files-bg: #d3e7f7;
|
||||
--icon-files-color: #0a8bff;
|
||||
--icon-activity-bg: #d3e7f7;
|
||||
--icon-activity-color: #0a8bff;
|
||||
--icon-config-bg: #9ca3af;
|
||||
--icon-config-color: #ffffff;
|
||||
--icon-files-color: var(--p-blue-500);
|
||||
|
||||
/* Colors for tooltips */
|
||||
--tooltip-title-bg: #dbefff;
|
||||
--tooltip-title-color: #31528e;
|
||||
--tooltip-header-bg: #31528e;
|
||||
--tooltip-header-bg: var(--p-blue-700);
|
||||
--tooltip-header-color: white;
|
||||
--tooltip-border: var(--border-default);
|
||||
|
||||
/* Inactive icon colors for light mode */
|
||||
--icon-inactive-bg: #9ca3af;
|
||||
--icon-inactive-bg: var(--p-gray-400);
|
||||
--icon-inactive-color: #ffffff;
|
||||
|
||||
/* New theme colors for text and icons */
|
||||
--tools-text-and-icon-color: var(--text-primary);
|
||||
|
||||
/* Tool picker sticky header variables (light mode) */
|
||||
--tool-header-bg: #dbefff;
|
||||
--tool-header-border: #bee2ff;
|
||||
--tool-header-text: #1e88e5;
|
||||
--tool-header-badge-bg: #c0ddff;
|
||||
--tool-header-badge-text: #004e99;
|
||||
|
||||
/* Subcategory title styling (light mode) */
|
||||
--tool-subcategory-text-color: #9ca3af; /* lighter text */
|
||||
--tool-subcategory-rule-color: #e5e7eb; /* doubly lighter rule line */
|
||||
--accent-interactive: #4a90e2;
|
||||
--text-instruction: #4a90e2;
|
||||
--text-brand: var(--color-gray-700);
|
||||
--text-brand-accent: #dc2626;
|
||||
/* doubly lighter rule line */
|
||||
--text-brand-accent: var(--p-red-600);
|
||||
|
||||
/* PDF text selection colors */
|
||||
--pdf-selection-bg: rgba(59, 130, 246, 0.2);
|
||||
--pdf-selection-ring: rgba(59, 130, 246, 0.18);
|
||||
|
||||
/* Placeholder text colors */
|
||||
--search-text-and-icon-color: #6b7382;
|
||||
--input-bg: #ffffff;
|
||||
|
||||
/* Tool panel search bar background colors */
|
||||
--tool-panel-search-bg: #eff1f4;
|
||||
--tool-panel-search-border-bottom: #eff1f4;
|
||||
--pdf-selection-bg: color-mix(in srgb, var(--p-blue-500) 20%, transparent);
|
||||
--pdf-selection-ring: color-mix(in srgb, var(--p-blue-500) 18%, transparent);
|
||||
|
||||
/* container */
|
||||
--landing-paper-bg: var(--bg-surface);
|
||||
--landing-inner-paper-bg: #eef8ff;
|
||||
--landing-inner-paper-border: #cdeaff;
|
||||
--landing-button-bg: var(--bg-surface);
|
||||
--landing-button-color: var(--icon-tools-bg);
|
||||
--landing-button-border: #e0f2f7;
|
||||
--landing-button-hover-bg: rgb(251, 251, 251);
|
||||
--landing-button-border: var(--p-blue-400);
|
||||
--landing-button-hover-bg: var(--p-gray-50);
|
||||
|
||||
/* drop state */
|
||||
--landing-drop-paper-bg: #e3f2fd;
|
||||
--landing-drop-inner-paper-bg: #bbdefb;
|
||||
--landing-drop-inner-paper-border: #90caf9;
|
||||
--landing-drop-inner-paper-bg: var(--p-blue-400);
|
||||
--landing-drop-inner-paper-border: var(--p-blue-400);
|
||||
|
||||
/* landing hero & stack */
|
||||
--landing-hero-gradient: linear-gradient(135deg, #4c8bf5 0%, #3a7be8 100%);
|
||||
--landing-hero-gradient: linear-gradient(135deg, var(--p-blue-500) 0%, var(--p-blue-500) 100%);
|
||||
--landing-stack-w: 224px;
|
||||
--landing-stack-h: 176px;
|
||||
--landing-stack-glow-bg: radial-gradient(
|
||||
circle,
|
||||
rgba(74, 144, 226, 0.18) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
|
||||
/* landing doc stack shadows */
|
||||
--landing-doc-shadow-back-idle:
|
||||
0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
--landing-doc-shadow-back-hover:
|
||||
0 12px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
--landing-doc-shadow-front-idle:
|
||||
0 8px 30px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.02);
|
||||
--landing-doc-shadow-front-hover:
|
||||
0 18px 48px rgba(0, 0, 0, 0.18), 0 8px 20px rgba(0, 0, 0, 0.1),
|
||||
0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
|
||||
/* landing action button shadows */
|
||||
--landing-action-transition:
|
||||
transform 0.28s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.32s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--landing-action-shadow-idle:
|
||||
0 6px 18px rgba(0, 0, 0, 0), 0 2px 6px rgba(0, 0, 0, 0);
|
||||
--landing-action-shadow-hover:
|
||||
0 6px 18px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.08);
|
||||
--landing-action-primary-shadow-idle:
|
||||
0 10px 26px rgba(58, 123, 232, 0), 0 4px 12px rgba(0, 0, 0, 0);
|
||||
--landing-action-primary-shadow-hover:
|
||||
0 10px 26px rgba(58, 123, 232, 0.42), 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* selected file header colors */
|
||||
--header-selected-bg: #1e88e5; /* light mode selected header matches dark */
|
||||
--header-selected-bg: var(--p-blue-500); /* light mode selected header matches dark */
|
||||
--header-selected-fg: #ffffff;
|
||||
--file-card-bg: #ffffff; /* file card background (light/dark paired) */
|
||||
--accordion-item-bg: #e8eaed; /* accordion item background - more distinguishable */
|
||||
|
||||
/* shadows */
|
||||
--drop-shadow-color: rgba(0, 0, 0, 0.08);
|
||||
--drop-shadow-color-strong: rgba(0, 0, 0, 0.04);
|
||||
--drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(0, 0, 0, 0.08))
|
||||
drop-shadow(0 0.6rem 0.6rem rgba(0, 0, 0, 0.06))
|
||||
drop-shadow(0 1.2rem 1rem rgba(0, 0, 0, 0.04));
|
||||
/* slightly more blue than dark mode header */
|
||||
|
||||
/* Light mode card hover and selection */
|
||||
--header-hover-bg: #3b4b6e; /* same family as selected, a touch muted for hover */
|
||||
--card-selected-border: #3fafff; /* slightly more blue than dark mode header */
|
||||
--checkbox-border: #2f83bf;
|
||||
--checkbox-checked-bg: #3fafff;
|
||||
--checkbox-tick: #ffffff;
|
||||
|
||||
--information-text-bg: #eaeaea;
|
||||
--information-text-color: #5e5e5e;
|
||||
/* Bulk selection panel specific colors (light mode) */
|
||||
--bulk-panel-bg: #ffffff; /* white background for parent container */
|
||||
--bulk-card-bg: #ffffff; /* white background for cards */
|
||||
--bulk-card-border: #e5e7eb; /* light gray border for cards and buttons */
|
||||
--bulk-card-hover-border: #d1d5db; /* slightly darker on hover */
|
||||
--unsupported-bar-bg: #5a616e;
|
||||
--unsupported-bar-border: #6b7280;
|
||||
--information-text-color: var(--p-zinc-400);
|
||||
/* white background for cards */
|
||||
--bulk-card-border: var(--p-gray-200); /* light gray border for cards and buttons */
|
||||
--bulk-card-hover-border: var(--p-gray-300); /* slightly darker on hover */
|
||||
|
||||
/* Config Modal colors (light mode) */
|
||||
--modal-nav-bg: #f5f6f8;
|
||||
--modal-nav-section-title: #6b7280;
|
||||
--modal-nav-item: #374151;
|
||||
--modal-nav-item-active: #0a8bff;
|
||||
--modal-nav-item-active-bg: rgba(10, 139, 255, 0.08);
|
||||
--modal-content-bg: #ffffff;
|
||||
--modal-header-border: rgba(0, 0, 0, 0.06);
|
||||
--modal-nav-item: var(--p-gray-700);
|
||||
|
||||
/* API Keys section colors (light mode) */
|
||||
--api-keys-card-bg: #ffffff;
|
||||
--api-keys-card-border: #e0e0e0;
|
||||
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
|
||||
--api-keys-input-bg: #f8f8f8;
|
||||
--api-keys-input-border: #e0e0e0;
|
||||
|
||||
/* PDF Report Colors (always light) */
|
||||
--pdf-light-header-bg: 239 246 255;
|
||||
--pdf-light-accent: 59 130 246;
|
||||
--pdf-light-text-primary: 30 41 59;
|
||||
--pdf-light-text-muted: 100 116 139;
|
||||
--pdf-light-box-bg: 248 250 252;
|
||||
--pdf-light-box-border: 226 232 240;
|
||||
--pdf-light-warning: 234 179 8;
|
||||
--pdf-light-danger: 248 113 113;
|
||||
--pdf-light-success: 34 197 94;
|
||||
--pdf-light-neutral: 148 163 184;
|
||||
--pdf-light-status-valid-bg: 209 250 229;
|
||||
--pdf-light-status-valid-text: 6 95 70;
|
||||
@@ -381,32 +223,20 @@
|
||||
--pdf-light-status-invalid-text: 153 27 27;
|
||||
--pdf-light-status-neutral-bg: 229 231 235;
|
||||
--pdf-light-status-neutral-text: 55 65 81;
|
||||
--pdf-light-report-container-bg: 249 250 251;
|
||||
--pdf-light-simulated-page-bg: 255 255 255;
|
||||
--pdf-light-simulated-page-text: 15 23 42;
|
||||
|
||||
/* Code token colors (light mode) */
|
||||
--code-kw-color: #1d4ed8; /* blue-700 */
|
||||
--code-str-color: #16a34a; /* green-600 */
|
||||
--code-num-color: #4338ca; /* indigo-700 */
|
||||
--code-com-color: #6b7280; /* gray-500 */
|
||||
/* Compare tool specific colors - only for colors that don't have existing theme pairs */
|
||||
--compare-upload-dropzone-bg: rgba(241, 245, 249, 0.45);
|
||||
--compare-upload-dropzone-border: rgba(148, 163, 184, 0.6);
|
||||
--compare-upload-icon-bg: rgba(148, 163, 184, 0.2);
|
||||
--compare-upload-icon-color: rgba(17, 24, 39, 0.75);
|
||||
--compare-upload-divider: rgba(148, 163, 184, 0.5);
|
||||
--code-kw-color: var(--p-blue-700); /* blue-700 */
|
||||
--code-str-color: var(--p-green-600); /* green-600 */
|
||||
--code-num-color: var(--p-blue-700); /* indigo-700 */
|
||||
--code-com-color: var(--p-gray-500); /* gray-500 */
|
||||
|
||||
/* Compare page label chip (light mode): slightly lighter than surrounding rows */
|
||||
--compare-page-label-bg: var(--bg-muted);
|
||||
--compare-page-label-fg: var(--text-secondary);
|
||||
|
||||
/* Status indicator dot ring shadow */
|
||||
--status-dot-ring: 0 0 0 2px rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* Input element styling */
|
||||
--input-bg: #ffffff;
|
||||
|
||||
/* Select/dropdown placeholder state */
|
||||
--select-placeholder-bg: var(--mantine-color-gray-1);
|
||||
--select-placeholder-text: var(--mantine-color-gray-6);
|
||||
@@ -414,7 +244,6 @@
|
||||
/* Grouped format dropdown */
|
||||
--dropdown-trigger-bg: var(--mantine-color-white);
|
||||
--dropdown-trigger-text: var(--mantine-color-dark-9);
|
||||
--dropdown-trigger-text-disabled: var(--mantine-color-dark-7);
|
||||
--dropdown-trigger-icon: var(--mantine-color-gray-6);
|
||||
--dropdown-panel-bg: var(--mantine-color-white);
|
||||
--dropdown-panel-border: var(--mantine-color-gray-4);
|
||||
@@ -423,211 +252,140 @@
|
||||
|
||||
/* Onboarding (light mode) */
|
||||
:root {
|
||||
--onboarding-title: #0a0a0a;
|
||||
--onboarding-body: #4a5565;
|
||||
--onboarding-step-active: #1e2939;
|
||||
--onboarding-step-inactive: #d1d5dc;
|
||||
--onboarding-step-active: var(--p-gray-800);
|
||||
--onboarding-step-inactive: var(--p-gray-300);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] {
|
||||
/* Dark theme gray scale (inverted) — navy-indigo tinted */
|
||||
--gray-50: 9 11 24;
|
||||
--gray-100: 13 16 32;
|
||||
--gray-200: 19 23 41;
|
||||
--gray-300: 28 35 64;
|
||||
--gray-400: 45 55 96;
|
||||
--gray-500: 90 98 128;
|
||||
--gray-600: 144 153 185;
|
||||
--gray-700: 195 200 230;
|
||||
--gray-800: 218 222 245;
|
||||
--gray-900: 238 241 252;
|
||||
/* Inverted dark gray scale — RGB channels for Tailwind's rgb(var(--gray-N)). */
|
||||
--gray-50: 10 10 11; /* zinc-950 */
|
||||
--gray-100: 16 16 18; /* zinc-900 */
|
||||
--gray-200: 24 24 27; /* zinc-800 */
|
||||
--gray-300: 42 42 46; /* zinc-650 */
|
||||
--gray-400: 63 63 70; /* zinc-500 */
|
||||
--gray-500: 82 82 91; /* zinc-400 */
|
||||
--gray-600: 113 113 122; /* zinc-300 */
|
||||
--gray-700: 161 161 170; /* zinc-200 */
|
||||
--gray-800: 212 212 216;
|
||||
--gray-900: 244 244 245; /* zinc-100 */
|
||||
|
||||
/* Dark semantic colors for Tailwind — aligned with portal tokens.css */
|
||||
--surface: 21 28 46; /* #151c2e — portal --color-surface */
|
||||
--background: 9 12 20; /* #090c14 — portal --color-bg */
|
||||
--border: 40 50 72; /* #283248 — portal --color-border */
|
||||
/* Dark semantic channels for Tailwind. */
|
||||
--surface: 24 24 27; /* zinc-800 */
|
||||
--background: 10 10 11; /* zinc-950 */
|
||||
--border: 42 42 46; /* zinc-650 */
|
||||
|
||||
/* Dark theme Mantine colors */
|
||||
--color-red-50: #2d1b1b;
|
||||
--color-red-100: #3a2323;
|
||||
--color-red-200: #4a2d2d;
|
||||
--color-red-300: #5c3535;
|
||||
--color-red-400: #7c4a4a;
|
||||
--color-red-500: #ef4444;
|
||||
--color-red-600: #dc2626;
|
||||
--color-red-700: #b91c1c;
|
||||
--color-red-800: #991b1b;
|
||||
--color-red-900: #7f1d1d;
|
||||
--color-red-50: var(--p-zinc-775);
|
||||
--color-red-100: var(--p-zinc-700);
|
||||
--color-red-200: var(--p-zinc-600);
|
||||
--color-red-300: var(--p-zinc-500);
|
||||
--color-red-400: var(--p-zinc-400);
|
||||
--color-red-500: var(--p-red-500);
|
||||
--color-red-600: var(--p-red-600);
|
||||
--color-red-700: var(--p-red-600);
|
||||
--color-red-800: var(--p-red-600);
|
||||
--color-red-900: var(--p-red-600);
|
||||
|
||||
--color-gray-50: #111827;
|
||||
--color-gray-100: #1f2329;
|
||||
--color-gray-200: #2a2f36;
|
||||
--color-gray-300: #374151;
|
||||
--color-gray-400: #4b5563;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #9ca3af;
|
||||
--color-gray-700: #d1d5db;
|
||||
--color-gray-800: #e5e7eb;
|
||||
--color-gray-900: #f3f4f6;
|
||||
--color-gray-50: var(--p-gray-900);
|
||||
--color-gray-100: var(--p-zinc-775);
|
||||
--color-gray-200: var(--p-zinc-650);
|
||||
--color-gray-300: var(--p-gray-700);
|
||||
--color-gray-400: var(--p-gray-600);
|
||||
--color-gray-500: var(--p-gray-500);
|
||||
--color-gray-600: var(--p-gray-400);
|
||||
--color-gray-700: var(--p-gray-300);
|
||||
--color-gray-800: var(--p-gray-200);
|
||||
--color-gray-900: var(--p-gray-100);
|
||||
|
||||
/* Categorical accents: lighter shades of the same hues, for contrast on dark. */
|
||||
--accent-green: #4ade80;
|
||||
--accent-blue: #60a5fa;
|
||||
--accent-red: #f87171;
|
||||
--accent-orange: #fb923c;
|
||||
--accent-purple: #c084fc;
|
||||
--accent-teal: #2dd4bf;
|
||||
--accent-pink: #f472b6;
|
||||
--accent-amber: #fbbf24;
|
||||
--accent-indigo: #818cf8;
|
||||
--accent-cyan: #22d3ee;
|
||||
--accent-violet: #a78bfa;
|
||||
--accent-gray: #9ca3af;
|
||||
--accent-orange: var(--p-amber-500);
|
||||
--accent-purple: var(--p-blue-400);
|
||||
--accent-teal: var(--p-blue-400);
|
||||
--accent-pink: var(--p-red-400);
|
||||
--accent-amber: var(--p-amber-400);
|
||||
--accent-indigo: var(--p-blue-400);
|
||||
--accent-cyan: var(--p-blue-400);
|
||||
--accent-violet: var(--p-blue-400);
|
||||
--accent-gray: var(--p-gray-400);
|
||||
|
||||
/* Category colors - same as light mode for consistency */
|
||||
--category-color-removal: #ef4444; /* Red for removal tools */
|
||||
--category-color-security: #f59e0b; /* Orange for security tools */
|
||||
--category-color-formatting: #8b5cf6; /* Purple for formatting tools */
|
||||
--category-color-extraction: #06b6d4; /* Cyan for extraction tools */
|
||||
--category-color-signing: #10b981; /* Green for signing tools */
|
||||
--category-color-general: #3b82f6; /* Blue for general tools */
|
||||
--category-color-verification: #f97316; /* Orange for verification tools */
|
||||
--category-color-automation: #ec4899; /* Pink for automation tools */
|
||||
--category-color-developer: #6b7280; /* Gray for developer tools */
|
||||
--category-color-default: #6b7280; /* Default gray */
|
||||
--category-color-removal: var(--p-red-500); /* Red for removal tools */
|
||||
--category-color-security: var(--p-amber-500); /* Orange for security tools */
|
||||
--category-color-formatting: var(--p-blue-400); /* Purple for formatting tools */
|
||||
--category-color-extraction: var(--p-blue-500); /* Cyan for extraction tools */
|
||||
--category-color-signing: var(--p-green-500); /* Green for signing tools */
|
||||
--category-color-general: var(--p-blue-500); /* Blue for general tools */
|
||||
--category-color-verification: var(--p-amber-600); /* Orange for verification tools */
|
||||
--category-color-automation: var(--p-red-400); /* Pink for automation tools */
|
||||
--category-color-developer: var(--p-gray-500); /* Gray for developer tools */
|
||||
--category-color-default: var(--p-gray-500); /* Default gray */
|
||||
|
||||
/* Special section colors - same as light mode for consistency */
|
||||
--special-color-favorites: #ffc107; /* Yellow/gold for favorites */
|
||||
--special-color-recommended: #1bb1d4; /* Cyan for recommended */
|
||||
--special-color-favorites: var(--p-amber-400); /* Yellow/gold for favorites */
|
||||
--special-color-recommended: var(--p-blue-500); /* Cyan for recommended */
|
||||
|
||||
/* Success (green) - dark */
|
||||
--color-green-50: #052e16;
|
||||
--color-green-100: #064e3b;
|
||||
--color-green-200: #065f46;
|
||||
--color-green-300: #047857;
|
||||
--color-green-400: #059669;
|
||||
--color-green-500: #22c55e;
|
||||
--color-green-600: #16a34a;
|
||||
--color-green-700: #4ade80;
|
||||
--color-green-800: #86efac;
|
||||
--color-green-900: #bbf7d0;
|
||||
--color-green-50: var(--p-green-700);
|
||||
--color-green-100: var(--p-green-700);
|
||||
--color-green-200: var(--p-green-700);
|
||||
--color-green-300: var(--p-green-700);
|
||||
--color-green-400: var(--p-green-600);
|
||||
--color-green-500: var(--p-green-500);
|
||||
--color-green-600: var(--p-green-600);
|
||||
--color-green-700: var(--p-green-500);
|
||||
--color-green-800: var(--p-green-500);
|
||||
--color-green-900: var(--p-green-500);
|
||||
|
||||
/* Warning (yellow) - dark */
|
||||
--color-yellow-50: #451a03;
|
||||
--color-yellow-100: #713f12;
|
||||
--color-yellow-200: #854d0e;
|
||||
--color-yellow-300: #a16207;
|
||||
--color-yellow-400: #ca8a04;
|
||||
--color-yellow-500: #eab308;
|
||||
--color-yellow-600: #facc15;
|
||||
--color-yellow-700: #fde047;
|
||||
--color-yellow-800: #fef08a;
|
||||
--color-yellow-900: #fef9c3;
|
||||
--color-yellow-50: var(--p-amber-600);
|
||||
--color-yellow-100: var(--p-amber-600);
|
||||
--color-yellow-200: var(--p-amber-600);
|
||||
--color-yellow-300: var(--p-amber-600);
|
||||
--color-yellow-400: var(--p-amber-600);
|
||||
--color-yellow-500: var(--p-amber-500);
|
||||
--color-yellow-600: var(--p-amber-400);
|
||||
--color-yellow-700: var(--p-amber-400);
|
||||
--color-yellow-800: var(--p-amber-400);
|
||||
--color-yellow-900: var(--p-amber-400);
|
||||
|
||||
/* Dark theme semantic colors — deep navy/indigo palette, aligned with portal
|
||||
tokens.css so the editor and portal (Processor) dark modes don't drift.
|
||||
Chrome surfaces (--bg-toolbar, --bg-file-manager) sit at the portal's
|
||||
sidebar level (#0d1120) so the left/right sidebars lift off the darker
|
||||
canvas (--bg-background) instead of blending into it. */
|
||||
--bg-surface: #151c2e; /* portal --color-surface */
|
||||
--bg-raised: #0d1120; /* portal --color-bg-alt */
|
||||
--bg-muted: #0d1120;
|
||||
--bg-background: #090c14; /* portal --color-bg */
|
||||
--bg-toolbar: #0d1120; /* portal --color-sidebar-bg — lifted off the canvas */
|
||||
--bg-file-manager: #0d1120; /* portal --color-sidebar-bg */
|
||||
--bg-file-list: #151c2e; /* portal --color-surface */
|
||||
--btn-open-file: #4f8ef5;
|
||||
--text-primary: #e8eaf6;
|
||||
--text-secondary: #9299b0;
|
||||
--text-muted: #5b6280;
|
||||
/* Dark theme semantic colours — the neutral zinc ramp. These legacy
|
||||
`--bg-*`/`--text-*` names are shadowed by compat.css (→ --c-*), so this is
|
||||
kept only for any consumer not yet on the semantic tokens. */
|
||||
/* Always-dark text (for use on light backgrounds like alerts) - does not change in dark mode */
|
||||
--text-always-dark: #1f2937;
|
||||
--text-always-dark-muted: #6b7280;
|
||||
--border-subtle: rgba(255, 255, 255, 0.05);
|
||||
--border-default: #1e2840; /* portal --color-border-light */
|
||||
--border-strong: #283248; /* portal --color-border */
|
||||
--border-hover: #3d4f6a; /* portal --color-border-hover */
|
||||
--hover-bg: #1c2640; /* portal --color-bg-hover */
|
||||
--active-bg: #243044; /* portal --color-bg-muted */
|
||||
--automation-entry-hover-bg: var(--color-gray-200);
|
||||
--text-always-dark: var(--p-gray-800);
|
||||
--text-always-dark-muted: var(--p-gray-500);
|
||||
|
||||
/* Icon colors for dark mode */
|
||||
--icon-user-bg: #131729;
|
||||
--icon-user-color: #6e7898;
|
||||
--icon-notifications-bg: #131729;
|
||||
--icon-notifications-color: #6e7898;
|
||||
--icon-tools-bg: #1c2340;
|
||||
--icon-tools-color: #e0e3f8;
|
||||
--icon-read-bg: #1c2340;
|
||||
--icon-read-color: #e0e3f8;
|
||||
--icon-sign-bg: #1c2340;
|
||||
--icon-sign-color: #e0e3f8;
|
||||
--icon-automate-bg: #1c2340;
|
||||
--icon-automate-color: #e0e3f8;
|
||||
--icon-files-bg: #1c2340;
|
||||
--icon-files-color: #e0e3f8;
|
||||
--icon-activity-bg: #1c2340;
|
||||
--icon-activity-color: #e0e3f8;
|
||||
--icon-config-bg: #1c2340;
|
||||
--icon-config-color: #e0e3f8;
|
||||
--icon-tools-bg: var(--p-zinc-650);
|
||||
--icon-tools-color: var(--p-blue-400);
|
||||
--icon-files-color: var(--p-blue-400);
|
||||
|
||||
/* Inactive icon colors for dark mode */
|
||||
--icon-inactive-bg: #131729;
|
||||
--icon-inactive-color: #6e7898;
|
||||
--icon-inactive-bg: var(--p-zinc-800);
|
||||
--icon-inactive-color: var(--p-gray-500);
|
||||
|
||||
/* Dark mode tooltip colors */
|
||||
--tooltip-title-bg: #1c2340;
|
||||
--tooltip-title-color: #e8eaf6;
|
||||
--tooltip-header-bg: var(--bg-raised);
|
||||
--tooltip-header-color: var(--text-primary);
|
||||
--tooltip-border: var(--border-default);
|
||||
|
||||
--accent-interactive: #e8eaf6;
|
||||
--text-instruction: #e8eaf6;
|
||||
--text-brand: var(--color-gray-800);
|
||||
--text-brand-accent: #ef4444;
|
||||
--text-brand-accent: var(--p-red-500);
|
||||
|
||||
/* Compare badge text colors (dark mode): lighter for readability */
|
||||
--spdf-compare-removed-badge-fg: var(--color-red-500);
|
||||
--spdf-compare-added-badge-fg: var(--color-green-500);
|
||||
|
||||
/* container */
|
||||
--landing-paper-bg: #090b18;
|
||||
--landing-inner-paper-bg: var(--bg-raised);
|
||||
--landing-inner-paper-border: #1c2340;
|
||||
|
||||
/* landing dark overrides */
|
||||
--landing-stack-glow-bg: radial-gradient(
|
||||
circle,
|
||||
rgba(50, 90, 210, 0.22) 0%,
|
||||
transparent 70%
|
||||
);
|
||||
--landing-doc-shadow-back-idle:
|
||||
0 4px 20px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
--landing-doc-shadow-back-hover:
|
||||
0 14px 44px rgba(0, 0, 0, 0.65), 0 6px 16px rgba(0, 0, 0, 0.45);
|
||||
--landing-doc-shadow-front-idle:
|
||||
0 8px 30px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.4),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||
--landing-doc-shadow-front-hover:
|
||||
0 20px 52px rgba(0, 0, 0, 0.7), 0 10px 24px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
--landing-button-color: #e8eaf6;
|
||||
--landing-button-hover-bg: var(--bg-raised);
|
||||
|
||||
/* selected file header colors for dark */
|
||||
--header-selected-bg: #2040a0;
|
||||
--header-selected-bg: var(--p-blue-700);
|
||||
--header-selected-fg: #ffffff;
|
||||
/* file card background (dark) */
|
||||
--file-card-bg: #0d1120;
|
||||
--accordion-item-bg: #1c2340; /* accordion item background - more distinguishable */
|
||||
|
||||
/* shadows */
|
||||
--drop-shadow-color: rgba(255, 255, 255, 0.05);
|
||||
--drop-shadow-color-strong: rgba(255, 255, 255, 0.03);
|
||||
--drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(0, 0, 0, 0.3))
|
||||
drop-shadow(0 0.6rem 0.6rem rgba(0, 0, 0, 0.2))
|
||||
drop-shadow(0 1.2rem 1rem rgba(0, 0, 0, 0.15));
|
||||
|
||||
/* Adjust shadows for dark mode */
|
||||
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
|
||||
@@ -636,87 +394,41 @@
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
|
||||
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.5);
|
||||
|
||||
--tools-text-and-icon-color: var(--text-primary);
|
||||
|
||||
/* Tool picker sticky header variables (dark mode) */
|
||||
--tool-header-bg: #131729;
|
||||
--tool-header-border: #1c2340;
|
||||
--tool-header-text: #c2c8e0;
|
||||
--tool-header-badge-bg: #1c2340;
|
||||
--tool-header-badge-text: #e8eaf6;
|
||||
|
||||
/* Subcategory title styling (dark mode) */
|
||||
--tool-subcategory-text-color: #5b6280;
|
||||
--tool-subcategory-rule-color: #1c2340;
|
||||
|
||||
/* PDF text selection colors (dark mode) */
|
||||
--pdf-selection-bg: rgba(96, 130, 250, 0.25);
|
||||
--pdf-selection-ring: rgba(96, 130, 250, 0.28);
|
||||
|
||||
/* Placeholder text colors (dark mode) */
|
||||
--search-text-and-icon-color: #e8eaf6 !important;
|
||||
--input-bg: #1c2340;
|
||||
--pdf-selection-bg: color-mix(in srgb, var(--p-blue-400) 25%, transparent);
|
||||
--pdf-selection-ring: color-mix(in srgb, var(--p-blue-400) 28%, transparent);
|
||||
|
||||
/* Tool panel search bar background colors (dark mode) */
|
||||
--tool-panel-search-bg: #0d1020;
|
||||
--tool-panel-search-border-bottom: #1c2340;
|
||||
|
||||
--information-text-bg: #131729;
|
||||
--information-text-color: #e0e3f8;
|
||||
--information-text-color: var(--p-blue-400);
|
||||
|
||||
/* Bulk selection panel specific colors (dark mode) */
|
||||
--bulk-panel-bg: var(--bg-raised); /* dark background for parent container */
|
||||
--bulk-card-bg: var(--bg-raised); /* dark background for cards */
|
||||
/* dark background for cards */
|
||||
--bulk-card-border: var(
|
||||
--border-default
|
||||
); /* default border for cards and buttons */
|
||||
--bulk-card-hover-border: var(--border-strong); /* stronger border on hover */
|
||||
--unsupported-bar-bg: #0d1020;
|
||||
--unsupported-bar-border: #1c2340;
|
||||
|
||||
/* Config Modal colors (dark mode) */
|
||||
--modal-nav-bg: #0d1020;
|
||||
--modal-nav-section-title: #5b6280;
|
||||
--modal-nav-item: #c2c8e0;
|
||||
--modal-nav-item-active: #4f8ef5;
|
||||
--modal-nav-item-active-bg: rgba(79, 142, 245, 0.15);
|
||||
--modal-content-bg: #131729;
|
||||
--modal-header-border: rgba(255, 255, 255, 0.05);
|
||||
--modal-nav-item: var(--p-gray-300);
|
||||
|
||||
/* Onboarding (dark mode) */
|
||||
--onboarding-title: #e8eaf6;
|
||||
--onboarding-body: #9299b0;
|
||||
--onboarding-step-active: #d8dcf0;
|
||||
--onboarding-step-inactive: #2d3560;
|
||||
--onboarding-step-active: var(--p-gray-250);
|
||||
--onboarding-step-inactive: var(--p-zinc-500);
|
||||
/* API Keys section colors (dark mode) */
|
||||
--api-keys-card-bg: #131729;
|
||||
--api-keys-card-border: #1c2340;
|
||||
--api-keys-card-shadow: none;
|
||||
--api-keys-input-bg: #0d1020;
|
||||
--api-keys-input-border: #1c2340;
|
||||
|
||||
/* Code token colors (dark mode - Cursor-like) */
|
||||
--code-kw-color: #c792ea; /* purple */
|
||||
--code-str-color: #c3e88d; /* green */
|
||||
--code-num-color: #f78c6c; /* orange */
|
||||
--code-com-color: #697098; /* muted gray-blue */
|
||||
/* Compare tool specific colors (dark mode) - only for colors that don't have existing theme pairs */
|
||||
--compare-upload-dropzone-bg: rgba(9, 11, 24, 0.45);
|
||||
--compare-upload-dropzone-border: rgba(28, 35, 64, 0.6);
|
||||
--compare-upload-icon-bg: rgba(28, 35, 64, 0.2);
|
||||
--compare-upload-icon-color: rgba(232, 234, 246, 0.75);
|
||||
--compare-upload-divider: rgba(28, 35, 64, 0.5);
|
||||
--code-kw-color: var(--p-blue-400); /* purple */
|
||||
--code-str-color: var(--p-green-500); /* green */
|
||||
--code-num-color: var(--p-red-400); /* orange */
|
||||
--code-com-color: var(--p-gray-500); /* muted gray-blue */
|
||||
|
||||
/* Compare page label chip (dark mode): slightly darker than surrounding rows */
|
||||
--compare-page-label-bg: #0d1020;
|
||||
--compare-page-label-fg: var(--text-secondary);
|
||||
|
||||
/* Status indicator dot ring shadow (dark) */
|
||||
--status-dot-ring: 0 0 0 2px rgba(255, 255, 255, 0.15);
|
||||
|
||||
/* Input element styling (dark) */
|
||||
--input-bg: #1c2340;
|
||||
|
||||
/* Select/dropdown placeholder state (dark) */
|
||||
--select-placeholder-bg: var(--mantine-color-dark-5);
|
||||
--select-placeholder-text: var(--mantine-color-dark-2);
|
||||
@@ -724,7 +436,6 @@
|
||||
/* Grouped format dropdown (dark) */
|
||||
--dropdown-trigger-bg: var(--mantine-color-dark-6);
|
||||
--dropdown-trigger-text: var(--mantine-color-dark-0);
|
||||
--dropdown-trigger-text-disabled: var(--mantine-color-dark-1);
|
||||
--dropdown-trigger-icon: var(--mantine-color-dark-2);
|
||||
--dropdown-panel-bg: var(--mantine-color-dark-7);
|
||||
--dropdown-panel-border: var(--mantine-color-dark-4);
|
||||
@@ -739,7 +450,6 @@
|
||||
|
||||
/* Plan section card borders - only override in dark mode */
|
||||
[data-mantine-color-scheme="dark"] .plan-card {
|
||||
--paper-border-color: rgb(44, 52, 120) !important;
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .plan-card [data-size="sm"] {
|
||||
@@ -809,22 +519,22 @@
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Smooth transitions for theme switching */
|
||||
/* Smooth transitions for theme switching. Text `color` is deliberately EXCLUDED:
|
||||
it must swap instantly, never fade white↔black through grey. */
|
||||
* {
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
/* ── PDF Link Overlay (viewer) ── */
|
||||
:root {
|
||||
--link-hover-bg: rgba(10, 139, 255, 0.1);
|
||||
--link-hover-border: rgba(10, 139, 255, 0.32);
|
||||
--link-hover-bg: color-mix(in srgb, var(--p-blue-500) 10%, transparent);
|
||||
--link-hover-border: color-mix(in srgb, var(--p-blue-500) 32%, transparent);
|
||||
--link-hover-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.35),
|
||||
0 1px 4px rgba(10, 139, 255, 0.12);
|
||||
--link-focus-ring: rgba(10, 139, 255, 0.25);
|
||||
--link-toolbar-bg: rgba(15, 23, 42, 0.88);
|
||||
--link-focus-ring: color-mix(in srgb, var(--p-blue-500) 25%, transparent);
|
||||
--link-toolbar-bg: color-mix(in srgb, var(--p-blue-700) 88%, transparent);
|
||||
--link-toolbar-border: rgba(255, 255, 255, 0.1);
|
||||
--link-toolbar-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.16),
|
||||
@@ -832,12 +542,12 @@
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] {
|
||||
--link-hover-bg: rgba(79, 130, 245, 0.14);
|
||||
--link-hover-border: rgba(79, 130, 245, 0.38);
|
||||
--link-hover-bg: color-mix(in srgb, var(--p-blue-500) 14%, transparent);
|
||||
--link-hover-border: color-mix(in srgb, var(--p-blue-500) 38%, transparent);
|
||||
--link-hover-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.1), 0 1px 4px rgba(79, 142, 245, 0.18);
|
||||
--link-focus-ring: rgba(79, 130, 245, 0.3);
|
||||
--link-toolbar-bg: rgba(9, 11, 24, 0.94);
|
||||
--link-focus-ring: color-mix(in srgb, var(--p-blue-500) 30%, transparent);
|
||||
--link-toolbar-bg: color-mix(in srgb, var(--p-blue-700) 94%, transparent);
|
||||
--link-toolbar-border: rgba(255, 255, 255, 0.07);
|
||||
--link-toolbar-shadow:
|
||||
0 12px 40px rgba(0, 0, 0, 0.5), 0 4px 12px rgba(0, 0, 0, 0.35),
|
||||
@@ -1021,7 +731,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
--shadow-color: rgba(15, 23, 42, 0.55);
|
||||
--shadow-color: color-mix(in srgb, var(--p-blue-700) 55%, transparent);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -1039,9 +749,8 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Theme-aware image display utilities */
|
||||
/* Use .theme-img-light-only for images to show in light mode only */
|
||||
/* Use .theme-img-dark-only for images to show in dark mode only */
|
||||
/* Theme-aware image display utilities: .theme-img-light-only shows in light
|
||||
mode only, .theme-img-dark-only shows in dark mode only. */
|
||||
.theme-img-light-only {
|
||||
display: inline;
|
||||
}
|
||||
@@ -1105,8 +814,8 @@
|
||||
fill: var(--mantine-color-body);
|
||||
}
|
||||
|
||||
/* Wordmark muted variant (used in empty/placeholder states) */
|
||||
/* Stirling text: 30% opacity in light mode, full opacity in dark */
|
||||
/* Wordmark muted variant (empty/placeholder states).
|
||||
Stirling text: 30% opacity in light mode, full opacity in dark. */
|
||||
.wordmark__text--muted {
|
||||
fill-opacity: 0.3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Theme system (`core/theme/`)
|
||||
|
||||
Read this before touching colours or theming anywhere in the frontend.
|
||||
|
||||
## TL;DR rules
|
||||
|
||||
1. **Never write a raw colour** (`#hex`, `rgb()`, `hsl()`, or a named colour like `red`) in a component, inline style, or stylesheet. Use a token:
|
||||
- `var(--c-…)` — a **semantic** token (preferred: `--c-text`, `--c-surface`, `--c-primary`, …).
|
||||
- `var(--p-…)` — a **palette** primitive, only when no semantic token fits.
|
||||
2. **The only file allowed to contain literal colours is `primitives.css`.** If you need a new hue, add it there first, then reference it.
|
||||
3. Structural `black` / `white` / `transparent` (shadows, scrims, overlays) are allowed anywhere.
|
||||
4. Colours must adapt to light/dark automatically. If you're reaching for a hardcoded colour "just for dark mode", you're doing it wrong — pick the right `--c-*` token.
|
||||
5. `task frontend:lint:colors` enforces rules 1–3 **inside `core/theme/`** (see [Linter](#linter)).
|
||||
|
||||
## Files
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `primitives.css` | **The palette.** 41 literal colours (`--p-*`) — one neutral ramp (`--p-gray-*` light, `--p-zinc-*` dark) + status hues (blue/green/amber/red). The ONLY place literals live. |
|
||||
| `colors.css` | **Semantic tokens** (`--c-*`), mapped from primitives per theme. This is what you should reference. |
|
||||
| `compat.css` | **Legacy-name aliases** → `--c-*`. Lets old files keep their `--bg-surface` / `--color-text-1` / etc. names working. Uses `:root:root` so it out-ranks legacy files. |
|
||||
| `dimensions.css` | Non-colour tokens: spacing, radius, z-index, type, motion. |
|
||||
| `index.css` | Barrel that `@import`s the above. Imported by `ThemeProvider` (and Storybook). |
|
||||
| `mantineTheme.ts` | Mantine theme object wiring. |
|
||||
|
||||
Two **legacy** colour files still exist outside this folder and are being phased out — prefer `--c-*` over their tokens, and don't add to them:
|
||||
`core/styles/theme.css` (editor `--bg-*`/`--text-*` vocab) and `core/tokens/tokens.css` (SUI `--color-*` vocab, gradients, code palette). `compat.css` already overrides their surface/text/border tokens, so most of what they define is dead weight.
|
||||
|
||||
## Semantic tokens (`--c-*`)
|
||||
|
||||
Reference these, not primitives, wherever possible:
|
||||
|
||||
- **Surfaces (elevation):** `--c-bg` (canvas) < `--c-bg-raised` (sidebars/toolbars) < `--c-surface` (cards/modals) < `--c-surface-raised` < `--c-surface-sunken`; plus `--c-input-bg`, `--c-hover`, `--c-active`, `--c-overlay`.
|
||||
- **Text:** `--c-text`, `--c-text-muted`, `--c-text-subtle`, `--c-text-on-primary` (foreground on a filled primary).
|
||||
- **Borders:** `--c-border`, `--c-border-subtle`, `--c-border-strong`.
|
||||
- **Accent:** `--c-primary`, `--c-primary-hover`, `--c-primary-subtle`, and `--c-accent-fg` (see below).
|
||||
- **Status:** `--c-success`, `--c-danger`.
|
||||
|
||||
## Theme model
|
||||
|
||||
The **mode** and the **accent colour** are independent.
|
||||
|
||||
- `preferences.theme` is the mode: `light` | `dark` | `system` (System follows the OS). There is no separate "custom" or "midnight" mode any more.
|
||||
- Light and dark each have their **own accent**: `preferences.lightPrimary` / `preferences.darkPrimary` (both default `#3b82f6` blue).
|
||||
- `ThemeProvider` resolves the mode to a concrete `light`/`dark` base, picks that side's accent, and **always** sets `data-app-theme="custom"` on `<html>`. So the custom-tint blocks in `colors.css` are the only themed blocks that apply — the chosen accent drives every accent **and** a subtle app-wide surface tint. With the default blue the tint is near-neutral.
|
||||
- Selection attributes on `<html>`: `data-theme` = `light|dark` (SUI + the tint blocks), `data-mantine-color-scheme` = `light|dark` (Mantine).
|
||||
|
||||
### Custom-theme contrast guardrails (`core/utils/customPrimary.ts`)
|
||||
|
||||
Because the accent is user-chosen, `deriveAccessiblePrimary(pick, base)` clamps it and injects three vars on `<html>`:
|
||||
|
||||
- `--user-primary` → `--c-primary`. Lightness-clamped so it can't collapse into the base (dark floor `L ≥ 0.42`, light ceil `L ≤ 0.6`). Used for **fills**.
|
||||
- `--user-primary-on` → `--c-text-on-primary`. White by default; flips to **black only for genuinely light picks** (relative-luminance cutoff `0.62`, so saturated amber/green/cyan keep white text).
|
||||
- `--user-accent-fg` → `--c-accent-fg`. The accent tuned as a **foreground** (text/icon on the app surface): forced light on dark bases (`L ≥ 0.62`), dark on light (`L ≤ 0.45`), so accent text never goes dark-on-dark.
|
||||
|
||||
**Rule of thumb:** a filled control's background uses `--c-primary` with its label on `--c-text-on-primary`; an accent used **as text/icon on a surface** (nav selection, links, tool-header text) uses `--c-accent-fg`. Because `compat.css` (`:root:root`) wins on specificity, accent-as-text legacy tokens are routed there as `var(--c-accent-fg, var(--c-primary))` — the fallback keeps non-custom builds on the raw primary.
|
||||
|
||||
The FAB / logo mark is a deliberate exception: it's pinned to white (`--p-white`), not the on-primary flip — a brand mark, not body text.
|
||||
|
||||
## Linter
|
||||
|
||||
One file: `editor/scripts/lint/theme-lint.mjs` (no baseline). Run via `task frontend:lint:colors` (part of `task frontend:lint`).
|
||||
|
||||
- **Default (blocking):** enforces "literals only in `primitives.css`; everything else in `core/theme/` references tokens; no duplicate primitives." Scope is deliberately just `core/theme/` — the layer this owns, which is clean.
|
||||
- **`node theme-lint.mjs contrast`** (task `frontend:contrast`, non-blocking): WCAG contrast report for text-on-surface / on-primary pairs per theme.
|
||||
|
||||
App-wide "no hardcoded colours in components" is **not** enforced yet (there are 260+ legacy sites); that's a separate migration. Don't add new hardcoded colours regardless.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`--mantine-*` vars are consumed by Mantine at runtime**, not via our `var()`. A source scan can't see their use — never delete them as "unused", and set them (not raw colours) when overriding Mantine.
|
||||
- **`--accent-*` (categorical hues) are used dynamically** via `` `var(--accent-${hue})` `` in `utils/accentColors.ts`. A literal search won't find them — don't treat them as unused.
|
||||
- **Tailwind consumes some vars** (`--gray-*`, `--color-*`, `--background`, `--border`) via `editor/tailwind.config.js` using `rgb(var(--x))`. That file is outside the usual scan roots — check it before removing those.
|
||||
- **Specificity:** `compat.css` uses `:root:root` (0,2,0) to beat legacy files. If a token you set in `colors.css` (`html[data-app-theme="custom"]`, 0,1,1) isn't winning, a `:root:root` or a `[data-mantine-color-scheme]`-compound block is probably overriding it — set it in `compat.css` or the more specific block.
|
||||
- **Adding a colour:** put the literal in `primitives.css`, map it to a `--c-*` in `colors.css` if it's a new semantic role, and reference the `--c-*` (or an existing `compat` alias) from components. Don't skip straight to a `--p-*` in a component unless there's genuinely no semantic fit.
|
||||
@@ -0,0 +1,206 @@
|
||||
/* COLORS — canonical semantic core (~21 --c-* tokens) mapped from primitives.css per theme; compat.css aliases legacy names onto these. Editor: always data-app-theme="custom" + data-mantine-color-scheme=light|dark. Portal/Storybook: data-theme=light|dark → LIGHT/MIDNIGHT. */
|
||||
/* Surface elevation: --c-bg (canvas) < --c-bg-raised (sidebars) < --c-surface (cards) < --c-surface-raised < --c-surface-sunken; plus --c-input-bg, --c-hover, --c-active, --c-overlay. */
|
||||
|
||||
/* ── LIGHT ───────────────────────────────────────────────────────────────── */
|
||||
:root,
|
||||
[data-theme="light"],
|
||||
html[data-app-theme="light"] {
|
||||
--c-bg: var(--p-gray-50);
|
||||
--c-bg-raised: var(--p-white);
|
||||
--c-surface: var(--p-white);
|
||||
--c-surface-raised: var(--p-white);
|
||||
--c-surface-sunken: var(--p-gray-100);
|
||||
--c-input-bg: var(--p-white);
|
||||
--c-hover: var(--p-gray-50);
|
||||
--c-active: var(--p-gray-100);
|
||||
--c-overlay: rgba(0, 0, 0, 0.5);
|
||||
|
||||
--c-text: var(--p-gray-900);
|
||||
--c-text-muted: var(--p-gray-600);
|
||||
--c-text-subtle: var(--p-gray-500);
|
||||
--c-text-on-primary: var(--p-white);
|
||||
|
||||
--c-border: var(--p-gray-250);
|
||||
--c-border-subtle: var(--p-gray-200);
|
||||
--c-border-strong: var(--p-gray-400);
|
||||
|
||||
--c-primary: var(--p-blue-500);
|
||||
--c-primary-hover: var(--p-blue-600);
|
||||
--c-primary-subtle: color-mix(in srgb, var(--p-blue-500) 10%, transparent);
|
||||
|
||||
--c-success: var(--p-green-600);
|
||||
--c-danger: var(--p-red-600);
|
||||
}
|
||||
|
||||
/* ── MIDNIGHT (original navy) — also the default portal/Storybook dark ────── */
|
||||
[data-theme="dark"],
|
||||
html[data-app-theme="midnight"] {
|
||||
--c-bg: var(--p-zinc-900);
|
||||
--c-bg-raised: var(--p-zinc-850);
|
||||
--c-surface: var(--p-zinc-800);
|
||||
--c-surface-raised: var(--p-zinc-650);
|
||||
--c-surface-sunken: var(--p-zinc-850);
|
||||
--c-input-bg: var(--p-zinc-650);
|
||||
--c-hover: var(--p-gray-800);
|
||||
--c-active: var(--p-gray-800);
|
||||
--c-overlay: rgba(0, 0, 0, 0.6);
|
||||
|
||||
--c-text: var(--p-zinc-100);
|
||||
--c-text-muted: var(--p-zinc-200);
|
||||
--c-text-subtle: var(--p-zinc-300);
|
||||
--c-text-on-primary: var(--p-white);
|
||||
|
||||
--c-border: var(--p-zinc-650);
|
||||
--c-border-subtle: rgba(255, 255, 255, 0.05);
|
||||
--c-border-strong: var(--p-zinc-500);
|
||||
|
||||
--c-primary: var(--p-blue-500);
|
||||
--c-primary-hover: var(--p-blue-700);
|
||||
--c-primary-subtle: color-mix(in srgb, var(--p-blue-500) 15%, transparent);
|
||||
|
||||
--c-success: var(--p-green-500);
|
||||
--c-danger: var(--p-red-500);
|
||||
}
|
||||
|
||||
/* ── CUSTOM — --user-primary drives every accent + a surface tint (whole app takes the hue); the [data-accent="default"] blocks at the end opt surfaces back to neutral. LIGHT base here, DARK base next. ── */
|
||||
html[data-app-theme="custom"] {
|
||||
--c-primary: var(--user-primary, var(--p-blue-500));
|
||||
--c-primary-hover: color-mix(in srgb, var(--c-primary) 85%, var(--p-black));
|
||||
--c-primary-subtle: color-mix(in srgb, var(--c-primary) 14%, transparent);
|
||||
/* Foreground on the filled primary — contrast-picked in JS (flips white/black). */
|
||||
--c-text-on-primary: var(--user-primary-on, var(--p-white));
|
||||
/* Accent as text/icon on a surface — JS lightness-clamped for legibility; fills/borders keep raw --c-primary. */
|
||||
--c-accent-fg: var(--user-accent-fg, var(--c-primary));
|
||||
|
||||
/* Primary-tinted surfaces (light base). Neutralised by the default override. */
|
||||
--c-bg: color-mix(in srgb, var(--c-primary) 7%, var(--p-gray-50));
|
||||
--c-bg-raised: color-mix(in srgb, var(--c-primary) 4%, var(--p-white));
|
||||
--c-surface: color-mix(in srgb, var(--c-primary) 3%, var(--p-white));
|
||||
--c-surface-raised: color-mix(in srgb, var(--c-primary) 4%, var(--p-white));
|
||||
--c-surface-sunken: color-mix(in srgb, var(--c-primary) 8%, var(--p-gray-100));
|
||||
--c-input-bg: color-mix(in srgb, var(--c-primary) 2%, var(--p-white));
|
||||
--c-hover: color-mix(in srgb, var(--c-primary) 9%, var(--p-gray-50));
|
||||
--c-active: color-mix(in srgb, var(--c-primary) 13%, var(--p-gray-100));
|
||||
--c-border: color-mix(in srgb, var(--c-primary) 14%, var(--p-gray-250));
|
||||
--c-border-subtle: color-mix(in srgb, var(--c-primary) 10%, var(--p-gray-200));
|
||||
|
||||
/* Mantine primary ramp (--color-primary-*) derived from --c-primary. */
|
||||
--color-primary-50: color-mix(in srgb, var(--c-primary) 12%, var(--p-white));
|
||||
--color-primary-100: color-mix(in srgb, var(--c-primary) 20%, var(--p-white));
|
||||
--color-primary-200: color-mix(in srgb, var(--c-primary) 35%, var(--p-white));
|
||||
--color-primary-300: color-mix(in srgb, var(--c-primary) 55%, var(--p-white));
|
||||
--color-primary-400: color-mix(in srgb, var(--c-primary) 78%, var(--p-white));
|
||||
--color-primary-500: var(--c-primary);
|
||||
--color-primary-600: color-mix(in srgb, var(--c-primary) 88%, var(--p-black));
|
||||
--color-primary-700: color-mix(in srgb, var(--c-primary) 74%, var(--p-black));
|
||||
--color-primary-800: color-mix(in srgb, var(--c-primary) 60%, var(--p-black));
|
||||
--color-primary-900: color-mix(in srgb, var(--c-primary) 46%, var(--p-black));
|
||||
--mantine-primary-color-filled: var(--c-primary);
|
||||
--mantine-primary-color-filled-hover: var(--c-primary-hover);
|
||||
/* Mantine "light" variant surfaces (chips, subtle buttons, some panels). */
|
||||
--mantine-primary-color-light: color-mix(in srgb, var(--c-primary) 12%, transparent);
|
||||
--mantine-primary-color-light-hover: color-mix(in srgb, var(--c-primary) 18%, transparent);
|
||||
--mantine-primary-color-light-color: var(--c-primary);
|
||||
|
||||
/* Brand-tint family — re-derived from --c-primary so legacy accents harmonise to the chosen hue. */
|
||||
|
||||
/* Fills / borders — text uses --c-text-on-primary. */
|
||||
--btn-open-file: var(--c-primary);
|
||||
--header-selected-bg: var(--c-primary);
|
||||
--header-selected-fg: var(--c-text-on-primary);
|
||||
--checkbox-checked-bg: var(--c-primary);
|
||||
--card-selected-border: var(--c-primary);
|
||||
|
||||
/* Accent-as-text/icon tokens live in compat.css (:root:root wins); only --icon-files-color is owned here. */
|
||||
--icon-files-color: var(--c-accent-fg);
|
||||
|
||||
/* Subtle surface tints (mix with the base surface so they adapt light/dark) */
|
||||
--tool-header-border: color-mix(in srgb, var(--c-primary) 28%, var(--c-surface));
|
||||
--tool-header-badge-bg: color-mix(in srgb, var(--c-primary) 20%, var(--c-surface));
|
||||
--tooltip-title-bg: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
|
||||
--landing-inner-paper-bg: color-mix(in srgb, var(--c-primary) 8%, var(--c-surface));
|
||||
--landing-inner-paper-border: color-mix(in srgb, var(--c-primary) 25%, var(--c-surface));
|
||||
--landing-button-border: color-mix(in srgb, var(--c-primary) 22%, var(--c-surface));
|
||||
--landing-hero-gradient: linear-gradient(
|
||||
135deg,
|
||||
var(--c-primary) 0%,
|
||||
var(--c-primary-hover) 100%
|
||||
);
|
||||
|
||||
/* Translucent state tints */
|
||||
--color-nav-active: color-mix(in srgb, var(--c-primary) 12%, transparent);
|
||||
--modal-nav-item-active-bg: color-mix(in srgb, var(--c-primary) 10%, transparent);
|
||||
--pdf-selection-bg: color-mix(in srgb, var(--c-primary) 20%, transparent);
|
||||
--pdf-selection-ring: color-mix(in srgb, var(--c-primary) 28%, transparent);
|
||||
--tool-panel-search-bg: color-mix(in srgb, var(--c-primary) 6%, var(--c-surface));
|
||||
}
|
||||
|
||||
/* ── DARK — editor dark theme: neutral text/borders/icons + accent-tinted surfaces (default override opts out). After :root so it wins for dark. ── */
|
||||
html[data-app-theme="custom"][data-mantine-color-scheme="dark"] {
|
||||
/* Neutral text / borders / overlay (not accent-tinted). */
|
||||
--c-text: var(--p-zinc-100);
|
||||
--c-text-muted: var(--p-zinc-200);
|
||||
--c-text-subtle: var(--p-zinc-300);
|
||||
--c-border-strong: var(--p-zinc-500);
|
||||
--c-overlay: rgba(0, 0, 0, 0.6);
|
||||
|
||||
/* Accent-tinted surfaces (dark base). Neutralised by the default override. */
|
||||
--c-bg: color-mix(in srgb, var(--c-primary) 8%, var(--p-zinc-950));
|
||||
--c-bg-raised: color-mix(in srgb, var(--c-primary) 9%, var(--p-zinc-850));
|
||||
--c-surface: color-mix(in srgb, var(--c-primary) 8%, var(--p-zinc-800));
|
||||
--c-surface-raised: color-mix(in srgb, var(--c-primary) 9%, var(--p-zinc-775));
|
||||
--c-surface-sunken: color-mix(in srgb, var(--c-primary) 8%, var(--p-zinc-900));
|
||||
--c-input-bg: color-mix(in srgb, var(--c-primary) 7%, var(--p-zinc-900));
|
||||
--c-hover: color-mix(in srgb, var(--c-primary) 12%, var(--p-zinc-750));
|
||||
--c-active: color-mix(in srgb, var(--c-primary) 15%, var(--p-zinc-700));
|
||||
--c-border: color-mix(in srgb, var(--c-primary) 14%, var(--p-zinc-650));
|
||||
--c-border-subtle: color-mix(in srgb, var(--c-primary) 10%, var(--p-zinc-700));
|
||||
|
||||
/* Dark-tuned status shades (lighter than the light-theme :root values). */
|
||||
--c-success: var(--p-green-500);
|
||||
--c-danger: var(--p-red-400);
|
||||
|
||||
/* Category icon chips → neutral. */
|
||||
--icon-tools-bg: var(--c-surface-raised);
|
||||
--icon-inactive-bg: var(--c-surface-raised);
|
||||
--icon-tools-color: var(--c-text-muted);
|
||||
--icon-files-color: var(--c-text-muted);
|
||||
--icon-inactive-color: var(--c-text-muted);
|
||||
--onboarding-step-inactive: var(--c-border-strong);
|
||||
|
||||
/* Point Mantine's dark ramp at the --c-* surfaces so components follow the theme. */
|
||||
--mantine-color-dark-4: var(--c-border);
|
||||
--mantine-color-dark-5: var(--c-surface-raised);
|
||||
--mantine-color-dark-6: var(--c-surface);
|
||||
--mantine-color-dark-7: var(--c-bg);
|
||||
--mantine-color-body: var(--c-bg);
|
||||
--mantine-color-default: var(--c-surface);
|
||||
--mantine-color-default-hover: var(--c-hover);
|
||||
}
|
||||
|
||||
/* ── DEFAULT (no tint) — surfaces opt out of the accent tint (neutral white/grey light, zinc dark); --c-primary stays for buttons. Extra [data-accent="default"] beats the tinted blocks. ── */
|
||||
html[data-app-theme="custom"][data-accent="default"] {
|
||||
--c-bg: var(--p-gray-50);
|
||||
--c-bg-raised: var(--p-white);
|
||||
--c-surface: var(--p-white);
|
||||
--c-surface-raised: var(--p-white);
|
||||
--c-surface-sunken: var(--p-gray-100);
|
||||
--c-input-bg: var(--p-white);
|
||||
--c-hover: var(--p-gray-50);
|
||||
--c-active: var(--p-gray-100);
|
||||
--c-border: var(--p-gray-250);
|
||||
--c-border-subtle: var(--p-gray-200);
|
||||
}
|
||||
|
||||
html[data-app-theme="custom"][data-accent="default"][data-mantine-color-scheme="dark"] {
|
||||
--c-bg: var(--p-zinc-950);
|
||||
--c-bg-raised: var(--p-zinc-850);
|
||||
--c-surface: var(--p-zinc-800);
|
||||
--c-surface-raised: var(--p-zinc-775);
|
||||
--c-surface-sunken: var(--p-zinc-900);
|
||||
--c-input-bg: var(--p-zinc-900);
|
||||
--c-hover: var(--p-zinc-750);
|
||||
--c-active: var(--p-zinc-700);
|
||||
--c-border: var(--p-zinc-650);
|
||||
--c-border-subtle: var(--p-zinc-700);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/* COMPAT — legacy token names (editor --bg-*/--text-* and SUI/portal --color-*) aliased onto the canonical --c-* set so existing files keep working. :root:root (0,2,0) out-ranks the legacy blocks; aliases are theme-agnostic. */
|
||||
|
||||
:root:root {
|
||||
/* ── Canvas (darkest / centre) ── */
|
||||
--bg-background: var(--c-bg);
|
||||
--bg-file-manager: var(--c-bg);
|
||||
--color-bg: var(--c-bg);
|
||||
|
||||
/* ── Raised chrome — sidebars, toolbars, headers, side nav. ── */
|
||||
--bg-toolbar: var(--c-bg-raised);
|
||||
--color-sidebar-bg: var(--c-bg-raised);
|
||||
--color-header-bg: var(--c-bg-raised);
|
||||
--modal-nav-bg: var(--c-bg-raised);
|
||||
--unsupported-bar-bg: var(--c-bg-raised);
|
||||
|
||||
/* ── Surfaces — cards, modals, dropdowns, popovers ── */
|
||||
--bg-surface: var(--c-surface);
|
||||
--bg-file-list: var(--c-surface);
|
||||
--modal-content-bg: var(--c-surface);
|
||||
--bulk-panel-bg: var(--c-surface);
|
||||
--bulk-card-bg: var(--c-surface);
|
||||
--api-keys-card-bg: var(--c-surface);
|
||||
--color-surface: var(--c-surface);
|
||||
--color-dropdown-bg: var(--c-surface);
|
||||
--information-text-bg: var(--c-surface);
|
||||
|
||||
--bg-raised: var(--c-surface-raised);
|
||||
--file-card-bg: var(--c-surface-raised);
|
||||
--color-surface-alt: var(--c-surface-raised);
|
||||
--landing-inner-paper-bg: var(--c-surface-raised);
|
||||
--accordion-item-bg: var(--c-surface-raised);
|
||||
|
||||
--bg-muted: var(--c-surface-sunken);
|
||||
--color-bg-muted: var(--c-surface-sunken);
|
||||
--tool-panel-search-bg: var(--c-surface-sunken);
|
||||
--compare-page-label-bg: var(--c-surface-sunken);
|
||||
|
||||
/* ── Inputs ── */
|
||||
--input-bg: var(--c-input-bg);
|
||||
--api-keys-input-bg: var(--c-input-bg);
|
||||
|
||||
/* ── Text ── */
|
||||
--text-primary: var(--c-text);
|
||||
--color-text-1: var(--c-text);
|
||||
--color-header-text: var(--c-text);
|
||||
--color-logo-text: var(--c-text);
|
||||
--tools-text-and-icon-color: var(--c-text);
|
||||
|
||||
--text-secondary: var(--c-text-muted);
|
||||
--color-text-2: var(--c-text-muted);
|
||||
--color-nav-hover-text: var(--c-text-muted);
|
||||
|
||||
--text-muted: var(--c-text-subtle);
|
||||
--color-text-3: var(--c-text-subtle);
|
||||
--color-text-4: var(--c-text-subtle);
|
||||
--color-text-5: var(--c-text-subtle);
|
||||
--color-text-6: var(--c-text-subtle);
|
||||
--color-text-muted: var(--c-text-subtle);
|
||||
--color-nav-text: var(--c-text-subtle);
|
||||
--search-text-and-icon-color: var(--c-text-subtle);
|
||||
|
||||
/* ── Borders ── */
|
||||
--border-default: var(--c-border);
|
||||
--color-border: var(--c-border);
|
||||
--color-border-input: var(--c-border);
|
||||
|
||||
--border-subtle: var(--c-border-subtle);
|
||||
--color-border-light: var(--c-border-subtle);
|
||||
--color-divider: var(--c-border-subtle);
|
||||
|
||||
--border-strong: var(--c-border-strong);
|
||||
--border-hover: var(--c-border-strong);
|
||||
--color-border-hover: var(--c-border-strong);
|
||||
|
||||
/* More borders/dividers that were still navy in neutral dark. */
|
||||
--color-header-border: var(--c-border);
|
||||
--color-sidebar-border: var(--c-border);
|
||||
--color-dropdown-border: var(--c-border);
|
||||
--color-search-border: var(--c-border);
|
||||
--tool-header-border: var(--c-border);
|
||||
--tool-panel-search-border-bottom: var(--c-border);
|
||||
--landing-inner-paper-border: var(--c-border);
|
||||
--unsupported-bar-border: var(--c-border);
|
||||
--api-keys-card-border: var(--c-border);
|
||||
--api-keys-input-border: var(--c-border);
|
||||
--color-usage-track: var(--c-border);
|
||||
--color-sidebar-divider: var(--c-border-subtle);
|
||||
--tool-subcategory-rule-color: var(--c-border-subtle);
|
||||
--modal-header-border: var(--c-border-subtle);
|
||||
|
||||
/* ── More surfaces that were still navy in neutral dark ── */
|
||||
--color-bg-alt: var(--c-bg-raised);
|
||||
--color-search-bg: var(--c-surface-sunken);
|
||||
--color-dropdown-hover: var(--c-hover);
|
||||
--tool-header-badge-bg: var(--c-surface-raised);
|
||||
--color-nav-active: var(--c-primary-subtle);
|
||||
--modal-nav-item-active-bg: var(--c-primary-subtle);
|
||||
|
||||
/* ── More text tokens that were still navy in neutral dark ── */
|
||||
--onboarding-title: var(--c-text);
|
||||
--onboarding-body: var(--c-text-muted);
|
||||
--modal-nav-section-title: var(--c-text-subtle);
|
||||
--tool-subcategory-text-color: var(--c-text-subtle);
|
||||
--dropdown-trigger-text-disabled: var(--c-text-subtle);
|
||||
--tool-header-badge-text: var(--c-accent-fg, var(--c-primary));
|
||||
--tooltip-title-bg: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
|
||||
--tooltip-title-color: var(--c-text);
|
||||
|
||||
/* ── Interaction states ── */
|
||||
--hover-bg: var(--c-hover);
|
||||
--color-bg-hover: var(--c-hover);
|
||||
--color-nav-hover: var(--c-hover);
|
||||
--automation-entry-hover-bg: var(--c-hover);
|
||||
--active-bg: var(--c-active);
|
||||
|
||||
/* ── Primary / accent — fills/borders use raw --c-primary; text/icon-on-surface uses --c-accent-fg (falls back to --c-primary outside the custom theme). ── */
|
||||
--color-blue: var(--c-primary);
|
||||
--accent-blue: var(--c-primary);
|
||||
--accent-interactive: var(--c-primary);
|
||||
--btn-open-file: var(--c-primary);
|
||||
--card-selected-border: var(--c-primary);
|
||||
--checkbox-checked-bg: var(--c-primary);
|
||||
--color-blue-dark: var(--c-primary-hover);
|
||||
--modal-nav-item-active: var(--c-accent-fg, var(--c-primary));
|
||||
--color-nav-active-text: var(--c-accent-fg, var(--c-primary));
|
||||
--tool-header-text: var(--c-accent-fg, var(--c-primary));
|
||||
--text-instruction: var(--c-accent-fg, var(--c-primary));
|
||||
--landing-button-color: var(--c-accent-fg, var(--c-primary));
|
||||
/* SUI "blue" accent border/light (secondary buttons, chips) — derived from the primary. */
|
||||
--color-blue-border: color-mix(in srgb, var(--c-primary) 38%, var(--c-surface));
|
||||
--color-blue-light: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
|
||||
|
||||
/* ── Status ── */
|
||||
--accent-green: var(--c-success);
|
||||
--accent-red: var(--c-danger);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/* DIMENSIONS — single source for every non-colour length (spacing, radius, sizing, borders, z-index, shadow, type, motion). Theme-agnostic. */
|
||||
|
||||
:root {
|
||||
/* ── Spacing — 4px grid. Canonical numeric scale + named aliases ── */
|
||||
--space-0: 0;
|
||||
--space-0_5: 0.125rem; /* 2px */
|
||||
--space-1: 0.25rem; /* 4px */
|
||||
--space-1_5: 0.375rem; /* 6px */
|
||||
--space-2: 0.5rem; /* 8px */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px */
|
||||
--space-5: 1.25rem; /* 20px */
|
||||
--space-6: 1.5rem; /* 24px */
|
||||
--space-8: 2rem; /* 32px */
|
||||
|
||||
/* T-shirt aliases (legacy editor names) → the numeric scale above. */
|
||||
--space-xs: var(--space-1); /* 4px */
|
||||
--space-sm: var(--space-2); /* 8px */
|
||||
--space-md: var(--space-4); /* 16px */
|
||||
--space-lg: var(--space-6); /* 24px */
|
||||
--space-xl: var(--space-8); /* 32px */
|
||||
|
||||
/* ── Radius — one coherent scale (resolves the 8px-vs-6px collision) ── */
|
||||
--radius-xs: 2px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-pill: 9999px;
|
||||
|
||||
/* ── Layout sizing ── */
|
||||
--footer-height: 2rem;
|
||||
--landing-stack-w: 224px;
|
||||
--landing-stack-h: 176px;
|
||||
|
||||
/* --shadow-* is intentionally NOT defined here: editor (drop shadows) and SUI/portal (inset hairlines) reuse the name with different values. */
|
||||
|
||||
/* ── Typography ── */
|
||||
--font-sans:
|
||||
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
|
||||
sans-serif;
|
||||
--font-mono:
|
||||
"SF Mono", "Fira Code", Menlo, Consolas, "DejaVu Sans Mono", monospace;
|
||||
--font-brand: "Alumni Sans", "Inter", sans-serif;
|
||||
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
|
||||
/* ── Motion ── */
|
||||
--motion-fast: 0.15s ease;
|
||||
--motion-base: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--motion-slow: 0.3s ease;
|
||||
--motion-enter: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--fullscreen-anim-duration-in: 0.28s;
|
||||
--fullscreen-anim-duration-out: 0.22s;
|
||||
|
||||
/* ── Z-index ladder ── */
|
||||
--z-dropdown: 25;
|
||||
--z-drawer: 50;
|
||||
--z-toast: 200;
|
||||
/* Fullscreen tool-picker surfaces (editor) */
|
||||
--z-fullscreen-icon-svg: 1;
|
||||
--z-toolpicker-star: 1;
|
||||
--z-fullscreen-surface: 1200;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
/* Consolidated theme entry. Order matters: primitives → dimensions → colors → compat (legacy aliases, :root:root wins). */
|
||||
@import "./primitives.css";
|
||||
@import "./dimensions.css";
|
||||
@import "./colors.css";
|
||||
@import "./compat.css";
|
||||
@@ -58,21 +58,18 @@ const gray: MantineColorsTuple = [
|
||||
"var(--color-gray-900)",
|
||||
];
|
||||
|
||||
// Navy-indigo dark scale — replaces Mantine's neutral gray defaults so all
|
||||
// dark-mode components (SegmentedControl, inputs, dropdowns, etc.) use the
|
||||
// portal palette automatically via --mantine-color-dark-*.
|
||||
// dark-0..3 = text/icon shades, dark-4..7 = surface elevations, dark-8..9 = deepest bg.
|
||||
// Neutral dark scale (zinc, mirroring --p-zinc-*) replacing Mantine's default gray ramp; colors.css re-points dark-4..7 at the --c-* surfaces. 0..3 text, 4..7 surfaces, 8..9 deepest.
|
||||
const dark: MantineColorsTuple = [
|
||||
"#c2c8e0", // dark-0 — primary text on dark bg
|
||||
"#9299b0", // dark-1 — secondary text
|
||||
"#6e7898", // dark-2 — muted text / icons
|
||||
"#4a5282", // dark-3 — subtle text / dividers
|
||||
"#1c2340", // dark-4 — elevated surface / selected bg (e.g. SegmentedControl indicator)
|
||||
"#131729", // dark-5 — card / panel surface
|
||||
"#0d1020", // dark-6 — toolbar / sidebar bg (e.g. SegmentedControl root)
|
||||
"#090b18", // dark-7 — page background (deepest reachable surface)
|
||||
"#07091a", // dark-8
|
||||
"#050714", // dark-9
|
||||
"#f4f4f5", // dark-0 — primary text on dark bg (zinc-100)
|
||||
"#a1a1aa", // dark-1 — secondary text (zinc-200)
|
||||
"#71717a", // dark-2 — muted text / icons (zinc-300)
|
||||
"#52525b", // dark-3 — subtle text / dividers (zinc-400)
|
||||
"#2a2a2e", // dark-4 — elevated surface / selected bg (zinc-650)
|
||||
"#202023", // dark-5 — card / panel surface (zinc-775)
|
||||
"#18181b", // dark-6 — toolbar / sidebar bg (zinc-800)
|
||||
"#0a0a0b", // dark-7 — page background (zinc-950)
|
||||
"#070708", // dark-8 — deeper than the reachable surfaces
|
||||
"#050506", // dark-9 — deepest
|
||||
];
|
||||
|
||||
export const mantineTheme = createTheme({
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* PRIMITIVES — the raw palette (theme-agnostic). The ONE place literal colours live: neutral ramps (gray=light, zinc=dark) + status hues; colors.css and compat.css reference these. */
|
||||
|
||||
:root {
|
||||
--p-white: #ffffff;
|
||||
--p-black: #000000;
|
||||
--p-gray-50: #f9fafb;
|
||||
--p-gray-100: #f3f4f6;
|
||||
--p-gray-150: #eef0f2;
|
||||
--p-gray-200: #e5e7eb;
|
||||
--p-gray-250: #e2e8f0;
|
||||
--p-gray-300: #d1d5db;
|
||||
--p-gray-400: #9ca3af;
|
||||
--p-gray-500: #6b7280;
|
||||
--p-gray-600: #4b5563;
|
||||
--p-gray-700: #374151;
|
||||
--p-gray-800: #1f2937;
|
||||
--p-gray-900: #111827;
|
||||
--p-zinc-950: #0a0a0b;
|
||||
--p-zinc-900: #101012;
|
||||
--p-zinc-850: #131315;
|
||||
--p-zinc-800: #18181b;
|
||||
--p-zinc-775: #202023;
|
||||
--p-zinc-750: #1f1f23;
|
||||
--p-zinc-700: #27272a;
|
||||
--p-zinc-650: #2a2a2e;
|
||||
--p-zinc-600: #333338;
|
||||
--p-zinc-500: #3f3f46;
|
||||
--p-zinc-400: #52525b;
|
||||
--p-zinc-300: #71717a;
|
||||
--p-zinc-200: #a1a1aa;
|
||||
--p-zinc-100: #f4f4f5;
|
||||
--p-blue-400: #60a5fa;
|
||||
--p-blue-500: #3b82f6;
|
||||
--p-blue-600: #2563eb;
|
||||
--p-blue-700: #1d4ed8;
|
||||
--p-green-500: #22c55e;
|
||||
--p-green-600: #16a34a;
|
||||
--p-green-700: #15803d;
|
||||
--p-amber-400: #fbbf24;
|
||||
--p-amber-500: #f59e0b;
|
||||
--p-amber-600: #d97706;
|
||||
--p-red-400: #f87171;
|
||||
--p-red-500: #ef4444;
|
||||
--p-red-600: #dc2626;
|
||||
}
|
||||
@@ -92,8 +92,10 @@ export const Colours: Story = {
|
||||
<Group
|
||||
heading="Brand & status"
|
||||
swatches={[
|
||||
{ label: "Blue", varName: "--color-blue" },
|
||||
{ label: "Blue dark", varName: "--color-blue-dark" },
|
||||
// The accent — follows the custom light/dark theme colour, not a fixed
|
||||
// blue (maps to --c-primary / --c-primary-hover). Will change the name in a future PR to reflect that it's the chosen accent, not a fixed blue.
|
||||
{ label: "Primary", varName: "--color-blue" },
|
||||
{ label: "Primary hover", varName: "--color-blue-dark" },
|
||||
{ label: "Purple", varName: "--color-purple" },
|
||||
{ label: "Green", varName: "--color-green" },
|
||||
{ label: "Amber", varName: "--color-amber" },
|
||||
|
||||
@@ -16,9 +16,7 @@ body,
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition:
|
||||
background var(--motion-slow),
|
||||
color var(--motion-slow);
|
||||
transition: background var(--motion-slow);
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -13,18 +13,11 @@
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
/* Text hierarchy */
|
||||
--color-text-1: #0f172a;
|
||||
--color-text-2: #1a202c;
|
||||
--color-text-3: #475569;
|
||||
--color-text-4: #64748b;
|
||||
/* text-5 was #94a3b8 (≈2.85:1 on white — fails WCAG AA). Bumped to #64748b
|
||||
so meta/timestamp copy at small sizes still passes contrast. The visual
|
||||
hierarchy now collapses text-4 and text-5 into the same shade — use weight
|
||||
or size to differentiate in components instead. */
|
||||
--color-text-5: #64748b;
|
||||
--color-text-6: #6b7280;
|
||||
--color-text-muted: #8b92a1;
|
||||
--color-text-placeholder: #9ca3af;
|
||||
--color-text-placeholder: var(--p-gray-400);
|
||||
|
||||
/* Body text colour for use on coloured / gradient backgrounds (blue buttons,
|
||||
assistant header, onboarding "done" badges). Theme-stable: even in dark
|
||||
@@ -32,105 +25,66 @@
|
||||
--color-text-on-accent: #ffffff;
|
||||
|
||||
/* Brand / status */
|
||||
--color-blue: #3b82f6;
|
||||
--color-blue-dark: #2563eb;
|
||||
--color-blue-light: #eff6ff;
|
||||
--color-blue-border: #bfdbfe;
|
||||
--color-purple: #8b5cf6;
|
||||
--color-purple-light: #f5f3ff;
|
||||
--color-purple-border: #ddd6fe;
|
||||
--color-purple-dark: #7c3aed;
|
||||
--color-green: #10b981;
|
||||
--color-green-light: #ecfdf5;
|
||||
--color-green-border: #a7f3d0;
|
||||
--color-green-dark: #16a34a;
|
||||
--color-red: #ef4444;
|
||||
--color-red-light: #fee2e2;
|
||||
--color-red-border: #fecaca;
|
||||
--color-red-dark: #dc2626;
|
||||
--color-amber: #f59e0b;
|
||||
--color-amber-light: #fef3c7;
|
||||
--color-amber-border: #fde68a;
|
||||
--color-amber-dark: #92400e;
|
||||
--color-orange: #f97316;
|
||||
--color-orange-light: #fff7ed;
|
||||
--color-orange-border: #fed7aa;
|
||||
--color-orange-dark: #9a3412;
|
||||
--color-purple: var(--p-blue-400);
|
||||
--color-purple-light: var(--p-blue-400);
|
||||
--color-purple-border: var(--p-blue-400);
|
||||
--color-purple-dark: var(--p-blue-600);
|
||||
--color-green: var(--p-green-500);
|
||||
--color-green-light: var(--p-green-500);
|
||||
--color-green-border: var(--p-green-500);
|
||||
--color-green-dark: var(--p-green-600);
|
||||
--color-red: var(--p-red-500);
|
||||
--color-red-light: var(--p-red-400);
|
||||
--color-red-border: var(--p-red-400);
|
||||
--color-red-dark: var(--p-red-600);
|
||||
--color-amber: var(--p-amber-500);
|
||||
--color-amber-light: var(--p-amber-400);
|
||||
--color-amber-border: var(--p-amber-400);
|
||||
--color-amber-dark: var(--p-amber-600);
|
||||
--color-orange: var(--p-amber-600);
|
||||
--color-orange-light: var(--p-amber-400);
|
||||
--color-orange-border: var(--p-amber-400);
|
||||
--color-orange-dark: var(--p-red-600);
|
||||
|
||||
/* Category accents (theme-stable) */
|
||||
--color-cat-insurance: #0ea5e9;
|
||||
--color-cat-compliance: #6366f1;
|
||||
--color-cat-finance: #10b981;
|
||||
--color-cat-legal: #3b82f6;
|
||||
--color-cat-healthcare: #8b5cf6;
|
||||
--color-cat-government: #dc2626;
|
||||
--color-cat-operations: #ec4899;
|
||||
--color-cat-hr: #f59e0b;
|
||||
--color-cat-realestate: #84cc16;
|
||||
--color-cat-energy: #f97316;
|
||||
--color-cat-extraction: #14b8a6;
|
||||
--color-cat-insurance: var(--p-blue-500);
|
||||
--color-cat-compliance: var(--p-blue-500);
|
||||
--color-cat-finance: var(--p-green-500);
|
||||
--color-cat-legal: var(--p-blue-500);
|
||||
--color-cat-healthcare: var(--p-blue-400);
|
||||
--color-cat-government: var(--p-red-600);
|
||||
--color-cat-operations: var(--p-red-400);
|
||||
--color-cat-hr: var(--p-amber-500);
|
||||
--color-cat-realestate: var(--p-green-500);
|
||||
--color-cat-energy: var(--p-amber-600);
|
||||
--color-cat-extraction: var(--p-blue-500);
|
||||
|
||||
/* Surfaces */
|
||||
--color-bg: #f8f9fb;
|
||||
--color-bg-alt: #f6f8fa;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-alt: #ffffff;
|
||||
--color-border: #e3e8ee;
|
||||
--color-border-light: #eef0f2;
|
||||
--color-border-input: #e2e8f0;
|
||||
--color-border-hover: #cbd5e1;
|
||||
--color-divider: #f0f0f0;
|
||||
--color-bg-subtle: #f9fafb;
|
||||
--color-bg-hover: #f8fafc;
|
||||
--color-bg-muted: #f3f4f6;
|
||||
--color-bg-code: #f1f5f9;
|
||||
--color-bg-subtle: var(--p-gray-50);
|
||||
--color-bg-code: var(--p-gray-100);
|
||||
|
||||
/* Dropdowns / tooltips */
|
||||
--color-dropdown-bg: #ffffff;
|
||||
--color-dropdown-border: #e5e7eb;
|
||||
--color-dropdown-hover: #f9fafb;
|
||||
--color-tooltip-bg: #1e293b;
|
||||
--color-tooltip-text: #f8fafc;
|
||||
--color-tooltip-bg: var(--p-gray-800);
|
||||
--color-tooltip-text: var(--p-gray-50);
|
||||
|
||||
/* Navigation */
|
||||
--color-nav-active: #eff6ff;
|
||||
--color-nav-active-text: #3b82f6;
|
||||
--color-nav-text: #4b5563;
|
||||
--color-nav-hover: #f6f8fa;
|
||||
--color-nav-hover-text: #1a202c;
|
||||
--color-section-label: #9ca3af;
|
||||
--color-logo-text: #111827;
|
||||
--color-section-label: var(--p-gray-400);
|
||||
|
||||
/* Header */
|
||||
--color-header-bg: #ffffff;
|
||||
--color-header-border: #f0f0f0;
|
||||
--color-header-text: #111827;
|
||||
--color-search-bg: #f9fafb;
|
||||
--color-search-border: #e5e7eb;
|
||||
--color-search-border-hover: #d1d5db;
|
||||
--color-search-text: #9ca3af;
|
||||
--color-search-border-hover: var(--p-gray-300);
|
||||
--color-search-text: var(--p-gray-400);
|
||||
|
||||
/* Sidebar */
|
||||
--color-sidebar-bg: #ffffff;
|
||||
--color-sidebar-border: #e5e7eb;
|
||||
--color-sidebar-divider: #f0f0f0;
|
||||
--color-usage-text: #6b7280;
|
||||
--color-usage-value: #374151;
|
||||
--color-usage-track: #f3f4f6;
|
||||
--color-usage-text: var(--p-gray-500);
|
||||
--color-usage-value: var(--p-gray-700);
|
||||
|
||||
--color-toggle-off: #cbd5e1;
|
||||
--color-badge-red: #ef4444;
|
||||
|
||||
/* Segmented control — semantic tokens so dark mode can decouple from blue surfaces */
|
||||
--color-toggle-off: var(--p-gray-300);
|
||||
--color-badge-red: var(--p-red-500);
|
||||
|
||||
/* Shadows */
|
||||
--shadow-sm: inset 0 0 0 1px #e3e8ee;
|
||||
--shadow-md: inset 0 0 0 1px #e3e8ee, 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
--shadow-lg: inset 0 0 0 1px #e3e8ee, 0 4px 12px rgba(15, 23, 42, 0.06);
|
||||
--shadow-blue:
|
||||
0 1px 2px rgba(59, 130, 246, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||
--shadow-blue-hover:
|
||||
0 2px 6px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.25);
|
||||
--shadow-sm: inset 0 0 0 1px var(--p-gray-250);
|
||||
--shadow-md: inset 0 0 0 1px var(--p-gray-250), 0 1px 2px color-mix(in srgb, var(--p-blue-700) 4%, transparent);
|
||||
--shadow-lg: inset 0 0 0 1px var(--p-gray-250), 0 4px 12px color-mix(in srgb, var(--p-blue-700) 6%, transparent);
|
||||
|
||||
/* Gradients */
|
||||
--grad-blue-btn: linear-gradient(
|
||||
@@ -163,103 +117,58 @@
|
||||
#ef4444 50%,
|
||||
#dc2626 100%
|
||||
);
|
||||
--grad-banner: linear-gradient(135deg, #eef2ff 0%, #ddd6fe 50%, #fce7f3 100%);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--color-text-1: #f1f5f9;
|
||||
--color-text-2: #e2e8f0;
|
||||
--color-text-3: #94a3b8;
|
||||
--color-text-4: #64748b;
|
||||
/* Bumped from #475569 (fails AA) — see light-theme note. */
|
||||
--color-text-5: #7c869a;
|
||||
--color-text-6: #94a3b8;
|
||||
--color-text-muted: #64748b;
|
||||
--color-text-placeholder: #475569;
|
||||
--color-text-placeholder: var(--p-gray-600);
|
||||
|
||||
--color-blue: #60a5fa;
|
||||
--color-blue-dark: #3b82f6;
|
||||
--color-blue-light: #1e293b;
|
||||
--color-blue-border: #1e3a5f;
|
||||
--color-purple: #a78bfa;
|
||||
--color-purple-light: #1e1b2e;
|
||||
--color-purple-border: #2e2650;
|
||||
--color-purple-dark: #8b5cf6;
|
||||
--color-green: #34d399;
|
||||
--color-green-light: #0d2818;
|
||||
--color-green-border: #065f46;
|
||||
--color-green-dark: #22c55e;
|
||||
--color-red: #f87171;
|
||||
--color-red-light: #2d1215;
|
||||
--color-red-border: #7f1d1d;
|
||||
--color-red-dark: #ef4444;
|
||||
--color-amber: #fbbf24;
|
||||
--color-amber-light: #2d2006;
|
||||
--color-amber-border: #78350f;
|
||||
--color-purple: var(--p-blue-400);
|
||||
--color-purple-light: var(--p-zinc-750);
|
||||
--color-purple-border: var(--p-zinc-600);
|
||||
--color-purple-dark: var(--p-blue-400);
|
||||
--color-green: var(--p-green-500);
|
||||
--color-green-light: var(--p-green-700);
|
||||
--color-green-border: var(--p-green-700);
|
||||
--color-green-dark: var(--p-green-500);
|
||||
--color-red: var(--p-red-400);
|
||||
--color-red-light: var(--p-zinc-800);
|
||||
--color-red-border: var(--p-red-600);
|
||||
--color-red-dark: var(--p-red-500);
|
||||
--color-amber: var(--p-amber-400);
|
||||
--color-amber-light: var(--p-amber-600);
|
||||
--color-amber-border: var(--p-amber-600);
|
||||
/* Deeper than the base so it stays distinct (hover shade + amber text on
|
||||
dark surfaces). ~7.4:1 on --color-amber-light, passes WCAG AA. Was
|
||||
#fbbf24 — identical to the base — which left Mantine's amber hover a
|
||||
no-op in dark mode. */
|
||||
--color-amber-dark: #f59e0b;
|
||||
--color-orange: #fb923c;
|
||||
--color-orange-light: #2a1408;
|
||||
--color-orange-border: #7c2d12;
|
||||
--color-orange-dark: #fdba74;
|
||||
--color-amber-dark: var(--p-amber-500);
|
||||
--color-orange: var(--p-amber-500);
|
||||
--color-orange-light: var(--p-amber-600);
|
||||
--color-orange-border: var(--p-red-600);
|
||||
--color-orange-dark: var(--p-amber-400);
|
||||
|
||||
--color-bg: #090c14;
|
||||
--color-bg-alt: #0d1120;
|
||||
--color-surface: #151c2e;
|
||||
--color-surface-alt: #1c2640;
|
||||
--color-border: #283248;
|
||||
--color-border-light: #1e2840;
|
||||
--color-border-input: #2e3c55;
|
||||
--color-border-hover: #3d4f6a;
|
||||
--color-divider: #1e2840;
|
||||
--color-bg-subtle: #111827;
|
||||
--color-bg-hover: #1c2640;
|
||||
--color-bg-muted: #243044;
|
||||
--color-bg-code: #0b0f1a;
|
||||
--color-bg-subtle: var(--p-gray-900);
|
||||
--color-bg-code: var(--p-zinc-900);
|
||||
|
||||
--color-dropdown-bg: #151c2e;
|
||||
--color-dropdown-border: #2e3c55;
|
||||
--color-dropdown-hover: #1c2640;
|
||||
--color-tooltip-bg: #e2e8f0;
|
||||
--color-tooltip-text: #0f172a;
|
||||
--color-tooltip-bg: var(--p-gray-250);
|
||||
--color-tooltip-text: var(--p-blue-700);
|
||||
|
||||
--color-nav-active: #172044;
|
||||
--color-nav-active-text: #60a5fa;
|
||||
--color-nav-text: #94a3b8;
|
||||
--color-nav-hover: #1c2640;
|
||||
--color-nav-hover-text: #e2e8f0;
|
||||
--color-section-label: #475569;
|
||||
--color-logo-text: #f1f5f9;
|
||||
--color-section-label: var(--p-gray-600);
|
||||
|
||||
--color-header-bg: #0d1120;
|
||||
--color-header-border: #1e2840;
|
||||
--color-header-text: #f1f5f9;
|
||||
--color-search-bg: #151c2e;
|
||||
--color-search-border: #2e3c55;
|
||||
--color-search-border-hover: #3d4f6a;
|
||||
--color-search-text: #64748b;
|
||||
--color-search-border-hover: var(--p-gray-600);
|
||||
--color-search-text: var(--p-gray-500);
|
||||
|
||||
--color-sidebar-bg: #0d1120;
|
||||
--color-sidebar-border: #1e2840;
|
||||
--color-sidebar-divider: #1e2840;
|
||||
--color-usage-text: #64748b;
|
||||
--color-usage-value: #94a3b8;
|
||||
--color-usage-track: #1e2840;
|
||||
--color-usage-text: var(--p-gray-500);
|
||||
--color-usage-value: var(--p-gray-400);
|
||||
|
||||
--color-toggle-off: #3d4f6a;
|
||||
--color-toggle-off: var(--p-gray-600);
|
||||
|
||||
/* Segmented control — blue-toned dark to match app surface colors */
|
||||
|
||||
--shadow-sm: inset 0 0 0 1px #283248;
|
||||
--shadow-md: inset 0 0 0 1px #283248, 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: inset 0 0 0 1px #283248, 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
--shadow-blue:
|
||||
0 1px 3px rgba(59, 130, 246, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
--shadow-blue-hover:
|
||||
0 2px 8px rgba(59, 130, 246, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
--shadow-sm: inset 0 0 0 1px var(--p-zinc-600);
|
||||
--shadow-md: inset 0 0 0 1px var(--p-zinc-600), 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: inset 0 0 0 1px var(--p-zinc-600), 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
|
||||
--grad-blue-btn: linear-gradient(
|
||||
180deg,
|
||||
@@ -267,74 +176,30 @@
|
||||
#2563eb 50%,
|
||||
#1d4ed8 100%
|
||||
);
|
||||
--grad-banner: linear-gradient(135deg, #0f172a 0%, #111827 50%, #1a1535 100%);
|
||||
}
|
||||
|
||||
/* Always-dark code palette. Not theme-switched. */
|
||||
:root {
|
||||
--code-bg: #0f172a;
|
||||
--code-bg-alt: #1e293b;
|
||||
--code-bg-header: #1a2332;
|
||||
--code-text: #e2e8f0;
|
||||
--code-dim: #64748b;
|
||||
--code-muted: #94a3b8;
|
||||
--code-keyword: #60a5fa;
|
||||
--code-string: #fbbf24;
|
||||
--code-number: #c084fc;
|
||||
--code-fn: #34d399;
|
||||
--code-type: #f472b6;
|
||||
--code-property: #93c5fd;
|
||||
--code-comment: #475569;
|
||||
--code-border: #1e293b;
|
||||
--code-bg: var(--p-blue-700);
|
||||
--code-bg-header: var(--p-gray-800);
|
||||
--code-text: var(--p-gray-250);
|
||||
--code-dim: var(--p-gray-500);
|
||||
--code-muted: var(--p-gray-400);
|
||||
--code-border: var(--p-gray-800);
|
||||
/* Window-chrome traffic-light dots in the code-block header. */
|
||||
--code-dot: #475569;
|
||||
--code-dot: var(--p-gray-600);
|
||||
}
|
||||
|
||||
/* Radii / typography / motion / spacing / z-index — theme-stable */
|
||||
:root {
|
||||
--radius-xs: 0.1875rem;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-pill: 0.625rem;
|
||||
|
||||
--font-sans:
|
||||
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
|
||||
sans-serif;
|
||||
--font-mono:
|
||||
"SF Mono", "Fira Code", Menlo, Consolas, "DejaVu Sans Mono", monospace;
|
||||
--font-brand: "Alumni Sans", "Inter", sans-serif;
|
||||
|
||||
--motion-fast: 0.15s ease;
|
||||
--motion-base: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--motion-slow: 0.3s ease;
|
||||
--motion-enter: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* 4-px grid spacing scale (rem so it tracks font-size changes). Use these
|
||||
in layout primitives + component CSS instead of bare 0.5rem / 0.75rem. */
|
||||
--space-0: 0;
|
||||
--space-0_5: 0.125rem;
|
||||
--space-1: 0.25rem;
|
||||
--space-1_5: 0.375rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.25rem;
|
||||
--space-6: 1.5rem;
|
||||
--space-8: 2rem;
|
||||
--space-10: 2.5rem;
|
||||
--space-12: 3rem;
|
||||
|
||||
/* Z-index hierarchy. Pick the highest tier that still puts you below the
|
||||
thing that's supposed to win. Modals beat drawers beat dropdowns. */
|
||||
--z-base: 0;
|
||||
--z-sticky: 10;
|
||||
--z-dropdown: 25;
|
||||
--z-fixed-overlay: 35;
|
||||
--z-drawer: 50;
|
||||
--z-modal: 100;
|
||||
--z-toast: 200;
|
||||
}
|
||||
|
||||
/* Keyframes used across the app. */
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
border: none;
|
||||
background: var(--color-blue, #3b82f6);
|
||||
color: #fff;
|
||||
background: var(--c-primary);
|
||||
color: var(--p-white);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
|
||||
box-shadow: 0 4px 16px color-mix(in srgb, var(--c-primary) 40%, transparent);
|
||||
transition:
|
||||
transform 180ms cubic-bezier(0.32, 0.72, 0, 1),
|
||||
box-shadow 180ms ease;
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
.chat-fab-btn:hover {
|
||||
transform: scale(1.09);
|
||||
box-shadow: 0 6px 22px rgba(59, 130, 246, 0.52);
|
||||
box-shadow: 0 6px 22px color-mix(in srgb, var(--c-primary) 52%, transparent);
|
||||
}
|
||||
|
||||
.chat-fab-btn:active {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type React from "react";
|
||||
import { type FocusEventHandler, type CSSProperties, useEffect, useState } from "react";
|
||||
import {
|
||||
ColorInput as MantineColorInput,
|
||||
type ColorInputProps as MantineColorInputProps,
|
||||
@@ -15,7 +15,7 @@ const SUI_INPUT_VARS = {
|
||||
"--input-placeholder-color": "var(--color-text-placeholder)",
|
||||
"--input-height-sm": "1.75rem",
|
||||
"--input-height-md": "2.25rem",
|
||||
} as React.CSSProperties;
|
||||
} as CSSProperties;
|
||||
|
||||
export type ColorInputSize = "sm" | "md";
|
||||
|
||||
@@ -23,6 +23,8 @@ export interface ColorInputProps {
|
||||
// Value
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
/** Fires once when picking finishes (pointer up / blur), not every drag frame — for committing expensive updates. */
|
||||
onChangeEnd?: (value: string) => void;
|
||||
defaultValue?: string;
|
||||
|
||||
// Behaviour
|
||||
@@ -30,6 +32,8 @@ export interface ColorInputProps {
|
||||
swatches?: string[];
|
||||
swatchesPerRow?: number;
|
||||
withPicker?: boolean;
|
||||
/** Restrict the gamut live: every change is run through this (handle sticks at the boundary), receiving the previous in-gamut value so a clamp can preserve hue at achromatic extremes. */
|
||||
clampValue?: (value: string, previous: string) => string;
|
||||
|
||||
// Popover escape hatch — only for zIndex / offset overrides in modals
|
||||
popoverProps?: MantineColorInputProps["popoverProps"];
|
||||
@@ -44,8 +48,8 @@ export interface ColorInputProps {
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
readOnly?: boolean;
|
||||
onFocus?: React.FocusEventHandler<HTMLInputElement>;
|
||||
onBlur?: React.FocusEventHandler<HTMLInputElement>;
|
||||
onFocus?: FocusEventHandler<HTMLInputElement>;
|
||||
onBlur?: FocusEventHandler<HTMLInputElement>;
|
||||
|
||||
// SUI — invalid applies error styling; FormField renders the message itself.
|
||||
inputSize?: ColorInputSize;
|
||||
@@ -57,6 +61,7 @@ type PassthroughProps = Omit<
|
||||
MantineColorInputProps,
|
||||
| "value"
|
||||
| "onChange"
|
||||
| "onChangeEnd"
|
||||
| "defaultValue"
|
||||
| "format"
|
||||
| "swatches"
|
||||
@@ -90,6 +95,8 @@ export function ColorInput({
|
||||
format = "hex",
|
||||
value,
|
||||
onChange,
|
||||
onChangeEnd,
|
||||
clampValue,
|
||||
defaultValue,
|
||||
swatches,
|
||||
swatchesPerRow,
|
||||
@@ -108,9 +115,27 @@ export function ColorInput({
|
||||
onBlur,
|
||||
}: ColorInputProps) {
|
||||
const inputRef = useInputAria({ describedBy: ariaDescribedBy });
|
||||
|
||||
// With a clamp, this component owns the displayed value so the picker reflects the constraint live (clamped and mirrored back each frame); without one, value/handlers pass straight through.
|
||||
const [clamped, setClamped] = useState(value ?? defaultValue ?? "");
|
||||
useEffect(() => {
|
||||
if (value !== undefined) setClamped(value);
|
||||
}, [value]);
|
||||
|
||||
const constrainedProps = clampValue
|
||||
? {
|
||||
value: clamped,
|
||||
onChange: (next: string) => {
|
||||
const c = clampValue(next, clamped);
|
||||
setClamped(c);
|
||||
onChange?.(c);
|
||||
},
|
||||
onChangeEnd: (next: string) => onChangeEnd?.(clampValue(next, clamped)),
|
||||
}
|
||||
: { value, onChange, onChangeEnd };
|
||||
|
||||
const passthroughProps: PassthroughProps = {
|
||||
value,
|
||||
onChange,
|
||||
...constrainedProps,
|
||||
defaultValue,
|
||||
format,
|
||||
swatches,
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
--_bd: var(--color-blue-border);
|
||||
--_tint: color-mix(in srgb, var(--color-blue) 12%, transparent);
|
||||
}
|
||||
|
||||
html[data-app-theme="custom"] .sui-acc-default {
|
||||
--_on: var(--c-text-on-primary);
|
||||
}
|
||||
.sui-acc-danger {
|
||||
--_solid: var(--color-red);
|
||||
--_solid-hover: var(--color-red-dark);
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// Derives an accessible custom-theme primary from a user-chosen colour: a lightness clamp keeps it visible on the base, and the on-primary colour is contrast-picked so filled content stays legible.
|
||||
|
||||
import type { ColorScheme } from "@app/constants/theme";
|
||||
|
||||
export interface DerivedPrimary {
|
||||
/** Contrast-safe primary to drive --user-primary. Tuned for FILLS. */
|
||||
primary: string;
|
||||
/** Legible foreground for content on the filled primary. */
|
||||
onPrimary: string;
|
||||
/** Accent tuned as a foreground (text/icon) on the app surface — light on dark, dark on light. */
|
||||
accentForeground: string;
|
||||
}
|
||||
|
||||
interface RGB {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
}
|
||||
|
||||
const FALLBACK = "#3b82f6"; // blue-500 — fallback when a stored colour won't parse
|
||||
|
||||
function parseHex(input: string): RGB | null {
|
||||
const s = input.trim().replace(/^#/, "");
|
||||
const hex =
|
||||
s.length === 3
|
||||
? s
|
||||
.split("")
|
||||
.map((c) => c + c)
|
||||
.join("")
|
||||
: s;
|
||||
if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
|
||||
return {
|
||||
r: parseInt(hex.slice(0, 2), 16),
|
||||
g: parseInt(hex.slice(2, 4), 16),
|
||||
b: parseInt(hex.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHsl({ r, g, b }: RGB): { h: number; s: number; l: number } {
|
||||
const rn = r / 255;
|
||||
const gn = g / 255;
|
||||
const bn = b / 255;
|
||||
const max = Math.max(rn, gn, bn);
|
||||
const min = Math.min(rn, gn, bn);
|
||||
const l = (max + min) / 2;
|
||||
const d = max - min;
|
||||
if (d === 0) return { h: 0, s: 0, l };
|
||||
const s = d / (1 - Math.abs(2 * l - 1));
|
||||
let h: number;
|
||||
if (max === rn) h = ((gn - bn) / d) % 6;
|
||||
else if (max === gn) h = (bn - rn) / d + 2;
|
||||
else h = (rn - gn) / d + 4;
|
||||
h *= 60;
|
||||
if (h < 0) h += 360;
|
||||
return { h, s, l };
|
||||
}
|
||||
|
||||
function hslToHex(h: number, s: number, l: number): string {
|
||||
const c = (1 - Math.abs(2 * l - 1)) * s;
|
||||
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
const m = l - c / 2;
|
||||
const [r, g, b] =
|
||||
h < 60
|
||||
? [c, x, 0]
|
||||
: h < 120
|
||||
? [x, c, 0]
|
||||
: h < 180
|
||||
? [0, c, x]
|
||||
: h < 240
|
||||
? [0, x, c]
|
||||
: h < 300
|
||||
? [x, 0, c]
|
||||
: [c, 0, x];
|
||||
const to = (v: number) =>
|
||||
Math.round((v + m) * 255)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
return `#${to(r)}${to(g)}${to(b)}`;
|
||||
}
|
||||
|
||||
function relLuminance({ r, g, b }: RGB): number {
|
||||
const lin = (v: number) => {
|
||||
const c = v / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
||||
}
|
||||
|
||||
// Relative-luminance cutoff above which on-primary flips white→black (tuned high so saturated brand hues keep white text).
|
||||
const ON_PRIMARY_DARK_CUTOFF = 0.62;
|
||||
|
||||
// Lightness floor (dark base) / ceiling (light base) so an accent stays distinguishable from the base surfaces.
|
||||
const DARK_MIN_L = 0.42;
|
||||
const LIGHT_MAX_L = 0.6;
|
||||
|
||||
// Stricter lightness bounds for an accent used as foreground text/icon (light on dark, dark on light).
|
||||
const DARK_FG_MIN_L = 0.62;
|
||||
const LIGHT_FG_MAX_L = 0.45;
|
||||
|
||||
// Saturation/lightness bounds so a stored accent stays a real, visible hue (never white/grey/black).
|
||||
const ACCENT_MIN_S = 0.35;
|
||||
const ACCENT_MIN_L = 0.32;
|
||||
const ACCENT_MAX_L = 0.68;
|
||||
|
||||
// Clamp a pick into gamut; at achromatic extremes (no hue in hex) borrow the hue from `previous` so black/white/grey give the darkest/lightest/least-saturated version of the working hue, not an unrelated colour.
|
||||
export function clampAccentChoice(input: string, previous?: string): string {
|
||||
const rgb = parseHex(input);
|
||||
if (!rgb) return FALLBACK;
|
||||
const { h, s, l } = rgbToHsl(rgb);
|
||||
const prevRgb = previous ? parseHex(previous) : null;
|
||||
const hue = s < 1e-6 && prevRgb ? rgbToHsl(prevRgb).h : h;
|
||||
return hslToHex(
|
||||
hue,
|
||||
Math.max(s, ACCENT_MIN_S),
|
||||
Math.min(Math.max(l, ACCENT_MIN_L), ACCENT_MAX_L),
|
||||
);
|
||||
}
|
||||
|
||||
export function deriveAccessiblePrimary(
|
||||
input: string,
|
||||
base: ColorScheme,
|
||||
): DerivedPrimary {
|
||||
const rgb = parseHex(input) ?? parseHex(FALLBACK)!;
|
||||
const { h, s } = rgbToHsl(rgb);
|
||||
let { l } = rgbToHsl(rgb);
|
||||
if (base === "dark" && l < DARK_MIN_L) l = DARK_MIN_L;
|
||||
if (base === "light" && l > LIGHT_MAX_L) l = LIGHT_MAX_L;
|
||||
|
||||
const primary = hslToHex(h, s, l);
|
||||
const lum = relLuminance(parseHex(primary)!);
|
||||
// White by default; flip to black only once the colour is genuinely light.
|
||||
const onPrimary = lum > ON_PRIMARY_DARK_CUTOFF ? "#000000" : "#ffffff";
|
||||
|
||||
// Foreground accent: same hue, lightness pushed so it stays legible on the base surface.
|
||||
let fgL = rgbToHsl(rgb).l;
|
||||
if (base === "dark") fgL = Math.max(fgL, DARK_FG_MIN_L);
|
||||
else fgL = Math.min(fgL, LIGHT_FG_MAX_L);
|
||||
const accentForeground = hslToHex(h, s, fgL);
|
||||
|
||||
return { primary, onPrimary, accentForeground };
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
align-items: center;
|
||||
gap: 1.25rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: #16213e;
|
||||
background: color-mix(in srgb, var(--c-primary) 22%, var(--p-zinc-950));
|
||||
}
|
||||
|
||||
.portal-editor-hero__logo {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.875rem 1.25rem;
|
||||
background: #16213e;
|
||||
background: color-mix(in srgb, var(--c-primary) 22%, var(--p-zinc-950));
|
||||
}
|
||||
|
||||
.portal-welcome__brand {
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { preferencesService } from "@app/services/preferencesService";
|
||||
import {
|
||||
getSystemTheme,
|
||||
resolveColorScheme,
|
||||
type ThemeMode,
|
||||
} from "@app/constants/theme";
|
||||
|
||||
export type Theme = "light" | "dark";
|
||||
|
||||
@@ -17,33 +23,43 @@ interface ThemeContextValue {
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
const STORAGE_KEY = "stirling.portal.theme";
|
||||
|
||||
function readInitialTheme(): Theme {
|
||||
if (typeof window === "undefined") return "light";
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === "light" || stored === "dark") return stored;
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
const [theme, setTheme] = useState<Theme>(readInitialTheme);
|
||||
// Mirror the shared theme MODE; re-read on cross-tab storage changes and,
|
||||
// while on "system", on OS scheme changes.
|
||||
const [mode, setMode] = useState<ThemeMode>(() =>
|
||||
preferencesService.getPreference("theme"),
|
||||
);
|
||||
const [systemScheme, setSystemScheme] = useState(getSystemTheme);
|
||||
|
||||
useEffect(() => {
|
||||
const syncMode = () => setMode(preferencesService.getPreference("theme"));
|
||||
window.addEventListener("storage", syncMode);
|
||||
const media = window.matchMedia?.("(prefers-color-scheme: dark)");
|
||||
const syncSystem = () => setSystemScheme(media?.matches ? "dark" : "light");
|
||||
media?.addEventListener("change", syncSystem);
|
||||
return () => {
|
||||
window.removeEventListener("storage", syncMode);
|
||||
media?.removeEventListener("change", syncSystem);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const theme = resolveColorScheme(mode, systemScheme);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
window.localStorage.setItem(STORAGE_KEY, theme);
|
||||
}, [theme]);
|
||||
|
||||
const value = useMemo<ThemeContextValue>(
|
||||
() => ({
|
||||
const value = useMemo<ThemeContextValue>(() => {
|
||||
const setTheme = (next: Theme) => {
|
||||
preferencesService.setPreference("theme", next);
|
||||
setMode(next);
|
||||
};
|
||||
return {
|
||||
theme,
|
||||
setTheme,
|
||||
toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")),
|
||||
}),
|
||||
[theme],
|
||||
);
|
||||
toggle: () => setTheme(theme === "light" ? "dark" : "light"),
|
||||
};
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
|
||||
:root {
|
||||
/* Auth page colors (light mode) */
|
||||
--auth-bg-color-light-only: #f3f4f6;
|
||||
--auth-bg-color-light-only: var(--p-gray-100);
|
||||
--auth-card-bg: #ffffff;
|
||||
--auth-card-bg-light-only: #ffffff;
|
||||
--auth-label-text-light-only: #374151;
|
||||
--auth-input-border-light-only: #d1d5db;
|
||||
--auth-label-text-light-only: var(--p-gray-700);
|
||||
--auth-input-border-light-only: var(--p-gray-300);
|
||||
--auth-input-bg-light-only: #ffffff;
|
||||
--auth-input-text-light-only: #111827;
|
||||
--auth-border-focus-light-only: #3b82f6;
|
||||
--auth-focus-ring-light-only: rgba(59, 130, 246, 0.1);
|
||||
--auth-button-bg-light-only: #af3434;
|
||||
--auth-input-text-light-only: var(--p-gray-900);
|
||||
--auth-border-focus-light-only: var(--p-blue-500);
|
||||
--auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 10%, transparent);
|
||||
--auth-button-bg-light-only: var(--p-red-600);
|
||||
--auth-button-text-light-only: #ffffff;
|
||||
--auth-magic-button-bg-light-only: #e5e7eb;
|
||||
--auth-magic-button-text-light-only: #374151;
|
||||
--auth-text-primary-light-only: #111827;
|
||||
--auth-text-secondary-light-only: #6b7280;
|
||||
--auth-magic-button-bg-light-only: var(--p-gray-200);
|
||||
--auth-magic-button-text-light-only: var(--p-gray-700);
|
||||
--auth-text-primary-light-only: var(--p-gray-900);
|
||||
--auth-text-secondary-light-only: var(--p-gray-500);
|
||||
--text-divider-rule-rgb-light: 229, 231, 235;
|
||||
--text-divider-label-rgb-light: 156, 163, 175;
|
||||
--tool-subcategory-rule-color-light: #e5e7eb;
|
||||
--tool-subcategory-text-color-light: #9ca3af;
|
||||
--tool-subcategory-rule-color-light: var(--p-gray-200);
|
||||
--tool-subcategory-text-color-light: var(--p-gray-400);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] {
|
||||
@@ -31,9 +31,9 @@
|
||||
--auth-input-border-light-only: var(--border-default);
|
||||
--auth-input-bg-light-only: var(--bg-raised);
|
||||
--auth-input-text-light-only: var(--text-primary);
|
||||
--auth-border-focus-light-only: #3b82f6;
|
||||
--auth-focus-ring-light-only: rgba(59, 130, 246, 0.2);
|
||||
--auth-button-bg-light-only: #af3434;
|
||||
--auth-border-focus-light-only: var(--p-blue-500);
|
||||
--auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 20%, transparent);
|
||||
--auth-button-bg-light-only: var(--p-red-600);
|
||||
--auth-button-text-light-only: #ffffff;
|
||||
--auth-magic-button-bg-light-only: var(--bg-raised);
|
||||
--auth-magic-button-text-light-only: var(--text-primary);
|
||||
|
||||
@@ -2,101 +2,93 @@
|
||||
|
||||
:root {
|
||||
/* Orange scale (used for warning toasts) */
|
||||
--color-orange-50: #fff4ed;
|
||||
--color-orange-100: #ffe1cc;
|
||||
--color-orange-200: #ffb089;
|
||||
--color-orange-300: #ff7a45;
|
||||
--color-orange-400: #d84a1b;
|
||||
--color-orange-50: var(--p-amber-400);
|
||||
--color-orange-100: var(--p-amber-400);
|
||||
--color-orange-200: var(--p-red-400);
|
||||
--color-orange-300: var(--p-red-400);
|
||||
--color-orange-400: var(--p-red-600);
|
||||
|
||||
/* Amber scale (trial/warning emphasis) */
|
||||
--color-amber-50: #fffbeb;
|
||||
--color-amber-100: #fef3c7;
|
||||
--color-amber-200: #fde68a;
|
||||
--color-amber-300: #fcd34d;
|
||||
--color-amber-400: #fbbf24;
|
||||
--color-amber-500: #f59e0b;
|
||||
--color-amber-600: #d97706;
|
||||
--color-amber-700: #b45309;
|
||||
--color-amber-800: #92400e;
|
||||
--color-amber-900: #78350f;
|
||||
--color-amber-50: var(--p-amber-400);
|
||||
--color-amber-100: var(--p-amber-400);
|
||||
--color-amber-200: var(--p-amber-400);
|
||||
--color-amber-300: var(--p-amber-400);
|
||||
--color-amber-400: var(--p-amber-400);
|
||||
--color-amber-500: var(--p-amber-500);
|
||||
--color-amber-600: var(--p-amber-600);
|
||||
--color-amber-700: var(--p-amber-600);
|
||||
--color-amber-800: var(--p-amber-600);
|
||||
--color-amber-900: var(--p-amber-600);
|
||||
|
||||
/* Subcategory / divider vars (light) */
|
||||
--tool-subcategory-text-color-light: #9ca3af;
|
||||
--tool-subcategory-rule-color-light: #e5e7eb;
|
||||
--text-divider-rule-color: var(--color-gray-200);
|
||||
--text-divider-label-color: var(--color-gray-400);
|
||||
--text-divider-rule-color-light: var(--color-gray-200);
|
||||
--text-divider-label-color-light: var(--color-gray-400);
|
||||
--tool-subcategory-text-color-light: var(--p-gray-400);
|
||||
--tool-subcategory-rule-color-light: var(--p-gray-200);
|
||||
|
||||
/* Auth color vars */
|
||||
--auth-input-bg: #f9fafb;
|
||||
--auth-input-border: #e5e7eb;
|
||||
--auth-input-text: #1f2937;
|
||||
--auth-label-text: #2b3230;
|
||||
--auth-button-bg: #af3434;
|
||||
--auth-input-bg: var(--p-gray-50);
|
||||
--auth-input-border: var(--p-gray-200);
|
||||
--auth-input-text: var(--p-gray-800);
|
||||
--auth-label-text: var(--p-zinc-650);
|
||||
--auth-button-bg: var(--p-red-600);
|
||||
--auth-button-text: #ffffff;
|
||||
--auth-magic-button-bg: #af3434;
|
||||
--auth-magic-button-bg: var(--p-red-600);
|
||||
--auth-magic-button-text: #ffffff;
|
||||
|
||||
/* Light-only auth colors (no dark mode equivalents) used for login/signup */
|
||||
--auth-input-bg-light-only: #f9fafb;
|
||||
--auth-input-border-light-only: #e5e7eb;
|
||||
--auth-input-text-light-only: #1f2937;
|
||||
--auth-label-text-light-only: #2b3230;
|
||||
--auth-button-bg-light-only: #af3434;
|
||||
--auth-input-bg-light-only: var(--p-gray-50);
|
||||
--auth-input-border-light-only: var(--p-gray-200);
|
||||
--auth-input-text-light-only: var(--p-gray-800);
|
||||
--auth-label-text-light-only: var(--p-zinc-650);
|
||||
--auth-button-bg-light-only: var(--p-red-600);
|
||||
--auth-button-text-light-only: #ffffff;
|
||||
--auth-magic-button-bg-light-only: #af3434;
|
||||
--auth-magic-button-bg-light-only: var(--p-red-600);
|
||||
--auth-magic-button-text-light-only: #ffffff;
|
||||
--auth-bg-color-light-only: #ffffff;
|
||||
--auth-card-bg-light-only: #ffffff;
|
||||
--auth-text-primary-light-only: #2b3230;
|
||||
--auth-text-secondary-light-only: #1f2937;
|
||||
--auth-border-color-light-only: #e5e7eb;
|
||||
--auth-border-focus-light-only: #cbd5e1;
|
||||
--auth-focus-ring-light-only: rgba(59, 130, 246, 0.15);
|
||||
--auth-text-primary-light-only: var(--p-zinc-650);
|
||||
--auth-text-secondary-light-only: var(--p-gray-800);
|
||||
--auth-border-focus-light-only: var(--p-gray-300);
|
||||
--auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 15%, transparent);
|
||||
|
||||
/* App Config Modal colors (light mode) */
|
||||
--modal-nav-bg: #f5f6f8;
|
||||
--modal-nav-section-title: #6b7280;
|
||||
--modal-nav-item: #374151;
|
||||
--modal-nav-item-active: #0a8bff;
|
||||
--modal-nav-item-active-bg: rgba(10, 139, 255, 0.08);
|
||||
--modal-nav-bg: var(--p-gray-100);
|
||||
--modal-nav-section-title: var(--p-gray-500);
|
||||
--modal-nav-item: var(--p-gray-700);
|
||||
--modal-nav-item-active: var(--p-blue-500);
|
||||
--modal-nav-item-active-bg: color-mix(in srgb, var(--p-blue-500) 8%, transparent);
|
||||
--modal-content-bg: #ffffff;
|
||||
--modal-header-border: rgba(0, 0, 0, 0.06);
|
||||
|
||||
/* API usage progress bar colors (light mode) */
|
||||
--usage-weekly-active: #3b82f6;
|
||||
--usage-bought-active: #14b8a6;
|
||||
--usage-total-used: #3b82f6;
|
||||
--usage-inactive: #e5e7eb;
|
||||
--usage-inactive: var(--p-gray-200);
|
||||
|
||||
/* API Keys section colors (light mode) */
|
||||
--api-keys-card-bg: #ffffff;
|
||||
--api-keys-card-border: #e0e0e0;
|
||||
--api-keys-card-border: var(--p-gray-200);
|
||||
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
|
||||
--api-keys-input-bg: #f8f8f8;
|
||||
--api-keys-input-border: #e0e0e0;
|
||||
--api-keys-input-bg: var(--p-gray-50);
|
||||
--api-keys-input-border: var(--p-gray-200);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] {
|
||||
/* Compare highlight colors (dark mode) */
|
||||
--spdf-compare-removed-bg: rgba(255, 107, 107, 0.45);
|
||||
--spdf-compare-added-bg: rgba(81, 207, 102, 0.35);
|
||||
--spdf-compare-removed-badge-bg: rgba(255, 59, 48, 0.15);
|
||||
--spdf-compare-removed-bg: color-mix(in srgb, var(--p-red-400) 45%, transparent);
|
||||
--spdf-compare-added-bg: color-mix(in srgb, var(--p-green-500) 35%, transparent);
|
||||
--spdf-compare-removed-badge-bg: color-mix(in srgb, var(--p-red-500) 15%, transparent);
|
||||
--spdf-compare-removed-badge-fg: var(--color-red-500);
|
||||
--spdf-compare-added-badge-bg: rgba(52, 199, 89, 0.18);
|
||||
--spdf-compare-added-badge-bg: color-mix(in srgb, var(--p-green-500) 18%, transparent);
|
||||
--spdf-compare-added-badge-fg: var(--color-green-500);
|
||||
--spdf-compare-inline-removed-bg: rgba(255, 59, 48, 0.25);
|
||||
--spdf-compare-inline-added-bg: rgba(52, 199, 89, 0.25);
|
||||
--compare-page-label-bg: #0d1020;
|
||||
--compare-page-label-fg: #c2c8e0;
|
||||
--spdf-compare-inline-removed-bg: color-mix(in srgb, var(--p-red-500) 25%, transparent);
|
||||
--spdf-compare-inline-added-bg: color-mix(in srgb, var(--p-green-500) 25%, transparent);
|
||||
--compare-page-label-bg: var(--p-zinc-850);
|
||||
--compare-page-label-fg: var(--p-gray-300);
|
||||
|
||||
/* Orange scale (dark mode mirrors light values to match UI) */
|
||||
--color-orange-50: #fff4ed;
|
||||
--color-orange-100: #ffe1cc;
|
||||
--color-orange-200: #ffb089;
|
||||
--color-orange-300: #ff7a45;
|
||||
--color-orange-400: #d84a1b;
|
||||
--color-orange-50: var(--p-amber-400);
|
||||
--color-orange-100: var(--p-amber-400);
|
||||
--color-orange-200: var(--p-red-400);
|
||||
--color-orange-300: var(--p-red-400);
|
||||
--color-orange-400: var(--p-red-600);
|
||||
|
||||
/* Auth page colors (dark mode) — mirror proprietary so the auth card themes dark */
|
||||
--auth-bg-color-light-only: var(--bg-muted);
|
||||
@@ -106,9 +98,9 @@
|
||||
--auth-input-border-light-only: var(--border-default);
|
||||
--auth-input-bg-light-only: var(--bg-raised);
|
||||
--auth-input-text-light-only: var(--text-primary);
|
||||
--auth-border-focus-light-only: #3b82f6;
|
||||
--auth-focus-ring-light-only: rgba(59, 130, 246, 0.2);
|
||||
--auth-button-bg-light-only: #af3434;
|
||||
--auth-border-focus-light-only: var(--p-blue-500);
|
||||
--auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 20%, transparent);
|
||||
--auth-button-bg-light-only: var(--p-red-600);
|
||||
--auth-button-text-light-only: #ffffff;
|
||||
--auth-magic-button-bg-light-only: var(--bg-raised);
|
||||
--auth-magic-button-text-light-only: var(--text-primary);
|
||||
@@ -116,31 +108,26 @@
|
||||
--auth-text-secondary-light-only: var(--text-secondary);
|
||||
--text-divider-rule-rgb-light: 229, 231, 235;
|
||||
--text-divider-label-rgb-light: 156, 163, 175;
|
||||
--tool-subcategory-rule-color-light: #e5e7eb;
|
||||
--tool-subcategory-text-color-light: #9ca3af;
|
||||
--tool-subcategory-rule-color-light: var(--p-gray-200);
|
||||
--tool-subcategory-text-color-light: var(--p-gray-400);
|
||||
|
||||
/* API usage progress bar colors (dark mode) */
|
||||
--usage-weekly-active: #4f8ef5;
|
||||
--usage-bought-active: #34d399;
|
||||
--usage-total-used: #e8eaf6;
|
||||
--usage-inactive: #1c2340;
|
||||
--usage-inactive: var(--p-zinc-650);
|
||||
|
||||
/* API Keys section colors (dark mode) */
|
||||
--api-keys-card-bg: #131729;
|
||||
--api-keys-card-border: #1c2340;
|
||||
--api-keys-card-bg: var(--p-zinc-800);
|
||||
--api-keys-card-border: var(--p-zinc-650);
|
||||
--api-keys-card-shadow: none;
|
||||
--api-keys-input-bg: #0d1020;
|
||||
--api-keys-input-border: #1c2340;
|
||||
--api-keys-input-bg: var(--p-zinc-850);
|
||||
--api-keys-input-border: var(--p-zinc-650);
|
||||
|
||||
--text-divider-rule-color: var(--tool-subcategory-rule-color);
|
||||
--text-divider-label-color: var(--text-muted);
|
||||
|
||||
/* App Config Modal colors (dark mode) */
|
||||
--modal-nav-bg: #0d1020;
|
||||
--modal-nav-section-title: #5b6280;
|
||||
--modal-nav-item: #c2c8e0;
|
||||
--modal-nav-item-active: #4f8ef5;
|
||||
--modal-nav-item-active-bg: rgba(79, 142, 245, 0.15);
|
||||
--modal-content-bg: #131729;
|
||||
--modal-nav-bg: var(--p-zinc-850);
|
||||
--modal-nav-section-title: var(--p-zinc-300);
|
||||
--modal-nav-item: var(--p-gray-300);
|
||||
--modal-nav-item-active: var(--p-blue-500);
|
||||
--modal-nav-item-active-bg: color-mix(in srgb, var(--p-blue-500) 15%, transparent);
|
||||
--modal-content-bg: var(--p-zinc-800);
|
||||
--modal-header-border: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
@@ -323,5 +323,10 @@ export default defineConfig(
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// Standalone node scripts have no bundler, so workspace aliases (@app/*)
|
||||
// don't resolve — relative imports are correct here.
|
||||
"no-restricted-imports": "off",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user