0bd4e5498e
- portal: new admin/ and partner/ surfaces with full component library (AppLauncher, Avatar, Badge, Card, Modal, Tabs, etc.), composables, layouts, partner-routing middleware, and supporting server APIs - pricing: Price schema/module with operator CRUD, pricing.vue catalog UI, Subscription extended with cycle/currency/perSeatAmount/seats snapshots for stable MRR aggregation - partner staff: User.partnerId, invite-partner-user DTO and flow, /partners/:slug/users endpoints, InvitePartnerUserModal, shared dezky-partner-staff Authentik group - /me: partner-aware endpoint returning user + partner context so portal can route between end-user and partner-admin surfaces - tenant: seats field for portfolio displays and future MRR calculations - operator: pricing page, signed-out page, useMe/useToast composables, ToastStack
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
// Cosmetic + role tweaks for the portal shell. Persisted in localStorage so
|
|
// reloads stay coherent during prototyping. Applied to <html> as data-*
|
|
// attributes; tokens.css picks them up via selector overrides.
|
|
//
|
|
// `role` is the most important tweak — it switches which sidebar nav + which
|
|
// pages are visible (end user / customer admin / partner admin). For real use
|
|
// the role would come from Authentik group claims; the tweak lets us preview
|
|
// all three views without standing up three orgs.
|
|
|
|
export type ThemeMode = 'dark' | 'light'
|
|
export type Density = 'comfy' | 'compact'
|
|
export type Accent = 'signal' | 'cobalt' | 'coral' | 'moss'
|
|
export type PortalRole = 'end-user' | 'customer-admin' | 'partner-admin'
|
|
|
|
interface TweakState {
|
|
theme: ThemeMode
|
|
density: Density
|
|
accent: Accent
|
|
role: PortalRole
|
|
}
|
|
|
|
const STORAGE_KEY = 'dezky-portal-tweaks'
|
|
|
|
const DEFAULTS: TweakState = {
|
|
theme: 'light',
|
|
density: 'comfy',
|
|
accent: 'signal',
|
|
role: 'customer-admin',
|
|
}
|
|
|
|
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)
|
|
root.setAttribute('data-accent', state.value.accent)
|
|
root.setAttribute('data-role', state.value.role)
|
|
}
|
|
|
|
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) {
|
|
const parsed = JSON.parse(raw) as Partial<TweakState>
|
|
state.value = {
|
|
theme: parsed.theme ?? DEFAULTS.theme,
|
|
density: parsed.density ?? DEFAULTS.density,
|
|
accent: parsed.accent ?? DEFAULTS.accent,
|
|
role: parsed.role ?? DEFAULTS.role,
|
|
}
|
|
}
|
|
} catch {
|
|
// ignore corrupt JSON
|
|
}
|
|
apply()
|
|
hydrated.value = true
|
|
}
|
|
|
|
export const usePortalTweaks = () => {
|
|
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),
|
|
setAccent: (v: Accent) => set('accent', v),
|
|
setRole: (v: PortalRole) => set('role', v),
|
|
}
|
|
}
|