Make admin light/dark mode follow real palettes end-to-end.
Local Build and Deploy / deploy (push) Successful in 55s
Local Build and Deploy / deploy (push) Successful in 55s
Light admin derives from the public light theme; dark keeps HK overrides. Remap inputs, cards, muted text, and kill the public wallpaper on admin so fonts and boxes stay readable in both modes. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
94904f2ddb
commit
7b6c02c07d
7 files changed
+154
-19
No files matched your search
@@ -223,41 +223,41 @@ const ADMIN_FIELDS: {
|
||||
}[] = [
|
||||
{
|
||||
key: "admin_canvas",
|
||||
label: "Page background",
|
||||
label: "Page background (dark)",
|
||||
def: "#0f172a",
|
||||
desc: "Main admin page background",
|
||||
desc: "Dark-mode admin canvas. Light mode follows the public light palette.",
|
||||
},
|
||||
{
|
||||
key: "admin_surface",
|
||||
label: "Card / panel",
|
||||
label: "Card / panel (dark)",
|
||||
def: "#1e293b",
|
||||
desc: "Admin cards, tables, and panel backgrounds",
|
||||
desc: "Dark-mode cards, tables, and panels",
|
||||
},
|
||||
{
|
||||
key: "admin_text",
|
||||
label: "Body text",
|
||||
label: "Body text (dark)",
|
||||
def: "#f1f5f9",
|
||||
desc: "Main admin text — must contrast with canvas/surface",
|
||||
desc: "Dark-mode text — must contrast with canvas/surface",
|
||||
bgKey: "admin_canvas",
|
||||
},
|
||||
{
|
||||
key: "admin_text_muted",
|
||||
label: "Muted text",
|
||||
label: "Muted text (dark)",
|
||||
def: "#94a3b8",
|
||||
desc: "Secondary labels, hints, descriptions",
|
||||
desc: "Dark-mode secondary labels and hints",
|
||||
bgKey: "admin_canvas",
|
||||
},
|
||||
{
|
||||
key: "admin_border",
|
||||
label: "Border",
|
||||
label: "Border (dark)",
|
||||
def: "#334155",
|
||||
desc: "Table borders, dividers, input outlines",
|
||||
desc: "Dark-mode table borders, dividers, input outlines",
|
||||
},
|
||||
{
|
||||
key: "admin_sidebar_bg",
|
||||
label: "Sidebar",
|
||||
label: "Sidebar (dark)",
|
||||
def: "#0f172a",
|
||||
desc: "Left navigation sidebar background",
|
||||
desc: "Dark-mode left navigation background",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+60
-1
@@ -95,7 +95,7 @@ html.dark textarea {
|
||||
}
|
||||
html.dark .admin-page table,
|
||||
html.dark .admin-card {
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
border-color: var(--admin-border);
|
||||
}
|
||||
html.dark .currency,
|
||||
html.dark .article-img {
|
||||
@@ -262,6 +262,14 @@ html.dark .site-bg {
|
||||
background-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* Admin must not inherit the public wallpaper — solid HK canvas only. */
|
||||
body.site-bg:has([data-admin]),
|
||||
html.dark body.site-bg:has([data-admin]) {
|
||||
background: var(--admin-canvas);
|
||||
background-image: none;
|
||||
background-blend-mode: normal;
|
||||
}
|
||||
|
||||
|
||||
.article-body {
|
||||
line-height: 1.7;
|
||||
@@ -1156,8 +1164,59 @@ body:has([data-admin]) {
|
||||
--color-surface-text-readable: var(--admin-text);
|
||||
--color-dropdown: var(--admin-surface);
|
||||
--color-dropdown-text-readable: var(--admin-text);
|
||||
--input-border-color: var(--admin-border);
|
||||
--input-focus-color: var(--admin-accent);
|
||||
--muted: var(--admin-text-muted);
|
||||
--border-subtle: color-mix(in srgb, var(--admin-text-muted) 22%, transparent);
|
||||
color: var(--admin-text);
|
||||
background-color: var(--admin-canvas);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* Admin forms / boxes follow admin tokens in both light and dark. */
|
||||
body:has([data-admin]) input:not([type="checkbox"]):not([type="radio"]):not(
|
||||
[type="range"]
|
||||
):not([type="file"]):not([type="color"]),
|
||||
body:has([data-admin]) select,
|
||||
body:has([data-admin]) textarea {
|
||||
background-color: var(--admin-surface-elevated);
|
||||
color: var(--admin-text);
|
||||
border-color: var(--admin-border);
|
||||
caret-color: var(--admin-text);
|
||||
}
|
||||
body:has([data-admin]) input::placeholder,
|
||||
body:has([data-admin]) textarea::placeholder {
|
||||
color: var(--admin-text-muted);
|
||||
opacity: 0.85;
|
||||
}
|
||||
body:has([data-admin]) .muted,
|
||||
body:has([data-admin]) .admin-empty,
|
||||
body:has([data-admin]) .text-muted-foreground {
|
||||
color: var(--admin-text-muted);
|
||||
}
|
||||
body:has([data-admin]) .admin-card,
|
||||
body:has([data-admin]) .admin-page table {
|
||||
background: var(--admin-surface);
|
||||
color: var(--admin-text);
|
||||
border-color: var(--admin-border);
|
||||
}
|
||||
body:has([data-admin]) .btn-outline {
|
||||
color: var(--admin-text);
|
||||
border-color: var(--admin-border);
|
||||
background: transparent;
|
||||
}
|
||||
body:has([data-admin]) .btn-outline:hover {
|
||||
background: color-mix(in srgb, var(--admin-accent) 12%, transparent);
|
||||
color: var(--admin-text);
|
||||
}
|
||||
/* Override public html.dark input hardcodes inside admin. */
|
||||
html.dark body:has([data-admin]) input:not([type="checkbox"]):not(
|
||||
[type="radio"]
|
||||
):not([type="range"]):not([type="file"]):not([type="color"]),
|
||||
html.dark body:has([data-admin]) select,
|
||||
html.dark body:has([data-admin]) textarea {
|
||||
background: var(--admin-surface-elevated);
|
||||
color: var(--admin-text);
|
||||
}
|
||||
|
||||
/* ── Details/summary open states for nav ──────────────────── */
|
||||
|
||||
@@ -90,7 +90,7 @@ export function LanguageSwitcher({
|
||||
aria-expanded={open}
|
||||
className={
|
||||
isAdmin
|
||||
? "flex items-center gap-1.5 h-9 rounded-xl border border-[var(--admin-border)] px-2 text-xs font-bold uppercase tracking-wide text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--admin-accent)]/15 hover:text-[var(--admin-accent)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)] disabled:opacity-60"
|
||||
? "flex items-center gap-1.5 h-9 rounded-xl border border-[var(--admin-border)] bg-[var(--admin-surface-elevated)] px-2 text-xs font-bold uppercase tracking-wide text-[var(--admin-text)] hover:bg-[var(--admin-accent)]/15 hover:text-[var(--admin-accent)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)] disabled:opacity-60"
|
||||
: "nav-item flex items-center gap-1.5 bg-transparent text-[13px] border-none cursor-pointer px-1"
|
||||
}
|
||||
>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function ThemeSwitcher({
|
||||
aria-label={label}
|
||||
title={title}
|
||||
suppressHydrationWarning
|
||||
className="flex-none grid place-items-center w-9 h-9 rounded-xl border border-[var(--admin-border)] text-[var(--admin-sidebar-text-readable)] hover:bg-[var(--admin-accent)]/15 hover:text-[var(--admin-accent)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]"
|
||||
className="flex-none grid place-items-center w-9 h-9 rounded-xl border border-[var(--admin-border)] bg-[var(--admin-surface-elevated)] text-[var(--admin-text)] hover:bg-[var(--admin-accent)]/15 hover:text-[var(--admin-accent)] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--admin-accent)]"
|
||||
style={{ visibility: ready ? "visible" : "hidden" }}
|
||||
>
|
||||
{dark ? <Sun size={16} /> : <Moon size={16} />}
|
||||
|
||||
@@ -131,6 +131,48 @@ describe("deriveAdminPalette", () => {
|
||||
expect(admin.surface).not.toBe("#00ff00");
|
||||
});
|
||||
|
||||
it("in light mode mirrors readable public surfaces", () => {
|
||||
const admin = deriveAdminPalette(
|
||||
{
|
||||
color_background: "#f8fafc",
|
||||
color_surface: "#ffffff",
|
||||
color_dropdown: "#f1f5f9",
|
||||
color_text: "#0f172a",
|
||||
color_text_muted: "#64748b",
|
||||
color_primary: "#f59e0b",
|
||||
color_navbar: "#ffffff",
|
||||
color_navbar_text: "#1e293b",
|
||||
},
|
||||
{
|
||||
canvas: "#0b0f19",
|
||||
surface: "#151b27",
|
||||
text: "#f8fafc",
|
||||
},
|
||||
"light",
|
||||
);
|
||||
expect(admin.canvas).toBe("#f8fafc");
|
||||
expect(admin.surface).toBe("#ffffff");
|
||||
expect(contrastRatio(admin.text, admin.canvas)).toBeGreaterThanOrEqual(4.5);
|
||||
expect(contrastRatio(admin.sidebarText, admin.sidebar)).toBeGreaterThanOrEqual(
|
||||
4.5,
|
||||
);
|
||||
});
|
||||
|
||||
it("ignores dark admin overrides while in light mode", () => {
|
||||
const admin = deriveAdminPalette(
|
||||
{
|
||||
color_background: "#ffffff",
|
||||
color_surface: "#ffffff",
|
||||
color_text: "#0f172a",
|
||||
color_primary: "#f59e0b",
|
||||
},
|
||||
{ canvas: "#0b0f19", text: "#f8fafc" },
|
||||
"light",
|
||||
);
|
||||
expect(admin.canvas).toBe("#ffffff");
|
||||
expect(admin.text.toLowerCase()).not.toBe("#f8fafc");
|
||||
});
|
||||
|
||||
it("repairs default light text when admin canvas is forced light", () => {
|
||||
const admin = deriveAdminPalette(
|
||||
{ color_primary: "#f59e0b" },
|
||||
|
||||
@@ -30,9 +30,31 @@ export interface AdminPalette {
|
||||
export function deriveAdminPalette(
|
||||
colors: PublicThemeColors,
|
||||
overrides?: Partial<AdminPalette>,
|
||||
mode: "light" | "dark" = "dark",
|
||||
): AdminPalette {
|
||||
const accent = overrides?.accent ?? colors.color_primary ?? "#f59e0b";
|
||||
const defaults: AdminPalette = {
|
||||
|
||||
const lightDefaults: AdminPalette = {
|
||||
canvas: colors.color_background ?? "#f8fafc",
|
||||
surface: colors.color_surface ?? "#ffffff",
|
||||
surfaceElevated: colors.color_dropdown ?? "#f1f5f9",
|
||||
text: colors.color_text ?? "#0f172a",
|
||||
muted: colors.color_text_muted ?? "#64748b",
|
||||
border: "#e2e8f0",
|
||||
accent,
|
||||
accentText: accent,
|
||||
accentForeground: "#ffffff",
|
||||
success: colors.color_success ?? "#16a34a",
|
||||
warning: colors.color_warning ?? "#eab308",
|
||||
error: colors.color_error ?? "#ef4444",
|
||||
info: colors.color_info ?? "#0ea5e9",
|
||||
sidebar: colors.color_navbar ?? colors.color_surface ?? "#ffffff",
|
||||
sidebarText: colors.color_navbar_text ?? colors.color_text ?? "#0f172a",
|
||||
overlay: "rgba(15, 23, 42, 0.45)",
|
||||
focusRing: accent,
|
||||
};
|
||||
|
||||
const darkDefaults: AdminPalette = {
|
||||
canvas: "#0b0f19",
|
||||
surface: "#151b27",
|
||||
surfaceElevated: "#1e2635",
|
||||
@@ -52,11 +74,18 @@ export function deriveAdminPalette(
|
||||
focusRing: accent,
|
||||
};
|
||||
|
||||
const defaults = mode === "light" ? lightDefaults : darkDefaults;
|
||||
|
||||
// Staff admin_* overrides are the dark-HK palette — apply only in dark mode
|
||||
// so light mode stays readable and tracks the public light theme.
|
||||
const merged: AdminPalette = {
|
||||
...defaults,
|
||||
...overrides,
|
||||
...(mode === "dark" ? overrides : {}),
|
||||
accent,
|
||||
overlay: overrides?.overlay ?? defaults.overlay,
|
||||
overlay:
|
||||
mode === "dark"
|
||||
? (overrides?.overlay ?? defaults.overlay)
|
||||
: defaults.overlay,
|
||||
focusRing: accent,
|
||||
};
|
||||
|
||||
|
||||
@@ -74,8 +74,13 @@ export function themePaletteCss(
|
||||
palette: ThemePalette,
|
||||
adminOverrides?: Partial<AdminPalette>,
|
||||
): string {
|
||||
const mode: "light" | "dark" = selector.includes("dark") ? "dark" : "light";
|
||||
const semantic = derivePublicForegrounds(palette);
|
||||
const admin = deriveAdminPalette(palette, adminOverrides);
|
||||
const admin = deriveAdminPalette(
|
||||
palette,
|
||||
mode === "dark" ? adminOverrides : undefined,
|
||||
mode,
|
||||
);
|
||||
const declarations = Object.entries(CSS_VARIABLES).map(
|
||||
([key, cssName]) => `--${cssName}:${palette[key as keyof ThemePalette]};`,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user