OpenSpec baseline specs, VitePress docs, Windows build support

This commit is contained in:
Anton A S 2026-02-28 11:38:27 +03:00
parent cf454a855e
commit d0e3c014f7
74 changed files with 3907 additions and 4 deletions

4
.gitignore vendored
View file

@ -28,3 +28,7 @@ public/canvaskit.wasm
test-results/
# Git worktrees for parallel agent work
.worktrees/
# VitePress
docs/.vitepress/dist
docs/.vitepress/cache

View file

@ -54,7 +54,6 @@ bun run dev
Requires [Rust](https://rustup.rs/), the Tauri CLI, and platform-specific prerequisites ([Tauri v2 guide](https://v2.tauri.app/start/prerequisites/)).
```sh
cargo install tauri-cli --version "^2"
bun run tauri dev # Dev mode with hot reload
bun run tauri build # Production build
bun run tauri build --target universal-apple-darwin # macOS universal
@ -62,6 +61,36 @@ bun run tauri build --target universal-apple-darwin # macOS universal
Cross-compilation to other platforms requires their respective toolchains or CI (e.g. GitHub Actions).
### Platform Prerequisites
#### macOS
Install Xcode Command Line Tools:
```sh
xcode-select --install
```
#### Windows
1. Install [Rust](https://rustup.rs/) — make sure the default toolchain is `stable-msvc`:
```sh
rustup default stable-msvc
```
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with the "Desktop development with C++" workload (MSVC compiler + Windows SDK)
3. WebView2 is pre-installed on Windows 10 (1803+) and Windows 11. If missing, download from [Microsoft](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
#### Linux
Install system dependencies (Debian/Ubuntu):
```sh
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
```
For other distros, see the [Tauri v2 prerequisites](https://v2.tauri.app/start/prerequisites/).
## Project Structure
```

247
bun.lock
View file

@ -34,10 +34,47 @@
"unplugin-icons": "^23.0.1",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.0.4",
"vitepress": "^1.6.4",
},
},
},
"packages": {
"@algolia/abtesting": ["@algolia/abtesting@1.15.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-2yuIC48rUuHGhU1U5qJ9kJHaxYpJ0jpDHJVI5ekOxSMYXlH4+HP+pA31G820lsAznfmu2nzDV7n5RO44zIY1zw=="],
"@algolia/autocomplete-core": ["@algolia/autocomplete-core@1.17.7", "", { "dependencies": { "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", "@algolia/autocomplete-shared": "1.17.7" } }, "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q=="],
"@algolia/autocomplete-plugin-algolia-insights": ["@algolia/autocomplete-plugin-algolia-insights@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A=="],
"@algolia/autocomplete-preset-algolia": ["@algolia/autocomplete-preset-algolia@1.17.7", "", { "dependencies": { "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA=="],
"@algolia/autocomplete-shared": ["@algolia/autocomplete-shared@1.17.7", "", { "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" } }, "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg=="],
"@algolia/client-abtesting": ["@algolia/client-abtesting@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h6M7HzPin+45/l09q0r2dYmocSSt2MMGOOk5c4O5K/bBBlEwf1BKfN6z+iX4b8WXcQQhf7rgQwC52kBZJt/ZZw=="],
"@algolia/client-analytics": ["@algolia/client-analytics@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-048T9/Z8OeLmTk8h76QUqaNFp7Rq2VgS2Zm6Y2tNMYGQ1uNuzePY/udB5l5krlXll7ZGflyCjFvRiOtlPZpE9g=="],
"@algolia/client-common": ["@algolia/client-common@5.49.1", "", {}, "sha512-vp5/a9ikqvf3mn9QvHN8PRekn8hW34aV9eX+O0J5mKPZXeA6Pd5OQEh2ZWf7gJY6yyfTlLp5LMFzQUAU+Fpqpg=="],
"@algolia/client-insights": ["@algolia/client-insights@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-B6N7PgkvYrul3bntTz/l6uXnhQ2bvP+M7NqTcayh681tSqPaA5cJCUBp/vrP7vpPRpej4Eeyx2qz5p0tE/2N2g=="],
"@algolia/client-personalization": ["@algolia/client-personalization@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-v+4DN+lkYfBd01Hbnb9ZrCHe7l+mvihyx218INRX/kaCXROIWUDIT1cs3urQxfE7kXBFnLsqYeOflQALv/gA5w=="],
"@algolia/client-query-suggestions": ["@algolia/client-query-suggestions@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Un11cab6ZCv0W+Jiak8UktGIqoa4+gSNgEZNfG8m8eTsXGqwIEr370H3Rqwj87zeNSlFpH2BslMXJ/cLNS1qtg=="],
"@algolia/client-search": ["@algolia/client-search@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-Nt9hri7nbOo0RipAsGjIssHkpLMHHN/P7QqENywAq5TLsoYDzUyJGny8FEiD/9KJUxtGH8blGpMedilI6kK3rA=="],
"@algolia/ingestion": ["@algolia/ingestion@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-b5hUXwDqje0Y4CpU6VL481DXgPgxpTD5sYMnfQTHKgUispGnaCLCm2/T9WbJo1YNUbX3iHtYDArp804eD6CmRQ=="],
"@algolia/monitoring": ["@algolia/monitoring@1.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-bvrXwZ0WsL3rN6Q4m4QqxsXFCo6WAew7sAdrpMQMK4Efn4/W920r9ptOuckejOSSvyLr9pAWgC5rsHhR2FYuYw=="],
"@algolia/recommend": ["@algolia/recommend@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-h2yz3AGeGkQwNgbLmoe3bxYs8fac4An1CprKTypYyTU/k3Q+9FbIvJ8aS1DoBKaTjSRZVoyQS7SZQio6GaHbZw=="],
"@algolia/requester-browser-xhr": ["@algolia/requester-browser-xhr@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-2UPyRuUR/qpqSqH8mxFV5uBZWEpxhGPHLlx9Xf6OVxr79XO2ctzZQAhsmTZ6X22x+N8MBWpB9UEky7YU2HGFgA=="],
"@algolia/requester-fetch": ["@algolia/requester-fetch@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-N+xlE4lN+wpuT+4vhNEwPVlrfN+DWAZmSX9SYhbz986Oq8AMsqdntOqUyiOXVxYsQtfLwmiej24vbvJGYv1Qtw=="],
"@algolia/requester-node-http": ["@algolia/requester-node-http@5.49.1", "", { "dependencies": { "@algolia/client-common": "5.49.1" } }, "sha512-zA5bkUOB5PPtTr182DJmajCiizHp0rCJQ0Chf96zNFvkdESKYlDeYA3tQ7r2oyHbu/8DiohAQ5PZ85edctzbXA=="],
"@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
@ -48,6 +85,12 @@
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@docsearch/css": ["@docsearch/css@3.8.2", "", {}, "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ=="],
"@docsearch/js": ["@docsearch/js@3.8.2", "", { "dependencies": { "@docsearch/react": "3.8.2", "preact": "^10.0.0" } }, "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ=="],
"@docsearch/react": ["@docsearch/react@3.8.2", "", { "dependencies": { "@algolia/autocomplete-core": "1.17.7", "@algolia/autocomplete-preset-algolia": "1.17.7", "@docsearch/css": "3.8.2", "algoliasearch": "^5.14.2" }, "peerDependencies": { "@types/react": ">= 16.8.0 < 19.0.0", "react": ">= 16.8.0 < 19.0.0", "react-dom": ">= 16.8.0 < 19.0.0", "search-insights": ">= 1 < 3" }, "optionalPeers": ["@types/react", "react", "react-dom", "search-insights"] }, "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
@ -110,6 +153,8 @@
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.94", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-P54R7gqZs3t1GZQmzW2WkXIkGb9RPCq77iCb7IDU94IIsVegzuSQg52hSTjhvDKRXGUDc1HdwOBIuqg3iLIluw=="],
"@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.71", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-rNoDFbq1fAYiEexBvrw613/xiUOPEu5MKVV/X8lI64AgdTzLQUUemr9f9fplxUMPoxCBP2rWzlhOEeTHk/Sf0Q=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
@ -258,6 +303,22 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="],
"@shikijs/core": ["@shikijs/core@2.5.0", "", { "dependencies": { "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.4" } }, "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^3.1.0" } }, "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw=="],
"@shikijs/langs": ["@shikijs/langs@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w=="],
"@shikijs/themes": ["@shikijs/themes@2.5.0", "", { "dependencies": { "@shikijs/types": "2.5.0" } }, "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw=="],
"@shikijs/transformers": ["@shikijs/transformers@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/types": "2.5.0" } }, "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg=="],
"@shikijs/types": ["@shikijs/types@2.5.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="],
@ -330,8 +391,20 @@
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20260228.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20260228.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20260228.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20260228.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20260228.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20260228.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20260228.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20260228.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-kEUE54Shr4VfD1aVlxBkT9EVRkdSklINPxcUdFDwWQYh+uNRfgM/KmGUapbhCnoaat8ALRv30X6r6Eu3WiEhGg=="],
@ -350,6 +423,8 @@
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20260228.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9Wq1d0ZbrJcF6NoODw4+SRNdcZtKMmm2w32Wlo+r6ZIziBF5HFWvJcwGXNE0pbFpX6H/Gs8dhLaDKZU2dLoWOA=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.29", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw=="],
@ -360,6 +435,12 @@
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
"@vue/reactivity": ["@vue/reactivity@3.5.29", "", { "dependencies": { "@vue/shared": "3.5.29" } }, "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.29", "", { "dependencies": { "@vue/reactivity": "3.5.29", "@vue/shared": "3.5.29" } }, "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg=="],
@ -372,6 +453,8 @@
"@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="],
"@vueuse/integrations": ["@vueuse/integrations@12.8.2", "", { "dependencies": { "@vueuse/core": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" }, "peerDependencies": { "async-validator": "^4", "axios": "^1", "change-case": "^5", "drauu": "^0.4", "focus-trap": "^7", "fuse.js": "^7", "idb-keyval": "^6", "jwt-decode": "^4", "nprogress": "^0.2", "qrcode": "^1.5", "sortablejs": "^1", "universal-cookie": "^7" }, "optionalPeers": ["async-validator", "axios", "change-case", "drauu", "focus-trap", "fuse.js", "idb-keyval", "jwt-decode", "nprogress", "qrcode", "sortablejs", "universal-cookie"] }, "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g=="],
"@vueuse/metadata": ["@vueuse/metadata@14.2.1", "", {}, "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw=="],
"@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="],
@ -380,22 +463,42 @@
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"algoliasearch": ["algoliasearch@5.49.1", "", { "dependencies": { "@algolia/abtesting": "1.15.1", "@algolia/client-abtesting": "5.49.1", "@algolia/client-analytics": "5.49.1", "@algolia/client-common": "5.49.1", "@algolia/client-insights": "5.49.1", "@algolia/client-personalization": "5.49.1", "@algolia/client-query-suggestions": "5.49.1", "@algolia/client-search": "5.49.1", "@algolia/ingestion": "1.49.1", "@algolia/monitoring": "1.49.1", "@algolia/recommend": "5.49.1", "@algolia/requester-browser-xhr": "5.49.1", "@algolia/requester-fetch": "5.49.1", "@algolia/requester-node-http": "5.49.1" } }, "sha512-X3Pp2aRQhg4xUC6PQtkubn5NpRKuUPQ9FPDQlx36SmpFwwH2N0/tw4c+NXV3nw3PsgeUs+BuWGP0gjz3TvENLQ=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"canvaskit-wasm": ["canvaskit-wasm@0.40.0", "", { "dependencies": { "@webgpu/types": "0.1.21" } }, "sha512-Od2o+ZmoEw9PBdN/yCGvzfu0WVqlufBPEWNG452wY7E9aT8RBE+ChpZF526doOlg7zumO4iCS+RAeht4P0Gbpw=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"emoji-regex-xs": ["emoji-regex-xs@1.0.0", "", {}, "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="],
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
@ -410,12 +513,24 @@
"fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
"focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"fzstd": ["fzstd@0.1.1", "", {}, "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
@ -446,6 +561,24 @@
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"mark.js": ["mark.js@8.11.1", "", {}, "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"minisearch": ["minisearch@7.2.0", "", {}, "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"mlly": ["mlly@1.8.0", "", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
@ -454,6 +587,8 @@
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"oniguruma-to-es": ["oniguruma-to-es@3.1.1", "", { "dependencies": { "emoji-regex-xs": "^1.0.0", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ=="],
"oxfmt": ["oxfmt@0.35.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.35.0", "@oxfmt/binding-android-arm64": "0.35.0", "@oxfmt/binding-darwin-arm64": "0.35.0", "@oxfmt/binding-darwin-x64": "0.35.0", "@oxfmt/binding-freebsd-x64": "0.35.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.35.0", "@oxfmt/binding-linux-arm-musleabihf": "0.35.0", "@oxfmt/binding-linux-arm64-gnu": "0.35.0", "@oxfmt/binding-linux-arm64-musl": "0.35.0", "@oxfmt/binding-linux-ppc64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-gnu": "0.35.0", "@oxfmt/binding-linux-riscv64-musl": "0.35.0", "@oxfmt/binding-linux-s390x-gnu": "0.35.0", "@oxfmt/binding-linux-x64-gnu": "0.35.0", "@oxfmt/binding-linux-x64-musl": "0.35.0", "@oxfmt/binding-openharmony-arm64": "0.35.0", "@oxfmt/binding-win32-arm64-msvc": "0.35.0", "@oxfmt/binding-win32-ia32-msvc": "0.35.0", "@oxfmt/binding-win32-x64-msvc": "0.35.0" }, "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q=="],
"oxlint": ["oxlint@1.50.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.50.0", "@oxlint/binding-android-arm64": "1.50.0", "@oxlint/binding-darwin-arm64": "1.50.0", "@oxlint/binding-darwin-x64": "1.50.0", "@oxlint/binding-freebsd-x64": "1.50.0", "@oxlint/binding-linux-arm-gnueabihf": "1.50.0", "@oxlint/binding-linux-arm-musleabihf": "1.50.0", "@oxlint/binding-linux-arm64-gnu": "1.50.0", "@oxlint/binding-linux-arm64-musl": "1.50.0", "@oxlint/binding-linux-ppc64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-gnu": "1.50.0", "@oxlint/binding-linux-riscv64-musl": "1.50.0", "@oxlint/binding-linux-s390x-gnu": "1.50.0", "@oxlint/binding-linux-x64-gnu": "1.50.0", "@oxlint/binding-linux-x64-musl": "1.50.0", "@oxlint/binding-openharmony-arm64": "1.50.0", "@oxlint/binding-win32-arm64-msvc": "1.50.0", "@oxlint/binding-win32-ia32-msvc": "1.50.0", "@oxlint/binding-win32-x64-msvc": "1.50.0" }, "peerDependencies": { "oxlint-tsgolint": ">=0.14.1" }, "optionalPeers": ["oxlint-tsgolint"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ=="],
@ -462,6 +597,8 @@
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
@ -474,16 +611,42 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"preact": ["preact@10.28.4", "", {}, "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="],
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"reka-ui": ["reka-ui@2.8.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-8lTKcJhmG+D3UyJxhBnNnW/720sLzm0pbA9AC1MWazmJ5YchJAyTSl+O00xP/kxBmEN0fw5JqWVHguiFmsGjzA=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="],
"search-insights": ["search-insights@2.17.3", "", {}, "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ=="],
"shiki": ["shiki@2.5.0", "", { "dependencies": { "@shikijs/core": "2.5.0", "@shikijs/engine-javascript": "2.5.0", "@shikijs/engine-oniguruma": "2.5.0", "@shikijs/langs": "2.5.0", "@shikijs/themes": "2.5.0", "@shikijs/types": "2.5.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
"tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="],
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
@ -494,6 +657,8 @@
"tinypool": ["tinypool@2.1.0", "", {}, "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
@ -502,6 +667,16 @@
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
"unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="],
"unplugin-icons": ["unplugin-icons@23.0.1", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/utils": "^3.1.0", "local-pkg": "^1.1.2", "obug": "^2.1.1", "unplugin": "^2.3.11" }, "peerDependencies": { "@svgr/core": ">=7.0.0", "@svgx/core": "^1.0.1", "@vue/compiler-sfc": "^3.0.2", "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@svgr/core", "@svgx/core", "@vue/compiler-sfc", "svelte"] }, "sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ=="],
@ -510,8 +685,14 @@
"unplugin-vue-components": ["unplugin-vue-components@31.0.0", "", { "dependencies": { "chokidar": "^5.0.0", "local-pkg": "^1.1.2", "magic-string": "^0.30.21", "mlly": "^1.8.0", "obug": "^2.1.1", "picomatch": "^4.0.3", "tinyglobby": "^0.2.15", "unplugin": "^2.3.11", "unplugin-utils": "^0.3.1" }, "peerDependencies": { "@nuxt/kit": "^3.2.2 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@nuxt/kit"] }, "sha512-4ULwfTZTLuWJ7+S9P7TrcStYLsSRkk6vy2jt/WTfgUEUb0nW9//xxmrfhyHUEVpZ2UKRRwfRb8Yy15PDbVZf+Q=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
"vitepress": ["vitepress@1.6.4", "", { "dependencies": { "@docsearch/css": "3.8.2", "@docsearch/js": "3.8.2", "@iconify-json/simple-icons": "^1.2.21", "@shikijs/core": "^2.1.0", "@shikijs/transformers": "^2.1.0", "@shikijs/types": "^2.1.0", "@types/markdown-it": "^14.1.2", "@vitejs/plugin-vue": "^5.2.1", "@vue/devtools-api": "^7.7.0", "@vue/shared": "^3.5.13", "@vueuse/core": "^12.4.0", "@vueuse/integrations": "^12.4.0", "focus-trap": "^7.6.4", "mark.js": "8.11.1", "minisearch": "^7.1.1", "shiki": "^2.1.0", "vite": "^5.4.14", "vue": "^3.5.13" }, "peerDependencies": { "markdown-it-mathjax3": "^4", "postcss": "^8" }, "optionalPeers": ["markdown-it-mathjax3", "postcss"], "bin": { "vitepress": "bin/vitepress.js" } }, "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg=="],
"vue": ["vue@3.5.29", "", { "dependencies": { "@vue/compiler-dom": "3.5.29", "@vue/compiler-sfc": "3.5.29", "@vue/runtime-dom": "3.5.29", "@vue/server-renderer": "3.5.29", "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA=="],
"vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
@ -520,6 +701,8 @@
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@ -532,10 +715,74 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@vueuse/integrations/@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
"@vueuse/integrations/@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"vitepress/@vitejs/plugin-vue": ["@vitejs/plugin-vue@5.2.4", "", { "peerDependencies": { "vite": "^5.0.0 || ^6.0.0", "vue": "^3.2.25" } }, "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA=="],
"vitepress/@vueuse/core": ["@vueuse/core@12.8.2", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "12.8.2", "@vueuse/shared": "12.8.2", "vue": "^3.5.13" } }, "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ=="],
"vitepress/vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
"@vueuse/integrations/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"vitepress/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@12.8.2", "", {}, "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A=="],
"vitepress/@vueuse/core/@vueuse/shared": ["@vueuse/shared@12.8.2", "", { "dependencies": { "vue": "^3.5.13" } }, "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w=="],
"vitepress/vite/esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
"vitepress/vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
"vitepress/vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
"vitepress/vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
"vitepress/vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
"vitepress/vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
"vitepress/vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
"vitepress/vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
"vitepress/vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
"vitepress/vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
"vitepress/vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
"vitepress/vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
"vitepress/vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
"vitepress/vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
"vitepress/vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
"vitepress/vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
"vitepress/vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
"vitepress/vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
"vitepress/vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
"vitepress/vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
"vitepress/vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
"vitepress/vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
"vitepress/vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
"vitepress/vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
}
}

71
docs/.vitepress/config.ts Normal file
View file

@ -0,0 +1,71 @@
import { defineConfig } from 'vitepress'
export default defineConfig({
title: 'OpenPencil',
description: 'Open-source, AI-native design editor. Figma alternative built from scratch with full .fig file compatibility.',
cleanUrls: true,
lastUpdated: true,
appearance: 'dark',
head: [
['link', { rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:title', content: 'OpenPencil' }],
['meta', { property: 'og:description', content: 'Open-source, AI-native design editor' }],
],
themeConfig: {
search: {
provider: 'local',
},
nav: [
{ text: 'Guide', link: '/guide/getting-started' },
{ text: 'Reference', link: '/reference/keyboard-shortcuts' },
{ text: 'Development', link: '/development/contributing' },
],
sidebar: [
{
text: 'Guide',
items: [
{ text: 'Getting Started', link: '/guide/getting-started' },
{ text: 'Features', link: '/guide/features' },
{ text: 'Architecture', link: '/guide/architecture' },
{ text: 'Tech Stack', link: '/guide/tech-stack' },
],
},
{
text: 'Reference',
items: [
{ text: 'Keyboard Shortcuts', link: '/reference/keyboard-shortcuts' },
{ text: 'Node Types', link: '/reference/node-types' },
{ text: 'MCP Tools', link: '/reference/mcp-tools' },
{ text: 'Scene Graph', link: '/reference/scene-graph' },
{ text: 'File Format', link: '/reference/file-format' },
],
},
{
text: 'Development',
items: [
{ text: 'Contributing', link: '/development/contributing' },
{ text: 'Testing', link: '/development/testing' },
{ text: 'OpenSpec Workflow', link: '/development/openspec' },
{ text: 'Roadmap', link: '/development/roadmap' },
],
},
],
socialLinks: [
{ icon: 'github', link: 'https://github.com/dannote/open-pencil' },
],
editLink: {
pattern: 'https://github.com/dannote/open-pencil/edit/main/docs/:path',
},
footer: {
message: 'Released under the MIT License.',
},
},
})

View file

@ -0,0 +1,81 @@
# Contributing
## Project Structure
```
src/
components/ Vue SFCs (canvas, panels, toolbar, color picker)
properties/ Property panel sections (Appearance, Fill, Stroke, etc.)
composables/ Canvas input, keyboard shortcuts, rendering hooks
stores/ Editor state (Vue reactivity)
engine/ Scene graph, renderer, layout, clipboard, undo, vector, snap
kiwi/ Figma file format (Kiwi codec, .fig import)
kiwi-schema/ Vendored from evanw/kiwi
types.ts Shared types
constants.ts UI colors, defaults, thresholds
desktop/ Tauri v2 (Rust + config)
tests/
e2e/ Playwright visual regression
engine/ Unit tests (bun:test)
docs/ VitePress documentation site
openspec/
specs/ Capability specifications (source of truth)
changes/ Active and archived changes
```
## Development Setup
```sh
bun install
bun run dev # Editor at localhost:1420
bun run docs:dev # Docs at localhost:5173
```
## Code Style
### Tooling
| Tool | Command | Purpose |
|------|---------|---------|
| oxlint | `bun run lint` | Linting (Rust-based, fast) |
| oxfmt | `bun run format` | Code formatting |
| tsgo | `bun run typecheck` | Type checking (Go-based TypeScript checker) |
Run all checks:
```sh
bun run check
```
### Conventions
- **File names** — kebab-case (`scene-graph.ts`, `use-canvas-input.ts`)
- **Components** — PascalCase Vue SFCs (`EditorCanvas.vue`, `ScrubInput.vue`)
- **Constants** — SCREAMING_SNAKE_CASE
- **Functions/variables** — camelCase
- **Types/interfaces** — PascalCase
## Making Changes
1. Check existing [openspec specs](/development/openspec) for the capability you're modifying
2. Create an openspec change if adding new behavior: `openspec new change "my-change"`
3. Implement the change
4. Run `bun run check` and `bun run test`
5. Submit a pull request
## Key Files
| File | Purpose |
|------|---------|
| `src/engine/scene-graph.ts` | Core data structure for all design nodes |
| `src/engine/renderer.ts` | CanvasKit rendering pipeline |
| `src/engine/layout.ts` | Yoga layout adapter |
| `src/engine/undo.ts` | Undo/redo manager |
| `src/engine/clipboard.ts` | Figma-compatible clipboard |
| `src/engine/vector.ts` | Vector network model |
| `src/stores/editor.ts` | Global editor state |
| `src/composables/use-canvas.ts` | Canvas rendering composable |
| `src/composables/use-canvas-input.ts` | Mouse/touch input handling |
| `src/composables/use-keyboard.ts` | Keyboard shortcut handling |
| `src/kiwi/codec.ts` | Kiwi binary encoder/decoder |
| `src/kiwi/fig-import.ts` | .fig file import logic |

View file

@ -0,0 +1,107 @@
# OpenSpec Workflow
OpenPencil uses [OpenSpec](https://github.com/Fission-AI/OpenSpec) for spec-driven development. Specifications are the source of truth for what the system does.
## Structure
```
openspec/
├── specs/ # Source of truth: how the system works now
│ ├── scene-graph/
│ │ └── spec.md
│ ├── canvas-rendering/
│ │ └── spec.md
│ ├── auto-layout/
│ │ └── spec.md
│ └── ... # 19 capability specs total
├── changes/ # Proposed changes (one directory per change)
│ └── archive/ # Completed changes
└── config.yaml # Optional configuration
```
## Current Specs
| Capability | Description |
|-----------|-------------|
| scene-graph | Flat Map storage, CRUD, hit testing |
| canvas-rendering | CanvasKit WASM rendering pipeline |
| canvas-navigation | Pan, zoom, hand tool |
| selection-manipulation | Click/marquee select, move, resize, rotate |
| undo-redo | Inverse-command pattern |
| text-editing | Text tool, Paragraph API, font loading |
| pen-tool | Vector network model, bezier curves |
| auto-layout | Yoga WASM flexbox, Shift+A toggle |
| figma-clipboard | Bidirectional fig-kiwi clipboard |
| fig-import | .fig file import pipeline |
| kiwi-codec | Kiwi binary codec, sparse field IDs |
| editor-ui | Vue 3 panels, toolbar, color picker |
| snap-guides | Edge/center snapping, rotation-aware |
| rulers | Canvas rulers, selection highlight |
| group-ungroup | ⌘G/⇧⌘G, position-based sort |
| desktop-app | Tauri v2, macOS menu bar |
| testing | Playwright E2E, bun:test unit |
| scrub-input | Drag-to-scrub numeric inputs |
| tooling | Vite 7, oxlint, oxfmt, tsgo, VitePress |
## Workflow
### 1. Propose a Change
```
/opsx:propose add-dark-mode
```
Creates `openspec/changes/add-dark-mode/` with:
- `proposal.md` — why and what changes
- `design.md` — technical approach
- `specs/` — delta specifications (ADDED/MODIFIED/REMOVED requirements)
- `tasks.md` — implementation checklist
### 2. Implement
```
/opsx:apply
```
Execute tasks from tasks.md, checking off items as they're completed.
### 3. Archive
```
/opsx:archive
```
- Merges delta specs into `openspec/specs/` (the baseline)
- Moves the change to `openspec/changes/archive/`
## Spec Format
Each spec file follows a consistent structure:
```markdown
# capability-name Specification
## Purpose
One-line description of what this capability does.
## Requirements
### Requirement: Name
Description using SHALL/MUST for normative requirements.
#### Scenario: Name
- **WHEN** condition
- **THEN** expected outcome
```
Every requirement has at least one scenario. Scenarios are potential test cases.
## CLI Commands
```sh
openspec list # List active changes
openspec show <name> # Show change details
openspec status --change <name> # Artifact status
openspec archive <name> # Archive completed change
openspec update # Regenerate skills/prompts
```

View file

@ -0,0 +1,88 @@
# Roadmap
## Phases
### Phase 1: Core Engine ✅
SceneGraph, Skia rendering, basic shapes, selection, zoom/pan, undo/redo.
**Delivered:**
- Scene graph with flat Map storage and parent-child tree
- CanvasKit WASM rendering for all shape types
- Click/shift/marquee selection with resize handles and rotation
- Zoom/pan with keyboard shortcuts and trackpad gestures
- Undo/redo wired into all operations
- Snap guides with edge and center snapping
### Phase 2: Editor UI + Layout ✅
Properties panel, layers panel, toolbar, Yoga layout integration, text editing.
**Delivered:**
- Vue 3 + Reka UI panels (properties, layers, toolbar)
- Properties panel split into sections (Appearance, Fill, Stroke, Typography, Layout, Position)
- ScrubInput component for all numeric inputs
- Color picker (HSV, hex, opacity)
- Layers panel with tree view, drag reorder, visibility toggle
- Auto-layout with Yoga WASM (direction, gap, padding, justify, align)
- Inline text editing with CanvasKit Paragraph API
- System font loading via Local Font Access API
- Canvas rulers with selection highlight
### Phase 3: File Format + Import ✅
.fig import, Kiwi codec, clipboard compatibility.
**Delivered:**
- .fig file import via Kiwi binary codec
- Vendored kiwi-schema with ESM + sparse field ID patches
- Figma-compatible clipboard (bidirectional fig-kiwi binary)
- Pen tool with vector network model
- vectorNetworkBlob binary encode/decode
- Group/ungroup (⌘G/⇧⌘G)
- Tauri v2 desktop app with native macOS menu bar
### Phase 4: Components + Variables 🔲
Components, instances, overrides, variants, variables, collections, modes/themes.
**Planned:**
- Component creation and instantiation
- Override propagation
- Variant switching
- Variable collections with modes (light/dark)
- Variable binding to node properties
### Phase 5: AI Integration 🔲
MCP server, design guidelines, screenshot verification loop.
**Planned:**
- Port MCP server from figma-use (117 tools)
- Design guidelines system
- AI-driven design workflow via MCP
- Screenshot verification loop
### Phase 6: Polish + Distribution 🔲
Prototyping, comments, desktop distribution, documentation, public launch.
**Planned:**
- Prototyping (frame connections, transitions)
- Comments (pin, threads, resolve)
- Cross-platform Tauri builds (macOS, Windows, Linux)
- PWA support
- Documentation site (VitePress) ← this is being built now
- Performance optimization (Lighthouse > 90)
- Full Figma compatibility test suite
## Timeline
| Phase | Estimated Duration | Status |
|-------|-------------------|--------|
| Phase 1: Core Engine | 3 months | ✅ Complete |
| Phase 2: Editor UI + Layout | 3 months | ✅ Complete |
| Phase 3: File Format + Import | 2 months | ✅ Complete |
| Phase 4: Components + Variables | 2 months | 🔲 Planned |
| Phase 5: AI Integration | 2 months | 🔲 Planned |
| Phase 6: Polish + Distribution | 2 months | 🔲 Planned |

View file

@ -0,0 +1,85 @@
# Testing
## Overview
| Type | Framework | Command | Location |
|------|-----------|---------|----------|
| E2E visual regression | Playwright | `bun run test` | `tests/e2e/` |
| Figma CDP reference | Playwright | `bun run test:figma` | `tests/figma/` |
| Unit tests | bun:test | `bun run test:unit` | `tests/engine/` |
## E2E Visual Regression
Playwright creates shapes on the canvas and compares screenshots against baseline PNGs.
```sh
bun run test # Run tests, compare against baselines
bun run test:update # Regenerate baseline screenshots
```
### How It Works
1. Tests load the editor in a headless browser
2. The editor signals readiness via a `data-ready` HTML attribute
3. Tests create shapes via the editor's API
4. Screenshots are taken and compared against baselines using `toMatchSnapshot`
5. Page is reused across test cases for speed (~2s total)
### No-Chrome Test Mode
The editor supports a test mode that hides UI chrome (toolbar, panels) for clean screenshot capture. Activated via URL parameter.
## Figma CDP Reference Tests
A separate Playwright project connects to Figma via Chrome DevTools Protocol to capture reference screenshots for pixel-perfect comparison.
```sh
bun run figma:debug # Launch Figma with debugging port
bun run test:figma # Connect to Figma, capture references
```
Requires Figma desktop app running with `--remote-debugging-port=9222`.
## Unit Tests
Engine unit tests use bun:test and target < 50ms execution:
```sh
bun run test:unit
```
Tests cover:
- Scene graph CRUD operations
- Parent-child relationships
- Z-ordering
- Hit testing
### Writing Unit Tests
```typescript
import { describe, expect, it } from 'bun:test'
import { SceneGraph } from '../../src/engine/scene-graph'
describe('SceneGraph', () => {
it('creates and retrieves a node', () => {
const sg = new SceneGraph()
const node = sg.createNode('RECTANGLE', sg.root, { name: 'Test' })
expect(sg.getNode(node.guid)).toBeDefined()
})
})
```
## Test Helpers
| File | Purpose |
|------|---------|
| `tests/helpers/canvas.ts` | Canvas setup and interaction utilities |
| `tests/helpers/figma.ts` | Figma CDP connection helpers |
## Performance Targets
| Metric | Target |
|--------|--------|
| E2E suite total | < 3s |
| Unit test suite total | < 50ms |
| Screenshot comparison | toMatchSnapshot (pixel-level) |

View file

@ -0,0 +1,88 @@
# Architecture
## System Overview
```
┌──────────────────────────────────────────────────────────────────┐
│ Tauri v2 Shell │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Editor (Web) │ │
│ │ │ │
│ │ Vue 3 UI Skia CanvasKit (WASM, 7MB) │ │
│ │ - Toolbar - Vector rendering │ │
│ │ - Panels - Text shaping │ │
│ │ - Properties - Image processing │ │
│ │ - Layers - Effects (blur, shadow) │ │
│ │ - Color Picker - Export (PNG, SVG, PDF) │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ Core Engine (TS) │ │ │
│ │ │ SceneGraph ─── Layout (Yoga) ─── Selection │ │ │
│ │ │ │ │ │ │ │
│ │ │ Undo/Redo ─── Constraints ─── Hit Testing │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ File Format Layer │ │ │
│ │ │ .openpencil (Kiwi) ── .fig import ── .svg export │ │ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ MCP Server (117 tools) Collaboration (planned, CRDT) │
└──────────────────────────────────────────────────────────────────┘
```
## Editor Layout
The UI follows Figma's UI3 layout — toolbar at the bottom, navigation on the left, properties on the right:
- **Navigation panel (left)** — Layers tree, asset library (planned), page tabs (planned)
- **Canvas (center)** — Infinite canvas with CanvasKit rendering, zoom/pan
- **Properties panel (right)** — Context-sensitive sections: Appearance, Fill, Stroke, Typography, Layout, Position
- **Toolbar (bottom)** — Tool selection: Select, Frame, Rectangle, Ellipse, Line, Text, Pen, Hand
## Components
### Rendering (CanvasKit WASM)
The same rendering engine as Figma. CanvasKit provides GPU-accelerated 2D drawing with:
- Vector shapes (rect, ellipse, path, line, star, polygon)
- Text shaping via Paragraph API
- Effects (shadows, blurs, blend modes)
- Export (PNG, SVG, PDF)
The 7MB WASM binary loads at startup and creates a GPU surface on the HTML canvas.
### Scene Graph
Flat `Map<string, Node>` keyed by GUID strings. Tree structure via `parentIndex` references. Provides O(1) lookup, efficient traversal, hit testing, and rectangular area queries for marquee selection.
See [Scene Graph Reference](/reference/scene-graph) for internals.
### Layout Engine (Yoga WASM)
Meta's Yoga provides CSS flexbox layout computation. A thin adapter maps Figma property names to Yoga equivalents:
| Figma Property | Yoga Equivalent |
|---|---|
| `stackMode: HORIZONTAL` | `flexDirection: row` |
| `stackMode: VERTICAL` | `flexDirection: column` |
| `stackSpacing` | `gap` |
| `stackPadding` | `padding` |
| `stackJustify` | `justifyContent` |
| `stackChildPrimaryGrow` | `flexGrow` |
### File Format (Kiwi Binary)
Reuses Figma's proven Kiwi binary codec with 194 message/enum/struct definitions. The `.fig` import pipeline: parse header → Zstd decompress → Kiwi decode → NodeChange[] → scene graph.
See [File Format Reference](/reference/file-format) for details.
### Undo/Redo
Inverse-command pattern. Before applying any change, affected fields are snapshotted. The snapshot becomes the inverse operation. Batching groups rapid changes (like drag) into single undo entries.
### Clipboard
Figma-compatible bidirectional clipboard. Encodes/decodes fig-kiwi binary using native browser copy/paste events (synchronous, not async Clipboard API).

73
docs/guide/features.md Normal file
View file

@ -0,0 +1,73 @@
# Features
## Figma .fig File Import
Open native Figma files directly. The Kiwi binary codec decodes the full 194-definition schema including NodeChange messages with ~390 fields. The pipeline: parse header → decompress Zstd → decode Kiwi schema → extract NodeChange[] → build scene graph.
## Figma Clipboard
Copy/paste between OpenPencil and Figma. When you copy in Figma, OpenPencil decodes the fig-kiwi binary from the clipboard. When you copy in OpenPencil, it encodes fig-kiwi binary that Figma can read. Also works between OpenPencil instances.
## Vector Networks
The pen tool uses Figma's vector network model — not simple paths. Click to place corner points, click+drag for bezier curves with tangent handles. Supports open and closed paths. Vector data uses the same `vectorNetworkBlob` binary format as Figma.
## Auto-Layout
Yoga WASM provides CSS flexbox layout. Frames support:
- **Direction** — horizontal, vertical, wrap
- **Gap** — spacing between children
- **Padding** — uniform or per-side
- **Justify** — start, center, end, space-between
- **Align** — start, center, end, stretch
- **Child sizing** — fixed, fill, hug
Shift+A toggles auto-layout on a frame or wraps selected nodes.
## Inline Text Editing
Text nodes use CanvasKit's Paragraph API for rendering with proper text shaping and line breaking. Double-click to edit inline with a textarea overlay. Supports font families, weights, sizes, and line height. System fonts are available via the Local Font Access API.
## Undo/Redo
Every operation is undoable. The system uses an inverse-command pattern — before applying any change, it snapshots affected fields. The snapshot becomes the inverse. ⌘Z undoes, ⇧⌘Z redoes.
## Snap Guides
Edge and center snapping with red guide lines when nodes align. Rotation-aware — snap calculations use actual visual bounds of rotated nodes. Coordinates are computed in absolute canvas space.
## Canvas Rulers
Rulers at the top and left edges show coordinate scales. When you select a node, rulers highlight its position with a translucent band and show coordinate badges at the start/end points.
## Color Picker
HSV color selection with hue slider, alpha slider, hex input, and opacity control. Connected to fill and stroke sections in the properties panel.
## Layers Panel
Tree view of the document hierarchy using Reka UI Tree component. Expand/collapse frames, drag to reorder (changes z-order), toggle visibility per node.
## Properties Panel
Context-sensitive panel with sections:
- **Appearance** — size, position, rotation, opacity, corner radius, visibility
- **Fill** — color with hex input, opacity, visibility toggle
- **Stroke** — color, weight, opacity
- **Typography** — font family, weight, size, alignment
- **Layout** — auto-layout controls when enabled
- **Position** — alignment buttons, rotation, flip
## Group/Ungroup
⌘G groups selected nodes. ⇧⌘G ungroups. Nodes are sorted by visual position when grouping to preserve reading order.
## Desktop App
Tauri v2 shell (~5MB vs Electron's ~100MB). Native macOS menu bar with File/Edit/View/Window menus. Menu events are wired to the Vue frontend. Developer Tools accessible via ⌘⌥I.
## ScrubInput
All numeric inputs in the properties panel use a drag-to-scrub interaction — drag horizontally to adjust the value, or click to type directly. Supports suffix display (°, px, %).

View file

@ -0,0 +1,82 @@
# Getting Started
## Prerequisites
- [Bun](https://bun.sh/) (package manager and runtime)
- [Rust](https://rustup.rs/) (for desktop app only)
## Installation
```sh
git clone https://github.com/dannote/open-pencil.git
cd open-pencil
bun install
```
## Development Server
```sh
bun run dev
```
Opens the editor at `http://localhost:1420`.
## Available Scripts
| Command | Description |
|---------|-------------|
| `bun run dev` | Dev server with HMR |
| `bun run build` | Production build |
| `bun run check` | Lint (oxlint) + typecheck (tsgo) |
| `bun run test` | E2E visual regression (Playwright) |
| `bun run test:update` | Regenerate screenshot baselines |
| `bun run test:unit` | Unit tests (bun:test) |
| `bun run docs:dev` | Documentation dev server |
| `bun run docs:build` | Build documentation site |
## Desktop App (Tauri)
The desktop app requires Rust and platform-specific prerequisites.
### macOS
```sh
xcode-select --install
cargo install tauri-cli --version "^2"
bun run tauri dev
```
### Windows
1. Install [Rust](https://rustup.rs/) with `stable-msvc` toolchain:
```sh
rustup default stable-msvc
```
2. Install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) with "Desktop development with C++" workload
3. WebView2 is pre-installed on Windows 10 (1803+) and Windows 11
4. Run:
```sh
bun run tauri dev
```
### Linux
Install system dependencies (Debian/Ubuntu):
```sh
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \
libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev
```
Then:
```sh
bun run tauri dev
```
### Build for Distribution
```sh
bun run tauri build # Current platform
bun run tauri build --target universal-apple-darwin # macOS universal
```

59
docs/guide/tech-stack.md Normal file
View file

@ -0,0 +1,59 @@
# Tech Stack
## Core Technologies
| Layer | Technology | Why |
|-------|-----------|-----|
| **Rendering** | Skia CanvasKit WASM | Same engine as Figma — proven performance, GPU-accelerated, pixel-perfect |
| **UI Framework** | Vue 3 + VueUse | Reactive composition API, excellent TypeScript support |
| **Components** | Reka UI | Headless, accessible UI primitives (tree, slider, etc.) |
| **Styling** | Tailwind CSS 4 | Utility-first, fast iteration, dark theme |
| **Layout** | Yoga WASM | CSS flexbox engine from Meta, battle-tested in React Native |
| **File Format** | Kiwi binary + Zstd | Figma's own format — compact, fast parsing, .fig compatible |
| **Color** | culori | Color space conversions (HSV, RGB, hex) |
| **Desktop** | Tauri v2 | ~5MB native app (vs Electron's ~100MB), Rust backend |
| **Build** | Vite 7 | Fast HMR, native ES modules |
| **Testing** | Playwright + bun:test | Visual regression (E2E) + fast unit tests |
| **Linting** | oxlint | Rust-based, orders of magnitude faster than ESLint |
| **Formatting** | oxfmt | Rust-based formatter |
| **Type Checking** | typescript-go (tsgo) | Native Go implementation of TypeScript type checker |
## Key Dependencies
```json
{
"canvaskit-wasm": "^0.40.0",
"vue": "^3.5.29",
"yoga-layout": "^3.2.1",
"reka-ui": "^2.8.2",
"tailwindcss": "^4.2.1",
"culori": "^4.0.2",
"fzstd": "^0.1.1",
"fflate": "^0.8.2"
}
```
## Why Not...
### Why not SVG rendering (like Penpot)?
SVG is slow for complex documents. Every node is a DOM element — 10,000 nodes means 10,000 DOM nodes with layout, paint, and compositing overhead. CanvasKit draws everything to a single GPU surface.
### Why not Electron (like Figma desktop)?
Tauri v2 uses the system webview (~5MB) instead of bundling Chromium (~100MB). The Rust backend provides native performance for file I/O and system integration.
### Why not React (like the original plan)?
The project migrated from React to Vue 3 early in development. Vue's reactivity system and VueUse composables proved more ergonomic for the editor's state management needs.
### Why not custom layout engine?
Yoga is maintained by Meta, battle-tested across billions of React Native devices, and implements the CSS flexbox spec. Building a custom engine would be months of work to reach the same correctness level.
## Planned Technologies
| Technology | Purpose | Phase |
|-----------|---------|-------|
| Yjs (CRDT) | Real-time collaboration | Phase 6 |
| MCP Server | AI agent integration (117 tools) | Phase 5 |

36
docs/index.md Normal file
View file

@ -0,0 +1,36 @@
---
layout: home
description: Open-source, AI-native design editor. Figma alternative with .fig import, auto-layout, and 117 MCP tools.
hero:
name: OpenPencil
text: AI-Native Design Editor
tagline: Open-source Figma alternative. Self-host it, extend it, talk to it.
actions:
- theme: brand
text: Get Started
link: /guide/getting-started
- theme: alt
text: View on GitHub
link: https://github.com/dannote/open-pencil
features:
- icon: 📂
title: Figma .fig Import
details: Open native Figma files directly via Kiwi binary codec with full NodeChange support.
- icon: 📋
title: Figma Clipboard
details: Copy/paste between OpenPencil and Figma. Bidirectional fig-kiwi binary encoding.
- icon: ✏️
title: Pen Tool & Vectors
details: Vector network model with bezier curves, open/closed paths, and tangent handles.
- icon: 📐
title: Auto-Layout
details: Yoga WASM flexbox engine. Direction, gap, padding, justify, align, child sizing.
- icon: 🤖
title: AI-Native (MCP)
details: 117 MCP tools expose the entire editor API to AI agents. Design with conversation.
- icon: 🖥️
title: Desktop App
details: Tauri v2 shell — ~5MB native app with macOS menu bar, not 100MB Electron.
---

4
docs/public/favicon.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#3B82F6"/>
<path d="M9 23 L12 9 L16 19 L20 9 L23 23" stroke="white" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 262 B

View file

@ -0,0 +1,70 @@
# File Format
## .fig File Structure
```
┌─────────────────────────────────┐
│ Magic header: "fig-kiwi" (8B) │
│ Version (4B uint32 LE) │
│ Schema length (4B uint32 LE) │
│ Compressed Kiwi schema │
│ Message length (4B uint32 LE) │
│ Compressed Kiwi message │ ← NodeChange[] (entire document)
│ Blob data │ ← Images, vector networks, fonts
└─────────────────────────────────┘
```
## Import Pipeline
```
.fig file → Parse header → Decompress Zstd → Decode Kiwi schema
→ Decode Message → NodeChange[] → Build SceneGraph
→ Resolve blob refs → Render on canvas
```
## Kiwi Binary Codec
The codec handles Figma's 194-definition Kiwi schema with NodeChange as the central type (~390 fields). Key components:
- **kiwi-schema** — vendored from evanw/kiwi, patched for ESM and sparse field IDs
- **codec.ts** — encode/decode Messages using the Kiwi schema
- **protocol.ts** — wire format parsing and message type detection
- **schema.ts** — 194 message/enum/struct definitions
### Sparse Field IDs
Figma's schema uses non-contiguous field IDs (e.g., 1, 2, 5, 10 with gaps). The vendored kiwi-schema parser is patched to handle this correctly.
### Compression
.fig files use Zstd compression for both the schema and message payloads. The `fzstd` library handles decompression. For clipboard encoding, `fflate` provides lightweight compression.
## .openpencil Format (Planned)
The native format will use the same Kiwi binary encoding:
- Same NodeChange-based structure (proven at Figma scale)
- Kiwi binary schema (compact, fast parsing)
- Zstd compression
- Superset of .fig — additional fields for OpenPencil features
- Full roundtrip: save → load → identical state
## Supported Formats
| Format | Import | Export |
|--------|--------|--------|
| .fig (Figma) | ✅ | — |
| .openpencil | Planned | Planned |
| .svg | Planned | Planned |
| .png | Planned | Planned |
| .pdf | — | Planned |
## Clipboard Format
Copy/paste uses the same fig-kiwi binary encoding:
1. **Copy** — encode selected NodeChange[] to Kiwi binary, compress, write to clipboard as `application/x-figma-design` MIME type
2. **Paste** — read clipboard, decompress, decode Kiwi binary, create nodes in scene graph
3. **Synchronous** — encoding happens in the copy event handler (not async Clipboard API) to ensure browser compatibility
This enables bidirectional clipboard between OpenPencil and Figma.

View file

@ -0,0 +1,111 @@
# Keyboard Shortcuts
Full Figma-compatible shortcut map. ✅ = implemented.
::: tip Symbol Legend
⌘ = Cmd (Mac) / Ctrl (Win/Linux) · ⇧ = Shift · ⌥ = Alt/Option · ⌃ = Ctrl · ⌫ = Backspace
:::
## Tools
| Key | Tool | Status |
|-----|------|--------|
| V | Move/Select | ✅ |
| K | Scale | |
| H | Hand | ✅ |
| F | Frame | ✅ |
| S | Section / Slice | |
| R | Rectangle | ✅ |
| O | Ellipse | ✅ |
| L | Line | ✅ |
| ⇧L | Arrow | |
| P | Pen | ✅ |
| ⇧P | Pencil | |
| T | Text | ✅ |
| C | Comment | |
| I | Eyedropper | |
## File
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘N | New Window | |
| ⌘O | Open File | ✅ |
| ⌘W | Close Tab | |
| ⌘S | Save | |
| ⇧⌘E | Export… | |
## Edit
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘Z | Undo | ✅ |
| ⇧⌘Z | Redo | ✅ |
| ⌘X | Cut | ✅ |
| ⌘C | Copy | ✅ |
| ⌘V | Paste | ✅ |
| ⇧⌘V | Paste Over Selection | |
| ⌘D | Duplicate | ✅ |
| ⌫ | Delete | ✅ |
| ⌘A | Select All | ✅ |
| ⇧⌘A | Select Inverse | |
| ⌥⌘C | Copy Properties | |
| ⌥⌘V | Paste Properties | |
## View
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘' | Pixel Grid | |
| ⌃G | Layout Guides | |
| ⇧R | Rulers | |
| ⌘\ | Show/Hide UI | |
| ⌘= | Zoom In | ✅ |
| ⌘- | Zoom Out | ✅ |
| ⌘0 | Zoom to 100% | ✅ |
| ⌘1 | Zoom to Fit | |
| ⌘2 | Zoom to Selection | |
## Object
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌥⌘G | Frame Selection | |
| ⌘G | Group Selection | ✅ |
| ⇧⌘G | Ungroup | ✅ |
| ⇧A | Add Auto Layout | ✅ |
| ⌥⌘K | Create Component | |
| ⌥⌘B | Detach Instance | |
| ⌘] | Bring Forward | |
| ⌥⌘] | Bring to Front | |
| ⌘[ | Send Backward | |
| ⌥⌘[ | Send to Back | |
| ⇧H | Flip Horizontal | |
| ⇧V | Flip Vertical | |
| ⌘E | Flatten | |
## Text
| Shortcut | Action | Status |
|----------|--------|--------|
| ⌘B | Bold | |
| ⌘I | Italic | |
| ⌘U | Underline | |
| ⇧⌘X | Strikethrough | |
## Canvas Interaction
| Input | Action | Status |
|-------|--------|--------|
| Click | Select node | ✅ |
| Shift+Click | Add/remove from selection | ✅ |
| Alt+Drag | Duplicate and move | ✅ |
| Shift+Drag (draw) | Constrain to square/circle | ✅ |
| Shift+Drag (resize) | Maintain aspect ratio | ✅ |
| Shift+Drag (rotate) | Snap to 15° | ✅ |
| Middle mouse drag | Pan | ✅ |
| Scroll | Pan | ✅ |
| Ctrl+Scroll / Pinch | Zoom | ✅ |
| Double-click text | Edit text inline | ✅ |
| Drag onto frame | Reparent into frame | ✅ |
| Escape | Deselect / Cancel | ✅ |

203
docs/reference/mcp-tools.md Normal file
View file

@ -0,0 +1,203 @@
# MCP Tools
The editor exposes its entire API through MCP (Model Context Protocol). Every operation available to a human is available to AI. 117 tools across 15 categories.
## AI Workflow
1. **Read** — `node_tree`, `find`, `analyze_*`
2. **Create/Modify** — `create_*`, `set_*`, `node_*`, `render` (JSX)
3. **Verify** — `export_screenshot` → inspect the image
4. **Iterate** — `diff_create` to see changes, fix issues
5. **Export** — `export_jsx`, `export_storybook` for developer handoff
## Create (14 tools)
| Tool | Description |
|------|-------------|
| `create_frame` | Create a frame |
| `create_rect` | Create a rectangle |
| `create_ellipse` | Create an ellipse |
| `create_text` | Create a text node |
| `create_line` | Create a line |
| `create_polygon` | Create a polygon |
| `create_star` | Create a star |
| `create_vector` | Create a vector path |
| `create_component` | Create a component |
| `create_instance` | Create a component instance |
| `create_section` | Create a section |
| `create_page` | Create a page |
| `create_slice` | Create a slice |
| `create_icon` | Create an icon from Iconify |
## Set / Modify (18 tools)
| Tool | Description |
|------|-------------|
| `set_fill` | Set fill color (hex or variable ref) |
| `set_stroke` | Set stroke color and weight |
| `set_stroke-align` | Set stroke alignment |
| `set_radius` | Set corner radius |
| `set_opacity` | Set opacity |
| `set_rotation` | Set rotation angle |
| `set_blend` | Set blend mode |
| `set_visible` | Set visibility |
| `set_locked` | Set locked state |
| `set_text` | Set text content |
| `set_text-resize` | Set text auto resize mode |
| `set_font` | Set font properties |
| `set_font-range` | Set font for a text range |
| `set_effect` | Set effect (shadow, blur) |
| `set_image` | Set image fill |
| `set_layout` | Set auto-layout properties |
| `set_constraints` | Set resize constraints |
| `set_minmax` | Set min/max width and height |
## Node Operations (15 tools)
| Tool | Description |
|------|-------------|
| `node_get` | Get node properties |
| `node_tree` | Get node tree with properties |
| `node_children` | Get child nodes |
| `node_ancestors` | Get ancestor chain to root |
| `node_bounds` | Get bounding box |
| `node_bindings` | Get variable bindings |
| `node_move` | Move a node |
| `node_resize` | Resize a node |
| `node_rename` | Rename a node |
| `node_clone` | Clone node(s) |
| `node_delete` | Delete node(s) |
| `node_set-parent` | Reparent a node |
| `node_replace-with` | Replace node with another |
| `node_to-component` | Convert frame to component |
| `find` | Find nodes by name or type |
## Variables & Collections (11 tools)
| Tool | Description |
|------|-------------|
| `variable_list` | List all variables |
| `variable_get` | Get variable by ID |
| `variable_find` | Find variables by name |
| `variable_create` | Create a variable |
| `variable_set` | Set variable value for mode |
| `variable_bind` | Bind variable to node property |
| `variable_delete` | Delete a variable |
| `collection_list` | List variable collections |
| `collection_get` | Get collection by ID |
| `collection_create` | Create a collection |
| `collection_delete` | Delete a collection |
## Components (4 tools)
| Tool | Description |
|------|-------------|
| `component_add-prop` | Add property to component |
| `component_edit-prop` | Edit component property |
| `component_delete-prop` | Delete component property |
| `component_combine` | Combine into component set (variants) |
## Styles (4 tools)
| Tool | Description |
|------|-------------|
| `style_list` | List local styles |
| `style_create-paint` | Create paint/color style |
| `style_create-text` | Create text style |
| `style_create-effect` | Create effect style |
## Boolean Operations (4 tools)
| Tool | Description |
|------|-------------|
| `boolean_union` | Union shapes |
| `boolean_subtract` | Subtract shapes |
| `boolean_intersect` | Intersect shapes |
| `boolean_exclude` | Exclude shapes |
## Vector Paths (5 tools)
| Tool | Description |
|------|-------------|
| `path_get` | Get vector path data |
| `path_set` | Set vector path data |
| `path_move` | Move all path points |
| `path_scale` | Scale path from center |
| `path_flip` | Flip path H/V |
## Groups (3 tools)
| Tool | Description |
|------|-------------|
| `group_create` | Group nodes |
| `group_ungroup` | Ungroup nodes |
| `group_flatten` | Flatten to single vector |
## Analyze (5 tools)
| Tool | Description |
|------|-------------|
| `analyze_colors` | Color palette usage |
| `analyze_typography` | Typography usage |
| `analyze_spacing` | Spacing values |
| `analyze_clusters` | Repeated patterns |
| `analyze_snapshot` | Accessibility tree |
## Export (6 tools)
| Tool | Description |
|------|-------------|
| `export_node` | Export node as image |
| `export_screenshot` | Screenshot viewport |
| `export_selection` | Export selection as image |
| `export_jsx` | Export as JSX component |
| `export_storybook` | Export as Storybook stories |
| `export_fonts` | List fonts used |
## Diff (5 tools)
| Tool | Description |
|------|-------------|
| `diff_create` | Diff between two nodes |
| `diff_show` | Show diff vs provided props |
| `diff_apply` | Apply a diff patch |
| `diff_visual` | Visual diff as PNG |
| `diff_jsx` | JSX diff between nodes |
## Pages & Viewport (7 tools)
| Tool | Description |
|------|-------------|
| `page_list` | List all pages |
| `page_current` | Get current page |
| `page_set` | Switch to page |
| `page_bounds` | Bounding box of all objects |
| `viewport_get` | Get viewport position/zoom |
| `viewport_set` | Set viewport position/zoom |
| `viewport_zoom-to-fit` | Zoom to fit nodes |
## Selection & Connectors (6 tools)
| Tool | Description |
|------|-------------|
| `selection_get` | Get selected nodes |
| `selection_set` | Set selection |
| `connector_create` | Create connector |
| `connector_get` | Get connector details |
| `connector_set` | Update connector |
| `connector_list` | List connectors |
## Meta (10 tools)
| Tool | Description |
|------|-------------|
| `status` | Check connection |
| `lint` | Lint design |
| `import` | Import SVG |
| `render` | Render JSX to nodes |
| `get_components` | Get all components |
| `get_styles` | Get local styles |
| `get_pages` | Get all pages |
| `font_list` | List available fonts |
| `comment_watch` | Wait for new comment |
| `eval` | Execute JS in editor |

View file

@ -0,0 +1,152 @@
# Node Types
The scene graph uses Figma's Kiwi schema with 28 node types. Each node is identified by a GUID (`sessionID:localID`) and has a parent reference via `parentIndex`.
## Type Table
| Type | ID | Description |
|------|----|-------------|
| DOCUMENT | 1 | Root node, one per file |
| CANVAS | 2 | Page |
| GROUP | 3 | Group container |
| FRAME | 4 | Primary container (artboard), supports auto-layout |
| BOOLEAN_OPERATION | 5 | Union/subtract/intersect/exclude result |
| VECTOR | 6 | Freeform vector path |
| STAR | 7 | Star shape |
| LINE | 8 | Line |
| ELLIPSE | 9 | Ellipse/circle, supports arc data |
| RECTANGLE | 10 | Rectangle |
| REGULAR_POLYGON | 11 | Regular polygon (3–12 sides) |
| ROUNDED_RECTANGLE | 12 | Rectangle with smooth corners |
| TEXT | 13 | Text with rich formatting |
| SLICE | 14 | Export region |
| SYMBOL | 15 | Component (main) |
| INSTANCE | 16 | Component instance |
| STICKY | 17 | FigJam sticky note |
| SHAPE_WITH_TEXT | 18 | FigJam shape |
| CONNECTOR | 19 | Connector line between nodes |
| CODE_BLOCK | 20 | FigJam code block |
| WIDGET | 21 | Plugin widget |
| STAMP | 22 | FigJam stamp |
| MEDIA | 23 | Video/GIF |
| HIGHLIGHT | 24 | FigJam highlight |
| SECTION | 25 | Canvas section (organizational) |
| SECTION_OVERLAY | 26 | Section overlay |
| WASHI_TAPE | 27 | FigJam washi tape |
| VARIABLE | 28 | Variable definition node |
## Node Hierarchy
```
Document
└── Canvas (page)
├── Frame
│ ├── Rectangle
│ ├── Text
│ └── Frame (nested)
│ ├── Ellipse
│ └── Instance (→ references Component)
├── Component
│ └── ...children
├── Section
│ └── Frame
├── Group
│ └── ...children
└── BooleanOperation
└── ...operand shapes
```
## Core Properties
Every node carries these fields (subset of NodeChange):
### Identity & Tree
- `guid` — unique identifier (`sessionID:localID`)
- `type` — node type enum
- `name` — display name
- `phase` — CREATED or REMOVED
- `parentIndex` — parent GUID + position string for z-ordering
### Transform
- `size` — width/height vector
- `transform` — 2×3 affine matrix
- `rotation` — degrees
### Appearance
- `fillPaints[]` — fill colors/gradients/images
- `strokePaints[]` — stroke colors
- `effects[]` — shadows, blurs
- `opacity` — 0–1
- `blendMode` — NORMAL, MULTIPLY, SCREEN, etc.
### Stroke
- `strokeWeight` — stroke thickness
- `strokeAlign` — inside / center / outside
- `strokeCap` — butt / round / square
- `strokeJoin` — miter / bevel / round
- `dashPattern[]` — dash/gap lengths
### Corners
- `cornerRadius` — uniform radius
- `cornerSmoothing` — squircle amount (0–1)
- Per-corner radii when `rectangleCornerRadiiIndependent` is true
### Visibility
- `visible` — show/hide
- `locked` — prevent editing
## Type-Specific Properties
### Text
`fontSize`, `fontName`, `lineHeight`, `letterSpacing`, `textAlignHorizontal`, `textAlignVertical`, `textAutoResize`, `textData` (characters, style overrides, baselines, glyphs)
### Vector
`vectorData` (vectorNetworkBlob, normalizedSize), `fillGeometry[]`, `strokeGeometry[]`, `handleMirroring`, `arcData`
### Layout (Frame)
`stackMode`, `stackSpacing`, `stackPadding`, `stackJustify`, `stackCounterAlign`, `stackPrimarySizing`, `stackCounterSizing`, `stackChildPrimaryGrow`, `stackChildAlignSelf`
### Component
`symbolData`, `componentKey`, `componentPropDefs[]`, `symbolDescription`
### Instance
`overriddenSymbolID`, `symbolData.symbolOverrides[]`, `componentPropRefs[]`, `componentPropAssignments[]`
## Paint
```
Paint {
type: SOLID | GRADIENT_LINEAR | GRADIENT_RADIAL |
GRADIENT_ANGULAR | GRADIENT_DIAMOND | IMAGE
color: {r, g, b, a} (0–1 floats)
opacity: 0–1
visible: boolean
blendMode: BlendMode
stops: ColorStop[] (for gradients)
transform: Matrix (for gradient/image positioning)
}
```
## Effect
```
Effect {
type: INNER_SHADOW | DROP_SHADOW | FOREGROUND_BLUR | BACKGROUND_BLUR
color: {r, g, b, a}
offset: {x, y}
radius: float
spread: float
visible: boolean
}
```

View file

@ -0,0 +1,75 @@
# Scene Graph
## In-Memory Representation
Nodes live in a flat `Map<string, Node>` keyed by GUID string. The tree structure is maintained via `parentIndex` references. This gives O(1) lookup by ID and efficient traversal.
```typescript
interface SceneGraph {
nodes: Map<string, Node>
root: string
getNode(id: string): Node
getChildren(id: string): Node[]
getParent(id: string): Node | null
createNode(type: NodeType, parent: string, props: Partial<NodeChange>): Node
updateNode(id: string, changes: Partial<NodeChange>): void
deleteNode(id: string): void
moveNode(id: string, newParent: string, position: string): void
findByType(type: NodeType): Node[]
findByName(pattern: string): Node[]
hitTest(point: Vector, canvas: string): Node | null
getNodesInRect(rect: Rect, canvas: string): Node[]
}
```
## Undo/Redo
The system uses Figma's **inverse command** pattern. Each undo entry contains the forward changes and their automatically-computed inverse:
| Operation | Forward | Inverse |
|-----------|---------|---------|
| Create node | `{guid, phase: CREATED, ...props}` | `{guid, phase: REMOVED}` |
| Delete node | `{guid, phase: REMOVED}` | `{guid, phase: CREATED, ...allProps}` |
| Change prop | `{guid, fill: "#F00"}` | `{guid, fill: "#00F"}` |
| Move node | `{guid, parentIndex: newParent}` | `{guid, parentIndex: oldParent}` |
Before applying any change, affected fields are snapshotted. The snapshot becomes the inverse.
**Batching** — operations like drag-to-move produce hundreds of position changes per second. These are debounced into a single undo entry. `beginBatch`/`commitBatch` wraps multi-step operations.
## Layout Engine (Yoga)
Figma's auto-layout properties map to Yoga flexbox:
| Figma Property | Yoga Equivalent |
|---|---|
| `stackMode: HORIZONTAL` | `flexDirection: row` |
| `stackMode: VERTICAL` | `flexDirection: column` |
| `stackSpacing` | `gap` |
| `stackPadding` | `padding` |
| `stackJustify: MIN/CENTER/MAX/SPACE_BETWEEN` | `justifyContent` |
| `stackCounterAlign` | `alignItems` |
| `stackPrimarySizing: FIXED/HUG/FILL` | width/height + flex-grow |
| `stackChildPrimaryGrow` | `flexGrow` |
| `stackChildAlignSelf` | `alignSelf` |
| `stackPositioning: ABSOLUTE` | `position: absolute` |
## Hit Testing
Given a point in canvas coordinates, the scene graph returns the topmost visible node at that position. The algorithm:
1. Traverse visible nodes in reverse z-order (top to bottom)
2. Transform the test point into each node's local coordinate system
3. Check if the point is within the node's bounds (including rotation)
4. Return the first match
For marquee selection, `getNodesInRect` returns all nodes whose bounds intersect the given rectangle.
## Coordinate System
Nodes store position and size relative to their parent. To get absolute (canvas) coordinates, walk up the parent chain applying transforms. The renderer uses this to draw nested frames with correct positioning.
Rotation is stored in degrees and applied as part of the 2×3 transform matrix. Snap guides and hit testing account for rotation when computing visual bounds.

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-28

View file

@ -0,0 +1,70 @@
## Context
The project uses Vite 7, Vue 3, and TypeScript. VitePress (Vue's official static site generator) fits naturally — it shares the Vite build pipeline and Vue component ecosystem. The existing documentation is:
- `PLAN.md` — 1565-line comprehensive design doc with architecture, tools, phases, keyboard shortcuts, technical deep dives
- `README.md` — project overview, features, tech stack, getting started, project structure
- `openspec/specs/` — 19 capability specs with requirements and scenarios
The editor app runs on port 1420. VitePress should use a different port for its dev server.
## Goals / Non-Goals
**Goals:**
- Browsable documentation site generated from Markdown
- Content restructured from existing PLAN.md, README, and openspec specs
- Sidebar navigation with logical grouping
- Dark theme matching editor aesthetic
- `docs:dev` / `docs:build` / `docs:preview` scripts
**Non-Goals:**
- Custom VitePress theme (use default theme)
- API documentation auto-generation from source code
- Deployment configuration (CI/CD, hosting)
- i18n / localization
- Search integration beyond VitePress built-in
## Decisions
### 1. VitePress in `docs/` directory (separate from app)
VitePress runs as an independent project in `docs/` with its own `.vitepress/config.ts`. It does NOT share the app's `vite.config.ts` — they are completely separate build pipelines.
**Why:** VitePress has its own Vite instance. Mixing with the app config would cause conflicts (different plugins, different entry points). Standard convention across all major VitePress projects.
### 2. Content structure: guide / reference / development
```
docs/
index.md — landing page (from README)
guide/
getting-started.md — installation, dev server, desktop app
features.md — feature overview with screenshots
architecture.md — system architecture diagram and explanation
tech-stack.md — tech choices and rationale
reference/
keyboard-shortcuts.md — full shortcut table from PLAN.md
node-types.md — 29 node types with properties
mcp-tools.md — 118 MCP tools reference
scene-graph.md — scene graph technical details
file-format.md — .fig/.openpencil format details
development/
contributing.md — how to contribute
testing.md — test setup and running
openspec.md — spec-driven development workflow
roadmap.md — phases and current progress
```
**Why:** Three-tier structure (guide → reference → development) is the standard pattern used by Vue, Vite, Pinia, and Nuxt documentation. Users discover via guides, look up via reference, and contribute via development docs.
### 3. VitePress as devDependency only
VitePress is added as a devDependency — it's a development tool, not shipped with the editor.
### 4. Port 5173 for docs dev server
VitePress defaults to 5173. The editor uses 1420. No conflict.
## Risks / Trade-offs
- **Content staleness** → Mitigation: docs pages are Markdown in-repo, reviewed alongside code changes. Openspec specs are the source of truth for capabilities.
- **Maintenance burden** → Mitigation: minimal custom configuration. Default VitePress theme. Content is restructured from existing artifacts, not written from scratch.

View file

@ -0,0 +1,29 @@
## Why
OpenPencil has extensive knowledge spread across PLAN.md (1565 lines), README.md, and 19 openspec specs — but no browsable documentation site. Contributors and users have to read raw Markdown in the repo. PLAN.md Phase 6 lists "documentation" as a deliverable. VitePress is a natural fit: same Vite ecosystem, Vue-native, generates static sites from Markdown, zero config out of the box.
## What Changes
- Add VitePress as dev dependency
- Create `docs/` directory with documentation site structure
- Content sourced from existing artifacts: PLAN.md sections, README.md, openspec specs
- Add `docs:dev` and `docs:build` scripts to package.json
- VitePress config with sidebar navigation, dark theme matching the editor
## Capabilities
### New Capabilities
- `vitepress-docs`: VitePress documentation site at `docs/` with content derived from PLAN.md, README, and openspec specs. Includes guide pages (getting started, architecture, tech stack), reference pages (keyboard shortcuts, node types, MCP tools), and development pages (contributing, testing, openspec workflow).
### Modified Capabilities
- `tooling`: Adding VitePress dependency and docs scripts to the build toolchain
## Impact
- `docs/` — new directory with VitePress config and Markdown pages
- `docs/.vitepress/config.ts` — VitePress configuration
- `package.json` — new `docs:dev`, `docs:build`, `docs:preview` scripts + vitepress devDependency
- `.gitignore` — add `docs/.vitepress/dist` and `docs/.vitepress/cache`
- No changes to the editor application itself

View file

@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Vite 7 build system
The project SHALL use Vite 7 as the build tool with dev server at port 1420 and HMR support. VitePress SHALL be installed as a devDependency for the documentation site. The `docs:dev`, `docs:build`, and `docs:preview` scripts SHALL be added to package.json.
#### Scenario: Dev server
- **WHEN** `bun run dev` is executed
- **THEN** Vite dev server starts at http://localhost:1420 with hot module replacement
#### Scenario: Docs scripts available
- **WHEN** `bun run docs:dev` is executed
- **THEN** VitePress dev server starts for the documentation site
#### Scenario: Docs build
- **WHEN** `bun run docs:build` is executed
- **THEN** VitePress builds the documentation site to `docs/.vitepress/dist/`
#### Scenario: Docs preview
- **WHEN** `bun run docs:preview` is executed
- **THEN** a static server previews the built documentation site

View file

@ -0,0 +1,69 @@
## ADDED Requirements
### Requirement: VitePress documentation site
The project SHALL have a VitePress documentation site in the `docs/` directory with its own `.vitepress/config.ts` configuration, independent from the app's Vite config.
#### Scenario: Docs dev server starts
- **WHEN** `bun run docs:dev` is executed
- **THEN** VitePress dev server starts and serves the documentation site
#### Scenario: Docs build succeeds
- **WHEN** `bun run docs:build` is executed
- **THEN** VitePress produces a static site in `docs/.vitepress/dist/`
### Requirement: Landing page
The docs site SHALL have an index.md landing page with project name, tagline, and quick navigation to guide and reference sections.
#### Scenario: Landing page loads
- **WHEN** user opens the docs site root URL
- **THEN** a landing page with "OpenPencil" title and feature highlights is displayed
### Requirement: Guide section
The docs site SHALL include a guide section with pages: Getting Started, Features, Architecture, Tech Stack.
#### Scenario: Getting started page
- **WHEN** user navigates to the Getting Started guide
- **THEN** installation instructions (`bun install`, `bun run dev`) and desktop app setup are displayed
#### Scenario: Architecture page
- **WHEN** user navigates to the Architecture guide
- **THEN** the system architecture diagram and component descriptions from PLAN.md are displayed
### Requirement: Reference section
The docs site SHALL include a reference section with pages: Keyboard Shortcuts, Node Types, MCP Tools, Scene Graph, File Format.
#### Scenario: Keyboard shortcuts page
- **WHEN** user navigates to Keyboard Shortcuts reference
- **THEN** the full shortcut table from PLAN.md is displayed with implementation status
#### Scenario: MCP tools page
- **WHEN** user navigates to MCP Tools reference
- **THEN** all 117 MCP tools are listed grouped by category
### Requirement: Development section
The docs site SHALL include a development section with pages: Contributing, Testing, OpenSpec Workflow, Roadmap.
#### Scenario: Roadmap page
- **WHEN** user navigates to the Roadmap development page
- **THEN** the 6 phases from PLAN.md are listed with current progress
### Requirement: Sidebar navigation
The VitePress config SHALL define a sidebar with logical grouping: Guide, Reference, Development.
#### Scenario: Sidebar groups
- **WHEN** user browses any documentation page
- **THEN** a sidebar shows three collapsible groups: Guide, Reference, Development
### Requirement: Dark theme
The docs site SHALL use dark appearance to match the editor's dark aesthetic.
#### Scenario: Dark mode
- **WHEN** user opens the documentation site
- **THEN** the site renders with VitePress dark theme
### Requirement: Build artifacts excluded from git
`docs/.vitepress/dist` and `docs/.vitepress/cache` SHALL be listed in `.gitignore`.
#### Scenario: Gitignore entries
- **WHEN** `bun run docs:build` creates output in `docs/.vitepress/dist/`
- **THEN** the output directory is not tracked by git

View file

@ -0,0 +1,35 @@
## 1. Setup VitePress
- [x] 1.1 Install vitepress as devDependency: `bun add -D vitepress`
- [x] 1.2 Add `docs:dev`, `docs:build`, `docs:preview` scripts to package.json
- [x] 1.3 Add `docs/.vitepress/dist` and `docs/.vitepress/cache` to .gitignore
- [x] 1.4 Create `docs/.vitepress/config.ts` with: site title/description, dark appearance, sidebar (Guide/Reference/Development groups), top nav, local search, cleanUrls, socialLinks (GitHub), editLink, lastUpdated
- [x] 1.5 Create `docs/public/` with favicon (or reuse existing logo if available)
## 2. Create documentation pages — Guide
- [x] 2.1 Create `docs/index.md` — VitePress home layout with hero (title, tagline, action buttons) and features grid
- [x] 2.2 Create `docs/guide/getting-started.md` — installation, dev server, desktop app setup per platform (from README)
- [x] 2.3 Create `docs/guide/features.md` — feature overview with descriptions (from README + PLAN.md tools section)
- [x] 2.4 Create `docs/guide/architecture.md` — system architecture as Mermaid diagram + component descriptions (from PLAN.md)
- [x] 2.5 Create `docs/guide/tech-stack.md` — technology choices and rationale table (from PLAN.md + README)
## 3. Create documentation pages — Reference
- [x] 3.1 Create `docs/reference/keyboard-shortcuts.md` — full shortcut tables with implementation status (from PLAN.md)
- [x] 3.2 Create `docs/reference/node-types.md` — 29 node types table + core properties (from PLAN.md Scene Graph)
- [x] 3.3 Create `docs/reference/mcp-tools.md` — 118 MCP tools in collapsible category groups (from PLAN.md AI section)
- [x] 3.4 Create `docs/reference/scene-graph.md` — scene graph internals: storage, undo/redo, layout engine (from PLAN.md)
- [x] 3.5 Create `docs/reference/file-format.md` — .fig import pipeline, .openpencil format, Kiwi codec (from PLAN.md)
## 4. Create documentation pages — Development
- [x] 4.1 Create `docs/development/contributing.md` — project structure, code style, tooling (oxlint/oxfmt/tsgo)
- [x] 4.2 Create `docs/development/testing.md` — E2E (Playwright), unit (bun:test), Figma CDP reference tests
- [x] 4.3 Create `docs/development/openspec.md` — spec-driven workflow: propose → apply → archive
- [x] 4.4 Create `docs/development/roadmap.md` — 6 phases from PLAN.md with current progress indicators
## 5. Verify
- [x] 5.1 Run `bun run docs:dev` and confirm the site loads with all pages and sidebar navigation
- [x] 5.2 Run `bun run docs:build` and confirm it produces output without errors

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-28

View file

@ -0,0 +1,70 @@
## Context
OpenPencil is an open-source Figma alternative with 92 commits of development history (2026-02-27 to 2026-02-28). The project has a detailed PLAN.md (1565 lines) covering architecture, tools, phases, and validation criteria — but no structured specs tracking. The openspec/ directory was just initialized and is empty.
The git history shows work spanning PLAN.md Phases 1-3 (core engine, editor UI, file format/import) with partial progress on later phases. We need to retroactively create baseline specs and assess documentation tooling.
## Goals / Non-Goals
**Goals:**
- Create baseline openspec specs for all 19 implemented capabilities identified from git history
- Document planned-but-unimplemented capabilities from PLAN.md as future specs with status markers
- Provide a VitePress readiness assessment
- Establish the openspec specs/ directory as the living source of truth going forward
**Non-Goals:**
- Implementing any new features or code changes
- Setting up VitePress or any documentation generation tooling
- Creating specs for capabilities from figma-use that haven't been ported yet
- Writing design system documentation or API docs
## Decisions
### 1. Spec organization: one file per capability (flat structure)
Each capability gets `openspec/specs/<capability-name>/spec.md`. No nesting by phase or module — capabilities are cross-cutting and may span multiple source directories.
**Why:** Matches openspec convention. Flat structure is easier to discover and prevents debates about categorization hierarchy.
### 2. Implemented vs planned: status field in spec header
Each spec includes a `status` field: `implemented`, `partial`, or `planned`. This lets us track what exists in code vs what's only in PLAN.md.
**Why:** We need to clearly distinguish "this works today" from "this is the vision". Avoids confusion when reading specs.
### 3. Git commit mapping: reference commits in spec scenarios
Each spec's scenarios reference the git commits that implemented them. This creates traceability from spec → code history.
**Why:** Makes it possible to understand when a capability was added and what changed. Critical for a retroactive documentation effort.
### 4. VitePress assessment: included as a standalone report, not a spec
The VitePress evaluation is a one-time assessment, not an ongoing capability. It goes into the change's design artifacts, not into openspec/specs/.
**Why:** A spec describes system behavior. "Do we have VitePress?" is an assessment question, not a capability.
## Risks / Trade-offs
- **Spec drift from code** → Mitigation: specs describe observable behavior and capabilities, not implementation details. They should stay accurate even as internals change.
- **Retroactive specs may miss edge cases** → Mitigation: focus on what git commits prove was implemented. Don't speculate about undocumented behavior.
- **Large number of specs to create at once** → Mitigation: each spec is focused and concise. Scenarios reference commits rather than rewriting commit messages.
## VitePress Assessment
**Finding: No VitePress artifacts exist in the project.**
Evidence:
- No `vitepress` dependency in package.json or bun.lock
- No `docs/` directory
- No `.vitepress/` configuration directory
- No documentation generation scripts in package.json
- No references to VitePress or documentation generation in PLAN.md, README.md, or any source files
- PLAN.md Phase 6 mentions "documentation" as a deliverable but does not specify VitePress or any specific documentation tooling
**Current documentation state:**
- `README.md` — project overview, features, tech stack, getting started, project structure (concise, well-written)
- `PLAN.md` — comprehensive 1565-line design document covering architecture, tools, phases, validation (serves as both design doc and roadmap)
- No API documentation, no user guides, no developer guides beyond README
**Recommendation:** VitePress would be a natural fit for this project when documentation becomes a priority (Phase 6). The content from PLAN.md could be restructured into VitePress pages. However, there is currently zero infrastructure for it — it would need to be built from scratch.

View file

@ -0,0 +1,46 @@
## Why
OpenPencil has 92 commits of implementation history and a comprehensive PLAN.md, but no structured specification tracking. The openspec/ directory is empty. To enable spec-driven development going forward, we need to retroactively document all completed work as baseline specs and capture the planned-but-not-yet-implemented capabilities from PLAN.md. This establishes the source of truth for what the system does today and what's next.
Additionally, we need to assess the current state of documentation tooling — specifically whether VitePress documentation generation has any existing artifacts or setup in the project.
## What Changes
- Create baseline openspec specs from the 92 git commits covering all implemented capabilities
- Document planned capabilities from PLAN.md phases that are not yet implemented
- Assess VitePress documentation generation readiness (no implementation — evaluation only)
## Capabilities
### New Capabilities
- `scene-graph`: Core scene graph with flat Map<GUID, Node> storage, parent-child tree via parentIndex, CRUD operations, hit testing, node types (FRAME, RECTANGLE, ELLIPSE, LINE, TEXT, VECTOR, STAR, POLYGON, GROUP, BOOLEAN_OPERATION)
- `canvas-rendering`: Skia CanvasKit WASM rendering — shapes, fills, strokes, text, effects, transforms, nested coordinate systems, selection outlines, snap guides, rulers
- `canvas-navigation`: Pan (space+drag, middle mouse, trackpad), zoom (ctrl+scroll, pinch, keyboard shortcuts), zoom-to-fit, pixel grid
- `selection-manipulation`: Click select, shift-click multi-select, marquee selection, move with drag, resize handles, rotation with 15° snap, duplicate (alt+drag, ⌘D), delete, nudge with arrows
- `undo-redo`: Inverse-command pattern undo/redo wired into all editor operations
- `text-editing`: Inline text editing with CanvasKit Paragraph API, textarea overlay, font loading (Inter default + Local Font Access API for system fonts), font weight, auto-width text boxes
- `pen-tool`: Pen tool with vector network model, bezier curves, open/closed paths, vectorNetworkBlob binary encode/decode
- `auto-layout`: Yoga WASM integration, horizontal/vertical/wrap, gap, padding, justify, align, child sizing, Shift+A toggle, drag reordering with insertion indicator, wrap selected nodes in auto-layout
- `figma-clipboard`: Bidirectional Figma clipboard — paste from Figma (fig-kiwi binary decode), copy for Figma (fig-kiwi binary encode), cut/copy between OpenPencil instances
- `fig-import`: .fig file import via Kiwi codec — header parsing, Zstd decompression, schema decode, NodeChange[] extraction, scene graph population
- `kiwi-codec`: Vendored kiwi-schema, Kiwi binary codec for Figma's 194-definition schema, ESM/CLI patching for sparse field IDs
- `editor-ui`: Vue 3 + Reka UI panels — toolbar (bottom), layers panel (tree with expand/collapse, drag reorder, visibility toggle), properties panel (appearance, fill, stroke, typography, layout, position sections), color picker (HSV, hex, opacity, alpha)
- `snap-guides`: Edge and center snapping, rotation-aware snap guides and selection bounds, sibling-based absolute coordinate snapping
- `rulers`: Canvas rulers with selection highlight, coordinate badges at selection start/end, tick label alignment, ?no-rulers URL param
- `group-ungroup`: Group/ungroup selection (⌘G / ⇧⌘G), sort nodes by position when wrapping
- `desktop-app`: Tauri v2 shell with native macOS menu bar, menu events wired to frontend, Developer Tools menu item
- `testing`: Playwright visual regression (E2E with Figma CDP reference), bun:test unit tests for SceneGraph, no-chrome test mode, data-ready optimization
- `scrub-input`: ScrubInput component — drag-to-scrub values, click-to-edit, suffix styling, used across all numeric property inputs
- `tooling`: oxlint, oxfmt, typescript-go, Vite 7, Tailwind CSS 4, unplugin-icons (Lucide via Iconify)
### Modified Capabilities
_(none — this is the initial baseline)_
## Impact
- `openspec/specs/` — populated with ~19 baseline spec files documenting current state
- `openspec/specs/` — additional specs for planned capabilities from PLAN.md marked as "planned"
- No source code changes — this is a documentation-only change
- Assessment report on VitePress readiness included in the specs

View file

@ -0,0 +1,47 @@
## ADDED Requirements
### Requirement: Yoga WASM layout engine
Auto-layout SHALL use Yoga WASM for flexbox-based layout computation. Frames with auto-layout SHALL have their children positioned by Yoga.
#### Scenario: Horizontal auto-layout
- **WHEN** a frame has stackMode HORIZONTAL with gap 16 and three children
- **THEN** the children are laid out horizontally with 16px between them
### Requirement: Layout properties
Auto-layout frames SHALL support: direction (horizontal/vertical/wrap), gap, padding (uniform and per-side), justify (start/center/end/space-between), align (start/center/end/stretch), and child sizing (fixed/fill/hug).
#### Scenario: Vertical layout with padding
- **WHEN** a frame has stackMode VERTICAL, padding 20, and two children
- **THEN** the children are stacked vertically with 20px padding on all sides
### Requirement: Shift+A toggle
Shift+A SHALL toggle auto-layout on the selected frame. If nodes are selected without a parent frame, Shift+A SHALL wrap them in a new auto-layout frame.
#### Scenario: Toggle auto-layout on frame
- **WHEN** user selects a frame and presses Shift+A
- **THEN** auto-layout is enabled on the frame with default settings
#### Scenario: Wrap selection in auto-layout
- **WHEN** user selects three standalone rectangles and presses Shift+A
- **THEN** the rectangles are wrapped in a new frame with auto-layout enabled
### Requirement: Drag reordering
Within an auto-layout frame, dragging a child SHALL reorder it among siblings with a visual insertion indicator.
#### Scenario: Reorder by drag
- **WHEN** user drags the second child past the third child in an auto-layout frame
- **THEN** the second child moves to position 3 with an insertion line indicating the drop position
### Requirement: Sort by position on wrap
When wrapping selected nodes in auto-layout, nodes SHALL be sorted by their visual position (left-to-right for horizontal, top-to-bottom for vertical).
#### Scenario: Wrap preserves visual order
- **WHEN** three scattered nodes are wrapped in horizontal auto-layout
- **THEN** they are ordered left-to-right based on their X position
### Requirement: Auto-layout properties panel
The properties panel SHALL show auto-layout controls (direction, gap, padding, justify, align, sizing) when an auto-layout frame is selected.
#### Scenario: Properties panel for auto-layout
- **WHEN** user selects a frame with auto-layout enabled
- **THEN** the properties panel shows the Layout section with editable direction, gap, padding, justify, and align controls

View file

@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Pan with space+drag
The canvas SHALL support panning via space+drag, middle mouse drag, and two-finger trackpad gestures.
#### Scenario: Space+drag pan
- **WHEN** user holds space and drags on the canvas
- **THEN** the viewport pans by the drag delta
### Requirement: Zoom with scroll
The canvas SHALL support zooming via ctrl+scroll (or pinch) toward the cursor position, and via keyboard shortcuts ⌘= (zoom in), ⌘- (zoom out), ⌘0 (100%).
#### Scenario: Ctrl+scroll zoom
- **WHEN** user ctrl+scrolls up on the canvas
- **THEN** the viewport zooms in centered on the cursor position
#### Scenario: Keyboard zoom
- **WHEN** user presses ⌘0
- **THEN** the viewport resets to 100% zoom level
### Requirement: Pinch-to-zoom isolation
Browser pinch-to-zoom SHALL be prevented on UI panels while allowing canvas pinch-to-zoom.
#### Scenario: Pinch on panel
- **WHEN** user performs a pinch gesture on the properties panel
- **THEN** the browser zoom does not change
### Requirement: Hand tool
Pressing H SHALL activate the hand tool for continuous panning without holding space.
#### Scenario: Hand tool activation
- **WHEN** user presses H
- **THEN** the hand tool is active and any drag pans the canvas

View file

@ -0,0 +1,50 @@
## ADDED Requirements
### Requirement: CanvasKit WASM rendering
The renderer SHALL use Skia CanvasKit WASM to draw all scene graph nodes onto an HTML canvas element.
#### Scenario: Canvas initialization
- **WHEN** the editor loads
- **THEN** CanvasKit WASM (canvaskit.wasm) is loaded and a GPU-accelerated surface is created
### Requirement: Shape rendering
The renderer SHALL draw RECTANGLE, ELLIPSE, LINE, STAR, POLYGON, and VECTOR nodes with their fill paints, stroke paints, opacity, and transforms.
#### Scenario: Rectangle with fill and stroke
- **WHEN** a rectangle node has a solid fill (#3B82F6) and a 2px stroke (#000)
- **THEN** the rectangle is rendered with the correct fill color, stroke color, and stroke width
### Requirement: Text rendering
The renderer SHALL render TEXT nodes using CanvasKit's Paragraph API with font family, weight, size, line height, and letter spacing.
#### Scenario: Text with Inter font
- **WHEN** a text node with fontName "Inter", fontSize 16, and characters "Hello" exists
- **THEN** the text is rendered at the correct position with the Inter font
### Requirement: Nested coordinate systems
The renderer SHALL apply parent transforms (position + rotation) when rendering child nodes, supporting arbitrary nesting depth.
#### Scenario: Rotated parent with child
- **WHEN** a frame is rotated 45° and contains a child rectangle
- **THEN** the child rectangle is rendered in the frame's rotated coordinate system
### Requirement: Selection outlines
The renderer SHALL draw selection outlines (blue rectangles) around selected nodes with resize handles at corners and edge midpoints.
#### Scenario: Select a node
- **WHEN** a node is selected
- **THEN** a blue outline with 8 resize handles appears around the node's bounds
### Requirement: Surface resize
The renderer SHALL recreate the Skia surface when the window is resized, maintaining correct dimensions.
#### Scenario: Window resize
- **WHEN** the browser window is resized
- **THEN** the canvas and Skia surface update to the new dimensions without artifacts
### Requirement: Silent crash prevention
The renderer SHALL detect and report rendering errors instead of silently failing.
#### Scenario: Rendering error
- **WHEN** a rendering operation encounters an error
- **THEN** the error is detected and reported rather than silently producing a blank canvas

View file

@ -0,0 +1,36 @@
## ADDED Requirements
### Requirement: Tauri v2 desktop shell
The editor SHALL run as a native desktop app via Tauri v2 with the web frontend loaded in a webview.
#### Scenario: Desktop app launch
- **WHEN** user runs `bun run tauri dev`
- **THEN** a native desktop window opens with the editor UI and CanvasKit rendering
### Requirement: Native macOS menu bar
The desktop app SHALL display a native macOS menu bar with File, Edit, View, and Window menus.
#### Scenario: Menu bar presence
- **WHEN** the desktop app launches on macOS
- **THEN** a native menu bar with standard menus is visible
### Requirement: Menu events wired to frontend
Menu item clicks SHALL fire events that the Vue frontend handles (e.g., Undo, Redo, Zoom In/Out).
#### Scenario: Menu undo
- **WHEN** user clicks Edit → Undo in the menu bar
- **THEN** the frontend receives the undo event and performs undo
### Requirement: Developer tools menu item
The menu SHALL include a Developer Tools item (⌘⌥I) that opens the webview inspector.
#### Scenario: Open dev tools
- **WHEN** user selects View → Developer Tools or presses ⌘⌥I
- **THEN** the webview developer tools panel opens
### Requirement: Desktop directory structure
The Tauri configuration and Rust source SHALL live in `desktop/` (not `src-tauri/`). Tauri CLI SHALL find it by scanning.
#### Scenario: Build from desktop directory
- **WHEN** `bun run tauri build` is run
- **THEN** Tauri finds configuration in `desktop/` and produces a native binary

View file

@ -0,0 +1,62 @@
## ADDED Requirements
### Requirement: Vue 3 + Reka UI component architecture
The editor UI SHALL be built with Vue 3, VueUse composables, and Reka UI headless components. The UI framework was migrated from React to Vue 3.
#### Scenario: Editor loads
- **WHEN** user opens the editor at localhost:1420
- **THEN** the Vue 3 application renders with canvas, toolbar, layers panel, and properties panel
### Requirement: Bottom toolbar
The toolbar SHALL be positioned at the bottom of the screen (Figma UI3 style) with tool selection: Select (V), Frame (F), Rectangle (R), Ellipse (O), Line (L), Text (T), Hand (H), Pen (P).
#### Scenario: Tool selection via keyboard
- **WHEN** user presses R
- **THEN** the rectangle tool is activated and the toolbar shows R as selected
### Requirement: Layers panel with tree view
The layers panel SHALL display a tree view of the document hierarchy using Reka UI Tree with expand/collapse and drag reordering.
#### Scenario: Expand/collapse frame children
- **WHEN** user clicks the chevron next to a frame in the layers panel
- **THEN** the frame's children are shown or hidden
#### Scenario: Drag reorder layers
- **WHEN** user drags a layer to a new position in the layers panel
- **THEN** the node's z-order in the scene graph changes accordingly
#### Scenario: Visibility toggle
- **WHEN** user clicks the visibility icon next to a layer
- **THEN** the node is hidden/shown on canvas
### Requirement: Properties panel with sections
The properties panel SHALL be split into sections: Appearance (size, position, rotation, opacity, corner radius, visibility), Fill, Stroke, Typography, Layout, Position.
#### Scenario: Properties panel shows fill section
- **WHEN** user selects a rectangle with a blue fill
- **THEN** the Fill section shows the hex color #3B82F6 with opacity and visibility controls
#### Scenario: Editable hex input
- **WHEN** user types a new hex value in the fill section
- **THEN** the node's fill updates to the entered color
### Requirement: Color picker
The color picker SHALL provide HSV color selection, hue slider, alpha slider, hex input, and opacity control.
#### Scenario: Change color via HSV
- **WHEN** user moves the color picker cursor in the HSV square
- **THEN** the selected node's fill updates in real time
### Requirement: Lucide icons via Iconify
UI icons SHALL use Lucide icons loaded via unplugin-icons with Iconify, replacing inline emoji/SVG.
#### Scenario: Icon rendering
- **WHEN** the toolbar renders tool icons
- **THEN** Lucide icons are displayed via the icon auto-import resolver
### Requirement: Tailwind CSS 4 styling
The editor SHALL use Tailwind CSS 4 for all styling.
#### Scenario: Dark theme
- **WHEN** the editor renders
- **THEN** the UI uses a dark theme styled with Tailwind CSS 4 utility classes

View file

@ -0,0 +1,22 @@
## ADDED Requirements
### Requirement: .fig file import
The editor SHALL import .fig files via the Kiwi codec pipeline: parse header (magic "fig-kiwi" + version), decompress Zstd, decode Kiwi schema, extract NodeChange[], and build the scene graph.
#### Scenario: Import a .fig file
- **WHEN** user opens a .fig file
- **THEN** all nodes from the file appear on the canvas with correct types, positions, sizes, and visual properties
### Requirement: File open via keyboard
⌘O SHALL open a file dialog for .fig import.
#### Scenario: Open file dialog
- **WHEN** user presses ⌘O
- **THEN** a file picker dialog appears filtered for .fig files
### Requirement: Blob reference resolution
The import pipeline SHALL resolve blob references (images, vector networks, font data) from the .fig file's blob section.
#### Scenario: Import with vector blobs
- **WHEN** a .fig file containing vector nodes with vectorNetworkBlob data is imported
- **THEN** the vector paths are correctly decoded and renderable

View file

@ -0,0 +1,36 @@
## ADDED Requirements
### Requirement: Paste from Figma
The editor SHALL accept paste events containing fig-kiwi binary data from Figma's clipboard. Pasted nodes SHALL be decoded and added to the scene graph.
#### Scenario: Paste Figma nodes
- **WHEN** user copies a frame in Figma and pastes in OpenPencil
- **THEN** the frame and its children appear on the canvas with preserved properties
### Requirement: Copy for Figma compatibility
The editor SHALL encode selected nodes as fig-kiwi binary on copy, allowing paste into Figma or other OpenPencil instances.
#### Scenario: Copy to Figma
- **WHEN** user copies a rectangle in OpenPencil and pastes in Figma
- **THEN** the rectangle appears in Figma with preserved fill, stroke, and dimensions
### Requirement: Native copy/paste events
Clipboard operations SHALL use native copy/paste browser events (not async Clipboard API) and build the fig-kiwi binary synchronously in the event handler.
#### Scenario: Synchronous clipboard encoding
- **WHEN** user presses ⌘C with a node selected
- **THEN** the fig-kiwi binary is built synchronously within the copy event handler
### Requirement: Cut operation
Cut SHALL copy nodes to clipboard and delete them from the scene graph.
#### Scenario: Cut and paste
- **WHEN** user cuts a node and pastes
- **THEN** the original is removed and a copy appears at the paste position
### Requirement: Copy between instances
Copy/paste SHALL work between multiple OpenPencil browser tabs/windows.
#### Scenario: Cross-instance paste
- **WHEN** user copies a node in one OpenPencil tab and pastes in another
- **THEN** the node appears in the second tab with all properties preserved

View file

@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: Group selection
⌘G SHALL group the current selection into a GROUP node. The group inherits the bounding box of its children.
#### Scenario: Group three nodes
- **WHEN** user selects three shapes and presses ⌘G
- **THEN** a GROUP node is created containing the three shapes as children
### Requirement: Ungroup selection
⇧⌘G SHALL ungroup the selected group, moving its children to the group's parent at the group's position.
#### Scenario: Ungroup
- **WHEN** user selects a group and presses ⇧⌘G
- **THEN** the group is removed and its children become siblings of the group's former parent
### Requirement: Position-based sort on wrap
When grouping or wrapping in auto-layout, nodes SHALL be sorted by their visual position to maintain reading order.
#### Scenario: Group preserves visual order
- **WHEN** three nodes at positions X=300, X=100, X=200 are grouped
- **THEN** the children are ordered by position (X=100 first, X=300 last)
### Requirement: Null node crash prevention
Group/ungroup operations SHALL handle null node references gracefully without crashing.
#### Scenario: Null node in selection
- **WHEN** a group operation encounters a null node reference in the selection
- **THEN** the operation completes without crashing, skipping the null reference

View file

@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Vendored kiwi-schema
The project SHALL vendor kiwi-schema (from evanw/kiwi) with patches for ESM module format and sparse field ID handling.
#### Scenario: ESM import
- **WHEN** kiwi-schema is imported as an ES module
- **THEN** all encoder/decoder functions are available without CommonJS compatibility issues
### Requirement: Kiwi binary codec
The codec SHALL encode and decode Figma's 194-definition Kiwi schema including the NodeChange message type with ~390 fields.
#### Scenario: Decode a Kiwi message
- **WHEN** a compressed Kiwi binary is provided
- **THEN** the codec decodes it into a structured NodeChange[] array
#### Scenario: Encode a Kiwi message
- **WHEN** a NodeChange[] array is provided
- **THEN** the codec encodes it into Kiwi binary format
### Requirement: Sparse field ID support
The kiwi-schema parser SHALL handle sparse field IDs (non-contiguous field numbering) used by Figma's schema.
#### Scenario: Parse schema with sparse IDs
- **WHEN** a Kiwi schema definition has field IDs like 1, 2, 5, 10 (with gaps)
- **THEN** the parser correctly handles all fields without errors
### Requirement: Zstd compression
The codec layer SHALL support Zstd decompression for .fig file payloads and compression for clipboard/export.
#### Scenario: Decompress .fig payload
- **WHEN** a Zstd-compressed Kiwi payload is encountered in a .fig file
- **THEN** it is decompressed before Kiwi decoding

View file

@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: Pen tool with vector network model
Pressing P SHALL activate the pen tool. Clicking places corner points, click+drag places curve points with bezier handles. The underlying data model SHALL use vector networks (not simple paths).
#### Scenario: Place corner points
- **WHEN** user clicks three positions with the pen tool
- **THEN** a path with three straight-line segments is created
#### Scenario: Place curve point
- **WHEN** user clicks and drags with the pen tool
- **THEN** a bezier curve point is created with tangent handles matching the drag direction and length
### Requirement: Open and closed paths
Clicking the first point SHALL close the path. Pressing Escape SHALL commit an open path.
#### Scenario: Close path
- **WHEN** user clicks on the first point of an in-progress path
- **THEN** the path closes into a loop
#### Scenario: Open path
- **WHEN** user presses Escape during pen tool drawing
- **THEN** the current path is committed as an open path
### Requirement: Vector network blob encoding
Vector network data SHALL be encoded/decoded using the Figma vectorNetworkBlob binary format for .fig compatibility.
#### Scenario: Encode vector data
- **WHEN** a vector node is created with the pen tool
- **THEN** the vector data can be encoded to the binary vectorNetworkBlob format
#### Scenario: Decode imported vector data
- **WHEN** a .fig file containing vector nodes is imported
- **THEN** the vectorNetworkBlob is decoded into editable vector network data
### Requirement: Open path rendering
Open paths SHALL be rendered as open strokes, not closed and filled.
#### Scenario: Open path stroke
- **WHEN** an open vector path has a stroke applied
- **THEN** the path is rendered as an open stroke line, not as a closed filled shape
### Requirement: Preview line
During pen tool drawing, a preview line SHALL connect the last placed point to the current cursor position. The preview SHALL NOT start from (0,0).
#### Scenario: Preview line from last point
- **WHEN** user has placed one point and moves the cursor
- **THEN** a preview line extends from the last placed point to the cursor position

View file

@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Canvas rulers
Horizontal and vertical rulers SHALL be displayed at the top and left edges of the canvas, showing coordinate scales that update with pan/zoom.
#### Scenario: Ruler display
- **WHEN** the editor loads
- **THEN** horizontal and vertical rulers are visible with tick marks and coordinate labels
### Requirement: Selection highlight on rulers
Rulers SHALL display a highlight band at the position/size of the current selection.
#### Scenario: Selection ruler highlight
- **WHEN** a 200px wide node at X=100 is selected
- **THEN** the horizontal ruler shows a translucent highlight band from 100 to 300
### Requirement: Coordinate badges
Rulers SHALL display coordinate badges at the selection's start and end positions.
#### Scenario: Coordinate badges
- **WHEN** a node spanning X 100-300, Y 50-200 is selected
- **THEN** coordinate badges show 100 and 300 on the horizontal ruler, 50 and 200 on the vertical ruler
### Requirement: Consistent text alignment
Ruler tick labels and badge text SHALL align to the same baseline for visual consistency.
#### Scenario: Label alignment
- **WHEN** rulers render tick labels and coordinate badges
- **THEN** text baselines are aligned consistently across ticks and badges
### Requirement: Vertical ruler text clipping
Vertical ruler text SHALL NOT clip or overflow outside the ruler bounds.
#### Scenario: Vertical ruler overflow
- **WHEN** the vertical ruler displays coordinate labels
- **THEN** no text is clipped or extends beyond the ruler's visible area
### Requirement: Rulers can be disabled
The URL parameter `?no-rulers` SHALL disable canvas rulers for testing or presentation.
#### Scenario: Disable rulers via URL
- **WHEN** the editor loads with `?no-rulers` in the URL
- **THEN** rulers are not rendered

View file

@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Flat node storage with GUID keys
The scene graph SHALL store all nodes in a flat `Map<string, Node>` keyed by GUID string. Tree structure SHALL be maintained via `parentIndex` references on each node.
#### Scenario: Create and retrieve a node
- **WHEN** a node is created with type RECTANGLE and a parent GUID
- **THEN** the node is stored in the map and retrievable by its GUID
#### Scenario: Parent-child relationship
- **WHEN** a child node references a parent via parentIndex
- **THEN** `getChildren(parentId)` returns the child sorted by position string
### Requirement: Node types
The scene graph SHALL support node types: FRAME, RECTANGLE, ELLIPSE, LINE, TEXT, VECTOR, STAR, POLYGON, GROUP, BOOLEAN_OPERATION, DOCUMENT, CANVAS.
#### Scenario: Create each supported node type
- **WHEN** a node of each supported type is created
- **THEN** the node is stored with the correct type enum value
### Requirement: CRUD operations
The scene graph SHALL support create, read, update, and delete operations on nodes.
#### Scenario: Delete a node
- **WHEN** a node is deleted by GUID
- **THEN** the node is removed from the map and no longer returned by queries
#### Scenario: Update node properties
- **WHEN** a node's properties (size, fill, rotation) are updated
- **THEN** subsequent reads return the updated values
### Requirement: Hit testing
The scene graph SHALL support hit testing — given a point in canvas coordinates, return the topmost visible node at that position.
#### Scenario: Hit test on overlapping nodes
- **WHEN** two rectangles overlap and a point inside the overlap area is tested
- **THEN** the node with higher z-order (later in position string sorting) is returned
### Requirement: Rectangular area query
The scene graph SHALL support querying all nodes that intersect a given rectangle (for marquee selection).
#### Scenario: Marquee selection
- **WHEN** a rectangle area query is performed
- **THEN** all nodes whose bounds intersect the rectangle are returned
### Requirement: Unit test coverage
The scene graph SHALL have bun:test unit tests covering CRUD, parent-child, z-ordering, and hit testing.
#### Scenario: Unit tests pass
- **WHEN** `bun test ./tests/engine` is run
- **THEN** all scene graph unit tests pass in <50ms

View file

@ -0,0 +1,36 @@
## ADDED Requirements
### Requirement: Drag-to-scrub numeric input
The ScrubInput component SHALL allow users to drag anywhere on the input field to scrub (adjust) the numeric value. The entire field area SHALL be draggable.
#### Scenario: Drag to increase value
- **WHEN** user clicks and drags right on a ScrubInput with value 100
- **THEN** the value increases proportionally to the drag distance
### Requirement: Click-to-edit mode
Clicking (without dragging) a ScrubInput SHALL enter direct text editing mode where the user can type a numeric value.
#### Scenario: Click to type
- **WHEN** user clicks a ScrubInput without dragging
- **THEN** the input becomes editable and accepts keyboard input
### Requirement: Suffix styling
ScrubInput SHALL support a visual suffix (e.g., "°", "px", "%") displayed after the value.
#### Scenario: Display suffix
- **WHEN** a ScrubInput has suffix "°" and value 45
- **THEN** the input displays "45°"
### Requirement: Used across all numeric properties
ScrubInput SHALL replace plain number inputs in all property panel sections (position, size, rotation, opacity, corner radius, gap, padding, font size, stroke weight).
#### Scenario: Position input uses ScrubInput
- **WHEN** the Position section renders X and Y inputs
- **THEN** they are ScrubInput components with drag-to-scrub behavior
### Requirement: Scrub label hit area
The clickable/draggable area of the scrub label SHALL be properly sized to avoid missing clicks.
#### Scenario: Reliable click target
- **WHEN** user clicks on the scrub label text
- **THEN** the click is registered and scrub mode activates

View file

@ -0,0 +1,78 @@
## ADDED Requirements
### Requirement: Click to select
Clicking on a node SHALL select it (deselecting all others). Clicking empty canvas SHALL deselect all.
#### Scenario: Select a single node
- **WHEN** user clicks on a rectangle
- **THEN** the rectangle is selected and all other nodes are deselected
### Requirement: Multi-select with shift
Shift+clicking a node SHALL toggle it in the current selection.
#### Scenario: Add to selection
- **WHEN** user shift+clicks a second node while one is already selected
- **THEN** both nodes are in the selection
### Requirement: Marquee selection
Dragging on empty canvas SHALL create a selection rectangle. All nodes intersecting the rectangle SHALL be selected on release.
#### Scenario: Marquee drag
- **WHEN** user drags from empty canvas across three nodes
- **THEN** all three nodes are selected when the drag ends
### Requirement: Move by dragging
Dragging a selected node SHALL move it (and all other selected nodes) by the drag delta.
#### Scenario: Drag to move
- **WHEN** user drags a selected node by 50px right
- **THEN** the node's X position increases by 50
### Requirement: Resize handles
Selected nodes SHALL display 8 resize handles (4 corners + 4 edge midpoints). Dragging a handle SHALL resize the node. Shift constrains proportions.
#### Scenario: Resize from corner
- **WHEN** user drags the bottom-right corner handle outward
- **THEN** the node's width and height increase
### Requirement: Rotation
Hovering just outside a corner handle SHALL show a rotation cursor. Dragging SHALL rotate the node. Shift SHALL snap to 15° increments.
#### Scenario: Rotate with snap
- **WHEN** user drags the rotation handle while holding shift
- **THEN** the rotation snaps to the nearest 15° increment
### Requirement: Duplicate
Alt+drag SHALL duplicate the selected node and move the duplicate. ⌘D SHALL duplicate in place.
#### Scenario: Alt+drag duplicate
- **WHEN** user alt+drags a selected rectangle
- **THEN** a copy of the rectangle is created at the drag position
### Requirement: Constrained drawing
Holding shift while drawing a shape SHALL constrain to square/circle.
#### Scenario: Shift+drag rectangle
- **WHEN** user draws a rectangle while holding shift
- **THEN** the rectangle is constrained to a square (equal width and height)
### Requirement: Nudge with arrows
Arrow keys SHALL move selected nodes by 1px. Shift+arrow SHALL move by 10px.
#### Scenario: Arrow nudge
- **WHEN** user presses the right arrow key with a node selected
- **THEN** the node moves 1px to the right
### Requirement: Delete selection
Backspace/Delete SHALL remove all selected nodes.
#### Scenario: Delete node
- **WHEN** user presses backspace with a node selected
- **THEN** the node is removed from the scene graph
### Requirement: Select all
⌘A SHALL select all nodes on the current canvas.
#### Scenario: Select all
- **WHEN** user presses ⌘A
- **THEN** all nodes on the canvas are selected

View file

@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: Edge and center snapping
When moving or resizing nodes, snap guides SHALL appear when edges or centers align with sibling nodes (±1px threshold).
#### Scenario: Horizontal edge snap
- **WHEN** user drags a node and its left edge aligns with another node's left edge
- **THEN** a red vertical snap guide line appears at the aligned X coordinate
#### Scenario: Center snap
- **WHEN** user drags a node and its horizontal center aligns with another node's center
- **THEN** a red snap guide line appears at the center coordinate
### Requirement: Rotation-aware snap guides
Snap guides SHALL use rotation-aware bounding boxes. Snap calculations SHALL work correctly for rotated nodes.
#### Scenario: Snap rotated node
- **WHEN** a rotated node is moved near an unrotated node
- **THEN** snap guides align to the rotated node's actual visual bounds
### Requirement: Sibling-based absolute coordinates
Snap guide calculations SHALL use sibling nodes in absolute (canvas) coordinates, not parent-relative coordinates.
#### Scenario: Snap across nesting levels
- **WHEN** a node inside a frame is moved near a node outside the frame
- **THEN** snap guides correctly compare positions in absolute canvas coordinates

View file

@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Playwright visual regression testing
E2E tests SHALL use Playwright to create shapes and compare screenshots against baseline snapshots.
#### Scenario: Visual regression test pass
- **WHEN** `bun run test` is executed
- **THEN** Playwright tests create shapes, take screenshots, and compare against baseline PNGs
### Requirement: Figma CDP reference tests
A separate Playwright project SHALL connect to Figma via Chrome DevTools Protocol (CDP) to capture reference screenshots for pixel-perfect comparison.
#### Scenario: Figma reference capture
- **WHEN** `bun run test:figma` is executed with Figma running in debug mode
- **THEN** Playwright connects to Figma via CDP and captures reference screenshots
### Requirement: No-chrome test mode
The editor SHALL support a test mode (activated via URL param or environment) that hides Chrome/UI elements for clean screenshot capture.
#### Scenario: Test mode rendering
- **WHEN** the editor loads in test mode
- **THEN** only canvas content is rendered, without toolbar, panels, or other UI chrome
### Requirement: Data-ready optimization
The editor SHALL signal readiness via a `data-ready` attribute for E2E test synchronization, replacing unreliable timeouts.
#### Scenario: Wait for ready
- **WHEN** a Playwright test waits for the data-ready attribute
- **THEN** it proceeds only when the canvas is fully rendered
### Requirement: Page reuse for speed
E2E tests SHALL reuse the browser page across test cases to minimize overhead. Target: all E2E tests complete in <3s.
#### Scenario: Fast E2E execution
- **WHEN** the full E2E test suite runs
- **THEN** it completes in under 3 seconds by reusing the page
### Requirement: bun:test unit tests
Engine unit tests SHALL use bun:test and complete in <50ms.
#### Scenario: Unit test speed
- **WHEN** `bun test ./tests/engine` is run
- **THEN** all unit tests pass in under 50ms

View file

@ -0,0 +1,47 @@
## ADDED Requirements
### Requirement: Text tool creates text nodes
Pressing T SHALL activate the text tool. Clicking on canvas SHALL create a new empty text node with cursor blinking.
#### Scenario: Create text node
- **WHEN** user presses T and clicks on the canvas
- **THEN** an empty text node is created at the click position with a visible cursor
### Requirement: Inline text editing
Double-clicking a text node SHALL enter inline editing mode with a textarea overlay positioned over the canvas text. The canvas text and selection SHALL be hidden during editing.
#### Scenario: Enter edit mode
- **WHEN** user double-clicks a text node
- **THEN** a textarea overlay appears and the canvas-rendered text is hidden
### Requirement: CanvasKit Paragraph API rendering
Text nodes SHALL be rendered using CanvasKit's Paragraph API for proper text shaping, line breaking, and font metrics.
#### Scenario: Multi-line text wrapping
- **WHEN** a text node with a fixed width contains text longer than the width
- **THEN** the text wraps to multiple lines using Paragraph API layout
### Requirement: Font loading
The editor SHALL load Inter as the default font. The Local Font Access API SHALL be used to enumerate and load system fonts when available.
#### Scenario: Default font
- **WHEN** a text node is created without specifying a font
- **THEN** the Inter font is used for rendering
#### Scenario: System fonts
- **WHEN** the Local Font Access API is available
- **THEN** system fonts are enumerable in the font picker
### Requirement: Font weight support
Font weight SHALL be applied to both the CanvasKit text style and the textarea overlay during editing.
#### Scenario: Bold text rendering
- **WHEN** a text node has fontWeight 700
- **THEN** both the canvas rendering and the editing overlay display bold text
### Requirement: Blue editing outline
A blue outline SHALL appear around a text node during editing to indicate edit mode.
#### Scenario: Edit mode indicator
- **WHEN** user is editing a text node inline
- **THEN** a blue outline is visible around the text node bounds

View file

@ -0,0 +1,43 @@
## ADDED Requirements
### Requirement: Vite 7 build system
The project SHALL use Vite 7 as the build tool with dev server at port 1420 and HMR support.
#### Scenario: Dev server
- **WHEN** `bun run dev` is executed
- **THEN** Vite dev server starts at http://localhost:1420 with hot module replacement
### Requirement: oxlint linting
The project SHALL use oxlint for fast linting with project-specific configuration in oxlint.json.
#### Scenario: Lint check
- **WHEN** `bun run lint` is executed
- **THEN** oxlint checks src/ for issues based on oxlint.json rules
### Requirement: oxfmt formatting
The project SHALL use oxfmt for code formatting with configuration in .oxfmtrc.json.
#### Scenario: Format code
- **WHEN** `bun run format` is executed
- **THEN** oxfmt formats all files in src/ according to project rules
### Requirement: typescript-go type checking
The project SHALL use typescript-go (tsgo) for type checking via `bun run typecheck`.
#### Scenario: Type check
- **WHEN** `bun run typecheck` is executed
- **THEN** tsgo --noEmit checks all TypeScript files for type errors
### Requirement: Combined check command
`bun run check` SHALL run both lint and typecheck sequentially.
#### Scenario: Full check
- **WHEN** `bun run check` is executed
- **THEN** oxlint runs first, then tsgo, and both must pass
### Requirement: Tailwind CSS 4 integration
Tailwind CSS 4 SHALL be integrated via @tailwindcss/vite plugin.
#### Scenario: Tailwind classes work
- **WHEN** a Vue component uses Tailwind utility classes
- **THEN** the corresponding CSS is generated and applied

View file

@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Inverse-command undo
The undo system SHALL use an inverse-command pattern where every operation records its inverse. ⌘Z SHALL undo and ⇧⌘Z SHALL redo.
#### Scenario: Undo a move
- **WHEN** user moves a node and presses ⌘Z
- **THEN** the node returns to its original position
#### Scenario: Redo after undo
- **WHEN** user undoes a move and presses ⇧⌘Z
- **THEN** the node moves back to the position after the original move
### Requirement: All operations undoable
Undo/redo SHALL be wired into all editor operations: create, delete, move, resize, rotate, property changes, reparent, layout changes.
#### Scenario: Undo node creation
- **WHEN** user creates a rectangle and presses ⌘Z
- **THEN** the rectangle is removed from the scene graph
#### Scenario: Undo property change
- **WHEN** user changes a node's fill color from blue to red and presses ⌘Z
- **THEN** the fill color reverts to blue

View file

@ -0,0 +1,10 @@
## 1. Validate artifacts
- [x] 1.1 Run `openspec status --change document-project-history` to confirm all artifacts (proposal, design, specs, tasks) are done
- [x] 1.2 Verify all 19 spec directories exist in `changes/document-project-history/specs/`: scene-graph, canvas-rendering, canvas-navigation, selection-manipulation, undo-redo, text-editing, pen-tool, auto-layout, figma-clipboard, fig-import, kiwi-codec, editor-ui, snap-guides, rulers, group-ungroup, desktop-app, testing, scrub-input, tooling
## 2. Archive and merge
- [x] 2.1 Run `openspec archive --change document-project-history` to merge delta specs into `openspec/specs/` and move the change to archive
- [x] 2.2 Verify all 19 spec directories now exist in `openspec/specs/` with their `spec.md` files
- [x] 2.3 Verify the change was moved to `openspec/changes/archive/document-project-history/`

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-28

View file

@ -0,0 +1,35 @@
## Context
The Tauri v2 desktop app's menu setup in `desktop/src/lib.rs` uses macOS-specific `PredefinedMenuItem` variants (About, Services, Hide, Hide Others, Show All) that are unavailable on Windows and Linux. The app_menu submenu containing these items is a macOS convention (the "application name" menu left of File). Windows and Linux apps don't have this submenu.
README only documents `bun install` + `bun run dev` for the web mode. Desktop build prerequisites vary by platform and are not documented.
## Goals / Non-Goals
**Goals:**
- Desktop app compiles and runs on Windows and Linux (in addition to macOS)
- README documents platform-specific prerequisites for all three platforms
**Non-Goals:**
- Platform-specific UI adaptations beyond the menu bar
- CI/CD pipeline for cross-platform builds
- Automated testing on Windows/Linux
## Decisions
### 1. `#[cfg(target_os = "macos")]` for app_menu
Wrap the entire app_menu (OpenPencil submenu) in a compile-time conditional. On non-macOS platforms, the menu starts directly with File.
**Alternative considered:** Runtime detection via `std::env::consts::OS` — rejected because menu construction happens at build time with Tauri's type system, and conditional compilation is the idiomatic Rust/Tauri pattern used by other projects (museeks, gitbutler).
### 2. `mut builder` pattern for conditional menu assembly
Use `let mut builder = MenuBuilder::new(app)` then conditionally `builder = builder.item(&app_menu)` inside a `#[cfg]` block, followed by `.items(&[...])` for the shared menus.
**Alternative considered:** Two separate `MenuBuilder` chains (one for macOS, one for others) — rejected to avoid duplicating the shared menu list.
## Risks / Trade-offs
- **[PredefinedMenuItem::fullscreen on Windows]** The `fullscreen` item in view_menu uses a macOS predefined — may be a no-op on Windows. Low risk, Tauri handles this gracefully. → Monitor and wrap in `#[cfg]` if needed later.
- **[README link rot]** Microsoft URLs for Visual Studio Build Tools may change. → Added descriptive text as fallback.

View file

@ -0,0 +1,23 @@
## Why
The desktop app menu code uses macOS-specific items (Services, Hide, Hide Others, Show All) that don't exist on Windows/Linux, and the README lacks platform setup instructions. This blocks Windows contributors from building and running the app.
## What Changes
- Conditional compilation of the macOS app menu (`#[cfg(target_os = "macos")]`) so the desktop app compiles and runs on Windows and Linux
- Platform-specific setup instructions in README (Windows, macOS, Linux prerequisites)
## Capabilities
### New Capabilities
_(none)_
### Modified Capabilities
- `desktop-app`: Menu bar requirements change from macOS-only to cross-platform. App menu (About, Services, Hide, etc.) is macOS-only; other menus (File, Edit, View, Object, Window, Help) work on all platforms.
## Impact
- `desktop/src/lib.rs` — conditional compilation of app_menu submenu
- `README.md` — new Platform Prerequisites section with Windows, macOS, Linux instructions

View file

@ -0,0 +1,44 @@
## MODIFIED Requirements
### Requirement: Native macOS menu bar
The desktop app SHALL display a native menu bar. On macOS, an app-level submenu (OpenPencil) with About, Services, Hide, Hide Others, Show All, and Quit items SHALL be shown. On Windows and Linux, this submenu SHALL be omitted. File, Edit, View, Object, Window, and Help menus SHALL be present on all platforms.
#### Scenario: Menu bar on macOS
- **WHEN** the desktop app launches on macOS
- **THEN** a native menu bar with the OpenPencil app submenu and all standard menus is visible
#### Scenario: Menu bar on Windows
- **WHEN** the desktop app launches on Windows
- **THEN** a native menu bar with File, Edit, View, Object, Window, and Help menus is visible and no macOS-specific app submenu is present
#### Scenario: Menu bar on Linux
- **WHEN** the desktop app launches on Linux
- **THEN** a native menu bar with File, Edit, View, Object, Window, and Help menus is visible and no macOS-specific app submenu is present
## ADDED Requirements
### Requirement: Cross-platform build
The desktop app SHALL compile and build on macOS, Windows, and Linux without platform-specific errors.
#### Scenario: Windows build
- **WHEN** `bun run tauri build` is run on Windows with MSVC toolchain installed
- **THEN** the build completes successfully and produces a Windows executable
#### Scenario: Linux build
- **WHEN** `bun run tauri build` is run on Linux with required system libraries installed
- **THEN** the build completes successfully and produces a Linux binary
### Requirement: Platform prerequisites documentation
The README SHALL document platform-specific prerequisites for building the desktop app on macOS, Windows, and Linux.
#### Scenario: Windows prerequisites documented
- **WHEN** a developer reads the README on Windows
- **THEN** they find instructions for installing Rust (stable-msvc), Visual Studio Build Tools, and WebView2
#### Scenario: macOS prerequisites documented
- **WHEN** a developer reads the README on macOS
- **THEN** they find instructions for installing Xcode Command Line Tools
#### Scenario: Linux prerequisites documented
- **WHEN** a developer reads the README on Linux
- **THEN** they find instructions for installing required system libraries (webkit2gtk, etc.)

View file

@ -0,0 +1,10 @@
## 1. Cross-platform menu compilation
- [x] 1.1 Wrap `app_menu` creation in `#[cfg(target_os = "macos")]` in `desktop/src/lib.rs`
- [x] 1.2 Use `let mut builder` pattern to conditionally include `app_menu` in `MenuBuilder`
## 2. README platform prerequisites
- [x] 2.1 Add macOS prerequisites subsection (Xcode Command Line Tools)
- [x] 2.2 Add Windows prerequisites subsection (Rust stable-msvc, Visual Studio Build Tools, WebView2)
- [x] 2.3 Add Linux prerequisites subsection (webkit2gtk and system libraries for Debian/Ubuntu, link to Tauri docs for other distros)

View file

@ -0,0 +1,51 @@
# auto-layout Specification
## Purpose
Flexbox-based auto-layout using Yoga WASM. Frames with auto-layout have children positioned automatically with configurable direction, gap, padding, justify, align, and child sizing.
## Requirements
### Requirement: Yoga WASM layout engine
Auto-layout SHALL use Yoga WASM for flexbox-based layout computation. Frames with auto-layout SHALL have their children positioned by Yoga.
#### Scenario: Horizontal auto-layout
- **WHEN** a frame has stackMode HORIZONTAL with gap 16 and three children
- **THEN** the children are laid out horizontally with 16px between them
### Requirement: Layout properties
Auto-layout frames SHALL support: direction (horizontal/vertical/wrap), gap, padding (uniform and per-side), justify (start/center/end/space-between), align (start/center/end/stretch), and child sizing (fixed/fill/hug).
#### Scenario: Vertical layout with padding
- **WHEN** a frame has stackMode VERTICAL, padding 20, and two children
- **THEN** the children are stacked vertically with 20px padding on all sides
### Requirement: Shift+A toggle
Shift+A SHALL toggle auto-layout on the selected frame. If nodes are selected without a parent frame, Shift+A SHALL wrap them in a new auto-layout frame.
#### Scenario: Toggle auto-layout on frame
- **WHEN** user selects a frame and presses Shift+A
- **THEN** auto-layout is enabled on the frame with default settings
#### Scenario: Wrap selection in auto-layout
- **WHEN** user selects three standalone rectangles and presses Shift+A
- **THEN** the rectangles are wrapped in a new frame with auto-layout enabled
### Requirement: Drag reordering
Within an auto-layout frame, dragging a child SHALL reorder it among siblings with a visual insertion indicator.
#### Scenario: Reorder by drag
- **WHEN** user drags the second child past the third child in an auto-layout frame
- **THEN** the second child moves to position 3 with an insertion line indicating the drop position
### Requirement: Sort by position on wrap
When wrapping selected nodes in auto-layout, nodes SHALL be sorted by their visual position (left-to-right for horizontal, top-to-bottom for vertical).
#### Scenario: Wrap preserves visual order
- **WHEN** three scattered nodes are wrapped in horizontal auto-layout
- **THEN** they are ordered left-to-right based on their X position
### Requirement: Auto-layout properties panel
The properties panel SHALL show auto-layout controls (direction, gap, padding, justify, align, sizing) when an auto-layout frame is selected.
#### Scenario: Properties panel for auto-layout
- **WHEN** user selects a frame with auto-layout enabled
- **THEN** the properties panel shows the Layout section with editable direction, gap, padding, justify, and align controls

View file

@ -0,0 +1,37 @@
# canvas-navigation Specification
## Purpose
Viewport control — panning, zooming, and the hand tool. Handles space+drag, middle mouse, trackpad gestures, keyboard shortcuts, and pinch-to-zoom isolation on UI panels.
## Requirements
### Requirement: Pan with space+drag
The canvas SHALL support panning via space+drag, middle mouse drag, and two-finger trackpad gestures.
#### Scenario: Space+drag pan
- **WHEN** user holds space and drags on the canvas
- **THEN** the viewport pans by the drag delta
### Requirement: Zoom with scroll
The canvas SHALL support zooming via ctrl+scroll (or pinch) toward the cursor position, and via keyboard shortcuts ⌘= (zoom in), ⌘- (zoom out), ⌘0 (100%).
#### Scenario: Ctrl+scroll zoom
- **WHEN** user ctrl+scrolls up on the canvas
- **THEN** the viewport zooms in centered on the cursor position
#### Scenario: Keyboard zoom
- **WHEN** user presses ⌘0
- **THEN** the viewport resets to 100% zoom level
### Requirement: Pinch-to-zoom isolation
Browser pinch-to-zoom SHALL be prevented on UI panels while allowing canvas pinch-to-zoom.
#### Scenario: Pinch on panel
- **WHEN** user performs a pinch gesture on the properties panel
- **THEN** the browser zoom does not change
### Requirement: Hand tool
Pressing H SHALL activate the hand tool for continuous panning without holding space.
#### Scenario: Hand tool activation
- **WHEN** user presses H
- **THEN** the hand tool is active and any drag pans the canvas

View file

@ -0,0 +1,54 @@
# canvas-rendering Specification
## Purpose
Skia CanvasKit WASM rendering pipeline. Draws all scene graph nodes (shapes, text, effects) with transforms, selection outlines, and visual feedback onto an HTML canvas element.
## Requirements
### Requirement: CanvasKit WASM rendering
The renderer SHALL use Skia CanvasKit WASM to draw all scene graph nodes onto an HTML canvas element.
#### Scenario: Canvas initialization
- **WHEN** the editor loads
- **THEN** CanvasKit WASM (canvaskit.wasm) is loaded and a GPU-accelerated surface is created
### Requirement: Shape rendering
The renderer SHALL draw RECTANGLE, ELLIPSE, LINE, STAR, POLYGON, and VECTOR nodes with their fill paints, stroke paints, opacity, and transforms.
#### Scenario: Rectangle with fill and stroke
- **WHEN** a rectangle node has a solid fill (#3B82F6) and a 2px stroke (#000)
- **THEN** the rectangle is rendered with the correct fill color, stroke color, and stroke width
### Requirement: Text rendering
The renderer SHALL render TEXT nodes using CanvasKit's Paragraph API with font family, weight, size, line height, and letter spacing.
#### Scenario: Text with Inter font
- **WHEN** a text node with fontName "Inter", fontSize 16, and characters "Hello" exists
- **THEN** the text is rendered at the correct position with the Inter font
### Requirement: Nested coordinate systems
The renderer SHALL apply parent transforms (position + rotation) when rendering child nodes, supporting arbitrary nesting depth.
#### Scenario: Rotated parent with child
- **WHEN** a frame is rotated 45° and contains a child rectangle
- **THEN** the child rectangle is rendered in the frame's rotated coordinate system
### Requirement: Selection outlines
The renderer SHALL draw selection outlines (blue rectangles) around selected nodes with resize handles at corners and edge midpoints.
#### Scenario: Select a node
- **WHEN** a node is selected
- **THEN** a blue outline with 8 resize handles appears around the node's bounds
### Requirement: Surface resize
The renderer SHALL recreate the Skia surface when the window is resized, maintaining correct dimensions.
#### Scenario: Window resize
- **WHEN** the browser window is resized
- **THEN** the canvas and Skia surface update to the new dimensions without artifacts
### Requirement: Silent crash prevention
The renderer SHALL detect and report rendering errors instead of silently failing.
#### Scenario: Rendering error
- **WHEN** a rendering operation encounters an error
- **THEN** the error is detected and reported rather than silently producing a blank canvas

View file

@ -0,0 +1,74 @@
# desktop-app Specification
## Purpose
Tauri v2 desktop shell. Cross-platform native menu bar with wired events, Developer Tools access, and `desktop/` directory structure for Tauri configuration and Rust source.
## Requirements
### Requirement: Tauri v2 desktop shell
The editor SHALL run as a native desktop app via Tauri v2 with the web frontend loaded in a webview.
#### Scenario: Desktop app launch
- **WHEN** user runs `bun run tauri dev`
- **THEN** a native desktop window opens with the editor UI and CanvasKit rendering
### Requirement: Native macOS menu bar
The desktop app SHALL display a native menu bar. On macOS, an app-level submenu (OpenPencil) with About, Services, Hide, Hide Others, Show All, and Quit items SHALL be shown. On Windows and Linux, this submenu SHALL be omitted. File, Edit, View, Object, Window, and Help menus SHALL be present on all platforms.
#### Scenario: Menu bar on macOS
- **WHEN** the desktop app launches on macOS
- **THEN** a native menu bar with the OpenPencil app submenu and all standard menus is visible
#### Scenario: Menu bar on Windows
- **WHEN** the desktop app launches on Windows
- **THEN** a native menu bar with File, Edit, View, Object, Window, and Help menus is visible and no macOS-specific app submenu is present
#### Scenario: Menu bar on Linux
- **WHEN** the desktop app launches on Linux
- **THEN** a native menu bar with File, Edit, View, Object, Window, and Help menus is visible and no macOS-specific app submenu is present
### Requirement: Menu events wired to frontend
Menu item clicks SHALL fire events that the Vue frontend handles (e.g., Undo, Redo, Zoom In/Out).
#### Scenario: Menu undo
- **WHEN** user clicks Edit → Undo in the menu bar
- **THEN** the frontend receives the undo event and performs undo
### Requirement: Developer tools menu item
The menu SHALL include a Developer Tools item (⌘⌥I) that opens the webview inspector.
#### Scenario: Open dev tools
- **WHEN** user selects View → Developer Tools or presses ⌘⌥I
- **THEN** the webview developer tools panel opens
### Requirement: Desktop directory structure
The Tauri configuration and Rust source SHALL live in `desktop/` (not `src-tauri/`). Tauri CLI SHALL find it by scanning.
#### Scenario: Build from desktop directory
- **WHEN** `bun run tauri build` is run
- **THEN** Tauri finds configuration in `desktop/` and produces a native binary
### Requirement: Cross-platform build
The desktop app SHALL compile and build on macOS, Windows, and Linux without platform-specific errors.
#### Scenario: Windows build
- **WHEN** `bun run tauri build` is run on Windows with MSVC toolchain installed
- **THEN** the build completes successfully and produces a Windows executable
#### Scenario: Linux build
- **WHEN** `bun run tauri build` is run on Linux with required system libraries installed
- **THEN** the build completes successfully and produces a Linux binary
### Requirement: Platform prerequisites documentation
The README SHALL document platform-specific prerequisites for building the desktop app on macOS, Windows, and Linux.
#### Scenario: Windows prerequisites documented
- **WHEN** a developer reads the README on Windows
- **THEN** they find instructions for installing Rust (stable-msvc), Visual Studio Build Tools, and WebView2
#### Scenario: macOS prerequisites documented
- **WHEN** a developer reads the README on macOS
- **THEN** they find instructions for installing Xcode Command Line Tools
#### Scenario: Linux prerequisites documented
- **WHEN** a developer reads the README on Linux
- **THEN** they find instructions for installing required system libraries (webkit2gtk, etc.)

View file

@ -0,0 +1,66 @@
# editor-ui Specification
## Purpose
Vue 3 + Reka UI editor interface. Bottom toolbar, layers panel with tree view and drag reorder, properties panel with sectioned layout (appearance, fill, stroke, typography, layout, position), color picker, and Lucide icons.
## Requirements
### Requirement: Vue 3 + Reka UI component architecture
The editor UI SHALL be built with Vue 3, VueUse composables, and Reka UI headless components. The UI framework was migrated from React to Vue 3.
#### Scenario: Editor loads
- **WHEN** user opens the editor at localhost:1420
- **THEN** the Vue 3 application renders with canvas, toolbar, layers panel, and properties panel
### Requirement: Bottom toolbar
The toolbar SHALL be positioned at the bottom of the screen (Figma UI3 style) with tool selection: Select (V), Frame (F), Rectangle (R), Ellipse (O), Line (L), Text (T), Hand (H), Pen (P).
#### Scenario: Tool selection via keyboard
- **WHEN** user presses R
- **THEN** the rectangle tool is activated and the toolbar shows R as selected
### Requirement: Layers panel with tree view
The layers panel SHALL display a tree view of the document hierarchy using Reka UI Tree with expand/collapse and drag reordering.
#### Scenario: Expand/collapse frame children
- **WHEN** user clicks the chevron next to a frame in the layers panel
- **THEN** the frame's children are shown or hidden
#### Scenario: Drag reorder layers
- **WHEN** user drags a layer to a new position in the layers panel
- **THEN** the node's z-order in the scene graph changes accordingly
#### Scenario: Visibility toggle
- **WHEN** user clicks the visibility icon next to a layer
- **THEN** the node is hidden/shown on canvas
### Requirement: Properties panel with sections
The properties panel SHALL be split into sections: Appearance (size, position, rotation, opacity, corner radius, visibility), Fill, Stroke, Typography, Layout, Position.
#### Scenario: Properties panel shows fill section
- **WHEN** user selects a rectangle with a blue fill
- **THEN** the Fill section shows the hex color #3B82F6 with opacity and visibility controls
#### Scenario: Editable hex input
- **WHEN** user types a new hex value in the fill section
- **THEN** the node's fill updates to the entered color
### Requirement: Color picker
The color picker SHALL provide HSV color selection, hue slider, alpha slider, hex input, and opacity control.
#### Scenario: Change color via HSV
- **WHEN** user moves the color picker cursor in the HSV square
- **THEN** the selected node's fill updates in real time
### Requirement: Lucide icons via Iconify
UI icons SHALL use Lucide icons loaded via unplugin-icons with Iconify, replacing inline emoji/SVG.
#### Scenario: Icon rendering
- **WHEN** the toolbar renders tool icons
- **THEN** Lucide icons are displayed via the icon auto-import resolver
### Requirement: Tailwind CSS 4 styling
The editor SHALL use Tailwind CSS 4 for all styling.
#### Scenario: Dark theme
- **WHEN** the editor renders
- **THEN** the UI uses a dark theme styled with Tailwind CSS 4 utility classes

View file

@ -0,0 +1,26 @@
# fig-import Specification
## Purpose
.fig file import pipeline. Parses Figma file header, decompresses Zstd payload, decodes Kiwi schema, extracts NodeChange[], resolves blob references, and populates the scene graph.
## Requirements
### Requirement: .fig file import
The editor SHALL import .fig files via the Kiwi codec pipeline: parse header (magic "fig-kiwi" + version), decompress Zstd, decode Kiwi schema, extract NodeChange[], and build the scene graph.
#### Scenario: Import a .fig file
- **WHEN** user opens a .fig file
- **THEN** all nodes from the file appear on the canvas with correct types, positions, sizes, and visual properties
### Requirement: File open via keyboard
⌘O SHALL open a file dialog for .fig import.
#### Scenario: Open file dialog
- **WHEN** user presses ⌘O
- **THEN** a file picker dialog appears filtered for .fig files
### Requirement: Blob reference resolution
The import pipeline SHALL resolve blob references (images, vector networks, font data) from the .fig file's blob section.
#### Scenario: Import with vector blobs
- **WHEN** a .fig file containing vector nodes with vectorNetworkBlob data is imported
- **THEN** the vector paths are correctly decoded and renderable

View file

@ -0,0 +1,40 @@
# figma-clipboard Specification
## Purpose
Bidirectional clipboard compatibility with Figma. Decodes fig-kiwi binary on paste, encodes fig-kiwi binary on copy, supports cut, and works across OpenPencil instances.
## Requirements
### Requirement: Paste from Figma
The editor SHALL accept paste events containing fig-kiwi binary data from Figma's clipboard. Pasted nodes SHALL be decoded and added to the scene graph.
#### Scenario: Paste Figma nodes
- **WHEN** user copies a frame in Figma and pastes in OpenPencil
- **THEN** the frame and its children appear on the canvas with preserved properties
### Requirement: Copy for Figma compatibility
The editor SHALL encode selected nodes as fig-kiwi binary on copy, allowing paste into Figma or other OpenPencil instances.
#### Scenario: Copy to Figma
- **WHEN** user copies a rectangle in OpenPencil and pastes in Figma
- **THEN** the rectangle appears in Figma with preserved fill, stroke, and dimensions
### Requirement: Native copy/paste events
Clipboard operations SHALL use native copy/paste browser events (not async Clipboard API) and build the fig-kiwi binary synchronously in the event handler.
#### Scenario: Synchronous clipboard encoding
- **WHEN** user presses ⌘C with a node selected
- **THEN** the fig-kiwi binary is built synchronously within the copy event handler
### Requirement: Cut operation
Cut SHALL copy nodes to clipboard and delete them from the scene graph.
#### Scenario: Cut and paste
- **WHEN** user cuts a node and pastes
- **THEN** the original is removed and a copy appears at the paste position
### Requirement: Copy between instances
Copy/paste SHALL work between multiple OpenPencil browser tabs/windows.
#### Scenario: Cross-instance paste
- **WHEN** user copies a node in one OpenPencil tab and pastes in another
- **THEN** the node appears in the second tab with all properties preserved

View file

@ -0,0 +1,33 @@
# group-ungroup Specification
## Purpose
Group and ungroup node operations (⌘G/⇧⌘G). Creates GROUP nodes from selection, ungroups by reparenting children, sorts by visual position, and handles null node edge cases.
## Requirements
### Requirement: Group selection
⌘G SHALL group the current selection into a GROUP node. The group inherits the bounding box of its children.
#### Scenario: Group three nodes
- **WHEN** user selects three shapes and presses ⌘G
- **THEN** a GROUP node is created containing the three shapes as children
### Requirement: Ungroup selection
⇧⌘G SHALL ungroup the selected group, moving its children to the group's parent at the group's position.
#### Scenario: Ungroup
- **WHEN** user selects a group and presses ⇧⌘G
- **THEN** the group is removed and its children become siblings of the group's former parent
### Requirement: Position-based sort on wrap
When grouping or wrapping in auto-layout, nodes SHALL be sorted by their visual position to maintain reading order.
#### Scenario: Group preserves visual order
- **WHEN** three nodes at positions X=300, X=100, X=200 are grouped
- **THEN** the children are ordered by position (X=100 first, X=300 last)
### Requirement: Null node crash prevention
Group/ungroup operations SHALL handle null node references gracefully without crashing.
#### Scenario: Null node in selection
- **WHEN** a group operation encounters a null node reference in the selection
- **THEN** the operation completes without crashing, skipping the null reference

View file

@ -0,0 +1,37 @@
# kiwi-codec Specification
## Purpose
Vendored Kiwi binary codec for Figma's 194-definition schema. Encodes/decodes NodeChange messages, handles sparse field IDs, and integrates Zstd compression for .fig file compatibility.
## Requirements
### Requirement: Vendored kiwi-schema
The project SHALL vendor kiwi-schema (from evanw/kiwi) with patches for ESM module format and sparse field ID handling.
#### Scenario: ESM import
- **WHEN** kiwi-schema is imported as an ES module
- **THEN** all encoder/decoder functions are available without CommonJS compatibility issues
### Requirement: Kiwi binary codec
The codec SHALL encode and decode Figma's 194-definition Kiwi schema including the NodeChange message type with ~390 fields.
#### Scenario: Decode a Kiwi message
- **WHEN** a compressed Kiwi binary is provided
- **THEN** the codec decodes it into a structured NodeChange[] array
#### Scenario: Encode a Kiwi message
- **WHEN** a NodeChange[] array is provided
- **THEN** the codec encodes it into Kiwi binary format
### Requirement: Sparse field ID support
The kiwi-schema parser SHALL handle sparse field IDs (non-contiguous field numbering) used by Figma's schema.
#### Scenario: Parse schema with sparse IDs
- **WHEN** a Kiwi schema definition has field IDs like 1, 2, 5, 10 (with gaps)
- **THEN** the parser correctly handles all fields without errors
### Requirement: Zstd compression
The codec layer SHALL support Zstd decompression for .fig file payloads and compression for clipboard/export.
#### Scenario: Decompress .fig payload
- **WHEN** a Zstd-compressed Kiwi payload is encountered in a .fig file
- **THEN** it is decompressed before Kiwi decoding

View file

@ -0,0 +1,52 @@
# pen-tool Specification
## Purpose
Pen tool for vector path creation using a vector network data model. Supports bezier curves with tangent handles, open/closed paths, and Figma-compatible vectorNetworkBlob binary encoding.
## Requirements
### Requirement: Pen tool with vector network model
Pressing P SHALL activate the pen tool. Clicking places corner points, click+drag places curve points with bezier handles. The underlying data model SHALL use vector networks (not simple paths).
#### Scenario: Place corner points
- **WHEN** user clicks three positions with the pen tool
- **THEN** a path with three straight-line segments is created
#### Scenario: Place curve point
- **WHEN** user clicks and drags with the pen tool
- **THEN** a bezier curve point is created with tangent handles matching the drag direction and length
### Requirement: Open and closed paths
Clicking the first point SHALL close the path. Pressing Escape SHALL commit an open path.
#### Scenario: Close path
- **WHEN** user clicks on the first point of an in-progress path
- **THEN** the path closes into a loop
#### Scenario: Open path
- **WHEN** user presses Escape during pen tool drawing
- **THEN** the current path is committed as an open path
### Requirement: Vector network blob encoding
Vector network data SHALL be encoded/decoded using the Figma vectorNetworkBlob binary format for .fig compatibility.
#### Scenario: Encode vector data
- **WHEN** a vector node is created with the pen tool
- **THEN** the vector data can be encoded to the binary vectorNetworkBlob format
#### Scenario: Decode imported vector data
- **WHEN** a .fig file containing vector nodes is imported
- **THEN** the vectorNetworkBlob is decoded into editable vector network data
### Requirement: Open path rendering
Open paths SHALL be rendered as open strokes, not closed and filled.
#### Scenario: Open path stroke
- **WHEN** an open vector path has a stroke applied
- **THEN** the path is rendered as an open stroke line, not as a closed filled shape
### Requirement: Preview line
During pen tool drawing, a preview line SHALL connect the last placed point to the current cursor position. The preview SHALL NOT start from (0,0).
#### Scenario: Preview line from last point
- **WHEN** user has placed one point and moves the cursor
- **THEN** a preview line extends from the last placed point to the cursor position

View file

@ -0,0 +1,47 @@
# rulers Specification
## Purpose
Canvas rulers at top and left edges showing coordinate scales. Displays selection highlight bands, coordinate badges at selection bounds, and can be disabled via URL parameter.
## Requirements
### Requirement: Canvas rulers
Horizontal and vertical rulers SHALL be displayed at the top and left edges of the canvas, showing coordinate scales that update with pan/zoom.
#### Scenario: Ruler display
- **WHEN** the editor loads
- **THEN** horizontal and vertical rulers are visible with tick marks and coordinate labels
### Requirement: Selection highlight on rulers
Rulers SHALL display a highlight band at the position/size of the current selection.
#### Scenario: Selection ruler highlight
- **WHEN** a 200px wide node at X=100 is selected
- **THEN** the horizontal ruler shows a translucent highlight band from 100 to 300
### Requirement: Coordinate badges
Rulers SHALL display coordinate badges at the selection's start and end positions.
#### Scenario: Coordinate badges
- **WHEN** a node spanning X 100-300, Y 50-200 is selected
- **THEN** coordinate badges show 100 and 300 on the horizontal ruler, 50 and 200 on the vertical ruler
### Requirement: Consistent text alignment
Ruler tick labels and badge text SHALL align to the same baseline for visual consistency.
#### Scenario: Label alignment
- **WHEN** rulers render tick labels and coordinate badges
- **THEN** text baselines are aligned consistently across ticks and badges
### Requirement: Vertical ruler text clipping
Vertical ruler text SHALL NOT clip or overflow outside the ruler bounds.
#### Scenario: Vertical ruler overflow
- **WHEN** the vertical ruler displays coordinate labels
- **THEN** no text is clipped or extends beyond the ruler's visible area
### Requirement: Rulers can be disabled
The URL parameter `?no-rulers` SHALL disable canvas rulers for testing or presentation.
#### Scenario: Disable rulers via URL
- **WHEN** the editor loads with `?no-rulers` in the URL
- **THEN** rulers are not rendered

View file

@ -0,0 +1,55 @@
# scene-graph Specification
## Purpose
Hierarchical data structure managing all design nodes. Provides flat Map<GUID, Node> storage with parent-child tree via parentIndex, CRUD operations, hit testing, and spatial queries.
## Requirements
### Requirement: Flat node storage with GUID keys
The scene graph SHALL store all nodes in a flat `Map<string, Node>` keyed by GUID string. Tree structure SHALL be maintained via `parentIndex` references on each node.
#### Scenario: Create and retrieve a node
- **WHEN** a node is created with type RECTANGLE and a parent GUID
- **THEN** the node is stored in the map and retrievable by its GUID
#### Scenario: Parent-child relationship
- **WHEN** a child node references a parent via parentIndex
- **THEN** `getChildren(parentId)` returns the child sorted by position string
### Requirement: Node types
The scene graph SHALL support node types: FRAME, RECTANGLE, ELLIPSE, LINE, TEXT, VECTOR, STAR, POLYGON, GROUP, BOOLEAN_OPERATION, DOCUMENT, CANVAS.
#### Scenario: Create each supported node type
- **WHEN** a node of each supported type is created
- **THEN** the node is stored with the correct type enum value
### Requirement: CRUD operations
The scene graph SHALL support create, read, update, and delete operations on nodes.
#### Scenario: Delete a node
- **WHEN** a node is deleted by GUID
- **THEN** the node is removed from the map and no longer returned by queries
#### Scenario: Update node properties
- **WHEN** a node's properties (size, fill, rotation) are updated
- **THEN** subsequent reads return the updated values
### Requirement: Hit testing
The scene graph SHALL support hit testing — given a point in canvas coordinates, return the topmost visible node at that position.
#### Scenario: Hit test on overlapping nodes
- **WHEN** two rectangles overlap and a point inside the overlap area is tested
- **THEN** the node with higher z-order (later in position string sorting) is returned
### Requirement: Rectangular area query
The scene graph SHALL support querying all nodes that intersect a given rectangle (for marquee selection).
#### Scenario: Marquee selection
- **WHEN** a rectangle area query is performed
- **THEN** all nodes whose bounds intersect the rectangle are returned
### Requirement: Unit test coverage
The scene graph SHALL have bun:test unit tests covering CRUD, parent-child, z-ordering, and hit testing.
#### Scenario: Unit tests pass
- **WHEN** `bun test ./tests/engine` is run
- **THEN** all scene graph unit tests pass in <50ms

View file

@ -0,0 +1,40 @@
# scrub-input Specification
## Purpose
Drag-to-scrub numeric input component. Replaces plain number inputs across all property panel sections with drag-to-adjust, click-to-edit, and suffix display capabilities.
## Requirements
### Requirement: Drag-to-scrub numeric input
The ScrubInput component SHALL allow users to drag anywhere on the input field to scrub (adjust) the numeric value. The entire field area SHALL be draggable.
#### Scenario: Drag to increase value
- **WHEN** user clicks and drags right on a ScrubInput with value 100
- **THEN** the value increases proportionally to the drag distance
### Requirement: Click-to-edit mode
Clicking (without dragging) a ScrubInput SHALL enter direct text editing mode where the user can type a numeric value.
#### Scenario: Click to type
- **WHEN** user clicks a ScrubInput without dragging
- **THEN** the input becomes editable and accepts keyboard input
### Requirement: Suffix styling
ScrubInput SHALL support a visual suffix (e.g., "°", "px", "%") displayed after the value.
#### Scenario: Display suffix
- **WHEN** a ScrubInput has suffix "°" and value 45
- **THEN** the input displays "45°"
### Requirement: Used across all numeric properties
ScrubInput SHALL replace plain number inputs in all property panel sections (position, size, rotation, opacity, corner radius, gap, padding, font size, stroke weight).
#### Scenario: Position input uses ScrubInput
- **WHEN** the Position section renders X and Y inputs
- **THEN** they are ScrubInput components with drag-to-scrub behavior
### Requirement: Scrub label hit area
The clickable/draggable area of the scrub label SHALL be properly sized to avoid missing clicks.
#### Scenario: Reliable click target
- **WHEN** user clicks on the scrub label text
- **THEN** the click is registered and scrub mode activates

View file

@ -0,0 +1,82 @@
# selection-manipulation Specification
## Purpose
Node selection and direct manipulation. Click/shift/marquee select, move by drag, resize handles, rotation with snapping, duplicate, nudge, delete, and select-all.
## Requirements
### Requirement: Click to select
Clicking on a node SHALL select it (deselecting all others). Clicking empty canvas SHALL deselect all.
#### Scenario: Select a single node
- **WHEN** user clicks on a rectangle
- **THEN** the rectangle is selected and all other nodes are deselected
### Requirement: Multi-select with shift
Shift+clicking a node SHALL toggle it in the current selection.
#### Scenario: Add to selection
- **WHEN** user shift+clicks a second node while one is already selected
- **THEN** both nodes are in the selection
### Requirement: Marquee selection
Dragging on empty canvas SHALL create a selection rectangle. All nodes intersecting the rectangle SHALL be selected on release.
#### Scenario: Marquee drag
- **WHEN** user drags from empty canvas across three nodes
- **THEN** all three nodes are selected when the drag ends
### Requirement: Move by dragging
Dragging a selected node SHALL move it (and all other selected nodes) by the drag delta.
#### Scenario: Drag to move
- **WHEN** user drags a selected node by 50px right
- **THEN** the node's X position increases by 50
### Requirement: Resize handles
Selected nodes SHALL display 8 resize handles (4 corners + 4 edge midpoints). Dragging a handle SHALL resize the node. Shift constrains proportions.
#### Scenario: Resize from corner
- **WHEN** user drags the bottom-right corner handle outward
- **THEN** the node's width and height increase
### Requirement: Rotation
Hovering just outside a corner handle SHALL show a rotation cursor. Dragging SHALL rotate the node. Shift SHALL snap to 15° increments.
#### Scenario: Rotate with snap
- **WHEN** user drags the rotation handle while holding shift
- **THEN** the rotation snaps to the nearest 15° increment
### Requirement: Duplicate
Alt+drag SHALL duplicate the selected node and move the duplicate. ⌘D SHALL duplicate in place.
#### Scenario: Alt+drag duplicate
- **WHEN** user alt+drags a selected rectangle
- **THEN** a copy of the rectangle is created at the drag position
### Requirement: Constrained drawing
Holding shift while drawing a shape SHALL constrain to square/circle.
#### Scenario: Shift+drag rectangle
- **WHEN** user draws a rectangle while holding shift
- **THEN** the rectangle is constrained to a square (equal width and height)
### Requirement: Nudge with arrows
Arrow keys SHALL move selected nodes by 1px. Shift+arrow SHALL move by 10px.
#### Scenario: Arrow nudge
- **WHEN** user presses the right arrow key with a node selected
- **THEN** the node moves 1px to the right
### Requirement: Delete selection
Backspace/Delete SHALL remove all selected nodes.
#### Scenario: Delete node
- **WHEN** user presses backspace with a node selected
- **THEN** the node is removed from the scene graph
### Requirement: Select all
⌘A SHALL select all nodes on the current canvas.
#### Scenario: Select all
- **WHEN** user presses ⌘A
- **THEN** all nodes on the canvas are selected

View file

@ -0,0 +1,30 @@
# snap-guides Specification
## Purpose
Smart alignment guides during node manipulation. Edge and center snapping with rotation-aware bounding boxes, using sibling nodes in absolute canvas coordinates.
## Requirements
### Requirement: Edge and center snapping
When moving or resizing nodes, snap guides SHALL appear when edges or centers align with sibling nodes (±1px threshold).
#### Scenario: Horizontal edge snap
- **WHEN** user drags a node and its left edge aligns with another node's left edge
- **THEN** a red vertical snap guide line appears at the aligned X coordinate
#### Scenario: Center snap
- **WHEN** user drags a node and its horizontal center aligns with another node's center
- **THEN** a red snap guide line appears at the center coordinate
### Requirement: Rotation-aware snap guides
Snap guides SHALL use rotation-aware bounding boxes. Snap calculations SHALL work correctly for rotated nodes.
#### Scenario: Snap rotated node
- **WHEN** a rotated node is moved near an unrotated node
- **THEN** snap guides align to the rotated node's actual visual bounds
### Requirement: Sibling-based absolute coordinates
Snap guide calculations SHALL use sibling nodes in absolute (canvas) coordinates, not parent-relative coordinates.
#### Scenario: Snap across nesting levels
- **WHEN** a node inside a frame is moved near a node outside the frame
- **THEN** snap guides correctly compare positions in absolute canvas coordinates

View file

@ -0,0 +1,47 @@
# testing Specification
## Purpose
Test infrastructure. Playwright visual regression (E2E with Figma CDP reference), bun:test unit tests, no-chrome test mode, data-ready synchronization, and page reuse optimization.
## Requirements
### Requirement: Playwright visual regression testing
E2E tests SHALL use Playwright to create shapes and compare screenshots against baseline snapshots.
#### Scenario: Visual regression test pass
- **WHEN** `bun run test` is executed
- **THEN** Playwright tests create shapes, take screenshots, and compare against baseline PNGs
### Requirement: Figma CDP reference tests
A separate Playwright project SHALL connect to Figma via Chrome DevTools Protocol (CDP) to capture reference screenshots for pixel-perfect comparison.
#### Scenario: Figma reference capture
- **WHEN** `bun run test:figma` is executed with Figma running in debug mode
- **THEN** Playwright connects to Figma via CDP and captures reference screenshots
### Requirement: No-chrome test mode
The editor SHALL support a test mode (activated via URL param or environment) that hides Chrome/UI elements for clean screenshot capture.
#### Scenario: Test mode rendering
- **WHEN** the editor loads in test mode
- **THEN** only canvas content is rendered, without toolbar, panels, or other UI chrome
### Requirement: Data-ready optimization
The editor SHALL signal readiness via a `data-ready` attribute for E2E test synchronization, replacing unreliable timeouts.
#### Scenario: Wait for ready
- **WHEN** a Playwright test waits for the data-ready attribute
- **THEN** it proceeds only when the canvas is fully rendered
### Requirement: Page reuse for speed
E2E tests SHALL reuse the browser page across test cases to minimize overhead. Target: all E2E tests complete in <3s.
#### Scenario: Fast E2E execution
- **WHEN** the full E2E test suite runs
- **THEN** it completes in under 3 seconds by reusing the page
### Requirement: bun:test unit tests
Engine unit tests SHALL use bun:test and complete in <50ms.
#### Scenario: Unit test speed
- **WHEN** `bun test ./tests/engine` is run
- **THEN** all unit tests pass in under 50ms

View file

@ -0,0 +1,51 @@
# text-editing Specification
## Purpose
Text creation and inline editing. Text tool (T), CanvasKit Paragraph API rendering, textarea overlay for editing, font loading (Inter default + system fonts via Local Font Access API), font weight support.
## Requirements
### Requirement: Text tool creates text nodes
Pressing T SHALL activate the text tool. Clicking on canvas SHALL create a new empty text node with cursor blinking.
#### Scenario: Create text node
- **WHEN** user presses T and clicks on the canvas
- **THEN** an empty text node is created at the click position with a visible cursor
### Requirement: Inline text editing
Double-clicking a text node SHALL enter inline editing mode with a textarea overlay positioned over the canvas text. The canvas text and selection SHALL be hidden during editing.
#### Scenario: Enter edit mode
- **WHEN** user double-clicks a text node
- **THEN** a textarea overlay appears and the canvas-rendered text is hidden
### Requirement: CanvasKit Paragraph API rendering
Text nodes SHALL be rendered using CanvasKit's Paragraph API for proper text shaping, line breaking, and font metrics.
#### Scenario: Multi-line text wrapping
- **WHEN** a text node with a fixed width contains text longer than the width
- **THEN** the text wraps to multiple lines using Paragraph API layout
### Requirement: Font loading
The editor SHALL load Inter as the default font. The Local Font Access API SHALL be used to enumerate and load system fonts when available.
#### Scenario: Default font
- **WHEN** a text node is created without specifying a font
- **THEN** the Inter font is used for rendering
#### Scenario: System fonts
- **WHEN** the Local Font Access API is available
- **THEN** system fonts are enumerable in the font picker
### Requirement: Font weight support
Font weight SHALL be applied to both the CanvasKit text style and the textarea overlay during editing.
#### Scenario: Bold text rendering
- **WHEN** a text node has fontWeight 700
- **THEN** both the canvas rendering and the editing overlay display bold text
### Requirement: Blue editing outline
A blue outline SHALL appear around a text node during editing to indicate edit mode.
#### Scenario: Edit mode indicator
- **WHEN** user is editing a text node inline
- **THEN** a blue outline is visible around the text node bounds

View file

@ -0,0 +1,59 @@
# tooling Specification
## Purpose
Build and development tooling. Vite 7 build system, oxlint linting, oxfmt formatting, typescript-go type checking, and Tailwind CSS 4 integration.
## Requirements
### Requirement: Vite 7 build system
The project SHALL use Vite 7 as the build tool with dev server at port 1420 and HMR support. VitePress SHALL be installed as a devDependency for the documentation site. The `docs:dev`, `docs:build`, and `docs:preview` scripts SHALL be added to package.json.
#### Scenario: Dev server
- **WHEN** `bun run dev` is executed
- **THEN** Vite dev server starts at http://localhost:1420 with hot module replacement
#### Scenario: Docs scripts available
- **WHEN** `bun run docs:dev` is executed
- **THEN** VitePress dev server starts for the documentation site
#### Scenario: Docs build
- **WHEN** `bun run docs:build` is executed
- **THEN** VitePress builds the documentation site to `docs/.vitepress/dist/`
#### Scenario: Docs preview
- **WHEN** `bun run docs:preview` is executed
- **THEN** a static server previews the built documentation site
### Requirement: oxlint linting
The project SHALL use oxlint for fast linting with project-specific configuration in oxlint.json.
#### Scenario: Lint check
- **WHEN** `bun run lint` is executed
- **THEN** oxlint checks src/ for issues based on oxlint.json rules
### Requirement: oxfmt formatting
The project SHALL use oxfmt for code formatting with configuration in .oxfmtrc.json.
#### Scenario: Format code
- **WHEN** `bun run format` is executed
- **THEN** oxfmt formats all files in src/ according to project rules
### Requirement: typescript-go type checking
The project SHALL use typescript-go (tsgo) for type checking via `bun run typecheck`.
#### Scenario: Type check
- **WHEN** `bun run typecheck` is executed
- **THEN** tsgo --noEmit checks all TypeScript files for type errors
### Requirement: Combined check command
`bun run check` SHALL run both lint and typecheck sequentially.
#### Scenario: Full check
- **WHEN** `bun run check` is executed
- **THEN** oxlint runs first, then tsgo, and both must pass
### Requirement: Tailwind CSS 4 integration
Tailwind CSS 4 SHALL be integrated via @tailwindcss/vite plugin.
#### Scenario: Tailwind classes work
- **WHEN** a Vue component uses Tailwind utility classes
- **THEN** the corresponding CSS is generated and applied

View file

@ -0,0 +1,27 @@
# undo-redo Specification
## Purpose
Inverse-command undo/redo system. Records the inverse of every operation, enabling ⌘Z/⇧⌘Z across all editor actions (create, delete, move, resize, rotate, property changes).
## Requirements
### Requirement: Inverse-command undo
The undo system SHALL use an inverse-command pattern where every operation records its inverse. ⌘Z SHALL undo and ⇧⌘Z SHALL redo.
#### Scenario: Undo a move
- **WHEN** user moves a node and presses ⌘Z
- **THEN** the node returns to its original position
#### Scenario: Redo after undo
- **WHEN** user undoes a move and presses ⇧⌘Z
- **THEN** the node moves back to the position after the original move
### Requirement: All operations undoable
Undo/redo SHALL be wired into all editor operations: create, delete, move, resize, rotate, property changes, reparent, layout changes.
#### Scenario: Undo node creation
- **WHEN** user creates a rectangle and presses ⌘Z
- **THEN** the rectangle is removed from the scene graph
#### Scenario: Undo property change
- **WHEN** user changes a node's fill color from blue to red and presses ⌘Z
- **THEN** the fill color reverts to blue

View file

@ -0,0 +1,73 @@
# vitepress-docs Specification
## Purpose
VitePress documentation site at `docs/` with content derived from PLAN.md, README, and openspec specs. Includes guide pages (getting started, architecture, tech stack), reference pages (keyboard shortcuts, node types, MCP tools), and development pages (contributing, testing, openspec workflow, roadmap).
## Requirements
### Requirement: VitePress documentation site
The project SHALL have a VitePress documentation site in the `docs/` directory with its own `.vitepress/config.ts` configuration, independent from the app's Vite config.
#### Scenario: Docs dev server starts
- **WHEN** `bun run docs:dev` is executed
- **THEN** VitePress dev server starts and serves the documentation site
#### Scenario: Docs build succeeds
- **WHEN** `bun run docs:build` is executed
- **THEN** VitePress produces a static site in `docs/.vitepress/dist/`
### Requirement: Landing page
The docs site SHALL have an index.md landing page with project name, tagline, and quick navigation to guide and reference sections.
#### Scenario: Landing page loads
- **WHEN** user opens the docs site root URL
- **THEN** a landing page with "OpenPencil" title and feature highlights is displayed
### Requirement: Guide section
The docs site SHALL include a guide section with pages: Getting Started, Features, Architecture, Tech Stack.
#### Scenario: Getting started page
- **WHEN** user navigates to the Getting Started guide
- **THEN** installation instructions (`bun install`, `bun run dev`) and desktop app setup are displayed
#### Scenario: Architecture page
- **WHEN** user navigates to the Architecture guide
- **THEN** the system architecture diagram and component descriptions from PLAN.md are displayed
### Requirement: Reference section
The docs site SHALL include a reference section with pages: Keyboard Shortcuts, Node Types, MCP Tools, Scene Graph, File Format.
#### Scenario: Keyboard shortcuts page
- **WHEN** user navigates to Keyboard Shortcuts reference
- **THEN** the full shortcut table from PLAN.md is displayed with implementation status
#### Scenario: MCP tools page
- **WHEN** user navigates to MCP Tools reference
- **THEN** all 117 MCP tools are listed grouped by category
### Requirement: Development section
The docs site SHALL include a development section with pages: Contributing, Testing, OpenSpec Workflow, Roadmap.
#### Scenario: Roadmap page
- **WHEN** user navigates to the Roadmap development page
- **THEN** the 6 phases from PLAN.md are listed with current progress
### Requirement: Sidebar navigation
The VitePress config SHALL define a sidebar with logical grouping: Guide, Reference, Development.
#### Scenario: Sidebar groups
- **WHEN** user browses any documentation page
- **THEN** a sidebar shows three collapsible groups: Guide, Reference, Development
### Requirement: Dark theme
The docs site SHALL use dark appearance to match the editor's dark aesthetic.
#### Scenario: Dark mode
- **WHEN** user opens the documentation site
- **THEN** the site renders with VitePress dark theme
### Requirement: Build artifacts excluded from git
`docs/.vitepress/dist` and `docs/.vitepress/cache` SHALL be listed in `.gitignore`.
#### Scenario: Gitignore entries
- **WHEN** `bun run docs:build` creates output in `docs/.vitepress/dist/`
- **THEN** the output directory is not tracked by git

View file

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "vite build",
"preview": "vite preview",
"tauri": "tauri",
"lint": "oxlint -c oxlint.json src/",
@ -16,7 +16,10 @@
"test:update": "playwright test --project=openpencil --update-snapshots",
"test:figma": "playwright test --project=figma",
"figma:debug": "open -a Figma --args --remote-debugging-port=9222",
"test:unit": "bun test ./tests/engine"
"test:unit": "bun test ./tests/engine",
"docs:dev": "vitepress dev docs",
"docs:build": "vitepress build docs",
"docs:preview": "vitepress preview docs"
},
"dependencies": {
"@tailwindcss/vite": "^4.2.1",
@ -47,6 +50,7 @@
"typescript": "~5.8.3",
"unplugin-icons": "^23.0.1",
"unplugin-vue-components": "^31.0.0",
"vite": "^7.0.4"
"vite": "^7.0.4",
"vitepress": "^1.6.4"
}
}