This commit is contained in:
2025-11-01 19:59:39 +01:00
parent e752361f39
commit 9559a1b5e2
3 changed files with 187 additions and 142 deletions
-119
View File
@@ -28,7 +28,6 @@ const DEFAULT_FOLDER_NAME = 'Documents';
const ROW_KEY_SEPARATOR = ':';
const DOCUMENT_ROW_PREFIX = 'document';
const FOLDER_ROW_PREFIX = 'folder';
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
const resolveApiPath = (path = '') => path;
@@ -199,66 +198,6 @@ const AppLayout = () => {
});
const lastNonDeskViewRef = useRef(documentsViewMode === 'desk' ? 'list' : documentsViewMode);
const loadThemeSettings = () => {
const defaults = { neutralHue: 260, mode: 'system' };
if (typeof window === 'undefined') {
if (typeof document !== 'undefined') {
const current = document.documentElement.style.getPropertyValue('--neutral-hue');
const parsed = Number.parseInt(current, 10);
return {
neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed,
mode: defaults.mode,
};
}
return defaults;
}
const root = document.documentElement;
const loadFromRoot = () => {
const current = root.style.getPropertyValue('--neutral-hue');
const parsed = Number.parseInt(current, 10);
return Number.isNaN(parsed) ? defaults.neutralHue : parsed;
};
let neutralHueValue = loadFromRoot();
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 storedMode = parsed?.mode;
if (storedMode === 'light' || storedMode === 'dark' || storedMode === 'system') {
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 (legacyMode === 'light' || legacyMode === 'dark' || legacyMode === 'system') {
modeValue = legacyMode;
}
}
return { neutralHue: neutralHueValue, mode: modeValue };
};
const initialThemeSettings = loadThemeSettings();
const [neutralHue, setNeutralHue] = useState(initialThemeSettings.neutralHue);
const [themeMode, setThemeMode] = useState(initialThemeSettings.mode);
useEffect(() => {
if (documentsViewMode !== 'desk') {
lastNonDeskViewRef.current = documentsViewMode;
@@ -363,56 +302,6 @@ const AppLayout = () => {
}
}, [token, appStatus, refreshAccessToken]);
useEffect(() => {
if (typeof document !== 'undefined') {
document.documentElement.style.setProperty('--neutral-hue', `${neutralHue}deg`);
}
}, [neutralHue]);
useEffect(() => {
if (typeof document !== 'undefined') {
const root = document.documentElement;
if (themeMode === 'system') {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', themeMode);
}
}
}, [themeMode]);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
const payload = JSON.stringify({ neutralHue, 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]);
const handleNeutralHueChange = useCallback((value) => {
if (value === '') {
setNeutralHue(180);
return;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return;
}
setNeutralHue(parsed);
}, []);
const handleThemeModeChange = useCallback((mode) => {
if (mode !== 'light' && mode !== 'dark' && mode !== 'system') {
return;
}
setThemeMode(mode);
}, []);
const clearFilters = useCallback(() => {
setSearchQuery('');
setActiveTagFilters([]);
@@ -5084,10 +4973,6 @@ const AppLayout = () => {
draggedFolderId,
onCreateFolder: handlePromptCreateFolder,
creatingFolder,
onNeutralHueChange: handleNeutralHueChange,
neutralHue,
themeMode,
onThemeModeChange: handleThemeModeChange,
tags,
activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter,
@@ -5146,10 +5031,6 @@ const AppLayout = () => {
handleCorrespondentCreate,
handlePromptCreateFolder,
creatingFolder,
handleNeutralHueChange,
neutralHue,
themeMode,
handleThemeModeChange,
],
);
+35 -21
View File
@@ -157,10 +157,6 @@ const Sidebar = ({
draggedFolderId,
onCreateFolder,
creatingFolder = false,
onNeutralHueChange,
neutralHue,
themeMode = 'system',
onThemeModeChange,
tags = [],
activeTagIds = [],
onToggleTagFilter,
@@ -184,7 +180,15 @@ const Sidebar = ({
onSelectTenant,
onOpenSettings,
}) => {
const { setCollapsed } = useSidebarContext();
const {
setCollapsed,
neutralHue,
setNeutralHue,
resetNeutralHue,
themeMode,
cycleThemeMode,
themeModes,
} = useSidebarContext();
const handleCollapse = useCallback(() => {
setCollapsed(true);
}, [setCollapsed]);
@@ -246,15 +250,31 @@ const Sidebar = ({
}, [creatingFolder, onCreateFolder]);
const handleNeutralHueReset = useCallback(() => {
onNeutralHueChange?.('');
}, [onNeutralHueChange]);
resetNeutralHue();
}, [resetNeutralHue]);
const themeModeIndex = THEME_MODES.indexOf(themeMode);
const handleNeutralHueChange = useCallback(
(value) => {
if (value === '') {
resetNeutralHue();
return;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return;
}
setNeutralHue(parsed);
},
[resetNeutralHue, setNeutralHue],
);
const themeModeList = themeModes && themeModes.length ? themeModes : THEME_MODES;
const themeModeIndex = themeModeList.indexOf(themeMode);
const safeThemeModeIndex = themeModeIndex === -1 ? 0 : themeModeIndex;
const resolvedThemeMode = THEME_MODES[safeThemeModeIndex];
const nextThemeMode = THEME_MODES[(safeThemeModeIndex + 1) % THEME_MODES.length];
const themeModeLabel = THEME_MODE_LABELS[resolvedThemeMode];
const nextThemeLabel = THEME_MODE_LABELS[nextThemeMode];
const resolvedThemeMode = themeModeList[safeThemeModeIndex] || THEME_MODES[0];
const nextThemeMode = themeModeList[(safeThemeModeIndex + 1) % themeModeList.length];
const themeModeLabel = THEME_MODE_LABELS[resolvedThemeMode] || THEME_MODE_LABELS.system;
const nextThemeLabel = THEME_MODE_LABELS[nextThemeMode] || THEME_MODE_LABELS.system;
const themeModeIcon = resolvedThemeMode === 'dark'
? <MoonIcon size={16} />
: resolvedThemeMode === 'light'
@@ -262,14 +282,8 @@ const Sidebar = ({
: <DesktopIcon size={16} />;
const handleThemeModeToggle = useCallback(() => {
if (!onThemeModeChange) {
return;
}
const currentIndex = THEME_MODES.indexOf(themeMode);
const safeIndex = currentIndex === -1 ? 0 : currentIndex;
const nextMode = THEME_MODES[(safeIndex + 1) % THEME_MODES.length];
onThemeModeChange(nextMode);
}, [onThemeModeChange, themeMode]);
cycleThemeMode();
}, [cycleThemeMode]);
const handleSearchInputChange = useCallback(
(event) => {
@@ -415,7 +429,7 @@ const Sidebar = ({
max="360"
step="1"
value={neutralHue}
onChange={(event) => onNeutralHueChange?.(event.target.value)}
onChange={(event) => handleNeutralHueChange(event.target.value)}
/>
<span className="sidebar-slider__value">{neutralHue}°</span>
</label>
+152 -2
View File
@@ -1,11 +1,161 @@
import React, { createContext, useContext, useMemo, useState, useCallback } from 'react';
import React, {
createContext,
useContext,
useMemo,
useState,
useCallback,
useEffect,
} from 'react';
const SidebarContext = createContext(null);
const THEME_STORAGE_KEY = 'papercrate_theme_settings';
const DEFAULT_NEUTRAL_HUE = 180;
const DEFAULT_THEME_MODE = 'system';
const THEME_MODES = ['system', 'light', 'dark'];
const loadInitialThemeSettings = () => {
const defaults = { neutralHue: DEFAULT_NEUTRAL_HUE, mode: DEFAULT_THEME_MODE };
if (typeof window === 'undefined') {
if (typeof document !== 'undefined') {
const current = document.documentElement.style.getPropertyValue('--neutral-hue');
const parsed = Number.parseInt(current, 10);
return {
neutralHue: Number.isNaN(parsed) ? defaults.neutralHue : parsed,
mode: defaults.mode,
};
}
return defaults;
}
const root = document.documentElement;
const loadFromRoot = () => {
const current = root.style.getPropertyValue('--neutral-hue');
const parsed = Number.parseInt(current, 10);
return Number.isNaN(parsed) ? defaults.neutralHue : parsed;
};
let neutralHueValue = loadFromRoot();
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 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, mode: modeValue };
};
export const SidebarProvider = ({ initialCollapsed = false, children }) => {
const [collapsed, setCollapsed] = useState(initialCollapsed);
const initialTheme = useMemo(() => loadInitialThemeSettings(), []);
const [neutralHue, setNeutralHueState] = useState(initialTheme.neutralHue);
const [themeMode, setThemeModeState] = useState(initialTheme.mode);
const contextValue = useMemo(() => ({ collapsed, setCollapsed }), [collapsed]);
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
document.documentElement.style.setProperty('--neutral-hue', `${neutralHue}deg`);
}, [neutralHue]);
useEffect(() => {
if (typeof document === 'undefined') {
return;
}
const root = document.documentElement;
if (themeMode === 'system') {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', themeMode);
}
}, [themeMode]);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
const payload = JSON.stringify({ neutralHue, 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]);
const setNeutralHue = useCallback((value) => {
setNeutralHueState((prev) => {
if (value === '' || value === null || typeof 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 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]);
const contextValue = useMemo(
() => ({
collapsed,
setCollapsed,
neutralHue,
setNeutralHue,
resetNeutralHue,
themeMode,
setThemeMode,
cycleThemeMode,
themeModes: THEME_MODES,
defaultNeutralHue: DEFAULT_NEUTRAL_HUE,
}),
[collapsed, neutralHue, setNeutralHue, resetNeutralHue, themeMode, setThemeMode, cycleThemeMode],
);
return <SidebarContext.Provider value={contextValue}>{children}</SidebarContext.Provider>;
};