feat: implement Story 2.1 — canvas workspace with @xyflow/react and unified graph model

Replace the placeholder diagram editor with a professional Studio layout featuring
an interactive @xyflow/react canvas, unified graph data model with bidirectional
converters, Zustand state management, and oklch design tokens. Includes 25 unit
tests for converters and store, with all code review fixes applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-24 02:07:59 +00:00
parent 098f4968be
commit 5033109656
17 changed files with 1922 additions and 96 deletions

163
pnpm-lock.yaml generated
View File

@@ -439,6 +439,9 @@ importers:
'@turbostarter/ui-web':
specifier: workspace:*
version: link:../../packages/ui/web
'@xyflow/react':
specifier: 12.10.1
version: 12.10.1(@types/react@19.2.7)(immer@10.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
accept-language:
specifier: 3.0.20
version: 3.0.20
@@ -530,6 +533,9 @@ importers:
'@turbostarter/tsconfig':
specifier: workspace:*
version: link:../../tooling/typescript
'@turbostarter/vitest-config':
specifier: workspace:*
version: link:../../tooling/vitest
'@types/node':
specifier: catalog:node22
version: 22.16.0
@@ -551,6 +557,9 @@ importers:
typescript:
specifier: 'catalog:'
version: 5.9.3
vitest:
specifier: 'catalog:'
version: 4.0.14(@opentelemetry/api@1.9.0)(@types/node@22.16.0)(@vitest/ui@4.0.14)(jiti@2.6.1)(jsdom@26.0.0)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0)
packages/ai:
dependencies:
@@ -8232,6 +8241,9 @@ packages:
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-ease@3.0.2':
resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
@@ -8244,6 +8256,9 @@ packages:
'@types/d3-scale@4.0.9':
resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
'@types/d3-shape@3.1.7':
resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
@@ -8253,6 +8268,12 @@ packages:
'@types/d3-timer@3.0.2':
resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -8716,6 +8737,15 @@ packages:
'@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
'@xyflow/react@12.10.1':
resolution: {integrity: sha512-5eSWtIK/+rkldOuFbOOz44CRgQRjtS9v5nufk77DV+XBnfCGL9HAQ8PG00o2ZYKqkEU/Ak6wrKC95Tu+2zuK3Q==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@xyflow/system@0.0.75':
resolution: {integrity: sha512-iXs+AGFLi8w/VlAoc/iSxk+CxfT6o64Uw/k0CKASOPqjqz6E0rb5jFZgJtXGZCpfQI6OQpu5EnumP5fGxQheaQ==}
JSONStream@1.3.5:
resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==}
hasBin: true
@@ -9333,6 +9363,9 @@ packages:
class-variance-authority@0.7.1:
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
clean-stack@2.2.0:
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
engines: {node: '>=6'}
@@ -15710,6 +15743,21 @@ packages:
zod@4.1.13:
resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': 19.2.7
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zustand@5.0.8:
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
engines: {node: '>=12.20.0'}
@@ -23432,6 +23480,10 @@ snapshots:
'@types/d3-color@3.1.3': {}
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-ease@3.0.2': {}
'@types/d3-interpolate@3.0.4':
@@ -23444,6 +23496,8 @@ snapshots:
dependencies:
'@types/d3-time': 3.0.4
'@types/d3-selection@3.0.11': {}
'@types/d3-shape@3.1.7':
dependencies:
'@types/d3-path': 3.1.1
@@ -23452,6 +23506,15 @@ snapshots:
'@types/d3-timer@3.0.2': {}
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-zoom@3.0.8':
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -23864,6 +23927,14 @@ snapshots:
optionalDependencies:
vite: 5.4.21(@types/node@22.16.0)(lightningcss@1.30.2)(terser@5.43.1)
'@vitest/mocker@4.0.14(vite@7.3.0(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 4.0.14
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.0(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0)
'@vitest/mocker@4.0.14(vite@7.3.0(@types/node@24.0.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0))':
dependencies:
'@vitest/spy': 4.0.14
@@ -23917,7 +23988,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vitest: 4.0.14(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(@vitest/ui@4.0.14)(jiti@2.6.1)(jsdom@26.0.0)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0)
vitest: 4.0.14(@opentelemetry/api@1.9.0)(@types/node@22.16.0)(@vitest/ui@4.0.14)(jiti@2.6.1)(jsdom@26.0.0)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0)
'@vitest/utils@2.1.9':
dependencies:
@@ -24016,6 +24087,29 @@ snapshots:
'@xtuc/long@4.2.2': {}
'@xyflow/react@12.10.1(@types/react@19.2.7)(immer@10.1.3)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@xyflow/system': 0.0.75
classcat: 5.0.5
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
zustand: 4.5.7(@types/react@19.2.7)(immer@10.1.3)(react@19.1.0)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.75':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
JSONStream@1.3.5:
dependencies:
jsonparse: 1.3.1
@@ -24752,6 +24846,8 @@ snapshots:
dependencies:
clsx: 2.1.1
classcat@5.0.5: {}
clean-stack@2.2.0: {}
cli-cursor@2.1.0:
@@ -32444,6 +32540,23 @@ snapshots:
lightningcss: 1.30.2
terser: 5.43.1
vite@7.3.0(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.44.2
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.16.0
fsevents: 2.3.3
jiti: 2.6.1
lightningcss: 1.30.2
terser: 5.43.1
tsx: 4.19.2
yaml: 2.8.0
vite@7.3.0(@types/node@24.0.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0):
dependencies:
esbuild: 0.27.2
@@ -32498,6 +32611,46 @@ snapshots:
- supports-color
- terser
vitest@4.0.14(@opentelemetry/api@1.9.0)(@types/node@22.16.0)(@vitest/ui@4.0.14)(jiti@2.6.1)(jsdom@26.0.0)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0):
dependencies:
'@vitest/expect': 4.0.14
'@vitest/mocker': 4.0.14(vite@7.3.0(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0))
'@vitest/pretty-format': 4.0.14
'@vitest/runner': 4.0.14
'@vitest/snapshot': 4.0.14
'@vitest/spy': 4.0.14
'@vitest/utils': 4.0.14
es-module-lexer: 1.7.0
expect-type: 1.2.2
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
picomatch: 4.0.3
std-env: 3.10.0
tinybench: 2.9.0
tinyexec: 0.3.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
vite: 7.3.0(@types/node@22.16.0)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/node': 22.16.0
'@vitest/ui': 4.0.14(vitest@4.0.14)
jsdom: 26.0.0
transitivePeerDependencies:
- jiti
- less
- lightningcss
- msw
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- yaml
vitest@4.0.14(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(@vitest/ui@4.0.14)(jiti@2.6.1)(jsdom@26.0.0)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.19.2)(yaml@2.8.0):
dependencies:
'@vitest/expect': 4.0.14
@@ -32867,6 +33020,14 @@ snapshots:
zod@4.1.13: {}
zustand@4.5.7(@types/react@19.2.7)(immer@10.1.3)(react@19.1.0):
dependencies:
use-sync-external-store: 1.6.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.7
immer: 10.1.3
react: 19.1.0
zustand@5.0.8(@types/react@19.2.7)(immer@10.1.3)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0)):
optionalDependencies:
'@types/react': 19.2.7