feat: configurable edge line style + width per type and per edge

Edge render (solid/dashed/dotted) and stroke width were hardcoded per
edge type. Expose both as user settings.

- Custom Style modal (Edges): line-style buttons, 1-4x width slider,
  live preview; left-list swatch renders the actual line.
- Per-edge EdgeModal: same controls; line style follows the type preset
  live until overridden.
- Renderer applies line_style/width_mult over BASE_STYLES (width scales
  markers + animation overlays); unset keeps the type default look.
- Persist line_style/width_mult through serializer, canvas save, and the
  edges API (new nullable columns, idempotent migration).

ha-relevant: yes
This commit is contained in:
Pouzor
2026-07-05 14:11:01 +02:00
parent 40ec26ab7e
commit 485d2f2b04
19 changed files with 469 additions and 9 deletions
@@ -125,6 +125,41 @@ describe('EdgeModal', () => {
expect(onSubmit.mock.calls[0][0].path_style).toBe('smooth')
})
// ── Line style + width ────────────────────────────────────────────────────
it('defaults line style to the edge type preset (ethernet → solid)', () => {
const onSubmit = vi.fn()
render(<EdgeModal open onClose={vi.fn()} onSubmit={onSubmit} />)
fireEvent.click(screen.getByRole('button', { name: 'Connect' }))
expect(onSubmit.mock.calls[0][0].line_style).toBe('solid')
expect(onSubmit.mock.calls[0][0].width_mult).toBe(1)
})
it('follows the type default (wifi → dashed) until overridden', () => {
const onSubmit = vi.fn()
render(<EdgeModal open onClose={vi.fn()} onSubmit={onSubmit} initial={{ type: 'wifi' }} />)
fireEvent.click(screen.getByRole('button', { name: 'Connect' }))
expect(onSubmit.mock.calls[0][0].line_style).toBe('dashed')
})
it('picking a line style + width sends line_style/width_mult', () => {
const onSubmit = vi.fn()
render(<EdgeModal open onClose={vi.fn()} onSubmit={onSubmit} />)
fireEvent.click(screen.getByRole('button', { name: 'Line style dotted' }))
fireEvent.change(screen.getByRole('slider', { name: 'Line width multiplier' }), { target: { value: '4' } })
fireEvent.click(screen.getByRole('button', { name: 'Connect' }))
expect(onSubmit.mock.calls[0][0].line_style).toBe('dotted')
expect(onSubmit.mock.calls[0][0].width_mult).toBe(4)
})
it('pre-fills line style + width from initial prop', () => {
const onSubmit = vi.fn()
render(<EdgeModal open onClose={vi.fn()} onSubmit={onSubmit} initial={{ line_style: 'dashed', width_mult: 3 }} />)
fireEvent.click(screen.getByRole('button', { name: 'Connect' }))
expect(onSubmit.mock.calls[0][0].line_style).toBe('dashed')
expect(onSubmit.mock.calls[0][0].width_mult).toBe(3)
})
// ── Animation select ──────────────────────────────────────────────────────
it('animation defaults to None — animated omitted from payload', () => {