typescript

This commit is contained in:
2025-11-13 01:07:55 +01:00
parent b812d748ea
commit ada089c05b
147 changed files with 7427 additions and 2534 deletions
+325
View File
@@ -0,0 +1,325 @@
import React, {
createContext,
useContext,
useMemo,
useState,
useCallback,
useEffect,
} from 'react';
interface SidebarContextValue {
collapsed: boolean;
setCollapsed: (value: boolean) => void;
neutralHue: number;
setNeutralHue: (value: number | string) => void;
resetNeutralHue: () => void;
neutralChroma: number;
setNeutralChroma: (value: number | string) => void;
resetNeutralChroma: () => void;
neutralContrast: number;
setNeutralContrast: (value: number | string) => void;
resetNeutralContrast: () => void;
themeMode: ThemeMode;
setThemeMode: (mode: ThemeMode) => void;
cycleThemeMode: () => void;
themeModes: ThemeMode[];
}
type ThemeMode = 'system' | 'light' | 'dark';
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
const SidebarContext = createContext<SidebarContextValue | null>(null);
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
const DEFAULT_NEUTRAL_HUE = 145;
const DEFAULT_NEUTRAL_CHROMA = 0.1;
const DEFAULT_NEUTRAL_CONTRAST = 0.3;
const DEFAULT_THEME_MODE = 'system';
const THEME_MODES = ['system', 'light', 'dark'];
const loadInitialThemeSettings = () => {
const defaults = {
neutralHue: DEFAULT_NEUTRAL_HUE,
neutralChroma: DEFAULT_NEUTRAL_CHROMA,
neutralContrast: DEFAULT_NEUTRAL_CONTRAST,
mode: DEFAULT_THEME_MODE,
};
const root = document.documentElement;
const readNumberVar = (name, fallback) => {
const inlineValue = root.style.getPropertyValue(name);
const inlineParsed = Number.parseFloat(inlineValue);
if (!Number.isNaN(inlineParsed)) {
return inlineParsed;
}
const computedValue = window.getComputedStyle(root).getPropertyValue(name);
const computedParsed = Number.parseFloat(computedValue);
if (!Number.isNaN(computedParsed)) {
return computedParsed;
}
return fallback;
};
const loadFromRoot = () => {
const current = root.style.getPropertyValue('--neutral-hue');
const parsed = Number.parseInt(current, 10);
return {
neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed,
neutralChroma: readNumberVar('--neutral-chroma', defaults.neutralChroma),
neutralContrast: readNumberVar('--neutral-contrast', defaults.neutralContrast),
};
};
const rootValues = loadFromRoot();
let neutralHueValue = rootValues.neutralHue;
let neutralChromaValue = rootValues.neutralChroma;
let neutralContrastValue = rootValues.neutralContrast;
let modeValue = defaults.mode;
const composite = window.localStorage.getItem(THEME_STORAGE_KEY);
if (composite) {
try {
const parsed = JSON.parse(composite);
const storedHue = Number.parseInt(parsed?.neutralHue, 10);
if (!Number.isNaN(storedHue)) {
neutralHueValue = storedHue;
}
const storedChroma = Number.parseFloat(parsed?.neutralChroma);
if (!Number.isNaN(storedChroma)) {
neutralChromaValue = Math.min(Math.max(storedChroma, 0), 1);
}
const storedContrast = Number.parseFloat(parsed?.neutralContrast);
if (!Number.isNaN(storedContrast)) {
neutralContrastValue = Math.min(Math.max(storedContrast, 0), 1);
}
const storedMode = parsed?.mode;
if (THEME_MODES.includes(storedMode)) {
modeValue = storedMode;
}
} catch (error) {
console.warn('[theme] failed to parse stored theme settings', error);
}
} else {
const legacyHue = window.localStorage.getItem('papercrate_neutral_hue');
if (legacyHue) {
const parsedLegacyHue = Number.parseInt(legacyHue, 10);
if (!Number.isNaN(parsedLegacyHue)) {
neutralHueValue = parsedLegacyHue;
}
}
const legacyMode = window.localStorage.getItem('papercrate_theme_mode');
if (THEME_MODES.includes(legacyMode)) {
modeValue = legacyMode;
}
}
return {
neutralHue: neutralHueValue,
neutralChroma: neutralChromaValue,
neutralContrast: neutralContrastValue,
mode: modeValue,
};
};
const loadInitialCollapsedState = (defaultValue) => {
try {
const stored = window.sessionStorage.getItem(SIDEBAR_COLLAPSE_STORAGE_KEY);
if (stored === '1' || stored === 'true') {
return true;
}
if (stored === '0' || stored === 'false') {
return false;
}
} catch (error) {
console.warn('[sidebar] failed to read collapse state', error);
}
return Boolean(defaultValue);
};
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
const [collapsed, setCollapsedState] = useState(() => loadInitialCollapsedState(initialCollapsed));
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
const [neutralChroma, setNeutralChromaState] = useState(initialTheme.neutralChroma);
const [neutralContrast, setNeutralContrastState] = useState(initialTheme.neutralContrast);
const [themeMode, setThemeModeState] = useState(initialTheme.mode);
useEffect(() => {
document.documentElement.style.setProperty('--neutral-hue', `${neutralHue}deg`);
}, [neutralHue]);
useEffect(() => {
document.documentElement.style.setProperty('--neutral-chroma', String(neutralChroma));
}, [neutralChroma]);
useEffect(() => {
document.documentElement.style.setProperty('--neutral-contrast', String(neutralContrast));
}, [neutralContrast]);
useEffect(() => {
const root = document.documentElement;
if (themeMode === 'system') {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', themeMode);
}
}, [themeMode]);
useEffect(() => {
try {
const payload = JSON.stringify({
neutralHue,
neutralChroma,
neutralContrast,
mode: themeMode,
});
window.localStorage.setItem(THEME_STORAGE_KEY, payload);
window.localStorage.removeItem('papercrate_neutral_hue');
window.localStorage.removeItem('papercrate_theme_mode');
} catch (error) {
console.warn('[theme] failed to persist theme settings', error);
}
}, [neutralHue, neutralChroma, neutralContrast, themeMode]);
const setNeutralHue = useCallback((value) => {
setNeutralHueState((prev) => {
if (value === '' || value === null || value === undefined) {
return DEFAULT_NEUTRAL_HUE;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return prev;
}
const clamped = Math.min(Math.max(Math.round(parsed), 0), 360);
return clamped;
});
}, []);
const setNeutralChroma = useCallback((value) => {
setNeutralChromaState((prev) => {
if (value === '' || value === null || value === undefined) {
return DEFAULT_NEUTRAL_CHROMA;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return prev;
}
const clamped = Math.min(Math.max(parsed, 0), 1);
return Math.round(clamped * 1000) / 1000;
});
}, []);
const resetNeutralChroma = useCallback(() => {
setNeutralChromaState(DEFAULT_NEUTRAL_CHROMA);
}, []);
const setNeutralContrast = useCallback((value) => {
setNeutralContrastState((prev) => {
if (value === '' || value === null || value === undefined) {
return DEFAULT_NEUTRAL_CONTRAST;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return prev;
}
const clamped = Math.min(Math.max(parsed, 0), 1);
return Math.round(clamped * 1000) / 1000;
});
}, []);
const resetNeutralContrast = useCallback(() => {
setNeutralContrastState(DEFAULT_NEUTRAL_CONTRAST);
}, []);
const resetNeutralHue = useCallback(() => {
setNeutralHueState(DEFAULT_NEUTRAL_HUE);
}, []);
const setThemeMode = useCallback((mode) => {
if (!THEME_MODES.includes(mode)) {
return;
}
setThemeModeState(mode);
}, []);
const cycleThemeMode = useCallback(() => {
const index = THEME_MODES.indexOf(themeMode);
const nextIndex = index === -1 ? 0 : (index + 1) % THEME_MODES.length;
setThemeModeState(THEME_MODES[nextIndex]);
}, [themeMode]);
useEffect(() => {
try {
window.sessionStorage.setItem(SIDEBAR_COLLAPSE_STORAGE_KEY, collapsed ? '1' : '0');
} catch (error) {
console.warn('[sidebar] failed to persist collapse state', error);
}
}, [collapsed]);
const setCollapsed = useCallback((value) => {
if (typeof value === 'function') {
setCollapsedState((prev) => Boolean(value(prev)));
return;
}
setCollapsedState(Boolean(value));
}, []);
const contextValue = useMemo(
() => ({
collapsed,
setCollapsed,
neutralHue,
setNeutralHue,
neutralChroma,
setNeutralChroma,
neutralContrast,
setNeutralContrast,
resetNeutralHue,
resetNeutralChroma,
resetNeutralContrast,
themeMode,
setThemeMode,
cycleThemeMode,
themeModes: THEME_MODES,
defaultNeutralHue: DEFAULT_NEUTRAL_HUE,
defaultNeutralChroma: DEFAULT_NEUTRAL_CHROMA,
defaultNeutralContrast: DEFAULT_NEUTRAL_CONTRAST,
}),
[
collapsed,
setCollapsed,
neutralHue,
setNeutralHue,
neutralChroma,
setNeutralChroma,
neutralContrast,
setNeutralContrast,
resetNeutralHue,
resetNeutralChroma,
resetNeutralContrast,
themeMode,
setThemeMode,
cycleThemeMode,
],
);
return <SidebarContext.Provider value={contextValue}>{children}</SidebarContext.Provider>;
};
export const useSidebarContext = () => {
const context = useContext(SidebarContext);
if (!context) {
throw new Error('useSidebarContext must be used within a SidebarProvider');
}
return context;
};
export const useSidebarControls = () => {
const { setCollapsed } = useSidebarContext();
const openSidebar = useCallback(() => setCollapsed(false), [setCollapsed]);
const closeSidebar = useCallback(() => setCollapsed(true), [setCollapsed]);
return { openSidebar, closeSidebar };
};
export default SidebarContext;