theme
This commit is contained in:
@@ -195,6 +195,12 @@ const Sidebar = ({
|
||||
neutralHue,
|
||||
setNeutralHue,
|
||||
resetNeutralHue,
|
||||
neutralChroma,
|
||||
setNeutralChroma,
|
||||
resetNeutralChroma,
|
||||
neutralContrast,
|
||||
setNeutralContrast,
|
||||
resetNeutralContrast,
|
||||
themeMode,
|
||||
cycleThemeMode,
|
||||
themeModes,
|
||||
@@ -204,6 +210,8 @@ const Sidebar = ({
|
||||
setCollapsed(true);
|
||||
}, [setCollapsed]);
|
||||
const neutralHueInputId = useId();
|
||||
const neutralChromaInputId = useId();
|
||||
const neutralContrastInputId = useId();
|
||||
const sortedCorrespondents = useMemo(() => {
|
||||
if (!Array.isArray(correspondents)) {
|
||||
return [];
|
||||
@@ -281,9 +289,11 @@ const Sidebar = ({
|
||||
[onUploadFiles, selectedFolder],
|
||||
);
|
||||
|
||||
const handleNeutralHueReset = useCallback(() => {
|
||||
const handleThemeAdjustmentsReset = useCallback(() => {
|
||||
resetNeutralHue();
|
||||
}, [resetNeutralHue]);
|
||||
resetNeutralChroma();
|
||||
resetNeutralContrast();
|
||||
}, [resetNeutralHue, resetNeutralChroma, resetNeutralContrast]);
|
||||
|
||||
const handleNeutralHueChange = useCallback(
|
||||
(value) => {
|
||||
@@ -300,6 +310,44 @@ const Sidebar = ({
|
||||
[resetNeutralHue, setNeutralHue],
|
||||
);
|
||||
|
||||
const handleNeutralChromaChange = useCallback(
|
||||
(value) => {
|
||||
if (value === '') {
|
||||
resetNeutralChroma();
|
||||
return;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return;
|
||||
}
|
||||
setNeutralChroma(parsed);
|
||||
},
|
||||
[resetNeutralChroma, setNeutralChroma],
|
||||
);
|
||||
|
||||
const handleNeutralContrastChange = useCallback(
|
||||
(value) => {
|
||||
if (value === '') {
|
||||
resetNeutralContrast();
|
||||
return;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return;
|
||||
}
|
||||
setNeutralContrast(parsed);
|
||||
},
|
||||
[resetNeutralContrast, setNeutralContrast],
|
||||
);
|
||||
|
||||
const formatSliderValue = useCallback((value) => {
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed)) {
|
||||
return value;
|
||||
}
|
||||
return parsed.toFixed(2);
|
||||
}, []);
|
||||
|
||||
const themeModeList = themeModes && themeModes.length ? themeModes : THEME_MODES;
|
||||
const themeModeIndex = themeModeList.indexOf(themeMode);
|
||||
const safeThemeModeIndex = themeModeIndex === -1 ? 0 : themeModeIndex;
|
||||
@@ -336,9 +384,9 @@ const Sidebar = ({
|
||||
<button
|
||||
type="button"
|
||||
className="icon-button"
|
||||
onClick={handleNeutralHueReset}
|
||||
aria-label="Reset neutral hue"
|
||||
title="Reset neutral hue"
|
||||
onClick={handleThemeAdjustmentsReset}
|
||||
aria-label="Reset theme adjustments"
|
||||
title="Reset theme adjustments"
|
||||
>
|
||||
<RestoreIcon size={16} />
|
||||
</button>
|
||||
@@ -354,9 +402,38 @@ const Sidebar = ({
|
||||
step="1"
|
||||
value={neutralHue}
|
||||
onChange={(event) => handleNeutralHueChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralHue}
|
||||
/>
|
||||
<span className="menu__slider-value">{neutralHue}°</span>
|
||||
</label>
|
||||
<label className="menu__slider" htmlFor={neutralChromaInputId}>
|
||||
<span className="menu__slider-label">Chroma</span>
|
||||
<input
|
||||
id={neutralChromaInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={neutralChroma}
|
||||
onChange={(event) => handleNeutralChromaChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralChroma}
|
||||
/>
|
||||
<span className="menu__slider-value">{formatSliderValue(neutralChroma)}</span>
|
||||
</label>
|
||||
<label className="menu__slider" htmlFor={neutralContrastInputId}>
|
||||
<span className="menu__slider-label">Contrast</span>
|
||||
<input
|
||||
id={neutralContrastInputId}
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={neutralContrast}
|
||||
onChange={(event) => handleNeutralContrastChange(event.target.value)}
|
||||
onDoubleClick={resetNeutralContrast}
|
||||
/>
|
||||
<span className="menu__slider-value">{formatSliderValue(neutralContrast)}</span>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
|
||||
@@ -12,19 +12,29 @@ const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
|
||||
const SidebarContext = createContext(null);
|
||||
|
||||
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
|
||||
const DEFAULT_NEUTRAL_HUE = 180;
|
||||
const DEFAULT_NEUTRAL_HUE = 145;
|
||||
const DEFAULT_NEUTRAL_CHROMA = 0.15;
|
||||
const DEFAULT_NEUTRAL_CONTRAST = 0.364;
|
||||
const DEFAULT_THEME_MODE = 'system';
|
||||
const THEME_MODES = ['system', 'light', 'dark'];
|
||||
|
||||
const loadInitialThemeSettings = () => {
|
||||
const defaults = { neutralHue: DEFAULT_NEUTRAL_HUE, mode: DEFAULT_THEME_MODE };
|
||||
const defaults = {
|
||||
neutralHue: DEFAULT_NEUTRAL_HUE,
|
||||
neutralChroma: DEFAULT_NEUTRAL_CHROMA,
|
||||
neutralContrast: DEFAULT_NEUTRAL_CONTRAST,
|
||||
mode: DEFAULT_THEME_MODE,
|
||||
};
|
||||
|
||||
if (typeof window === 'undefined') {
|
||||
if (typeof document !== 'undefined') {
|
||||
const current = document.documentElement.style.getPropertyValue('--neutral-hue');
|
||||
const root = document.documentElement;
|
||||
const current = root.style.getPropertyValue('--neutral-hue');
|
||||
const parsed = Number.parseInt(current, 10);
|
||||
return {
|
||||
neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed,
|
||||
neutralChroma: defaults.neutralChroma,
|
||||
neutralContrast: defaults.neutralContrast,
|
||||
mode: defaults.mode,
|
||||
};
|
||||
}
|
||||
@@ -32,13 +42,36 @@ const loadInitialThemeSettings = () => {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.getComputedStyle) {
|
||||
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 Number.isNaN(parsed) ? defaults.neutralHue : parsed;
|
||||
return {
|
||||
neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed,
|
||||
neutralChroma: readNumberVar('--neutral-chroma', defaults.neutralChroma),
|
||||
neutralContrast: readNumberVar('--neutral-contrast', defaults.neutralContrast),
|
||||
};
|
||||
};
|
||||
|
||||
let neutralHueValue = loadFromRoot();
|
||||
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);
|
||||
@@ -49,6 +82,14 @@ const loadInitialThemeSettings = () => {
|
||||
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;
|
||||
@@ -70,7 +111,12 @@ const loadInitialThemeSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { neutralHue: neutralHueValue, mode: modeValue };
|
||||
return {
|
||||
neutralHue: neutralHueValue,
|
||||
neutralChroma: neutralChromaValue,
|
||||
neutralContrast: neutralContrastValue,
|
||||
mode: modeValue,
|
||||
};
|
||||
};
|
||||
|
||||
const loadInitialCollapsedState = (defaultValue) => {
|
||||
@@ -95,6 +141,8 @@ 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(() => {
|
||||
@@ -104,6 +152,20 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
||||
document.documentElement.style.setProperty('--neutral-hue', `${neutralHue}deg`);
|
||||
}, [neutralHue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
document.documentElement.style.setProperty('--neutral-chroma', String(neutralChroma));
|
||||
}, [neutralChroma]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
document.documentElement.style.setProperty('--neutral-contrast', String(neutralContrast));
|
||||
}, [neutralContrast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
@@ -121,14 +183,19 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = JSON.stringify({ neutralHue, mode: themeMode });
|
||||
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, themeMode]);
|
||||
}, [neutralHue, neutralChroma, neutralContrast, themeMode]);
|
||||
|
||||
const setNeutralHue = useCallback((value) => {
|
||||
setNeutralHueState((prev) => {
|
||||
@@ -144,6 +211,42 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const setNeutralChroma = useCallback((value) => {
|
||||
setNeutralChromaState((prev) => {
|
||||
if (value === '' || value === null || typeof 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 || typeof 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);
|
||||
}, []);
|
||||
@@ -186,19 +289,33 @@ export const SidebarProvider = ({ initialCollapsed = false, children }) => {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user