Files
vantage/adminsite/lib/theme.tsx
T
mrhid6andClaude Opus 5 34a0373eca
Server Deploy / deploy (push) Successful in 53s
feat(adminsite): one masthead, a shared page frame, collapsible instance records
The console had components but no shell: a brand bar and a nav strip stacked
into 100px carrying eight words, no sign-out, no account identity, and an
Overview link hardcoded to text-accent so it read as the current page on
every screen. Nine pages each hand-rolled their own header.

AppBar replaces both bars and derives its active state from usePathname.
Settings moves into AccountMenu — it is your password, not a destination —
taking appearance with it, which finally sets the data-theme attribute the
token blocks have supported in both directions since they were written.
That leaves three customer destinations: Overview, People, Billing.

PageFrame adds a support rail so a page has a floor, and InstanceRecord
replaces InstanceCard with one component that opens and closes: an account
with a single instance used to render a third of a row of summary with its
substance a click away. It defaults open when the instance is the only one
or needs attention.

No plan card in the rail: tier, limits and expiry belong to a licence and a
licence belongs to one instance, so an account holding a Free cloud instance
and a Professional self-hosted one has no single plan. The rail carries only
what is account-wide.

Tokens and globals.css are untouched — they stay verbatim shared with site/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:45:53 +01:00

57 lines
2.0 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
export type ThemePref = "light" | "dark" | "system";
const KEY = "vantage-hq-theme";
/*
* globals.css has always defined the dark tokens under BOTH
* :root[data-theme="dark"] and :root[data-theme="light"], specifically so an
* in-page control can win over the OS preference in either direction. Nothing
* ever set the attribute. This is that missing half.
*
* "system" removes the attribute rather than writing a value, which hands the
* decision back to the prefers-color-scheme block.
*/
export function applyTheme(pref: ThemePref) {
const root = document.documentElement;
if (pref === "system") root.removeAttribute("data-theme");
else root.setAttribute("data-theme", pref);
}
export function readTheme(): ThemePref {
if (typeof localStorage === "undefined") return "system";
const v = localStorage.getItem(KEY);
return v === "light" || v === "dark" ? v : "system";
}
export function useTheme(): [ThemePref, (p: ThemePref) => void] {
// Starts at "system" on both server and first client render so hydration
// matches; the real value lands in the effect below. The inline script in
// app/layout.tsx has already painted the correct colours by then, so there
// is no flash — only this control's own highlight settles a tick late.
const [pref, setPref] = useState<ThemePref>("system");
useEffect(() => setPref(readTheme()), []);
return [
pref,
(next: ThemePref) => {
setPref(next);
if (next === "system") localStorage.removeItem(KEY);
else localStorage.setItem(KEY, next);
applyTheme(next);
},
];
}
/*
* Runs before first paint, so a dark-preferring user never sees a white flash.
* Inlined as a string because it has to execute ahead of React.
*/
export const THEME_BOOT_SCRIPT = `try{var t=localStorage.getItem(${JSON.stringify(
KEY,
)});if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t)}catch(e){}`;