feat: add custom style theme with per-type node/edge style editor
- New 'custom' ThemeId with dedicated ThemeCard + pencil edit button - CustomStyleModal: two-column editor for all node types (excl. groupRect/group) and edge types - Per node: border, background, icon color + opacity, default size (w/h) - Per edge: color + opacity, path style, animation - 'Apply to existing [Type]' per type, 'Apply All to Canvas' footer action - canvasStore: applyTypeNodeStyle, applyTypeEdgeStyle, applyAllCustomStyles actions - themeStore: customStyle state + setCustomStyle action - Backend: custom_style JSON column on canvas_state (ALTER TABLE migration), saved/loaded with canvas - App.tsx: custom_style included in save payload, restored on load (API + standalone) - Tests: +8 frontend (themeStore + canvasStore), +3 backend (canvas API)
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { useThemeStore } from '@/stores/themeStore'
|
||||
import type { CustomStyleDef } from '@/types'
|
||||
|
||||
describe('themeStore', () => {
|
||||
beforeEach(() => {
|
||||
useThemeStore.setState({ activeTheme: 'default' })
|
||||
useThemeStore.setState({ activeTheme: 'default', customStyle: { nodes: {}, edges: {} } })
|
||||
})
|
||||
|
||||
it('starts with default theme', () => {
|
||||
@@ -15,8 +16,8 @@ describe('themeStore', () => {
|
||||
expect(useThemeStore.getState().activeTheme).toBe('matrix')
|
||||
})
|
||||
|
||||
it('setTheme can switch between all presets', () => {
|
||||
const themes = ['default', 'dark', 'light', 'neon', 'matrix'] as const
|
||||
it('setTheme can switch between all presets including custom', () => {
|
||||
const themes = ['default', 'dark', 'light', 'neon', 'matrix', 'custom'] as const
|
||||
for (const id of themes) {
|
||||
useThemeStore.getState().setTheme(id)
|
||||
expect(useThemeStore.getState().activeTheme).toBe(id)
|
||||
@@ -28,4 +29,26 @@ describe('themeStore', () => {
|
||||
useThemeStore.getState().setTheme('default')
|
||||
expect(useThemeStore.getState().activeTheme).toBe('default')
|
||||
})
|
||||
|
||||
it('starts with empty customStyle', () => {
|
||||
const { customStyle } = useThemeStore.getState()
|
||||
expect(customStyle.nodes).toEqual({})
|
||||
expect(customStyle.edges).toEqual({})
|
||||
})
|
||||
|
||||
it('setCustomStyle replaces the entire definition', () => {
|
||||
const def: CustomStyleDef = {
|
||||
nodes: { server: { borderColor: '#ff0000', borderOpacity: 1, bgColor: '#000000', bgOpacity: 1, iconColor: '#ff0000', iconOpacity: 1, width: 200, height: 80 } },
|
||||
edges: { ethernet: { color: '#00ff00', opacity: 0.8, pathStyle: 'bezier', animated: 'none' } },
|
||||
}
|
||||
useThemeStore.getState().setCustomStyle(def)
|
||||
expect(useThemeStore.getState().customStyle.nodes.server?.borderColor).toBe('#ff0000')
|
||||
expect(useThemeStore.getState().customStyle.edges.ethernet?.color).toBe('#00ff00')
|
||||
})
|
||||
|
||||
it('setCustomStyle with empty def clears styles', () => {
|
||||
useThemeStore.getState().setCustomStyle({ nodes: { server: { borderColor: '#aaa', borderOpacity: 1, bgColor: '#000', bgOpacity: 1, iconColor: '#aaa', iconOpacity: 1, width: 0, height: 0 } }, edges: {} })
|
||||
useThemeStore.getState().setCustomStyle({ nodes: {}, edges: {} })
|
||||
expect(useThemeStore.getState().customStyle.nodes).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user