84 lines
2.9 KiB
TypeScript
84 lines
2.9 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
|
|
import { ChromeConfigContext, UiStoreContext, type ChromeConfig, type Contrast, type Density, type UiStore } from './chrome-store'
|
|
|
|
export function ChromeConfigProvider({ value, children }: { value: ChromeConfig; children: ReactNode }) {
|
|
return <ChromeConfigContext.Provider value={value}>{children}</ChromeConfigContext.Provider>
|
|
}
|
|
|
|
function readStored<T>(key: string, fallback: T): T {
|
|
try {
|
|
const raw = window.localStorage.getItem(key)
|
|
return raw ? (JSON.parse(raw) as T) : fallback
|
|
} catch {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
function writeStored(key: string, value: unknown) {
|
|
try {
|
|
window.localStorage.setItem(key, JSON.stringify(value))
|
|
} catch {
|
|
/* без сохранения настроек */
|
|
}
|
|
}
|
|
|
|
/** Плотность, контраст, закреплённые пункты и палитра ⌘K. Атрибуты html[data-density|contrast] читает portal-theme.css. */
|
|
export function PortalUiProvider({ children, storageKey = 'portal-ui' }: { children: ReactNode; storageKey?: string }) {
|
|
const [paletteOpen, setPaletteOpen] = useState(false)
|
|
const [density, setDensityState] = useState<Density>(() => readStored(`${storageKey}:density`, 'comfortable'))
|
|
const [contrast, setContrastState] = useState<Contrast>(() => readStored(`${storageKey}:contrast`, 'standard'))
|
|
const [pins, setPins] = useState<string[]>(() => readStored(`${storageKey}:pins`, []))
|
|
|
|
const setDensity = useCallback(
|
|
(v: Density) => {
|
|
setDensityState(v)
|
|
writeStored(`${storageKey}:density`, v)
|
|
},
|
|
[storageKey],
|
|
)
|
|
const setContrast = useCallback(
|
|
(v: Contrast) => {
|
|
setContrastState(v)
|
|
writeStored(`${storageKey}:contrast`, v)
|
|
},
|
|
[storageKey],
|
|
)
|
|
const togglePin = useCallback(
|
|
(id: string) => {
|
|
setPins((prev) => {
|
|
const next = prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
writeStored(`${storageKey}:pins`, next)
|
|
return next
|
|
})
|
|
},
|
|
[storageKey],
|
|
)
|
|
|
|
useEffect(() => {
|
|
const root = document.documentElement
|
|
root.dataset.density = density
|
|
root.dataset.contrast = contrast
|
|
return () => {
|
|
delete root.dataset.density
|
|
delete root.dataset.contrast
|
|
}
|
|
}, [density, contrast])
|
|
|
|
useEffect(() => {
|
|
function onKey(e: KeyboardEvent) {
|
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
|
e.preventDefault()
|
|
setPaletteOpen((v) => !v)
|
|
}
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => window.removeEventListener('keydown', onKey)
|
|
}, [])
|
|
|
|
const value = useMemo<UiStore>(
|
|
() => ({ paletteOpen, setPaletteOpen, pins, togglePin, density, setDensity, contrast, setContrast }),
|
|
[paletteOpen, pins, togglePin, density, setDensity, contrast, setContrast],
|
|
)
|
|
return <UiStoreContext.Provider value={value}>{children}</UiStoreContext.Provider>
|
|
}
|