feat(code): add live JSX and HTML/CSS editing (#525)

* feat(code): isolate Design JSX execution

- Transform JSX with the existing Sucrase dependency and execute it in a disposable opaque-origin iframe worker

- Block ambient network capabilities and enforce source, timeout, output, depth, and element limits

- Validate that only bounded plain structured data returns to the application

* feat(code): add editable Design JSX

- Add a lazy CodeMirror editor with JSX syntax support, completion, diagnostics, and bounded scrolling

- Convert validated sandbox output into trusted Design JSX helpers before rendering

- Apply or insert JSX as one undoable graph transaction while preserving dirty drafts

* feat(code): localize JSX editor actions

- Add translated-message fallbacks for editing, applying, inserting, and draft state

- Document the editable JSX workflow in the unreleased changelog

* fix(code): satisfy typed sandbox validation

- Keep the sandbox document terminator literal and preserve optional selection handling under type-aware lint

* test(code): resolve sandbox probes through app aliases

* fix(code): bound sandbox results before cloning

- Enforce string, array, object, depth, element, and byte limits inside the disposable worker

- Retain host-side validation as a second structured-data boundary

* fix(code): keep JSX and HTML editing modes separate

- Switch generated Tailwind output back to OpenPencil JSX before editing

- Close the JSX editor when opening HTML/CSS import and avoid stacking both editors

* feat(code): support authored Design JSX programs

- Allow local constants, function components, arrays, conditionals, fragments, and multiple roots

- Preserve replacement positions for multiple selected roots and reject mixed-parent or locked selections

- Cover multi-root undo, redo, and all-or-nothing rollback

* feat(code): add explicit JSX view mode

- Let authors leave CodeMirror without applying a draft

- Keep the editable surface and HTML/CSS importer mutually exclusive

* feat(code): diagnose unknown JSX vocabulary

- Warn on OpenPencil elements and properties that are absent from the canonical schema

- Surface diagnostics inline through CodeMirror lint markers

* test(code): accept WebKit isolation diagnostics

- Cover the sandbox architecture against Playwright WebKit

- Accept engine-specific wording while preserving the same unavailable-window assertion

* refactor(code): consolidate Design JSX vocabulary

- Drive renderer warnings, completion, and diagnostics from one supported-property schema

- Recognize locally declared components and add focused schema and transform tests

- Replace CodeMirror basicSetup with an explicit feature set and remove the umbrella package

* fix(code): support Design JSX variable helpers

* refactor(code): unify JSX sandbox validation

* fix(code): account for complete sandbox output

* fix(code): preserve locked JSX descendants

* fix(code): preserve JSX sibling order

* fix(code): recompute JSX parent layouts

* feat(code): add live code previews

* test(code): centralize graph assertions

* fix(code): harden live preview sessions

* docs: describe live code editing

* fix(code): update editor accessibility labels

* fix(code): use theme-aware error colors

* fix(dev): stop Vite disconnect error loops

* fix(code): clarify live preview status

* fix(ui): avoid tooltip attribute warnings

* test(code): assert semantic preview status

* refactor(code): remove obsolete editor messages

* fix(code): harden preview concurrency and isolation

* fix(code): cancel stale reset previews
This commit is contained in:
Danila Poyarkov 2026-08-15 09:48:42 +03:00 committed by GitHub
parent d8c968c04a
commit 6193b6da7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 2302 additions and 614 deletions

View file

@ -7,6 +7,13 @@
- Add explicit shared/view editor-state ownership and canvas render-state hooks as a foundation for independent same-document canvas panes.
- Show Figma-style temporary distance measurements between selected and Option/Alt-hovered layers. (#491)
- Add a reproducible Dev Container for web, package, CLI, and non-browser test development.
- Add local crash recovery for unsaved and pathless documents, including MCP-created documents. (#487)
- Add isolated visual inspection that sends bounded selection renders to the configured Vision model and returns text findings without retaining image data in Design chat history. (#232, #471)
- Add image attachments to AI chat with bounded analysis, immediate transcript thumbnails, hover previews, and click-to-view images. (#232)
- Add a single CodeMirror editor for live Design JSX and HTML/CSS canvas previews, with Tailwind JSX viewing, completion, diagnostics, line numbers, bounded execution, and session-level undo. (#130)
- Allow supported AI model profiles to set a provider-specific reasoning effort. (#454)
- Show unavailable or substituted document fonts with affected-layer selection and retry actions, and expose font fidelity through the Figma API and MCP tooling. (#503)
- Add reusable remote MCP connections for ACP agents, with Streamable HTTP endpoints and bearer tokens stored in the configured credential backend.
- Author multidimensional component variants in the Design panel, including property/value renaming, sparse-combination diagnostics, variant duplication, and exact instance transitions. (#239)
- Create deterministic, dependency-complete component-library revisions through the provider-neutral Core library catalog, with browser IndexedDB persistence for local catalogs. (#239)

234
bun.lock
View file

@ -14,6 +14,15 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.7.9",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
"@chenglou/pretext": "^0.0.7",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.10.4",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/language": "^6.12.4",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.8",
"@open-pencil/cli": "workspace:*",
"@open-pencil/core": "workspace:*",
"@open-pencil/dom-css": "workspace:*",
@ -49,7 +58,6 @@
"lib0": "^0.2.117",
"motion-v": "^2.0.0",
"opentype.js": "^2.0.0",
"refractor": "^5.0.0",
"reka-ui": "^2.9.0",
"svg2pdf.js": "^2.7.0",
"tailwind-merge": "^3.5.0",
@ -113,7 +121,7 @@
"unplugin-raw": "^0.6.4",
"unplugin-vue": "^7.1.1",
"unplugin-vue-components": "^31.0.0",
"vite": "^8.0.0",
"vite": "8.1.4",
"vite-plugin-pwa": "^1.2.0",
"vue-tsc": "^3.2.5",
"workbox-window": "^7.4.0",
@ -571,6 +579,26 @@
"@clack/prompts": ["@clack/prompts@0.9.1", "", { "dependencies": { "@clack/core": "0.4.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-JIpyaboYZeWYlyP0H+OoPPxd6nqueG/CmN6ixBiNFsIDHREevjIf0n0Ohh5gr5C8pEDknzgvz+pIJ8dMhzWIeg=="],
"@codemirror/autocomplete": ["@codemirror/autocomplete@6.20.3", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g=="],
"@codemirror/commands": ["@codemirror/commands@6.10.4", "", { "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.7.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } }, "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg=="],
"@codemirror/lang-css": ["@codemirror/lang-css@6.3.1", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@lezer/common": "^1.0.2", "@lezer/css": "^1.1.7" } }, "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg=="],
"@codemirror/lang-html": ["@codemirror/lang-html@6.4.12", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/lang-css": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/css": "^1.1.0", "@lezer/html": "^1.3.12" } }, "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w=="],
"@codemirror/lang-javascript": ["@codemirror/lang-javascript@6.2.5", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.6.0", "@codemirror/lint": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0", "@lezer/javascript": "^1.0.0" } }, "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A=="],
"@codemirror/language": ["@codemirror/language@6.12.4", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A=="],
"@codemirror/lint": ["@codemirror/lint@6.9.7", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg=="],
"@codemirror/search": ["@codemirror/search@6.7.1", "", { "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA=="],
"@codemirror/state": ["@codemirror/state@6.7.1", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A=="],
"@codemirror/view": ["@codemirror/view@6.43.8", "", { "dependencies": { "@codemirror/state": "^6.7.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-qtItTDssZ/5GFfi94hrILu9j/VUeFPDPkhovEfmWFj2ipTxnzPB8DdHgfbb8HYTzLTYhrndKmyQxXUz/PDLenw=="],
"@colors/colors": ["@colors/colors@1.5.0", "", {}, "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ=="],
"@dependents/detective-less": ["@dependents/detective-less@5.0.3", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-v6oD9Ukp+N7V4n6p5I/+mM5fIohSfkrDSGlFm5w/pYmchvbk+sMIHsLxrFJ5Lnujewj1BzWL0K84d88lwZAMQA=="],
@ -795,6 +823,18 @@
"@leichtgewicht/ip-codec": ["@leichtgewicht/ip-codec@2.0.5", "", {}, "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw=="],
"@lezer/common": ["@lezer/common@1.5.2", "", {}, "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ=="],
"@lezer/css": ["@lezer/css@1.3.6", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.3.0" } }, "sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g=="],
"@lezer/highlight": ["@lezer/highlight@1.2.3", "", { "dependencies": { "@lezer/common": "^1.3.0" } }, "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g=="],
"@lezer/html": ["@lezer/html@1.3.13", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg=="],
"@lezer/javascript": ["@lezer/javascript@1.5.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.1.3", "@lezer/lr": "^1.3.0" } }, "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA=="],
"@lezer/lr": ["@lezer/lr@1.4.10", "", { "dependencies": { "@lezer/common": "^1.0.0" } }, "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A=="],
"@libp2p/bootstrap": ["@libp2p/bootstrap@11.0.42", "", { "dependencies": { "@libp2p/interface": "^2.10.4", "@libp2p/interface-internal": "^2.3.17", "@libp2p/peer-id": "^5.1.7", "@multiformats/mafmt": "^12.1.6", "@multiformats/multiaddr": "^12.4.4", "main-event": "^1.0.1" } }, "sha512-xe5LMZrXR2cnFcR69ax/cHqNCc8zwSvqDWOf82u/ifAsUae7M0eeoEVi15lfrPewWst4YPkTqYDkZSPWC65I3Q=="],
"@libp2p/crypto": ["@libp2p/crypto@5.1.13", "", { "dependencies": { "@libp2p/interface": "^3.1.0", "@noble/curves": "^2.0.1", "@noble/hashes": "^2.0.1", "multiformats": "^13.4.0", "protons-runtime": "^5.6.0", "uint8arraylist": "^2.4.8", "uint8arrays": "^5.1.0" } }, "sha512-8NN9cQP3jDn+p9+QE9ByiEoZ2lemDFf/unTgiKmS3JF93ph240EUVdbCyyEgOMfykzb0okTM4gzvwfx9osJebQ=="],
@ -827,6 +867,8 @@
"@loaderkit/resolve": ["@loaderkit/resolve@1.0.6", "", { "dependencies": { "@braidai/lang": "^1.0.0" } }, "sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg=="],
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.3", "", {}, "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA=="],
"@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="],
@ -1321,8 +1363,6 @@
"@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="],
"@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="],
"@types/raf": ["@types/raf@3.4.3", "", {}, "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
@ -1599,8 +1639,6 @@
"character-parser": ["character-parser@2.2.0", "", { "dependencies": { "is-regex": "^1.0.3" } }, "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw=="],
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
"check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
@ -1661,6 +1699,8 @@
"cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="],
"crelt": ["crelt@1.0.7", "", {}, "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"crypto-random-string": ["crypto-random-string@2.0.0", "", {}, "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="],
@ -1979,14 +2019,10 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
"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=="],
"hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="],
"help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
@ -2047,10 +2083,6 @@
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
"is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="],
@ -2069,8 +2101,6 @@
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
"is-electron": ["is-electron@2.2.2", "", {}, "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg=="],
@ -2089,8 +2119,6 @@
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
"is-loopback-addr": ["is-loopback-addr@2.0.2", "", {}, "sha512-26POf2KRCno/KTNL5Q0b/9TYnL00xEsSaLfiFRmjM7m7Lw7ZMmFybzzuX4CcsLAluZGd+niLUiMRxEooVE3aqg=="],
@ -2481,7 +2509,7 @@
"nanoevents": ["nanoevents@9.1.0", "", {}, "sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="],
@ -2561,8 +2589,6 @@
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
@ -2611,7 +2637,7 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
@ -2719,8 +2745,6 @@
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
"refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="],
"regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="],
"regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.2", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g=="],
@ -2925,6 +2949,8 @@
"strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@ -3133,7 +3159,7 @@
"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@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
"vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="],
@ -3165,6 +3191,8 @@
"vue-tsc": ["vue-tsc@3.2.6", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.6" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-gYW/kWI0XrwGzd0PKc7tVB/qpdeAkIZLNZb10/InizkQjHjnT8weZ/vBarZoj4kHKbUTZT/bAVgoOr8x4NsQ/Q=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="],
"weald": ["weald@1.1.1", "", { "dependencies": { "ms": "^3.0.0-canary.1", "supports-color": "^10.0.0" } }, "sha512-PaEQShzMCz8J/AD2N3dJMc1hTZWkJeLKS2NMeiVkV5KDHwgZe7qXLEzyodsT/SODxWDdXJJqocuwf3kHzcXhSQ=="],
@ -3423,6 +3451,8 @@
"@vue/compiler-sfc/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"@vue/compiler-ssr/@vue/compiler-dom": ["@vue/compiler-dom@3.5.31", "", { "dependencies": { "@vue/compiler-core": "3.5.31", "@vue/shared": "3.5.31" } }, "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw=="],
"@vue/compiler-ssr/@vue/shared": ["@vue/shared@3.5.31", "", {}, "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw=="],
@ -3523,8 +3553,6 @@
"p-queue/p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
@ -3591,15 +3619,17 @@
"unplugin-vue/unplugin": ["unplugin@3.0.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg=="],
"unplugin-vue/vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
"unplugin-vue-components/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"unplugin-vue-components/obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"unplugin-vue-components/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"vite/rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
"vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"vite-plugin-pwa/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
@ -3611,8 +3641,6 @@
"vitepress/shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
"vitepress/vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
"vitepress-plugin-llms/markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
"vitepress-plugin-llms/pretty-bytes": ["pretty-bytes@7.1.0", "", {}, "sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw=="],
@ -3687,6 +3715,8 @@
"@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"@vueuse/integrations/@vueuse/core/@vueuse/metadata": ["@vueuse/metadata@14.3.0", "", {}, "sha512-BwxmbAzwAVF50+MW57GXOUEV61nFBGnlBvrTqj49PqWJu3uw7hdu72ztXeZ33RdZtDY6kO+bfCAE1PCn88Tktw=="],
"@waku/enr/@libp2p/crypto/@libp2p/interface": ["@libp2p/interface@2.11.0", "", { "dependencies": { "@multiformats/dns": "^1.0.6", "@multiformats/multiaddr": "^12.4.4", "it-pushable": "^3.2.3", "it-stream-types": "^2.0.2", "main-event": "^1.0.1", "multiformats": "^13.3.6", "progress-events": "^1.0.1", "uint8arraylist": "^2.4.8" } }, "sha512-0MUFKoXWHTQW3oWIgSHApmYMUKWO/Y02+7Hpyp+n3z+geD4Xo2Rku2gYWmxcq+Pyjkz6Q9YjDWz3Yb2SoV2E8Q=="],
@ -3729,6 +3759,8 @@
"pptxgenjs/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"precinct/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"rolldown-plugin-dts/@babel/generator/@babel/types": ["@babel/types@8.0.0", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.0" } }, "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw=="],
"rolldown-plugin-dts/@babel/parser/@babel/types": ["@babel/types@8.0.0", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0", "@babel/helper-validator-identifier": "^8.0.0" } }, "sha512-K8ponJDxBwDHigkeFqaqT5wLGl4bTlwMafR8k7b5CPxr6Ww+UG9ls8Yx6Tcpboxu97eeGVEEyKcHmEyOwN1vSw=="],
@ -3783,39 +3815,45 @@
"unplugin-vue-components/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
"unplugin-vue/vite/postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
"unplugin-vue/vite/rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
"vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
"unplugin-vue/vite/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
"vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
"vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
"vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
"vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
"vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
"vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
"vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
"vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
"vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
"vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
"vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
"vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
"vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
"vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
"vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
"vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
"vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
"vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
"vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
"vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
"vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
"vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
"vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
"vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
"vite/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
"vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
"vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"vite/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"vitepress-plugin-llms/markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@ -3837,12 +3875,6 @@
"vitepress/shiki/@shikijs/themes": ["@shikijs/themes@4.3.1", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
"vitepress/vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"vitepress/vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"vitepress/vite/rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"vue-component-meta/@vue/language-core/@vue/compiler-dom": ["@vue/compiler-dom@3.5.34", "", { "dependencies": { "@vue/compiler-core": "3.5.34", "@vue/shared": "3.5.34" } }, "sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw=="],
"vue-component-meta/@vue/language-core/@vue/shared": ["@vue/shared@3.5.34", "", {}, "sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA=="],
@ -3869,8 +3901,6 @@
"vue/@vue/compiler-sfc/@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.39", "", { "dependencies": { "@vue/compiler-dom": "3.5.39", "@vue/shared": "3.5.39" } }, "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw=="],
"vue/@vue/compiler-sfc/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"@open-pencil/docs/@tailwindcss/vite/@tailwindcss/node/enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
"@open-pencil/docs/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.2", "", { "os": "android", "cpu": "arm64" }, "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA=="],
@ -3911,6 +3941,8 @@
"detective-vue2/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"detective-vue2/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"rolldown-plugin-dts/@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
"rolldown-plugin-dts/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
@ -3921,7 +3953,41 @@
"twoslash-vue/@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.34", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.34", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"unplugin-vue/vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"unplugin-vue/vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
"unplugin-vue/vite/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
"vitepress/@vue/devtools-api/@vue/devtools-kit/@vue/devtools-shared": ["@vue/devtools-shared@8.1.5", "", {}, "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ=="],
@ -3931,54 +3997,18 @@
"vitepress/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="],
"vitepress/vite/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"vitepress/vite/rolldown/@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
"vitepress/vite/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
"vitepress/vite/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
"vitepress/vite/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
"vitepress/vite/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
"vitepress/vite/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
"vitepress/vite/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
"vitepress/vite/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
"vitepress/vite/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
"vitepress/vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"vitepress/vite/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"vue-component-meta/@vue/language-core/@vue/compiler-dom/@vue/compiler-core": ["@vue/compiler-core@3.5.34", "", { "dependencies": { "@babel/parser": "^7.29.3", "@vue/shared": "3.5.34", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw=="],
"vue-docgen-api/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"vue-docgen-api/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"vue-docgen-api/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"vue/@vue/compiler-sfc/@babel/parser/@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
"vue/@vue/compiler-sfc/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"vue/@vue/compiler-sfc/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"@open-pencil/docs/@tailwindcss/vite/@tailwindcss/node/enhanced-resolve/tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
"@open-pencil/docs/@tailwindcss/vite/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
@ -4003,11 +4033,7 @@
"twoslash-vue/@vue/language-core/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"vue-component-meta/@vue/language-core/@vue/compiler-dom/@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
@ -4015,6 +4041,12 @@
"vue/@vue/compiler-sfc/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"unplugin-vue/vite/rolldown/@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
}
}

View file

@ -70,6 +70,15 @@
"@atlaskit/pragmatic-drag-and-drop": "^1.7.9",
"@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
"@chenglou/pretext": "^0.0.7",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.10.4",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.5",
"@codemirror/language": "^6.12.4",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.1",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.8",
"@open-pencil/cli": "workspace:*",
"@open-pencil/core": "workspace:*",
"@open-pencil/dom-css": "workspace:*",
@ -105,7 +114,6 @@
"lib0": "^0.2.117",
"motion-v": "^2.0.0",
"opentype.js": "^2.0.0",
"refractor": "^5.0.0",
"reka-ui": "^2.9.0",
"svg2pdf.js": "^2.7.0",
"tailwind-merge": "^3.5.0",
@ -169,7 +177,7 @@
"unplugin-raw": "^0.6.4",
"unplugin-vue": "^7.1.1",
"unplugin-vue-components": "^31.0.0",
"vite": "^8.0.0",
"vite": "8.1.4",
"vite-plugin-pwa": "^1.2.0",
"vue-tsc": "^3.2.5",
"workbox-window": "^7.4.0",

View file

@ -60,6 +60,17 @@ export { defineVars, designVar, isVariable, type DesignVariable, type VarDef } f
export { createElement } from './mini-react'
export { renderJSX, renderTreeNode, buildComponent } from './render'
export {
DESIGN_JSX_ELEMENTS,
DESIGN_JSX_HELPERS,
DESIGN_JSX_PROPERTIES,
DESIGN_JSX_SUPPORTED_PROPERTIES,
DESIGN_JSX_SUPPORTED_PROPERTY_NAMES,
type DesignJSXElementDefinition,
type DesignJSXHelperDefinition,
type DesignJSXPropertyDefinition
} from './schema'
export { transformDesignJSXExpression } from './transform'
import jsxReference from '#core/tools/prompts/jsx-reference.md'

View file

@ -2,6 +2,7 @@ import { transform } from 'sucrase'
import type { SceneGraph } from '@open-pencil/scene-graph'
import { DESIGN_JSX_SUPPORTED_PROPERTIES } from '#core/design-jsx/schema'
import type { RenderOptions as RenderJSXOptions } from '#core/design-jsx/types'
import { backgroundBlur, dropShadow, foregroundBlur, innerShadow, layerBlur } from './effects'
@ -21,114 +22,7 @@ import { isTreeNode, resolveToTree, type TreeNode } from './tree'
* Build a component function from a JSX string using sucrase.
* Works in both Node/Bun and the browser (no native bindings).
*/
const SUPPORTED_PROPS = new Set([
'name',
'key',
'flex',
'flow',
'dir',
'gap',
'wrap',
'rowGap',
'columnGap',
'justify',
'justifyContent',
'items',
'align',
'alignItems',
'grow',
'w',
'h',
'width',
'height',
'minW',
'maxW',
'minH',
'maxH',
'x',
'y',
'top',
'left',
'position',
'p',
'padding',
'px',
'py',
'pt',
'pr',
'pb',
'pl',
'bg',
'fill',
'fills',
'background',
'backgroundColor',
'stroke',
'border',
'borderColor',
'strokeWidth',
'borderWidth',
'strokeAlign',
'strokeDash',
'rounded',
'borderRadius',
'roundedTL',
'roundedTR',
'roundedBL',
'roundedBR',
'cornerRadius',
'cornerSmoothing',
'opacity',
'blendMode',
'rotate',
'rotation',
'overflow',
'shadow',
'blur',
'effects',
'size',
'fontSize',
'font',
'fontFamily',
'weight',
'fontWeight',
'color',
'text',
'characters',
'content',
'value',
'title',
'textAlign',
'textAlignHorizontal',
'textHorizontalAlignment',
'textAlignVertical',
'textVerticalAlignment',
'textAutoResize',
'lineHeight',
'letterSpacing',
'textDecoration',
'textCase',
'maxLines',
'truncate',
'grid',
'columns',
'rows',
'colStart',
'rowStart',
'col',
'row',
'colSpan',
'rowSpan',
'points',
'pointCount',
'innerRadius',
'label',
'style',
'bind',
'component',
'componentId',
'of'
])
const SUPPORTED_PROPS = DESIGN_JSX_SUPPORTED_PROPERTIES
function stripHTMLComments(jsxString: string): string {
return jsxString.replace(/<!--[\s\S]*?-->/g, '')

View file

@ -0,0 +1,165 @@
export type DesignJSXElementDefinition = {
name: string
runtimeType: string
description: string
}
export type DesignJSXNamedDefinition = {
name: string
description: string
}
export type DesignJSXPropertyDefinition = DesignJSXNamedDefinition
export type DesignJSXHelperDefinition = DesignJSXNamedDefinition
export const DESIGN_JSX_ELEMENTS: DesignJSXElementDefinition[] = [
{ name: 'Frame', runtimeType: 'frame', description: 'A frame or auto-layout container.' },
{ name: 'Text', runtimeType: 'text', description: 'A text layer.' },
{ name: 'Rectangle', runtimeType: 'rectangle', description: 'A rectangle layer.' },
{ name: 'Ellipse', runtimeType: 'ellipse', description: 'An ellipse layer.' },
{ name: 'Line', runtimeType: 'line', description: 'A line layer.' },
{ name: 'Star', runtimeType: 'star', description: 'A star layer.' },
{ name: 'Polygon', runtimeType: 'polygon', description: 'A polygon layer.' },
{ name: 'Vector', runtimeType: 'vector', description: 'A vector layer.' },
{ name: 'Group', runtimeType: 'group', description: 'A group of layers.' },
{ name: 'Section', runtimeType: 'section', description: 'A canvas section.' },
{ name: 'Component', runtimeType: 'component', description: 'A reusable component.' },
{ name: 'ComponentSet', runtimeType: 'component-set', description: 'A component variant set.' },
{ name: 'Instance', runtimeType: 'instance', description: 'An instance of a component.' },
{ name: 'View', runtimeType: 'frame', description: 'An alias for Frame.' },
{ name: 'Rect', runtimeType: 'rectangle', description: 'An alias for Rectangle.' },
{ name: 'Icon', runtimeType: 'icon', description: 'An Iconify icon.' }
]
export const DESIGN_JSX_SUPPORTED_PROPERTY_NAMES = [
'name',
'key',
'flex',
'flow',
'dir',
'gap',
'wrap',
'rowGap',
'columnGap',
'justify',
'justifyContent',
'items',
'align',
'alignItems',
'grow',
'w',
'h',
'width',
'height',
'minW',
'maxW',
'minH',
'maxH',
'x',
'y',
'top',
'left',
'position',
'p',
'padding',
'px',
'py',
'pt',
'pr',
'pb',
'pl',
'bg',
'fill',
'fills',
'background',
'backgroundColor',
'stroke',
'border',
'borderColor',
'strokeWidth',
'borderWidth',
'strokeAlign',
'strokeDash',
'rounded',
'borderRadius',
'roundedTL',
'roundedTR',
'roundedBL',
'roundedBR',
'cornerRadius',
'cornerSmoothing',
'opacity',
'blendMode',
'rotate',
'rotation',
'overflow',
'shadow',
'blur',
'effects',
'size',
'fontSize',
'font',
'fontFamily',
'weight',
'fontWeight',
'color',
'text',
'characters',
'content',
'value',
'title',
'textAlign',
'textAlignHorizontal',
'textHorizontalAlignment',
'textAlignVertical',
'textVerticalAlignment',
'textAutoResize',
'lineHeight',
'letterSpacing',
'textDecoration',
'textCase',
'maxLines',
'truncate',
'grid',
'columns',
'rows',
'colStart',
'rowStart',
'col',
'row',
'colSpan',
'rowSpan',
'points',
'pointCount',
'innerRadius',
'label',
'style',
'bind',
'component',
'componentId',
'of'
] as const
export const DESIGN_JSX_SUPPORTED_PROPERTIES = new Set<string>(DESIGN_JSX_SUPPORTED_PROPERTY_NAMES)
export const DESIGN_JSX_PROPERTIES: DesignJSXPropertyDefinition[] =
DESIGN_JSX_SUPPORTED_PROPERTY_NAMES.map((name) => ({
name,
description: `OpenPencil ${name} property.`
}))
export const DESIGN_JSX_HELPERS: DesignJSXHelperDefinition[] = [
'solid',
'gradient',
'linearGradient',
'radialGradient',
'angularGradient',
'diamondGradient',
'dropShadow',
'innerShadow',
'layerBlur',
'backgroundBlur',
'foregroundBlur',
'designVar',
'defineVars'
].map((name) => ({ name, description: `OpenPencil ${name} helper.` }))

View file

@ -0,0 +1,44 @@
import { transform } from 'sucrase'
const options = {
transforms: ['typescript', 'jsx'] as Array<'typescript' | 'jsx'>,
jsxPragma: '__h',
jsxFragmentPragma: '__fragment',
production: true
}
function transformExpression(source: string): string {
return transform(`return (${source.trim()})`, options).code
}
function statementBoundaries(source: string): number[] {
const boundaries = new Set<number>()
for (let index = 0; index < source.length; index++) {
if (source[index] === '\n') boundaries.add(index + 1)
}
return [...boundaries].sort((left, right) => right - left)
}
/**
* Transform Design JSX into a function body. A plain JSX expression is accepted directly.
* For authored programs, top-level declarations must precede a final expression on a new line.
*/
export function transformDesignJSXExpression(source: string): string {
const trimmed = source.trim()
try {
return transformExpression(trimmed)
} catch (expressionError) {
for (const boundary of statementBoundaries(trimmed)) {
const statements = trimmed.slice(0, boundary).trim()
const expression = trimmed.slice(boundary).trim()
if (!statements || !expression) continue
try {
const transformedStatements = transform(statements, options).code
return `${transformedStatements}\n${transformExpression(expression)}`
} catch {
continue
}
}
throw expressionError
}
}

View file

@ -24,7 +24,6 @@
"createCollection": "Sammlung erstellen",
"localVariables": "Lokale Variablen",
"noVariableCollections": "Keine Variablensammlungen",
"selectLayerForJSX": "Wähle eine Ebene, um ihren JSX-Code zu sehen",
"connectAIProvider": "Verbinde einen KI-Anbieter, um den Chat zu starten.",
"connect": "Verbinden",
"testConnection": "Verbindung testen",
@ -192,6 +191,7 @@
"copiedExclamation": "Kopiert!",
"copyMessage": "Nachricht kopieren",
"copyJSXReference": "JSX-Prop-Referenz in die Zwischenablage kopieren",
"jsxUpToDate": "Up to date",
"newTab": "Neuer Tab",
"closeTab": "{name} schließen",
"showUI": "UI anzeigen ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "Schließen",
"toolRunning": "Wird ausgeführt…",
"toolError": "Fehler",
"permissionRequestTitle": "Berechtigungsanfrage"
"permissionRequestTitle": "Berechtigungsanfrage",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "Crear colección",
"localVariables": "Variables locales",
"noVariableCollections": "No hay colecciones de variables",
"selectLayerForJSX": "Selecciona una capa para ver su código JSX",
"connectAIProvider": "Conecta un proveedor de IA para empezar a chatear.",
"connect": "Conectar",
"testConnection": "Probar conexión",
@ -192,6 +191,7 @@
"copiedExclamation": "¡Copiado!",
"copyMessage": "Copiar mensaje",
"copyJSXReference": "Copiar referencia de props JSX al portapapeles",
"jsxUpToDate": "Up to date",
"newTab": "Nueva pestaña",
"closeTab": "Cerrar {name}",
"showUI": "Mostrar interfaz ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "Descartar",
"toolRunning": "Ejecutando…",
"toolError": "Error",
"permissionRequestTitle": "Solicitud de permiso"
"permissionRequestTitle": "Solicitud de permiso",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "Créer une collection",
"localVariables": "Variables locales",
"noVariableCollections": "Aucune collection de variables",
"selectLayerForJSX": "Sélectionnez un calque pour voir son code JSX",
"connectAIProvider": "Connectez un fournisseur dIA pour commencer à discuter.",
"connect": "Connecter",
"testConnection": "Tester la connexion",
@ -192,6 +191,7 @@
"copiedExclamation": "Copié !",
"copyMessage": "Copier le message",
"copyJSXReference": "Copier la référence des props JSX dans le presse-papiers",
"jsxUpToDate": "Up to date",
"newTab": "Nouvel onglet",
"closeTab": "Fermer {name}",
"showUI": "Afficher linterface ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "Ignorer",
"toolRunning": "Exécution…",
"toolError": "Erreur",
"permissionRequestTitle": "Demande dautorisation"
"permissionRequestTitle": "Demande dautorisation",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "Crea raccolta",
"localVariables": "Variabili locali",
"noVariableCollections": "Nessuna raccolta di variabili",
"selectLayerForJSX": "Seleziona un livello per vedere il suo codice JSX",
"connectAIProvider": "Collega un provider AI per iniziare a chattare.",
"connect": "Connetti",
"testConnection": "Verifica connessione",
@ -192,6 +191,7 @@
"copiedExclamation": "Copiato!",
"copyMessage": "Copia messaggio",
"copyJSXReference": "Copia riferimento props JSX negli appunti",
"jsxUpToDate": "Up to date",
"newTab": "Nuova scheda",
"closeTab": "Chiudi {name}",
"showUI": "Mostra interfaccia ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "Chiudi",
"toolRunning": "Esecuzione…",
"toolError": "Errore",
"permissionRequestTitle": "Richiesta di autorizzazione"
"permissionRequestTitle": "Richiesta di autorizzazione",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "コレクションを作成",
"localVariables": "ローカル変数",
"noVariableCollections": "変数コレクションがありません",
"selectLayerForJSX": "JSXコードを表示するレイヤーを選択してください",
"connectAIProvider": "チャットを始めるには、AIプロバイダーを接続してください。",
"connect": "接続",
"testConnection": "接続をテスト",
@ -192,6 +191,7 @@
"copiedExclamation": "コピーしました!",
"copyMessage": "メッセージをコピー",
"copyJSXReference": "JSXのprops参照をクリップボードにコピー",
"jsxUpToDate": "Up to date",
"newTab": "新しいタブ",
"closeTab": "{name} を閉じる",
"showUI": "UIを表示 ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "閉じる",
"toolRunning": "実行中…",
"toolError": "エラー",
"permissionRequestTitle": "権限リクエスト"
"permissionRequestTitle": "権限リクエスト",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "Utwórz kolekcję",
"localVariables": "Zmienne lokalne",
"noVariableCollections": "Brak kolekcji zmiennych",
"selectLayerForJSX": "Wybierz warstwę, aby zobaczyć jej kod JSX",
"connectAIProvider": "Połącz dostawcę AI, aby rozpocząć czat.",
"connect": "Połącz",
"testConnection": "Testuj połączenie",
@ -192,6 +191,7 @@
"copiedExclamation": "Skopiowano!",
"copyMessage": "Kopiuj wiadomość",
"copyJSXReference": "Skopiuj referencję propsów JSX do schowka",
"jsxUpToDate": "Up to date",
"newTab": "Nowa karta",
"closeTab": "Zamknij {name}",
"showUI": "Pokaż interfejs ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "Zamknij",
"toolRunning": "Uruchamianie…",
"toolError": "Błąd",
"permissionRequestTitle": "Prośba o uprawnienie"
"permissionRequestTitle": "Prośba o uprawnienie",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "Создать коллекцию",
"localVariables": "Локальные переменные",
"noVariableCollections": "Нет коллекций переменных",
"selectLayerForJSX": "Выберите слой, чтобы увидеть его JSX-код",
"connectAIProvider": "Подключите AI-провайдера, чтобы начать чат.",
"connect": "Подключить",
"testConnection": "Проверить подключение",
@ -192,6 +191,7 @@
"copiedExclamation": "Скопировано!",
"copyMessage": "Скопировать сообщение",
"copyJSXReference": "Скопировать справку по JSX-свойствам в буфер",
"jsxUpToDate": "Up to date",
"newTab": "Новая вкладка",
"closeTab": "Закрыть {name}",
"showUI": "Показать интерфейс ({shortcut})",
@ -276,5 +276,16 @@
"dismiss": "Закрыть",
"toolRunning": "Выполняется…",
"toolError": "Ошибка",
"permissionRequestTitle": "Запрос разрешения"
"permissionRequestTitle": "Запрос разрешения",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -24,7 +24,6 @@
"createCollection": "创建集合",
"localVariables": "本地变量",
"noVariableCollections": "没有变量集合",
"selectLayerForJSX": "选择一个图层以查看其 JSX 代码",
"connectAIProvider": "连接 AI 提供商以开始聊天。",
"connect": "连接",
"testConnection": "测试连接",
@ -192,6 +191,7 @@
"copiedExclamation": "已复制!",
"copyMessage": "复制消息",
"copyJSXReference": "复制 JSX 属性参考到剪贴板",
"jsxUpToDate": "Up to date",
"newTab": "新标签页",
"closeTab": "关闭 {name}",
"showUI": "显示界面({shortcut}",
@ -276,5 +276,16 @@
"dismiss": "关闭",
"toolRunning": "正在运行…",
"toolError": "错误",
"permissionRequestTitle": "权限请求"
"permissionRequestTitle": "权限请求",
"codeSource": "Code source",
"codeSourceDesignJSX": "Design JSX",
"codeSourceTailwindJSX": "Tailwind JSX",
"codeSourceHTMLCSS": "HTML/CSS",
"codeEditorDesignLabel": "Design JSX",
"codeEditorHTMLCSSLabel": "HTML and CSS",
"codeUpdating": "Updating…",
"codeUpdatedLive": "Updated live",
"codePreviewFailed": "Preview failed",
"codeGeneratedReadOnly": "Generated, read only",
"codeReset": "Reset"
}

View file

@ -33,8 +33,19 @@ export const dialogMessageDefaults = {
duplicateMode: 'Duplicate mode',
deleteMode: 'Delete mode',
setDefaultMode: 'Set as default',
selectLayerForJSX: 'Select a layer to see its JSX code',
codeSource: 'Code source',
codeSourceDesignJSX: 'Design JSX',
codeSourceTailwindJSX: 'Tailwind JSX',
codeSourceHTMLCSS: 'HTML/CSS',
codeEditorDesignLabel: 'Design JSX',
codeEditorHTMLCSSLabel: 'HTML and CSS',
codeUpdating: 'Updating…',
codeUpdatedLive: 'Updated live',
codePreviewFailed: 'Preview failed',
codeGeneratedReadOnly: 'Generated, read only',
codeReset: 'Reset',
copyJSXReference: 'Copy JSX prop reference to clipboard',
jsxUpToDate: 'Up to date',
importLabel: 'Import',
importHTMLCSS: 'Import HTML/CSS',
importHTMLCSSDescription: 'Paste HTML plus optional CSS or compiled Tailwind CSS.',

View file

@ -50,6 +50,8 @@
--color-warning-action: #fcd34d;
--color-success: #4ade80;
--color-error: #f87171;
--color-error-bg: rgb(248 113 113 / 0.1);
--color-error-border: rgb(248 113 113 / 0.4);
--color-success-bg: #16a34a;
--color-success-bg-hover: #15803d;
--color-code-tag: #7dd3fc;
@ -87,6 +89,8 @@ html[data-theme='light'] {
--color-warning-action: #78350f;
--color-success: #15803d;
--color-error: #b91c1c;
--color-error-bg: rgb(185 28 28 / 0.08);
--color-error-border: rgb(185 28 28 / 0.28);
--color-success-bg: #16a34a;
--color-success-bg-hover: #15803d;
--color-code-tag: #0369a1;
@ -123,32 +127,6 @@ body {
-webkit-user-select: none;
}
.code-highlight .token.tag {
color: var(--color-code-tag);
}
.code-highlight .token.attr-name {
color: var(--color-code-attribute);
}
.code-highlight .token.attr-value,
.code-highlight .token.string {
color: var(--color-code-string);
}
.code-highlight .token.number,
.code-highlight .token.boolean {
color: var(--color-code-number);
}
.code-highlight .token.punctuation {
color: var(--color-code-punctuation);
}
.code-highlight .token.keyword {
color: var(--color-code-attribute);
}
input[type='number'] {
appearance: textfield;
-moz-appearance: textfield;

View file

@ -0,0 +1,61 @@
import { browserHTMLToSceneGraph } from '@open-pencil/dom-css/browser'
import type { SceneGraph } from '@open-pencil/scene-graph'
import type { EditorStore } from '@/app/editor/active-store'
export type DOMCodeSession = {
originalGraph: SceneGraph
originalPageId: string
previewGraph: SceneGraph | null
previewPageId: string | null
}
export function createDOMCodeSession(store: EditorStore): DOMCodeSession {
return {
originalGraph: store.graph,
originalPageId: store.state.currentPageId,
previewGraph: null,
previewPageId: null
}
}
export async function previewDOMCode(
store: EditorStore,
session: DOMCodeSession,
source: string
): Promise<{ ok: true } | { ok: false; error: string }> {
try {
const graph = await browserHTMLToSceneGraph(source, { pageName: 'Code preview' })
const pageId = graph.getPages()[0]?.id ?? graph.rootId
session.previewGraph = graph
session.previewPageId = pageId
store.replaceGraph(graph)
return { ok: true }
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
}
export function resetDOMCodePreview(store: EditorStore, session: DOMCodeSession): void {
store.replaceGraph(session.originalGraph)
void store.switchPage(session.originalPageId)
session.previewGraph = null
session.previewPageId = null
}
export function commitDOMCodeSession(store: EditorStore, session: DOMCodeSession): void {
const after = session.previewGraph
const afterPageId = session.previewPageId
if (!after || !afterPageId) return
store.pushUndoEntry({
label: 'Edit HTML/CSS',
forward: () => {
store.replaceGraph(after)
void store.switchPage(afterPageId)
},
inverse: () => {
store.replaceGraph(session.originalGraph)
void store.switchPage(session.originalPageId)
}
})
}

View file

@ -1,9 +0,0 @@
import { toHtml as toHTML } from 'hast-util-to-html'
import { refractor } from 'refractor/core'
import jsx from 'refractor/jsx'
refractor.register(jsx)
export function highlightJSX(code: string): string {
return toHTML(refractor.highlight(code, 'jsx'))
}

View file

@ -0,0 +1,147 @@
import type { TreeNode } from '@open-pencil/core/design-jsx'
import { renderTree } from '@open-pencil/core/design-jsx'
import { computeAllLayouts } from '@open-pencil/core/layout'
import type { Vector } from '@open-pencil/scene-graph'
import { convertDesignJSXRoots } from '@/app/code/sandbox/convert'
import { evaluateDesignJSX } from '@/app/code/sandbox/evaluate'
import type { EditorStore } from '@/app/editor/active-store'
export type ApplyDesignJSXResult = { ok: true; nodeIds: string[] } | { ok: false; error: string }
export type DesignJSXEditSession = {
originalSnapshot: ReturnType<EditorStore['snapshotPage']>
originalSelectionIds: string[]
targetParentId: string
targetIndex: number
origins: Vector[]
fallbackOrigin: Vector
previewSnapshot: ReturnType<EditorStore['snapshotPage']> | null
previewNodeIds: string[]
}
function lockedSelectionError(store: EditorStore, ids: string[]): string | null {
const locked = ids.some((id) => {
const node = store.graph.getNode(id)
if (node?.locked) return true
return [...store.graph.getAllNodes()].some(
(candidate) => candidate.locked && store.graph.isDescendant(candidate.id, id)
)
})
return locked ? 'Unlock the selected layers and their contents before editing code.' : null
}
export function createDesignJSXEditSession(
store: EditorStore
): { ok: true; session: DesignJSXEditSession } | { ok: false; error: string } {
const selected = [...store.state.selectedIds]
.map((id) => store.graph.getNode(id))
.filter((node) => node !== undefined)
.sort((left, right) => {
if (left.parentId !== right.parentId) return left.id.localeCompare(right.id)
const parent = left.parentId ? store.graph.getNode(left.parentId) : undefined
return (parent?.childIds.indexOf(left.id) ?? 0) - (parent?.childIds.indexOf(right.id) ?? 0)
})
const lockedError = lockedSelectionError(
store,
selected.map(({ id }) => id)
)
if (lockedError) return { ok: false, error: lockedError }
const parentIds = new Set(selected.map(({ parentId }) => parentId ?? store.state.currentPageId))
if (parentIds.size > 1) {
return { ok: false, error: 'Select layers with the same parent before editing code.' }
}
const first = selected.at(0)
const targetParentId = first?.parentId ?? store.state.currentPageId
const targetIndex = first
? (store.graph.getNode(targetParentId)?.childIds.indexOf(first.id) ?? -1)
: -1
return {
ok: true,
session: {
originalSnapshot: store.snapshotPage(),
originalSelectionIds: selected.map(({ id }) => id),
targetParentId,
targetIndex,
origins: selected.map(({ x, y }) => ({ x, y })),
fallbackOrigin: first ? { x: first.x, y: first.y } : store.viewportCanvasCenter(),
previewSnapshot: null,
previewNodeIds: []
}
}
}
export async function previewDesignJSX(
store: EditorStore,
session: DesignJSXEditSession,
source: string
): Promise<ApplyDesignJSXResult> {
const evaluated = await evaluateDesignJSX(source)
if (!evaluated.ok) return evaluated
let roots: TreeNode[]
try {
roots = convertDesignJSXRoots(evaluated.roots)
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
store.restorePageFromSnapshot(session.originalSnapshot)
try {
for (const id of session.originalSelectionIds) store.graph.deleteNode(id)
const results = []
for (const [index, root] of roots.entries()) {
const origin = session.origins.at(index) ?? {
x: session.fallbackOrigin.x + index * 24,
y: session.fallbackOrigin.y + index * 24
}
const result = await renderTree(store.graph, root, {
parentId: session.targetParentId,
x: origin.x,
y: origin.y
})
results.push(result)
if (session.targetIndex >= 0) {
store.graph.insertChildAt(result.id, session.targetParentId, session.targetIndex + index)
}
}
const nodeIds = results.map(({ id }) => id)
computeAllLayouts(store.graph, store.state.currentPageId)
store.select(nodeIds)
store.requestRender()
session.previewSnapshot = store.snapshotPage()
session.previewNodeIds = nodeIds
return { ok: true, nodeIds }
} catch (error) {
store.restorePageFromSnapshot(session.previewSnapshot ?? session.originalSnapshot)
store.select(
session.previewNodeIds.length > 0 ? session.previewNodeIds : session.originalSelectionIds
)
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
}
export function resetDesignJSXPreview(store: EditorStore, session: DesignJSXEditSession): void {
store.restorePageFromSnapshot(session.originalSnapshot)
store.select(session.originalSelectionIds)
session.previewSnapshot = null
session.previewNodeIds = []
}
export function commitDesignJSXSession(store: EditorStore, session: DesignJSXEditSession): void {
const after = session.previewSnapshot
if (!after) return
const previewIds = [...session.previewNodeIds]
const originalSnapshot = session.originalSnapshot
const originalSelectionIds = [...session.originalSelectionIds]
store.pushUndoEntry({
label: session.originalSelectionIds.length > 0 ? 'Edit JSX' : 'Insert JSX',
forward: () => {
store.restorePageFromSnapshot(after)
store.select(previewIds)
},
inverse: () => {
store.restorePageFromSnapshot(originalSnapshot)
store.select(originalSelectionIds)
}
})
}

View file

@ -0,0 +1,97 @@
import {
angularGradient,
backgroundBlur,
designVar,
defineVars,
diamondGradient,
dropShadow,
foregroundBlur,
gradient,
innerShadow,
layerBlur,
linearGradient,
radialGradient,
solid,
type TreeNode
} from '@open-pencil/core/design-jsx'
import type { DesignJSXHelperDescriptor } from '@/app/code/sandbox/types'
const HELPERS = {
solid,
gradient,
linearGradient,
radialGradient,
angularGradient,
diamondGradient,
dropShadow,
innerShadow,
layerBlur,
backgroundBlur,
foregroundBlur,
designVar,
defineVars
}
type HelperName = keyof typeof HELPERS
type PlainRecord = { [key: string]: unknown }
type SerializedDesignJSXElement = {
type: string
props: PlainRecord
children: unknown[]
}
function isRecord(value: unknown): value is PlainRecord {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function isSerializedElement(value: unknown): value is SerializedDesignJSXElement {
return (
isRecord(value) &&
typeof value.type === 'string' &&
isRecord(value.props) &&
Array.isArray(value.children)
)
}
function isHelper(value: unknown): value is DesignJSXHelperDescriptor {
return (
isRecord(value) && typeof value.__openPencilHelper === 'string' && Array.isArray(value.args)
)
}
function convertValue(value: unknown): unknown {
if (isHelper(value)) {
const helperName = value.__openPencilHelper
const helper = Object.hasOwn(HELPERS, helperName)
? (HELPERS[helperName as HelperName] as (...args: unknown[]) => unknown)
: undefined
if (typeof helper !== 'function') {
throw new TypeError(`Unknown Design JSX helper "${helperName}".`)
}
return helper(...value.args.map(convertValue))
}
if (Array.isArray(value)) return value.map(convertValue)
if (isRecord(value))
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, convertValue(v)]))
return value
}
function convertTree(value: unknown): TreeNode {
if (!isSerializedElement(value)) {
throw new Error('Design JSX must return an OpenPencil element.')
}
const children = value.children.map((child): TreeNode | string => {
if (typeof child === 'string') return child
if (typeof child === 'number') return String(child)
return convertTree(child)
})
return { type: value.type, props: convertValue(value.props) as PlainRecord, children }
}
export function convertDesignJSXRoots(values: unknown[]): TreeNode[] {
// Call validateDesignJSXOutput first; it enforces the depth bound used by this recursion.
return values.map(convertTree)
}

View file

@ -0,0 +1,121 @@
import type { DesignJSXValidationLimits } from '@/app/code/sandbox/types'
const workerSource = String.raw`
self.onmessage = ({ data }) => {
const { id, code, elements, helpers: helperNames, limits } = data
const blocked = () => { throw new Error('Network access is unavailable in Design JSX.') }
self.fetch = blocked
self.XMLHttpRequest = undefined
self.WebSocket = undefined
self.EventSource = undefined
self.importScripts = blocked
const normalizeChildren = (children) => children.flat(Infinity).filter((child) => child != null && child !== false)
const __fragment = ''
const __h = (type, props, ...children) => {
const normalizedProps = props == null ? {} : props
const normalizedChildren = normalizeChildren(children)
if (typeof type === 'function') return type({ ...normalizedProps, children: normalizedChildren })
if (type === __fragment) return normalizedChildren
return { type, props: normalizedProps, children: normalizedChildren }
}
const helper = (name) => (...args) => ({ __openPencilHelper: name, args })
const helperRuntime = (name) => {
if (name === 'defineVars') return (vars) => Object.fromEntries(Object.entries(vars).map(([key, value]) => [key, { __openPencilHelper: 'designVar', args: [value] }]))
return helper(name)
}
const names = Object.keys(elements)
const tags = elements
const helpers = Object.fromEntries(helperNames.map((name) => [name, helperRuntime(name)]))
const validate = (value, depth = 0, state = { elements: 0, bytes: 0 }) => {
if (depth > limits.depth) throw new Error('Design JSX output is too deeply nested.')
if (value === null) {
state.bytes += 4
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
return state
}
if (typeof value === 'boolean') {
state.bytes += value ? 4 : 5
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
return state
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) throw new Error('Design JSX output contains an invalid number.')
state.bytes += 8
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
return state
}
if (typeof value === 'string') {
if (value.length > limits.stringLength) throw new Error('Design JSX output contains a string that is too long.')
state.bytes += new TextEncoder().encode(value).byteLength
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
return state
}
if (Array.isArray(value)) {
state.bytes += 2 + Math.max(0, value.length - 1)
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
if (value.length > limits.arrayLength) throw new Error('Design JSX output contains an array that is too long.')
for (const item of value) validate(item, depth + 1, state)
return state
}
if (typeof value !== 'object') throw new Error('Design JSX output contains an unsupported value.')
const keys = Object.keys(value)
state.bytes += 2 + Math.max(0, keys.length - 1)
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
if (keys.length > limits.objectKeys) throw new Error('Design JSX output contains too many object properties.')
if ('type' in value && 'props' in value && 'children' in value && ++state.elements > limits.elements) throw new Error('Design JSX output has too many elements.')
for (const key of keys) {
if (key === '__proto__' || key === 'prototype' || key === 'constructor') throw new Error('Design JSX output contains a blocked property.')
state.bytes += key.length
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
validate(value[key], depth + 1, state)
}
return state
}
try {
const argumentNames = ['__h', '__fragment', ...names, ...helperNames]
const argumentValues = [__h, __fragment, ...names.map((name) => tags[name]), ...helperNames.map((name) => helpers[name])]
const run = new Function(...argumentNames, code)
const value = run(...argumentValues)
validate(value)
self.postMessage({ id, ok: true, value })
} catch (error) {
self.postMessage({ id, ok: false, error: error instanceof Error ? error.message : String(error) })
}
}
`
export type SandboxDocumentOptions = {
elements: Record<string, string>
helpers: string[]
limits: DesignJSXValidationLimits
}
export function sandboxDocument({ elements, helpers, limits }: SandboxDocumentOptions): string {
const escapedWorkerSource = JSON.stringify(workerSource)
const escapedElements = JSON.stringify(elements)
const escapedHelpers = JSON.stringify(helpers)
const escapedLimits = JSON.stringify(limits)
return `<!doctype html><html><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; connect-src 'none'; img-src 'none'; media-src 'none'; font-src 'none'; style-src 'none'; object-src 'none'; frame-src 'none'; worker-src blob:; base-uri 'none'; form-action 'none'"></head><body><script>
const workerURL = URL.createObjectURL(new Blob([${escapedWorkerSource}], { type: 'text/javascript' }))
const workers = new Map()
addEventListener('message', (event) => {
const message = event.data
if (!message || message.type !== 'open-pencil-design-jsx-run') return
const worker = new Worker(workerURL)
workers.set(message.id, worker)
worker.onmessage = ({ data }) => {
parent.postMessage({ type: 'open-pencil-design-jsx-result', ...data }, '*')
worker.terminate()
workers.delete(message.id)
}
worker.onerror = () => {
parent.postMessage({ type: 'open-pencil-design-jsx-result', id: message.id, ok: false, error: 'Design JSX execution failed.' }, '*')
worker.terminate()
workers.delete(message.id)
}
worker.postMessage({ id: message.id, code: message.code, elements: ${escapedElements}, helpers: ${escapedHelpers}, limits: ${escapedLimits} })
})
parent.postMessage({ type: 'open-pencil-design-jsx-ready' }, '*')
</script></body></html>`
}

View file

@ -0,0 +1,108 @@
import { IS_BROWSER } from '@open-pencil/core/constants'
import {
DESIGN_JSX_ELEMENTS,
DESIGN_JSX_HELPERS,
transformDesignJSXExpression
} from '@open-pencil/core/design-jsx'
import { sandboxDocument } from '@/app/code/sandbox/document'
import {
DESIGN_JSX_DEFAULT_READY_TIMEOUT_MS,
DESIGN_JSX_DEFAULT_TIMEOUT_MS,
DESIGN_JSX_MAX_SOURCE_BYTES,
resolveDesignJSXValidationLimits,
type DesignJSXSandboxLimits,
type DesignJSXSandboxResult
} from '@/app/code/sandbox/types'
import { validateDesignJSXOutput } from '@/app/code/sandbox/validate'
function sourceByteLength(source: string): number {
return new TextEncoder().encode(source).byteLength
}
export async function evaluateDesignJSX(
source: string,
limits: DesignJSXSandboxLimits = {}
): Promise<DesignJSXSandboxResult> {
if (!IS_BROWSER) {
return { ok: false, error: 'Design JSX execution requires a browser.' }
}
const sourceBytes = limits.sourceBytes ?? DESIGN_JSX_MAX_SOURCE_BYTES
if (sourceByteLength(source) > sourceBytes) {
return { ok: false, error: 'Design JSX source is too large.' }
}
let code: string
try {
code = transformDesignJSXExpression(source)
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : String(error) }
}
const validationLimits = resolveDesignJSXValidationLimits(limits)
const iframe = document.createElement('iframe')
iframe.hidden = true
iframe.setAttribute('sandbox', 'allow-scripts')
iframe.srcdoc = sandboxDocument({
elements: Object.fromEntries(
DESIGN_JSX_ELEMENTS.map(({ name, runtimeType }) => [name, runtimeType])
),
helpers: DESIGN_JSX_HELPERS.map(({ name }) => name),
limits: validationLimits
})
document.body.append(iframe)
return new Promise((resolve) => {
const id = crypto.randomUUID()
const timeoutMs = limits.timeoutMs ?? DESIGN_JSX_DEFAULT_TIMEOUT_MS
const readyTimeoutMs = limits.readyTimeoutMs ?? DESIGN_JSX_DEFAULT_READY_TIMEOUT_MS
let settled = false
let timer: ReturnType<typeof setTimeout>
const finish = (result: DesignJSXSandboxResult) => {
if (settled) return
settled = true
clearTimeout(timer)
window.removeEventListener('message', onMessage)
iframe.remove()
// eslint-disable-next-line promise/no-multiple-resolved -- settled guard makes completion single-shot
resolve(result)
}
const onMessage = (event: MessageEvent) => {
if (event.source !== iframe.contentWindow || event.origin !== 'null') return
const message = event.data as {
type?: string
id?: string
ok?: boolean
value?: unknown
error?: string
}
if (message.type === 'open-pencil-design-jsx-ready') {
clearTimeout(timer)
// eslint-disable-next-line promise/no-multiple-resolved -- finish has a settled guard
timer = setTimeout(
() => finish({ ok: false, error: 'Design JSX execution timed out.' }),
timeoutMs
)
iframe.contentWindow?.postMessage({ type: 'open-pencil-design-jsx-run', id, code }, '*')
return
}
if (message.type !== 'open-pencil-design-jsx-result' || message.id !== id) return
if (!message.ok) {
finish({ ok: false, error: message.error ?? 'Design JSX execution failed.' })
return
}
try {
const roots = validateDesignJSXOutput(message.value, validationLimits)
finish({ ok: true, roots })
} catch (error) {
finish({ ok: false, error: error instanceof Error ? error.message : String(error) })
}
}
timer = setTimeout(
() => finish({ ok: false, error: 'Design JSX sandbox did not become ready.' }),
readyTimeoutMs
)
window.addEventListener('message', onMessage)
})
}

View file

@ -0,0 +1,50 @@
export const DESIGN_JSX_MAX_SOURCE_BYTES = 256_000
export const DESIGN_JSX_MAX_OUTPUT_BYTES = 2_000_000
export const DESIGN_JSX_MAX_ELEMENTS = 5_000
export const DESIGN_JSX_MAX_DEPTH = 100
export const DESIGN_JSX_MAX_ARRAY_LENGTH = 5_000
export const DESIGN_JSX_MAX_OBJECT_KEYS = 1_000
export const DESIGN_JSX_MAX_STRING_LENGTH = 100_000
export const DESIGN_JSX_DEFAULT_TIMEOUT_MS = 1_000
export const DESIGN_JSX_DEFAULT_READY_TIMEOUT_MS = 5_000
export type DesignJSXValidationLimits = {
outputBytes: number
elements: number
depth: number
arrayLength: number
objectKeys: number
stringLength: number
}
export type DesignJSXSandboxLimits = {
sourceBytes?: number
outputBytes?: number
elements?: number
depth?: number
arrayLength?: number
objectKeys?: number
stringLength?: number
timeoutMs?: number
readyTimeoutMs?: number
}
export function resolveDesignJSXValidationLimits(
limits: DesignJSXSandboxLimits
): DesignJSXValidationLimits {
return {
outputBytes: limits.outputBytes ?? DESIGN_JSX_MAX_OUTPUT_BYTES,
elements: limits.elements ?? DESIGN_JSX_MAX_ELEMENTS,
depth: limits.depth ?? DESIGN_JSX_MAX_DEPTH,
arrayLength: limits.arrayLength ?? DESIGN_JSX_MAX_ARRAY_LENGTH,
objectKeys: limits.objectKeys ?? DESIGN_JSX_MAX_OBJECT_KEYS,
stringLength: limits.stringLength ?? DESIGN_JSX_MAX_STRING_LENGTH
}
}
export type DesignJSXHelperDescriptor = {
__openPencilHelper: string
args: unknown[]
}
export type DesignJSXSandboxResult = { ok: true; roots: unknown[] } | { ok: false; error: string }

View file

@ -0,0 +1,108 @@
import type { DesignJSXValidationLimits } from '@/app/code/sandbox/types'
const BLOCKED_KEYS = new Set(['__proto__', 'prototype', 'constructor'])
type ValidationState = {
elements: number
bytes: number
}
type PlainRecord = { [key: string]: unknown }
function isPlainRecord(value: unknown): value is PlainRecord {
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false
const prototype = Object.getPrototypeOf(value)
return prototype === Object.prototype || prototype === null
}
function addBytes(state: ValidationState, limits: DesignJSXValidationLimits, bytes: number): void {
state.bytes += bytes
if (state.bytes > limits.outputBytes) throw new Error('Design JSX output is too large.')
}
function validatePrimitive(
value: unknown,
limits: DesignJSXValidationLimits,
state: ValidationState
): boolean {
if (value === null) {
addBytes(state, limits, 4)
return true
}
if (typeof value === 'boolean') {
addBytes(state, limits, value ? 4 : 5)
return true
}
if (typeof value === 'number') {
if (!Number.isFinite(value)) throw new Error('Design JSX output contains an invalid number.')
addBytes(state, limits, 8)
return true
}
if (typeof value === 'string') {
if (value.length > limits.stringLength) {
throw new Error('Design JSX output contains a string that is too long.')
}
addBytes(state, limits, new TextEncoder().encode(value).byteLength)
return true
}
return false
}
function validateRecord(
record: PlainRecord,
limits: DesignJSXValidationLimits,
state: ValidationState,
depth: number
): void {
const keys = Object.keys(record)
addBytes(state, limits, 2 + Math.max(0, keys.length - 1))
if (keys.length > limits.objectKeys) {
throw new Error('Design JSX output contains too many object properties.')
}
for (const key of keys) {
if (BLOCKED_KEYS.has(key)) throw new Error(`Design JSX output contains blocked key "${key}".`)
}
if ('type' in record && 'props' in record && 'children' in record) {
state.elements += 1
if (state.elements > limits.elements)
throw new Error('Design JSX output has too many elements.')
}
for (const [key, item] of Object.entries(record)) {
addBytes(state, limits, key.length)
validateValue(item, limits, state, depth + 1)
}
}
function validateValue(
value: unknown,
limits: DesignJSXValidationLimits,
state: ValidationState,
depth: number
): void {
if (depth > limits.depth) throw new Error('Design JSX output is too deeply nested.')
if (validatePrimitive(value, limits, state)) return
if (Array.isArray(value)) {
addBytes(state, limits, 2 + Math.max(0, value.length - 1))
if (value.length > limits.arrayLength) {
throw new Error('Design JSX output contains an array that is too long.')
}
for (const item of value) validateValue(item, limits, state, depth + 1)
return
}
if (!isPlainRecord(value)) {
throw new Error('Design JSX output must contain plain structured data only.')
}
validateRecord(value, limits, state, depth)
}
export function validateDesignJSXOutput(
value: unknown,
limits: DesignJSXValidationLimits
): unknown[] {
if (value === undefined) throw new Error('Design JSX must return an OpenPencil element.')
const roots = Array.isArray(value) ? value : [value]
if (roots.length === 0) throw new Error('Design JSX must return an OpenPencil element.')
const state: ValidationState = { elements: 0, bytes: 0 }
validateValue(roots, limits, state, 0)
return roots
}

17
src/app/code/templates.ts Normal file
View file

@ -0,0 +1,17 @@
export type CodeSource = 'design-jsx' | 'tailwind-jsx' | 'html-css'
export const DESIGN_JSX_STARTER_SOURCE = '<Frame name="New frame" w={320} h={240} fill="#ffffff" />'
export const HTML_CSS_STARTER_SOURCE = `<style>
.card {
padding: 16px;
border-radius: 12px;
background: white;
}
</style>
<div class="card">Hello</div>`
export function starterSourceFor(language: CodeSource): string {
return language === 'html-css' ? HTML_CSS_STARTER_SOURCE : DESIGN_JSX_STARTER_SOURCE
}

View file

@ -1,235 +1,299 @@
<script setup lang="ts">
import { ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui'
import { useClipboard } from '@vueuse/core'
import { computed, ref, watch } from 'vue'
import { useClipboard, useDebounceFn } from '@vueuse/core'
import { computed, defineAsyncComponent, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
import { tv } from 'tailwind-variants'
import { JSX_REFERENCE, selectionToJSX } from '@open-pencil/core/design-jsx'
import { useI18n, useSceneComputed } from '@open-pencil/vue'
import { highlightJSX } from '@/app/code/highlight'
import {
commitDesignJSXSession,
createDesignJSXEditSession,
previewDesignJSX,
resetDesignJSXPreview,
type DesignJSXEditSession
} from '@/app/code/live-preview'
import {
commitDOMCodeSession,
createDOMCodeSession,
previewDOMCode,
resetDOMCodePreview,
type DOMCodeSession
} from '@/app/code/dom-preview'
import { starterSourceFor, type CodeSource } from '@/app/code/templates'
import { useEditorStore } from '@/app/editor/active-store'
import AppPlaceholder from '@/components/ui/AppPlaceholder.vue'
import AppSelect from '@/components/ui/AppSelect.vue'
import AppTextButton from '@/components/ui/AppTextButton.vue'
import Tip from '@/components/ui/Tip.vue'
import statusTheme from '@/theme/status'
import type { JSXFormat } from '@open-pencil/core/design-jsx'
const CodeEditor = defineAsyncComponent(() => import('@/components/code-editor/CodeEditor.vue'))
const { active = true } = defineProps<{ active?: boolean }>()
const store = useEditorStore()
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
const editorActive = computed(() => active)
const { dialogs } = useI18n()
const jsxFormat = ref<JSXFormat>('openpencil')
const showImporter = ref(false)
const importHTML = ref('')
const importCSS = ref('')
const importError = ref('')
const importing = ref(false)
const { copy, copied } = useClipboard({ copiedDuring: 2000 })
const { copy: copyReference, copied: copiedReference } = useClipboard({ copiedDuring: 2000 })
const source = ref<CodeSource>('design-jsx')
const draft = ref('')
const baseline = ref('')
const status = ref<'idle' | 'updating' | 'updated' | 'error'>('idle')
const error = ref('')
const designSession = shallowRef<DesignJSXEditSession | null>(null)
let domSession: DOMCodeSession | null = null
let previewQueue = Promise.resolve()
let pendingPreview: Promise<void> | undefined
let commitPromise: Promise<void> | undefined
let updateVersion = 0
let disposing = false
function toggleFormat() {
jsxFormat.value = jsxFormat.value === 'openpencil' ? 'tailwind' : 'openpencil'
}
const jsxCode = useSceneComputed(() => {
if (!active) return ''
const generatedJSX = useSceneComputed(() => {
if (!editorActive.value || source.value === 'html-css' || designSession.value) return ''
void store.state.sceneVersion
const ids = [...store.state.selectedIds]
if (ids.length === 0) return ''
return selectionToJSX(ids, store.graph, jsxFormat.value)
if (ids.length === 0) return starterSourceFor(source.value)
return selectionToJSX(
ids,
store.graph,
source.value === 'tailwind-jsx' ? 'tailwind' : 'openpencil'
)
})
const highlightedLines = computed(() => {
if (!jsxCode.value) return []
return jsxCode.value.split('\n').map(highlightJSX)
const sourceOptions = computed(() => [
{ value: 'design-jsx' as const, label: dialogs.value.codeSourceDesignJSX },
{ value: 'tailwind-jsx' as const, label: dialogs.value.codeSourceTailwindJSX },
{ value: 'html-css' as const, label: dialogs.value.codeSourceHTMLCSS }
])
const readOnly = computed(() => source.value === 'tailwind-jsx')
const dirty = computed(() => draft.value !== baseline.value)
const editorLabel = computed(() =>
source.value === 'html-css'
? dialogs.value.codeEditorHTMLCSSLabel
: dialogs.value.codeEditorDesignLabel
)
const statusTone = computed(() => {
if (status.value === 'error') return 'error'
if (status.value === 'updated') return 'success'
return 'neutral'
})
const statusStyles = computed(() => tv(statusTheme)({ tone: statusTone.value }))
const statusText = computed(() => {
if (status.value === 'updating') return dialogs.value.codeUpdating
if (status.value === 'error') return dialogs.value.codePreviewFailed
if (dirty.value) return dialogs.value.codeUpdatedLive
return dialogs.value.jsxUpToDate
})
const { copy: copyRef, copied: copiedRef } = useClipboard({ copiedDuring: 2000 })
const canImport = computed(() => importHTML.value.trim().length > 0)
watch([importHTML, importCSS], () => {
importError.value = ''
})
function errorMessage(error: unknown) {
if (error instanceof Error && error.message) return error.message
return dialogs.value.importFailed
}
function toggleImporter() {
showImporter.value = !showImporter.value
}
async function pasteImportHTML() {
try {
importError.value = ''
importHTML.value = await navigator.clipboard.readText()
} catch (e) {
importError.value = errorMessage(e)
function beginDesignSession(): DesignJSXEditSession | null {
if (designSession.value) return designSession.value
const result = createDesignJSXEditSession(store)
if (!result.ok) {
status.value = 'error'
error.value = result.error
return null
}
designSession.value = result.session
return result.session
}
async function importCode() {
if (!canImport.value || importing.value) return
function beginDOMSession(): DOMCodeSession {
domSession ??= createDOMCodeSession(store)
return domSession
}
async function commitCurrentSession(): Promise<void> {
if (commitPromise) return commitPromise
const operation = (async () => {
await pendingPreview
await previewQueue
updateVersion += 1
const design = designSession.value
const dom = domSession
designSession.value = null
domSession = null
if (design) commitDesignJSXSession(store, design)
if (dom) commitDOMCodeSession(store, dom)
pendingPreview = undefined
})()
commitPromise = operation
try {
importing.value = true
importError.value = ''
await store.importDOMText(importHTML.value, {
cssText: importCSS.value.trim() || undefined
})
} catch (e) {
importError.value = errorMessage(e)
await operation
} finally {
importing.value = false
if (commitPromise === operation) commitPromise = undefined
}
}
function copyCode() {
copy(jsxCode.value)
async function runPreview(version: number): Promise<void> {
if (version !== updateVersion || readOnly.value || !draft.value.trim()) return
status.value = 'updating'
error.value = ''
let result: { ok: true } | { ok: false; error: string }
if (source.value === 'html-css') {
result = await previewDOMCode(store, beginDOMSession(), draft.value)
} else {
const session = beginDesignSession()
result = session
? await previewDesignJSX(store, session, draft.value)
: { ok: false, error: error.value }
}
if (version !== updateVersion) return
if (!result.ok) {
status.value = 'error'
error.value = result.error
return
}
status.value = 'updated'
}
function copyReference() {
copyRef(JSX_REFERENCE)
const schedulePreview = useDebounceFn(
(version: number) => {
previewQueue = previewQueue.then(() => runPreview(version))
return previewQueue
},
350,
{ maxWait: 1_000 }
)
function updateDraft(value: string): void {
draft.value = value
error.value = ''
updateVersion += 1
pendingPreview = schedulePreview(updateVersion)
}
async function resetDraft(): Promise<void> {
updateVersion += 1
await pendingPreview
await previewQueue
const design = designSession.value
const dom = domSession
designSession.value = null
domSession = null
if (design) resetDesignJSXPreview(store, design)
if (dom) resetDOMCodePreview(store, dom)
pendingPreview = undefined
draft.value = baseline.value
error.value = ''
status.value = 'idle'
}
async function changeSource(next: CodeSource): Promise<void> {
if (next === source.value) return
await commitCurrentSession()
source.value = next
const initial = next === 'html-css' ? starterSourceFor(next) : generatedFor(next)
baseline.value = initial
draft.value = initial
error.value = ''
status.value = 'idle'
}
function generatedFor(next: Exclude<CodeSource, 'html-css'>): string {
const ids = [...store.state.selectedIds]
if (ids.length === 0) return starterSourceFor(next)
return selectionToJSX(ids, store.graph, next === 'tailwind-jsx' ? 'tailwind' : 'openpencil')
}
watch(
generatedJSX,
(value) => {
if (source.value === 'html-css' || designSession.value || dirty.value) return
baseline.value = value
draft.value = value
},
{ immediate: true }
)
onBeforeUnmount(() => {
disposing = true
void commitCurrentSession()
})
watch(
() => editorActive.value,
(value) => {
if (!value && !disposing) void commitCurrentSession()
}
)
</script>
<template>
<div data-test-id="code-panel-root" class="flex min-h-0 flex-1 flex-col">
<div
v-if="jsxCode"
data-test-id="code-panel-header"
class="flex shrink-0 items-center justify-between border-b border-border px-3 py-1.5"
>
<div class="flex items-center gap-1.5">
<span class="text-[11px] text-muted">JSX</span>
<header class="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
<AppSelect
:model-value="source"
:options="sourceOptions"
:label="dialogs.codeSource"
data-test-id="code-panel-source"
:ui="{ trigger: 'h-7 min-w-0 flex-1 text-[11px]' }"
@update:model-value="changeSource"
/>
<Tip v-if="source !== 'html-css'" :label="dialogs.copyJSXReference">
<AppTextButton
data-test-id="code-panel-format-toggle"
:ui="{ base: 'rounded px-1.5 py-0.5 text-[11px] hover:bg-hover' }"
@click="toggleFormat"
data-test-id="code-panel-copy-ref"
:ui="{ base: 'rounded p-1.5 text-[11px] hover:bg-hover' }"
@click="copyReference(JSX_REFERENCE)"
>
{{ jsxFormat === 'openpencil' ? 'OpenPencil' : 'Tailwind' }}
<icon-lucide-check v-if="copiedReference" class="size-3 text-[var(--color-success)]" />
<icon-lucide-book-open v-else class="size-3" />
</AppTextButton>
</div>
</Tip>
</header>
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
<CodeEditor
:model-value="draft"
:language="source"
:read-only="readOnly"
:label="editorLabel"
@update:model-value="updateDraft"
/>
</div>
<div
v-if="error"
role="alert"
data-test-id="code-panel-error"
class="shrink-0 border-t border-[var(--color-error-border)] bg-[var(--color-error-bg)] px-3 py-2 text-[11px] leading-snug text-[var(--color-error)]"
>
{{ error }}
</div>
<footer
class="flex shrink-0 items-center justify-between gap-2 border-t border-border px-3 py-2"
>
<span
data-test-id="code-panel-status"
class="min-w-0 truncate"
:data-tone="statusTone"
:class="statusStyles.text()"
>
{{ readOnly ? dialogs.codeGeneratedReadOnly : statusText }}
</span>
<div class="flex items-center gap-1">
<AppTextButton
data-test-id="code-panel-import-toggle"
:ui="{ base: 'flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] hover:bg-hover' }"
@click="toggleImporter"
v-if="dirty && !readOnly"
data-test-id="code-panel-reset"
:ui="{
base: 'flex items-center gap-1 rounded px-2 py-1 text-[11px] hover:bg-hover'
}"
@click="resetDraft"
>
<icon-lucide-file-input class="size-3" />
{{ dialogs.importLabel }}
<icon-lucide-rotate-ccw class="size-3" />
{{ dialogs.codeReset }}
</AppTextButton>
<Tip :label="dialogs.copyJSXReference">
<AppTextButton
data-test-id="code-panel-copy-ref"
:ui="{
base: 'flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] hover:bg-hover'
}"
@click="copyReference"
>
<icon-lucide-check v-if="copiedRef" class="size-3 text-[var(--color-success)]" />
<icon-lucide-book-open v-else class="size-3" />
</AppTextButton>
</Tip>
<AppTextButton
data-test-id="code-panel-copy"
:ui="{ base: 'flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] hover:bg-hover' }"
@click="copyCode"
:ui="{ base: 'flex items-center gap-1 rounded px-2 py-1 text-[11px] hover:bg-hover' }"
@click="copy(draft)"
>
<icon-lucide-check v-if="copied" class="size-3 text-[var(--color-success)]" />
<icon-lucide-copy v-else class="size-3" />
{{ copied ? dialogs.copied : dialogs.copy }}
</AppTextButton>
</div>
</div>
<div
v-if="showImporter || !jsxCode"
data-test-id="code-panel-importer"
class="shrink-0 border-b border-border p-3"
>
<div class="mb-2 flex items-center justify-between gap-2">
<div class="min-w-0">
<div class="text-xs font-medium text-surface">{{ dialogs.importHTMLCSS }}</div>
<div class="text-[11px] text-muted">
{{ dialogs.importHTMLCSSDescription }}
</div>
</div>
<AppTextButton
data-test-id="code-panel-paste-import"
:ui="{ base: 'rounded px-1.5 py-0.5 text-[11px] hover:bg-hover' }"
@click="pasteImportHTML"
>
{{ dialogs.paste }}
</AppTextButton>
</div>
<textarea
v-model="importHTML"
data-test-id="code-panel-import-html"
class="mb-2 h-28 w-full resize-none rounded border border-border bg-panel px-2 py-1.5 font-mono text-xs text-surface outline-none placeholder:text-muted/50 focus:border-accent"
placeholder='<div class="card">Hello</div>'
spellcheck="false"
/>
<textarea
v-model="importCSS"
data-test-id="code-panel-import-css"
class="mb-2 h-20 w-full resize-none rounded border border-border bg-panel px-2 py-1.5 font-mono text-xs text-surface outline-none placeholder:text-muted/50 focus:border-accent"
placeholder=".card { width: 240px; padding: 16px; border-radius: 12px; background: white; }"
spellcheck="false"
/>
<div
v-if="importError"
data-test-id="code-panel-import-error"
class="mb-2 rounded border border-red-500/40 bg-red-500/10 px-2 py-1.5 text-[11px] text-red-200"
>
{{ importError }}
</div>
<div class="flex items-center justify-between gap-2">
<span class="text-[11px] text-muted">{{ dialogs.importReplacesDocument }}</span>
<AppTextButton
data-test-id="code-panel-import"
:ui="{
base: [
'rounded px-2 py-1 text-[11px]',
canImport && !importing
? 'bg-accent text-black hover:bg-accent/90'
: 'cursor-not-allowed opacity-50'
].join(' ')
}"
@click="importCode"
>
{{ importing ? dialogs.importing : dialogs.importToCanvas }}
</AppTextButton>
</div>
</div>
<AppPlaceholder
v-if="!jsxCode"
data-test-id="code-panel-empty"
:label="dialogs.selectLayerForJSX"
>
<template #icon>
<icon-lucide-code-2 class="size-5" />
</template>
</AppPlaceholder>
<ScrollAreaRoot v-else data-test-id="code-panel" class="min-h-0 flex-1">
<ScrollAreaViewport class="code-highlight size-full">
<div class="p-3">
<div v-for="(html, i) in highlightedLines" :key="i" class="flex text-xs leading-5">
<span
class="mr-3 shrink-0 text-right text-muted/40 select-none"
style="min-width: 1.5em"
>{{ i + 1 }}</span
>
<pre
class="m-0 min-w-0 flex-1 break-words whitespace-pre-wrap"
><code v-html="html" /></pre>
</div>
</div>
</ScrollAreaViewport>
<ScrollAreaScrollbar orientation="vertical" class="flex w-1.5 touch-none p-px select-none">
<ScrollAreaThumb class="relative flex-1 rounded-full bg-white/10" />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</footer>
</div>
</template>

View file

@ -0,0 +1,163 @@
<script setup lang="ts">
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'
import { defaultKeymap, history, historyKeymap, redo, undo } from '@codemirror/commands'
import { html } from '@codemirror/lang-html'
import { javascript } from '@codemirror/lang-javascript'
import {
bracketMatching,
defaultHighlightStyle,
foldGutter,
foldKeymap,
indentOnInput,
syntaxHighlighting
} from '@codemirror/language'
import { lintKeymap } from '@codemirror/lint'
import { searchKeymap } from '@codemirror/search'
import { Compartment, EditorState, Transaction, type Extension } from '@codemirror/state'
import {
drawSelection,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers
} from '@codemirror/view'
import { onBeforeUnmount, onMounted, useTemplateRef, watch } from 'vue'
import { designJSXExtensions } from '@/components/code-editor/extensions'
import type { CodeEditorLanguage } from '@/components/code-editor/types'
const {
modelValue,
language = 'design-jsx',
readOnly = false,
label = 'Code'
} = defineProps<{
modelValue: string
language?: CodeEditorLanguage
readOnly?: boolean
label?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const host = useTemplateRef('host')
const languageCompartment = new Compartment()
const editableCompartment = new Compartment()
const labelCompartment = new Compartment()
let editor: EditorView | undefined
let externalUpdate = false
function languageExtensions(language: CodeEditorLanguage): Extension {
if (language === 'html-css') return html()
return [
javascript({ jsx: true, typescript: true }),
...(language === 'design-jsx' ? designJSXExtensions() : [])
]
}
function editableExtensions(readOnly: boolean): Extension {
return [EditorState.readOnly.of(readOnly), EditorView.editable.of(!readOnly)]
}
onMounted(() => {
const parent = host.value
if (!parent) return
editor = new EditorView({
doc: modelValue,
parent,
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
drawSelection(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
bracketMatching(),
closeBrackets(),
highlightActiveLine(),
keymap.of([
{ key: 'Ctrl-z', run: undo },
{ key: 'Ctrl-Shift-z', run: redo },
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
]),
languageCompartment.of(languageExtensions(language)),
editableCompartment.of(editableExtensions(readOnly)),
labelCompartment.of(EditorView.contentAttributes.of({ 'aria-label': label })),
EditorView.lineWrapping,
EditorView.theme({
'&': { height: '100%', backgroundColor: 'transparent', color: 'var(--color-surface)' },
'.cm-scroller': { overflow: 'auto', fontFamily: 'var(--font-mono)' },
'.cm-content': { padding: '12px 0', caretColor: 'var(--color-accent)' },
'.cm-line': { padding: '0 12px' },
'.cm-gutters': {
backgroundColor: 'transparent',
color: 'color-mix(in srgb, var(--color-muted) 45%, transparent)',
border: 'none'
},
'&.cm-focused': { outline: 'none' },
'.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
backgroundColor: 'color-mix(in srgb, var(--color-accent) 22%, transparent)'
}
}),
EditorView.updateListener.of((update) => {
if (!update.docChanged || externalUpdate) return
emit('update:modelValue', update.state.doc.toString())
})
]
})
})
watch(
() => modelValue,
(value) => {
if (!editor || editor.state.doc.toString() === value) return
externalUpdate = true
editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: value },
annotations: Transaction.addToHistory.of(false)
})
externalUpdate = false
}
)
watch(
() => language,
(language) =>
editor?.dispatch({ effects: languageCompartment.reconfigure(languageExtensions(language)) })
)
watch(
() => readOnly,
(readOnly) =>
editor?.dispatch({ effects: editableCompartment.reconfigure(editableExtensions(readOnly)) })
)
watch(
() => label,
(label) =>
editor?.dispatch({
effects: labelCompartment.reconfigure(
EditorView.contentAttributes.of({ 'aria-label': label })
)
})
)
onBeforeUnmount(() => editor?.destroy())
</script>
<template>
<div ref="host" data-slot="code-editor" class="min-h-0 flex-1 overflow-hidden text-xs" />
</template>

View file

@ -0,0 +1,114 @@
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete'
import { autocompletion } from '@codemirror/autocomplete'
import { syntaxTree } from '@codemirror/language'
import { linter, type Diagnostic } from '@codemirror/lint'
import type { Extension } from '@codemirror/state'
import type { EditorView } from '@codemirror/view'
import {
DESIGN_JSX_ELEMENTS,
DESIGN_JSX_HELPERS,
DESIGN_JSX_PROPERTIES
} from '@open-pencil/core/design-jsx'
const ELEMENTS = DESIGN_JSX_ELEMENTS.map(({ name }) => name)
const PROPS = DESIGN_JSX_PROPERTIES.map(({ name }) => name)
const HELPERS = DESIGN_JSX_HELPERS.map(({ name }) => name)
const BUILTIN_ELEMENTS = new Set(ELEMENTS)
const PROPERTY_SET = new Set(PROPS)
const HELPER_SET = new Set(HELPERS)
function completionType(label: string): 'class' | 'function' | 'property' {
if (BUILTIN_ELEMENTS.has(label)) return 'class'
if (HELPER_SET.has(label)) return 'function'
return 'property'
}
function completeDesignJSX(context: CompletionContext): CompletionResult | null {
const before = context.matchBefore(/[\w]*$/)
if (!before || (before.from === before.to && !context.explicit)) return null
const documentLine = context.state.doc.lineAt(context.pos)
const line = documentLine.text.slice(0, context.pos - documentLine.from)
const isInsideTag = line.lastIndexOf('<') > line.lastIndexOf('>')
const labels = isInsideTag ? [...ELEMENTS, ...PROPS] : HELPERS
return {
from: before.from,
options: labels.map((label) => ({
label,
type: completionType(label)
}))
}
}
function localComponentNames(view: EditorView): Set<string> {
const names = new Set<string>()
const source = view.state.doc.toString()
for (const match of source.matchAll(/\b(?:const|let|var|function)\s+([A-Z][\w$]*)/g)) {
const name = match[1]
if (name) names.add(name)
}
return names
}
function semanticDiagnostics(view: EditorView): Diagnostic[] {
const diagnostics: Diagnostic[] = []
const knownElements = new Set([...BUILTIN_ELEMENTS, ...localComponentNames(view)])
const tree = syntaxTree(view.state)
tree.iterate({
enter(node) {
if (node.name !== 'JSXIdentifier') return
const parentName = node.node.parent?.name
const name = view.state.doc.sliceString(node.from, node.to)
if (parentName === 'JSXAttribute' && !PROPERTY_SET.has(name)) {
diagnostics.push({
from: node.from,
to: node.to,
severity: 'warning',
message: `Unknown OpenPencil property “${name}”.`
})
return
}
if (
(parentName === 'JSXOpenTag' ||
parentName === 'JSXSelfClosingTag' ||
parentName === 'JSXCloseTag') &&
/^[A-Z]/.test(name) &&
!knownElements.has(name)
) {
diagnostics.push({
from: node.from,
to: node.to,
severity: 'warning',
message: `Unknown OpenPencil element <${name}>.`
})
}
}
})
return diagnostics
}
function syntaxDiagnostics(view: EditorView): Diagnostic[] {
const errors: Diagnostic[] = []
const tree = syntaxTree(view.state)
tree.iterate({
enter(node) {
if (node.type.isError) {
errors.push({
from: node.from,
to: Math.max(node.from + 1, node.to),
severity: 'error',
message: 'Invalid JSX syntax.'
})
}
}
})
return errors
}
export function designJSXExtensions(): Extension[] {
return [
autocompletion({ override: [completeDesignJSX] }),
linter((view) => [...syntaxDiagnostics(view), ...semanticDiagnostics(view)])
]
}

View file

@ -0,0 +1,3 @@
import type { CodeSource } from '@/app/code/templates'
export type CodeEditorLanguage = CodeSource

View file

@ -36,11 +36,13 @@ function setLayoutMode(mode: string) {
@change="setLayoutMode"
>
<template #option="{ option }">
<Tip :label="option.label" class="flex items-center justify-center">
<icon-lucide-move v-if="option.value === 'NONE'" class="size-3.5" />
<icon-lucide-rows-2 v-else-if="option.value === 'VERTICAL'" class="size-3.5" />
<icon-lucide-columns-2 v-else-if="option.value === 'HORIZONTAL'" class="size-3.5" />
<icon-lucide-layout-grid v-else class="size-3.5" />
<Tip :label="option.label">
<span class="flex items-center justify-center">
<icon-lucide-move v-if="option.value === 'NONE'" class="size-3.5" />
<icon-lucide-rows-2 v-else-if="option.value === 'VERTICAL'" class="size-3.5" />
<icon-lucide-columns-2 v-else-if="option.value === 'HORIZONTAL'" class="size-3.5" />
<icon-lucide-layout-grid v-else class="size-3.5" />
</span>
</Tip>
</template>
</SegmentedControl>

View file

@ -93,10 +93,12 @@ function isGrid(grid: LayoutGrid): boolean {
"
>
<template #option="{ option }">
<Tip :label="option.label" class="flex items-center justify-center">
<icon-lucide-columns-3 v-if="option.value === 'COLUMNS'" class="size-3.5" />
<icon-lucide-rows-3 v-else-if="option.value === 'ROWS'" class="size-3.5" />
<icon-lucide-layout-grid v-else class="size-3.5" />
<Tip :label="option.label">
<span class="flex items-center justify-center">
<icon-lucide-columns-3 v-if="option.value === 'COLUMNS'" class="size-3.5" />
<icon-lucide-rows-3 v-else-if="option.value === 'ROWS'" class="size-3.5" />
<icon-lucide-layout-grid v-else class="size-3.5" />
</span>
</Tip>
</template>
</SegmentedControl>

View file

@ -48,10 +48,12 @@ function setMode(value: TextResizeMode) {
@change="setMode($event as TextResizeMode)"
>
<template #option="{ option }">
<Tip :label="option.label" class="flex items-center justify-center">
<icon-lucide-move-horizontal v-if="option.value === 'AUTO_WIDTH'" class="size-3.5" />
<icon-lucide-wrap-text v-else-if="option.value === 'AUTO_HEIGHT'" class="size-3.5" />
<icon-lucide-lock v-else class="size-3.5" />
<Tip :label="option.label">
<span class="flex items-center justify-center">
<icon-lucide-move-horizontal v-if="option.value === 'AUTO_WIDTH'" class="size-3.5" />
<icon-lucide-wrap-text v-else-if="option.value === 'AUTO_HEIGHT'" class="size-3.5" />
<icon-lucide-lock v-else class="size-3.5" />
</span>
</Tip>
</template>
</SegmentedControl>

View file

@ -1,10 +1,11 @@
import { test, expect, useEditorSetup } from '#tests/e2e/fixtures'
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import { getMobileDrawerSnap } from '#tests/helpers/code-panel'
const editor = useEditorSetup()
test.use({ viewport: { width: 390, height: 844 } })
test('closed mobile Code drawer defers JSX until reopened', async () => {
test('mobile Code drawer mounts one live editor only while open', async () => {
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
@ -13,21 +14,12 @@ test('closed mobile Code drawer defers JSX until reopened', async () => {
})
await editor.page.getByTestId('mobile-ribbon-code').click()
await expect(editor.page.getByTestId('code-panel')).toBeVisible()
const panel = editor.page.getByTestId('code-panel-root')
await expect(panel).toBeVisible()
await expect(panel.locator('[data-slot="code-editor"]')).toHaveCount(1)
await expect(panel.locator('.cm-content')).toContainText('Frame')
await expect(panel.locator('.cm-lineNumbers')).toBeVisible()
await editor.page.getByTestId('mobile-ribbon-code').click()
await expect
.poll(() => editor.page.evaluate(() => window.openPencil?.getStore?.().state.mobileDrawerSnap))
.toBe('closed')
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
store.clearSelection()
const rectangleId = store.createShape('RECTANGLE', 120, 0, 100, 100)
store.select([rectangleId])
})
await editor.page.getByTestId('mobile-ribbon-code').click()
await expect(editor.page.getByTestId('code-panel')).toContainText('Rectangle')
await expect.poll(() => getMobileDrawerSnap(editor.page)).toBe('closed')
})

View file

@ -1,7 +1,21 @@
import { expect, test, useEditorSetup } from '#tests/e2e/fixtures'
import {
getCodeNodeSummaries,
getFirstSelectedNodeId,
getUndoLabel,
hasNode,
hasNodeNamed,
waitForNode,
waitForNodeNamed
} from '#tests/helpers/code-panel'
const editor = useEditorSetup()
test.beforeEach(async () => {
await editor.page.reload()
await editor.canvas.waitForInit()
})
function codeTab() {
return editor.page.getByTestId('properties-tab-code')
}
@ -11,29 +25,32 @@ function designTab() {
}
function codePanel() {
return editor.page.getByTestId('code-panel')
return editor.page.getByTestId('code-panel-root')
}
function codePanelEmpty() {
return editor.page.getByTestId('code-panel-empty')
function codeEditor() {
return editor.page.locator('[data-slot="code-editor"] .cm-content')
}
function formatToggle() {
return editor.page.getByTestId('code-panel-format-toggle')
async function openCodePanel() {
await codeTab().click()
await expect(codeEditor()).toBeVisible()
}
function copyButton() {
return editor.page.getByTestId('code-panel-copy')
async function selectSource(label: string) {
await editor.page.getByTestId('code-panel-source').click()
await editor.page.getByRole('option', { name: label }).click()
}
test('inactive Code tab skips JSX generation for large selections', async () => {
test('inactive Code tab defers JSX generation for large selections', async () => {
const selectionDuration = await editor.page.evaluate(async () => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const pageId = store.state.currentPageId
const ids: string[] = []
for (let frameIndex = 0; frameIndex < 50; frameIndex++) {
const frame = store.graph.createNode('FRAME', pageId, { name: `Frame ${frameIndex}` })
const frame = store.graph.createNode('FRAME', store.state.currentPageId, {
name: `Frame ${frameIndex}`
})
ids.push(frame.id)
for (let childIndex = 0; childIndex < 100; childIndex++) {
store.graph.createNode('RECTANGLE', frame.id, { name: `Child ${childIndex}` })
@ -46,134 +63,152 @@ test('inactive Code tab skips JSX generation for large selections', async () =>
})
return performance.now() - startedAt
})
expect(selectionDuration).toBeLessThan(1000)
await expect(designTab()).toHaveAttribute('data-state', 'active')
await codeTab().click()
await expect(codePanel()).toContainText('Frame')
await editor.page.evaluate(() => window.openPencil?.getStore?.().clearSelection())
await designTab().click()
await openCodePanel()
await expect(codeEditor()).toContainText('Frame')
})
test('Code tab shows empty state with no selection', async () => {
await codeTab().click()
await expect(codePanelEmpty()).toBeVisible()
await expect(codePanelEmpty()).toContainText('Select a layer')
test('shows one editor with line numbers and no import form', async () => {
await openCodePanel()
await expect(codePanel().locator('[data-slot="code-editor"]')).toHaveCount(1)
await expect(codePanel().locator('.cm-lineNumbers')).toBeVisible()
await expect(codePanel().locator('textarea')).toHaveCount(0)
await expect(codePanel().getByRole('button', { name: /^Import$/ })).toHaveCount(0)
})
test('selecting a rectangle shows JSX code', async () => {
test('desktop exposes one Code panel and one editor', async () => {
await openCodePanel()
await expect(codePanel()).toHaveCount(1)
await expect(codePanel().locator('.cm-editor')).toHaveCount(1)
})
test('live previews Design JSX and keeps one undo transaction', async () => {
await editor.canvas.drawRect(100, 100, 200, 150)
await editor.canvas.waitForRender()
const originalId = await getFirstSelectedNodeId(editor.page)
await openCodePanel()
await codeEditor().fill('<Frame name="Live JSX" w={320} h={180} fill="#ffffff" />')
await waitForNodeNamed(editor.page, 'Live JSX')
await codeEditor().fill('<Frame name="Live JSX final" w={360} h={180} fill="#ffffff" />')
await waitForNodeNamed(editor.page, 'Live JSX final')
await expect(codePanel()).toBeVisible()
const code = await codePanel().textContent()
expect(code).toContain('Rectangle')
await designTab().click()
await editor.page.waitForTimeout(50)
await editor.page.keyboard.press('Meta+z')
await waitForNode(editor.page, originalId)
expect(await hasNode(editor.page, originalId)).toBe(true)
expect(await hasNodeNamed(editor.page, 'Live JSX final')).toBe(false)
})
test('format toggle switches between OpenPencil and Tailwind', async () => {
await expect(formatToggle()).toBeVisible()
test('Ctrl+Z edits the CodeMirror draft before touching the canvas transaction', async () => {
await openCodePanel()
const initial = await codeEditor().textContent()
await codeEditor().pressSequentially('\n// local draft')
await expect(codeEditor()).toContainText('local draft')
await codeEditor().press('Control+z')
await expect(codeEditor()).not.toContainText('local draft')
expect(await codeEditor().textContent()).toBe(initial)
})
const initialFormat = await formatToggle().textContent()
expect(initialFormat).toContain('OpenPencil')
test('keeps the last valid preview while showing invalid-code diagnostics', async () => {
await openCodePanel()
await codeEditor().fill('<Frame name="Last valid" />')
await waitForNodeNamed(editor.page, 'Last valid')
await codeEditor().fill('<Frame>')
const errorAlert = editor.page.getByTestId('code-panel-error')
await expect(errorAlert).toBeVisible()
await expect(errorAlert).toHaveCSS('color', 'rgb(248, 113, 113)')
await formatToggle().click()
await expect(formatToggle()).toContainText('Tailwind')
await editor.page.evaluate(async () => {
const themeModulePath = '/src/app/shell/theme.ts'
const themeModule = await import(themeModulePath)
themeModule.useAppTheme().setTheme('light')
})
await editor.page.waitForFunction(() => document.documentElement.dataset.theme === 'light')
await expect(errorAlert).toHaveCSS('color', 'rgb(185, 28, 28)')
expect(await hasNodeNamed(editor.page, 'Last valid')).toBe(true)
})
const code = await codePanel().textContent()
expect(code).toContain('div')
test('Reset restores generated source and original canvas state', async () => {
await editor.canvas.drawRect(100, 100, 200, 150)
const originalId = await getFirstSelectedNodeId(editor.page)
await openCodePanel()
await codeEditor().fill('<Frame name="Reset preview" />')
await waitForNodeNamed(editor.page, 'Reset preview')
await expect(editor.page.getByTestId('code-panel-reset').locator('svg')).toHaveCount(1)
await editor.page.getByTestId('code-panel-reset').click()
await waitForNode(editor.page, originalId)
expect(await hasNode(editor.page, originalId)).toBe(true)
await expect(codeEditor()).toContainText('Rectangle')
})
await formatToggle().click()
await expect(formatToggle()).toContainText('OpenPencil')
test('HTML/CSS live preview commits as one undoable session', async () => {
await editor.canvas.drawRect(80, 80, 120, 90)
const originalId = await getFirstSelectedNodeId(editor.page)
await openCodePanel()
await selectSource('HTML/CSS')
await codeEditor().fill('<div style="width: 180px; height: 90px">First</div>')
await expect.poll(() => hasNode(editor.page, originalId)).toBe(false)
await codeEditor().fill('<div style="width: 220px; height: 110px">Final</div>')
await expect(editor.page.getByTestId('code-panel-status')).toContainText('Updated live')
await expect(editor.page.getByTestId('code-panel-status')).toHaveAttribute('data-tone', 'success')
await designTab().click()
await expect.poll(() => getUndoLabel(editor.page)).toBe('Edit HTML/CSS')
await editor.page.keyboard.press('Meta+z')
await waitForNode(editor.page, originalId)
expect(await hasNode(editor.page, originalId)).toBe(true)
await editor.page.keyboard.press('Meta+Shift+z')
await expect.poll(() => hasNode(editor.page, originalId)).toBe(false)
})
test('switching source formats commits the current live session', async () => {
await editor.canvas.drawRect(90, 90, 140, 100)
const originalId = await getFirstSelectedNodeId(editor.page)
await openCodePanel()
await codeEditor().fill('<Frame name="Committed on switch" />')
await waitForNodeNamed(editor.page, 'Committed on switch')
await selectSource('HTML/CSS')
await expect(codeEditor()).toContainText('<style>')
await expect.poll(() => getUndoLabel(editor.page)).toBe('Edit JSX')
await editor.page.keyboard.press('Meta+z')
await waitForNode(editor.page, originalId)
})
test('Reset before debounce cancels the pending preview', async () => {
await editor.canvas.drawRect(100, 100, 200, 150)
const originalId = await getFirstSelectedNodeId(editor.page)
await openCodePanel()
await codeEditor().fill('<Frame name="Stale reset preview" />')
await editor.page.getByTestId('code-panel-reset').click()
await editor.page.waitForTimeout(450)
expect(await hasNode(editor.page, originalId)).toBe(true)
expect(await hasNodeNamed(editor.page, 'Stale reset preview')).toBe(false)
})
test('HTML/CSS uses the same editor and live reloads the canvas', async () => {
await openCodePanel()
await selectSource('HTML/CSS')
await expect(codePanel().locator('[data-slot="code-editor"]')).toHaveCount(1)
await expect(codeEditor()).toHaveAttribute('aria-label', 'HTML and CSS')
await codeEditor().fill(
'<style>.card { width: 240px; height: 120px; background: red; }</style><div class="card">Hello</div>'
)
await expect(editor.page.getByTestId('code-panel-status')).toContainText('Updated live')
const importedNodes = await getCodeNodeSummaries(editor.page)
expect(importedNodes.some((node) => node.type !== 'DOCUMENT' && node.type !== 'PAGE')).toBe(true)
})
test('Tailwind JSX is generated read-only in the same editor', async () => {
await editor.canvas.drawRect(100, 100, 200, 150)
await openCodePanel()
await selectSource('Tailwind JSX')
await expect(editor.page.getByTestId('code-panel-status')).toContainText('Generated, read only')
await expect(codeEditor()).toHaveAttribute('contenteditable', 'false')
})
test('copy button works and shows confirmation', async () => {
await copyButton().click()
await expect(copyButton()).toContainText('Copied')
await editor.page.waitForTimeout(2500)
await expect(copyButton()).toContainText('Copy')
})
test('deselecting shows empty state again', async () => {
await editor.page.keyboard.press('Escape')
await editor.canvas.waitForRender()
await expect(codePanelEmpty()).toBeVisible()
})
test('selecting a frame shows Frame in JSX', async () => {
// Create a frame via store to avoid click-targeting issues
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const id = store.createShape('FRAME', 300, 100, 200, 200)
store.select([id])
})
await editor.canvas.waitForRender()
const code = await codePanel().textContent()
expect(code).toContain('Frame')
})
test('switching back to Design tab works', async () => {
await designTab().click()
await expect(
editor.page
.getByTestId('design-panel-single')
.or(editor.page.getByTestId('design-panel-empty'))
.first()
).toBeVisible()
})
test('shows import errors in the Code panel', async () => {
await codeTab().click()
await editor.page.getByTestId('code-panel-import-toggle').click()
await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
const importDOMText = store.importDOMText
store.importDOMText = async () => {
store.importDOMText = importDOMText
throw new Error('CSS import failed')
}
})
await editor.page.getByTestId('code-panel-import-html').fill('<div class="card">Broken DOM</div>')
await editor.page.getByTestId('code-panel-import').click()
await expect(editor.page.getByTestId('code-panel-import-error')).toBeVisible()
await expect(editor.page.getByTestId('code-panel-import-error')).toContainText(
'CSS import failed'
)
await editor.page.getByTestId('code-panel-import-html').fill('<div class="card">Recovered</div>')
await expect(editor.page.getByTestId('code-panel-import-error')).toBeHidden()
await editor.page.getByTestId('code-panel-import-toggle').click()
})
test('imports HTML and CSS into the canvas', async () => {
await codeTab().click()
await editor.page.getByTestId('code-panel-import-toggle').click()
await editor.page.getByTestId('code-panel-import-html').fill('<div class="card">Hello DOM</div>')
await editor.page
.getByTestId('code-panel-import-css')
.fill('.card { width: 240px; height: 120px; padding: 16px; background: #ffffff; }')
await editor.page.getByTestId('code-panel-import').click()
await editor.page.waitForFunction(() => {
const store = window.openPencil?.getStore?.()
return store?.graph.getAllNodes().some((node) => node.name.includes('Hello DOM'))
})
await editor.canvas.waitForRender()
const imported = await editor.page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.graph.getAllNodes().some((node) => node.name.includes('Hello DOM'))
})
expect(imported).toBe(true)
await openCodePanel()
await editor.page.getByTestId('code-panel-copy').click()
await expect(editor.page.getByTestId('code-panel-copy')).toContainText('Copied')
})

View file

@ -0,0 +1,167 @@
import { expect, test, type Page } from '@playwright/test'
import type { evaluateDesignJSX as EvaluateDesignJSX } from '@/app/code/sandbox/evaluate'
import { CanvasHelper } from '#tests/helpers/canvas'
test.beforeEach(async ({ page }) => {
await page.goto('/')
const canvas = new CanvasHelper(page)
await canvas.waitForInit()
})
async function evaluate(page: Page, source: string, timeoutMs = 1_000) {
return page.evaluate(
async ({ source, timeoutMs }) => {
const modulePath = '/src/app/code/sandbox/evaluate.ts'
const { evaluateDesignJSX } = (await import(/* @vite-ignore */ modulePath)) as {
evaluateDesignJSX: typeof EvaluateDesignJSX
}
return evaluateDesignJSX(source, { timeoutMs })
},
{ source, timeoutMs }
)
}
test('evaluates JSX into plain inert data', async ({ page }) => {
const result = await evaluate(
page,
'<Frame w={320} fill={solid("#fff")}><Text>Hello</Text></Frame>'
)
expect(result).toEqual({
ok: true,
roots: [
{
type: 'frame',
props: {
w: 320,
fill: { __openPencilHelper: 'solid', args: ['#fff'] }
},
children: [{ type: 'text', props: {}, children: ['Hello'] }]
}
]
})
})
test('supports local constants, function components, arrays, and conditionals', async ({
page
}) => {
const result = await evaluate(
page,
`const colors = { surface: '#fff' }
const Card = ({ title, visible }) => (
<Frame fill={colors.surface}>
{visible ? <Text>{title}</Text> : null}
{[1, 2].map((item) => <Text>{item}</Text>)}
</Frame>
)
<Card title="Hello" visible />`
)
expect(result).toEqual({
ok: true,
roots: [
{
type: 'frame',
props: { fill: '#fff' },
children: [
{ type: 'text', props: {}, children: ['Hello'] },
{ type: 'text', props: {}, children: [1] },
{ type: 'text', props: {}, children: [2] }
]
}
]
})
})
test('converts variable helpers without executing host capabilities', async ({ page }) => {
const result = await evaluate(
page,
`const vars = defineVars({ primary: { name: 'Primary', value: '#ff0000' } })
<Frame fill={vars.primary}><Text color={designVar('text', '#000')}>Hello</Text></Frame>`
)
expect(result.ok).toBe(true)
if (!result.ok) return
expect(result.roots[0]).toMatchObject({
type: 'frame',
props: {
fill: { __openPencilHelper: 'designVar' }
}
})
})
test('supports multiple roots through fragments', async ({ page }) => {
const result = await evaluate(page, '<><Rectangle name="One" /><Ellipse name="Two" /></>')
expect(result).toEqual({
ok: true,
roots: [
{ type: 'rectangle', props: { name: 'One' }, children: [] },
{ type: 'ellipse', props: { name: 'Two' }, children: [] }
]
})
})
test('cannot access the parent or application origin', async ({ page }) => {
const result = await evaluate(page, '<Frame>{String(window.parent.document)}</Frame>')
expect(result.ok).toBe(false)
if (!result.ok) expect(result.error).toMatch(/window is not defined|Can't find variable: window/)
})
test('blocks network requests from evaluated code', async ({ page }) => {
const requests: string[] = []
page.on('request', (request) => {
if (request.url().includes('sandbox-probe.invalid')) requests.push(request.url())
})
const result = await evaluate(
page,
'<Frame>{fetch("https://sandbox-probe.invalid/leak")}</Frame>'
)
expect(result.ok).toBe(false)
expect(requests).toEqual([])
})
test('terminates accidental infinite loops without freezing the host', async ({ page }) => {
const started = Date.now()
const result = await evaluate(page, '(() => { while (true) {} })()', 100)
expect(result).toEqual({ ok: false, error: 'Design JSX execution timed out.' })
expect(Date.now() - started).toBeLessThan(2_000)
await expect(page.getByTestId('editor-root')).toBeVisible()
})
test('rejects oversized source and output', async ({ page }) => {
const sourceResult = await page.evaluate(async () => {
const modulePath = '/src/app/code/sandbox/evaluate.ts'
const { evaluateDesignJSX } = (await import(/* @vite-ignore */ modulePath)) as {
evaluateDesignJSX: typeof EvaluateDesignJSX
}
return evaluateDesignJSX('<Text>too large</Text>', { sourceBytes: 4 })
})
expect(sourceResult).toEqual({ ok: false, error: 'Design JSX source is too large.' })
const outputResult = await page.evaluate(async () => {
const modulePath = '/src/app/code/sandbox/evaluate.ts'
const { evaluateDesignJSX } = (await import(/* @vite-ignore */ modulePath)) as {
evaluateDesignJSX: typeof EvaluateDesignJSX
}
return evaluateDesignJSX('<Text>{"x".repeat(100)}</Text>', { outputBytes: 20 })
})
expect(outputResult).toEqual({ ok: false, error: 'Design JSX output is too large.' })
const arrayResult = await page.evaluate(async () => {
const modulePath = '/src/app/code/sandbox/evaluate.ts'
const { evaluateDesignJSX } = (await import(/* @vite-ignore */ modulePath)) as {
evaluateDesignJSX: typeof EvaluateDesignJSX
}
return evaluateDesignJSX(
'<Frame>{Array.from({ length: 10 }, (_, i) => <Text>{i}</Text>)}</Frame>',
{
arrayLength: 5
}
)
})
expect(arrayResult).toEqual({
ok: false,
error: 'Design JSX output contains an array that is too long.'
})
})

View file

@ -1,15 +0,0 @@
import { describe, expect, test } from 'bun:test'
import { highlightJSX } from '@/app/code/highlight'
describe('code highlighting', () => {
test('highlights JavaScript and JSX syntax without a Prism global', () => {
const existingPrism = Reflect.get(globalThis, 'Prism')
const html = highlightJSX('const view = <Button disabled>{label}</Button>')
expect(html).toContain('<span class="token keyword">const</span>')
expect(html).toContain('<span class="token class-name">Button</span>')
expect(html).toContain('<span class="token attr-name">disabled</span>')
expect(Reflect.get(globalThis, 'Prism')).toBe(existingPrism)
})
})

View file

@ -0,0 +1,106 @@
import { describe, expect, test } from 'bun:test'
import { convertDesignJSXRoots } from '@/app/code/sandbox/convert'
import {
DESIGN_JSX_MAX_ARRAY_LENGTH,
DESIGN_JSX_MAX_DEPTH,
DESIGN_JSX_MAX_ELEMENTS,
DESIGN_JSX_MAX_OBJECT_KEYS,
DESIGN_JSX_MAX_OUTPUT_BYTES,
DESIGN_JSX_MAX_STRING_LENGTH,
resolveDesignJSXValidationLimits
} from '@/app/code/sandbox/types'
import { validateDesignJSXOutput } from '@/app/code/sandbox/validate'
const limits = resolveDesignJSXValidationLimits({})
function frame(props: Record<string, unknown> = {}, children: unknown[] = []) {
return { type: 'frame', props, children }
}
describe('Design JSX sandbox output', () => {
test('resolves one complete validation contract', () => {
expect(limits).toEqual({
outputBytes: DESIGN_JSX_MAX_OUTPUT_BYTES,
elements: DESIGN_JSX_MAX_ELEMENTS,
depth: DESIGN_JSX_MAX_DEPTH,
arrayLength: DESIGN_JSX_MAX_ARRAY_LENGTH,
objectKeys: DESIGN_JSX_MAX_OBJECT_KEYS,
stringLength: DESIGN_JSX_MAX_STRING_LENGTH
})
})
test('validates and converts the same serialized element contract', () => {
const roots = validateDesignJSXOutput(
frame({ fill: { __openPencilHelper: 'solid', args: ['#ff0000'] } }, [
{ type: 'text', props: {}, children: ['Hello ', 2] }
]),
limits
)
expect(convertDesignJSXRoots(roots)).toMatchObject([
{
type: 'frame',
props: { fill: { type: 'SOLID' } },
children: [{ type: 'text', children: ['Hello ', '2'] }]
}
])
})
test('rejects empty output and malformed elements before rendering', () => {
expect(() => validateDesignJSXOutput(undefined, limits)).toThrow(
'must return an OpenPencil element'
)
expect(() => validateDesignJSXOutput([], limits)).toThrow('must return an OpenPencil element')
expect(() => convertDesignJSXRoots([{ type: 'frame', props: {}, children: null }])).toThrow(
'must return an OpenPencil element'
)
})
test('rejects blocked keys and non-plain prototypes', () => {
const blocked = Object.fromEntries([['safe', true]])
Object.defineProperty(blocked, 'constructor', { value: 'blocked', enumerable: true })
expect(() => validateDesignJSXOutput(frame(blocked), limits)).toThrow('blocked key')
expect(() => validateDesignJSXOutput(frame({ value: new Date() }), limits)).toThrow(
'plain structured data only'
)
})
test('enforces array, object, string, and depth limits on the host', () => {
expect(() =>
validateDesignJSXOutput(
frame({}, ['a', 'b']),
resolveDesignJSXValidationLimits({ arrayLength: 1 })
)
).toThrow('array')
expect(() =>
validateDesignJSXOutput(
frame({ a: 1, b: 2 }),
resolveDesignJSXValidationLimits({ objectKeys: 1 })
)
).toThrow('object properties')
expect(() =>
validateDesignJSXOutput('abc', resolveDesignJSXValidationLimits({ stringLength: 2 }))
).toThrow('string')
expect(() =>
validateDesignJSXOutput([[['x']]], resolveDesignJSXValidationLimits({ depth: 2 }))
).toThrow('deeply nested')
})
test('counts container and primitive data toward the output limit', () => {
const tiny = resolveDesignJSXValidationLimits({ outputBytes: 1 })
expect(() => validateDesignJSXOutput([], tiny)).toThrow('must return an OpenPencil element')
expect(() => validateDesignJSXOutput(true, tiny)).toThrow('too large')
expect(() => validateDesignJSXOutput(1, tiny)).toThrow('too large')
expect(() => validateDesignJSXOutput(frame(), tiny)).toThrow('too large')
})
test('rejects unknown helper descriptors during trusted conversion', () => {
for (const helperName of ['unknown', 'toString', 'constructor', 'valueOf']) {
const roots = validateDesignJSXOutput(
frame({ fill: { __openPencilHelper: helperName, args: [] } }),
limits
)
expect(() => convertDesignJSXRoots(roots)).toThrow('Unknown Design JSX helper')
}
})
})

View file

@ -0,0 +1,42 @@
import { describe, expect, test } from 'bun:test'
import {
DESIGN_JSX_SUPPORTED_PROPERTIES,
transformDesignJSXExpression
} from '@open-pencil/core/design-jsx'
describe('Design JSX schema', () => {
test('includes renderer compatibility properties', () => {
for (const property of [
'name',
'columnGap',
'backgroundColor',
'cornerSmoothing',
'textDecoration',
'componentId'
]) {
expect(DESIGN_JSX_SUPPORTED_PROPERTIES.has(property)).toBe(true)
}
})
})
describe('Design JSX transform', () => {
test('transforms a JSX expression', () => {
const code = transformDesignJSXExpression('<Frame><Text>Hello</Text></Frame>')
expect(code).toContain('__h(Frame')
expect(code).toContain('__h(Text')
})
test('transforms declarations followed by a final expression', () => {
const code = transformDesignJSXExpression(
`const Card = ({ title }) => <Text>{title}</Text>
<Card title="Hello" />`
)
expect(code).toContain('const Card')
expect(code).toContain('return (__h(Card')
})
test('rejects declarations without a final expression', () => {
expect(() => transformDesignJSXExpression('const value = 1')).toThrow()
})
})

View file

@ -0,0 +1,73 @@
import type { Page } from '@playwright/test'
export type CodeNodeSummary = {
type: string
name: string
text: string
}
export function getFirstSelectedNodeId(page: Page): Promise<string | null> {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.state.selectedIds][0] ?? null
})
}
export function hasNode(page: Page, id: string | null): Promise<boolean> {
return page.evaluate((nodeId) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return nodeId !== null && store.graph.getNode(nodeId) !== undefined
}, id)
}
export function hasNodeNamed(page: Page, name: string): Promise<boolean> {
return page.evaluate((nodeName) => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.graph.getAllNodes()].some((node) => node.name === nodeName)
}, name)
}
export async function waitForNode(page: Page, id: string | null): Promise<void> {
await page.waitForFunction((nodeId) => {
const store = window.openPencil?.getStore?.()
return nodeId !== null && store?.graph.getNode(nodeId) !== undefined
}, id)
}
export async function waitForNodeNamed(page: Page, name: string): Promise<void> {
await page.waitForFunction((nodeName) => {
const store = window.openPencil?.getStore?.()
return [...(store?.graph.getAllNodes() ?? [])].some((node) => node.name === nodeName)
}, name)
}
export function getCodeNodeSummaries(page: Page): Promise<CodeNodeSummary[]> {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return [...store.graph.getAllNodes()].map((node) => ({
type: node.type,
name: node.name,
text: node.text
}))
})
}
export function getUndoLabel(page: Page): Promise<string | null> {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.undo.undoLabel
})
}
export function getMobileDrawerSnap(page: Page): Promise<string> {
return page.evaluate(() => {
const store = window.openPencil?.getStore?.()
if (!store) throw new Error('OpenPencil store not initialized')
return store.state.mobileDrawerSnap
})
}