mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea0824878d | ||
|
|
4787be4eb1 |
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "clawhub",
|
||||
@@ -25,6 +26,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@resvg/resvg-wasm": "^2.6.2",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
@@ -49,6 +51,8 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.4",
|
||||
"shiki": "^4.0.2",
|
||||
@@ -591,6 +595,8 @@
|
||||
|
||||
"@shikijs/primitive": ["@shikijs/primitive@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw=="],
|
||||
|
||||
"@shikijs/rehype": ["@shikijs/rehype@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2", "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.1", "shiki": "4.0.2", "unified": "^11.0.5", "unist-util-visit": "^5.1.0" } }, "sha512-cmPlKLD8JeojasNFoY64162ScpEdEdQUMuVodPCrv1nx1z3bjmGwoKWDruQWa/ejSznImlaeB0Ty6Q3zPaVQAA=="],
|
||||
|
||||
"@shikijs/themes": ["@shikijs/themes@4.0.2", "", { "dependencies": { "@shikijs/types": "4.0.2" } }, "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA=="],
|
||||
|
||||
"@shikijs/types": ["@shikijs/types@4.0.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg=="],
|
||||
@@ -961,12 +967,26 @@
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="],
|
||||
|
||||
"hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="],
|
||||
|
||||
"hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="],
|
||||
|
||||
"hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="],
|
||||
|
||||
"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-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
|
||||
|
||||
"hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="],
|
||||
|
||||
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"hookable": ["hookable@6.1.0", "", {}, "sha512-ZoKZSJgu8voGK2geJS+6YtYjvIzu9AOM/KZXsBxr83uhLL++e9pEv/dlgwgy3dvHg06kTz6JOh1hk3C8Ceiymw=="],
|
||||
|
||||
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
|
||||
@@ -1283,6 +1303,10 @@
|
||||
|
||||
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
|
||||
|
||||
"rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="],
|
||||
|
||||
"rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="],
|
||||
|
||||
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
|
||||
|
||||
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
|
||||
@@ -1441,6 +1465,8 @@
|
||||
|
||||
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
|
||||
|
||||
"vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="],
|
||||
|
||||
"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.5", "", { "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 || ^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-nmu43Qvq9UopTRfMx2jOYW5l16pb3iDC1JH6yMuPkpVbzK0k+L7dfsEDH4jRgYFmsg0sTAqkojoZgzLMlwHsCQ=="],
|
||||
@@ -1453,6 +1479,8 @@
|
||||
|
||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
@@ -1595,6 +1623,8 @@
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"hast-util-raw/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
|
||||
|
||||
"htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 394 KiB |
@@ -52,6 +52,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.11",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@resvg/resvg-wasm": "^2.6.2",
|
||||
"@shikijs/rehype": "^4.0.2",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@tanstack/react-devtools": "0.10.0",
|
||||
"@tanstack/react-router": "1.168.1",
|
||||
@@ -76,6 +77,8 @@
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"semver": "^7.7.4",
|
||||
"shiki": "^4.0.2",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 133 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 281 KiB |
+102
@@ -0,0 +1,102 @@
|
||||
<svg width="1200" height="630" viewBox="0 0 1200 630" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<radialGradient id="bgGlowRight" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1048 328) rotate(180) scale(358 260)">
|
||||
<stop stop-color="#7B1F18" stop-opacity="0.58"/>
|
||||
<stop offset="0.45" stop-color="#431210" stop-opacity="0.26"/>
|
||||
<stop offset="1" stop-color="#030305" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="bgGlowBottom" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(192 610) rotate(-90) scale(180 420)">
|
||||
<stop stop-color="#A12A1D" stop-opacity="0.2"/>
|
||||
<stop offset="1" stop-color="#030305" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="frameStroke" x1="40" y1="106" x2="1146" y2="530" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#6D3437" stop-opacity="0.8"/>
|
||||
<stop offset="0.55" stop-color="#DF5D35" stop-opacity="0.36"/>
|
||||
<stop offset="1" stop-color="#FF6D39" stop-opacity="0.9"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="frameGlow" x1="164" y1="164" x2="1114" y2="476" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#130D10"/>
|
||||
<stop offset="0.5" stop-color="#170B0E"/>
|
||||
<stop offset="1" stop-color="#261011"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="logoStroke" x1="112" y1="140" x2="398" y2="430" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#4C2A28" stop-opacity="0.55"/>
|
||||
<stop offset="1" stop-color="#E05831" stop-opacity="0.28"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="searchStroke" x1="146" y1="480" x2="1068" y2="480" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#7F342A" stop-opacity="0.65"/>
|
||||
<stop offset="1" stop-color="#FF6F37" stop-opacity="0.85"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="buttonFill" x1="838" y1="445" x2="1084" y2="510" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#D55335"/>
|
||||
<stop offset="1" stop-color="#EB6A3E"/>
|
||||
</linearGradient>
|
||||
<filter id="softBlur" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="20"/>
|
||||
</filter>
|
||||
<filter id="glowBlur" x="-40%" y="-40%" width="180%" height="180%">
|
||||
<feGaussianBlur stdDeviation="10"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="1200" height="630" fill="#030305"/>
|
||||
<rect width="1200" height="630" fill="url(#bgGlowRight)"/>
|
||||
<rect width="1200" height="630" fill="url(#bgGlowBottom)"/>
|
||||
|
||||
<g opacity="0.22">
|
||||
<circle cx="998" cy="406" r="1.8" fill="#FF7649"/>
|
||||
<circle cx="1036" cy="446" r="1.4" fill="#FF7649"/>
|
||||
<circle cx="1088" cy="492" r="1.2" fill="#FF7649"/>
|
||||
<circle cx="1116" cy="540" r="1.1" fill="#FF7649"/>
|
||||
<circle cx="964" cy="502" r="1.3" fill="#FF7649"/>
|
||||
<circle cx="880" cy="528" r="1.3" fill="#FF7649"/>
|
||||
<circle cx="716" cy="452" r="1.1" fill="#FF7649"/>
|
||||
<circle cx="622" cy="396" r="1.4" fill="#FF7649"/>
|
||||
<circle cx="188" cy="558" r="1.3" fill="#FF7649"/>
|
||||
<circle cx="152" cy="580" r="1.1" fill="#FF7649"/>
|
||||
<circle cx="92" cy="594" r="1.4" fill="#FF7649"/>
|
||||
</g>
|
||||
|
||||
<path d="M870 146C990 166 1082 230 1142 328" stroke="#AA3D2B" stroke-opacity="0.16" stroke-width="2"/>
|
||||
<path d="M926 190C1036 234 1108 306 1168 420" stroke="#AA3D2B" stroke-opacity="0.12" stroke-width="2"/>
|
||||
<path d="M1044 374H1200" stroke="#B74A36" stroke-opacity="0.28" stroke-width="2"/>
|
||||
<path d="M24 522H164" stroke="#B74A36" stroke-opacity="0.22" stroke-width="2"/>
|
||||
|
||||
<rect x="42" y="108" width="1092" height="430" rx="42" fill="url(#frameGlow)"/>
|
||||
<rect x="42.75" y="108.75" width="1090.5" height="428.5" rx="41.25" stroke="url(#frameStroke)" stroke-width="1.5"/>
|
||||
<rect x="113" y="148" width="292" height="292" rx="38" fill="#09090C"/>
|
||||
<rect x="113.75" y="148.75" width="290.5" height="290.5" rx="37.25" stroke="url(#logoStroke)" stroke-width="1.5"/>
|
||||
|
||||
<ellipse cx="1118" cy="180" rx="86" ry="42" fill="#FF6532" fill-opacity="0.18" filter="url(#glowBlur)"/>
|
||||
<ellipse cx="1018" cy="328" rx="208" ry="164" fill="#8A2218" fill-opacity="0.12" filter="url(#softBlur)"/>
|
||||
<ellipse cx="96" cy="532" rx="48" ry="10" fill="#FF5E35" fill-opacity="0.24" filter="url(#softBlur)"/>
|
||||
<ellipse cx="572" cy="494" rx="302" ry="12" fill="#FF5E35" fill-opacity="0.12" filter="url(#softBlur)"/>
|
||||
|
||||
<image href="clawd-logo.png" x="124" y="158" width="270" height="270" preserveAspectRatio="xMidYMid meet"/>
|
||||
|
||||
<text x="500" y="256" fill="#F8EEE8" font-size="88" font-weight="900" letter-spacing="-4.2" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">ClawHub.ai</text>
|
||||
<text x="500" y="338" fill="#F8EEE8" font-size="42" font-weight="800" letter-spacing="-1.2" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">Equip. Install. <tspan fill="#FF6236">Build.</tspan></text>
|
||||
<text x="500" y="390" fill="#E1D4CF" font-size="23" font-weight="500" letter-spacing="-0.15" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">
|
||||
<tspan x="500" dy="0">Developer tools and agent skills</tspan>
|
||||
<tspan x="500" dy="28">for your next project.</tspan>
|
||||
</text>
|
||||
|
||||
<g>
|
||||
<rect x="148" y="432" width="924" height="104" rx="31" fill="#11090D"/>
|
||||
<rect x="148.75" y="432.75" width="922.5" height="102.5" rx="30.25" stroke="url(#searchStroke)" stroke-width="1.5"/>
|
||||
<circle cx="220" cy="484" r="19" stroke="#FFF9F3" stroke-width="5"/>
|
||||
<line x1="233" y1="497" x2="249" y2="513" stroke="#FFF9F3" stroke-width="5" stroke-linecap="round"/>
|
||||
<text x="272" y="495" fill="#DCD0CB" font-size="31" font-weight="600" letter-spacing="-0.4" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">What are you looking for?</text>
|
||||
<rect x="824" y="450" width="258" height="66" rx="21" fill="url(#buttonFill)"/>
|
||||
<text x="953" y="494" text-anchor="middle" fill="#FFF8F1" font-size="28" font-weight="800" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">Search tools</text>
|
||||
</g>
|
||||
|
||||
<g>
|
||||
<rect x="292" y="574" width="214" height="44" rx="22" fill="#1B1317" stroke="#3A292C"/>
|
||||
<text x="399" y="603" text-anchor="middle" fill="#EEE3DF" font-size="18" font-weight="500" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">self-improving</text>
|
||||
<rect x="530" y="574" width="248" height="44" rx="22" fill="#1B1317" stroke="#3A292C"/>
|
||||
<text x="654" y="603" text-anchor="middle" fill="#EEE3DF" font-size="18" font-weight="500" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">GitHub integration</text>
|
||||
<rect x="802" y="574" width="180" height="44" rx="22" fill="#1B1317" stroke="#3A292C"/>
|
||||
<text x="892" y="603" text-anchor="middle" fill="#EEE3DF" font-size="18" font-weight="500" font-family="'Helvetica Neue', Helvetica, Arial, sans-serif">dashboard</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
@@ -0,0 +1,176 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MarkdownPreview } from "./MarkdownPreview";
|
||||
|
||||
function renderMarkdown(source: string) {
|
||||
// Disable Shiki highlighting to keep the tree synchronous for assertions.
|
||||
const { container } = render(<MarkdownPreview highlight={false}>{source}</MarkdownPreview>);
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("MarkdownPreview — raw HTML passthrough", () => {
|
||||
it("renders an <h1 align=\"center\"> block as a real <h1>", () => {
|
||||
const container = renderMarkdown(`<h1 align="center">Hello logo</h1>`);
|
||||
const h1 = container.querySelector("h1");
|
||||
expect(h1).not.toBeNull();
|
||||
expect(h1?.textContent).toBe("Hello logo");
|
||||
});
|
||||
|
||||
it("renders a <div align=\"center\"> block as a real <div>", () => {
|
||||
const container = renderMarkdown(`<div align="center">centered</div>`);
|
||||
const div = container.querySelector("div[align=\"center\"]");
|
||||
expect(div).not.toBeNull();
|
||||
expect(div?.textContent).toBe("centered");
|
||||
});
|
||||
|
||||
it("renders <picture> with <source> + <img> fallback", () => {
|
||||
const container = renderMarkdown(
|
||||
`<picture><source media="(prefers-color-scheme: dark)" srcset="dark.png"/><img alt="Logo" src="light.png"/></picture>`,
|
||||
);
|
||||
expect(container.querySelector("picture")).not.toBeNull();
|
||||
expect(container.querySelector("picture source")).not.toBeNull();
|
||||
const img = container.querySelector("picture img");
|
||||
expect(img).not.toBeNull();
|
||||
expect(img?.getAttribute("alt")).toBe("Logo");
|
||||
expect(img?.getAttribute("src")).toBe("light.png");
|
||||
});
|
||||
|
||||
it("renders standalone <img> tags with src and alt", () => {
|
||||
const container = renderMarkdown(`<img src="screenshot.png" alt="Demo screenshot"/>`);
|
||||
const img = container.querySelector("img");
|
||||
expect(img).not.toBeNull();
|
||||
expect(img?.getAttribute("src")).toBe("screenshot.png");
|
||||
expect(img?.getAttribute("alt")).toBe("Demo screenshot");
|
||||
});
|
||||
|
||||
it("renders <br/> as a real line break", () => {
|
||||
const container = renderMarkdown(`line one<br/>line two`);
|
||||
expect(container.querySelector("br")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders the Opik README banner (centered h1 + picture + img)", () => {
|
||||
const opikBanner = `<h1 align="center">
|
||||
<a href="https://www.comet.com/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="dark.svg"/>
|
||||
<img alt="Comet Opik logo" src="light.svg" width="200"/>
|
||||
</picture>
|
||||
</a>
|
||||
<br/>OpenClaw Opik Observability Plugin
|
||||
</h1>`;
|
||||
const container = renderMarkdown(opikBanner);
|
||||
expect(container.querySelector("h1")).not.toBeNull();
|
||||
expect(container.querySelector("picture")).not.toBeNull();
|
||||
const img = container.querySelector("img");
|
||||
expect(img?.getAttribute("alt")).toBe("Comet Opik logo");
|
||||
// And the escaped tag must NOT be present as literal text anywhere.
|
||||
expect(container.textContent ?? "").not.toContain("<picture>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MarkdownPreview — standard markdown still renders", () => {
|
||||
it("renders ATX headings", () => {
|
||||
const container = renderMarkdown(`## Why This Plugin`);
|
||||
const h2 = container.querySelector("h2");
|
||||
expect(h2?.textContent).toBe("Why This Plugin");
|
||||
});
|
||||
|
||||
it("renders markdown links", () => {
|
||||
const container = renderMarkdown(`[Opik](https://example.com/opik)`);
|
||||
const a = container.querySelector("a");
|
||||
expect(a?.getAttribute("href")).toBe("https://example.com/opik");
|
||||
expect(a?.textContent).toBe("Opik");
|
||||
});
|
||||
|
||||
it("renders inline code", () => {
|
||||
const container = renderMarkdown("Use `@opik/opik-openclaw` now.");
|
||||
const code = container.querySelector("code");
|
||||
expect(code?.textContent).toBe("@opik/opik-openclaw");
|
||||
});
|
||||
|
||||
it("renders unordered lists", () => {
|
||||
const container = renderMarkdown(`- one\n- two\n- three`);
|
||||
const items = container.querySelectorAll("li");
|
||||
expect(items.length).toBe(3);
|
||||
expect(items[0].textContent).toBe("one");
|
||||
});
|
||||
|
||||
it("renders GFM tables", () => {
|
||||
const container = renderMarkdown(
|
||||
[
|
||||
"| Key | Value |",
|
||||
"| --- | ----- |",
|
||||
"| a | 1 |",
|
||||
"| b | 2 |",
|
||||
].join("\n"),
|
||||
);
|
||||
expect(container.querySelector("table")).not.toBeNull();
|
||||
expect(container.querySelectorAll("tbody tr").length).toBe(2);
|
||||
});
|
||||
|
||||
it("renders fenced code blocks as <pre><code>", () => {
|
||||
const container = renderMarkdown("```ts\nconst x = 1;\n```");
|
||||
const code = container.querySelector("pre code");
|
||||
expect(code).not.toBeNull();
|
||||
expect(code?.textContent).toContain("const x = 1;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MarkdownPreview — syntax highlighting", () => {
|
||||
it("shiki-highlights fenced code blocks (produces colored <span> tokens)", async () => {
|
||||
const { container } = render(
|
||||
<MarkdownPreview>{"```ts\nconst x: number = 1;\n```"}</MarkdownPreview>,
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const pre = container.querySelector("pre");
|
||||
// Shiki wraps the output in <pre class="shiki ..."> and tokens are
|
||||
// <span style="color:#...">.
|
||||
expect(pre?.className ?? "").toMatch(/shiki/);
|
||||
const coloredSpans = container.querySelectorAll("pre span[style*='color']");
|
||||
expect(coloredSpans.length).toBeGreaterThan(0);
|
||||
},
|
||||
{ timeout: 8000 },
|
||||
);
|
||||
|
||||
// Raw code text must still be present after highlighting
|
||||
expect(container.querySelector("pre")?.textContent).toContain("const x");
|
||||
});
|
||||
|
||||
it("leaves the highlight prop honored — highlight={false} renders plain <pre><code>", () => {
|
||||
const { container } = render(
|
||||
<MarkdownPreview highlight={false}>{"```ts\nconst x = 1;\n```"}</MarkdownPreview>,
|
||||
);
|
||||
const pre = container.querySelector("pre");
|
||||
// No shiki class, no colored spans
|
||||
expect(pre?.className ?? "").not.toMatch(/shiki/);
|
||||
expect(container.querySelectorAll("pre span[style*='color']").length).toBe(0);
|
||||
expect(pre?.textContent).toContain("const x = 1;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MarkdownPreview — sanitization of malicious HTML", () => {
|
||||
it("strips <script> tags", () => {
|
||||
const container = renderMarkdown(`hello<script>window.__pwn = 1;</script>world`);
|
||||
expect(container.querySelector("script")).toBeNull();
|
||||
expect(container.textContent ?? "").not.toContain("window.__pwn");
|
||||
});
|
||||
|
||||
it("strips onerror handlers on <img>", () => {
|
||||
const container = renderMarkdown(`<img src="x" onerror="window.__pwn = 1" alt="x"/>`);
|
||||
const img = container.querySelector("img");
|
||||
// The img itself can render; the handler must be gone.
|
||||
expect(img?.getAttribute("onerror")).toBeNull();
|
||||
});
|
||||
|
||||
it("strips javascript: hrefs on anchors", () => {
|
||||
const container = renderMarkdown(`<a href="javascript:alert(1)">click</a>`);
|
||||
const a = container.querySelector("a");
|
||||
// Either the href is removed entirely or rewritten — it must not start with javascript:
|
||||
const href = a?.getAttribute("href") ?? "";
|
||||
expect(href.toLowerCase().startsWith("javascript:")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,94 +1,104 @@
|
||||
import { parse } from "@create-markdown/core";
|
||||
import { blocksToHTML, renderAsync, shikiPlugin } from "@create-markdown/preview";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import rehypeRaw from "rehype-raw";
|
||||
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { HighlighterGeneric } from "shiki";
|
||||
import { cn } from "../lib/utils";
|
||||
|
||||
interface MarkdownPreviewProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
/** Enable Shiki syntax highlighting for code blocks (async). Default: true */
|
||||
/** Enable Shiki syntax highlighting for fenced code blocks. Default: true. */
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link bare URLs in HTML that aren't already inside anchor tags or attributes.
|
||||
* Matches http/https URLs in text nodes only (not inside tags).
|
||||
*/
|
||||
function autolinkURLs(html: string): string {
|
||||
// Split HTML into tags and text segments, then only linkify text segments
|
||||
return html.replace(
|
||||
/(<[^>]*>)|((https?:\/\/)[^\s<>"')\]]+)/gi,
|
||||
(match, tag: string | undefined, url: string | undefined) => {
|
||||
// If it's an HTML tag, leave it alone
|
||||
if (tag) return tag;
|
||||
// If it's a bare URL in text content, wrap it
|
||||
if (url) {
|
||||
// Trim trailing punctuation that's likely not part of the URL
|
||||
const trailingPunct = /[.,;:!?)]+$/.exec(url);
|
||||
const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url;
|
||||
const suffix = trailingPunct ? trailingPunct[0] : "";
|
||||
return `<a href="${cleanUrl}" target="_blank" rel="noopener noreferrer">${cleanUrl}</a>${suffix}`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
const schema = {
|
||||
...defaultSchema,
|
||||
tagNames: [...(defaultSchema.tagNames ?? []), "picture", "source"],
|
||||
attributes: {
|
||||
...defaultSchema.attributes,
|
||||
"*": [...(defaultSchema.attributes?.["*"] ?? []), "align"],
|
||||
img: [...(defaultSchema.attributes?.img ?? []), "width", "height"],
|
||||
source: ["media", "srcSet", "srcset", "type"],
|
||||
picture: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Order matters: rehype-sanitize runs BEFORE rehype-shiki so sanitize only
|
||||
// sees user-authored HTML; shiki's trusted styled output flows through after.
|
||||
const baseRehype = [rehypeRaw, [rehypeSanitize, schema]] as const;
|
||||
|
||||
const SHIKI_THEME = "github-dark";
|
||||
const SHIKI_LANGS = [
|
||||
"bash",
|
||||
"sh",
|
||||
"shell",
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"json",
|
||||
"yaml",
|
||||
"md",
|
||||
"python",
|
||||
"nix",
|
||||
"http",
|
||||
"html",
|
||||
"css",
|
||||
"toml",
|
||||
"rust",
|
||||
"go",
|
||||
"dockerfile",
|
||||
"diff",
|
||||
];
|
||||
|
||||
type AnyHighlighter = HighlighterGeneric<string, string>;
|
||||
let highlighterPromise: Promise<AnyHighlighter> | null = null;
|
||||
|
||||
function loadHighlighter(): Promise<AnyHighlighter> {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = import("shiki").then(({ createHighlighter }) =>
|
||||
createHighlighter({ themes: [SHIKI_THEME], langs: SHIKI_LANGS }),
|
||||
);
|
||||
}
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rich markdown preview using @create-markdown/preview.
|
||||
* Renders markdown → HTML with optional Shiki syntax highlighting.
|
||||
* Falls back to synchronous (unhighlighted) rendering while Shiki loads.
|
||||
*/
|
||||
export function MarkdownPreview({ children, className, highlight = true }: MarkdownPreviewProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Initial sync render (no highlighting) for instant display
|
||||
const [html, setHtml] = useState(() => {
|
||||
try {
|
||||
const blocks = parse(children);
|
||||
return autolinkURLs(blocksToHTML(blocks));
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
});
|
||||
const [highlighter, setHighlighter] = useState<AnyHighlighter | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!highlight) return;
|
||||
let cancelled = false;
|
||||
|
||||
// Re-parse synchronously on content change
|
||||
try {
|
||||
const blocks = parse(children);
|
||||
const syncHtml = autolinkURLs(blocksToHTML(blocks));
|
||||
setHtml(syncHtml);
|
||||
|
||||
if (!highlight) return;
|
||||
|
||||
// Async render with Shiki syntax highlighting
|
||||
void renderAsync(blocks, {
|
||||
plugins: [shikiPlugin({ theme: "github-dark" })],
|
||||
loadHighlighter()
|
||||
.then((h) => {
|
||||
if (!cancelled) setHighlighter(h);
|
||||
})
|
||||
.then((highlighted) => {
|
||||
if (!cancelled) {
|
||||
setHtml(autolinkURLs(highlighted));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Shiki failed to load — keep the sync render
|
||||
});
|
||||
} catch {
|
||||
// Parse failed — clear
|
||||
setHtml("");
|
||||
}
|
||||
|
||||
.catch(() => {
|
||||
// Shiki failed to initialize — keep plain rendering.
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [children, highlight]);
|
||||
}, [highlight]);
|
||||
|
||||
const rehypePlugins = useMemo(() => {
|
||||
if (highlight && highlighter) {
|
||||
return [
|
||||
...baseRehype,
|
||||
[rehypeShikiFromHighlighter, highlighter, { theme: SHIKI_THEME }] as const,
|
||||
];
|
||||
}
|
||||
return baseRehype;
|
||||
}, [highlight, highlighter]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("markdown", className)}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
<div className={cn("markdown", className)}>
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]} rehypePlugins={rehypePlugins}>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ import Header from "../components/Header";
|
||||
import { getSiteDescription, getSiteMode, getSiteName, getSiteUrlForMode } from "../lib/site";
|
||||
import appCss from "../styles.css?url";
|
||||
|
||||
const OG_IMAGE_VERSION = "20260420-12";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => {
|
||||
const mode = getSiteMode();
|
||||
const siteName = getSiteName(mode);
|
||||
const siteDescription = getSiteDescription(mode);
|
||||
const siteUrl = getSiteUrlForMode(mode);
|
||||
const ogImage = `${siteUrl}/og.png`;
|
||||
const ogImage = `${siteUrl}/og.png?v=${OG_IMAGE_VERSION}`;
|
||||
|
||||
return {
|
||||
meta: [
|
||||
|
||||
Reference in New Issue
Block a user