Server Deploy / deploy (push) Successful in 53s
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>
57 lines
2.0 KiB
TypeScript
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){}`;
|