diff --git a/PLAN.md b/PLAN.md index d4c1230a9..e6071ae19 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1368,13 +1368,18 @@ interface CommentPin { #### Implementation order -1. JSX renderer in `@open-pencil/core` (port from figma-use) -2. Chat panel UI — `ChatPanel.vue`, `useChat()`, message list, tool timeline, `⌘J` toggle -3. API key settings — input UI, Stronghold storage (Tauri) / localStorage (browser) -4. Core tools — render, set_fill, set_layout, set_text, snapshot, get_selection -5. Screenshot + diff tools -6. Comment pin system -7. Full tool set (remaining 118 tools) +1. ✅ JSX renderer in `@open-pencil/core` (port from figma-use) +2. ✅ Chat panel UI — `ChatPanel.vue`, `useChat()`, message list, tool timeline, `⌘J` toggle +3. Code panel — "Code" tab in properties panel, JSX export with syntax highlighting + - `sceneNodeToJsx()` in `packages/core/src/render/export-jsx.ts` (reverse of renderer.ts) + - `CodePanel.vue` with Prism.js highlighting, copy button, reka-ui ScrollArea + - Third tab in PropertiesPanel: Design | Code | AI + - Subtree export: full JSX tree for selection, multi-selection separated by blank lines +4. API key settings — input UI, Stronghold storage (Tauri) / localStorage (browser) +5. Core tools — render, set_fill, set_layout, set_text, snapshot, get_selection +6. Screenshot + diff tools +7. Comment pin system +8. Full tool set (remaining 118 tools) #### Validation diff --git a/bun.lock b/bun.lock index 61a932e63..0918d8886 100644 --- a/bun.lock +++ b/bun.lock @@ -5,8 +5,12 @@ "": { "name": "open-pencil-app", "dependencies": { + "@ai-sdk/openai": "^3.0.37", + "@ai-sdk/valibot": "^2.0.17", + "@ai-sdk/vue": "^3.0.105", "@open-pencil/cli": "workspace:*", "@open-pencil/core": "workspace:*", + "@openrouter/ai-sdk-provider": "^2.2.3", "@tailwindcss/vite": "^4.2.1", "@tanstack/vue-table": "^8.21.3", "@tauri-apps/api": "^2", @@ -14,13 +18,18 @@ "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", "@vueuse/core": "^14.2.1", + "ai": "^6.0.105", "canvaskit-wasm": "^0.40.0", "culori": "^4.0.2", + "dedent": "^1.7.1", "fflate": "^0.8.2", "fzstd": "^0.1.1", + "prismjs": "^1.30.0", "reka-ui": "^2.8.2", "tailwindcss": "^4.2.1", + "valibot": "^1.2.0", "vue": "^3.5.29", + "vue-stream-markdown": "^0.6.0", "yoga-layout": "^3.2.1", }, "devDependencies": { @@ -29,12 +38,14 @@ "@tauri-apps/cli": "^2", "@types/culori": "^4.0.1", "@types/node": "^25.3.2", + "@types/prismjs": "^1.26.6", "@typescript/native-preview": "^7.0.0-dev.20260228.1", "@vitejs/plugin-vue": "^6.0.4", "esbuild": "^0.27.3", "jscpd": "^4.0.8", "oxfmt": "^0.35.0", "oxlint": "^1.50.0", + "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", "unplugin-icons": "^23.0.1", "unplugin-vue-components": "^31.0.0", @@ -75,6 +86,18 @@ }, }, "packages": { + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.59", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-MbtheWHgEFV/8HL1Z6E3hOAsmP73zZlNFg0F0nJAD0Adnjp4J/plqNK00Y896d+dWTw+r0OXzyov9/2wCFjH0Q=="], + + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.37", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bcYjT3/58i/C0DN3AnrjiGsAb0kYivZLWWUtgTjsBurHSht/LTEy+w3dw5XQe3FmZwX7Z/mUQCiA3wB/5Kf7ow=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.16", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-kBvDqNkt5EwlzF9FujmNhhtl8FYg3e8FO8P5uneKliqfRThWemzBj+wfYr7ZCymAQhTRnwSSz1/SOqhOAwmx9g=="], + + "@ai-sdk/valibot": ["@ai-sdk/valibot@2.0.17", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.16" }, "peerDependencies": { "@valibot/to-json-schema": "^1.3.0", "valibot": "^1.1.0" } }, "sha512-4/kEvGE7JaDGPIMVyrEq1by483kiSpbzhsHoQfi3K5ScbEs1dHXdnWr8P9eGyFBHyQi0uNPTqjnQGa2K4FSWsg=="], + + "@ai-sdk/vue": ["@ai-sdk/vue@3.0.105", "", { "dependencies": { "@ai-sdk/provider-utils": "4.0.16", "ai": "6.0.105", "swrv": "^1.0.4" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-qdf4G6p8lubBrDKw9u6gG7KJft+J7A/xqk0WRsWw+KAJkG+TgziaK4a+/5amuhOQnvnQL/NFlmQHfYnHzM2KMA=="], + "@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=="], @@ -231,6 +254,10 @@ "@open-pencil/core": ["@open-pencil/core@workspace:packages/core"], + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@2.2.3", "", { "peerDependencies": { "ai": "^6.0.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-NovC+BaCfEeJwhToDrs8JeDYXXlJdEyz7lcxkjtyePSE4eoAKik872SyDK0MzXKcz8MRkv7XlNhPI6zz4TQp0g=="], + + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + "@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.35.0", "", { "os": "android", "cpu": "arm" }, "sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA=="], "@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.35.0", "", { "os": "android", "cpu": "arm64" }, "sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ=="], @@ -377,6 +404,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@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=="], @@ -453,10 +482,14 @@ "@types/culori": ["@types/culori@4.0.1", "", {}, "sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@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/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + "@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=="], @@ -465,8 +498,12 @@ "@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="], + "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="], + "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -491,6 +528,10 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@valibot/to-json-schema": ["@valibot/to-json-schema@1.5.0", "", { "peerDependencies": { "valibot": "^1.2.0" } }, "sha512-GE7DmSr1C2UCWPiV0upRH6mv0cCPsqYGs819fb6srCS1tWhyXrkGGe+zxUiwzn/L1BOfADH4sNjY/YHCuP8phQ=="], + + "@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@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=="], @@ -531,6 +572,8 @@ "agentfmt": ["agentfmt@0.1.3", "", { "dependencies": { "picocolors": "^1.1.1" } }, "sha512-BVSZr8ovigxjYewuBx42Ep4RJj00HURaQYUQ+id41sgb7r0PMUSEWsnD/+h96xj6t6v2WVyPuGPH0K6RtjftKQ=="], + "ai": ["ai@6.0.105", "", { "dependencies": { "@ai-sdk/gateway": "3.0.59", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.16", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rp+exWtZS3J0DDvZIfetpKCIg7D3cCsvBPoFN3I67IDTs9aoBZDbpecoIkmNLT+U9RBkoEial3OGHRvme23HCw=="], + "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=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -563,6 +606,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "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=="], @@ -595,6 +640,12 @@ "culori": ["culori@4.0.2", "", {}, "sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="], + + "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], @@ -625,10 +676,14 @@ "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + "execa": ["execa@4.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "human-signals": "^1.1.1", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA=="], "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], @@ -637,6 +692,8 @@ "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], @@ -645,6 +702,8 @@ "focus-trap": ["focus-trap@7.8.0", "", { "dependencies": { "tabbable": "^6.4.0" } }, "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA=="], + "format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="], + "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], @@ -653,6 +712,8 @@ "fzstd": ["fzstd@0.1.1", "", {}, "sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], @@ -713,10 +774,14 @@ "jscpd-sarif-reporter": ["jscpd-sarif-reporter@4.0.6", "", { "dependencies": { "colors": "^1.4.0", "fs-extra": "^11.2.0", "node-sarif-builder": "^3.4.0" } }, "sha512-b9Sm3IPZ3+m8Lwa4gZa+4/LhDhlc/ZLEsLXKSOy1DANQ6kx0ueqZT+fUHWEdQ6m0o3+RIVIa7DmvLSojQD05ng=="], + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], "jstransformer": ["jstransformer@1.0.0", "", { "dependencies": { "is-promise": "^2.0.0", "promise": "^7.0.1" } }, "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A=="], + "katex": ["katex@0.16.33", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA=="], + "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=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], @@ -743,26 +808,112 @@ "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "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=="], "markdown-table": ["markdown-table@2.0.0", "", { "dependencies": { "repeat-string": "^1.0.0" } }, "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A=="], + "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], + + "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="], + + "mdast-util-frontmatter": ["mdast-util-frontmatter@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "escape-string-regexp": "^5.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-extension-frontmatter": "^2.0.0" } }, "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA=="], + + "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], + + "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], + + "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], + + "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], + + "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], + + "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + + "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], + "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=="], + "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], + + "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], + + "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], + + "micromark-extension-cjk-friendly": ["micromark-extension-cjk-friendly@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-OkzoYVTL1ChbvQ8Cc1ayTIz7paFQz8iS9oIYmewncweUSwmWR+hkJF9spJ1lxB90XldJl26A1F4IkPOKS3bDXw=="], + + "micromark-extension-cjk-friendly-gfm-strikethrough": ["micromark-extension-cjk-friendly-gfm-strikethrough@2.0.1", "", { "dependencies": { "devlop": "^1.1.0", "get-east-asian-width": "^1.4.0", "micromark-extension-cjk-friendly-util": "3.0.1", "micromark-util-character": "^2.1.1", "micromark-util-chunked": "^2.0.1", "micromark-util-resolve-all": "^2.0.1", "micromark-util-symbol": "^2.0.1" }, "peerDependencies": { "micromark": "^4.0.0", "micromark-util-types": "^2.0.0" }, "optionalPeers": ["micromark-util-types"] }, "sha512-wVC0zwjJNqQeX+bb07YTPu/CvSAyCTafyYb7sMhX1r62/Lw5M/df3JyYaANyp8g15c1ypJRFSsookTqA1IDsUg=="], + + "micromark-extension-cjk-friendly-util": ["micromark-extension-cjk-friendly-util@3.0.1", "", { "dependencies": { "get-east-asian-width": "^1.4.0", "micromark-util-character": "^2.1.1", "micromark-util-symbol": "^2.0.1" } }, "sha512-GcbXqTTHOsiZHyF753oIddP/J2eH8j9zpyQPhkof6B2JNxfEJabnQqxbCgzJNuNes0Y2jTNJ3LiYPSXr6eJA8w=="], + + "micromark-extension-frontmatter": ["micromark-extension-frontmatter@2.0.0", "", { "dependencies": { "fault": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg=="], + + "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], + + "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], + + "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], + + "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], + + "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], + + "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], + + "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], + + "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], + + "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], + + "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], + + "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], + "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-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], + + "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], + + "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], + + "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], + + "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], + "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], + "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], + + "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], + + "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], + "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-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], + "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], @@ -777,6 +928,8 @@ "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=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], @@ -823,6 +976,8 @@ "preact": ["preact@10.28.4", "", {}, "sha512-uKFfOHWuSNpRFVTnljsCluEFq57OKT+0QdOiQo8XWnQ/pSvg7OpX5eNOejELXJMWy+BwM2nobz0FkvzmnpCNsQ=="], + "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], @@ -911,6 +1066,8 @@ "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "swrv": ["swrv@1.1.0", "", { "peerDependencies": { "vue": ">=3.2.26 < 4" } }, "sha512-pjllRDr2s0iTwiE5Isvip51dZGR7GjLH1gCSVyE8bQnbAx6xackXsFdojau+1O5u98yHF5V73HQGOFxKUXO9gQ=="], + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], "tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="], @@ -931,6 +1088,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], @@ -941,6 +1100,8 @@ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "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=="], @@ -957,6 +1118,8 @@ "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=="], + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + "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=="], @@ -971,6 +1134,8 @@ "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=="], + "vue-stream-markdown": ["vue-stream-markdown@0.6.0", "", { "dependencies": { "@floating-ui/dom": "^1.7.5", "@vueuse/core": "^14.2.1", "marked": "^17.0.3", "mdast-util-from-markdown": "^2.0.3", "mdast-util-frontmatter": "^2.0.1", "mdast-util-gfm": "^3.1.0", "mdast-util-math": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "micromark-extension-cjk-friendly": "^2.0.1", "micromark-extension-cjk-friendly-gfm-strikethrough": "^2.0.1", "micromark-extension-frontmatter": "^2.0.0", "micromark-extension-gfm": "^3.0.0", "micromark-extension-math": "^3.1.0", "micromark-util-types": "^2.0.2" }, "peerDependencies": { "beautiful-mermaid": ">=0.1.0", "katex": ">=0.16.20", "mermaid": ">=11", "shiki": ">=3.15.0", "vue": ">=3.0.0" }, "optionalPeers": ["beautiful-mermaid", "katex", "mermaid", "shiki"] }, "sha512-RLuI5zt5czh6dkTA0/XkkMv+ApVDQXSEGUs5TgEPc0bEhhcalXItu/IxZZU0/Kf0xJ3zSMawF7PRsxoMLc88Yg=="], + "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -981,6 +1146,8 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "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=="], @@ -1001,6 +1168,10 @@ "is-expression/acorn": ["acorn@7.4.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A=="], + "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], + + "mdast-util-gfm-table/markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "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=="], diff --git a/components.d.ts b/components.d.ts index 2df71cea2..fb4c5a1ad 100644 --- a/components.d.ts +++ b/components.d.ts @@ -11,10 +11,17 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + ApiKeySetup: typeof import('./src/components/chat/ApiKeySetup.vue')['default'] + APIKeySetup: typeof import('./src/components/chat/APIKeySetup.vue')['default'] AppearanceSection: typeof import('./src/components/properties/AppearanceSection.vue')['default'] CanvasContextMenu: typeof import('./src/components/CanvasContextMenu.vue')['default'] + ChatInput: typeof import('./src/components/chat/ChatInput.vue')['default'] + ChatMessage: typeof import('./src/components/chat/ChatMessage.vue')['default'] + ChatPanel: typeof import('./src/components/ChatPanel.vue')['default'] + CodePanel: typeof import('./src/components/CodePanel.vue')['default'] ColorInput: typeof import('./src/components/ColorInput.vue')['default'] ColorPicker: typeof import('./src/components/ColorPicker.vue')['default'] + DesignPanel: typeof import('./src/components/DesignPanel.vue')['default'] EditorCanvas: typeof import('./src/components/EditorCanvas.vue')['default'] EffectsSection: typeof import('./src/components/properties/EffectsSection.vue')['default'] ExportSection: typeof import('./src/components/properties/ExportSection.vue')['default'] @@ -34,10 +41,13 @@ declare module 'vue' { IconLucideBaseline: typeof import('~icons/lucide/baseline')['default'] IconLucideBlend: typeof import('~icons/lucide/blend')['default'] IconLucideBold: typeof import('~icons/lucide/bold')['default'] + IconLucideBot: typeof import('~icons/lucide/bot')['default'] IconLucideCheck: typeof import('~icons/lucide/check')['default'] IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideCircleDot: typeof import('~icons/lucide/circle-dot')['default'] + IconLucideCode: typeof import('~icons/lucide/code')['default'] + IconLucideCopy: typeof import('~icons/lucide/copy')['default'] IconLucideEye: typeof import('~icons/lucide/eye')['default'] IconLucideEyeOff: typeof import('~icons/lucide/eye-off')['default'] IconLucideFile: typeof import('~icons/lucide/file')['default'] @@ -47,15 +57,22 @@ declare module 'vue' { IconLucideHash: typeof import('~icons/lucide/hash')['default'] IconLucideImage: typeof import('~icons/lucide/image')['default'] IconLucideItalic: typeof import('~icons/lucide/italic')['default'] + IconLucideKeyRound: typeof import('~icons/lucide/key-round')['default'] IconLucideLink: typeof import('~icons/lucide/link')['default'] + IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle')['default'] + IconLucideMessageCircle: typeof import('~icons/lucide/message-circle')['default'] IconLucidePlus: typeof import('~icons/lucide/plus')['default'] IconLucideRadius: typeof import('~icons/lucide/radius')['default'] IconLucideRotateCcw: typeof import('~icons/lucide/rotate-ccw')['default'] IconLucideRotateCw: typeof import('~icons/lucide/rotate-cw')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default'] + IconLucideSend: typeof import('~icons/lucide/send')['default'] IconLucideSettings2: typeof import('~icons/lucide/settings2')['default'] + IconLucideSparkles: typeof import('~icons/lucide/sparkles')['default'] + IconLucideSquare: typeof import('~icons/lucide/square')['default'] IconLucideStrikethrough: typeof import('~icons/lucide/strikethrough')['default'] IconLucideToggleLeft: typeof import('~icons/lucide/toggle-left')['default'] + IconLucideTriangleAlert: typeof import('~icons/lucide/triangle-alert')['default'] IconLucideType: typeof import('~icons/lucide/type')['default'] IconLucideUnderline: typeof import('~icons/lucide/underline')['default'] IconLucideUnlink: typeof import('~icons/lucide/unlink')['default'] diff --git a/package.json b/package.json index 647e70079..e0c49c53f 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,12 @@ "docs:preview": "vitepress preview docs" }, "dependencies": { + "@ai-sdk/openai": "^3.0.37", + "@ai-sdk/valibot": "^2.0.17", + "@ai-sdk/vue": "^3.0.105", "@open-pencil/cli": "workspace:*", "@open-pencil/core": "workspace:*", + "@openrouter/ai-sdk-provider": "^2.2.3", "@tailwindcss/vite": "^4.2.1", "@tanstack/vue-table": "^8.21.3", "@tauri-apps/api": "^2", @@ -37,13 +41,18 @@ "@tauri-apps/plugin-fs": "^2.4.5", "@tauri-apps/plugin-opener": "^2", "@vueuse/core": "^14.2.1", + "ai": "^6.0.105", "canvaskit-wasm": "^0.40.0", "culori": "^4.0.2", + "dedent": "^1.7.1", "fflate": "^0.8.2", "fzstd": "^0.1.1", + "prismjs": "^1.30.0", "reka-ui": "^2.8.2", "tailwindcss": "^4.2.1", + "valibot": "^1.2.0", "vue": "^3.5.29", + "vue-stream-markdown": "^0.6.0", "yoga-layout": "^3.2.1" }, "devDependencies": { @@ -52,12 +61,14 @@ "@tauri-apps/cli": "^2", "@types/culori": "^4.0.1", "@types/node": "^25.3.2", + "@types/prismjs": "^1.26.6", "@typescript/native-preview": "^7.0.0-dev.20260228.1", "@vitejs/plugin-vue": "^6.0.4", "esbuild": "^0.27.3", "jscpd": "^4.0.8", "oxfmt": "^0.35.0", "oxlint": "^1.50.0", + "tw-animate-css": "^1.4.0", "typescript": "~5.8.3", "unplugin-icons": "^23.0.1", "unplugin-vue-components": "^31.0.0", diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 26873e350..c301bb516 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -72,3 +72,33 @@ export const RULER_MAJOR_TOLERANCE = 0.01 export const TEXT_SELECTION_COLOR = { r: 0.26, g: 0.52, b: 0.96, a: 0.3 } export const TEXT_CARET_COLOR = { r: 0, g: 0, b: 0, a: 1 } export const TEXT_CARET_WIDTH = 1 + +export interface ModelOption { + id: string + name: string + provider: string + tag?: string +} + +export const AI_MODELS: ModelOption[] = [ + // Best for design: vision + frontend + tool calling (WebDev Arena #1, DesignBench, SWE-bench 79.6%) + { id: 'anthropic/claude-sonnet-4.6', name: 'Claude Sonnet 4.6', provider: 'Anthropic', tag: 'Best for design' }, + { id: 'anthropic/claude-opus-4.6', name: 'Claude Opus 4.6', provider: 'Anthropic', tag: 'Smartest' }, + // 76.8% SWE-bench, vision + UI-to-code specialist + { id: 'moonshotai/kimi-k2.5', name: 'Kimi K2.5', provider: 'Moonshot', tag: 'Vision + code' }, + // 1M context, multimodal (text+image+audio+video), 78% SWE-bench + { id: 'google/gemini-3.1-pro-preview', name: 'Gemini 3.1 Pro', provider: 'Google', tag: '1M context' }, + // 80% SWE-bench, 400K context, agentic coding + { id: 'openai/gpt-5.3-codex', name: 'GPT-5.3 Codex', provider: 'OpenAI' }, + + // Fast & cheap + { id: 'google/gemini-3-flash-preview', name: 'Gemini 3 Flash', provider: 'Google', tag: 'Fast' }, + { id: 'deepseek/deepseek-v3.2', name: 'DeepSeek V3.2', provider: 'DeepSeek', tag: 'Cheap' }, + { id: 'qwen/qwen3.5-flash-02-23', name: 'Qwen 3.5 Flash', provider: 'Qwen', tag: 'Cheap' }, + + // Free (with tool calling) + { id: 'qwen/qwen3-coder:free', name: 'Qwen3 Coder', provider: 'Qwen', tag: 'Free' }, + { id: 'openai/gpt-oss-120b:free', name: 'GPT-OSS 120B', provider: 'OpenAI', tag: 'Free' }, +] + +export const DEFAULT_AI_MODEL = AI_MODELS[0].id diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 534e8bc2f..99ff35c0a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -112,7 +112,9 @@ export { type BaseProps, type TextProps, type StyleProps, - type RenderResult + type RenderResult, + sceneNodeToJsx, + selectionToJsx } from './render' export { parseFigmaClipboard, diff --git a/packages/core/src/render/export-jsx.ts b/packages/core/src/render/export-jsx.ts new file mode 100644 index 000000000..d50bb0949 --- /dev/null +++ b/packages/core/src/render/export-jsx.ts @@ -0,0 +1,280 @@ +import { colorToHex } from '../color' + +import type { SceneGraph, SceneNode, Fill, Stroke, Effect, NodeType, Color } from '../scene-graph' + +const NODE_TYPE_TO_TAG: Partial> = { + FRAME: 'Frame', + RECTANGLE: 'Rectangle', + ROUNDED_RECTANGLE: 'Rectangle', + ELLIPSE: 'Ellipse', + TEXT: 'Text', + LINE: 'Line', + STAR: 'Star', + POLYGON: 'Polygon', + VECTOR: 'Vector', + GROUP: 'Group', + SECTION: 'Section', + COMPONENT: 'Component', + COMPONENT_SET: 'Frame', + INSTANCE: 'Frame' +} + +function formatColor(color: Color, opacity = 1): string { + const hex = colorToHex(color) + if (opacity < 1) return hex + Math.round(opacity * 255).toString(16).padStart(2, '0') + return hex +} + +function solidFillColor(fills: Fill[]): string | null { + const visible = fills.filter((f) => f.visible && f.type === 'SOLID') + if (visible.length !== 1) return null + return formatColor(visible[0].color, visible[0].opacity) +} + +function solidStroke(strokes: Stroke[]): { color: string; weight: number } | null { + const visible = strokes.filter((s) => s.visible) + if (visible.length !== 1) return null + return { + color: formatColor(visible[0].color, visible[0].opacity), + weight: visible[0].weight + } +} + +function formatShadow(e: Effect): string | null { + if (e.type !== 'DROP_SHADOW' && e.type !== 'INNER_SHADOW') return null + return `${e.offset.x} ${e.offset.y} ${e.radius} ${formatColor(e.color, e.color.a)}` +} + +function formatProp(key: string, value: unknown): string { + if (typeof value === 'string') return `${key}="${value}"` + if (typeof value === 'number') return `${key}={${value}}` + if (typeof value === 'boolean') return value ? key : `${key}={false}` + return `${key}={${JSON.stringify(value)}}` +} + +function collectProps(node: SceneNode, graph: SceneGraph): [string, unknown][] { + const props: [string, unknown][] = [] + + if (node.name && node.name !== node.type) { + props.push(['name', node.name]) + } + + if (node.layoutMode !== 'NONE') { + props.push(['flex', node.layoutMode === 'HORIZONTAL' ? 'row' : 'col']) + } + + const isAutoLayout = node.layoutMode !== 'NONE' + const parent = node.parentId ? graph.getNode(node.parentId) : null + const parentIsAutoLayout = parent ? parent.layoutMode !== 'NONE' : false + + if (isAutoLayout) { + const primaryAxis = node.layoutMode === 'HORIZONTAL' ? 'width' : 'height' + const crossAxis = node.layoutMode === 'HORIZONTAL' ? 'height' : 'width' + + if (node.primaryAxisSizing === 'HUG') { + /* hug is default with flex, omit */ + } else if (node.primaryAxisSizing === 'FILL') { + props.push([primaryAxis === 'width' ? 'w' : 'h', 'fill']) + } else { + props.push([primaryAxis === 'width' ? 'w' : 'h', node[primaryAxis]]) + } + + if (node.counterAxisSizing === 'HUG') { + /* hug is default, omit */ + } else if (node.counterAxisSizing === 'FILL') { + props.push([crossAxis === 'width' ? 'w' : 'h', 'fill']) + } else { + props.push([crossAxis === 'width' ? 'w' : 'h', node[crossAxis]]) + } + } else { + if (node.width > 0) props.push(['w', node.width]) + if (node.height > 0) props.push(['h', node.height]) + } + + if (parentIsAutoLayout && node.layoutGrow > 0) { + props.push(['grow', node.layoutGrow]) + } + + if (isAutoLayout && node.itemSpacing > 0) { + props.push(['gap', node.itemSpacing]) + } + + if (isAutoLayout && node.layoutWrap === 'WRAP') { + props.push(['wrap', true]) + if (node.counterAxisSpacing > 0) { + props.push(['rowGap', node.counterAxisSpacing]) + } + } + + if (isAutoLayout) { + if (node.primaryAxisAlign === 'CENTER') props.push(['justify', 'center']) + else if (node.primaryAxisAlign === 'MAX') props.push(['justify', 'end']) + else if (node.primaryAxisAlign === 'SPACE_BETWEEN') props.push(['justify', 'between']) + + if (node.counterAxisAlign === 'CENTER') props.push(['items', 'center']) + else if (node.counterAxisAlign === 'MAX') props.push(['items', 'end']) + else if (node.counterAxisAlign === 'STRETCH') props.push(['items', 'stretch']) + } + + if (isAutoLayout) { + const { paddingTop: pt, paddingRight: pr, paddingBottom: pb, paddingLeft: pl } = node + if (pt > 0 || pr > 0 || pb > 0 || pl > 0) { + if (pt === pr && pr === pb && pb === pl) { + props.push(['p', pt]) + } else if (pt === pb && pl === pr) { + props.push(['py', pt]) + props.push(['px', pl]) + } else { + if (pt > 0) props.push(['pt', pt]) + if (pr > 0) props.push(['pr', pr]) + if (pb > 0) props.push(['pb', pb]) + if (pl > 0) props.push(['pl', pl]) + } + } + } + + const bg = solidFillColor(node.fills) + if (bg) props.push(['bg', bg]) + + const stroke = solidStroke(node.strokes) + if (stroke) { + props.push(['stroke', stroke.color]) + if (stroke.weight !== 1) props.push(['strokeWidth', stroke.weight]) + } + + if (node.cornerRadius > 0) { + if (node.independentCorners) { + const { topLeftRadius: tl, topRightRadius: tr, bottomRightRadius: br, bottomLeftRadius: bl } = node + if (tl === tr && tr === br && br === bl) { + props.push(['rounded', tl]) + } else { + if (tl > 0) props.push(['roundedTL', tl]) + if (tr > 0) props.push(['roundedTR', tr]) + if (br > 0) props.push(['roundedBR', br]) + if (bl > 0) props.push(['roundedBL', bl]) + } + } else { + props.push(['rounded', node.cornerRadius]) + } + } + + if (node.cornerSmoothing > 0) props.push(['cornerSmoothing', node.cornerSmoothing]) + if (node.opacity < 1) props.push(['opacity', Math.round(node.opacity * 100) / 100]) + if (node.rotation !== 0) props.push(['rotate', Math.round(node.rotation * 100) / 100]) + if (node.blendMode !== 'PASS_THROUGH' && node.blendMode !== 'NORMAL') { + props.push(['blendMode', node.blendMode.toLowerCase()]) + } + if (node.clipsContent) props.push(['overflow', 'hidden']) + + for (const effect of node.effects) { + if (!effect.visible) continue + if (effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') { + const shadow = formatShadow(effect) + if (shadow) props.push(['shadow', shadow]) + } else if (effect.type === 'LAYER_BLUR' || effect.type === 'BACKGROUND_BLUR') { + props.push(['blur', effect.radius]) + } + } + + if (node.type === 'TEXT') { + if (node.fontSize !== 14) props.push(['size', node.fontSize]) + if (node.fontFamily && node.fontFamily !== 'Inter') props.push(['font', node.fontFamily]) + if (node.fontWeight !== 400) { + if (node.fontWeight === 700) props.push(['weight', 'bold']) + else if (node.fontWeight === 500) props.push(['weight', 'medium']) + else props.push(['weight', node.fontWeight]) + } + if (node.textAlignHorizontal !== 'LEFT') { + props.push(['textAlign', node.textAlignHorizontal.toLowerCase()]) + } + const textColor = solidFillColor(node.fills) + if (textColor) { + const bgIdx = props.findIndex(([k]) => k === 'bg') + if (bgIdx !== -1) props.splice(bgIdx, 1) + props.push(['color', textColor]) + } + } + + if (node.type === 'STAR') { + if (node.pointCount !== 5) props.push(['points', node.pointCount]) + if (node.starInnerRadius !== 0.382) props.push(['innerRadius', node.starInnerRadius]) + } + if (node.type === 'POLYGON' && node.pointCount !== 3) { + props.push(['points', node.pointCount]) + } + + return props +} + +function nodeToJsx( + node: SceneNode, + graph: SceneGraph, + indent: number +): string { + const tag = NODE_TYPE_TO_TAG[node.type] + if (!tag) return '' + + const prefix = ' '.repeat(indent) + const props = collectProps(node, graph) + const propsStr = props.map(([k, v]) => formatProp(k, v)).join(' ') + const opening = propsStr ? `<${tag} ${propsStr}` : `<${tag}` + + const children = graph.getChildren(node.id) + const isText = node.type === 'TEXT' + + if (isText) { + const text = node.text + if (!text) return `${prefix}${opening} />` + const escaped = text.replace(/[{}<>&]/g, (c) => + c === '{' ? '{' : c === '}' ? '}' : c === '<' ? '<' : c === '>' ? '>' : '&' + ) + if (!escaped.includes('\n')) { + return `${prefix}${opening}>${escaped}` + } + const lines = escaped.split('\n') + const parts = [ + `${prefix}${opening}>`, + ...lines.map((l) => `${prefix} ${l}`), + `${prefix}` + ] + return parts.join('\n') + } + + if (children.length === 0) { + return `${prefix}${opening} />` + } + + const childJsx = children + .filter((c) => c.visible) + .map((c) => nodeToJsx(c, graph, indent + 1)) + .filter(Boolean) + + if (childJsx.length === 0) { + return `${prefix}${opening} />` + } + + return [ + `${prefix}${opening}>`, + ...childJsx, + `${prefix}` + ].join('\n') +} + +export function sceneNodeToJsx( + nodeId: string, + graph: SceneGraph +): string { + const node = graph.getNode(nodeId) + if (!node) return '' + return nodeToJsx(node, graph, 0) +} + +export function selectionToJsx( + nodeIds: string[], + graph: SceneGraph +): string { + return nodeIds + .map((id) => sceneNodeToJsx(id, graph)) + .filter(Boolean) + .join('\n\n') +} diff --git a/packages/core/src/render/index.ts b/packages/core/src/render/index.ts index ad854a1b1..40537c409 100644 --- a/packages/core/src/render/index.ts +++ b/packages/core/src/render/index.ts @@ -22,3 +22,5 @@ export { type TreeNode, type BaseProps, type TextProps, type StyleProps, isTreeN export { renderTree, type RenderResult } from './renderer' export { renderJsx, renderTreeNode, buildComponent } from './render-jsx' + +export { sceneNodeToJsx, selectionToJsx } from './export-jsx' diff --git a/src/ai/tools.ts b/src/ai/tools.ts new file mode 100644 index 000000000..61876d84c --- /dev/null +++ b/src/ai/tools.ts @@ -0,0 +1,310 @@ +import { valibotSchema } from '@ai-sdk/valibot' +import { tool } from 'ai' +import * as v from 'valibot' + +import type { EditorStore } from '@/stores/editor' + +const nodeTypeSchema = v.picklist([ + 'FRAME', + 'RECTANGLE', + 'ELLIPSE', + 'TEXT', + 'LINE', + 'STAR', + 'POLYGON', + 'SECTION' +]) + +const rgbaSchema = v.object({ + r: v.pipe(v.number(), v.minValue(0), v.maxValue(1)), + g: v.pipe(v.number(), v.minValue(0), v.maxValue(1)), + b: v.pipe(v.number(), v.minValue(0), v.maxValue(1)), + a: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(1)), 1) +}) + +const hexSchema = v.pipe( + v.string(), + v.regex(/^#?[0-9a-fA-F]{6,8}$/), + v.description('Hex color like #ff0000 or #ff000080') +) + +const colorInputSchema = v.union([rgbaSchema, hexSchema]) + +type ColorInput = v.InferOutput + +function resolveColor(input: ColorInput) { + if (typeof input === 'string') { + const h = input.replace('#', '') + return { + r: parseInt(h.slice(0, 2), 16) / 255, + g: parseInt(h.slice(2, 4), 16) / 255, + b: parseInt(h.slice(4, 6), 16) / 255, + a: h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1 + } + } + return { ...input, a: input.a ?? 1 } +} + +export function createAITools(store: EditorStore) { + return { + create_shape: tool({ + description: + 'Create a shape on the canvas. Use FRAME for containers/cards, RECTANGLE for solid blocks, ELLIPSE for circles, TEXT for labels, SECTION for page sections.', + inputSchema: valibotSchema( + v.object({ + type: nodeTypeSchema, + x: v.pipe(v.number(), v.description('X position in canvas coordinates')), + y: v.pipe(v.number(), v.description('Y position in canvas coordinates')), + width: v.pipe(v.number(), v.minValue(1), v.description('Width in pixels')), + height: v.pipe(v.number(), v.minValue(1), v.description('Height in pixels')), + name: v.optional(v.pipe(v.string(), v.description('Node name shown in layers panel'))), + parent_id: v.optional(v.pipe(v.string(), v.description('Parent node ID to nest inside'))) + }) + ), + execute: async ({ type, x, y, width, height, name, parent_id }) => { + const id = store.createShape(type, x, y, width, height, parent_id) + if (name) store.renameNode(id, name) + store.select([id]) + return { id, type, x, y, width, height, name: name ?? type.toLowerCase() } + } + }), + + set_fill: tool({ + description: 'Set the fill color of a node. Accepts hex (#ff0000) or RGBA object.', + inputSchema: valibotSchema( + v.object({ + id: v.pipe(v.string(), v.description('Node ID')), + color: colorInputSchema + }) + ), + execute: async ({ id, color }) => { + const c = resolveColor(color) + store.updateNodeWithUndo( + id, + { fills: [{ type: 'SOLID', color: c, opacity: 1, visible: true }] }, + 'Set fill' + ) + return { id, color: c } + } + }), + + set_stroke: tool({ + description: 'Set the stroke (border) of a node.', + inputSchema: valibotSchema( + v.object({ + id: v.pipe(v.string(), v.description('Node ID')), + color: colorInputSchema, + weight: v.optional(v.pipe(v.number(), v.minValue(0.1)), 1), + align: v.optional(v.picklist(['INSIDE', 'CENTER', 'OUTSIDE']), 'INSIDE') + }) + ), + execute: async ({ id, color, weight, align }) => { + const c = resolveColor(color) + store.updateNodeWithUndo( + id, + { strokes: [{ color: c, weight, opacity: 1, visible: true, align }] }, + 'Set stroke' + ) + return { id, color: c, weight } + } + }), + + update_node: tool({ + description: + 'Update properties of an existing node: position, size, opacity, corner radius, visibility, text content, font.', + inputSchema: valibotSchema( + v.object({ + id: v.pipe(v.string(), v.description('Node ID')), + x: v.optional(v.number()), + y: v.optional(v.number()), + width: v.optional(v.pipe(v.number(), v.minValue(1))), + height: v.optional(v.pipe(v.number(), v.minValue(1))), + opacity: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(1))), + corner_radius: v.optional(v.pipe(v.number(), v.minValue(0))), + visible: v.optional(v.boolean()), + text: v.optional(v.pipe(v.string(), v.description('Text content (for TEXT nodes)'))), + font_size: v.optional(v.pipe(v.number(), v.minValue(1))), + font_weight: v.optional(v.number()), + name: v.optional(v.string()) + }) + ), + execute: async ({ id, corner_radius, font_size, font_weight, name, ...rest }) => { + const changes: Record = { ...rest } + if (corner_radius !== undefined) changes.cornerRadius = corner_radius + if (font_size !== undefined) changes.fontSize = font_size + if (font_weight !== undefined) changes.fontWeight = font_weight + store.updateNodeWithUndo(id, changes, 'Update node') + if (name !== undefined) store.renameNode(id, name) + return { id, updated: Object.keys(changes) } + } + }), + + set_layout: tool({ + description: + 'Set auto-layout (flexbox) on a frame. Direction, alignment, spacing, and padding.', + inputSchema: valibotSchema( + v.object({ + id: v.pipe(v.string(), v.description('Frame node ID')), + direction: v.picklist(['HORIZONTAL', 'VERTICAL']), + spacing: v.optional( + v.pipe(v.number(), v.minValue(0), v.description('Gap between items')), + 0 + ), + padding: v.optional( + v.pipe(v.number(), v.minValue(0), v.description('Equal padding on all sides')) + ), + padding_horizontal: v.optional(v.pipe(v.number(), v.minValue(0))), + padding_vertical: v.optional(v.pipe(v.number(), v.minValue(0))), + align: v.optional( + v.pipe( + v.picklist(['MIN', 'CENTER', 'MAX', 'SPACE_BETWEEN']), + v.description('Primary axis alignment') + ), + 'MIN' + ), + counter_align: v.optional( + v.pipe( + v.picklist(['MIN', 'CENTER', 'MAX', 'STRETCH']), + v.description('Cross axis alignment') + ), + 'MIN' + ) + }) + ), + execute: async ({ + id, + direction, + spacing, + padding, + padding_horizontal, + padding_vertical, + align, + counter_align + }) => { + store.setLayoutMode(id, direction) + const ph = padding_horizontal ?? padding ?? 0 + const pv = padding_vertical ?? padding ?? 0 + store.updateNodeWithUndo( + id, + { + itemSpacing: spacing, + primaryAxisAlign: align, + counterAxisAlign: counter_align, + paddingLeft: ph, + paddingRight: ph, + paddingTop: pv, + paddingBottom: pv + }, + 'Set layout' + ) + return { id, direction, spacing } + } + }), + + delete_node: tool({ + description: 'Delete a node by ID.', + inputSchema: valibotSchema( + v.object({ + id: v.pipe(v.string(), v.description('Node ID to delete')) + }) + ), + execute: async ({ id }) => { + store.select([id]) + store.deleteSelected() + return { deleted: id } + } + }), + + select_nodes: tool({ + description: 'Select one or more nodes by ID.', + inputSchema: valibotSchema( + v.object({ + ids: v.pipe(v.array(v.string()), v.minLength(1), v.description('Node IDs to select')) + }) + ), + execute: async ({ ids }) => { + store.select(ids) + return { selected: ids } + } + }), + + get_page_tree: tool({ + description: + 'Get the node tree of the current page. Returns all nodes with their hierarchy, types, positions, and sizes.', + inputSchema: valibotSchema(v.object({})), + execute: async () => { + const graph = store.graph + const pageId = store.state.currentPageId + const page = graph.getNode(pageId) + if (!page) return { error: 'No current page' } + + function nodeToJSON(id: string): object | null { + const node = graph.getNode(id) + if (!node) return null + const children = graph + .getChildren(id) + .map((c) => nodeToJSON(c.id)) + .filter(Boolean) + return { + id: node.id, + type: node.type, + name: node.name, + x: node.x, + y: node.y, + width: node.width, + height: node.height, + ...(children.length > 0 ? { children } : {}), + ...(node.fills.length > 0 ? { fills: node.fills } : {}), + ...(node.text ? { text: node.text } : {}) + } + } + + return { + page: page.name, + children: graph + .getChildren(pageId) + .map((c) => nodeToJSON(c.id)) + .filter(Boolean) + } + } + }), + + get_selection: tool({ + description: 'Get details about currently selected nodes.', + inputSchema: valibotSchema(v.object({})), + execute: async () => { + const nodes = store.selectedNodes.value + if (nodes.length === 0) return { selection: [] } + return { + selection: nodes.map((n) => ({ + id: n.id, + type: n.type, + name: n.name, + x: n.x, + y: n.y, + width: n.width, + height: n.height, + fills: n.fills, + text: n.text || undefined + })) + } + } + }), + + rename_node: tool({ + description: 'Rename a node in the layers panel.', + inputSchema: valibotSchema( + v.object({ + id: v.string(), + name: v.string() + }) + ), + execute: async ({ id, name }) => { + store.renameNode(id, name) + return { id, name } + } + }) + } +} + +export type AITools = ReturnType diff --git a/src/app.css b/src/app.css index 09571ec2b..1022e5871 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,5 @@ @import 'tailwindcss'; +@import 'tw-animate-css'; @theme { --color-panel: #2a2a2a; diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue new file mode 100644 index 000000000..f5ca1f7a4 --- /dev/null +++ b/src/components/ChatPanel.vue @@ -0,0 +1,96 @@ + + + diff --git a/src/components/CodePanel.vue b/src/components/CodePanel.vue new file mode 100644 index 000000000..0975544ec --- /dev/null +++ b/src/components/CodePanel.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/src/components/DesignPanel.vue b/src/components/DesignPanel.vue new file mode 100644 index 000000000..7e0eec094 --- /dev/null +++ b/src/components/DesignPanel.vue @@ -0,0 +1,85 @@ + + + diff --git a/src/components/PropertiesPanel.vue b/src/components/PropertiesPanel.vue index 881af1595..4b5adabe1 100644 --- a/src/components/PropertiesPanel.vue +++ b/src/components/PropertiesPanel.vue @@ -1,29 +1,15 @@ diff --git a/src/components/chat/ApiKeySetup.vue b/src/components/chat/ApiKeySetup.vue new file mode 100644 index 000000000..0dd9c3263 --- /dev/null +++ b/src/components/chat/ApiKeySetup.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/chat/ChatInput.vue b/src/components/chat/ChatInput.vue new file mode 100644 index 000000000..69fb9392b --- /dev/null +++ b/src/components/chat/ChatInput.vue @@ -0,0 +1,140 @@ + + + diff --git a/src/components/chat/ChatMessage.vue b/src/components/chat/ChatMessage.vue new file mode 100644 index 000000000..b6f521552 --- /dev/null +++ b/src/components/chat/ChatMessage.vue @@ -0,0 +1,133 @@ + + + diff --git a/src/composables/use-chat.ts b/src/composables/use-chat.ts new file mode 100644 index 000000000..0fbd5c8d7 --- /dev/null +++ b/src/composables/use-chat.ts @@ -0,0 +1,111 @@ +import { Chat } from '@ai-sdk/vue' +import { DEFAULT_AI_MODEL } from '@open-pencil/core' +import { createOpenRouter } from '@openrouter/ai-sdk-provider' +import { DirectChatTransport, ToolLoopAgent } from 'ai' +import dedent from 'dedent' +import { computed, ref, watch } from 'vue' + +import { createAITools } from '@/ai/tools' + +import type { EditorStore } from '@/stores/editor' +import type { UIMessage } from 'ai' + +export { AI_MODELS as MODELS } from '@open-pencil/core' +export type { ModelOption } from '@open-pencil/core' + +const API_KEY_STORAGE = 'open-pencil:openrouter-api-key' +const MODEL_STORAGE = 'open-pencil:model' + +const SYSTEM_PROMPT = dedent` + You are a design assistant inside OpenPencil, a Figma-like design editor. + Help users create and modify designs. Be concise and direct. + When describing changes, use specific design terminology. + + Available node types: FRAME (containers/cards), RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON, SECTION. + Colors can be hex strings (#ff0000) or RGBA objects with values 0–1. + Coordinates use canvas space — (0, 0) is the top-left of the page. + + Always use tools to make changes. After creating nodes, briefly describe what you did. + When the user asks to create a layout, use create_shape with FRAME, then set_layout for auto-layout. +` + +const apiKey = ref(localStorage.getItem(API_KEY_STORAGE) ?? '') +const modelId = ref(localStorage.getItem(MODEL_STORAGE) ?? DEFAULT_AI_MODEL) +const activeTab = ref<'design' | 'ai'>('design') + +let editorStore: EditorStore | null = null + +watch(apiKey, (key) => { + if (key) { + localStorage.setItem(API_KEY_STORAGE, key) + } else { + localStorage.removeItem(API_KEY_STORAGE) + } +}) + +watch(modelId, (id) => { + localStorage.setItem(MODEL_STORAGE, id) +}) + +const isConfigured = computed(() => apiKey.value.length > 0) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- test-only mock transports don't implement full generics +let overrideTransport: (() => any) | null = null + +let chat: Chat | null = null + +function createTransport() { + if (overrideTransport) return overrideTransport() + + const openrouter = createOpenRouter({ + apiKey: apiKey.value, + headers: { + 'X-OpenRouter-Title': 'OpenPencil', + 'HTTP-Referer': 'https://github.com/dannote/open-pencil' + } + }) + + const tools = editorStore ? createAITools(editorStore) : {} + + const agent = new ToolLoopAgent({ + model: openrouter(modelId.value), + instructions: SYSTEM_PROMPT, + tools + }) + + return new DirectChatTransport({ agent }) +} + +function ensureChat(): Chat | null { + if (!apiKey.value) return null + if (!chat) { + chat = new Chat({ + transport: createTransport() + }) + } + return chat +} + +function resetChat() { + chat = null +} + +if (typeof window !== 'undefined') { + window.__OPEN_PENCIL_SET_TRANSPORT__ = (factory) => { + overrideTransport = factory + } +} + +export function useAIChat(store?: EditorStore) { + if (store) { + editorStore = store + } + return { + apiKey, + modelId, + activeTab, + isConfigured, + ensureChat, + resetChat + } +} diff --git a/src/composables/use-keyboard.ts b/src/composables/use-keyboard.ts index 44460f163..b77a2ee74 100644 --- a/src/composables/use-keyboard.ts +++ b/src/composables/use-keyboard.ts @@ -1,5 +1,6 @@ import { useEventListener } from '@vueuse/core' +import { useAIChat } from '@/composables/use-chat' import { TOOL_SHORTCUTS } from '@/stores/editor' import { openFileDialog } from './use-menu' @@ -11,6 +12,7 @@ function isEditing(e: Event) { } export function useKeyboard(store: EditorStore) { + const { activeTab } = useAIChat() useEventListener(window, 'copy', (e: ClipboardEvent) => { if (isEditing(e)) return e.preventDefault() @@ -79,6 +81,11 @@ export function useKeyboard(store: EditorStore) { } if (e.metaKey || e.ctrlKey) { + if (e.code === 'KeyJ') { + e.preventDefault() + activeTab.value = activeTab.value === 'ai' ? 'design' : 'ai' + return + } if (e.key === 'z' && !e.shiftKey) { e.preventDefault() store.undoAction() diff --git a/src/engine/fonts.ts b/src/engine/fonts.ts index f5b7757cc..9af480382 100644 --- a/src/engine/fonts.ts +++ b/src/engine/fonts.ts @@ -77,5 +77,3 @@ export async function loadFont(family: string, style = 'Regular'): Promise }[] > + // eslint-disable-next-line @typescript-eslint/no-explicit-any + __OPEN_PENCIL_SET_TRANSPORT__?(factory: () => any): void } diff --git a/tests/e2e/chat-panel.spec.ts b/tests/e2e/chat-panel.spec.ts new file mode 100644 index 000000000..6f40ab668 --- /dev/null +++ b/tests/e2e/chat-panel.spec.ts @@ -0,0 +1,201 @@ +import { expect, test, type Page } from '@playwright/test' + +import { CanvasHelper } from '../helpers/canvas' + +const USE_REAL_LLM = process.env.TEST_REAL_LLM === '1' +const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY ?? '' + +let page: Page +let canvas: CanvasHelper + +test.describe.configure({ mode: 'serial' }) + +test.beforeAll(async ({ browser }) => { + page = await browser.newPage() + await page.goto('/') + canvas = new CanvasHelper(page) + await canvas.waitForInit() + + if (!USE_REAL_LLM) { + await injectMockTransport(page) + } +}) + +test.afterAll(async () => { + await page.close() +}) + +async function injectMockTransport(page: Page) { + await page.evaluate(() => { + const setTransport = window.__OPEN_PENCIL_SET_TRANSPORT__ + if (!setTransport) throw new Error('Transport override not available') + + let msgCounter = 0 + + setTransport(() => ({ + async sendMessages({ + messages, + }: { + messages: Array<{ role: string; parts: Array<{ type: string; text?: string }> }> + }) { + const lastUser = [...messages].reverse().find((m) => m.role === 'user') + const text = lastUser?.parts?.find((p) => p.type === 'text')?.text ?? '' + const msgId = `mock-msg-${++msgCounter}` + const wantsTool = text.toLowerCase().includes('frame') || text.toLowerCase().includes('rectangle') + + return new ReadableStream({ + start(controller) { + controller.enqueue({ type: 'start', messageId: msgId }) + + if (wantsTool) { + const toolCallId = `call-${msgId}` + controller.enqueue({ + type: 'tool-input-start', + toolCallId, + toolName: 'create_shape', + }) + controller.enqueue({ + type: 'tool-input-delta', + toolCallId, + inputTextDelta: '{"type":"FRAME","x":100,"y":100,"width":200,"height":150,"name":"Card"}', + }) + controller.enqueue({ + type: 'tool-input-available', + toolCallId, + toolName: 'create_shape', + input: { type: 'FRAME', x: 100, y: 100, width: 200, height: 150, name: 'Card' }, + }) + controller.enqueue({ + type: 'tool-output-available', + toolCallId, + toolName: 'create_shape', + output: { id: '0:99', type: 'FRAME', x: 100, y: 100, width: 200, height: 150, name: 'Card' }, + }) + } + + const words = wantsTool + ? ['Created', 'a', 'frame', 'called', '"Card".'] + : `I'll help you with: "${text}". Here's a mock response.`.split(' ') + + controller.enqueue({ type: 'text-start', id: 'text-1' }) + for (const word of words) { + controller.enqueue({ type: 'text-delta', id: 'text-1', delta: word + ' ' }) + } + controller.enqueue({ type: 'text-end', id: 'text-1' }) + controller.enqueue({ type: 'finish', finishReason: 'stop' }) + controller.close() + }, + }) + }, + async reconnectToStream() { + return null + }, + })) + }) +} + +function chatTab() { + return page.getByRole('tab', { name: 'AI' }) +} + +function designTab() { + return page.getByRole('tab', { name: 'Design' }) +} + +function chatInput() { + return page.locator('input[placeholder="Describe a change…"]') +} + +function apiKeyInput() { + return page.locator('input[placeholder="sk-or-…"]') +} + +test('⌘J switches to AI tab', async () => { + await designTab().waitFor() + await page.keyboard.press('Meta+j') + await expect(chatTab()).toHaveAttribute('data-state', 'active') +}) + +test('⌘J switches back to Design tab', async () => { + await page.keyboard.press('Meta+j') + await expect(designTab()).toHaveAttribute('data-state', 'active') +}) + +test('clicking AI tab shows API key setup when no key set', async () => { + await chatTab().click() + await expect(apiKeyInput()).toBeVisible() + await expect(page.getByText('Enter your OpenRouter API key')).toBeVisible() +}) + +test('saving API key shows chat interface', async () => { + const key = USE_REAL_LLM ? OPENROUTER_KEY : 'sk-or-test-key-12345' + await apiKeyInput().fill(key) + await page.locator('button:has-text("Save")').click() + + await expect(chatInput()).toBeVisible() + await expect(page.getByText('Describe what you want to create or change.')).toBeVisible() +}) + +test('empty input has disabled send button', async () => { + const sendButton = page.locator('button[type="submit"]') + await expect(sendButton).toBeDisabled() +}) + +test('typing enables send button', async () => { + await chatInput().fill('Make a red rectangle') + const sendButton = page.locator('button[type="submit"]') + await expect(sendButton).toBeEnabled() +}) + +test('Enter submits message and clears input', async () => { + await chatInput().fill('Hello there') + await chatInput().press('Enter') + + await expect(page.getByText('Hello there', { exact: true })).toBeVisible({ timeout: 5000 }) + await expect(chatInput()).toHaveValue('') +}) + +test('assistant responds', async () => { + if (USE_REAL_LLM) { + await expect( + page.locator('.chat-markdown, [class*="rounded-tl-md"]').first(), + ).toBeVisible({ timeout: 30000 }) + } else { + await expect(page.getByText('mock response', { exact: false })).toBeVisible({ timeout: 5000 }) + } +}) + +test('model selector is visible and clickable', async () => { + const trigger = page.getByRole('combobox') + await expect(trigger).toBeVisible() + await trigger.click() + + await expect(page.getByRole('option', { name: /Claude Sonnet 4\.6/ })).toBeVisible() + await expect(page.getByText('Best for design')).toBeVisible() + await expect(page.getByText('Free').first()).toBeVisible() + + await page.keyboard.press('Escape') +}) + +test('tool calls render in assistant message', async () => { + await chatInput().fill('Create a frame') + await chatInput().press('Enter') + + if (USE_REAL_LLM) { + await expect( + page.locator('.chat-markdown, [class*="rounded-tl-md"]').first(), + ).toBeVisible({ timeout: 30000 }) + } else { + await expect(page.getByText('Create Shape')).toBeVisible({ timeout: 5000 }) + await expect(page.getByText('Done')).toBeVisible() + await expect(page.getByText('Created a frame', { exact: false })).toBeVisible() + } +}) + +test('switching tabs preserves chat', async () => { + await designTab().click() + await expect(designTab()).toHaveAttribute('data-state', 'active') + + await chatTab().click() + await expect(page.getByText('Hello there', { exact: true })).toBeVisible() +}) diff --git a/tests/engine/export-jsx.test.ts b/tests/engine/export-jsx.test.ts new file mode 100644 index 000000000..68db01cdf --- /dev/null +++ b/tests/engine/export-jsx.test.ts @@ -0,0 +1,293 @@ +import { describe, expect, test } from 'bun:test' + +import { SceneGraph, sceneNodeToJsx, selectionToJsx } from '@open-pencil/core' + +function makeGraph() { + const graph = new SceneGraph() + graph.createNode('CANVAS', graph.rootId, { name: 'Page 1' }) + return graph +} + +function pageId(graph: SceneGraph) { + return graph.getPages()[0].id +} + +describe('sceneNodeToJsx', () => { + test('basic rectangle', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + name: 'Box', + width: 100, + height: 50 + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toBe('') + }) + + test('rectangle with fill and rounded corners', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + name: 'Card', + width: 320, + height: 200, + fills: [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 }, opacity: 1, visible: true }], + cornerRadius: 16 + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('w={320}') + expect(jsx).toContain('h={200}') + expect(jsx).toContain('bg="#FFFFFF"') + expect(jsx).toContain('rounded={16}') + expect(jsx).toContain('') + }) + + test('text node', () => { + const graph = makeGraph() + const node = graph.createNode('TEXT', pageId(graph), { + name: 'Title', + width: 200, + height: 24, + text: 'Hello World', + fontSize: 18, + fontWeight: 700, + fills: [ + { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } + ] + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('Hello World') + }) + + test('frame with auto-layout', () => { + const graph = makeGraph() + const frame = graph.createNode('FRAME', pageId(graph), { + name: 'Row', + width: 400, + height: 100, + layoutMode: 'HORIZONTAL', + itemSpacing: 16, + paddingTop: 12, + paddingRight: 12, + paddingBottom: 12, + paddingLeft: 12, + primaryAxisSizing: 'FIXED', + counterAxisSizing: 'HUG' + }) + const jsx = sceneNodeToJsx(frame.id, graph) + expect(jsx).toContain('flex="row"') + expect(jsx).toContain('gap={16}') + expect(jsx).toContain('p={12}') + expect(jsx).toContain('w={400}') + }) + + test('frame with children', () => { + const graph = makeGraph() + const frame = graph.createNode('FRAME', pageId(graph), { + name: 'Card', + width: 320, + height: 200, + layoutMode: 'VERTICAL', + itemSpacing: 8, + primaryAxisSizing: 'HUG', + counterAxisSizing: 'FIXED' + }) + graph.createNode('RECTANGLE', frame.id, { + name: 'Image', + width: 320, + height: 120 + }) + graph.createNode('TEXT', frame.id, { + name: 'Title', + width: 200, + height: 20, + text: 'Card Title', + fontSize: 16, + fontWeight: 700, + fills: [ + { type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true } + ] + }) + + const jsx = sceneNodeToJsx(frame.id, graph) + expect(jsx).toContain('Card Title') + expect(jsx).toContain('') + }) + + test('asymmetric padding', () => { + const graph = makeGraph() + const frame = graph.createNode('FRAME', pageId(graph), { + name: 'Box', + width: 100, + height: 100, + layoutMode: 'HORIZONTAL', + paddingTop: 8, + paddingRight: 16, + paddingBottom: 8, + paddingLeft: 16, + primaryAxisSizing: 'FIXED', + counterAxisSizing: 'FIXED' + }) + const jsx = sceneNodeToJsx(frame.id, graph) + expect(jsx).toContain('py={8}') + expect(jsx).toContain('px={16}') + expect(jsx).not.toContain('pt=') + expect(jsx).not.toContain('pb=') + }) + + test('independent corners', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + name: 'Pill', + width: 100, + height: 40, + cornerRadius: 20, + independentCorners: true, + topLeftRadius: 20, + topRightRadius: 0, + bottomRightRadius: 0, + bottomLeftRadius: 20 + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('roundedTL={20}') + expect(jsx).toContain('roundedBL={20}') + expect(jsx).not.toContain('roundedTR=') + expect(jsx).not.toContain('roundedBR=') + }) + + test('opacity and rotation', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + width: 50, + height: 50, + opacity: 0.5, + rotation: 45 + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('opacity={0.5}') + expect(jsx).toContain('rotate={45}') + }) + + test('stroke', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + width: 100, + height: 100, + strokes: [ + { color: { r: 1, g: 0, b: 0, a: 1 }, weight: 2, opacity: 1, visible: true, align: 'INSIDE' as const } + ] + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('stroke="#FF0000"') + expect(jsx).toContain('strokeWidth={2}') + }) + + test('hidden children are excluded', () => { + const graph = makeGraph() + const frame = graph.createNode('FRAME', pageId(graph), { + name: 'Parent', + width: 100, + height: 100 + }) + graph.createNode('RECTANGLE', frame.id, { + name: 'Visible', + width: 50, + height: 50 + }) + graph.createNode('RECTANGLE', frame.id, { + name: 'Hidden', + width: 50, + height: 50, + visible: false + }) + const jsx = sceneNodeToJsx(frame.id, graph) + expect(jsx).toContain('Visible') + expect(jsx).not.toContain('Hidden') + }) + + test('empty text node renders self-closing', () => { + const graph = makeGraph() + const node = graph.createNode('TEXT', pageId(graph), { + name: 'Empty', + width: 100, + height: 20, + text: '' + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('/>') + expect(jsx).not.toContain('') + }) + + test('omits default values', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + width: 100, + height: 100, + opacity: 1, + rotation: 0, + cornerRadius: 0 + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).not.toContain('opacity') + expect(jsx).not.toContain('rotate') + expect(jsx).not.toContain('rounded') + }) + + test('effects: shadow and blur', () => { + const graph = makeGraph() + const node = graph.createNode('RECTANGLE', pageId(graph), { + width: 100, + height: 100, + effects: [ + { + type: 'DROP_SHADOW', + color: { r: 0, g: 0, b: 0, a: 0.25 }, + offset: { x: 0, y: 4 }, + radius: 8, + spread: 0, + visible: true + }, + { + type: 'LAYER_BLUR', + color: { r: 0, g: 0, b: 0, a: 0 }, + offset: { x: 0, y: 0 }, + radius: 4, + spread: 0, + visible: true + } + ] + }) + const jsx = sceneNodeToJsx(node.id, graph) + expect(jsx).toContain('shadow="0 4 8') + expect(jsx).toContain('blur={4}') + }) +}) + +describe('selectionToJsx', () => { + test('multiple nodes separated by blank lines', () => { + const graph = makeGraph() + const a = graph.createNode('RECTANGLE', pageId(graph), { + name: 'A', + width: 10, + height: 10 + }) + const b = graph.createNode('ELLIPSE', pageId(graph), { + name: 'B', + width: 20, + height: 20 + }) + const jsx = selectionToJsx([a.id, b.id], graph) + expect(jsx).toContain('