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');
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 [selectedRowKeys, setSelectedRowKeys] = useState(initialRowSelection);
const [selectionOrder, setSelectionOrder] = useState(initialRowSelection);
@@ -558,6 +574,27 @@ const AppLayout = () => {
}
}, [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(() => {
setSearchQuery('');
setActiveTagFilters([]);
@@ -5035,6 +5072,8 @@ const AppLayout = () => {
draggedFolderId,
onCreateFolder: handlePromptCreateFolder,
creatingFolder,
onNeutralHueChange: handleNeutralHueChange,
neutralHue,
tags,
activeTagIds: activeTagFilters,
onToggleTagFilter: toggleTagFilter,
@@ -5093,6 +5132,8 @@ const AppLayout = () => {
handleCorrespondentCreate,
handlePromptCreateFolder,
creatingFolder,
handleNeutralHueChange,
neutralHue,
],
);
+36
View File
@@ -11,6 +11,7 @@ import {
CheckIcon,
PlusIcon,
FolderPlusIcon,
RestoreIcon,
} from '../ui/icons';
import { getTagColorStyle } from '../utils/colors';
@@ -142,6 +143,8 @@ const Sidebar = ({
draggedFolderId,
onCreateFolder,
creatingFolder = false,
onNeutralHueChange,
neutralHue,
tags = [],
activeTagIds = [],
onToggleTagFilter,
@@ -220,6 +223,10 @@ const Sidebar = ({
onCreateFolder?.();
}, [creatingFolder, onCreateFolder]);
const handleNeutralHueReset = useCallback(() => {
onNeutralHueChange?.('');
}, [onNeutralHueChange]);
const handleSearchInputChange = useCallback(
(event) => {
onSearchChange?.(event.target.value);
@@ -587,6 +594,35 @@ const Sidebar = ({
})}
</ul>
</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>
</aside>
);
+20
View File
@@ -1518,6 +1518,26 @@ button.danger:hover:not([disabled]) {
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 {
background: transparent;
padding: 0.16rem 0.26rem;
+10
View File
@@ -17,6 +17,7 @@ import {
IconTextScan2,
IconFolderPlus,
IconRefresh,
IconRestore,
IconMinusVertical,
IconLogout,
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 }) => (
<IconArrowUp
className={composeClassName('icon', className)}