Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c88ac7972 | |||
| 0f984d4dea | |||
| 0a1d70b605 | |||
| 40bf063490 | |||
| 01300c7061 | |||
| 7d36b6c9ea | |||
| e5eaa4f8d7 | |||
| d8d917d315 | |||
| 5887baf324 | |||
| 8db56d9ab7 | |||
| 6cc028305a | |||
| f64d5a73d2 | |||
| 22f13820ab | |||
| 459993a78d |
@@ -6,12 +6,6 @@
|
||||
:80 {
|
||||
encode zstd gzip
|
||||
|
||||
header {
|
||||
X-Frame-Options "DENY"
|
||||
X-Content-Type-Options "nosniff"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
}
|
||||
|
||||
handle /mcp {
|
||||
reverse_proxy 127.0.0.1:3001
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"mcp-handler": "^1.1.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"shiki": "^4.1.0",
|
||||
"zod": "^4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -136,6 +137,22 @@
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@1.1.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="],
|
||||
|
||||
"@shikijs/core": ["@shikijs/core@4.1.0", "", { "dependencies": { "@shikijs/primitive": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-jLJtSJeuFffqX6/inRE1zqU5aFv2hrszvYgq3OjbAgFRZiWv7abKMDdQzYxuSDfmUPQozZvI/kuy6VMTvnvqTQ=="],
|
||||
|
||||
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.6" } }, "sha512-YquhawCUgaBfhsS72e2Y/dI59gCBNPHu3fEO/tvLaXrTssxZrY5ddjtNLTwndrMgPo8b3IscE+xoICDzpTmlFQ=="],
|
||||
|
||||
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-axLpjVs45YBvvINa+dJF+NPW+KtFkNXsFr4SDw2BMj9GdeMnGxVB9PQb2xXlJYovslt/nz6giedAyOANkfc7hg=="],
|
||||
|
||||
"@shikijs/langs": ["@shikijs/langs@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-nwOMruEkbgdZfQ/b8CgpNBVOpvG1k0N5tbmgiFeqsan401+x3ILqlzZJowSla4Agmq4hG2Uf2wh5jLTEhR8VSg=="],
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-zx2/2Uwj2q9X3KSyYREEhXO23xBw5WUhP4orK2lE4r+t9JGITmEe0JH+wPmJhqHpOT2bRRs6lAL945+LDvOAGw=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@4.1.0", "", { "dependencies": { "@shikijs/types": "4.1.0" } }, "sha512-emCcTnUM7yO2wltYbaxm+yLvcCI4+h8XBKc4KmJ7EZUXoSGjcCHifkI//R4OFit9ewpg7H2/9tjOuXrT2v/Knw=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.1.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3EQWX54fMpniOrDblzAhiwiJwpiTMW6+B9DWyUd9ska483tbayFYuw47UxwuPknI31bKnySfVQ/QW+jFL4rFdA=="],
|
||||
|
||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/d3": ["@types/d3@7.4.3", "", { "dependencies": { "@types/d3-array": "*", "@types/d3-axis": "*", "@types/d3-brush": "*", "@types/d3-chord": "*", "@types/d3-color": "*", "@types/d3-contour": "*", "@types/d3-delaunay": "*", "@types/d3-dispatch": "*", "@types/d3-drag": "*", "@types/d3-dsv": "*", "@types/d3-ease": "*", "@types/d3-fetch": "*", "@types/d3-force": "*", "@types/d3-format": "*", "@types/d3-geo": "*", "@types/d3-hierarchy": "*", "@types/d3-interpolate": "*", "@types/d3-path": "*", "@types/d3-polygon": "*", "@types/d3-quadtree": "*", "@types/d3-random": "*", "@types/d3-scale": "*", "@types/d3-scale-chromatic": "*", "@types/d3-selection": "*", "@types/d3-shape": "*", "@types/d3-time": "*", "@types/d3-time-format": "*", "@types/d3-timer": "*", "@types/d3-transition": "*", "@types/d3-zoom": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="],
|
||||
@@ -202,6 +219,10 @@
|
||||
|
||||
"@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
|
||||
@@ -210,6 +231,10 @@
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.1", "", {}, "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ=="],
|
||||
|
||||
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
@@ -240,8 +265,14 @@
|
||||
|
||||
"canvas-roundrect-polyfill": ["canvas-roundrect-polyfill@0.0.1", "", {}, "sha512-yWq+R3U3jE+coOeEb3a3GgE2j/0MMiDKM/QpLb6h9ihf5fGY9UXtvK9o4vNqjWXoZz7/3EaSVU3IX53TvFFUOw=="],
|
||||
|
||||
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
|
||||
|
||||
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||
|
||||
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
|
||||
|
||||
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
|
||||
|
||||
"chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="],
|
||||
|
||||
"chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="],
|
||||
@@ -252,6 +283,8 @@
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="],
|
||||
@@ -354,8 +387,12 @@
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||
|
||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||
|
||||
"dompurify": ["dompurify@3.4.5", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
@@ -428,8 +465,14 @@
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hono": ["hono@4.12.23", "", {}, "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA=="],
|
||||
|
||||
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
@@ -492,12 +535,24 @@
|
||||
|
||||
"mcp-handler": ["mcp-handler@1.1.0", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^11.1.0", "redis": "^4.6.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "1.26.0", "next": ">=13.0.0" }, "optionalPeers": ["next"], "bin": { "mcp-adapter": "dist/cli/index.js", "mcp-handler": "dist/cli/index.js", "create-mcp-route": "dist/cli/index.js" } }, "sha512-MVCES7g18gcoZy+R/3v5nadkUMzMAWdos8jRl6DyljOKvd2/ZKDmwlCjL6zp4vo+7FeCXOYL1uWinHWlkKAAUg=="],
|
||||
|
||||
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mermaid": ["mermaid@11.15.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.1.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.1", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.19", "dompurify": "^3.3.1", "es-toolkit": "^1.45.1", "katex": "^0.16.25", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw=="],
|
||||
|
||||
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
|
||||
|
||||
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
|
||||
|
||||
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
|
||||
|
||||
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
|
||||
|
||||
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
@@ -522,6 +577,10 @@
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"open-color": ["open-color@1.9.1", "", {}, "sha512-vCseG/EQ6/RcvxhUcGJiHViOgrtz4x0XbZepXvKik66TMGkvbmjeJrKFyBEx6daG5rNyyd14zYXhz0hZVwQFOw=="],
|
||||
|
||||
"package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="],
|
||||
@@ -560,6 +619,8 @@
|
||||
|
||||
"pretty-quick": ["pretty-quick@4.2.2", "", { "dependencies": { "@pkgr/core": "^0.2.7", "ignore": "^7.0.5", "mri": "^1.2.0", "picocolors": "^1.1.1", "picomatch": "^4.0.2", "tinyexec": "^0.3.2", "tslib": "^2.8.1" }, "peerDependencies": { "prettier": "^3.0.0" }, "bin": { "pretty-quick": "lib/cli.mjs" } }, "sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w=="],
|
||||
|
||||
"property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"pwacompat": ["pwacompat@2.0.17", "", {}, "sha512-6Du7IZdIy7cHiv7AhtDy4X2QRM8IAD5DII69mt5qWibC2d15ZU8DmBG1WdZKekG11cChSu4zkSUGPF9sweOl6w=="],
|
||||
@@ -584,6 +645,12 @@
|
||||
|
||||
"redis": ["redis@4.7.1", "", { "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", "@redis/graph": "1.1.1", "@redis/json": "1.0.7", "@redis/search": "1.2.0", "@redis/time-series": "1.1.0" } }, "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ=="],
|
||||
|
||||
"regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="],
|
||||
|
||||
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="],
|
||||
@@ -610,6 +677,8 @@
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shiki": ["shiki@4.1.0", "", { "dependencies": { "@shikijs/core": "4.1.0", "@shikijs/engine-javascript": "4.1.0", "@shikijs/engine-oniguruma": "4.1.0", "@shikijs/langs": "4.1.0", "@shikijs/themes": "4.1.0", "@shikijs/types": "4.1.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-l/ABZPUR5v70jI10EzqfMS/I96vjSGv2y0ihUV+WYFzv0EfvW4s54m0Lg8wCrrL+2IkwBzFTuxkZjPf8b2NX9Q=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="],
|
||||
@@ -624,8 +693,12 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
|
||||
|
||||
"stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
@@ -634,6 +707,8 @@
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
@@ -646,6 +721,16 @@
|
||||
|
||||
"undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
|
||||
|
||||
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
|
||||
|
||||
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
|
||||
|
||||
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
|
||||
|
||||
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
|
||||
|
||||
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
@@ -658,6 +743,10 @@
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||
|
||||
"vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="],
|
||||
|
||||
"vscode-languageserver": ["vscode-languageserver@9.0.1", "", { "dependencies": { "vscode-languageserver-protocol": "3.17.5" }, "bin": { "installServerIntoExtension": "bin/installServerIntoExtension" } }, "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g=="],
|
||||
@@ -684,6 +773,8 @@
|
||||
|
||||
"zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
|
||||
"@antfu/install-pkg/tinyexec": ["tinyexec@1.2.2", "", {}, "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g=="],
|
||||
|
||||
"@chevrotain/cst-dts-gen/@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="],
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"mcp-handler": "^1.1.0",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6",
|
||||
"shiki": "^4.1.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
File diff suppressed because one or more lines are too long
+239
-2
@@ -1,7 +1,30 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useEffectEvent,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { CaptureUpdateAction } from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||
import { DrawingSidebar, DrawingsToggle } from "./components/DrawingSidebar";
|
||||
import {
|
||||
CodeBlockSidebar,
|
||||
InsertCodeBlockButton,
|
||||
} from "./components/CodeBlockSidebar";
|
||||
import { renderCodeBlockEmbeddable } from "./components/CodeBlockEmbeddable";
|
||||
import { EditorCanvas } from "./components/EditorCanvas";
|
||||
import { PublicViewer } from "./components/PublicViewer";
|
||||
import {
|
||||
createCodeBlockElement,
|
||||
DEFAULT_CODE_BLOCK_HEIGHT,
|
||||
DEFAULT_CODE_BLOCK_WIDTH,
|
||||
EMPTY_CODE_BLOCK_SELECTION_STATE,
|
||||
updateCodeBlockElements,
|
||||
type CodeBlockDraftState,
|
||||
type CodeBlockLanguage,
|
||||
type CodeBlockSelectionState,
|
||||
} from "./codeblock";
|
||||
import { usePublicDrawing } from "./hooks/usePublicDrawing";
|
||||
import { useDrawingSession } from "./hooks/useDrawingSession";
|
||||
import { useThemeTokenSync } from "./hooks/useThemeTokenSync";
|
||||
@@ -23,7 +46,13 @@ function routeFromPath(pathname = window.location.pathname): AppRoute {
|
||||
|
||||
function PrivateApp() {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
const [codeBlockSelection, setCodeBlockSelection] =
|
||||
useState<CodeBlockSelectionState>(EMPTY_CODE_BLOCK_SELECTION_STATE);
|
||||
const [codeBlockDraft, setCodeBlockDraft] =
|
||||
useState<CodeBlockDraftState | null>(null);
|
||||
const appShellRef = useRef<HTMLDivElement | null>(null);
|
||||
const excalidrawApiRef = useRef<ExcalidrawImperativeAPI | null>(null);
|
||||
const codeBlockDraftRef = useRef<CodeBlockDraftState | null>(null);
|
||||
const scheduleThemeTokenSync = useThemeTokenSync(appShellRef);
|
||||
const {
|
||||
drawings,
|
||||
@@ -48,6 +77,38 @@ function PrivateApp() {
|
||||
disablePublication,
|
||||
} = useDrawingSession();
|
||||
|
||||
const flushCodeBlockDraft = useEffectEvent(() => {
|
||||
const api = excalidrawApiRef.current;
|
||||
const draft = codeBlockDraftRef.current;
|
||||
if (!api || !draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentElements = api.getSceneElementsIncludingDeleted();
|
||||
const nextElements = updateCodeBlockElements(
|
||||
currentElements,
|
||||
draft.elementId,
|
||||
{
|
||||
code: draft.code,
|
||||
language: draft.language,
|
||||
showBackground: draft.showBackground,
|
||||
},
|
||||
);
|
||||
|
||||
if (nextElements === currentElements) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.updateScene({
|
||||
elements: nextElements,
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
codeBlockDraftRef.current = codeBlockDraft;
|
||||
}, [codeBlockDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!scene || loading) {
|
||||
return;
|
||||
@@ -73,6 +134,62 @@ function PrivateApp() {
|
||||
};
|
||||
}, [isSidebarOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
flushCodeBlockDraft();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedCodeBlock = codeBlockSelection.selectedCodeBlock;
|
||||
setCodeBlockDraft((current) => {
|
||||
const nextDraft = selectedCodeBlock
|
||||
? {
|
||||
elementId: selectedCodeBlock.id,
|
||||
code: selectedCodeBlock.customData.code,
|
||||
language: selectedCodeBlock.customData.language,
|
||||
showBackground: selectedCodeBlock.customData.showBackground,
|
||||
}
|
||||
: null;
|
||||
|
||||
if (
|
||||
current?.elementId === nextDraft?.elementId &&
|
||||
current?.code === nextDraft?.code &&
|
||||
current?.language === nextDraft?.language &&
|
||||
current?.showBackground === nextDraft?.showBackground
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return nextDraft;
|
||||
});
|
||||
|
||||
return () => {
|
||||
flushCodeBlockDraft();
|
||||
};
|
||||
}, [codeBlockSelection.selectedCodeBlockId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!codeBlockDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
flushCodeBlockDraft();
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [codeBlockDraft]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
setCodeBlockSelection(EMPTY_CODE_BLOCK_SELECTION_STATE);
|
||||
setCodeBlockDraft(null);
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
const openSidebar = useCallback(() => {
|
||||
setIsSidebarOpen(true);
|
||||
}, []);
|
||||
@@ -94,6 +211,102 @@ function PrivateApp() {
|
||||
void createDrawing();
|
||||
}, [createDrawing]);
|
||||
|
||||
const handleInsertCodeBlock = useCallback(() => {
|
||||
const api = excalidrawApiRef.current;
|
||||
if (!api) {
|
||||
return;
|
||||
}
|
||||
|
||||
const appState = api.getAppState();
|
||||
const viewportWidth =
|
||||
typeof appState.width === "number" ? appState.width : window.innerWidth;
|
||||
const viewportHeight =
|
||||
typeof appState.height === "number"
|
||||
? appState.height
|
||||
: window.innerHeight;
|
||||
const zoom = appState.zoom.value;
|
||||
const x =
|
||||
-appState.scrollX +
|
||||
viewportWidth / (2 * zoom) -
|
||||
DEFAULT_CODE_BLOCK_WIDTH / 2;
|
||||
const y =
|
||||
-appState.scrollY +
|
||||
viewportHeight / (2 * zoom) -
|
||||
DEFAULT_CODE_BLOCK_HEIGHT / 2;
|
||||
const codeBlock = createCodeBlockElement({ x, y });
|
||||
|
||||
api.updateScene({
|
||||
elements: [...api.getSceneElementsIncludingDeleted(), codeBlock],
|
||||
appState: {
|
||||
selectedElementIds: {
|
||||
[codeBlock.id]: true,
|
||||
},
|
||||
},
|
||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCodeBlockSelectionChange = useCallback(
|
||||
(selection: CodeBlockSelectionState) => {
|
||||
setCodeBlockSelection((current) => {
|
||||
if (
|
||||
current.isPanelOpen === selection.isPanelOpen &&
|
||||
current.selectedCodeBlockId === selection.selectedCodeBlockId &&
|
||||
current.selectedCodeBlock?.customData.code ===
|
||||
selection.selectedCodeBlock?.customData.code &&
|
||||
current.selectedCodeBlock?.customData.language ===
|
||||
selection.selectedCodeBlock?.customData.language &&
|
||||
current.selectedCodeBlock?.customData.showBackground ===
|
||||
selection.selectedCodeBlock?.customData.showBackground
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return selection;
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCodeBlockCodeChange = useCallback((code: string) => {
|
||||
setCodeBlockDraft((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
code,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleCodeBlockLanguageChange = useCallback(
|
||||
(language: CodeBlockLanguage) => {
|
||||
setCodeBlockDraft((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
language,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCodeBlockShowBackgroundChange = useCallback(
|
||||
(showBackground: boolean) => {
|
||||
setCodeBlockDraft((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
showBackground,
|
||||
}
|
||||
: current,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="app-shell" ref={appShellRef}>
|
||||
{toastMessage && (
|
||||
@@ -101,6 +314,10 @@ function PrivateApp() {
|
||||
{toastMessage}
|
||||
</div>
|
||||
)}
|
||||
<InsertCodeBlockButton
|
||||
onClick={handleInsertCodeBlock}
|
||||
disabled={loading || scene === null}
|
||||
/>
|
||||
<main className="editor-shell">
|
||||
<div className="app-actions">
|
||||
<DrawingsToggle onClick={openSidebar} />
|
||||
@@ -113,7 +330,20 @@ function PrivateApp() {
|
||||
error={error}
|
||||
editorReloadNonce={editorReloadNonce}
|
||||
onSceneChange={handleSceneChange}
|
||||
onSelectionStateChange={handleCodeBlockSelectionChange}
|
||||
onEditorActivity={scheduleThemeTokenSync}
|
||||
onExcalidrawAPI={(api) => {
|
||||
excalidrawApiRef.current = api;
|
||||
}}
|
||||
renderEmbeddable={renderCodeBlockEmbeddable}
|
||||
/>
|
||||
<CodeBlockSidebar
|
||||
open={codeBlockSelection.isPanelOpen}
|
||||
draft={codeBlockDraft}
|
||||
onCodeChange={handleCodeBlockCodeChange}
|
||||
onLanguageChange={handleCodeBlockLanguageChange}
|
||||
onShowBackgroundChange={handleCodeBlockShowBackgroundChange}
|
||||
onCommit={flushCodeBlockDraft}
|
||||
/>
|
||||
</main>
|
||||
<DrawingSidebar
|
||||
@@ -141,7 +371,14 @@ function PrivateApp() {
|
||||
function PublicApp({ slug }: { slug: string }) {
|
||||
const { drawing, loading, error } = usePublicDrawing(slug);
|
||||
|
||||
return <PublicViewer drawing={drawing} loading={loading} error={error} />;
|
||||
return (
|
||||
<PublicViewer
|
||||
drawing={drawing}
|
||||
loading={loading}
|
||||
error={error}
|
||||
renderEmbeddable={renderCodeBlockEmbeddable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
DEFAULT_CODE_BLOCK_SHOW_BACKGROUND,
|
||||
EMPTY_CODE_BLOCK_SELECTION_STATE,
|
||||
createCodeBlockCustomData,
|
||||
getCodeBlockSelectionState,
|
||||
getCodeBlockCustomData,
|
||||
isCodeBlockEmbeddable,
|
||||
sanitizeCodeBlockLanguage,
|
||||
updateCodeBlockElements,
|
||||
} from "./codeblock";
|
||||
|
||||
const baseEmbeddable = {
|
||||
id: "codeblock-1",
|
||||
type: "embeddable" as const,
|
||||
x: 10,
|
||||
y: 20,
|
||||
strokeColor: "transparent",
|
||||
backgroundColor: "transparent",
|
||||
fillStyle: "solid" as const,
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid" as const,
|
||||
roundness: null,
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
width: 420,
|
||||
height: 260,
|
||||
angle: 0,
|
||||
seed: 1,
|
||||
version: 1,
|
||||
versionNonce: 2,
|
||||
index: null,
|
||||
isDeleted: false,
|
||||
groupIds: [],
|
||||
frameId: null,
|
||||
boundElements: null,
|
||||
updated: 1,
|
||||
link: null,
|
||||
locked: false,
|
||||
};
|
||||
|
||||
describe("codeblock helpers", () => {
|
||||
test("type guard accepts only repo-owned codeblock embeddables", () => {
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
customData: createCodeBlockCustomData(),
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
customData: {
|
||||
excaliType: "other",
|
||||
version: 2,
|
||||
code: "",
|
||||
language: "tsx",
|
||||
showBackground: true,
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
link: "https://example.com",
|
||||
customData: createCodeBlockCustomData(),
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
type: "iframe",
|
||||
customData: createCodeBlockCustomData(),
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isCodeBlockEmbeddable({
|
||||
...baseEmbeddable,
|
||||
customData: {
|
||||
excaliType: "codeblock",
|
||||
version: 1,
|
||||
code: "",
|
||||
language: "tsx",
|
||||
showBackground: true,
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test("create helper defaults background to enabled", () => {
|
||||
expect(createCodeBlockCustomData().showBackground).toBe(
|
||||
DEFAULT_CODE_BLOCK_SHOW_BACKGROUND,
|
||||
);
|
||||
});
|
||||
|
||||
test("update helper preserves unrelated fields and non-codeblock embeddables", () => {
|
||||
const codeBlock = {
|
||||
...baseEmbeddable,
|
||||
customData: createCodeBlockCustomData({ code: "const a = 1;" }),
|
||||
};
|
||||
const foreignEmbeddable = {
|
||||
...baseEmbeddable,
|
||||
id: "embed-2",
|
||||
customData: { source: "foreign" },
|
||||
};
|
||||
const elements = [codeBlock, foreignEmbeddable];
|
||||
|
||||
const nextElements = updateCodeBlockElements(elements, "codeblock-1", {
|
||||
code: "const a = 2;",
|
||||
language: "ts",
|
||||
showBackground: false,
|
||||
});
|
||||
|
||||
expect(nextElements).not.toBe(elements);
|
||||
expect(nextElements[0]).not.toBe(codeBlock);
|
||||
expect(nextElements[0]).toMatchObject({
|
||||
id: "codeblock-1",
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 420,
|
||||
customData: {
|
||||
excaliType: "codeblock",
|
||||
version: 2,
|
||||
code: "const a = 2;",
|
||||
language: "typescript",
|
||||
showBackground: false,
|
||||
},
|
||||
});
|
||||
expect(nextElements[1]).toBe(foreignEmbeddable);
|
||||
expect(
|
||||
updateCodeBlockElements(elements, "embed-2", { code: "ignored" }),
|
||||
).toBe(elements);
|
||||
});
|
||||
|
||||
test("selection helper opens only for a single selected codeblock", () => {
|
||||
const codeBlock = {
|
||||
...baseEmbeddable,
|
||||
customData: createCodeBlockCustomData(),
|
||||
};
|
||||
const other = {
|
||||
...baseEmbeddable,
|
||||
id: "shape-2",
|
||||
type: "rectangle" as const,
|
||||
};
|
||||
|
||||
expect(
|
||||
getCodeBlockSelectionState([codeBlock, other], {
|
||||
selectedElementIds: { "codeblock-1": true },
|
||||
}),
|
||||
).toMatchObject({
|
||||
isPanelOpen: true,
|
||||
selectedCodeBlockId: "codeblock-1",
|
||||
selectedCodeBlock: {
|
||||
customData: {
|
||||
showBackground: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
getCodeBlockSelectionState([codeBlock, other], {
|
||||
selectedElementIds: { "codeblock-1": true, "shape-2": true },
|
||||
}),
|
||||
).toEqual(EMPTY_CODE_BLOCK_SELECTION_STATE);
|
||||
|
||||
expect(
|
||||
getCodeBlockSelectionState([codeBlock, other], {
|
||||
selectedElementIds: { "shape-2": true },
|
||||
}),
|
||||
).toEqual(EMPTY_CODE_BLOCK_SELECTION_STATE);
|
||||
});
|
||||
|
||||
test("language sanitization accepts shiki aliases and normalizes them", () => {
|
||||
expect(sanitizeCodeBlockLanguage("ts")).toBe("typescript");
|
||||
expect(sanitizeCodeBlockLanguage("js")).toBe("javascript");
|
||||
expect(sanitizeCodeBlockLanguage("md")).toBe("markdown");
|
||||
expect(sanitizeCodeBlockLanguage("bash")).toBe("shellscript");
|
||||
expect(sanitizeCodeBlockLanguage("plaintext")).toBe("plaintext");
|
||||
});
|
||||
|
||||
test("custom data reads normalize persisted shiki aliases", () => {
|
||||
const codeBlock = {
|
||||
...baseEmbeddable,
|
||||
customData: {
|
||||
excaliType: "codeblock" as const,
|
||||
version: 2 as const,
|
||||
code: "echo hi",
|
||||
language: "bash" as const,
|
||||
showBackground: true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(getCodeBlockCustomData(codeBlock)).toMatchObject({
|
||||
code: "echo hi",
|
||||
language: "shellscript",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,327 @@
|
||||
import type { AppState } from "@excalidraw/excalidraw/types";
|
||||
import type {
|
||||
ExcalidrawEmbeddableElement,
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import {
|
||||
bundledLanguages,
|
||||
bundledLanguagesInfo,
|
||||
type BundledLanguage,
|
||||
} from "shiki";
|
||||
|
||||
export type CodeBlockLanguage = "plaintext" | BundledLanguage;
|
||||
|
||||
export type CodeBlockCustomData = {
|
||||
excaliType: "codeblock";
|
||||
version: 2;
|
||||
code: string;
|
||||
language: CodeBlockLanguage;
|
||||
showBackground: boolean;
|
||||
};
|
||||
|
||||
export type CodeBlockElement = NonDeleted<ExcalidrawEmbeddableElement> & {
|
||||
customData: CodeBlockCustomData;
|
||||
};
|
||||
|
||||
export type CodeBlockSelectionState = {
|
||||
isPanelOpen: boolean;
|
||||
selectedCodeBlock: CodeBlockElement | null;
|
||||
selectedCodeBlockId: string | null;
|
||||
};
|
||||
|
||||
export type CodeBlockDraftState = {
|
||||
elementId: string;
|
||||
code: string;
|
||||
language: CodeBlockLanguage;
|
||||
showBackground: boolean;
|
||||
};
|
||||
|
||||
const SHIKI_LANGUAGE_SET = new Set<string>(Object.keys(bundledLanguages));
|
||||
const SHIKI_LANGUAGE_ALIAS_TO_ID = new Map<string, BundledLanguage>(
|
||||
bundledLanguagesInfo.flatMap(({ id, aliases }) =>
|
||||
(aliases ?? []).map((alias) => [alias, id as BundledLanguage] as const),
|
||||
),
|
||||
);
|
||||
|
||||
export const CODE_BLOCK_LANGUAGES = Object.freeze([
|
||||
"plaintext",
|
||||
...bundledLanguagesInfo
|
||||
.map(({ id }) => id as BundledLanguage)
|
||||
.sort((left, right) => left.localeCompare(right)),
|
||||
]) as readonly CodeBlockLanguage[];
|
||||
|
||||
export const DEFAULT_CODE_BLOCK_LANGUAGE: CodeBlockLanguage = "tsx";
|
||||
export const DEFAULT_CODE_BLOCK_CODE = [
|
||||
"export function Example() {",
|
||||
' return <button type="button">Click me</button>;',
|
||||
"}",
|
||||
].join("\n");
|
||||
export const DEFAULT_CODE_BLOCK_WIDTH = 420;
|
||||
export const DEFAULT_CODE_BLOCK_HEIGHT = 260;
|
||||
export const CODE_BLOCK_SHIKI_THEME = "poimandres";
|
||||
export const DEFAULT_CODE_BLOCK_SHOW_BACKGROUND = true;
|
||||
|
||||
export const EMPTY_CODE_BLOCK_SELECTION_STATE: CodeBlockSelectionState = {
|
||||
isPanelOpen: false,
|
||||
selectedCodeBlock: null,
|
||||
selectedCodeBlockId: null,
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function randomInt(): number {
|
||||
return (
|
||||
globalThis.crypto?.getRandomValues(new Uint32Array(1))[0] ??
|
||||
Math.floor(Math.random() * 2 ** 31)
|
||||
);
|
||||
}
|
||||
|
||||
function createElementId(): string {
|
||||
if (typeof globalThis.crypto?.randomUUID === "function") {
|
||||
// randomUUID is only available in https context
|
||||
return globalThis.crypto.randomUUID();
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
globalThis.crypto?.getRandomValues(bytes);
|
||||
|
||||
// Mark the identifier as RFC 4122 version 4 when Web Crypto UUID support is
|
||||
// unavailable, such as Firefox over plain HTTP.
|
||||
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
||||
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
||||
|
||||
const segments = [
|
||||
bytes.subarray(0, 4),
|
||||
bytes.subarray(4, 6),
|
||||
bytes.subarray(6, 8),
|
||||
bytes.subarray(8, 10),
|
||||
bytes.subarray(10, 16),
|
||||
];
|
||||
|
||||
return segments
|
||||
.map((segment) =>
|
||||
Array.from(segment, (byte) => byte.toString(16).padStart(2, "0")).join(
|
||||
"",
|
||||
),
|
||||
)
|
||||
.join("-");
|
||||
}
|
||||
|
||||
function getCanonicalCodeBlockLanguage(
|
||||
language: unknown,
|
||||
): CodeBlockLanguage | null {
|
||||
if (language === "plaintext") {
|
||||
return language;
|
||||
}
|
||||
|
||||
if (typeof language !== "string" || !SHIKI_LANGUAGE_SET.has(language)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
SHIKI_LANGUAGE_ALIAS_TO_ID.get(language) ?? (language as BundledLanguage)
|
||||
);
|
||||
}
|
||||
|
||||
export function sanitizeCodeBlockLanguage(
|
||||
language: unknown,
|
||||
): CodeBlockLanguage {
|
||||
return getCanonicalCodeBlockLanguage(language) ?? DEFAULT_CODE_BLOCK_LANGUAGE;
|
||||
}
|
||||
|
||||
export function createCodeBlockCustomData(
|
||||
overrides?: Partial<
|
||||
Pick<CodeBlockCustomData, "code" | "language" | "showBackground">
|
||||
>,
|
||||
): CodeBlockCustomData {
|
||||
return {
|
||||
excaliType: "codeblock",
|
||||
version: 2,
|
||||
code: overrides?.code ?? DEFAULT_CODE_BLOCK_CODE,
|
||||
language: sanitizeCodeBlockLanguage(overrides?.language),
|
||||
showBackground:
|
||||
overrides?.showBackground ?? DEFAULT_CODE_BLOCK_SHOW_BACKGROUND,
|
||||
};
|
||||
}
|
||||
|
||||
export function isCodeBlockCustomData(
|
||||
value: unknown,
|
||||
): value is CodeBlockCustomData {
|
||||
return (
|
||||
isRecord(value) &&
|
||||
value.excaliType === "codeblock" &&
|
||||
value.version === 2 &&
|
||||
typeof value.code === "string" &&
|
||||
getCanonicalCodeBlockLanguage(value.language) !== null &&
|
||||
typeof value.showBackground === "boolean"
|
||||
);
|
||||
}
|
||||
|
||||
export function isCodeBlockEmbeddable(
|
||||
element: unknown,
|
||||
): element is CodeBlockElement {
|
||||
return (
|
||||
isRecord(element) &&
|
||||
element.type === "embeddable" &&
|
||||
element.link === null &&
|
||||
isCodeBlockCustomData(element.customData)
|
||||
);
|
||||
}
|
||||
|
||||
export function getCodeBlockCustomData(
|
||||
element: unknown,
|
||||
): CodeBlockCustomData | null {
|
||||
if (!isCodeBlockEmbeddable(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const language = sanitizeCodeBlockLanguage(element.customData.language);
|
||||
if (language === element.customData.language) {
|
||||
return element.customData;
|
||||
}
|
||||
|
||||
return {
|
||||
...element.customData,
|
||||
language,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCodeBlockElement({
|
||||
x,
|
||||
y,
|
||||
width = DEFAULT_CODE_BLOCK_WIDTH,
|
||||
height = DEFAULT_CODE_BLOCK_HEIGHT,
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}): CodeBlockElement {
|
||||
const now = Date.now();
|
||||
|
||||
return {
|
||||
id: createElementId(),
|
||||
type: "embeddable",
|
||||
x,
|
||||
y,
|
||||
strokeColor: "transparent",
|
||||
backgroundColor: "transparent",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roundness: null,
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
width,
|
||||
height,
|
||||
angle: 0,
|
||||
seed: randomInt(),
|
||||
version: 1,
|
||||
versionNonce: randomInt(),
|
||||
index: null,
|
||||
isDeleted: false,
|
||||
groupIds: [],
|
||||
frameId: null,
|
||||
boundElements: null,
|
||||
updated: now,
|
||||
link: null,
|
||||
locked: false,
|
||||
customData: createCodeBlockCustomData(),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCodeBlockElement<TElement extends ExcalidrawElement>(
|
||||
element: TElement,
|
||||
patch: Partial<
|
||||
Pick<CodeBlockCustomData, "code" | "language" | "showBackground">
|
||||
>,
|
||||
): TElement {
|
||||
if (!isCodeBlockEmbeddable(element)) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const nextCustomData = {
|
||||
...element.customData,
|
||||
...(patch.code !== undefined ? { code: patch.code } : {}),
|
||||
...(patch.language !== undefined
|
||||
? { language: sanitizeCodeBlockLanguage(patch.language) }
|
||||
: {}),
|
||||
...(patch.showBackground !== undefined
|
||||
? { showBackground: patch.showBackground }
|
||||
: {}),
|
||||
} satisfies CodeBlockCustomData;
|
||||
|
||||
if (
|
||||
nextCustomData.code === element.customData.code &&
|
||||
nextCustomData.language === element.customData.language &&
|
||||
nextCustomData.showBackground === element.customData.showBackground
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
|
||||
return {
|
||||
...element,
|
||||
customData: nextCustomData,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateCodeBlockElements<TElement extends ExcalidrawElement>(
|
||||
elements: readonly TElement[],
|
||||
elementId: string,
|
||||
patch: Partial<
|
||||
Pick<CodeBlockCustomData, "code" | "language" | "showBackground">
|
||||
>,
|
||||
): readonly TElement[] {
|
||||
let changed = false;
|
||||
|
||||
const nextElements = elements.map((element) => {
|
||||
if (element.id !== elementId) {
|
||||
return element;
|
||||
}
|
||||
|
||||
const nextElement = updateCodeBlockElement(element, patch);
|
||||
changed ||= nextElement !== element;
|
||||
return nextElement;
|
||||
});
|
||||
|
||||
return changed ? nextElements : elements;
|
||||
}
|
||||
|
||||
export function getCodeBlockSelectionState(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: Pick<AppState, "selectedElementIds">,
|
||||
): CodeBlockSelectionState {
|
||||
const selectedIds = Object.entries(appState.selectedElementIds ?? {})
|
||||
.filter(([, selected]) => selected)
|
||||
.map(([elementId]) => elementId);
|
||||
|
||||
if (selectedIds.length !== 1) {
|
||||
return EMPTY_CODE_BLOCK_SELECTION_STATE;
|
||||
}
|
||||
|
||||
const selectedCodeBlock = elements.find(
|
||||
(element) =>
|
||||
element.id === selectedIds[0] && isCodeBlockEmbeddable(element),
|
||||
);
|
||||
|
||||
if (!selectedCodeBlock || !isCodeBlockEmbeddable(selectedCodeBlock)) {
|
||||
return EMPTY_CODE_BLOCK_SELECTION_STATE;
|
||||
}
|
||||
|
||||
const customData = getCodeBlockCustomData(selectedCodeBlock);
|
||||
if (!customData) {
|
||||
return EMPTY_CODE_BLOCK_SELECTION_STATE;
|
||||
}
|
||||
|
||||
return {
|
||||
isPanelOpen: true,
|
||||
selectedCodeBlock: {
|
||||
...selectedCodeBlock,
|
||||
customData,
|
||||
},
|
||||
selectedCodeBlockId: selectedCodeBlock.id,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { getSingletonHighlighter, type Highlighter } from "shiki";
|
||||
import { CODE_BLOCK_SHIKI_THEME, type CodeBlockLanguage } from "./codeblock";
|
||||
|
||||
let highlighterPromise: Promise<Highlighter> | null = null;
|
||||
|
||||
export function getCodeBlockHighlighter() {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = getSingletonHighlighter({
|
||||
themes: [CODE_BLOCK_SHIKI_THEME],
|
||||
});
|
||||
}
|
||||
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
export function getHighlightableCodeBlockLanguage(
|
||||
language: CodeBlockLanguage,
|
||||
): Exclude<CodeBlockLanguage, "plaintext"> | null {
|
||||
return language === "plaintext" ? null : language;
|
||||
}
|
||||
|
||||
export async function renderHighlightedCodeBlockHtml(
|
||||
code: string,
|
||||
language: CodeBlockLanguage,
|
||||
): Promise<string | null> {
|
||||
const shikiLanguage = getHighlightableCodeBlockLanguage(language);
|
||||
if (!shikiLanguage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const highlighter = await getCodeBlockHighlighter();
|
||||
await highlighter.loadLanguage(shikiLanguage);
|
||||
return highlighter.codeToHtml(code, {
|
||||
lang: shikiLanguage,
|
||||
theme: CODE_BLOCK_SHIKI_THEME,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import type { ExcalidrawProps } from "@excalidraw/excalidraw/types";
|
||||
import type {
|
||||
ExcalidrawEmbeddableElement,
|
||||
NonDeleted,
|
||||
} from "@excalidraw/excalidraw/element/types";
|
||||
import { getCodeBlockCustomData, isCodeBlockEmbeddable } from "../codeblock";
|
||||
import { renderHighlightedCodeBlockHtml } from "../codeblockHighlight";
|
||||
|
||||
type CodeBlockEmbeddableProps = {
|
||||
element: NonDeleted<ExcalidrawEmbeddableElement>;
|
||||
};
|
||||
|
||||
const CodeBlockEmbeddable = memo(function CodeBlockEmbeddable({
|
||||
element,
|
||||
}: CodeBlockEmbeddableProps) {
|
||||
const customData = getCodeBlockCustomData(element);
|
||||
const [html, setHtml] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!customData) {
|
||||
setHtml(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
void renderHighlightedCodeBlockHtml(customData.code, customData.language)
|
||||
.then((nextHtml) => {
|
||||
if (!cancelled) {
|
||||
setHtml(nextHtml);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setHtml(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [customData]);
|
||||
|
||||
if (!customData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"codeblock-embeddable",
|
||||
customData.showBackground
|
||||
? "codeblock-surface--background"
|
||||
: "codeblock-surface--transparent",
|
||||
].join(" ")}
|
||||
>
|
||||
{html ? (
|
||||
<div
|
||||
className="codeblock-embeddable-html"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
) : (
|
||||
<pre className="codeblock-embeddable-fallback">
|
||||
<code>{customData.code}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const renderCodeBlockEmbeddable: NonNullable<
|
||||
ExcalidrawProps["renderEmbeddable"]
|
||||
> = (element) => {
|
||||
if (!isCodeBlockEmbeddable(element)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <CodeBlockEmbeddable element={element} />;
|
||||
};
|
||||
@@ -0,0 +1,184 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
CODE_BLOCK_LANGUAGES,
|
||||
type CodeBlockDraftState,
|
||||
type CodeBlockLanguage,
|
||||
} from "../codeblock";
|
||||
import { renderHighlightedCodeBlockHtml } from "../codeblockHighlight";
|
||||
|
||||
type CodeBlockSidebarProps = {
|
||||
open: boolean;
|
||||
draft: CodeBlockDraftState | null;
|
||||
onCodeChange: (code: string) => void;
|
||||
onLanguageChange: (language: CodeBlockLanguage) => void;
|
||||
onShowBackgroundChange: (showBackground: boolean) => void;
|
||||
onCommit: () => void;
|
||||
};
|
||||
|
||||
function CodeBlockEditor({
|
||||
draft,
|
||||
onCodeChange,
|
||||
onCommit,
|
||||
}: {
|
||||
draft: CodeBlockDraftState;
|
||||
onCodeChange: (code: string) => void;
|
||||
onCommit: () => void;
|
||||
}) {
|
||||
const [html, setHtml] = useState<string | null>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const highlightRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void renderHighlightedCodeBlockHtml(draft.code, draft.language)
|
||||
.then((nextHtml) => {
|
||||
if (!cancelled) {
|
||||
setHtml(nextHtml);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setHtml(null);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [draft.code, draft.language]);
|
||||
|
||||
const syncScroll = () => {
|
||||
const textarea = textareaRef.current;
|
||||
const highlight = highlightRef.current;
|
||||
if (!textarea || !highlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
highlight.scrollTop = textarea.scrollTop;
|
||||
highlight.scrollLeft = textarea.scrollLeft;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
"codeblock-editor-surface",
|
||||
draft.showBackground
|
||||
? "codeblock-surface--background"
|
||||
: "codeblock-surface--transparent",
|
||||
].join(" ")}
|
||||
>
|
||||
<div
|
||||
ref={highlightRef}
|
||||
className="codeblock-editor-highlight"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{html ? (
|
||||
<div
|
||||
className="codeblock-editor-highlight-html"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
) : (
|
||||
<pre className="codeblock-editor-highlight-fallback">
|
||||
<code>{draft.code || " "}</code>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="codeblock-editor-textarea"
|
||||
value={draft.code}
|
||||
onChange={(event) => onCodeChange(event.target.value)}
|
||||
onBlur={onCommit}
|
||||
onScroll={syncScroll}
|
||||
spellCheck={false}
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
aria-label="Code block contents"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlockIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M8 8 4.5 12 8 16M16 8l3.5 4-3.5 4M13 6l-2 12"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.8"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function InsertCodeBlockButton({
|
||||
onClick,
|
||||
disabled,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button app-actions-toggle codeblock-toolbar-button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label="Insert code block"
|
||||
>
|
||||
<CodeBlockIcon />
|
||||
<span className="sr-only">Insert code block</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function CodeBlockSidebar({
|
||||
open,
|
||||
draft,
|
||||
onCodeChange,
|
||||
onLanguageChange,
|
||||
onShowBackgroundChange,
|
||||
onCommit,
|
||||
}: CodeBlockSidebarProps) {
|
||||
if (!open || !draft) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="codeblock-sidebar">
|
||||
<select
|
||||
className="codeblock-language-select"
|
||||
value={draft.language}
|
||||
onChange={(event) =>
|
||||
onLanguageChange(event.target.value as CodeBlockLanguage)
|
||||
}
|
||||
onBlur={onCommit}
|
||||
aria-label="Code block language"
|
||||
>
|
||||
{CODE_BLOCK_LANGUAGES.map((language) => (
|
||||
<option key={language} value={language}>
|
||||
{language}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<CodeBlockEditor
|
||||
key={draft.elementId}
|
||||
draft={draft}
|
||||
onCodeChange={onCodeChange}
|
||||
onCommit={onCommit}
|
||||
/>
|
||||
<label className="codeblock-background-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={draft.showBackground}
|
||||
onChange={(event) => onShowBackgroundChange(event.target.checked)}
|
||||
onBlur={onCommit}
|
||||
/>
|
||||
<span>Background</span>
|
||||
</label>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -3,10 +3,16 @@ import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFiles,
|
||||
ExcalidrawImperativeAPI,
|
||||
ExcalidrawInitialDataState,
|
||||
ExcalidrawProps,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import "../../../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
|
||||
import { type ScenePayload } from "../../core/shared";
|
||||
import {
|
||||
getCodeBlockSelectionState,
|
||||
type CodeBlockSelectionState,
|
||||
} from "../codeblock";
|
||||
|
||||
type EditorCanvasProps = {
|
||||
activeId: string | null;
|
||||
@@ -15,7 +21,10 @@ type EditorCanvasProps = {
|
||||
error: string | null;
|
||||
editorReloadNonce: number;
|
||||
onSceneChange: (scene: ScenePayload) => void;
|
||||
onSelectionStateChange: (selection: CodeBlockSelectionState) => void;
|
||||
onEditorActivity: () => void;
|
||||
onExcalidrawAPI: (api: ExcalidrawImperativeAPI) => void;
|
||||
renderEmbeddable?: ExcalidrawProps["renderEmbeddable"];
|
||||
};
|
||||
|
||||
function sceneToInitialData(scene: ScenePayload): ExcalidrawInitialDataState {
|
||||
@@ -42,7 +51,10 @@ export const EditorCanvas = memo(function EditorCanvas({
|
||||
error,
|
||||
editorReloadNonce,
|
||||
onSceneChange,
|
||||
onSelectionStateChange,
|
||||
onEditorActivity,
|
||||
onExcalidrawAPI,
|
||||
renderEmbeddable,
|
||||
}: EditorCanvasProps) {
|
||||
if (loading || !scene) {
|
||||
return <div className="editor-loading">{error ?? "Loading..."}</div>;
|
||||
@@ -53,9 +65,14 @@ export const EditorCanvas = memo(function EditorCanvas({
|
||||
<Excalidraw
|
||||
key={`${activeId}:${editorReloadNonce}`}
|
||||
initialData={sceneToInitialData(scene)}
|
||||
excalidrawAPI={onExcalidrawAPI}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
onChange={(elements, appState, files) => {
|
||||
onEditorActivity();
|
||||
onSceneChange(sceneFromEditor(elements, appState, files));
|
||||
onSelectionStateChange(
|
||||
getCodeBlockSelectionState(elements, appState),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { memo } from "react";
|
||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||
import type { ExcalidrawInitialDataState } from "@excalidraw/excalidraw/types";
|
||||
import type {
|
||||
ExcalidrawInitialDataState,
|
||||
ExcalidrawProps,
|
||||
} from "@excalidraw/excalidraw/types";
|
||||
import "../../../node_modules/@excalidraw/excalidraw/dist/prod/index.css";
|
||||
import { type PublicDrawing } from "../../core/shared";
|
||||
|
||||
@@ -8,6 +11,7 @@ type PublicViewerProps = {
|
||||
drawing: PublicDrawing | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
renderEmbeddable?: ExcalidrawProps["renderEmbeddable"];
|
||||
};
|
||||
|
||||
function drawingToInitialData(
|
||||
@@ -35,6 +39,7 @@ export const PublicViewer = memo(function PublicViewer({
|
||||
drawing,
|
||||
loading,
|
||||
error,
|
||||
renderEmbeddable,
|
||||
}: PublicViewerProps) {
|
||||
if (loading || !drawing) {
|
||||
return <div className="editor-loading">{error ?? "Loading..."}</div>;
|
||||
@@ -47,6 +52,7 @@ export const PublicViewer = memo(function PublicViewer({
|
||||
viewModeEnabled={true}
|
||||
zenModeEnabled={true}
|
||||
UIOptions={viewerUiOptions}
|
||||
renderEmbeddable={renderEmbeddable}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -231,6 +231,8 @@ export function useDrawingSession() {
|
||||
}
|
||||
if (isManual) {
|
||||
showToast("Saved", 2000);
|
||||
} else {
|
||||
showToast(null);
|
||||
}
|
||||
})
|
||||
.catch((saveError: unknown) => {
|
||||
@@ -251,6 +253,8 @@ export function useDrawingSession() {
|
||||
);
|
||||
if (isManual) {
|
||||
showToast("Save failed", 2000);
|
||||
} else {
|
||||
showToast("Autosave failed");
|
||||
}
|
||||
throw saveError;
|
||||
})
|
||||
|
||||
+208
-1
@@ -27,7 +27,9 @@ body {
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
@@ -211,6 +213,7 @@ input {
|
||||
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 48px);
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.app-actions-toggle {
|
||||
@@ -227,6 +230,13 @@ input {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
|
||||
.codeblock-toolbar-button {
|
||||
position: fixed;
|
||||
top: max(16px, calc(env(safe-area-inset-top) + 16px));
|
||||
left: calc(50% + 314px);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.app-actions-toggle svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -323,6 +333,190 @@ input {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.codeblock-sidebar {
|
||||
position: fixed;
|
||||
top: 88px;
|
||||
right: max(16px, calc(env(safe-area-inset-right) + 16px));
|
||||
bottom: 16px;
|
||||
z-index: 25;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
width: min(360px, calc(100vw - 32px));
|
||||
padding: 24px 12px 12px;
|
||||
border: 1px solid var(--app-sidebar-border);
|
||||
border-radius: 16px;
|
||||
background: color-mix(in srgb, var(--app-sidebar-bg) 92%, white);
|
||||
box-shadow: var(--app-sidebar-shadow);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.codeblock-language-select,
|
||||
.codeblock-editor-surface {
|
||||
border: 1px solid var(--app-input-border);
|
||||
color: var(--app-input-color);
|
||||
}
|
||||
|
||||
.codeblock-background-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--app-text-color);
|
||||
font-size: var(--app-sidebar-font-size);
|
||||
}
|
||||
|
||||
.codeblock-background-toggle input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.codeblock-language-select {
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
background: var(--app-input-bg);
|
||||
}
|
||||
|
||||
.codeblock-editor-surface {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codeblock-editor-highlight,
|
||||
.codeblock-editor-highlight-html,
|
||||
.codeblock-editor-highlight-html :where(pre.shiki),
|
||||
.codeblock-editor-highlight-fallback,
|
||||
.codeblock-editor-textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.codeblock-editor-highlight,
|
||||
.codeblock-editor-textarea,
|
||||
.codeblock-editor-highlight-html :where(pre.shiki),
|
||||
.codeblock-editor-highlight-fallback {
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.codeblock-editor-highlight {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.codeblock-editor-highlight-html :where(pre.shiki),
|
||||
.codeblock-editor-highlight-fallback {
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.codeblock-editor-highlight-html :where(code),
|
||||
.codeblock-editor-highlight-fallback code {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.codeblock-editor-highlight-fallback,
|
||||
.codeblock-embeddable-fallback {
|
||||
color: #a6accd;
|
||||
}
|
||||
|
||||
.codeblock-editor-textarea {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
padding: 12px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
caret-color: #e4f0fb;
|
||||
}
|
||||
|
||||
.codeblock-editor-textarea::selection {
|
||||
background: rgba(113, 124, 180, 0.45);
|
||||
}
|
||||
|
||||
.codeblock-editor-textarea:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.codeblock-embeddable,
|
||||
.codeblock-embeddable-html,
|
||||
.codeblock-embeddable-html :where(pre.shiki),
|
||||
.codeblock-embeddable-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.codeblock-embeddable {
|
||||
overflow: auto;
|
||||
border-radius: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codeblock-embeddable-html :where(pre.shiki),
|
||||
.codeblock-embeddable-fallback {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
monospace;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.codeblock-embeddable-html :where(pre.shiki) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.codeblock-surface--background {
|
||||
background: #1b1e28;
|
||||
}
|
||||
|
||||
.codeblock-surface--background .codeblock-embeddable-html :where(pre.shiki),
|
||||
.codeblock-surface--background
|
||||
.codeblock-editor-highlight-html
|
||||
:where(pre.shiki) {
|
||||
background: #1b1e28 !important;
|
||||
}
|
||||
|
||||
.codeblock-surface--background .codeblock-embeddable-fallback,
|
||||
.codeblock-surface--background .codeblock-editor-highlight-fallback {
|
||||
background: #1b1e28;
|
||||
}
|
||||
|
||||
.codeblock-surface--transparent {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codeblock-surface--transparent .codeblock-embeddable-html :where(pre.shiki),
|
||||
.codeblock-surface--transparent
|
||||
.codeblock-editor-highlight-html
|
||||
:where(pre.shiki) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.codeblock-surface--transparent .codeblock-embeddable-fallback,
|
||||
.codeblock-surface--transparent .codeblock-editor-highlight-fallback {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.codeblock-embeddable-html :where(code),
|
||||
.codeblock-embeddable-fallback code {
|
||||
display: block;
|
||||
min-height: 100%;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.editor-loading {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
@@ -359,4 +553,17 @@ input {
|
||||
right: max(16px, calc(env(safe-area-inset-right) + 16px));
|
||||
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 56px);
|
||||
}
|
||||
|
||||
.codeblock-toolbar-button {
|
||||
left: calc(50% + 254px);
|
||||
}
|
||||
|
||||
.codeblock-sidebar {
|
||||
top: auto;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: calc(max(16px, env(safe-area-inset-bottom)) + 72px);
|
||||
width: auto;
|
||||
height: min(44vh, 360px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export function createServer(options: CreateServerOptions = {}) {
|
||||
return {
|
||||
port: Number(process.env.PORT ?? "3000"),
|
||||
hostname: process.env.HOST ?? "localhost",
|
||||
idleTimeout: 120,
|
||||
routes: {
|
||||
"/": (request: Request) => {
|
||||
const drawing = drawingStore.ensureInitialDrawing();
|
||||
|
||||
Reference in New Issue
Block a user