hue control
This commit is contained in:
@@ -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,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user