hue control

This commit is contained in:
2025-10-30 02:38:48 +01:00
parent 9fe97f2b8a
commit c65a3d2c40
4 changed files with 107 additions and 0 deletions
+41
View File
@@ -461,6 +461,22 @@ const AppLayout = () => {
const stored = window.localStorage.getItem('papercrate_view_mode'); const stored = window.localStorage.getItem('papercrate_view_mode');
return stored === 'grid' ? 'grid' : 'list'; return stored === 'grid' ? 'grid' : 'list';
}); });
const [neutralHue, setNeutralHue] = useState(() => {
if (typeof window === 'undefined') {
return 260;
}
const stored = window.localStorage.getItem('papercrate_neutral_hue');
if (stored) {
const parsedStored = Number.parseInt(stored, 10);
if (!Number.isNaN(parsedStored)) {
return parsedStored;
}
}
const root = document.documentElement;
const current = root.style.getPropertyValue('--neutral-hue');
const parsed = Number.parseInt(current, 10);
return Number.isNaN(parsed) ? 260 : parsed;
});
const initialRowSelection = []; const initialRowSelection = [];
const [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection); const [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection);
const [selectionOrder, setSelectionOrder] = useState(initialRowSelection); const [selectionOrder, setSelectionOrder] = useState(initialRowSelection);
@@ -558,6 +574,27 @@ const AppLayout = () => {
} }
}, [token, appStatus, refreshAccessToken]); }, [token, appStatus, refreshAccessToken]);
useEffect(() => {
if (typeof document !== 'undefined') {
document.documentElement.style.setProperty('--neutral-hue', `${neutralHue}deg`);
}
if (typeof window !== 'undefined') {
window.localStorage.setItem('papercrate_neutral_hue', String(neutralHue));
}
}, [neutralHue]);
const handleNeutralHueChange = useCallback((value) => {
if (value === '') {
setNeutralHue(260);
return;
}
const parsed = Number(value);
if (Number.isNaN(parsed)) {
return;
}
setNeutralHue(parsed);
}, []);
const clearFilters = useCallback(() => { const clearFilters = useCallback(() => {
setSearchQuery(''); setSearchQuery('');
setActiveTagFilters([]); setActiveTagFilters([]);
@@ -5035,6 +5072,8 @@ const AppLayout = () => {
draggedFolderId, draggedFolderId,
onCreateFolder: handlePromptCreateFolder, onCreateFolder: handlePromptCreateFolder,
creatingFolder, creatingFolder,
onNeutralHueChange: handleNeutralHueChange,
neutralHue,
tags, tags,
activeTagIds: activeTagFilters, activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter, onToggleTagFilter: toggleTagFilter,
@@ -5093,6 +5132,8 @@ const AppLayout = () => {
handleCorrespondentCreate, handleCorrespondentCreate,
handlePromptCreateFolder, handlePromptCreateFolder,
creatingFolder, creatingFolder,
handleNeutralHueChange,
neutralHue,
], ],
); );
+36
View File
@@ -11,6 +11,7 @@ import {
CheckIcon, CheckIcon,
PlusIcon, PlusIcon,
FolderPlusIcon, FolderPlusIcon,
RestoreIcon,
} from '../ui/icons'; } from '../ui/icons';
import { getTagColorStyle } from '../utils/colors'; import { getTagColorStyle } from '../utils/colors';
@@ -142,6 +143,8 @@ const Sidebar = ({
draggedFolderId, draggedFolderId,
onCreateFolder, onCreateFolder,
creatingFolder = false, creatingFolder = false,
onNeutralHueChange,
neutralHue,
tags = [], tags = [],
activeTagIds = [], activeTagIds = [],
onToggleTagFilter, onToggleTagFilter,
@@ -220,6 +223,10 @@ const Sidebar = ({
onCreateFolder?.(); onCreateFolder?.();
}, [creatingFolder, onCreateFolder]); }, [creatingFolder, onCreateFolder]);
const handleNeutralHueReset = useCallback(() => {
onNeutralHueChange?.('');
}, [onNeutralHueChange]);
const handleSearchInputChange = useCallback( const handleSearchInputChange = useCallback(
(event) => { (event) => {
onSearchChange?.(event.target.value); onSearchChange?.(event.target.value);
@@ -587,6 +594,35 @@ const Sidebar = ({
})} })}
</ul> </ul>
</div> </div>
{typeof neutralHue === 'number' || typeof neutralHue === 'string' ? (
<div className="sidebar-section">
<div className="sidebar-section__header">
<h3>Theme</h3>
<div className="sidebar-section__actions">
<button
type="button"
className="icon-button"
onClick={handleNeutralHueReset}
aria-label="Reset neutral hue"
>
<RestoreIcon size={16} />
</button>
</div>
</div>
<label className="sidebar-slider">
<span className="sidebar-slider__label">Neutral hue</span>
<input
type="range"
min="0"
max="360"
step="1"
value={neutralHue}
onChange={(event) => onNeutralHueChange?.(event.target.value)}
/>
<span className="sidebar-slider__value">{neutralHue}°</span>
</label>
</div>
) : null}
</div> </div>
</aside> </aside>
); );
+20
View File
@@ -1518,6 +1518,26 @@ button.danger:hover:not([disabled]) {
color: var(--muted); color: var(--muted);
} }
.sidebar-slider {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--muted);
}
.sidebar-slider input[type='range'] {
flex: 1;
accent-color: var(--accent);
}
.sidebar-slider__value {
min-width: 3rem;
text-align: right;
color: var(--fg);
font-variant-numeric: tabular-nums;
}
.sidebar-item { .sidebar-item {
background: transparent; background: transparent;
padding: 0.16rem 0.26rem; padding: 0.16rem 0.26rem;
+10
View File
@@ -17,6 +17,7 @@ import {
IconTextScan2, IconTextScan2,
IconFolderPlus, IconFolderPlus,
IconRefresh, IconRefresh,
IconRestore,
IconMinusVertical, IconMinusVertical,
IconLogout, IconLogout,
IconChevronDown, IconChevronDown,
@@ -171,6 +172,15 @@ export const RefreshIcon = ({ className, size = '1em', stroke = 1.6, ...rest })
/> />
); );
export const RestoreIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconRestore
className={composeClassName('icon', className)}
size={size}
stroke={stroke}
{...rest}
/>
);
export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => ( export const ArrowUpIcon = ({ className, size = '1em', stroke = 1.6, ...rest }) => (
<IconArrowUp <IconArrowUp
className={composeClassName('icon', className)} className={composeClassName('icon', className)}