Files
summercms/admin/src/state/useSidebar.ts
Jakub Zych 21b0d39de0 feat(quick-261016-0fm): manual theme switch on the rail
- the rail's top button cycles system, light and dark and names the
  current choice; light and dark ignore prefers-color-scheme
- usePreferences applies the cached theme before the first await of boot,
  saves the choice through PUT /account/preferences and reverts with a
  toast when the save fails; me() applies the stored preference
2026-10-16 09:54:02 +02:00

75 lines
2.4 KiB
TypeScript

// Sidebar collapsed state (design: Navigation shell, Collapsed / tablet;
// D-06, D-10). The section panel hides below about 1100px or when the admin
// collapses it. The viewport only forces the collapse and never overwrites
// the admin's own choice, which persists in localStorage as a boolean under
// SIDEBAR_KEY. The other writers of browser storage are listState.ts,
// which keeps list query parameters per list, and usePreferences.ts, which
// caches the theme and locale codes; tokens and record data are never stored
// (T-10-22, T-16.1-03).
import { computed, readonly, ref } from 'vue'
export const SIDEBAR_KEY = 'summer-admin.sidebar'
export const NARROW_QUERY = '(max-width: 1099px)'
const manual = ref(false)
const narrow = ref(false)
/** The panel is hidden: forced by the viewport or chosen by the admin. */
export const sidebarCollapsed = computed(() => narrow.value || manual.value)
function storage(): Storage | null {
try {
return typeof window === 'undefined' ? null : window.localStorage
} catch {
return null
}
}
function readPreference(): boolean {
try {
return storage()?.getItem(SIDEBAR_KEY) === 'true'
} catch {
return false
}
}
/** Records the admin's choice; only 'true' or 'false' is stored. */
export function setCollapsed(value: boolean): void {
manual.value = value
try {
storage()?.setItem(SIDEBAR_KEY, value ? 'true' : 'false')
} catch {
// Storage may be full or disabled; the choice still holds for the session.
}
}
/**
* Reads the stored preference and follows the viewport breakpoint. Returns
* the unsubscribe function; the shell calls it when it unmounts.
*/
export function watchViewport(): () => void {
manual.value = readPreference()
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
narrow.value = false
return () => undefined
}
const query = window.matchMedia(NARROW_QUERY)
narrow.value = query.matches
const onChange = (event: MediaQueryListEvent): void => {
narrow.value = event.matches
}
query.addEventListener('change', onChange)
return () => query.removeEventListener('change', onChange)
}
export function useSidebar() {
return {
collapsed: sidebarCollapsed,
/** The viewport is below the breakpoint; expanding is not possible. */
narrow: readonly(narrow),
manual: readonly(manual),
setCollapsed,
watchViewport,
}
}