style(frontend): sync remaining glass UI theme files and cyberpunk theme

This commit is contained in:
sagitchu
2026-04-21 12:40:00 +08:00
parent 29407c90b6
commit ea156c33bc
35 changed files with 2634 additions and 1555 deletions
+44 -24
View File
@@ -5,6 +5,8 @@
* re-renders when the active theme changes.
*/
import type { ThemePackage, ComponentKey, LayoutKey, PageKey } from "./types";
import React, {
createContext,
useContext,
@@ -14,7 +16,6 @@ import React, {
useMemo,
} from "react";
import type { ThemePackage, ComponentKey, LayoutKey, PageKey } from "./types";
import {
subscribe,
getActiveTheme,
@@ -59,11 +60,20 @@ interface ThemeContextValue {
unregister: (id: string) => void;
/** Resolve a component (returns themed override or fallback). */
component: <P = any>(key: ComponentKey, fallback: React.ComponentType<P>) => React.ComponentType<P>;
component: <P = any>(
key: ComponentKey,
fallback: React.ComponentType<P>,
) => React.ComponentType<P>;
/** Resolve a layout. */
layout: (key: LayoutKey, fallback: React.ComponentType<{ children: React.ReactNode }>) => React.ComponentType<{ children: React.ReactNode }>;
layout: (
key: LayoutKey,
fallback: React.ComponentType<{ children: React.ReactNode }>,
) => React.ComponentType<{ children: React.ReactNode }>;
/** Resolve a page. */
page: <P = any>(key: PageKey, fallback: React.ComponentType<P>) => React.ComponentType<P>;
page: <P = any>(
key: PageKey,
fallback: React.ComponentType<P>,
) => React.ComponentType<P>;
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
@@ -73,6 +83,7 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
// We use a monotonic counter to create new snapshot references when the
// registry notifies.
let snapshotCounter = 0;
function getSnapshot() {
return snapshotCounter;
}
@@ -81,6 +92,7 @@ const originalSubscribe = (onStoreChange: () => void) => {
snapshotCounter++;
onStoreChange();
});
return unsub;
};
@@ -98,7 +110,9 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
useEffect(() => {
const mq = window.matchMedia("(prefers-color-scheme: dark)");
const handler = () => reapplyActiveTheme();
mq.addEventListener("change", handler);
return () => mq.removeEventListener("change", handler);
}, []);
@@ -115,27 +129,27 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const register = useCallback((pkg: ThemePackage) => registerTheme(pkg), []);
const unregister = useCallback((id: string) => unregisterTheme(id), []);
const value = useMemo<ThemeContextValue>(() => ({
activeTheme: getActiveTheme(),
activeThemeId: getActiveThemeId(),
themes: getRegisteredThemes(),
mode: getSavedMode(),
effectiveMode: getEffectiveMode(),
switchTheme,
resetTheme,
setMode,
register,
unregister,
component: resolveComponent,
layout: resolveLayout,
page: resolvePage,
// eslint-disable-next-line react-hooks/exhaustive-deps
}), [snapshotCounter, switchTheme, resetTheme, setMode, register, unregister]);
const value = useMemo<ThemeContextValue>(
() => ({
activeTheme: getActiveTheme(),
activeThemeId: getActiveThemeId(),
themes: getRegisteredThemes(),
mode: getSavedMode(),
effectiveMode: getEffectiveMode(),
switchTheme,
resetTheme,
setMode,
register,
unregister,
component: resolveComponent,
layout: resolveLayout,
page: resolvePage,
}),
[snapshotCounter, switchTheme, resetTheme, setMode, register, unregister],
);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
};
@@ -144,7 +158,10 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
/** Access the full theme context. */
export function useThemeContext(): ThemeContextValue {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useThemeContext must be used within <ThemeProvider>");
if (!ctx)
throw new Error("useThemeContext must be used within <ThemeProvider>");
return ctx;
}
@@ -166,6 +183,7 @@ export function useThemedComponent<P = any>(
fallback: React.ComponentType<P>,
): React.ComponentType<P> {
const ctx = useThemeContext();
return ctx.component(key, fallback);
}
@@ -175,6 +193,7 @@ export function useThemedLayout(
fallback: React.ComponentType<{ children: React.ReactNode }>,
): React.ComponentType<{ children: React.ReactNode }> {
const ctx = useThemeContext();
return ctx.layout(key, fallback);
}
@@ -184,5 +203,6 @@ export function useThemedPage<P = any>(
fallback: React.ComponentType<P>,
): React.ComponentType<P> {
const ctx = useThemeContext();
return ctx.page(key, fallback);
}
@@ -10,10 +10,11 @@
* or you can build a completely new component from scratch.
*/
import type { ButtonProps } from "@/shadcn-bridge/heroui/button";
import React from "react";
// Import the original Button's props interface for compatibility
import type { ButtonProps } from "@/shadcn-bridge/heroui/button";
// Optionally import the original to wrap it
import { Button as OriginalButton } from "@/shadcn-bridge/heroui/button";
@@ -12,6 +12,7 @@
*/
import type { ThemePackage } from "../types";
import { CyberpunkButton } from "./components/button";
const cyberpunkTheme: ThemePackage = {
+31 -3
View File
@@ -9,7 +9,13 @@
* integration lives in `./context.tsx`.
*/
import type { ThemePackage, ThemeTokens, ComponentKey, LayoutKey, PageKey } from "./types";
import type {
ThemePackage,
ThemeTokens,
ComponentKey,
LayoutKey,
PageKey,
} from "./types";
// ─── internal state ──────────────────────────────────────────────────────────
@@ -62,7 +68,7 @@ export function getActiveThemeId(): string | null {
/** Get the currently active ThemePackage (or null). */
export function getActiveTheme(): ThemePackage | null {
return activeId ? installed.get(activeId) ?? null : null;
return activeId ? (installed.get(activeId) ?? null) : null;
}
// ─── theme mode ──────────────────────────────────────────────────────────────
@@ -71,13 +77,18 @@ export type ThemeMode = "light" | "dark" | "system";
export function resolveSystemMode(): "light" | "dark" {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
export function getSavedMode(): ThemeMode {
if (typeof window === "undefined") return "system";
const raw = localStorage.getItem(MODE_KEY);
if (raw === "light" || raw === "dark" || raw === "system") return raw;
return "system";
}
@@ -88,6 +99,7 @@ export function saveMode(mode: ThemeMode): void {
export function getEffectiveMode(): "light" | "dark" {
const mode = getSavedMode();
return mode === "system" ? resolveSystemMode() : mode;
}
@@ -103,8 +115,10 @@ export function getEffectiveMode(): "light" | "dark" {
*/
export function activateTheme(id: string): void {
const pkg = installed.get(id);
if (!pkg) {
console.warn(`[FLVX themes] Theme "${id}" is not registered.`);
return;
}
@@ -116,6 +130,7 @@ export function activateTheme(id: string): void {
// Inject tokens
const mode = getEffectiveMode();
const tokens = mode === "dark" ? pkg.tokens?.dark : pkg.tokens?.light;
if (tokens) injectTokens(tokens);
// Inject custom CSS
@@ -128,6 +143,7 @@ export function activateTheme(id: string): void {
// Update dark class
const root = document.documentElement;
root.classList.toggle("dark", mode === "dark");
root.style.colorScheme = mode;
@@ -143,6 +159,7 @@ export function activateTheme(id: string): void {
/** Deactivate the current theme, reverting all overrides. */
export function deactivateTheme(): void {
const prev = activeId ? installed.get(activeId) : null;
prev?.onDeactivate?.();
// Remove injected tokens
@@ -163,20 +180,24 @@ export function reapplyActiveTheme(): void {
const id = activeId;
// quick re-inject without full lifecycle
const pkg = installed.get(id);
if (!pkg) return;
clearInjectedTokens();
const mode = getEffectiveMode();
const tokens = mode === "dark" ? pkg.tokens?.dark : pkg.tokens?.light;
if (tokens) injectTokens(tokens);
const root = document.documentElement;
root.classList.toggle("dark", mode === "dark");
root.style.colorScheme = mode;
} else {
// No theme active — just set dark class based on mode
const mode = getEffectiveMode();
const root = document.documentElement;
root.classList.toggle("dark", mode === "dark");
root.style.colorScheme = mode;
}
@@ -195,6 +216,7 @@ export function resolveComponent<P = any>(
): React.ComponentType<P> {
const pkg = activeId ? installed.get(activeId) : null;
const override = pkg?.components?.[key];
return (override as React.ComponentType<P>) ?? fallback;
}
@@ -204,6 +226,7 @@ export function resolveLayout(
fallback: React.ComponentType<{ children: React.ReactNode }>,
): React.ComponentType<{ children: React.ReactNode }> {
const pkg = activeId ? installed.get(activeId) : null;
return pkg?.layouts?.[key] ?? fallback;
}
@@ -214,6 +237,7 @@ export function resolvePage<P = any>(
): React.ComponentType<P> {
const pkg = activeId ? installed.get(activeId) : null;
const override = pkg?.pages?.[key];
return (override as React.ComponentType<P>) ?? fallback;
}
@@ -221,6 +245,7 @@ export function resolvePage<P = any>(
export function subscribe(listener: ChangeListener): () => void {
changeListeners.add(listener);
return () => changeListeners.delete(listener);
}
@@ -232,6 +257,7 @@ export function subscribe(listener: ChangeListener): () => void {
*/
export function initThemeSystem(): void {
const savedId = localStorage.getItem(STORAGE_KEY);
if (savedId && installed.has(savedId)) {
activateTheme(savedId);
} else {
@@ -246,6 +272,7 @@ const injectedVars: string[] = [];
function injectTokens(tokens: ThemeTokens): void {
const root = document.documentElement;
for (const [varName, value] of Object.entries(tokens)) {
if (value !== undefined) {
root.style.setProperty(varName, value);
@@ -256,6 +283,7 @@ function injectTokens(tokens: ThemeTokens): void {
function clearInjectedTokens(): void {
const root = document.documentElement;
for (const varName of injectedVars) {
root.style.removeProperty(varName);
}
+8 -2
View File
@@ -182,7 +182,11 @@ export type ComponentKey =
* All overridable layout keys.
* Layouts receive `{ children: React.ReactNode }` as props.
*/
export type LayoutKey = "AdminLayout" | "H5Layout" | "H5SimpleLayout" | "DefaultLayout";
export type LayoutKey =
| "AdminLayout"
| "H5Layout"
| "H5SimpleLayout"
| "DefaultLayout";
/**
* All overridable page keys.
@@ -283,7 +287,9 @@ export interface ThemePackage {
/**
* Map of layout overrides. Each layout receives `{ children }`.
*/
layouts?: Partial<Record<LayoutKey, ComponentType<{ children: React.ReactNode }>>>;
layouts?: Partial<
Record<LayoutKey, ComponentType<{ children: React.ReactNode }>>
>;
/**
* Map of page overrides. Each page is a full route-level component.