feat: support multi-canvas (designs) in frontend-only standalone mode

Standalone mode (VITE_STANDALONE=true) skipped the designs system entirely:
the design list stayed empty (switcher hidden) and all canvases collapsed onto
a single localStorage key. Add a localStorage-backed design layer mirroring the
backend designs API.

- standaloneStorage util: list/create/update/delete designs + per-design canvas
  storage (homelable_designs + homelable_canvas:<id>). ensureSeed migrates a
  legacy single-canvas install into a default design so existing data survives.
- App.tsx: load/seed designs, save + switch canvases per design id in standalone.
- Sidebar.tsx: create/update/delete dispatch to standaloneStorage when standalone.
- Tests for the new storage util.

ha-relevant: no
This commit is contained in:
Pouzor
2026-06-29 23:13:56 +02:00
parent c7b4db206b
commit ca171089c2
4 changed files with 271 additions and 28 deletions
+14 -5
View File
@@ -6,6 +6,7 @@ import { useCanvasStore } from '@/stores/canvasStore'
import { useDesignStore } from '@/stores/designStore'
import { useAuthStore } from '@/stores/authStore'
import { designsApi } from '@/api/client'
import * as standaloneStorage from '@/utils/standaloneStorage'
import { resolveDesignIcon, DEFAULT_DESIGN_ICON } from '@/utils/designIcons'
import { DesignModal, type DesignFormData } from '@/components/modals/DesignModal'
import type { Design } from '@/types'
@@ -43,11 +44,15 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
if (!designModal) return
try {
if (designModal.mode === 'create') {
const res = await designsApi.create({ name: data.name, icon: data.icon })
addDesign(res.data)
const created = STANDALONE
? standaloneStorage.createDesign(data.name, data.icon)
: (await designsApi.create({ name: data.name, icon: data.icon })).data
addDesign(created)
} else if (designModal.design) {
const res = await designsApi.update(designModal.design.id, { name: data.name, icon: data.icon })
updateDesign(res.data.id, { name: res.data.name, icon: res.data.icon })
const updated = STANDALONE
? standaloneStorage.updateDesign(designModal.design.id, { name: data.name, icon: data.icon })
: (await designsApi.update(designModal.design.id, { name: data.name, icon: data.icon })).data
if (updated) updateDesign(updated.id, { name: updated.name, icon: updated.icon })
}
setDesignModal(null)
} catch {
@@ -59,7 +64,11 @@ export function Sidebar({ onAddNode, onAddGroupRect, onAddText, onScan, onZigbee
if (designs.length <= 1) { toast.error('Cannot delete the only canvas'); return }
if (!window.confirm(`Delete canvas "${d.name}"? Its nodes and links will be removed.`)) return
try {
await designsApi.delete(d.id)
if (STANDALONE) {
standaloneStorage.deleteDesign(d.id)
} else {
await designsApi.delete(d.id)
}
removeDesign(d.id)
toast.success('Canvas deleted')
} catch {