Checkpoint

This commit is contained in:
2026-07-28 03:53:38 -03:00
parent 0f506ec19e
commit 435e4e58e8
26 changed files with 1174 additions and 48 deletions
+32 -5
View File
@@ -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.
![Screenshot](/images/screenshot-dashboard-throttling-windows.png)
+4
View File
@@ -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
View File
@@ -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.
+17
View File
@@ -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
View File
@@ -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]
![Screenshot](/images/screenshot-dashboard-empty.png)
## 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.
![Screenshot](/images/screenshot-dashboard-create-application.png)
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.
![Screenshot](/images/screenshot-dashboard-application.png) <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.
![Screenshot](/images/screenshot-dashboard-create-protection-profile.png)
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.
![Screenshot](/images/screenshot-dashboard-protection-profile.png) <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.
![Screenshot](/images/screenshot-dashboard-throttling-windows.png) 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>
+4
View File
@@ -0,0 +1,4 @@
---
title: C
icon: SiC
---
+137
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
---
title: Python 3
icon: SiPython
---
+4
View File
@@ -0,0 +1,4 @@
---
title: React
icon: SiReact
---
+4
View File
@@ -0,0 +1,4 @@
---
title: Rust (No STD)
icon: SiRust
---
+4
View File
@@ -0,0 +1,4 @@
---
title: Rust (STD)
icon: SiRust
---
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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"],
},
}, },
}); });
+1 -1
View File
@@ -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
View File
@@ -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;
+265
View File
@@ -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} />;
}
+118
View File
@@ -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>
);
}
+4 -1
View File
@@ -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;
} }
+200
View File
@@ -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/, '-');
}
+28
View File
@@ -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>;
+179
View File
@@ -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>
);
}
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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]);
}, },
}); });