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:
parent
d8c968c04a
commit
6193b6da7e
|
|
@ -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
234
bun.lock
|
|
@ -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=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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, '')
|
||||
|
|
|
|||
165
packages/core/src/design-jsx/schema.ts
Normal file
165
packages/core/src/design-jsx/schema.ts
Normal 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.` }))
|
||||
44
packages/core/src/design-jsx/transform.ts
Normal file
44
packages/core/src/design-jsx/transform.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 d’IA 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 l’interface ({shortcut})",
|
||||
|
|
@ -276,5 +276,16 @@
|
|||
"dismiss": "Ignorer",
|
||||
"toolRunning": "Exécution…",
|
||||
"toolError": "Erreur",
|
||||
"permissionRequestTitle": "Demande d’autorisation"
|
||||
"permissionRequestTitle": "Demande d’autorisation",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
30
src/app.css
30
src/app.css
|
|
@ -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;
|
||||
|
|
|
|||
61
src/app/code/dom-preview.ts
Normal file
61
src/app/code/dom-preview.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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'))
|
||||
}
|
||||
147
src/app/code/live-preview.ts
Normal file
147
src/app/code/live-preview.ts
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
97
src/app/code/sandbox/convert.ts
Normal file
97
src/app/code/sandbox/convert.ts
Normal 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)
|
||||
}
|
||||
121
src/app/code/sandbox/document.ts
Normal file
121
src/app/code/sandbox/document.ts
Normal 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>`
|
||||
}
|
||||
108
src/app/code/sandbox/evaluate.ts
Normal file
108
src/app/code/sandbox/evaluate.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
50
src/app/code/sandbox/types.ts
Normal file
50
src/app/code/sandbox/types.ts
Normal 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 }
|
||||
108
src/app/code/sandbox/validate.ts
Normal file
108
src/app/code/sandbox/validate.ts
Normal 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
17
src/app/code/templates.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
163
src/components/code-editor/CodeEditor.vue
Normal file
163
src/components/code-editor/CodeEditor.vue
Normal 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>
|
||||
114
src/components/code-editor/extensions.ts
Normal file
114
src/components/code-editor/extensions.ts
Normal 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)])
|
||||
]
|
||||
}
|
||||
3
src/components/code-editor/types.ts
Normal file
3
src/components/code-editor/types.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import type { CodeSource } from '@/app/code/templates'
|
||||
|
||||
export type CodeEditorLanguage = CodeSource
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
167
tests/e2e/code/sandbox.spec.ts
Normal file
167
tests/e2e/code/sandbox.spec.ts
Normal 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.'
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
106
tests/engine/app/code/sandbox.test.ts
Normal file
106
tests/engine/app/code/sandbox.test.ts
Normal 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')
|
||||
}
|
||||
})
|
||||
})
|
||||
42
tests/engine/render/jsx/schema.test.ts
Normal file
42
tests/engine/render/jsx/schema.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
73
tests/helpers/code-panel.ts
Normal file
73
tests/helpers/code-panel.ts
Normal 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
|
||||
})
|
||||
}
|
||||
Loading…
Reference in a new issue