c93865e187
A toggle-able env badge is a sticker, not a safety signal. Move env to
useEnv() which reads window.location.hostname:
*.local / localhost → 'dev'
*staging* → 'staging'
everything else → 'prod' (safest default)
- New composable: apps/operator/composables/useEnv.ts
- Topbar reads useEnv() instead of useTweaks().env
- useTweaks loses the env field; hydrate strips it from stale
localStorage payloads so old entries don't break
- TweaksPanel: env section removed (theme + density remain)
- Settings: env section removed from Appearance; added a read-only
Environment row to the Profile card showing the detected env +
hostname source ("auto-detected from operator.dezky.local")
76 lines
2.1 KiB
TypeScript
76 lines
2.1 KiB
TypeScript
// Cosmetic user-controllable preferences for the operator shell — theme
|
|
// (dark/light) and density (comfy/compact). Persisted in localStorage so
|
|
// the choices survive page reloads. The values are applied to <html> as
|
|
// data-* attributes; tokens.css picks them up via selector overrides.
|
|
//
|
|
// NOTE: `env` used to live here but is now derived from the hostname via
|
|
// `useEnv()` so it's a real environment signal, not a sticker the operator
|
|
// can flip.
|
|
|
|
export type ThemeMode = 'dark' | 'light'
|
|
export type Density = 'comfy' | 'compact'
|
|
|
|
interface TweakState {
|
|
theme: ThemeMode
|
|
density: Density
|
|
}
|
|
|
|
const STORAGE_KEY = 'dezky-operator-tweaks'
|
|
|
|
const DEFAULTS: TweakState = { theme: 'dark', density: 'comfy' }
|
|
|
|
const state = ref<TweakState>({ ...DEFAULTS })
|
|
const hydrated = ref(false)
|
|
|
|
function apply() {
|
|
if (!import.meta.client) return
|
|
const root = document.documentElement
|
|
root.setAttribute('data-theme', state.value.theme)
|
|
root.setAttribute('data-density', state.value.density)
|
|
}
|
|
|
|
function persist() {
|
|
if (!import.meta.client) return
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state.value))
|
|
} catch {
|
|
// localStorage can throw in private mode; tweaks are cosmetic so swallow.
|
|
}
|
|
}
|
|
|
|
function hydrate() {
|
|
if (!import.meta.client || hydrated.value) return
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
if (raw) {
|
|
// Old payloads carried an `env` field — pick fields we still care about
|
|
// and ignore the rest so a stale localStorage entry doesn't break.
|
|
const parsed = JSON.parse(raw) as Partial<TweakState>
|
|
state.value = {
|
|
theme: parsed.theme ?? DEFAULTS.theme,
|
|
density: parsed.density ?? DEFAULTS.density,
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore corrupt JSON
|
|
}
|
|
apply()
|
|
hydrated.value = true
|
|
}
|
|
|
|
export const useTweaks = () => {
|
|
if (import.meta.client) hydrate()
|
|
|
|
function set<K extends keyof TweakState>(key: K, value: TweakState[K]) {
|
|
state.value = { ...state.value, [key]: value }
|
|
apply()
|
|
persist()
|
|
}
|
|
|
|
return {
|
|
state,
|
|
setTheme: (v: ThemeMode) => set('theme', v),
|
|
setDensity: (v: Density) => set('density', v),
|
|
}
|
|
}
|