First Commit
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { type StoreApi, useStore } from "zustand";
|
||||
|
||||
import { type FontKey, fontRegistry } from "@/lib/fonts/registry";
|
||||
import {
|
||||
CONTENT_LAYOUT_VALUES,
|
||||
NAVBAR_STYLE_VALUES,
|
||||
SIDEBAR_COLLAPSIBLE_VALUES,
|
||||
SIDEBAR_VARIANT_VALUES,
|
||||
} from "@/lib/preferences/layout";
|
||||
import { THEME_MODE_VALUES, THEME_PRESET_VALUES } from "@/lib/preferences/theme";
|
||||
import { applyThemeMode, subscribeToSystemTheme } from "@/lib/preferences/theme-utils";
|
||||
|
||||
import { createPreferencesStore, type PreferencesState } from "./preferences-store";
|
||||
|
||||
const PreferencesStoreContext = createContext<StoreApi<PreferencesState> | null>(null);
|
||||
|
||||
const FONT_VALUES = Object.keys(fontRegistry) as FontKey[];
|
||||
|
||||
function getSafeValue<T extends string>(raw: string | null, allowed: readonly T[]): T | undefined {
|
||||
if (!raw) return undefined;
|
||||
return allowed.includes(raw as T) ? (raw as T) : undefined;
|
||||
}
|
||||
|
||||
function readDomState(): Partial<PreferencesState> {
|
||||
const root = document.documentElement;
|
||||
|
||||
const themeModeAttr = getSafeValue(root.getAttribute("data-theme-mode"), THEME_MODE_VALUES);
|
||||
const resolvedMode = root.classList.contains("dark") ? "dark" : "light";
|
||||
|
||||
return {
|
||||
themeMode: themeModeAttr ?? resolvedMode,
|
||||
resolvedThemeMode: resolvedMode,
|
||||
themePreset: getSafeValue(root.getAttribute("data-theme-preset"), THEME_PRESET_VALUES),
|
||||
font: getSafeValue(root.getAttribute("data-font"), FONT_VALUES),
|
||||
contentLayout: getSafeValue(root.getAttribute("data-content-layout"), CONTENT_LAYOUT_VALUES),
|
||||
navbarStyle: getSafeValue(root.getAttribute("data-navbar-style"), NAVBAR_STYLE_VALUES),
|
||||
sidebarVariant: getSafeValue(root.getAttribute("data-sidebar-variant"), SIDEBAR_VARIANT_VALUES),
|
||||
sidebarCollapsible: getSafeValue(root.getAttribute("data-sidebar-collapsible"), SIDEBAR_COLLAPSIBLE_VALUES),
|
||||
};
|
||||
}
|
||||
|
||||
export const PreferencesStoreProvider = ({
|
||||
children,
|
||||
themeMode,
|
||||
themePreset,
|
||||
font,
|
||||
contentLayout,
|
||||
navbarStyle,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
themeMode: PreferencesState["themeMode"];
|
||||
themePreset: PreferencesState["themePreset"];
|
||||
font: PreferencesState["font"];
|
||||
contentLayout: PreferencesState["contentLayout"];
|
||||
navbarStyle: PreferencesState["navbarStyle"];
|
||||
}) => {
|
||||
const [store] = useState<StoreApi<PreferencesState>>(() =>
|
||||
createPreferencesStore({
|
||||
themeMode,
|
||||
themePreset,
|
||||
font,
|
||||
contentLayout,
|
||||
navbarStyle,
|
||||
}),
|
||||
);
|
||||
|
||||
const domSnapshotRef = useRef<Partial<PreferencesState> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const domState = readDomState();
|
||||
domSnapshotRef.current = domState;
|
||||
|
||||
store.setState((prev) => ({
|
||||
...prev,
|
||||
...domState,
|
||||
isSynced: true,
|
||||
}));
|
||||
}, [store]);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribeMedia: (() => void) | undefined;
|
||||
|
||||
const applyFromMode = (mode: PreferencesState["themeMode"]) => {
|
||||
unsubscribeMedia?.();
|
||||
const resolved = applyThemeMode(mode);
|
||||
store.setState((prev) => ({ ...prev, resolvedThemeMode: resolved }));
|
||||
|
||||
if (mode === "system") {
|
||||
unsubscribeMedia = subscribeToSystemTheme(() => {
|
||||
const next = applyThemeMode("system");
|
||||
store.setState((prev) => ({ ...prev, resolvedThemeMode: next }));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const startMode = domSnapshotRef.current?.themeMode ?? store.getState().themeMode;
|
||||
applyFromMode(startMode);
|
||||
|
||||
const unsubscribeStore = store.subscribe((s, p) => {
|
||||
if (s.themeMode !== p.themeMode) applyFromMode(s.themeMode);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeMedia?.();
|
||||
unsubscribeStore();
|
||||
};
|
||||
}, [store]);
|
||||
|
||||
return <PreferencesStoreContext.Provider value={store}>{children}</PreferencesStoreContext.Provider>;
|
||||
};
|
||||
|
||||
export const usePreferencesStore = <T,>(selector: (state: PreferencesState) => T): T => {
|
||||
const store = useContext(PreferencesStoreContext);
|
||||
if (!store) throw new Error("Missing PreferencesStoreProvider");
|
||||
return useStore(store, selector);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
|
||||
import type { FontKey } from "@/lib/fonts/registry";
|
||||
import type { ContentLayout, NavbarStyle, SidebarCollapsible, SidebarVariant } from "@/lib/preferences/layout";
|
||||
import { PREFERENCE_DEFAULTS } from "@/lib/preferences/preferences-config";
|
||||
import type { ResolvedThemeMode, ThemeMode, ThemePreset } from "@/lib/preferences/theme";
|
||||
|
||||
export type PreferencesState = {
|
||||
themeMode: ThemeMode;
|
||||
resolvedThemeMode: ResolvedThemeMode;
|
||||
themePreset: ThemePreset;
|
||||
font: FontKey;
|
||||
contentLayout: ContentLayout;
|
||||
navbarStyle: NavbarStyle;
|
||||
sidebarVariant: SidebarVariant;
|
||||
sidebarCollapsible: SidebarCollapsible;
|
||||
setThemeMode: (mode: ThemeMode) => void;
|
||||
setResolvedThemeMode: (mode: ResolvedThemeMode) => void;
|
||||
setThemePreset: (preset: ThemePreset) => void;
|
||||
setFont: (font: FontKey) => void;
|
||||
setContentLayout: (layout: ContentLayout) => void;
|
||||
setNavbarStyle: (style: NavbarStyle) => void;
|
||||
setSidebarVariant: (variant: SidebarVariant) => void;
|
||||
setSidebarCollapsible: (mode: SidebarCollapsible) => void;
|
||||
isSynced: boolean;
|
||||
setIsSynced: (val: boolean) => void;
|
||||
};
|
||||
|
||||
export const createPreferencesStore = (init?: Partial<PreferencesState>) =>
|
||||
createStore<PreferencesState>()((set) => ({
|
||||
themeMode: init?.themeMode ?? PREFERENCE_DEFAULTS.theme_mode,
|
||||
resolvedThemeMode: init?.resolvedThemeMode ?? "light",
|
||||
themePreset: init?.themePreset ?? PREFERENCE_DEFAULTS.theme_preset,
|
||||
font: init?.font ?? PREFERENCE_DEFAULTS.font,
|
||||
contentLayout: init?.contentLayout ?? PREFERENCE_DEFAULTS.content_layout,
|
||||
navbarStyle: init?.navbarStyle ?? PREFERENCE_DEFAULTS.navbar_style,
|
||||
sidebarVariant: init?.sidebarVariant ?? PREFERENCE_DEFAULTS.sidebar_variant,
|
||||
sidebarCollapsible: init?.sidebarCollapsible ?? PREFERENCE_DEFAULTS.sidebar_collapsible,
|
||||
setThemeMode: (mode) => set({ themeMode: mode }),
|
||||
setResolvedThemeMode: (mode) => set({ resolvedThemeMode: mode }),
|
||||
setThemePreset: (preset) => set({ themePreset: preset }),
|
||||
setFont: (font) => set({ font }),
|
||||
setContentLayout: (layout) => set({ contentLayout: layout }),
|
||||
setNavbarStyle: (style) => set({ navbarStyle: style }),
|
||||
setSidebarVariant: (variant) => set({ sidebarVariant: variant }),
|
||||
setSidebarCollapsible: (mode) => set({ sidebarCollapsible: mode }),
|
||||
isSynced: init?.isSynced ?? false,
|
||||
setIsSynced: (val) => set({ isSynced: val }),
|
||||
}));
|
||||
Reference in New Issue
Block a user