- 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
75 lines
2.4 KiB
TypeScript
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,
|
|
}
|
|
}
|