Checkpoint
This commit is contained in:
@@ -5,20 +5,29 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": ".",
|
"name": ".",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.6.0",
|
||||||
|
"@fuma-translate/react": "^1.0.2",
|
||||||
"@fumadocs/base-ui": "^16.12.1",
|
"@fumadocs/base-ui": "^16.12.1",
|
||||||
|
"@icons-pack/react-simple-icons": "^13.13.0",
|
||||||
"@orama/orama": "^3.1.18",
|
"@orama/orama": "^3.1.18",
|
||||||
|
"@typescript/ata": "^0.9.8",
|
||||||
|
"@typescript/vfs": "^1.6.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"cnfast": "^0.0.8",
|
"cnfast": "^0.0.8",
|
||||||
"fumadocs-core": "16.12.1",
|
"fumadocs-core": "16.12.1",
|
||||||
"fumadocs-mdx": "15.2.0",
|
"fumadocs-mdx": "15.2.0",
|
||||||
|
"fumadocs-twoslash": "^3.3.0",
|
||||||
"fumadocs-ui": "npm:@fumadocs/[email protected]",
|
"fumadocs-ui": "npm:@fumadocs/[email protected]",
|
||||||
"lucide-react": "^1.3.0",
|
"lucide-react": "^1.3.0",
|
||||||
"next": "^16.2.12",
|
"next": "^16.2.12",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"takumi-js": "^2.4.0",
|
"takumi-js": "^2.4.0",
|
||||||
|
"twoslash": "^0.3.9",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.5",
|
"@biomejs/biome": "^2.5.5",
|
||||||
|
"@duckity/js": "^0.1.4",
|
||||||
"@tailwindcss/postcss": "^4.3.3",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/mdx": "^2.0.14",
|
"@types/mdx": "^2.0.14",
|
||||||
"@types/node": "^26.1.1",
|
"@types/node": "^26.1.1",
|
||||||
@@ -59,6 +68,10 @@
|
|||||||
|
|
||||||
"@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g=="],
|
"@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g=="],
|
||||||
|
|
||||||
|
"@duckity/js": ["@duckity/[email protected]", "", { "dependencies": { "@duckity/wasm": "1.1.16" }, "peerDependencies": { "typescript": "^5" } }, "sha512-zjzV2vpKdrFwt954x5thbocTAflrNUVj7yTqMUC8E0ICAbJ9YP30arZvGZqTE98+vemxOIb9hH4SWja7BXH37w=="],
|
||||||
|
|
||||||
|
"@duckity/wasm": ["@duckity/[email protected]", "", {}, "sha512-P0TDKQ187S68D3ri2ZW9m60gCnUj+/iDLgwWwJLAbfiJhxEFfsCb7tJDgaQQR9mJ2PRETuBHc4TjbLphSuAjGg=="],
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
"@emnapi/runtime": ["@emnapi/[email protected]", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||||
@@ -127,6 +140,8 @@
|
|||||||
|
|
||||||
"@fumadocs/tailwind": ["@fumadocs/[email protected]", "", { "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-BnPe52UxSaG8yKlHMKBxXw8h6GpK5qO55ci6+Qd5JnquTvIw6SpfbC1P+qAi82PuPWv1KZAWY8bxRk4+x9ctXw=="],
|
"@fumadocs/tailwind": ["@fumadocs/[email protected]", "", { "peerDependencies": { "tailwindcss": "^4.0.0" }, "optionalPeers": ["tailwindcss"] }, "sha512-BnPe52UxSaG8yKlHMKBxXw8h6GpK5qO55ci6+Qd5JnquTvIw6SpfbC1P+qAi82PuPWv1KZAWY8bxRk4+x9ctXw=="],
|
||||||
|
|
||||||
|
"@icons-pack/react-simple-icons": ["@icons-pack/[email protected]", "", { "peerDependencies": { "react": "^16.13 || ^17 || ^18 || ^19" } }, "sha512-B5HhQMIpcSH4z8IZ8HFhD59CboHceKYMpPC9kAwGyKntvPdyJJv26DLu4Z1wAjcCLyrJhf11tMhiQGom9Rxb9g=="],
|
||||||
|
|
||||||
"@img/colour": ["@img/[email protected]", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
"@img/colour": ["@img/[email protected]", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
"@img/sharp-darwin-arm64": ["@img/[email protected]", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||||
@@ -221,6 +236,8 @@
|
|||||||
|
|
||||||
"@shikijs/themes": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
|
"@shikijs/themes": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/types": "4.3.1" } }, "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA=="],
|
||||||
|
|
||||||
|
"@shikijs/twoslash": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/types": "4.3.1", "twoslash": "^0.3.9" }, "peerDependencies": { "typescript": ">=5.5.0" } }, "sha512-xK8inH/gK++1V4rTxrwCwjvaNwkkJ7oDjOIpdqONVxIpAFnVC3gzqjH5KiXGTelUcxpUJ3PtOKWct1YQ0kAloA=="],
|
||||||
|
|
||||||
"@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="],
|
"@shikijs/types": ["@shikijs/[email protected]", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g=="],
|
||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/[email protected]", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/[email protected]", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
@@ -303,6 +320,10 @@
|
|||||||
|
|
||||||
"@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
"@types/unist": ["@types/[email protected]", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
|
"@typescript/ata": ["@typescript/[email protected]", "", { "peerDependencies": { "typescript": ">=4.4.4" } }, "sha512-+M815CeDRJS5H5ciWfhFCKp25nNfF+LFWawWAaBhNlquFb2wS5IIMDI+2bKWN3GuU6mpj+FzySsOD29M4nG8Xg=="],
|
||||||
|
|
||||||
|
"@typescript/vfs": ["@typescript/[email protected]", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
|
||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
|
"@ungap/structured-clone": ["@ungap/[email protected]", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="],
|
||||||
|
|
||||||
"@yuku-analyzer/binding-darwin-arm64": ["@yuku-analyzer/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rpIP7IeybjyvWUf6WnU24h1qo+JdxIHr1o3yb06HoE8tM3S/Jh5RrUw9aw5P9BKSIvSPbLyVlItX7PcD3o5bQ=="],
|
"@yuku-analyzer/binding-darwin-arm64": ["@yuku-analyzer/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9rpIP7IeybjyvWUf6WnU24h1qo+JdxIHr1o3yb06HoE8tM3S/Jh5RrUw9aw5P9BKSIvSPbLyVlItX7PcD3o5bQ=="],
|
||||||
@@ -415,7 +436,7 @@
|
|||||||
|
|
||||||
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["[email protected]", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"decode-named-character-reference": ["[email protected]", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
"decode-named-character-reference": ["[email protected]", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
|
||||||
|
|
||||||
@@ -477,6 +498,8 @@
|
|||||||
|
|
||||||
"fumadocs-mdx": ["[email protected]", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.1", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "magic-string": "^0.30.21", "mdast-util-mdx": "^3.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.5", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "yaml": "^2.9.0", "yuku-analyzer": "^0.6.3", "zod": "^4.4.3" }, "peerDependencies": { "@fumadocs/satteri": "0.x.x", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "rolldown": "*", "satteri": "^0.9.4", "vite": "7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/satteri", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "rolldown", "satteri", "vite"], "bin": { "fumadocs-mdx": "./bin.js" } }, "sha512-+yBP8QYw5wA9LF5eVdMhwbP7KT1OF4B/YfC6PZoD2jz0amZi1B+6QHTI6XoRRSTmhWrI4cL5LU1DspW0itk+NA=="],
|
"fumadocs-mdx": ["[email protected]", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.1", "estree-util-value-to-estree": "^3.5.0", "github-slugger": "^2.0.0", "magic-string": "^0.30.21", "mdast-util-mdx": "^3.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.5", "tinyexec": "^1.2.4", "tinyglobby": "^0.2.17", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "yaml": "^2.9.0", "yuku-analyzer": "^0.6.3", "zod": "^4.4.3" }, "peerDependencies": { "@fumadocs/satteri": "0.x.x", "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^16.7.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "rolldown": "*", "satteri": "^0.9.4", "vite": "7.x.x || 8.x.x" }, "optionalPeers": ["@fumadocs/satteri", "@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "rolldown", "satteri", "vite"], "bin": { "fumadocs-mdx": "./bin.js" } }, "sha512-+yBP8QYw5wA9LF5eVdMhwbP7KT1OF4B/YfC6PZoD2jz0amZi1B+6QHTI6XoRRSTmhWrI4cL5LU1DspW0itk+NA=="],
|
||||||
|
|
||||||
|
"fumadocs-twoslash": ["[email protected]", "", { "dependencies": { "@base-ui/react": "^1.6.0", "@shikijs/twoslash": "^4.3.1", "cnfast": "^0.0.8", "mdast-util-from-markdown": "^2.0.3", "mdast-util-gfm": "^3.1.0", "mdast-util-to-hast": "^13.2.1", "twoslash": "^0.3.9" }, "peerDependencies": { "@types/react": "*", "fumadocs-core": "^16.7.16", "fumadocs-ui": "^16.7.16", "react": "^19.2.0", "react-dom": "^19.2.0", "shiki": "4.x.x" }, "optionalPeers": ["@types/react"] }, "sha512-IR+oYbjsR59h/R7AvdeEdHEY48ygCHII3tUZGC3QnUVug5+qN1wlRNr30gAfOvrau/mdiRKwONuJCOKOWqEFbg=="],
|
||||||
|
|
||||||
"fumadocs-ui": ["@fumadocs/[email protected]", "", { "dependencies": { "@base-ui/react": "^1.6.0", "@fuma-translate/react": "^1.0.2", "@fumadocs/tailwind": "0.1.1", "class-variance-authority": "^0.7.1", "cnfast": "^0.0.8", "lucide-react": "^1.25.0", "motion": "^12.42.2", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.3.1", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.12.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "takumi-js": "*" }, "optionalPeers": ["@types/mdx", "@types/react", "next", "takumi-js"] }, "sha512-FOHMJgFlyZ5VyujVqCit+6Bn+DQgPf8I9x9vGMJzc7pwsCR+LCeqE3+jNEzKpTTbJhof6MXjX4n3pEaJx3tvBQ=="],
|
"fumadocs-ui": ["@fumadocs/[email protected]", "", { "dependencies": { "@base-ui/react": "^1.6.0", "@fuma-translate/react": "^1.0.2", "@fumadocs/tailwind": "0.1.1", "class-variance-authority": "^0.7.1", "cnfast": "^0.0.8", "lucide-react": "^1.25.0", "motion": "^12.42.2", "next-themes": "^0.4.6", "react-remove-scroll": "^2.7.2", "rehype-raw": "^7.0.0", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^4.3.1", "unist-util-visit": "^5.1.0" }, "peerDependencies": { "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "16.12.1", "next": "16.x.x", "react": "^19.2.0", "react-dom": "^19.2.0", "takumi-js": "*" }, "optionalPeers": ["@types/mdx", "@types/react", "next", "takumi-js"] }, "sha512-FOHMJgFlyZ5VyujVqCit+6Bn+DQgPf8I9x9vGMJzc7pwsCR+LCeqE3+jNEzKpTTbJhof6MXjX4n3pEaJx3tvBQ=="],
|
||||||
|
|
||||||
"get-nonce": ["[email protected]", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
"get-nonce": ["[email protected]", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
@@ -695,7 +718,7 @@
|
|||||||
|
|
||||||
"motion-utils": ["[email protected]", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
"motion-utils": ["[email protected]", "", {}, "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ=="],
|
||||||
|
|
||||||
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
"nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||||
|
|
||||||
@@ -853,6 +876,10 @@
|
|||||||
|
|
||||||
"tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["[email protected]", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"twoslash": ["[email protected]", "", { "dependencies": { "@typescript/vfs": "^1.6.4", "twoslash-protocol": "0.3.9" }, "peerDependencies": { "typescript": "^5.5.0 || ^6.0.0" } }, "sha512-rDclk+OtzuTX+tnea7DYLCkqGQ3eP0IyfD+kzUJ7t46X/NzlaxwrhecmEBNuSCuEn3V+n1PhcjUUQQ7gUJzX5Q=="],
|
||||||
|
|
||||||
|
"twoslash-protocol": ["[email protected]", "", {}, "sha512-9/iwp+CXOnjFMPQuPL5PkuRbZnDoNpBvtJCLs9t8kDYkL3YHujbvnHfZA1i5fApDftVEdBw+T/4F+dH5kIzpYQ=="],
|
||||||
|
|
||||||
"type-fest": ["[email protected]", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
"type-fest": ["[email protected]", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="],
|
||||||
|
|
||||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
@@ -927,9 +954,9 @@
|
|||||||
|
|
||||||
"chalk-template/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk-template/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"fumadocs-ui/lucide-react": ["[email protected]6.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg=="],
|
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
"micromark/debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"fumadocs-ui/lucide-react": ["[email protected]", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg=="],
|
||||||
|
|
||||||
"mime-types/mime-db": ["[email protected]", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="],
|
"mime-types/mime-db": ["[email protected]", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="],
|
||||||
|
|
||||||
@@ -947,7 +974,7 @@
|
|||||||
|
|
||||||
"chalk-template/chalk/ansi-styles": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"chalk-template/chalk/ansi-styles": ["[email protected]", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"micromark/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"ansi-align/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-align/string-width/strip-ansi/ansi-regex": ["[email protected]", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
title: Tune a Protection Profile
|
||||||
|
---
|
||||||
|
|
||||||
|
### Throttling Windows
|
||||||
|
|
||||||
|
The first of all settings a protection profile has are the throttling windows. Throttling windows
|
||||||
|
rate-limit challenge issuance.
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: Write Your Own SDK
|
||||||
|
description: Learn to write your own Duckity SDK, for when an official one is not yet available.
|
||||||
|
---
|
||||||
+1
-2
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
title: Introduction
|
title: Overview
|
||||||
description: Welcome to Duckity's documentation site!
|
description: Welcome to Duckity's documentation site!
|
||||||
icon: Fish
|
icon: Fish
|
||||||
---
|
---
|
||||||
@@ -16,4 +16,3 @@ DDoS protection service. Use Cloudflare, Bunny.net, or similar for protection of
|
|||||||
|
|
||||||
If you're new to Duckity, head over to the [Quick Start](/quick-start). It'll guide you on how to get started and
|
If you're new to Duckity, head over to the [Quick Start](/quick-start). It'll guide you on how to get started and
|
||||||
give you some templates to reduce the boilerplate configuration tasks.
|
give you some templates to reduce the boilerplate configuration tasks.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"---Introduction---",
|
||||||
|
"index",
|
||||||
|
"quick-start",
|
||||||
|
"---[BookOpenText]Guides---",
|
||||||
|
"guides/tune-a-protection-profile",
|
||||||
|
"guides/write-your-own-sdk",
|
||||||
|
"---[CodeXml]Integrate---",
|
||||||
|
"sdks/javascript",
|
||||||
|
"sdks/react",
|
||||||
|
"sdks/c",
|
||||||
|
"sdks/python",
|
||||||
|
"sdks/rust-std",
|
||||||
|
"sdks/rust-no-std"
|
||||||
|
]
|
||||||
|
}
|
||||||
+65
-29
@@ -8,34 +8,24 @@ Getting started with Duckity is quite simple. Head over to
|
|||||||
[Duckity's Dashboard](https://app.duckity.com/) and log in using your favorite method. If it's your
|
[Duckity's Dashboard](https://app.duckity.com/) and log in using your favorite method. If it's your
|
||||||
first time logging in, your account will be automatically created.
|
first time logging in, your account will be automatically created.
|
||||||
|
|
||||||
If it's your first time logging into Duckity, your dashboard will be empty.
|
## Create an Application [step]
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Applications
|
|
||||||
|
|
||||||
Duckity is divided into one or more applications within your account. You create one application
|
Duckity is divided into one or more applications within your account. You create one application
|
||||||
per project in which you want to integrate Duckity. To create a new application, press on the
|
per project in which you want to integrate Duckity. To create a new application, press on the
|
||||||
yellow **+ New Application** button at the top right corner of the screen.
|
yellow **+ New Application** button at the top right corner of the screen.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Once you've set the name of it, press on the **✓ Create Application** button. Congratulations! You
|
Once you've set the name of it, press on the **✓ Create Application** button. Congratulations! You
|
||||||
now have your first application created.
|
now have your first application created.
|
||||||
|
|
||||||

|
<Callout>
|
||||||
|
Additionally, in case your application is a web app, you can set up the CORS
|
||||||
|
origins to fit your application's origins. For example, if you'll be
|
||||||
|
protecting the login form of your app hosted at `https://example.com/login`,
|
||||||
|
add `https://example.com` (scheme and host, no path) to your app's origins. If
|
||||||
|
your application is not a web app, you can leave the CORS origins empty.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
Each application has a credentials pair consisting of:
|
## Create a Protection Profile [step]
|
||||||
|
|
||||||
- **Application ID**: The unique identifier of your app.
|
|
||||||
- **Application Secret**: Your app's "password". As the name implies, keep it secret.
|
|
||||||
|
|
||||||
Additionally, in case your application is a web app, you can set up the CORS origins to fit your
|
|
||||||
application's origins. For example, if you'll be protecting the login form of your app hosted at
|
|
||||||
`https://example.com/login`, add `https://example.com` (scheme and host, no path) to your app's
|
|
||||||
origins. If your application is not a web app, you can leave the CORS origins empty.
|
|
||||||
|
|
||||||
## Protection Profiles
|
|
||||||
|
|
||||||
Applications commonly have multiple features that need to be protected. For example, you may have a
|
Applications commonly have multiple features that need to be protected. For example, you may have a
|
||||||
signup form and a login form with different protection needs. For each, you'll create one
|
signup form and a login form with different protection needs. For each, you'll create one
|
||||||
@@ -44,20 +34,66 @@ signup form and a login form with different protection needs. For each, you'll c
|
|||||||
Each application needs at least one protection profile to work. To create your first one, press the
|
Each application needs at least one protection profile to work. To create your first one, press the
|
||||||
**+ Create Protection Profile** button.
|
**+ Create Protection Profile** button.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Set the name to the name of the feature you'll be protecting, like "Sign-up Form" or "Add Friend".
|
Set the name to the name of the feature you'll be protecting, like "Sign-up Form" or "Add Friend".
|
||||||
This is for you to recognize it later, it won't be displayed to your users. Once you're done
|
This is for you to recognize it later, it won't be displayed to your users. Once you're done
|
||||||
filling it, press on **✓ Create Protection Profile**.
|
filling it, press on **✓ Create Protection Profile**.
|
||||||
|
|
||||||
Congrats! Your first protection profile is now created. You'll see a lot of configuration options,
|
Copy your protection profile's ID using the button at the top right corner, that's all you'll need
|
||||||
but don't worry; We'll go one by one explaining them below.
|
to integrate it into your application.
|
||||||
|
|
||||||

|
<Callout>
|
||||||
|
Don't worry about all the settings displayed for now, you'll learn to tune
|
||||||
|
them later.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
### Throttling Windows
|
## Install an SDK [step]
|
||||||
|
|
||||||
The first of all settings a protection profile has are the throttling windows. Throttling windows
|
To integrate your protection profile with your application, you'll need a client-side SDK and a
|
||||||
rate-limit challenge issuance.
|
server-side SDK.
|
||||||
|
|
||||||

|
Duckity has multiple official SDKs for you to plug and play.
|
||||||
|
|
||||||
|
import {
|
||||||
|
SiC,
|
||||||
|
SiJavascript,
|
||||||
|
SiPython,
|
||||||
|
SiRust,
|
||||||
|
SiReact,
|
||||||
|
} from "@icons-pack/react-simple-icons";
|
||||||
|
|
||||||
|
<Cards>
|
||||||
|
<Card href="/sdks/javascript" title="JavaScript" icon={<SiJavascript />}>
|
||||||
|
Integrate client-side code with Duckity using the JavaScript SDK.
|
||||||
|
</Card>
|
||||||
|
<Card href="/sdks/react" title="React" icon={<SiReact />}>
|
||||||
|
Integrate client-side code with Duckity using the React SDK.
|
||||||
|
</Card>
|
||||||
|
<Card href="/sdks/c" title="C" icon={<SiC />}>
|
||||||
|
Integrate client-side code with Duckity using the C SDK.
|
||||||
|
</Card>
|
||||||
|
<Card href="/sdks/python" title="Python 3" icon={<SiPython />}>
|
||||||
|
Integrate client-side code with Duckity using the Python SDK.
|
||||||
|
</Card>
|
||||||
|
<Card href="/sdks/rust-std" title="Rust (STD)" icon={<SiRust />}>
|
||||||
|
Integrate client-side code with Duckity using the Rust SDK. Uses GMP via
|
||||||
|
`rug`.
|
||||||
|
</Card>
|
||||||
|
<Card href="/sdks/rust-no-std" title="Rust (No STD)" icon={<SiRust />}>
|
||||||
|
Integrate client-side code with Duckity using the Rust SDK. Uses
|
||||||
|
`num-bigint-dig`.
|
||||||
|
</Card>
|
||||||
|
</Cards>
|
||||||
|
|
||||||
|
In case we don't have a client SDK for your language yet, you can use the C SDK. It's written in
|
||||||
|
Rust, you can link to it through the exposed C API, and it wraps all the required encoding and
|
||||||
|
decoding in a simple API. You may also find the following guide useful:
|
||||||
|
|
||||||
|
import { FileCodeCorner } from "lucide-react";
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="Write Your Own SDK"
|
||||||
|
icon={<FileCodeCorner />}
|
||||||
|
href="/guides/write-your-own-sdk"
|
||||||
|
>
|
||||||
|
Learn how to write your own SDK in any language.
|
||||||
|
</Card>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: C
|
||||||
|
icon: SiC
|
||||||
|
---
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
title: JavaScript (Client-Side)
|
||||||
|
description: Learn how to integrate Duckity into your web application.
|
||||||
|
icon: SiJavascript
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to the Duckity JavaScript + WASM SDK documentation! This guide will teach you how to
|
||||||
|
install and set up the SDK in no time.
|
||||||
|
|
||||||
|
The following SDKs depend on this one and provide specialized wrappers for different frameworks:
|
||||||
|
|
||||||
|
import { SiReact } from "@icons-pack/react-simple-icons";
|
||||||
|
|
||||||
|
<Cards>
|
||||||
|
<Card href="/sdks/react" title="React" icon={<SiReact />}>
|
||||||
|
Integrate client-side code with Duckity using the React SDK.
|
||||||
|
</Card>
|
||||||
|
</Cards>
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Before you can integrate Duckity into your application, you'll need to have the following:
|
||||||
|
|
||||||
|
1. An application,
|
||||||
|
2. CORS origins set up pointing to your application's origin(s),
|
||||||
|
3. At least one protection profile created in that application, and
|
||||||
|
4. The ID of the protection profiles to use
|
||||||
|
|
||||||
|
If you're missing either of those, head over to the [Duckity Dashboard](https://app.duckity.com) or
|
||||||
|
read the [Quick Start](/quick-start) guide to learn how to set those up.
|
||||||
|
|
||||||
|
Once you got those ready, follow these steps to get things running on your client:
|
||||||
|
|
||||||
|
### Install the SDK [step]
|
||||||
|
|
||||||
|
Follow these steps depending on your application:
|
||||||
|
|
||||||
|
<Tabs items={["Installed", "Via CDN"]}>
|
||||||
|
<Tab>
|
||||||
|
Run the following line in your terminal to install the Duckity SDK.
|
||||||
|
|
||||||
|
```package-install
|
||||||
|
@duckity/js
|
||||||
|
```
|
||||||
|
|
||||||
|
Then import it in your code:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
import duckity from "@duckity/js";
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab>
|
||||||
|
If you're using the SDK from a static site, import it using a CDN like [esm.sh](https://esm.sh/)
|
||||||
|
instead.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
// Using esm.sh
|
||||||
|
import duckity from "https://esm.sh/@duckity/js";
|
||||||
|
|
||||||
|
// Using jsdelivr.net
|
||||||
|
import duckity from "https://cdn.jsdelivr.net/npm/@duckity/js";
|
||||||
|
|
||||||
|
// Using UNPKG
|
||||||
|
import duckity from "https://unpkg.com/@duckity/js";
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Solve a Challenge [step]
|
||||||
|
|
||||||
|
Once you have the SDK installed, you can request a challenge whenever you need it using
|
||||||
|
`duckity.solve()`.
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
const PROTECTION_PROFILE_ID: string = "";
|
||||||
|
// ---cut---
|
||||||
|
import duckity from "@duckity/js";
|
||||||
|
|
||||||
|
let solution = await duckity.solve(PROTECTION_PROFILE_ID);
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout title="TIP">
|
||||||
|
Hover over the code to see the type definitions.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
Solving a challenge on request works well for simple setups. However, both security and UX can be
|
||||||
|
greatly improved changing a few settings and planning when to solve challenges.
|
||||||
|
|
||||||
|
If your challenges do not require threat correlation keys (set up in the protection profile's
|
||||||
|
settings), issue the challenge as soon as possible. Note, however, that the challenge
|
||||||
|
|
||||||
|
### Threat Correlation Keys
|
||||||
|
|
||||||
|
To pass threat correlation keys when issuing a challenge, set them in the `options` argument of
|
||||||
|
`duckity.solve()`.
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
const PROTECTION_PROFILE_ID: string = "py83YHkXV6ZpIsJZGVxzS";
|
||||||
|
// ---cut---
|
||||||
|
import duckity from "@duckity/js";
|
||||||
|
|
||||||
|
let solution = await duckity.solve(
|
||||||
|
PROTECTION_PROFILE_ID,
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
email: "[email protected]",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using On Self-Hosted Ducklings
|
||||||
|
|
||||||
|
Self-hosted ducklings are hosted at a different domain from Duckity-hosted ducklings. To point it
|
||||||
|
to a custom domain, change the following setting:
|
||||||
|
|
||||||
|
```ts twoslash
|
||||||
|
const PROTECTION_PROFILE_ID: string = "py83YHkXV6ZpIsJZGVxzS";
|
||||||
|
// ---cut---
|
||||||
|
import duckity from "@duckity/js";
|
||||||
|
|
||||||
|
let solution = await duckity.solve(
|
||||||
|
PROTECTION_PROFILE_ID,
|
||||||
|
{
|
||||||
|
api: "https://quack.duckity.com",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
The default value for the `api` parameter is `"https://quack.duckity.com"`, which points to
|
||||||
|
Duckity's hosted duckling. Change the domain name to your
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: Python 3
|
||||||
|
icon: SiPython
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: React
|
||||||
|
icon: SiReact
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: Rust (No STD)
|
||||||
|
icon: SiRust
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: Rust (STD)
|
||||||
|
icon: SiRust
|
||||||
|
---
|
||||||
+2
-2
@@ -4,9 +4,9 @@ const withMDX = createMDX();
|
|||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
serverExternalPackages: ["@takumi-rs/core"],
|
serverExternalPackages: ["@takumi-rs/core", "typescript", "twoslash"],
|
||||||
output: "export",
|
output: "export",
|
||||||
reactStrictMode: false,
|
reactStrictMode: true,
|
||||||
allowedDevOrigins: ["duckity.com"],
|
allowedDevOrigins: ["duckity.com"],
|
||||||
basePath: "/docs",
|
basePath: "/docs",
|
||||||
trailingSlash: true,
|
trailingSlash: true,
|
||||||
|
|||||||
+9
-1
@@ -12,17 +12,25 @@
|
|||||||
"format": "biome format --write"
|
"format": "biome format --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.6.0",
|
||||||
|
"@fuma-translate/react": "^1.0.2",
|
||||||
"@fumadocs/base-ui": "^16.12.1",
|
"@fumadocs/base-ui": "^16.12.1",
|
||||||
|
"@icons-pack/react-simple-icons": "^13.13.0",
|
||||||
"@orama/orama": "^3.1.18",
|
"@orama/orama": "^3.1.18",
|
||||||
|
"@typescript/ata": "^0.9.8",
|
||||||
|
"@typescript/vfs": "^1.6.4",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"cnfast": "^0.0.8",
|
"cnfast": "^0.0.8",
|
||||||
"fumadocs-core": "16.12.1",
|
"fumadocs-core": "16.12.1",
|
||||||
"fumadocs-mdx": "15.2.0",
|
"fumadocs-mdx": "15.2.0",
|
||||||
|
"fumadocs-twoslash": "^3.3.0",
|
||||||
"fumadocs-ui": "npm:@fumadocs/[email protected]",
|
"fumadocs-ui": "npm:@fumadocs/[email protected]",
|
||||||
"lucide-react": "^1.3.0",
|
"lucide-react": "^1.3.0",
|
||||||
"next": "^16.2.12",
|
"next": "^16.2.12",
|
||||||
"react": "^19.2.8",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.8",
|
"react-dom": "^19.2.8",
|
||||||
"takumi-js": "^2.4.0"
|
"takumi-js": "^2.4.0",
|
||||||
|
"twoslash": "^0.3.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.5",
|
"@biomejs/biome": "^2.5.5",
|
||||||
|
|||||||
+61
-4
@@ -1,10 +1,16 @@
|
|||||||
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
|
import ts from "typescript";
|
||||||
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
|
import { setupTypeAcquisition } from "@typescript/ata";
|
||||||
|
import { defineConfig, defineDocs } from "fumadocs-mdx/config";
|
||||||
|
import { metaSchema, pageSchema } from "fumadocs-core/source/schema";
|
||||||
|
import { remarkSteps } from "fumadocs-core/mdx-plugins/remark-steps";
|
||||||
|
import { transformerTwoslash } from "fumadocs-twoslash";
|
||||||
|
import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins";
|
||||||
|
import { createFileSystemTypesCache } from "fumadocs-twoslash/cache-fs";
|
||||||
|
|
||||||
// You can customize Zod schemas for frontmatter and `meta.json` here
|
// You can customize Zod schemas for frontmatter and `meta.json` here
|
||||||
// see https://fumadocs.dev/docs/mdx/collections
|
// see https://fumadocs.dev/docs/mdx/collections
|
||||||
export const docs = defineDocs({
|
export const docs = defineDocs({
|
||||||
dir: 'content',
|
dir: "content",
|
||||||
docs: {
|
docs: {
|
||||||
schema: pageSchema,
|
schema: pageSchema,
|
||||||
postprocess: {
|
postprocess: {
|
||||||
@@ -16,8 +22,59 @@ export const docs = defineDocs({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Allow importing @duckity/js from twoslash via `import { Duckity } from "@duckity/js"`.
|
||||||
|
//
|
||||||
|
// For this, we need to create a virtual file system that includes the @duckity/js package from node_modules.
|
||||||
|
const sharedFsMap = new Map<string, string>();
|
||||||
|
|
||||||
|
const imports = `
|
||||||
|
import duckity from "@duckity/js";
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ata = setupTypeAcquisition({
|
||||||
|
projectName: "twoslash-cdn",
|
||||||
|
typescript: ts,
|
||||||
|
logger: console,
|
||||||
|
delegate: {
|
||||||
|
receivedFile: (code, path) => {
|
||||||
|
// ATA paths look like "/node_modules/@types/lodash/index.d.ts"
|
||||||
|
// Strip the leading slash to make it a valid path for the virtual file system
|
||||||
|
|
||||||
|
sharedFsMap.set(path, code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ata(imports);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
mdxOptions: {
|
mdxOptions: {
|
||||||
// MDX options
|
remarkPlugins: [remarkSteps],
|
||||||
|
rehypeCodeOptions: {
|
||||||
|
themes: {
|
||||||
|
light: "github-light",
|
||||||
|
dark: "github-dark",
|
||||||
|
},
|
||||||
|
transformers: [
|
||||||
|
...(rehypeCodeDefaultOptions.transformers ?? []),
|
||||||
|
transformerTwoslash({
|
||||||
|
typesCache: createFileSystemTypesCache({}),
|
||||||
|
twoslashOptions: {
|
||||||
|
fsMap: sharedFsMap,
|
||||||
|
compilerOptions: {
|
||||||
|
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
||||||
|
target: ts.ScriptTarget.ESNext,
|
||||||
|
module: ts.ModuleKind.ESNext,
|
||||||
|
esModuleInterop: true,
|
||||||
|
allowJs: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
// important: Shiki doesn't support lazy loading languages for codeblocks in Twoslash popups
|
||||||
|
// make sure to define them first (e.g. the common ones)
|
||||||
|
langs: ["js", "jsx", "ts", "tsx"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function generateMetadata(
|
|||||||
if (!page) notFound();
|
if (!page) notFound();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: page.data.title,
|
title: page.data.title + " · Duckity Docs",
|
||||||
description: page.data.description,
|
description: page.data.description,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
images: getPageImageUrl(page).url,
|
images: getPageImageUrl(page).url,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'fumadocs-ui/css/neutral.css';
|
@import 'fumadocs-ui/css/neutral.css';
|
||||||
@import 'fumadocs-ui/css/preset.css';
|
@import 'fumadocs-ui/css/preset.css';
|
||||||
|
@import 'fumadocs-twoslash/twoslash.css';
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
'use client';
|
||||||
|
import { Check, Clipboard } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
type ComponentProps,
|
||||||
|
createContext,
|
||||||
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
|
type RefObject,
|
||||||
|
use,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import { cn } from '../lib/cn';
|
||||||
|
import { useCopyButton } from '@fumadocs/base-ui/utils/use-copy-button';
|
||||||
|
import { buttonVariants } from './ui/button';
|
||||||
|
import { useTranslations } from '@fuma-translate/react';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||||
|
import { mergeRefs } from '../lib/merge-refs';
|
||||||
|
|
||||||
|
export interface CodeBlockProps extends Omit<ComponentProps<'figure'>, 'title'> {
|
||||||
|
title?: ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon of code block
|
||||||
|
*
|
||||||
|
* When passed as a string, it assumes the value is the HTML of icon
|
||||||
|
*/
|
||||||
|
icon?: ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow to copy code with copy button
|
||||||
|
*
|
||||||
|
* @defaultValue true
|
||||||
|
*/
|
||||||
|
allowCopy?: boolean | 'true' | 'false';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep original background color generated by Shiki or Rehype Code
|
||||||
|
*
|
||||||
|
* @defaultValue false
|
||||||
|
*/
|
||||||
|
keepBackground?: boolean;
|
||||||
|
|
||||||
|
viewportProps?: HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* show line numbers
|
||||||
|
*/
|
||||||
|
'data-line-numbers'?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defaultValue 1
|
||||||
|
*/
|
||||||
|
'data-line-numbers-start'?: number;
|
||||||
|
|
||||||
|
Actions?: (props: { className?: string; children?: ReactNode }) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContext = createContext<{
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
nested: boolean;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
export function Pre(props: ComponentProps<'pre'>) {
|
||||||
|
return (
|
||||||
|
<pre {...props} className={cn('min-w-full w-max *:flex *:flex-col', props.className)}>
|
||||||
|
{props.children}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlock({
|
||||||
|
ref,
|
||||||
|
title,
|
||||||
|
allowCopy = true,
|
||||||
|
keepBackground = false,
|
||||||
|
icon,
|
||||||
|
viewportProps = {},
|
||||||
|
children,
|
||||||
|
Actions = (props) => <div {...props} className={cn('empty:hidden', props.className)} />,
|
||||||
|
...props
|
||||||
|
}: CodeBlockProps) {
|
||||||
|
const inTab = use(TabsContext) !== null;
|
||||||
|
const areaRef = useRef<HTMLDivElement>(null);
|
||||||
|
if (allowCopy === 'true') allowCopy = true;
|
||||||
|
else if (allowCopy === 'false') allowCopy = false;
|
||||||
|
return (
|
||||||
|
<figure
|
||||||
|
ref={ref}
|
||||||
|
dir="ltr"
|
||||||
|
{...props}
|
||||||
|
tabIndex={-1}
|
||||||
|
className={cn(
|
||||||
|
inTab ? 'bg-fd-secondary -mx-px -mb-px last:rounded-b-xl' : 'my-4 bg-fd-card rounded-xl',
|
||||||
|
keepBackground && 'bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)',
|
||||||
|
|
||||||
|
'shiki relative border shadow-sm not-prose overflow-hidden text-sm',
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title ? (
|
||||||
|
<div className="flex text-fd-muted-foreground items-center gap-2 h-9.5 border-b px-4">
|
||||||
|
{typeof icon === 'string' ? (
|
||||||
|
<div
|
||||||
|
className="[&_svg]:size-3.5"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: icon,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
icon
|
||||||
|
)}
|
||||||
|
<figcaption className="flex-1 truncate">{title}</figcaption>
|
||||||
|
{Actions({
|
||||||
|
className: '-me-2',
|
||||||
|
children: allowCopy && <CopyButton containerRef={areaRef} />,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
Actions({
|
||||||
|
className:
|
||||||
|
'absolute top-2 right-2 z-2 backdrop-blur-lg rounded-lg text-fd-muted-foreground',
|
||||||
|
children: allowCopy && <CopyButton containerRef={areaRef} />,
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
ref={areaRef}
|
||||||
|
{...viewportProps}
|
||||||
|
role="region"
|
||||||
|
tabIndex={0}
|
||||||
|
className={cn(
|
||||||
|
'text-[0.8125rem] py-3.5 overflow-auto max-h-[600px] fd-scroll-container focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-fd-ring',
|
||||||
|
viewportProps.className,
|
||||||
|
)}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
// space for toolbar
|
||||||
|
'--padding-right': !title ? 'calc(var(--spacing) * 8)' : undefined,
|
||||||
|
counterSet: props['data-line-numbers']
|
||||||
|
? `line ${Number(props['data-line-numbers-start'] ?? 1) - 1}`
|
||||||
|
: undefined,
|
||||||
|
...viewportProps.style,
|
||||||
|
} as object
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CopyButton({
|
||||||
|
className,
|
||||||
|
containerRef,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<'button'> & {
|
||||||
|
containerRef: RefObject<HTMLElement | null>;
|
||||||
|
}) {
|
||||||
|
const t = useTranslations({ note: 'code block' });
|
||||||
|
const [checked, onClick] = useCopyButton(() => {
|
||||||
|
const pre = containerRef.current?.getElementsByTagName('pre').item(0);
|
||||||
|
if (!pre) return;
|
||||||
|
|
||||||
|
const clone = pre.cloneNode(true) as HTMLElement;
|
||||||
|
clone.querySelectorAll('.nd-copy-ignore').forEach((node) => {
|
||||||
|
node.replaceWith('\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
void navigator.clipboard.writeText(clone.textContent ?? '');
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-checked={checked || undefined}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
className: 'hover:text-fd-accent-foreground data-checked:text-fd-accent-foreground',
|
||||||
|
size: 'icon-xs',
|
||||||
|
}),
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
aria-label={
|
||||||
|
checked ? t('Copied Text', { note: 'aria-label' }) : t('Copy Text', { note: 'aria-label' })
|
||||||
|
}
|
||||||
|
onClick={onClick}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{checked ? <Check /> : <Clipboard />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlockTabs({ ref, className, ...props }: ComponentProps<typeof Tabs>) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const nested = use(TabsContext) !== null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
ref={mergeRefs(containerRef, ref)}
|
||||||
|
{...props}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'bg-fd-card rounded-xl border',
|
||||||
|
!nested && 'my-4',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TabsContext
|
||||||
|
value={useMemo(
|
||||||
|
() => ({
|
||||||
|
containerRef,
|
||||||
|
nested,
|
||||||
|
}),
|
||||||
|
[nested],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TabsContext>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlockTabsList({ className, ...props }: ComponentProps<typeof TabsList>) {
|
||||||
|
return (
|
||||||
|
<TabsList
|
||||||
|
{...props}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'flex flex-row px-2 overflow-x-auto text-fd-muted-foreground',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</TabsList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlockTabsTrigger({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof TabsTrigger>) {
|
||||||
|
return (
|
||||||
|
<TabsTrigger
|
||||||
|
{...props}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'relative group inline-flex text-sm font-medium text-nowrap items-center transition-colors gap-2 px-2 py-1.5 [&_svg]:size-3.5',
|
||||||
|
s.active ? 'text-fd-primary' : 'hover:text-fd-accent-foreground',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-x-2 bottom-0 h-px group-data-active:bg-fd-primary" />
|
||||||
|
{children}
|
||||||
|
</TabsTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CodeBlockTab(props: ComponentProps<typeof TabsContent>) {
|
||||||
|
return <TabsContent {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { cn } from '../lib/cn';
|
||||||
|
import { GitFork, Star } from 'lucide-react';
|
||||||
|
import { type ComponentProps, use } from 'react';
|
||||||
|
|
||||||
|
export interface FetchRepositoryInfoOptions {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
|
||||||
|
baseUrl?: string;
|
||||||
|
token?: string;
|
||||||
|
fetchOptions?: RequestInit;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepositoryInfo {
|
||||||
|
stars: number;
|
||||||
|
forks: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GithubInfoProps extends ComponentProps<'a'>, FetchRepositoryInfoOptions {
|
||||||
|
locale?: Intl.LocalesArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchRepositoryInfo({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
token,
|
||||||
|
baseUrl = 'https://api.github.com',
|
||||||
|
fetchOptions = {
|
||||||
|
// default revalidate options for Next.js (optional)
|
||||||
|
next: {
|
||||||
|
revalidate: 60,
|
||||||
|
},
|
||||||
|
} as RequestInit,
|
||||||
|
}: FetchRepositoryInfoOptions): Promise<RepositoryInfo> {
|
||||||
|
const endpoint = `${baseUrl}/repos/${owner}/${repo}`;
|
||||||
|
const headers = new Headers(fetchOptions.headers);
|
||||||
|
|
||||||
|
headers.set('Content-Type', 'application/json');
|
||||||
|
if (token) headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
...fetchOptions,
|
||||||
|
headers,
|
||||||
|
} as RequestInit);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
|
||||||
|
throw new Error(`Failed to fetch repository data: ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return {
|
||||||
|
stars: data.stargazers_count,
|
||||||
|
forks: data.forks_count,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses compact notation (e.g., 1.5K, 2.3M).
|
||||||
|
*/
|
||||||
|
const formatterOptions: Intl.NumberFormatOptions = {
|
||||||
|
notation: 'compact',
|
||||||
|
maximumFractionDigits: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultFormatter = new Intl.NumberFormat(undefined, formatterOptions);
|
||||||
|
|
||||||
|
const promises: Record<string, Promise<RepositoryInfo>> = {};
|
||||||
|
|
||||||
|
export function GithubInfo({
|
||||||
|
repo,
|
||||||
|
owner,
|
||||||
|
token,
|
||||||
|
baseUrl,
|
||||||
|
fetchOptions,
|
||||||
|
locale,
|
||||||
|
...props
|
||||||
|
}: GithubInfoProps) {
|
||||||
|
const options: FetchRepositoryInfoOptions = {
|
||||||
|
repo,
|
||||||
|
owner,
|
||||||
|
token,
|
||||||
|
baseUrl,
|
||||||
|
fetchOptions,
|
||||||
|
};
|
||||||
|
const { stars, forks } = use(
|
||||||
|
(promises[JSON.stringify(options)] ??= fetchRepositoryInfo(options)),
|
||||||
|
);
|
||||||
|
const formatter = locale ? new Intl.NumberFormat(locale, formatterOptions) : defaultFormatter;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`https://github.com/${owner}/${repo}`}
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col gap-1.5 p-2 rounded-lg text-sm text-fd-foreground/80 transition-colors hover:text-fd-accent-foreground hover:bg-fd-accent',
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className="flex items-center gap-2 truncate">
|
||||||
|
<svg fill="currentColor" viewBox="0 0 24 24" className="size-3.5">
|
||||||
|
<title>GitHub</title>
|
||||||
|
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
|
||||||
|
</svg>
|
||||||
|
{owner}/{repo}
|
||||||
|
</p>
|
||||||
|
<div className="flex text-xs items-center gap-1 text-fd-muted-foreground">
|
||||||
|
<Star className="size-3" />
|
||||||
|
<span>{formatter.format(stars)}</span>
|
||||||
|
<GitFork className="size-3 ms-2" />
|
||||||
|
<span>{formatter.format(forks)}</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import defaultMdxComponents from "fumadocs-ui/mdx";
|
import defaultMdxComponents from "fumadocs-ui/mdx";
|
||||||
|
import * as TabsComponents from 'fumadocs-ui/components/tabs';
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import type { MDXComponents } from "mdx/types";
|
import type { MDXComponents } from "mdx/types";
|
||||||
|
import * as Twoslash from 'fumadocs-twoslash/ui';
|
||||||
|
|
||||||
export function getMDXComponents(components?: MDXComponents) {
|
export function getMDXComponents(components?: MDXComponents) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...defaultMdxComponents,
|
...defaultMdxComponents,
|
||||||
img: (props) => <Image {...(props as any)} />,
|
img: (props) => <Image {...(props as any)} />,
|
||||||
|
...TabsComponents,
|
||||||
|
...Twoslash,
|
||||||
...components,
|
...components,
|
||||||
} satisfies MDXComponents;
|
} satisfies MDXComponents;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import {
|
||||||
|
type ComponentProps,
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useId,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { cn } from '../lib/cn';
|
||||||
|
import * as Unstyled from './ui/tabs';
|
||||||
|
|
||||||
|
type CollectionKey = string | symbol;
|
||||||
|
|
||||||
|
export interface TabsProps extends Omit<
|
||||||
|
ComponentProps<typeof Unstyled.Tabs>,
|
||||||
|
'value' | 'onValueChange'
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Use simple mode instead of advanced usage as documented in https://radix-ui.com/primitives/docs/components/tabs.
|
||||||
|
*/
|
||||||
|
items?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortcut for `defaultValue` when `items` is provided.
|
||||||
|
*
|
||||||
|
* @defaultValue 0
|
||||||
|
*/
|
||||||
|
defaultIndex?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional label in tabs list when `items` is provided.
|
||||||
|
*/
|
||||||
|
label?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContext = createContext<{
|
||||||
|
items?: string[];
|
||||||
|
collection: CollectionKey[];
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
function useTabContext() {
|
||||||
|
const ctx = useContext(TabsContext);
|
||||||
|
if (!ctx) throw new Error('You must wrap your component in <Tabs>');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsList({ className, ...props }: React.ComponentProps<typeof Unstyled.TabsList>) {
|
||||||
|
return (
|
||||||
|
<Unstyled.TabsList
|
||||||
|
{...props}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'flex gap-3.5 text-fd-secondary-foreground overflow-x-auto px-4 not-prose',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Unstyled.TabsTrigger>) {
|
||||||
|
return (
|
||||||
|
<Unstyled.TabsTrigger
|
||||||
|
{...props}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'inline-flex items-center gap-2 whitespace-nowrap text-fd-muted-foreground border-b border-transparent py-2 text-sm font-medium transition-colors [&_svg]:size-4 hover:text-fd-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[active]:border-fd-primary data-[active]:text-fd-primary',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tabs({
|
||||||
|
ref,
|
||||||
|
className,
|
||||||
|
items,
|
||||||
|
label,
|
||||||
|
defaultIndex = 0,
|
||||||
|
defaultValue = items ? escapeValue(items[defaultIndex]) : undefined,
|
||||||
|
...props
|
||||||
|
}: TabsProps) {
|
||||||
|
const [value, setValue] = useState(defaultValue);
|
||||||
|
const collection = useMemo<CollectionKey[]>(() => [], []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Unstyled.Tabs
|
||||||
|
ref={ref}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'flex flex-col overflow-hidden rounded-xl border bg-fd-secondary my-4',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(v: string) => {
|
||||||
|
if (items && !items.some((item) => escapeValue(item) === v)) return;
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{items && (
|
||||||
|
<TabsList>
|
||||||
|
{label && <span className="text-sm font-medium my-auto me-auto">{label}</span>}
|
||||||
|
{items.map((item) => (
|
||||||
|
<TabsTrigger key={item} value={escapeValue(item)}>
|
||||||
|
{item}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
)}
|
||||||
|
<TabsContext.Provider value={useMemo(() => ({ items, collection }), [collection, items])}>
|
||||||
|
{props.children}
|
||||||
|
</TabsContext.Provider>
|
||||||
|
</Unstyled.Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabProps extends Omit<ComponentProps<typeof Unstyled.TabsContent>, 'value'> {
|
||||||
|
/**
|
||||||
|
* Value of tab, detect from index if unspecified.
|
||||||
|
*/
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tab({ value, ...props }: TabProps) {
|
||||||
|
const { items } = useTabContext();
|
||||||
|
const resolved =
|
||||||
|
value ??
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks -- `value` is not supposed to change
|
||||||
|
items?.at(useCollectionIndex());
|
||||||
|
if (!resolved)
|
||||||
|
throw new Error(
|
||||||
|
'Failed to resolve tab `value`, please pass a `value` prop to the Tab component.',
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContent value={escapeValue(resolved)} {...props}>
|
||||||
|
{props.children}
|
||||||
|
</TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsContent({
|
||||||
|
value,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComponentProps<typeof Unstyled.TabsContent>) {
|
||||||
|
return (
|
||||||
|
<Unstyled.TabsContent
|
||||||
|
value={value}
|
||||||
|
className={(s) =>
|
||||||
|
cn(
|
||||||
|
'p-4 text-[0.9375rem] bg-fd-background rounded-xl outline-none prose-no-margin data-[inactive]:hidden [&>figure:only-child]:-m-4 [&>figure:only-child]:border-none',
|
||||||
|
typeof className === 'function' ? className(s) : className,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Unstyled.TabsContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inspired by Headless UI.
|
||||||
|
*
|
||||||
|
* Return the index of children, this is made possible by registering the order of render from children using React context.
|
||||||
|
* This is supposed by work with pre-rendering & pure client-side rendering.
|
||||||
|
*/
|
||||||
|
function useCollectionIndex() {
|
||||||
|
const key = useId();
|
||||||
|
const { collection } = useTabContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
const idx = collection.indexOf(key);
|
||||||
|
if (idx !== -1) collection.splice(idx, 1);
|
||||||
|
};
|
||||||
|
}, [key, collection]);
|
||||||
|
|
||||||
|
if (!collection.includes(key)) collection.push(key);
|
||||||
|
return collection.indexOf(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* only escape whitespaces in values in simple mode
|
||||||
|
*/
|
||||||
|
function escapeValue(v: string): string {
|
||||||
|
return v.toLowerCase().replace(/\s/, '-');
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80',
|
||||||
|
outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||||
|
ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||||
|
secondary:
|
||||||
|
'border bg-fd-secondary text-fd-secondary-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const buttonVariants = cva(
|
||||||
|
'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: variants,
|
||||||
|
// fumadocs use `color` instead of `variant`
|
||||||
|
color: variants,
|
||||||
|
size: {
|
||||||
|
sm: 'gap-1 px-2 py-1.5 text-xs',
|
||||||
|
icon: 'p-1.5 [&_svg]:size-5',
|
||||||
|
'icon-sm': 'p-1.5 [&_svg]:size-4.5',
|
||||||
|
'icon-xs': 'p-1 [&_svg]:size-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type ButtonProps = VariantProps<typeof buttonVariants>;
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ComponentProps,
|
||||||
|
createContext,
|
||||||
|
use,
|
||||||
|
useEffectEvent,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { Tabs as Primitive } from '@base-ui/react/tabs';
|
||||||
|
import { mergeRefs } from '../../lib/merge-refs';
|
||||||
|
|
||||||
|
type ChangeListener = (v: string) => void;
|
||||||
|
const listeners = new Map<string, Set<ChangeListener>>();
|
||||||
|
|
||||||
|
export interface TabsProps extends ComponentProps<typeof Primitive.Root> {
|
||||||
|
/**
|
||||||
|
* Identifier for Sharing value of tabs
|
||||||
|
*/
|
||||||
|
groupId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable persistent
|
||||||
|
*/
|
||||||
|
persist?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, updates the URL hash based on the tab's id
|
||||||
|
*/
|
||||||
|
updateAnchor?: boolean;
|
||||||
|
|
||||||
|
onValueChange?: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContext = createContext<{
|
||||||
|
valueToIdMap: Map<string, string>;
|
||||||
|
/**
|
||||||
|
* Mounted tab panels, mapped by their value.
|
||||||
|
*
|
||||||
|
* Only populated for panels that stay in the DOM (e.g. `keepMounted`), which is
|
||||||
|
* what allows us to open the tab containing a hash target.
|
||||||
|
*/
|
||||||
|
panels: Map<string, HTMLElement>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
function useTabContext() {
|
||||||
|
const ctx = use(TabsContext);
|
||||||
|
if (!ctx) throw new Error('You must wrap your component in <Tabs>');
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabsList = Primitive.List;
|
||||||
|
|
||||||
|
export const TabsTrigger = Primitive.Tab;
|
||||||
|
|
||||||
|
export function Tabs({
|
||||||
|
ref,
|
||||||
|
groupId,
|
||||||
|
persist = false,
|
||||||
|
updateAnchor = false,
|
||||||
|
defaultValue,
|
||||||
|
value: _value,
|
||||||
|
onValueChange: _onValueChange,
|
||||||
|
...props
|
||||||
|
}: TabsProps) {
|
||||||
|
const tabsRef = useRef<HTMLDivElement>(null);
|
||||||
|
const valueToIdMap = useMemo(() => new Map<string, string>(), []);
|
||||||
|
const panels = useMemo(() => new Map<string, HTMLElement>(), []);
|
||||||
|
const [value, setValue] =
|
||||||
|
_value === undefined
|
||||||
|
? // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
|
||||||
|
useState(defaultValue)
|
||||||
|
: // eslint-disable-next-line react-hooks/rules-of-hooks -- not supposed to change controlled/uncontrolled
|
||||||
|
[_value, useEffectEvent((v: string) => _onValueChange?.(v))];
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!groupId) return;
|
||||||
|
let previous = sessionStorage.getItem(groupId);
|
||||||
|
if (persist) previous ??= localStorage.getItem(groupId);
|
||||||
|
if (previous) setValue(previous);
|
||||||
|
|
||||||
|
const groupListeners = listeners.get(groupId) ?? new Set();
|
||||||
|
groupListeners.add(setValue);
|
||||||
|
listeners.set(groupId, groupListeners);
|
||||||
|
return () => {
|
||||||
|
groupListeners.delete(setValue);
|
||||||
|
};
|
||||||
|
}, [groupId, persist, setValue]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const openFromHash = () => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
if (!hash) return;
|
||||||
|
|
||||||
|
// hash points to a tab's own anchor id
|
||||||
|
for (const [value, id] of valueToIdMap.entries()) {
|
||||||
|
if (id === hash) {
|
||||||
|
setValue(value);
|
||||||
|
tabsRef.current?.scrollIntoView();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hash points to an element inside a mounted (e.g. `keepMounted`) panel,
|
||||||
|
// open the tab it belongs to, then scroll to it once the panel is visible.
|
||||||
|
const target = document.getElementById(hash);
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
for (const [value, panel] of panels.entries()) {
|
||||||
|
if (!panel.contains(target)) continue;
|
||||||
|
|
||||||
|
setValue(value);
|
||||||
|
requestAnimationFrame(() => target.scrollIntoView());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
openFromHash();
|
||||||
|
window.addEventListener('hashchange', openFromHash);
|
||||||
|
return () => window.removeEventListener('hashchange', openFromHash);
|
||||||
|
}, [setValue, valueToIdMap, panels]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Primitive.Root
|
||||||
|
ref={mergeRefs(ref, tabsRef)}
|
||||||
|
value={value}
|
||||||
|
onValueChange={(v: string) => {
|
||||||
|
if (updateAnchor) {
|
||||||
|
const id = valueToIdMap.get(v);
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
window.history.replaceState(null, '', `#${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
const groupListeners = listeners.get(groupId);
|
||||||
|
if (groupListeners) {
|
||||||
|
for (const listener of groupListeners) listener(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.setItem(groupId, v);
|
||||||
|
if (persist) localStorage.setItem(groupId, v);
|
||||||
|
} else {
|
||||||
|
setValue(v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<TabsContext value={useMemo(() => ({ valueToIdMap, panels }), [valueToIdMap, panels])}>
|
||||||
|
{props.children}
|
||||||
|
</TabsContext>
|
||||||
|
</Primitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabsContent({ value, ref, ...props }: ComponentProps<typeof Primitive.Panel>) {
|
||||||
|
const { valueToIdMap, panels } = useTabContext();
|
||||||
|
|
||||||
|
if (props.id) {
|
||||||
|
valueToIdMap.set(value, props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Primitive.Panel
|
||||||
|
ref={mergeRefs(ref, (element) => {
|
||||||
|
if (element) panels.set(value, element);
|
||||||
|
else panels.delete(value);
|
||||||
|
})}
|
||||||
|
value={value}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Primitive.Panel>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type * as React from 'react';
|
||||||
|
|
||||||
|
export function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]): React.RefCallback<T> {
|
||||||
|
return (value) => {
|
||||||
|
refs.forEach((ref) => {
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(value);
|
||||||
|
} else if (ref) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
+1
-1
@@ -5,7 +5,7 @@ export const docsContentRoute = '/llms.mdx/docs';
|
|||||||
|
|
||||||
// fill this with your actual GitHub info, for example:
|
// fill this with your actual GitHub info, for example:
|
||||||
export const gitConfig = {
|
export const gitConfig = {
|
||||||
user: 'duckity',
|
user: 'duckity-dev',
|
||||||
repo: 'documentation',
|
repo: 'documentation',
|
||||||
branch: 'main',
|
branch: 'main',
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,7 @@
|
|||||||
import { docs } from "collections/server";
|
import { docs } from "collections/server";
|
||||||
import { loader } from "fumadocs-core/source";
|
import { loader } from "fumadocs-core/source";
|
||||||
import { icons } from "lucide-react";
|
import { icons as lucide } from "lucide-react";
|
||||||
|
import * as simple from "@icons-pack/react-simple-icons";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
import { docsContentRoute, docsImageRoute, docsRoute } from "./shared";
|
import { docsContentRoute, docsImageRoute, docsRoute } from "./shared";
|
||||||
|
|
||||||
@@ -15,7 +16,10 @@ export const source = loader({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon in icons) return createElement(icons[icon as keyof typeof icons]);
|
if (icon in lucide)
|
||||||
|
return createElement(lucide[icon as keyof typeof lucide]);
|
||||||
|
if (icon in simple)
|
||||||
|
return createElement(simple[icon as keyof typeof simple]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user