diff --git a/bun.lock b/bun.lock
index d7b4bfc89..b899f4e4f 100644
--- a/bun.lock
+++ b/bun.lock
@@ -21,23 +21,12 @@
"name": "@inkeep/open-knowledge-docs",
"version": "0.0.8",
"dependencies": {
- "@codemirror/commands": "^6.10.3",
- "@codemirror/lang-markdown": "^6.5.0",
- "@codemirror/language": "^6.12.3",
- "@codemirror/state": "^6.6.0",
- "@codemirror/view": "^6.41.0",
- "@floating-ui/dom": "^1.7.6",
"@hookform/resolvers": "^5.4.0",
"@inkeep/cxkit-react": "^0.5.117",
"@inkeep/open-knowledge-core": "workspace:*",
"@radix-ui/react-popover": "^1.1.17",
- "@tiptap/core": "^3.22.3",
- "@tiptap/extension-drag-handle": "3.22.3",
- "@tiptap/extensions": "^3.22.3",
- "@tiptap/pm": "^3.22.3",
- "@tiptap/react": "^3.22.3",
- "@tiptap/suggestion": "^3.22.3",
"@types/mdx": "^2.0.13",
+ "@vercel/microfrontends": "^2.3.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "~16.1.0",
@@ -1138,7 +1127,7 @@
"@napi-rs/wasm-tools-win32-x64-msvc": ["@napi-rs/wasm-tools-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rEAf05nol3e3eei2sRButmgXP+6ATgm0/38MKhz9Isne82T4rPIMYsCIFj0kOisaGeVwoi2fnm7O9oWp5YVnYQ=="],
- "@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
+ "@next/env": ["@next/env@16.0.10", "", {}, "sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg=="],
@@ -1902,6 +1891,8 @@
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
+ "@types/md5": ["@types/md5@2.3.6", "", {}, "sha512-WD69gNXtRBnpknfZcb4TRQ0XJQbUPZcai/Qdhmka3sxUR3Et8NrXoeAoknG/LghYHTf4ve795rInVYHBTQdNVA=="],
+
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
@@ -1968,6 +1959,8 @@
"@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="],
+ "@vercel/microfrontends": ["@vercel/microfrontends@2.3.6", "", { "dependencies": { "@next/env": "16.0.10", "@types/md5": "^2.3.5", "ajv": "^8.17.1", "commander": "^12.1.0", "cookie": "1.0.2", "fast-glob": "^3.3.2", "http-proxy": "^1.18.1", "jsonc-parser": "^3.3.1", "md5": "^2.3.0", "nanoid": "^3.3.9", "path-to-regexp": "6.3.0", "semver": "^7.7.2" }, "peerDependencies": { "@sveltejs/kit": ">=1", "@vercel/analytics": ">=1.5.0", "@vercel/speed-insights": ">=1.2.0", "next": ">=13", "react": ">=17.0.0", "react-dom": ">=17.0.0", "vite": ">=5" }, "optionalPeers": ["@sveltejs/kit", "@vercel/analytics", "@vercel/speed-insights", "next", "react", "react-dom", "vite"], "bin": { "microfrontends": "cli/index.cjs" } }, "sha512-iMgZp5wmNWMeFO8Sg4BlzshjsjbiXfnIOOGwcFWWb+90nrySy6YV0fgXxj2ZubDGFa6BwWD/Mh/fKKXr4I+iuQ=="],
+
"@vimeo/player": ["@vimeo/player@2.30.4", "", { "dependencies": { "native-promise-only": "0.8.1", "weakmap-polyfill": "2.0.4" } }, "sha512-M8m1UAhJSb+KCWuXDLWHViwj+3YY/0ogwFquRfMHs9e9LYjXT9iB7n+sOCKwUusbiXuU2HKmXx+FEGHtYZfUSg=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
@@ -2212,6 +2205,8 @@
"chardet": ["chardet@2.1.1", "", {}, "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ=="],
+ "charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="],
+
"chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="],
"chevrotain-allstar": ["chevrotain-allstar@0.4.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^12.0.0" } }, "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA=="],
@@ -2298,7 +2293,7 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
- "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+ "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
@@ -2322,6 +2317,8 @@
"crossws": ["crossws@0.4.4", "", { "peerDependencies": { "srvx": ">=0.7.1" }, "optionalPeers": ["srvx"] }, "sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg=="],
+ "crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="],
+
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
@@ -2704,6 +2701,8 @@
"float-tooltip": ["float-tooltip@1.7.5", "", { "dependencies": { "d3-selection": "2 - 3", "kapsule": "^1.16", "preact": "10" } }, "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg=="],
+ "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
+
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"force-graph": ["force-graph@1.51.2", "", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "bezier-js": "3 - 6", "canvas-color-tracker": "^1.3", "d3-array": "1 - 3", "d3-drag": "2 - 3", "d3-force-3d": "2 - 3", "d3-scale": "1 - 4", "d3-scale-chromatic": "1 - 3", "d3-selection": "2 - 3", "d3-zoom": "2 - 3", "float-tooltip": "^1.7", "index-array-by": "1", "kapsule": "^1.16", "lodash-es": "4" } }, "sha512-zZNdMqx8qIQGurgnbgYIUsdXxSfvhfRSIdncsKGv/twUOZpwCsk9hPHmdjdcme1+epATgb41G0rkIGHJ0Wydng=="],
@@ -2880,6 +2879,8 @@
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+ "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
+
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
@@ -2948,6 +2949,8 @@
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
+ "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="],
+
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
@@ -3190,6 +3193,8 @@
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+ "md5": ["md5@2.3.0", "", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="],
+
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
@@ -3532,7 +3537,7 @@
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
- "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
+ "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
@@ -3812,6 +3817,8 @@
"require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="],
+ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
+
"resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="],
"resend": ["resend@6.16.0", "", { "dependencies": { "postal-mime": "2.7.4", "standardwebhooks": "1.0.0" }, "peerDependencies": { "@react-email/render": "*" }, "optionalPeers": ["@react-email/render"] }, "sha512-SaKISwHtxvAxneF84Njgnzg+zdngUu1vOT/paRU1De9QF+zXQR3GnwJHSh+mpJPjUhsGD4WxYi5CfKkXMkDqwg=="],
@@ -4598,6 +4605,8 @@
"@types/yauzl/@types/node": ["@types/node@24.12.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
+ "@vercel/microfrontends/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
+
"ajv-keywords/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
"app-builder-lib/@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="],
@@ -4678,6 +4687,8 @@
"estimo/nanoid": ["nanoid@5.1.5", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="],
+ "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
"extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
"filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
@@ -4690,6 +4701,8 @@
"formdata-node/web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="],
+ "fumadocs-core/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
+
"fumadocs-mdx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"fumadocs-mdx/lru-cache": ["lru-cache@11.3.3", "", {}, "sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ=="],
@@ -4716,6 +4729,8 @@
"hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
+ "http-proxy/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+
"iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="],
"iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="],
@@ -4752,7 +4767,7 @@
"msw/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
- "msw/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
+ "next/@next/env": ["@next/env@16.2.3", "", {}, "sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
@@ -4846,6 +4861,8 @@
"rolldown-plugin-dts/get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
+ "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="],
+
"seek-bzip/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
"serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="],
diff --git a/docs/next.config.ts b/docs/next.config.ts
index 6ba8a0ac5..195cc665e 100644
--- a/docs/next.config.ts
+++ b/docs/next.config.ts
@@ -1,6 +1,13 @@
+import { existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { withMicrofrontends } from '@vercel/microfrontends/next/config';
import { createMDX } from 'fumadocs-mdx/next';
import type { NextConfig } from 'next';
+// Routing home is `/docs` — this app serves no landing page at `/`. In the
+// production deployment the apex `/` is served by a separate app, so `/` never
+// reaches this project there; a standalone deployment should set its default
+// route to `/docs`.
const nextConfig: NextConfig = {
reactStrictMode: true,
reactCompiler: {
@@ -125,5 +132,13 @@ const nextConfig: NextConfig = {
};
const withMDX = createMDX();
+const baseConfig = withMDX(nextConfig);
-export default withMDX(nextConfig);
+// `withMicrofrontends` requires a microfrontends.json that declares this app. That
+// file names the private marketing app and is a Microfrontends deploy concern of
+// agents-private only, so it is excluded from the public mirror — the standalone
+// OSS docs build has none. Apply the wrapper only when the config is present,
+// leaving the mirror (and any standalone clone) a plain Next.js app.
+const microfrontendsConfig = fileURLToPath(new URL('./microfrontends.json', import.meta.url));
+
+export default existsSync(microfrontendsConfig) ? withMicrofrontends(baseConfig) : baseConfig;
diff --git a/docs/package.json b/docs/package.json
index 05a2f992e..94518aa0d 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -17,23 +17,12 @@
"postinstall": "fumadocs-mdx"
},
"dependencies": {
- "@codemirror/commands": "^6.10.3",
- "@codemirror/lang-markdown": "^6.5.0",
- "@codemirror/language": "^6.12.3",
- "@codemirror/state": "^6.6.0",
- "@codemirror/view": "^6.41.0",
- "@floating-ui/dom": "^1.7.6",
"@hookform/resolvers": "^5.4.0",
"@inkeep/cxkit-react": "^0.5.117",
"@inkeep/open-knowledge-core": "workspace:*",
"@radix-ui/react-popover": "^1.1.17",
- "@tiptap/core": "^3.22.3",
- "@tiptap/extension-drag-handle": "3.22.3",
- "@tiptap/extensions": "^3.22.3",
- "@tiptap/pm": "^3.22.3",
- "@tiptap/react": "^3.22.3",
- "@tiptap/suggestion": "^3.22.3",
"@types/mdx": "^2.0.13",
+ "@vercel/microfrontends": "^2.3.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "~16.1.0",
diff --git a/docs/src/app/(home)/brand/page.tsx b/docs/src/app/(home)/brand/page.tsx
index 7bbd0e50f..13b70e914 100644
--- a/docs/src/app/(home)/brand/page.tsx
+++ b/docs/src/app/(home)/brand/page.tsx
@@ -1,9 +1,9 @@
import type { Metadata } from 'next';
+import { SiteFooter } from '@/components/footer';
+import { MarketingButton } from '@/components/marketing-button';
+import SectionHeading from '@/components/section-heading';
import { BRAND_ASSETS, BRAND_ROUTE, BRAND_ZIP } from '@/lib/brand-assets';
import { cn } from '@/lib/utils';
-import { SiteFooter } from '../footer';
-import { MarketingButton } from '../marketing-button';
-import SectionHeading from '../section-heading';
export const metadata: Metadata = {
title: 'Brand assets',
diff --git a/docs/src/app/(home)/feature-item.tsx b/docs/src/app/(home)/feature-item.tsx
deleted file mode 100644
index 738492bb7..000000000
--- a/docs/src/app/(home)/feature-item.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { LucideIcon } from 'lucide-react';
-import { cn } from '@/lib/utils';
-
-interface FeatureItemProps {
- icon: LucideIcon;
- title: string;
- description: string;
- className?: string;
-}
-
-const FeatureItem = ({ icon: Icon, title, description, className }: FeatureItemProps) => {
- return (
-
-
-
-
-
-
{title}
-
{description}
-
-
- );
-};
-
-export default FeatureItem;
diff --git a/docs/src/app/(home)/layout.tsx b/docs/src/app/(home)/layout.tsx
deleted file mode 100644
index d252fb144..000000000
--- a/docs/src/app/(home)/layout.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { getGitHubStars } from '@inkeep/open-knowledge-core';
-import { SiteNav } from './site-nav';
-
-export default async function Layout({ children }: LayoutProps<'/'>) {
- const stars = await getGitHubStars({ next: { revalidate: 3600 } });
- return (
- <>
-
- {children}
- >
- );
-}
diff --git a/docs/src/app/(home)/page.tsx b/docs/src/app/(home)/page.tsx
deleted file mode 100644
index a75eac588..000000000
--- a/docs/src/app/(home)/page.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { SoftwareApplication, WithContext } from 'schema-dts';
-import { JsonLd } from '@/components/seo/json-ld';
-import { DOWNLOAD_URL, GITHUB_URL, SITE_DESCRIPTION, SITE_NAME, SITE_URL } from '@/lib/site';
-import { SiteFooter } from './footer';
-import { CallToAction } from './sections/call-to-action';
-import { Collaboration } from './sections/collaboration';
-import { Hero } from './sections/hero';
-import { MadeForAgents } from './sections/made-for-agents';
-import { OwnYourKnowledge } from './sections/own-your-knowledge';
-import { PutItToWork } from './sections/put-it-to-work';
-import { RichEditing } from './sections/rich-editing';
-
-const softwareAppLd = {
- '@context': 'https://schema.org',
- '@type': 'SoftwareApplication',
- name: SITE_NAME,
- applicationCategory: 'DeveloperApplication',
- operatingSystem: 'macOS',
- url: SITE_URL,
- downloadUrl: DOWNLOAD_URL,
- description: SITE_DESCRIPTION,
- isAccessibleForFree: true,
- offers: {
- '@type': 'Offer',
- price: '0',
- priceCurrency: 'USD',
- },
- author: {
- '@type': 'Organization',
- name: 'Inkeep',
- url: 'https://inkeep.com',
- },
- sameAs: GITHUB_URL,
-} satisfies WithContext;
-
-export default function HomePage() {
- return (
-
- );
-}
diff --git a/docs/src/app/(home)/section.tsx b/docs/src/app/(home)/section.tsx
deleted file mode 100644
index 208a20ba2..000000000
--- a/docs/src/app/(home)/section.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { cn } from '@/lib/utils';
-
-export function Section({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return (
-
- );
-}
diff --git a/docs/src/app/(home)/sections/agent-brain-graphic.tsx b/docs/src/app/(home)/sections/agent-brain-graphic.tsx
deleted file mode 100644
index bce9d3d3e..000000000
--- a/docs/src/app/(home)/sections/agent-brain-graphic.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-import {
- Box,
- Code,
- FileText,
- Globe,
- type LucideIcon,
- MessageCircleMore,
- Sparkles,
- User,
-} from 'lucide-react';
-
-const RINGS = [20, 44, 68, 94] as const;
-
-type Bubble = { icon: LucideIcon; angle: number; radius: number };
-
-const BUBBLES: Bubble[] = [
- { icon: Globe, angle: 8, radius: 68 }, // right, outer ring
- { icon: User, angle: 65, radius: 44 }, // upper-right, inner ring
- { icon: FileText, angle: 128, radius: 68 }, // upper-left, outer ring
- { icon: MessageCircleMore, angle: 185, radius: 44 }, // left, inner ring
- { icon: Code, angle: 248, radius: 68 }, // lower-left, outer ring
- { icon: Box, angle: 305, radius: 44 }, // lower-right, inner ring
-];
-
-const CENTER = { x: 80, y: 45 };
-
-function orbitMotion(radius: number) {
- return radius > 50
- ? { duration: 130, direction: 'reverse' as const }
- : { duration: 90, direction: 'normal' as const };
-}
-
-function AgentMark() {
- return (
-
-
-
- );
-}
-
-export function AgentBrainGraphic() {
- return (
-
-
-
- {RINGS.map((r) => (
-
- ))}
-
-
- {BUBBLES.map((b, i) => {
- const Icon = b.icon;
- const { duration, direction } = orbitMotion(b.radius);
- const counterDirection = direction === 'reverse' ? 'normal' : 'reverse';
- return (
-
- );
- })}
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/call-to-action.tsx b/docs/src/app/(home)/sections/call-to-action.tsx
deleted file mode 100644
index 56a437073..000000000
--- a/docs/src/app/(home)/sections/call-to-action.tsx
+++ /dev/null
@@ -1,126 +0,0 @@
-'use client';
-
-import Image from 'next/image';
-import Link from 'next/link';
-import { useEffect, useState } from 'react';
-import { DOWNLOAD_ROUTE } from '@/lib/site';
-import { useIsInView } from '@/lib/use-is-in-view';
-import { cn } from '@/lib/utils';
-import { DotTexture } from '../dot-texture';
-import { MarketingButton } from '../marketing-button';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-
-/* Two soft organic shapes drift toward center while blurring when the section
- scrolls into view, overlapping behind the headline. multiply darkens the
- overlap into a richer blue on the light background; screen is the dark-mode
- equivalent (multiply over near-black renders the shapes invisible). */
-function CtaShape({
- src,
- width,
- height,
- settled,
- wrapperClassName,
- imageClassName,
- fromX,
- toX,
-}: {
- src: string;
- width: number;
- height: number;
- settled: boolean;
- wrapperClassName?: string;
- imageClassName?: string;
- fromX: string;
- toX: string;
-}) {
- return (
-
-
-
- );
-}
-
-export function CallToAction() {
- const [stageRef, inView] = useIsInView('-15%');
- const [settled, setSettled] = useState(false);
- useEffect(() => {
- if (inView) setSettled(true);
- }, [inView]);
-
- return (
-
-
-
-
-
-
-
-
-
-
- Start building knowledge.
-
-
-
-
- DOWNLOAD FOR MAC
-
-
- More platforms
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/collaboration.tsx b/docs/src/app/(home)/sections/collaboration.tsx
deleted file mode 100644
index d52fcb3e7..000000000
--- a/docs/src/app/(home)/sections/collaboration.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import { GitBranch, type LucideIcon, MousePointerClick, Share2, Sparkle } from 'lucide-react';
-import type { ReactNode, SVGProps } from 'react';
-import { ClaudeIcon } from '@/components/icons/claude';
-import { CodexBrandMonoIcon } from '@/components/icons/codex';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-
-const CLAUDE_BRAND = '#D97757';
-
-type EditorTint = {
- highlightBg: string;
- badgeBg: string;
- badgeText: string;
-};
-
-const CLAUDE_TINT: EditorTint = {
- highlightBg: 'color-mix(in srgb, var(--color-orange-light) 80%, transparent)',
- badgeBg: CLAUDE_BRAND,
- badgeText: '#ffffff',
-};
-
-const ALAN_TINT: EditorTint = {
- highlightBg: 'color-mix(in srgb, var(--color-crystal-blue) 70%, transparent)',
- badgeBg: 'var(--color-azure-blue)',
- badgeText: '#ffffff',
-};
-
-const CODEX_TINT: EditorTint = {
- highlightBg: 'color-mix(in srgb, var(--color-purple-light) 70%, transparent)',
- badgeBg: '#7c6df0',
- badgeText: '#ffffff',
-};
-
-function ShareButton() {
- return (
-
-
- Share
-
- );
-}
-
-function PresenceAvatars() {
- return (
-
- {/* Icon-based "agent" avatars use a white plate + thin colored ring so the
- brand mark stays legible. Text-initial "human" avatars (Alan) keep the
- solid-color treatment so the initial reads at high contrast. */}
-
-
- A
-
-
-
-
-
-
-
-
-
- );
-}
-
-function PresenceAvatar({
- tint,
- ringClass,
- children,
-}: {
- tint: EditorTint;
- ringClass: string;
- children: ReactNode;
-}) {
- return (
-
- {children}
-
- );
-}
-
-function Highlight({
- tint,
- name,
- icon: Icon,
- children,
-}: {
- tint: EditorTint;
- name: string;
- icon?: (props: SVGProps) => ReactNode;
- children: ReactNode;
-}) {
- return (
-
- {children}
- {/* Inline-block anchor at the trailing edge of the last line. Holds both
- the caret and the floating name badge so they stay glued together when
- the highlight wraps across lines (absolute positioning on the outer
- span otherwise stretches the caret across both line boxes and floats
- the badge over the first line). */}
-
-
-
- {Icon ? : null}
- {name}
-
-
-
- );
-}
-
-function CollaborationPreview() {
- return (
-
- {/* Document chrome — filename + presence */}
-
-
- {/* Document body */}
-
-
- Q3 Roadmap
-
-
-
- We’re bringing the whole team into{' '}
-
- one shared workspace
-
- , where people and agents edit the same files instead of losing context in a thread.
-
-
-
-
This quarter
-
-
-
-
- Ship the{' '}
-
- collaborative editor
- {' '}
- for docs and spaces
-
-
-
-
- One click to share a doc, or a whole workspace
-
-
-
-
-
-
- Git-backed sync so history and ownership stay yours
-
-
-
-
-
-
-
-
- );
-}
-
-function Bullet() {
- return (
-
- );
-}
-
-function FeatureItem({ icon: Icon, label }: { icon: LucideIcon; label: string }) {
- return (
-
-
- {label}
-
- );
-}
-
-export function Collaboration() {
- return (
-
-
-
-
-
- Collaborate with your team.
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/eng-specs-graphic.tsx b/docs/src/app/(home)/sections/eng-specs-graphic.tsx
deleted file mode 100644
index b208fee1f..000000000
--- a/docs/src/app/(home)/sections/eng-specs-graphic.tsx
+++ /dev/null
@@ -1,241 +0,0 @@
-import { Code, Sparkles } from 'lucide-react';
-
-const RINGS = [56, 82, 108] as const;
-const RING_CENTER = { x: 80, y: 45 };
-
-const CARD_BG = 'var(--slide-bg-elevated)';
-const CARD_RADIUS = '3.6cqw';
-const FRONT_SHADOW = '0 1px 3px rgba(15, 23, 42, 0.07), 0 16px 34px -10px rgba(15, 23, 42, 0.18)';
-const BACK_SHADOW = '0 1px 2px rgba(15, 23, 42, 0.05), 0 6px 18px -8px rgba(15, 23, 42, 0.12)';
-
-const LINE_FILL = 'color-mix(in srgb, var(--slide-text) 13%, transparent)';
-const ACCENT_LINE = 'color-mix(in srgb, var(--slide-accent) 45%, transparent)';
-const HAIRLINE = 'color-mix(in srgb, var(--slide-text) 8%, transparent)';
-const MUTED_FILL = 'color-mix(in srgb, var(--slide-muted) 14%, transparent)';
-
-function BackgroundRings() {
- return (
-
- {RINGS.map((r) => (
-
- ))}
-
- );
-}
-
-type Seg = { w: string; accent?: boolean };
-
-function CodeLine({ indent, segs }: { indent?: string; segs: Seg[] }) {
- return (
-
- {segs.map((s, i) => (
-
- ))}
-
- );
-}
-
-function SkeletonLine({ width, delay }: { width: string; delay: string }) {
- return (
-
- );
-}
-
-function CodeCard() {
- return (
-
-
-
-
- auth.ts
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-
-function SpecCard() {
- return (
-
-
- SPEC
-
-
- Auth service
-
-
-
-
-
-
-
-
-
-
-
-
-
- You and your agents
-
-
-
- );
-}
-
-export function EngSpecsGraphic() {
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/hero-preview.tsx b/docs/src/app/(home)/sections/hero-preview.tsx
deleted file mode 100644
index 61b217880..000000000
--- a/docs/src/app/(home)/sections/hero-preview.tsx
+++ /dev/null
@@ -1,691 +0,0 @@
-'use client';
-
-import { ArrowUp, Check, GitBranch, Globe, Loader2 } from 'lucide-react';
-import { useEffect, useRef, useState } from 'react';
-import { ClaudeIcon } from '@/components/icons/claude';
-import { CodexBrandIcon } from '@/components/icons/codex';
-import { CursorIcon } from '@/components/icons/cursor';
-import {
- OkEditorBody,
- OkEditorModeToggle,
- OkEditorProvider,
- parseEditorMarkdown,
- useEditorDocStats,
- useOkEditor,
-} from '@/components/ok-editor/ok-editor';
-import {
- HERO_FRONTMATTER,
- HERO_FRONTMATTER_YAML,
- heroRevealMarkdown,
-} from '@/components/ok-editor/seed';
-import { OkIcon } from '@/components/ok-icon';
-import { OkWordmark } from '@/components/ok-wordmark';
-import { useIsInView } from '@/lib/use-is-in-view';
-import { usePrefersReducedMotion } from '@/lib/use-prefers-reduced-motion';
-import { cn } from '@/lib/utils';
-
-const AGENT_META = {
- claude: { label: 'Claude', Icon: ClaudeIcon, brandColor: '#D97757' as string | undefined },
- cursor: {
- label: 'Cursor',
- Icon: CursorIcon,
- brandColor: 'var(--slide-text)' as string | undefined,
- },
- codex: { label: 'Codex', Icon: CodexBrandIcon, brandColor: '#7A9DFF' as string | undefined },
-} as const;
-
-export type HeroPreviewAgentId = keyof typeof AGENT_META;
-
-const AGENT_IDS = Object.keys(AGENT_META) as HeroPreviewAgentId[];
-
-const PAUSED = false;
-
-const USER_MESSAGE = 'Help me write up our launch week';
-const AGENT_STATUS = 'Drafting your launch recap in OpenKnowledge.';
-const DOC_PATH = 'retros/launch-week';
-const TOOL_NAME = 'open-knowledge · write';
-const TOOL_SUMMARY = 'Create recap + add daily activity chart';
-
-type Phase =
- | 'rest'
- | 'user-typing'
- | 'agent-status'
- | 'tool-appear'
- | 'tool-filling'
- | 'tool-done'
- | 'hold'
- | 'reset';
-
-const SEGMENTS: ReadonlyArray<{ phase: Phase; ms: number }> = [
- { phase: 'rest', ms: 800 },
- { phase: 'user-typing', ms: 800 },
- { phase: 'agent-status', ms: 500 },
- { phase: 'tool-appear', ms: 400 },
- { phase: 'tool-filling', ms: 2000 },
- { phase: 'tool-done', ms: 600 },
- { phase: 'hold', ms: 2200 },
- { phase: 'reset', ms: 500 },
-];
-const RUN_ONCE_MS = SEGMENTS.filter((s) => s.phase !== 'reset').reduce((sum, s) => sum + s.ms, 0);
-
-const FINAL_STATE: State = {
- phase: 'hold',
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: AGENT_STATUS.length,
- contentStep: 5,
-};
-
-const FILL_THRESHOLDS_MS = [0, 450, 900, 1350] as const;
-
-type State = {
- phase: Phase;
- userTypedLen: number;
- agentTypedLen: number;
- contentStep: number;
-};
-
-function computeState(elapsed: number): State {
- let cursor = 0;
- for (const seg of SEGMENTS) {
- if (elapsed < cursor + seg.ms) {
- const local = elapsed - cursor;
- const localProgress = local / seg.ms;
- return computeStateForSegment(seg.phase, local, localProgress);
- }
- cursor += seg.ms;
- }
- return {
- phase: 'hold',
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: AGENT_STATUS.length,
- contentStep: 5,
- };
-}
-
-function computeStateForSegment(phase: Phase, local: number, localProgress: number): State {
- switch (phase) {
- case 'rest':
- return { phase, userTypedLen: 0, agentTypedLen: 0, contentStep: 0 };
- case 'user-typing':
- return {
- phase,
- userTypedLen: Math.floor(localProgress * USER_MESSAGE.length),
- agentTypedLen: 0,
- contentStep: 0,
- };
- case 'agent-status':
- return {
- phase,
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: Math.floor(localProgress * AGENT_STATUS.length),
- contentStep: 0,
- };
- case 'tool-appear':
- return {
- phase,
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: AGENT_STATUS.length,
- contentStep: 1,
- };
- case 'tool-filling': {
- let step = 1;
- for (let i = 0; i < FILL_THRESHOLDS_MS.length; i++) {
- if (local >= FILL_THRESHOLDS_MS[i]) step = 2 + i;
- }
- return {
- phase,
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: AGENT_STATUS.length,
- contentStep: step,
- };
- }
- case 'tool-done':
- case 'hold':
- return {
- phase,
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: AGENT_STATUS.length,
- contentStep: 5,
- };
- case 'reset':
- return {
- phase,
- userTypedLen: USER_MESSAGE.length,
- agentTypedLen: AGENT_STATUS.length,
- contentStep: 5,
- };
- }
-}
-
-export function HeroPreview({ activeAgentId }: { activeAgentId: HeroPreviewAgentId }) {
- const activeAgent = AGENT_META[activeAgentId];
- const [state, setState] = useState(() => computeState(0));
- const [animationDone, setAnimationDone] = useState(false);
-
- const [containerRef, inView] = useIsInView('100px');
- const prefersReducedMotion = usePrefersReducedMotion();
-
- useEffect(() => {
- if (PAUSED) return;
- if (animationDone) return;
- if (prefersReducedMotion) {
- setState(FINAL_STATE);
- setAnimationDone(true);
- return;
- }
- if (!inView) return;
- let raf = 0;
- let lastPhase: Phase | null = null;
- let lastUserLen = -1;
- let lastAgentLen = -1;
- let lastStep = -1;
- const start = performance.now();
- const step = (now: number) => {
- const elapsed = now - start;
- if (elapsed >= RUN_ONCE_MS) {
- setState(FINAL_STATE);
- setAnimationDone(true);
- return;
- }
- const next = computeState(elapsed);
- if (
- next.phase !== lastPhase ||
- next.userTypedLen !== lastUserLen ||
- next.agentTypedLen !== lastAgentLen ||
- next.contentStep !== lastStep
- ) {
- lastPhase = next.phase;
- lastUserLen = next.userTypedLen;
- lastAgentLen = next.agentTypedLen;
- lastStep = next.contentStep;
- setState(next);
- }
- raf = requestAnimationFrame(step);
- };
- raf = requestAnimationFrame(step);
- return () => cancelAnimationFrame(raf);
- }, [inView, prefersReducedMotion, animationDone]);
-
- const mobileScene: 'chat' | 'editor' =
- state.phase === 'rest' ||
- state.phase === 'user-typing' ||
- state.phase === 'agent-status' ||
- state.phase === 'tool-appear'
- ? 'chat'
- : 'editor';
-
- return (
-
- {/* Two layout modes:
- -
-
- {/* Chat cell — one chat panel per agent, stacked and cross-faded. All
- share the single animation state; only the active agent's branding
- is visible, so switching agents just cross-fades the chat. */}
-
- {AGENT_IDS.map((id) => {
- const meta = AGENT_META[id];
- return (
-
-
-
- );
- })}
-
-
-
-
-
-
-
- );
-}
-
-/* ---------------------------------------------------------------------------
- * Window chrome
- * --------------------------------------------------------------------------- */
-
-function TrafficLights() {
- return (
-
-
-
-
-
- );
-}
-
-/* ---------------------------------------------------------------------------
- * Chat panel (left)
- * --------------------------------------------------------------------------- */
-
-function ChatPanel({
- AgentIcon,
- brandColor,
- agentLabel,
- phase,
- userTypedLen,
- agentTypedLen,
-}: {
- AgentIcon: typeof ClaudeIcon;
- brandColor: string | undefined;
- agentLabel: string;
- phase: Phase;
- userTypedLen: number;
- agentTypedLen: number;
-}) {
- const scrollRef = useRef(null);
- const userTyped = USER_MESSAGE.slice(0, userTypedLen);
- const userRest = USER_MESSAGE.slice(userTypedLen);
- const agentTyped = AGENT_STATUS.slice(0, agentTypedLen);
- const agentRest = AGENT_STATUS.slice(agentTypedLen);
-
- const showUserBubble = userTypedLen > 0;
- const showUserCaret = phase === 'user-typing';
- const showAgentLine =
- phase === 'agent-status' ||
- phase === 'tool-appear' ||
- phase === 'tool-filling' ||
- phase === 'tool-done' ||
- phase === 'hold';
- const showAgentCaret = phase === 'agent-status';
- const showToolCall =
- phase === 'tool-appear' ||
- phase === 'tool-filling' ||
- phase === 'tool-done' ||
- phase === 'hold';
- const showFollowUp = phase === 'tool-done' || phase === 'hold';
- const isResetting = phase === 'reset';
-
- useEffect(() => {
- if (phase === 'reset') return;
- const el = scrollRef.current;
- if (!el) return;
- const scroll = () => {
- const top = phase === 'rest' ? 0 : el.scrollHeight;
- el.scrollTo({ top, behavior: 'smooth' });
- };
- scroll();
- const t = window.setTimeout(scroll, 350);
- return () => window.clearTimeout(t);
- }, [phase]);
-
- return (
-
- {/* Chat sub-header — traffic lights + agent label, only on this side */}
-
-
- {/* User bubble */}
-
-
- {userTyped}
- {showUserCaret && (
-
- )}
-
- {userRest}
-
-
-
-
- {/* Agent status line — fades in at Beat 2 */}
-
-
-
- {agentTyped}
- {showAgentCaret && (
-
- )}
-
- {agentRest}
-
-
-
-
-
-
- {/* Agent follow-up message after the tool call lands */}
-
-
-
- Updated {DOC_PATH}.md — added a "Highlights"
- section with three wins and the daily activity chart.
-
-
-
-
- {/* Mock chat input — pinned below the scrolling messages region */}
-
-
- );
-}
-
-function ToolCallCard({
- phase,
- visible,
- isResetting,
-}: {
- phase: Phase;
- visible: boolean;
- isResetting: boolean;
-}) {
- const isPending = phase === 'tool-appear' || phase === 'tool-filling';
- const isDone = phase === 'tool-done' || phase === 'hold';
- const cardOpacity = isResetting ? 0 : !visible ? 0 : phase === 'tool-appear' ? 0.85 : 1;
-
- return (
-
-
-
- {TOOL_NAME}
-
- {/* Badge sizes to content — the right edge is pinned by the parent's
- justify-between so the tool name's left edge is stable. */}
- {isPending ? (
-
-
- Calling
-
- ) : (
-
-
- Done
-
- )}
-
- {/* Line 2 — reserved at zero opacity during pending so the card doesn't jump */}
-
- {DOC_PATH}.md
- {TOOL_SUMMARY}
-
-
- );
-}
-
-/* ---------------------------------------------------------------------------
- * Editor panel (right) — OpenKnowledge editor
- * --------------------------------------------------------------------------- */
-
-function EditorPanel({
- contentStep,
- AgentIcon,
- brandColor,
- agentLabel,
- animationDone,
-}: {
- contentStep: number;
- AgentIcon: typeof ClaudeIcon;
- brandColor: string | undefined;
- agentLabel: string;
- animationDone: boolean;
-}) {
- return (
-
-
- {/* Editor sub-header — row 1: URL bar; row 2: mode toggle + presence avatar */}
-
-
-
- https://openknowledge.ai/{DOC_PATH}
-
-
-
- {/* Icon-only on small screens; the full wordmark lockup on large. */}
-
-
-
-
- {animationDone ? : }
-
-
-
- {/* Fade beneath the header so document content scrolls in softly. */}
-
-
-
-
-
-
-
-
-
-
- {/* Word-count footer — pinned below the scrolling document body */}
-
- {/* Fade above the footer so document content dissolves into it. */}
-
-
- main
-
- {/* Invisible placeholder pins a min width; live stats overlay it. */}
-
-
- 76 words · 385 chars · ~97 tokens
-
-
-
-
-
-
- );
-}
-
-function HeroEditorReveal({ step, live }: { step: number; live: boolean }) {
- const { editor } = useOkEditor();
- const appliedStep = useRef(step);
- useEffect(() => {
- if (!editor) return;
- if (appliedStep.current !== step) {
- appliedStep.current = step;
- const json = parseEditorMarkdown(heroRevealMarkdown(step));
- queueMicrotask(() => {
- if (!editor.isDestroyed) editor.commands.setContent(json);
- });
- }
- editor.setEditable(live);
- }, [editor, step, live]);
- return null;
-}
-
-function HeroEditorStats() {
- const { words, chars, tokens } = useEditorDocStats();
- return (
-
- {words} words · {chars} chars · ~{tokens} tokens
-
- );
-}
-
-function PropertyRow({ label, value }: { label: string; value: React.ReactNode }) {
- return (
-
- {label}
- {value}
-
- );
-}
-
-function FrontmatterPanel() {
- const { mode } = useOkEditor();
- if (mode === 'source') return null;
- return (
-
-
-
- {HERO_FRONTMATTER.tags.map((t) => (
-
- #{t}
-
- ))}
-
- }
- />
-
- );
-}
-
-/* ---------------------------------------------------------------------------
- * ModeTogglePreview — static mirror of the editor's Visual/Markdown toggle.
- * Mirrors the real ToggleGroup with variant="segmented" size="sm". Pinned to Visual.
- * --------------------------------------------------------------------------- */
-
-function ModeTogglePreview() {
- return (
-
-
-
- Visual
-
-
-
- Markdown
-
-
- );
-}
-
-function TextboxIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function MarkdownIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/hero.tsx b/docs/src/app/(home)/sections/hero.tsx
deleted file mode 100644
index 466c176d5..000000000
--- a/docs/src/app/(home)/sections/hero.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-'use client';
-
-import Image from 'next/image';
-import Link from 'next/link';
-import { useState } from 'react';
-import { ClaudeIcon } from '@/components/icons/claude';
-import { CodexBrandIcon } from '@/components/icons/codex';
-import { CursorIcon } from '@/components/icons/cursor';
-import { DOWNLOAD_ROUTE, SITE_HEADLINE } from '@/lib/site';
-import { cn } from '@/lib/utils';
-import { DotTexture } from '../dot-texture';
-import { MarketingButton } from '../marketing-button';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-import { HeroPreview, type HeroPreviewAgentId } from './hero-preview';
-
-const AGENTS = [
- { id: 'claude', label: 'Claude', Icon: ClaudeIcon, brandColor: '#D97757' },
- { id: 'cursor', label: 'Cursor', Icon: CursorIcon, brandColor: 'var(--slide-text)' },
- { id: 'codex', label: 'Codex', Icon: CodexBrandIcon, brandColor: '#7A9DFF' },
-] as const satisfies ReadonlyArray<{
- id: HeroPreviewAgentId;
- label: string;
- Icon: typeof ClaudeIcon;
- brandColor: string | undefined;
-}>;
-
-type AgentId = (typeof AGENTS)[number]['id'];
-
-export function Hero() {
- const [activeId, setActiveId] = useState('claude');
-
- return (
-
-
-
-
-
- {SITE_HEADLINE}
-
-
-
-
- DOWNLOAD FOR MAC
-
-
- More platforms
-
-
-
-
-
-
- Use with
-
-
- {AGENTS.map(({ id, label, Icon, brandColor }) => (
- // biome-ignore lint/a11y/useSemanticElements: chip is a button styled with icon+label; would force a visually-hidden input + label workaround for the same role.
- setActiveId(id)}
- aria-checked={activeId === id}
- className={cn(
- 'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-semibold transition-colors',
- activeId === id
- ? 'bg-slide-bg-elevated text-slide-text shadow-sm ring-1 ring-slide-border/20'
- : 'text-slide-text/70 hover:text-slide-text',
- )}
- >
-
- {label}
-
- ))}
-
-
-
-
-
-
-
- {/* `flex-1 min-h-0` fills the card and hands a definite pixel height
- down the editor's `h-full` chain — more robust than a percentage
- chain (Safari in particular mishandles `height:100%` descendants of
- some sized boxes). Single-pane (
- {/* One shared preview: the editor + intro animation are a single
- instance, so switching agents only cross-fades the chat panel —
- the intro never replays and editor edits persist. */}
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/knowledge-base-graphic.tsx b/docs/src/app/(home)/sections/knowledge-base-graphic.tsx
deleted file mode 100644
index 112feb5b4..000000000
--- a/docs/src/app/(home)/sections/knowledge-base-graphic.tsx
+++ /dev/null
@@ -1,248 +0,0 @@
-import { Sparkles } from 'lucide-react';
-import type { CSSProperties } from 'react';
-
-const RINGS = [56, 82, 108] as const;
-const RING_CENTER = { x: 80, y: 45 };
-
-const CARD_BG = 'var(--slide-bg-elevated)';
-const CARD_RADIUS = '3.6cqw';
-const BACK_SHADOW = '0 1px 2px rgba(15, 23, 42, 0.05), 0 6px 18px -8px rgba(15, 23, 42, 0.12)';
-const FRONT_SHADOW = '0 1px 3px rgba(15, 23, 42, 0.07), 0 16px 34px -10px rgba(15, 23, 42, 0.18)';
-const MUTED_FILL = 'color-mix(in srgb, var(--slide-muted) 14%, transparent)';
-
-type Avatar = { label?: string; agent?: boolean; bg: string; fg: string };
-
-const AVATARS: Avatar[] = [
- { label: 'M', bg: '#f0ece3', fg: '#78716c' },
- { label: 'J', bg: 'var(--color-orange-light)', fg: '#b45309' },
- { agent: true, bg: 'var(--color-crystal-blue)', fg: 'var(--color-azure-blue)' },
- { label: 'K', bg: 'var(--color-purple-light)', fg: '#6d28d9' },
-];
-
-function BackgroundRings() {
- return (
-
- {RINGS.map((r) => (
-
- ))}
-
- );
-}
-
-function CardSkeletonLine({ width, delay }: { width: string; delay: string }) {
- return (
-
- );
-}
-
-function Avatars() {
- return (
-
- {AVATARS.map((a, i) => (
-
- {a.agent ? (
-
- ) : (
- a.label
- )}
-
- ))}
-
- );
-}
-
-function BackCard({
- title,
- align,
- style,
-}: {
- title: string;
- align: 'top' | 'bottom';
- style: CSSProperties;
-}) {
- return (
-
- );
-}
-
-export function KnowledgeBaseGraphic() {
- return (
-
-
-
-
-
-
-
-
-
-
-
- DOCS
-
-
- Deploy runbook
-
-
- Updated 2 hours ago
-
-
-
-
-
-
-
-
-
-
-
-
-
- Kept current by 12 people
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/made-for-agents-preview.tsx b/docs/src/app/(home)/sections/made-for-agents-preview.tsx
deleted file mode 100644
index affdb1864..000000000
--- a/docs/src/app/(home)/sections/made-for-agents-preview.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-'use client';
-
-import { Check, Loader2, Undo2 } from 'lucide-react';
-import { useEffect, useState } from 'react';
-import { ClaudeIcon } from '@/components/icons/claude';
-import { useIsInView } from '@/lib/use-is-in-view';
-import { usePrefersReducedMotion } from '@/lib/use-prefers-reduced-motion';
-
-const CLAUDE_MESSAGE = "I'll capture this as a decision doc.";
-const DONE_INTRO = 'Done — edit landed:';
-const DONE_PATH = 'decisions/api-strategy.md';
-const DONE_SUMMARY = 'Capture API choice decision.';
-
-const TOOL_PARAMS: ReadonlyArray<{ label: string; value: string }> = [
- { label: 'docName', value: 'decisions/api-strategy' },
- { label: 'position', value: 'replace' },
- { label: 'summary', value: 'Capture API choice decision' },
-];
-
-type TimelineEntry = {
- writer: string;
- timeAgo: string;
- summary: string;
- path: string;
-};
-
-const NEW_ENTRY: TimelineEntry = {
- writer: 'claude-code',
- timeAgo: 'just now',
- summary: 'Capture API choice decision',
- path: 'decisions/api-strategy',
-};
-
-const PRIOR_ENTRIES: ReadonlyArray = [
- {
- writer: 'claude-code',
- timeAgo: '18 min ago',
- summary: 'Add migration runbook section',
- path: 'runbooks/db-migrations',
- },
- {
- writer: 'claude-code',
- timeAgo: '1 hr ago',
- summary: 'Sync standup notes',
- path: 'team/standup-notes',
- },
-];
-
-const STEPS = {
- claudeMessage: 300,
- toolCard: 900,
- param0: 1200,
- param1: 1400,
- param2: 1600,
- calling: 2250,
- done: 2750,
- doneIntro: 3050,
- doneEntry: 3300,
- chatHoldEnd: 4500,
-
- timelineIn: 4700,
-
- newEntryIn: 5500,
- newEntryHighlightEnd: 6500,
-
- fadeOut: 8200,
- cycleEnd: 8800,
-} as const;
-
-const REVEAL_EASE = 'cubic-bezier(0.16, 1, 0.3, 1)';
-const REVEAL_MS = 480;
-const SCENE_MS = 520;
-
-function revealStyle(visible: boolean) {
- return {
- opacity: visible ? 1 : 0,
- transform: visible ? 'translateY(0)' : 'translateY(6px)',
- transition: `opacity ${REVEAL_MS}ms ${REVEAL_EASE}, transform ${REVEAL_MS}ms ${REVEAL_EASE}`,
- } as const;
-}
-
-function dropInStyle(visible: boolean) {
- return {
- opacity: visible ? 1 : 0,
- transform: visible ? 'translateY(0)' : 'translateY(-10px)',
- transition: `opacity ${REVEAL_MS}ms ${REVEAL_EASE}, transform ${REVEAL_MS}ms ${REVEAL_EASE}`,
- } as const;
-}
-
-function DropInNewEntry({ highlighted }: { highlighted: boolean }) {
- const [mounted, setMounted] = useState(false);
- useEffect(() => {
- const raf = requestAnimationFrame(() => setMounted(true));
- return () => cancelAnimationFrame(raf);
- }, []);
- return (
-
-
-
- );
-}
-
-function TimelineEntryRow({
- entry,
- highlighted = false,
-}: {
- entry: TimelineEntry;
- highlighted?: boolean;
-}) {
- return (
-
-
-
- {entry.writer}
-
- {entry.timeAgo}
-
-
-
-
-
-
• {entry.summary}
-
{entry.path}
-
-
- );
-}
-
-export function MadeForAgentsPreview() {
- const [elapsed, setElapsed] = useState(0);
- const [containerRef, inView] = useIsInView();
- const prefersReducedMotion = usePrefersReducedMotion();
-
- useEffect(() => {
- if (prefersReducedMotion) {
- setElapsed(STEPS.newEntryIn + 600);
- return;
- }
- if (!inView) return;
- let raf = 0;
- const start = performance.now();
- const step = (now: number) => {
- setElapsed((now - start) % STEPS.cycleEnd);
- raf = requestAnimationFrame(step);
- };
- raf = requestAnimationFrame(step);
- return () => cancelAnimationFrame(raf);
- }, [inView, prefersReducedMotion]);
-
- const beforeFadeOut = elapsed < STEPS.fadeOut;
- const inChatPhase = elapsed < STEPS.timelineIn;
- const inTimelinePhase = elapsed >= STEPS.timelineIn && beforeFadeOut;
-
- const showChat = inChatPhase && beforeFadeOut;
- const showTimeline = inTimelinePhase;
-
- const showClaude = elapsed >= STEPS.claudeMessage && inChatPhase;
- const showTool = elapsed >= STEPS.toolCard && inChatPhase;
- const paramVisible = [
- elapsed >= STEPS.param0 && inChatPhase,
- elapsed >= STEPS.param1 && inChatPhase,
- elapsed >= STEPS.param2 && inChatPhase,
- ] as const;
- const showCalling = elapsed >= STEPS.calling && elapsed < STEPS.done;
- const showDone = elapsed >= STEPS.done && inChatPhase;
- const showDoneIntro = elapsed >= STEPS.doneIntro && inChatPhase;
- const showDoneEntry = elapsed >= STEPS.doneEntry && inChatPhase;
-
- const showNewEntry = elapsed >= STEPS.newEntryIn && inTimelinePhase;
- const newEntryHighlighted = elapsed >= STEPS.newEntryIn && elapsed < STEPS.newEntryHighlightEnd;
-
- return (
-
-
-
-
- Claude
-
-
-
-
-
-
-
- Used
- open-knowledge:
- write
-
-
-
-
- Calling
-
-
-
- 200 ok
-
-
-
-
- {TOOL_PARAMS.map((p, i) => (
-
- {p.label}
- :
- {p.value}
-
- ))}
-
-
-
-
-
- {DONE_INTRO}
-
-
- 1.
- {DONE_PATH}
- —
- {DONE_SUMMARY}
-
-
-
-
-
-
- Timeline
-
-
-
- {showNewEntry && }
- {PRIOR_ENTRIES.map((entry) => (
-
- ))}
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/made-for-agents.tsx b/docs/src/app/(home)/sections/made-for-agents.tsx
deleted file mode 100644
index 38b9cdbe6..000000000
--- a/docs/src/app/(home)/sections/made-for-agents.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Hexagon, type LucideIcon, Plug, Search } from 'lucide-react';
-import Image from 'next/image';
-import { EXAMPLE_KB_SHARE_URL } from '@/lib/site';
-import { MarketingButton } from '../marketing-button';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-import { MadeForAgentsPreview } from './made-for-agents-preview';
-
-type Feature = {
- icon: LucideIcon;
- title: string;
- description: string;
-};
-
-const features: Feature[] = [
- {
- icon: Hexagon,
- title: 'Agent skills',
- description: 'Agents know how to navigate, edit and grow your knowledge base out of the box.',
- },
- {
- icon: Plug,
- title: 'Native MCP',
- description: 'Plug your knowledge base into Claude, Cursor, Codex, and other agents.',
- },
- {
- icon: Search,
- title: 'Agentic Search',
- description: 'Help agents find the right content with embeddings and hierarchical RAG.',
- },
-];
-
-function FeatureRow({ icon: Icon, title, description }: Feature) {
- return (
-
-
-
{title}
-
{description}
-
- );
-}
-
-export function MadeForAgents() {
- return (
-
-
-
-
-
- Made for agents.
-
-
- {features.map((feature) => (
-
- ))}
-
-
-
- Try a template
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/own-your-knowledge.tsx b/docs/src/app/(home)/sections/own-your-knowledge.tsx
deleted file mode 100644
index 0d75570bc..000000000
--- a/docs/src/app/(home)/sections/own-your-knowledge.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { GitFork, Laptop, Puzzle } from 'lucide-react';
-import FeatureItem from '../feature-item';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-
-const features = [
- {
- icon: Laptop,
- title: 'Local-first',
- description: 'Runs on your machine; private by design.',
- },
- {
- icon: GitFork,
- title: 'Open source',
- description: 'Auditable, self-hostable, & community-built.',
- },
- {
- icon: Puzzle,
- title: 'Platform-agnostic',
- description: 'Use with any agent and any tool.',
- },
-];
-
-export function OwnYourKnowledge() {
- return (
-
-
- Own your knowledge.
-
-
- {features.map((feature) => (
-
- ))}
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/put-it-to-work.tsx b/docs/src/app/(home)/sections/put-it-to-work.tsx
deleted file mode 100644
index 8217c2a52..000000000
--- a/docs/src/app/(home)/sections/put-it-to-work.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import type { ReactNode } from 'react';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-import { AgentBrainGraphic } from './agent-brain-graphic';
-import { EngSpecsGraphic } from './eng-specs-graphic';
-import { KnowledgeBaseGraphic } from './knowledge-base-graphic';
-
-type UseCaseCard = {
- visual: ReactNode;
- title: string;
- description: string;
-};
-
-const cards: UseCaseCard[] = [
- {
- visual: ,
- title: 'Agent brain',
- description:
- 'A shared knowledge base your agents read from and write to — the persistent memory behind every session.',
- },
- {
- visual: ,
- title: 'Knowledge base',
- description:
- 'A living wiki for you and your team — skills, docs, notes in plain markdown, kept automatically in sync.',
- },
- {
- visual: ,
- title: 'Engineering specs',
- description:
- 'Specs, PRDs, and roadmaps living next to the code, edited by humans and coding agents in the same loop.',
- },
-];
-
-export function PutItToWork() {
- return (
-
- Put it to work.
-
- {cards.map((card) => (
-
-
- {card.visual}
-
-
-
{card.title}
-
{card.description}
-
-
- ))}
-
-
- );
-}
diff --git a/docs/src/app/(home)/sections/rich-editing.tsx b/docs/src/app/(home)/sections/rich-editing.tsx
deleted file mode 100644
index 3ecca360d..000000000
--- a/docs/src/app/(home)/sections/rich-editing.tsx
+++ /dev/null
@@ -1,625 +0,0 @@
-'use client';
-
-import {
- BookOpen,
- ChevronRight,
- Code2,
- Compass,
- Image as ImageIcon,
- Info,
- LayoutPanelTop,
- MessageSquareWarning,
- Network,
- PenLine,
- Play,
- Video as VideoIcon,
- Workflow,
-} from 'lucide-react';
-import NextImage from 'next/image';
-import { useEffect, useRef, useState } from 'react';
-import { useIsInView } from '@/lib/use-is-in-view';
-import { usePrefersReducedMotion } from '@/lib/use-prefers-reduced-motion';
-import { cn } from '@/lib/utils';
-import { Section } from '../section';
-import SectionHeading from '../section-heading';
-
-function MockupCard({ children, className }: { children: React.ReactNode; className?: string }) {
- return (
- {children}
- );
-}
-
-function FeatureCard({
- title,
- description,
- children,
-}: {
- title: string;
- description: string;
- children: React.ReactNode;
-}) {
- return (
-
-
- {children}
-
-
-
{title}
-
{description}
-
-
- );
-}
-
-const BLOCK_CYCLE_MS = 2400;
-
-type BlockPreview = {
- label: string;
- description: string;
- render: () => React.ReactNode;
-};
-
-const BLOCK_PREVIEWS: BlockPreview[] = [
- {
- label: 'Callout',
- description: 'Highlight a tip, warning, or aside inline.',
- render: () => (
-
-
-
-
- Tip
-
-
- Pin key context next to the section that needs it.
-
-
- ),
- },
- {
- label: 'Accordion',
- description: 'Collapsible section with a clickable summary.',
- render: () => (
-
-
-
-
- Click to expand
-
-
-
- ),
- },
- {
- label: 'Tabs',
- description: 'Switch between content panes inline.',
- render: () => (
-
-
-
- Setup
-
-
Run
-
Ship
-
-
Install deps and run init.
-
- ),
- },
- {
- label: 'Mermaid',
- description: 'Render flowcharts and diagrams from text.',
- render: () => {
- const NODE_FILL = 'color-mix(in srgb, var(--slide-accent) 10%, white)';
- const NODE_STROKE = 'color-mix(in srgb, var(--slide-accent) 38%, white)';
- const ARROW = 'color-mix(in srgb, var(--slide-accent) 45%, white)';
- const TEXT = 'color-mix(in srgb, var(--slide-accent) 80%, black)';
- return (
-
-
- Flowchart preview
- {/* draft (rectangle) */}
-
-
- draft
-
- {/* arrow */}
-
-
- {/* review (diamond) */}
-
-
- review
-
- {/* arrow */}
-
-
- {/* ship (rectangle) */}
-
-
- ship
-
-
-
- );
- },
- },
- {
- label: 'Image',
- description: 'Drop in images with captions and resize controls.',
- render: () => (
-
-
-
- ),
- },
- {
- label: 'Video',
- description: 'Embed videos with playback controls inline.',
- render: () => (
-
- ),
- },
-];
-
-function BlockComponents() {
- const sections = [
- {
- heading: 'Components',
- items: [
- { icon: MessageSquareWarning, label: 'Callout' },
- { icon: ChevronRight, label: 'Accordion' },
- { icon: LayoutPanelTop, label: 'Tabs' },
- { icon: Workflow, label: 'Mermaid' },
- ],
- },
- {
- heading: 'Media',
- items: [
- { icon: ImageIcon, label: 'Image' },
- { icon: VideoIcon, label: 'Video' },
- ],
- },
- ];
-
- const [rootRef, inView] = useIsInView();
- const prefersReducedMotion = usePrefersReducedMotion();
- const [activeIdx, setActiveIdx] = useState(0);
- useEffect(() => {
- if (!inView) return;
- if (prefersReducedMotion) return;
- const id = window.setInterval(() => {
- setActiveIdx((i) => (i + 1) % BLOCK_PREVIEWS.length);
- }, BLOCK_CYCLE_MS);
- return () => window.clearInterval(id);
- }, [inView, prefersReducedMotion]);
-
- const activeLabel = BLOCK_PREVIEWS[activeIdx].label;
-
- const itemRefs = useRef>({});
- const scrollRef = useRef(null);
- useEffect(() => {
- const container = scrollRef.current;
- const item = itemRefs.current[activeLabel];
- if (!container || !item) return;
-
- if (activeLabel === BLOCK_PREVIEWS[0].label) {
- container.scrollTo({ top: 0, behavior: 'smooth' });
- return;
- }
-
- const itemTop = item.offsetTop;
- const itemBottom = itemTop + item.offsetHeight;
- const visibleTop = container.scrollTop;
- const visibleBottom = visibleTop + container.clientHeight;
-
- if (itemTop - visibleTop < 12) {
- container.scrollTo({ top: itemTop - 12, behavior: 'smooth' });
- } else if (itemBottom - visibleBottom > -12) {
- container.scrollTo({
- top: itemBottom - container.clientHeight + 12,
- behavior: 'smooth',
- });
- }
- }, [activeLabel]);
-
- return (
-
-
-
-
-
- {sections.map((section) => (
-
-
- {section.heading}
-
-
- {section.items.map(({ icon: Icon, label }) => {
- const active = label === activeLabel;
- return (
-
{
- itemRefs.current[label] = el;
- }}
- className={cn(
- 'flex items-center gap-2 rounded-md px-2 py-1 text-[11px]',
- active ? 'bg-slide-bg text-slide-text' : 'text-slide-muted',
- )}
- >
-
- {label}
-
- );
- })}
-
-
- ))}
-
-
-
-
- {/* Light inset card holds the active preview, centered both axes.
- Stacks all previews absolute; only the active one is opaque. */}
-
- {BLOCK_PREVIEWS.map((preview, i) => (
-
- ))}
-
-
- {BLOCK_PREVIEWS.map((preview, i) => (
-
- {preview.description}
-
- ))}
-
-
-
-
-
- );
-}
-
-const TEMPLATES = [
- { Icon: BookOpen, widths: ['w-3/4', 'w-1/2', 'w-2/3'] },
- { Icon: PenLine, widths: ['w-2/3', 'w-3/4', 'w-1/2'] },
- { Icon: Network, widths: ['w-1/2', 'w-2/3', 'w-3/5'] },
- { Icon: Compass, widths: ['w-3/5', 'w-1/2', 'w-2/3'] },
-] as const;
-
-const TEMPLATE_CYCLE_MS = 1800;
-
-function Templates() {
- const [ref, inView] = useIsInView();
- const prefersReducedMotion = usePrefersReducedMotion();
- const [activeIndex, setActiveIndex] = useState(1);
- useEffect(() => {
- if (!inView) return;
- if (prefersReducedMotion) return;
- const id = window.setInterval(() => {
- setActiveIndex((i) => (i + 1) % TEMPLATES.length);
- }, TEMPLATE_CYCLE_MS);
- return () => window.clearInterval(id);
- }, [inView, prefersReducedMotion]);
-
- return (
-
- {TEMPLATES.map(({ Icon, widths }, i) => {
- const active = i === activeIndex;
- return (
-
-
-
- {widths.map((w, j) => (
-
- ))}
-
-
- );
- })}
-
- );
-}
-
-function EmbedShell({ src, children }: { src: string; children: React.ReactNode }) {
- return (
-
-
-
- {src}
-
-
-
{children}
-
- );
-}
-
-/* ============================================================================
- * EMBEDDABLE HTML — Stock ticker
- * Google-Finance-style single-stock featured card: symbol bold top-left,
- * company name beneath in muted, signed % change in colored top-right, big
- * mono colored price below. Live behavior: price ticks on a mean-reverting
- * random walk; each tick briefly flashes the price (accent for up, rose for
- * down) and the displayed percent re-derives from price / previousClose.
- * ========================================================================== */
-type StockQuote = {
- symbol: string;
- name: string;
- price: number;
- percent: number;
-};
-
-const TICK_MS = 1400;
-const TICK_MAGNITUDE = 0.08;
-
-function useTickingPrice(base: number, magnitude: number, intervalMs: number, enabled: boolean) {
- const [price, setPrice] = useState(base);
- const prefersReducedMotion = usePrefersReducedMotion();
- useEffect(() => {
- if (!enabled) return;
- if (prefersReducedMotion) return;
- const id = window.setInterval(() => {
- setPrice((prev) => {
- const pull = (base - prev) * 0.35;
- const noise = (Math.random() - 0.5) * 2 * magnitude;
- return prev + pull + noise;
- });
- }, intervalMs);
- return () => window.clearInterval(id);
- }, [base, magnitude, intervalMs, enabled, prefersReducedMotion]);
- return price;
-}
-
-function RollingDigit({ value }: { value: number }) {
- return (
-
-
- {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((d) => (
-
- {d}
-
- ))}
-
-
- );
-}
-
-function RollingPrice({ value }: { value: number }) {
- const str = value.toFixed(2);
- return (
- <>
- ${str}
-
- $
- {str.split('').map((c, i) => {
- if (c === '.') {
- return (
- // biome-ignore lint/suspicious/noArrayIndexKey: stable position in fixed-format price string
- .
- );
- }
- return (
- // biome-ignore lint/suspicious/noArrayIndexKey: stable position in fixed-format price string
-
- );
- })}
-
- >
- );
-}
-
-function EmbedStockTicker({ stock }: { stock: StockQuote }) {
- const [ref, inView] = useIsInView();
- const previousClose = stock.price / (1 + stock.percent / 100);
- const livePrice = useTickingPrice(stock.price, TICK_MAGNITUDE, TICK_MS, inView);
- const livePercent = (livePrice / previousClose - 1) * 100;
-
- const isGain = livePercent >= 0;
- const deltaColor = isGain ? 'text-slide-accent' : 'text-rose-600 dark:text-rose-400';
- const sign = isGain ? '+' : '−';
-
- return (
-
-
-
-
-
- {stock.symbol}
-
-
- {stock.name}
-
-
-
- {sign}
- {Math.abs(livePercent).toFixed(2)}%
-
-
-
-
-
-
-
- );
-}
-
-const STOCK_INKP: StockQuote = {
- symbol: 'INKP',
- name: 'Inkeep, Inc.',
- price: 147.85,
- percent: 3.01,
-};
-
-function EmbeddableHtml() {
- return (
-
- );
-}
-
-function TemplateAnimationStyles() {
- return (
-
- );
-}
-
-export function RichEditing() {
- return (
-
-
-
- A canvas for your knowledge.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/site-nav.tsx b/docs/src/app/(home)/site-nav.tsx
deleted file mode 100644
index cdc7f012d..000000000
--- a/docs/src/app/(home)/site-nav.tsx
+++ /dev/null
@@ -1,299 +0,0 @@
-'use client';
-
-import { BookOpen, Menu, Star, X } from 'lucide-react';
-import Link from 'next/link';
-import type { FC, SVGProps } from 'react';
-import { useEffect, useState } from 'react';
-import { DiscordIcon } from '@/components/icons/discord';
-import { GitHubIcon } from '@/components/icons/github';
-import { XIcon } from '@/components/icons/x';
-import { LogoContextMenu } from '@/components/logo-context-menu';
-import { OkWordmark } from '@/components/ok-wordmark';
-import { Sheet, SheetClose, SheetContent, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
-import { DISCORD_URL, DOWNLOAD_ROUTE, GITHUB_URL, X_URL } from '@/lib/site';
-import { MarketingButton } from './marketing-button';
-
-type NavLink = {
- href: string;
- label: string;
- external: boolean;
- icon?: FC>;
- iconOnly?: boolean;
- desktopIconHidden?: boolean;
- showStars?: boolean;
-};
-
-const docsLink: NavLink = {
- href: '/docs',
- label: 'Docs',
- external: false,
- icon: BookOpen,
- desktopIconHidden: true,
-};
-
-const socialLinks: NavLink[] = [
- {
- href: X_URL,
- label: 'X (Twitter)',
- external: true,
- icon: XIcon,
- iconOnly: true,
- },
- {
- href: DISCORD_URL,
- label: 'Discord',
- external: true,
- icon: DiscordIcon,
- iconOnly: true,
- },
-];
-
-const githubLink: NavLink = {
- href: GITHUB_URL,
- label: 'GitHub',
- external: true,
- icon: GitHubIcon,
- showStars: true,
-};
-
-const mobileLinks: NavLink[] = [docsLink, ...socialLinks, githubLink];
-
-const starFormatter = new Intl.NumberFormat('en-US', {
- notation: 'compact',
- maximumFractionDigits: 1,
-});
-
-const fullStarFormatter = new Intl.NumberFormat('en-US');
-
-function NavLinkContent({ link, surface }: { link: NavLink; surface: 'desktop' | 'mobile' }) {
- const Icon = link.icon;
- const showIcon = !!Icon && (surface === 'mobile' || !link.desktopIconHidden);
- const showLabel = surface === 'mobile' || !link.iconOnly;
- return (
- <>
- {showIcon ? : null}
- {showLabel ? link.label : null}
- >
- );
-}
-
-function NavItem({
- link,
- className,
- surface,
- onSelect,
-}: {
- link: NavLink;
- className: string;
- surface: 'desktop' | 'mobile';
- onSelect?: () => void;
-}) {
- const ariaLabel = surface === 'desktop' && link.iconOnly ? link.label : undefined;
- return link.external ? (
-
-
-
- ) : (
-
-
-
- );
-}
-
-function StarCount({ stars }: { stars: number }) {
- return (
- <>
-
- {starFormatter.format(stars)}
- >
- );
-}
-
-function GitHubStarButton({
- link,
- stars,
- variant,
- onSelect,
-}: {
- link: NavLink;
- stars: number | null;
- variant: 'pill' | 'row';
- onSelect?: () => void;
-}) {
- const Icon = link.icon;
- const title = stars != null ? `${fullStarFormatter.format(stars)} GitHub stars` : undefined;
-
- if (variant === 'row') {
- return (
-
-
- {Icon ? : null}
- {link.label}
-
- {stars != null ? (
-
-
-
- ) : null}
-
- );
- }
-
- return (
-
-
- {Icon ? : null}
- {link.label}
-
- {stars != null ? (
-
-
-
- ) : null}
-
- );
-}
-
-export function SiteNav({ stars }: { stars: number | null }) {
- const [open, setOpen] = useState(false);
-
- useEffect(() => {
- const mq = window.matchMedia('(min-width: 768px)');
- const closeOnDesktop = () => {
- if (mq.matches) setOpen(false);
- };
- closeOnDesktop();
- mq.addEventListener('change', closeOnDesktop);
- return () => mq.removeEventListener('change', closeOnDesktop);
- }, []);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {socialLinks.map((link) => (
-
- ))}
-
-
-
- Download
-
-
-
-
-
-
-
-
-
-
- Site navigation
-
-
-
-
-
-
- {mobileLinks.map((link) =>
- link.showStars ? (
- setOpen(false)}
- />
- ) : (
- setOpen(false)}
- className="flex items-center gap-2 rounded-md px-3 py-2 text-slide-text transition-colors hover:bg-slide-bg-elevated"
- />
- ),
- )}
- setOpen(false)}
- >
- Download
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/d/[encoded]/splash-share-view.tsx b/docs/src/app/d/[encoded]/splash-share-view.tsx
index 7f976d906..49aaf936a 100644
--- a/docs/src/app/d/[encoded]/splash-share-view.tsx
+++ b/docs/src/app/d/[encoded]/splash-share-view.tsx
@@ -1,11 +1,11 @@
import { DotIcon, GitBranchIcon } from 'lucide-react';
import Link from 'next/link';
+import { DotTexture } from '@/components/dot-texture';
+import { SiteFooter } from '@/components/footer';
import { GitHubIcon } from '@/components/icons/github';
import { OkWordmark } from '@/components/ok-wordmark';
import { buildCloneCommand, SPLASH_INSTALL_COMMAND, type SplashView } from '@/lib/share-splash';
import { DOWNLOAD_ROUTE } from '@/lib/site';
-import { DotTexture } from '../../(home)/dot-texture';
-import { SiteFooter } from '../../(home)/footer';
import { SplashButtonLabel, splashPrimaryButton } from './splash-buttons';
import { SplashCliButton } from './splash-cli-button';
import { SplashCtaPanel } from './splash-cta-panel';
diff --git a/docs/src/app/provider.tsx b/docs/src/app/provider.tsx
index eb813cda0..aa0ff9f82 100644
--- a/docs/src/app/provider.tsx
+++ b/docs/src/app/provider.tsx
@@ -1,20 +1,13 @@
'use client';
import { RootProvider } from 'fumadocs-ui/provider/next';
import dynamic from 'next/dynamic';
-import { usePathname } from 'next/navigation';
import type { ReactNode } from 'react';
const SearchDialog = dynamic(() => import('@/components/inkeep-search-and-chat'), { ssr: false });
-const LIGHT_ONLY_ROUTES = new Set(['/']);
-
export function Provider({ children }: { children: ReactNode }) {
- const pathname = usePathname();
- const forcedTheme = LIGHT_ONLY_ROUTES.has(pathname) ? 'light' : undefined;
-
return (
) {
return (
@@ -20,60 +20,3 @@ export function CodexIcon(props: SVGProps) {
);
}
-
-export function CodexBrandIcon(props: SVGProps) {
- const gradId = `codex-brand-gradient-${useId().replace(/:/g, '')}`;
- return (
-
- Codex icon
-
-
-
-
-
-
-
-
-
- );
-}
-
-export function CodexBrandMonoIcon(props: SVGProps) {
- return (
-
- Codex icon
-
-
- );
-}
diff --git a/docs/src/components/logo-context-menu.tsx b/docs/src/components/logo-context-menu.tsx
deleted file mode 100644
index 22e71cd38..000000000
--- a/docs/src/components/logo-context-menu.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-'use client';
-
-import { ArrowRight, Check, Copy, Download } from 'lucide-react';
-import Link from 'next/link';
-import { type ReactNode, useState } from 'react';
-import {
- ContextMenu,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuSeparator,
- ContextMenuTrigger,
-} from '@/components/ui/context-menu';
-import { BRAND_ROUTE, PRIMARY_BRAND_ASSET } from '@/lib/brand-assets';
-import { copySvgToClipboard } from '@/lib/copy-svg';
-
-export function LogoContextMenu({ children }: { children: ReactNode }) {
- const [copied, setCopied] = useState(false);
-
- async function copy() {
- try {
- await copySvgToClipboard(PRIMARY_BRAND_ASSET.svg);
- setCopied(true);
- setTimeout(() => setCopied(false), 1500);
- } catch {}
- }
-
- return (
- <>
- {/* Always-mounted live region so the copy confirmation is announced to
- screen readers — a text swap on the already-focused menu item isn't
- reliably announced on its own. */}
-
- {copied ? 'Copied to clipboard' : ''}
-
-
- {children}
-
- {
- event.preventDefault();
- void copy();
- }}
- >
- {copied ? : }
- {copied ? 'Copied' : 'Copy as SVG'}
-
-
-
-
- Download SVG
-
-
-
-
-
- Download PNG
-
-
-
-
-
-
- All brand assets
-
-
-
-
- >
- );
-}
diff --git a/docs/src/app/(home)/marketing-button.tsx b/docs/src/components/marketing-button.tsx
similarity index 100%
rename from docs/src/app/(home)/marketing-button.tsx
rename to docs/src/components/marketing-button.tsx
diff --git a/docs/src/components/ok-editor/block-type-selector.tsx b/docs/src/components/ok-editor/block-type-selector.tsx
deleted file mode 100644
index 42b55eb8a..000000000
--- a/docs/src/components/ok-editor/block-type-selector.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-'use client';
-
-import type { Editor } from '@tiptap/core';
-import { useEditorState } from '@tiptap/react';
-import {
- ChevronDown,
- Heading1,
- Heading2,
- Heading3,
- List,
- ListOrdered,
- ListTodo,
- Pilcrow,
- Quote,
- SquareCode,
-} from 'lucide-react';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import { cn } from '@/lib/utils';
-
-type BlockType = {
- name: string;
- label: string;
- Icon: typeof Pilcrow;
- isActive: (e: Editor) => boolean;
- run: (e: Editor) => void;
-};
-
-const BLOCK_TYPES: BlockType[] = [
- {
- name: 'paragraph',
- label: 'Text',
- Icon: Pilcrow,
- isActive: (e) => e.isActive('paragraph') && !e.isActive('list'),
- run: (e) => e.chain().focus().setParagraph().run(),
- },
- {
- name: 'heading1',
- label: 'Heading 1',
- Icon: Heading1,
- isActive: (e) => e.isActive('heading', { level: 1 }),
- run: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(),
- },
- {
- name: 'heading2',
- label: 'Heading 2',
- Icon: Heading2,
- isActive: (e) => e.isActive('heading', { level: 2 }),
- run: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),
- },
- {
- name: 'heading3',
- label: 'Heading 3',
- Icon: Heading3,
- isActive: (e) => e.isActive('heading', { level: 3 }),
- run: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),
- },
- {
- name: 'bulletList',
- label: 'Bullet List',
- Icon: List,
- isActive: (e) => e.isActive('list', { ordered: false }),
- run: (e) => e.chain().focus().toggleBulletList().run(),
- },
- {
- name: 'orderedList',
- label: 'Ordered List',
- Icon: ListOrdered,
- isActive: (e) => e.isActive('list', { ordered: true }),
- run: (e) => e.chain().focus().toggleOrderedList().run(),
- },
- {
- name: 'taskList',
- label: 'Task List',
- Icon: ListTodo,
- isActive: (e) =>
- e.isActive('listItem', { checked: true }) || e.isActive('listItem', { checked: false }),
- run: (e) => e.chain().focus().toggleTaskList().run(),
- },
- {
- name: 'blockquote',
- label: 'Quote',
- Icon: Quote,
- isActive: (e) => e.isActive('blockquote'),
- run: (e) => e.chain().focus().toggleBlockquote().run(),
- },
- {
- name: 'codeBlock',
- label: 'Code Block',
- Icon: SquareCode,
- isActive: (e) => e.isActive('codeBlock'),
- run: (e) => e.chain().focus().toggleCodeBlock({ language: 'js' }).run(),
- },
-];
-
-const DEFAULT_TYPE = BLOCK_TYPES[0] as BlockType;
-
-export function BlockTypeSelector({ editor }: { editor: Editor }) {
- const { current, activeStates } = useEditorState({
- editor,
- selector: ({ editor: e }) => {
- const activeStates = Object.fromEntries(BLOCK_TYPES.map((b) => [b.name, b.isActive(e)]));
- const current = BLOCK_TYPES.find((b) => activeStates[b.name]) ?? DEFAULT_TYPE;
- return { current, activeStates };
- },
- });
- const CurrentIcon = current.Icon;
-
- return (
-
-
-
-
- {current.label}
-
-
-
-
- {BLOCK_TYPES.map((b) => {
- const Icon = b.Icon;
- return (
- e.preventDefault()}
- onSelect={() => b.run(editor)}
- >
-
- {b.label}
-
- );
- })}
-
-
- );
-}
diff --git a/docs/src/components/ok-editor/bubble-menu.tsx b/docs/src/components/ok-editor/bubble-menu.tsx
deleted file mode 100644
index 2913be65b..000000000
--- a/docs/src/components/ok-editor/bubble-menu.tsx
+++ /dev/null
@@ -1,169 +0,0 @@
-'use client';
-
-import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
-import { type Editor, posToDOMRect } from '@tiptap/core';
-import { useEditorState } from '@tiptap/react';
-import { BubbleMenu } from '@tiptap/react/menus';
-import { Bold, Code, Highlighter, Italic, Strikethrough, Underline } from 'lucide-react';
-import { useEffect, useRef } from 'react';
-import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
-import { cn } from '@/lib/utils';
-import { BlockTypeSelector } from './block-type-selector';
-
-const FORMAT_ACTIONS = [
- {
- name: 'Bold',
- Icon: Bold,
- run: (e: Editor) => e.chain().focus().toggleBold().run(),
- mark: 'strong',
- shortcut: '⌘B',
- },
- {
- name: 'Italic',
- Icon: Italic,
- run: (e: Editor) => e.chain().focus().toggleItalic().run(),
- mark: 'emphasis',
- shortcut: '⌘I',
- },
- {
- name: 'Underline',
- Icon: Underline,
- run: (e: Editor) => e.chain().focus().toggleUnderline().run(),
- mark: 'underline',
- shortcut: '⌘U',
- },
- {
- name: 'Strikethrough',
- Icon: Strikethrough,
- run: (e: Editor) => e.chain().focus().toggleStrike().run(),
- mark: 'strike',
- shortcut: '⌘⇧S',
- },
- {
- name: 'Code',
- Icon: Code,
- run: (e: Editor) => e.chain().focus().toggleCode().run(),
- mark: 'code',
- shortcut: '⌘E',
- },
- {
- name: 'Highlight',
- Icon: Highlighter,
- run: (e: Editor) => e.chain().focus().toggleHighlight().run(),
- mark: 'highlight',
- shortcut: '⌘⇧H',
- },
-] as const;
-
-function shouldShow({ editor }: { editor: Editor }): boolean {
- if (!editor.isEditable) return false;
- const { from, to, empty } = editor.state.selection;
- if (empty) return false;
- return Boolean(editor.state.doc.textBetween(from, to, ' ').trim());
-}
-
-export function OkBubbleMenu({ editor }: { editor: Editor }) {
- const menuRef = useRef(null);
- const stopAutoUpdate = useRef<(() => void) | null>(null);
-
- useEffect(() => () => stopAutoUpdate.current?.(), []);
-
- const active = useEditorState({
- editor,
- selector: ({ editor: e }): Record =>
- Object.fromEntries(FORMAT_ACTIONS.map((a) => [a.mark, e.isActive(a.mark)])),
- });
- const inCodeBlock = useEditorState({
- editor,
- selector: ({ editor: e }) => e.isActive('codeBlock'),
- });
-
- let contextElement: Element | undefined;
- try {
- contextElement = editor.view.dom;
- } catch {
- contextElement = undefined;
- }
-
- const anchor = {
- getBoundingClientRect: () => {
- try {
- const { from, to } = editor.state.selection;
- return posToDOMRect(editor.view, from, to);
- } catch {
- return new DOMRect();
- }
- },
- contextElement,
- };
-
- const onShow = () => {
- const popup = menuRef.current;
- if (!popup) return;
- stopAutoUpdate.current?.();
- stopAutoUpdate.current = autoUpdate(anchor, popup, () => {
- computePosition(anchor, popup, {
- placement: 'top',
- strategy: 'fixed',
- middleware: [offset(8), flip(), shift({ padding: 8 })],
- })
- .then(({ x, y }) => {
- if (!popup.isConnected) return;
- popup.style.position = 'fixed';
- popup.style.left = `${x}px`;
- popup.style.top = `${y}px`;
- })
- .catch(() => {});
- });
- };
-
- const onHide = () => {
- stopAutoUpdate.current?.();
- stopAutoUpdate.current = null;
- };
-
- return (
- document.body}
- options={{ onShow, onHide, strategy: 'fixed' }}
- className="ok-bubble-menu"
- >
-
- {/* Marks don't apply in code blocks — show only the block-type dropdown
- there (so the block can still be converted back to text). */}
- {!inCodeBlock && }
- {/* delayDuration 0 (the shadcn default) feels twitchy in a dense toolbar;
- a short delay reads as intentional hover. */}
- {!inCodeBlock && (
-
- {FORMAT_ACTIONS.map(({ name, Icon, run, mark, shortcut }) => (
-
-
- {
- e.preventDefault();
- run(editor);
- }}
- >
-
-
-
-
- {name}
- {shortcut}
-
-
- ))}
-
- )}
-
- );
-}
diff --git a/docs/src/components/ok-editor/drag-handle.tsx b/docs/src/components/ok-editor/drag-handle.tsx
deleted file mode 100644
index dd451784c..000000000
--- a/docs/src/components/ok-editor/drag-handle.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-'use client';
-
-import { offset } from '@floating-ui/dom';
-import { type Editor, Extension } from '@tiptap/core';
-import { DragHandlePlugin, normalizeNestedOptions } from '@tiptap/extension-drag-handle';
-import type { Node as PmNode } from '@tiptap/pm/model';
-import { TextSelection } from '@tiptap/pm/state';
-
-const HANDLE_HEIGHT = 24;
-const MAX_SINGLE_LINE_HEIGHT = 44;
-const BODY_LINE_HEIGHT = 26;
-
-const PLUS_SVG = ` `;
-const GRIP_SVG = ` `;
-
-function buildControls() {
- const container = document.createElement('div');
- container.className = 'ok-block-controls';
-
- const addBtn = document.createElement('button');
- addBtn.type = 'button';
- addBtn.className = 'ok-block-btn';
- addBtn.setAttribute('aria-label', 'Add block below');
- addBtn.innerHTML = PLUS_SVG;
- addBtn.addEventListener('mousedown', (e) => {
- e.preventDefault();
- e.stopPropagation();
- });
-
- const grip = document.createElement('button');
- grip.type = 'button';
- grip.className = 'ok-block-btn ok-block-grip';
- grip.setAttribute('aria-label', 'Drag to move');
- grip.setAttribute('tabindex', '-1');
- grip.innerHTML = GRIP_SVG;
-
- container.append(addBtn, grip);
- return { container, addBtn, grip };
-}
-
-function addBlockBelow(editor: Editor, pos: number, node: PmNode) {
- const { state, view } = editor;
- const insertAt = pos + node.nodeSize;
- if (insertAt > state.doc.content.size) return;
- const paragraph = state.schema.nodes.paragraph?.create();
- if (!paragraph) return;
- const tr = state.tr.insert(insertAt, paragraph);
- tr.setSelection(TextSelection.near(tr.doc.resolve(insertAt + 1))).scrollIntoView();
- view.dispatch(tr);
- view.focus();
- editor.commands.insertContent('/');
-}
-
-export const BlockDragHandle = Extension.create({
- name: 'okBlockDragHandle',
-
- addProseMirrorPlugins() {
- const editor = this.editor;
- let curNode: PmNode | null = null;
- let curPos = -1;
-
- const { container, addBtn, grip } = buildControls();
-
- addBtn.addEventListener('click', () => {
- if (curNode && curPos >= 0) addBlockBelow(editor, curPos, curNode);
- });
- grip.addEventListener('click', () => {
- if (curPos < 0) return;
- try {
- editor.chain().focus().setNodeSelection(curPos).run();
- } catch {}
- });
-
- return [
- DragHandlePlugin({
- editor,
- element: container,
- nestedOptions: normalizeNestedOptions(false),
- computePositionConfig: {
- placement: 'left-start',
- strategy: 'absolute',
- middleware: [
- offset(({ rects }) => {
- const firstLineHeight =
- rects.reference.height <= MAX_SINGLE_LINE_HEIGHT
- ? rects.reference.height
- : BODY_LINE_HEIGHT;
- return { mainAxis: 6, crossAxis: (firstLineHeight - HANDLE_HEIGHT) / 2 };
- }),
- ],
- },
- onNodeChange: ({ node, pos }) => {
- curNode = node;
- curPos = pos;
- },
- }).plugin,
- ];
- },
-});
diff --git a/docs/src/components/ok-editor/mode-toggle.tsx b/docs/src/components/ok-editor/mode-toggle.tsx
deleted file mode 100644
index b0f3c0525..000000000
--- a/docs/src/components/ok-editor/mode-toggle.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-'use client';
-
-export type EditorMode = 'visual' | 'source';
-
-export function ModeToggle({
- mode,
- onChange,
-}: {
- mode: EditorMode;
- onChange: (mode: EditorMode) => void;
-}) {
- return (
-
- onChange('visual')}
- >
-
- Visual
-
- onChange('source')}
- >
-
- Markdown
-
-
- );
-}
-
-function TextboxIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
-
-function MarkdownIcon({ className }: { className?: string }) {
- return (
-
-
-
- );
-}
diff --git a/docs/src/components/ok-editor/ok-editor.css b/docs/src/components/ok-editor/ok-editor.css
deleted file mode 100644
index 673a6e6a6..000000000
--- a/docs/src/components/ok-editor/ok-editor.css
+++ /dev/null
@@ -1,538 +0,0 @@
-/*
- * Scoped editor typography — ported from the real editor's canonical
- * ProseMirror rules in packages/app/src/globals.css and remapped onto the
- * docs site's design tokens (--slide-*, --color-gray-*, --primary, --border).
- * Everything is scoped under `.ok-editor-prose` (the class set on the
- * ProseMirror root via editorProps) so it never leaks to docs prose.
- *
- * Drift note: the source of truth for the editor's look is the app stylesheet.
- * If the app's `.ProseMirror` typography changes, re-sync the rules below.
- */
-
-.ok-editor-prose {
- outline: none;
- font-size: 0.9375rem;
- line-height: 1.7;
- color: var(--slide-text);
- /* Left margin room for the block drag handle (positioned left-start). */
- padding-left: 1.5rem;
-}
-
-/* Anchor for the absolutely-positioned drag handle (appended next to the
- ProseMirror root by DragHandlePlugin). */
-.ok-editor-visual {
- position: relative;
-}
-
-/* Block hover controls — "+" (add block) and a drag grip in the left margin.
- DragHandlePlugin toggles visibility/pointer-events; we only style looks. */
-.ok-block-controls {
- display: flex;
- align-items: center;
- gap: 0.0625rem;
-}
-.ok-block-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 1.125rem;
- height: 1.5rem;
- border-radius: 0.3rem;
- color: color-mix(in oklab, var(--slide-muted) 65%, transparent);
- cursor: pointer;
- transition:
- background-color 0.12s ease,
- color 0.12s ease;
-}
-.ok-block-btn:hover {
- background-color: color-mix(in oklab, var(--slide-text) 8%, transparent);
- color: var(--slide-text);
-}
-.ok-block-grip {
- cursor: grab;
-}
-.ok-block-controls[data-dragging="true"] .ok-block-grip {
- cursor: grabbing;
-}
-
-.ok-editor-prose p {
- margin-bottom: 0.5em;
-}
-
-/* Placeholder ("Type '/' for commands") — the Placeholder extension tags the
- focused empty node with `is-empty` + a `data-placeholder` attr. */
-.ok-editor-prose p.is-empty::before {
- content: attr(data-placeholder);
- pointer-events: none;
- float: left;
- height: 0;
- color: color-mix(in oklab, var(--slide-muted) 60%, transparent);
-}
-
-.ok-editor-prose > :first-child {
- margin-top: 0;
-}
-
-/* Headings */
-.ok-editor-prose h1,
-.ok-editor-prose h2,
-.ok-editor-prose h3 {
- font-weight: 600;
- margin-top: 1em;
- margin-bottom: 0.4em;
- color: var(--slide-text);
-}
-
-.ok-editor-prose > h1:first-child,
-.ok-editor-prose > h2:first-child,
-.ok-editor-prose > h3:first-child {
- margin-top: 0;
-}
-
-.ok-editor-prose h1 {
- font-size: 1.5em;
- letter-spacing: -0.04em;
-}
-.ok-editor-prose h2 {
- font-size: 1.25em;
- letter-spacing: -0.03em;
-}
-.ok-editor-prose h3 {
- font-size: 1.1em;
- letter-spacing: -0.02em;
-}
-
-/* Lists */
-.ok-editor-prose ul {
- list-style-type: disc;
- padding-left: 1.5em;
- margin-bottom: 0.5em;
-}
-
-.ok-editor-prose ol {
- list-style-type: decimal;
- padding-left: 2.5em;
- margin-bottom: 0.5em;
-}
-
-.ok-editor-prose li {
- margin-bottom: 0.15em;
-}
-
-.ok-editor-prose li > p {
- margin-bottom: 0.15em;
-}
-
-.ok-editor-prose ul ul {
- list-style-type: circle;
- margin-bottom: 0;
-}
-
-.ok-editor-prose ul ul ul {
- list-style-type: square;
-}
-
-/* Task lists — the unified List extension emits */
-.ok-editor-prose ul:has(> li[data-type="taskItem"]) {
- list-style: none;
- padding-left: 0;
-}
-
-.ok-editor-prose li[data-type="taskItem"] {
- display: flex;
- align-items: flex-start;
- gap: 0.5em;
- margin-bottom: 0.5em;
-}
-
-.ok-editor-prose li[data-type="taskItem"] > label {
- flex-shrink: 0;
- margin-top: 0.3em;
-}
-
-.ok-editor-prose li[data-type="taskItem"] > label input[type="checkbox"] {
- appearance: none;
- width: 1rem;
- height: 1rem;
- border: 1px solid var(--color-gray-300);
- border-radius: 3px;
- background: transparent;
- cursor: pointer;
- display: grid;
- place-content: center;
- margin: 0;
- transition:
- background-color 150ms,
- border-color 150ms;
-}
-
-.ok-editor-prose li[data-type="taskItem"] > label input[type="checkbox"]:checked {
- background-color: var(--primary);
- border-color: var(--primary);
-}
-
-.ok-editor-prose li[data-type="taskItem"] > label input[type="checkbox"]:checked::after {
- content: "";
- width: 0.55rem;
- height: 0.55rem;
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
- mask-size: contain;
- mask-repeat: no-repeat;
- background-color: #fff;
-}
-
-.ok-editor-prose li[data-type="taskItem"] > label input[type="checkbox"]:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: 2px;
-}
-
-.ok-editor-prose li[data-type="taskItem"] > div {
- flex: 1;
-}
-
-.ok-editor-prose li[data-type="taskItem"] > div p {
- margin-bottom: 0;
-}
-
-.ok-editor-prose li[data-type="taskItem"][data-checked="true"] > div {
- text-decoration: line-through;
- color: var(--color-gray-400);
-}
-
-/* Blockquote */
-.ok-editor-prose blockquote {
- border-left: 3px solid var(--color-gray-300);
- padding-left: 1em;
- margin-left: 0;
- margin-bottom: 0.5em;
- color: var(--color-gray-600);
-}
-
-/* Links */
-.ok-editor-prose a {
- color: var(--slide-accent-strong);
- text-decoration: underline;
- text-underline-offset: 2px;
-}
-
-/* Highlight mark */
-.ok-editor-prose mark {
- color: inherit;
- padding: 0.05em 0.25em;
- border-radius: 0.2em;
- background: color-mix(in oklab, #f59e0b 22%, transparent);
-}
-
-/* Inline code */
-.ok-editor-prose code {
- font-family: var(--font-mono);
- font-size: 0.875em;
- background: var(--color-gray-100);
- padding: 0.1em 0.35em;
- border-radius: 0.3em;
-}
-
-/* Code block fallback (until the preview NodeView lands) */
-.ok-editor-prose pre {
- background: var(--color-gray-100);
- padding: 0.75em 1em;
- margin-bottom: 0.5em;
- overflow-x: auto;
- font-family: var(--font-mono);
- font-size: 0.875em;
- line-height: 1.6;
- border-radius: 0.5rem;
-}
-
-.ok-editor-prose pre code {
- background: none;
- padding: 0;
- font-size: inherit;
-}
-
-/* Keyboard focus ring for the editor's interactive buttons (a11y). */
-.ok-block-btn:focus-visible,
-.ok-slash-item:focus-visible,
-.ok-mode-toggle-btn:focus-visible,
-.ok-bubble-btn:focus-visible,
-.ok-bubble-blocktype:focus-visible {
- outline: 2px solid var(--primary);
- outline-offset: 1px;
-}
-
-/* --- Visual / Markdown segmented toggle (mirrors ModeTogglePreview) ------ */
-
-.ok-mode-toggle {
- display: inline-flex;
- flex-shrink: 0;
- align-items: center;
- gap: 0.125rem;
- border-radius: 8px;
- padding: 0.125rem;
- background-color: color-mix(in srgb, var(--slide-text) 5%, transparent);
-}
-
-.ok-mode-toggle-btn {
- display: flex;
- height: 1.5rem;
- align-items: center;
- gap: 0.25rem;
- border-radius: 6px;
- padding: 0 0.375rem;
- font-family: var(--font-mono);
- font-size: 10px;
- font-weight: 500;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--slide-muted);
- cursor: pointer;
- transition:
- color 150ms,
- background-color 150ms;
-}
-
-.ok-mode-toggle-btn[data-active="true"] {
- color: var(--slide-text);
- background-color: var(--slide-bg-elevated);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
-}
-
-.ok-mode-toggle-icon {
- width: 0.75rem;
- height: 0.75rem;
- flex-shrink: 0;
-}
-
-/* --- Source pane -------------------------------------------------------- */
-
-.ok-source-host {
- min-height: 0;
-}
-
-/* --- html-preview embeds (chart / stat cards) --------------------------- */
-
-.ok-editor-prose .ok-embed-preview {
- position: relative;
- margin: 1.5em 0 0.75em;
-}
-
-/* Hover-revealed delete button (top-right of the embed). */
-.ok-embed-delete {
- position: absolute;
- top: 0.5rem;
- right: 0.5rem;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 1.5rem;
- height: 1.5rem;
- border-radius: 0.375rem;
- color: var(--slide-muted);
- background: color-mix(in srgb, var(--slide-bg-elevated) 82%, transparent);
- box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
- cursor: pointer;
- opacity: 0;
- transition:
- opacity 0.12s ease,
- background-color 0.12s ease,
- color 0.12s ease;
-}
-.ok-embed-preview:hover .ok-embed-delete,
-.ok-embed-delete:focus-visible {
- opacity: 1;
-}
-.ok-embed-delete:hover {
- color: #ef4444;
- background: color-mix(in srgb, #ef4444 12%, var(--slide-bg-elevated));
-}
-
-.ok-embed-iframe {
- display: block;
- width: 100%;
- /* Softer card border + a background nudged toward white — lighter, airier
- embeds (applies to both the line chart and the bar chart). */
- border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
- border-radius: 0.625rem;
- background: color-mix(in srgb, var(--slide-bg) 50%, var(--slide-bg-elevated));
-}
-
-.ok-editor-prose pre.ok-code-block {
- /* same surface as the generic code-block fallback above */
- background: var(--color-gray-100);
- border-radius: 0.5rem;
-}
-
-/* --- Reveal entrance — the newly landed block fades + rises in. The unified
- reveal appends one block per step, so the last child is the fresh one; only
- it animates (rebuilt earlier blocks appear instantly, no whole-doc re-pulse).
- Also gives a gentle fade when the user adds a block later.
- prefers-reduced-motion users get no movement. --- */
-@media (prefers-reduced-motion: no-preference) {
- .ok-editor-prose > *:last-child {
- animation: ok-reveal-in 360ms ease-out both;
- }
-}
-
-@keyframes ok-reveal-in {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
- to {
- opacity: 1;
- transform: none;
- }
-}
-
-/* --- Slash-command menu (popup mounts on document.body, so global) ------- */
-
-.ok-slash-popup {
- position: fixed;
- z-index: 60;
-}
-
-.ok-slash-menu {
- display: flex;
- flex-direction: column;
- gap: 1px;
- width: 200px;
- max-height: 280px;
- overflow-y: auto;
- padding: 0.25rem;
- border-radius: 0.625rem;
- background: var(--slide-bg-elevated);
- border: 1px solid var(--border);
- box-shadow:
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 8px 24px -8px rgba(15, 23, 42, 0.18);
- font-family: var(--font-inter, system-ui, sans-serif);
- /* Subtle thin scrollbar — native CSS (no @apply: this standalone file isn't
- connected to the docs Tailwind context, and `subtle-scrollbar` lives only
- in the app's globals.css). Mirrors that class's intent. */
- scrollbar-width: thin;
- scrollbar-color: color-mix(in oklab, var(--slide-text) 18%, transparent) transparent;
-}
-
-.ok-slash-item {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- width: 100%;
- padding: 0.375rem 0.5rem;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- text-align: left;
- color: var(--slide-text);
- cursor: pointer;
-}
-
-.ok-slash-item[data-active="true"] {
- background: color-mix(in srgb, var(--slide-text) 6%, transparent);
-}
-
-.ok-slash-item-icon {
- width: 0.875rem;
- height: 0.875rem;
- flex-shrink: 0;
- color: var(--slide-muted);
-}
-
-/* Selection bubble menu (inline format toolbar). Positioned fixed via
- floating-ui in bubble-menu.tsx; this styles only the appearance. */
-.ok-bubble-menu {
- display: flex;
- align-items: center;
- gap: 0.125rem;
- padding: 0.25rem;
- border-radius: 0.625rem;
- background: var(--slide-bg-elevated);
- border: 1px solid var(--border);
- box-shadow:
- 0 1px 2px rgba(15, 23, 42, 0.04),
- 0 8px 24px -8px rgba(15, 23, 42, 0.18);
- z-index: 60;
-}
-
-.ok-bubble-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 1.75rem;
- height: 1.75rem;
- border-radius: 0.375rem;
- color: var(--slide-text);
- cursor: pointer;
- transition:
- background-color 0.12s ease,
- color 0.12s ease;
-}
-.ok-bubble-btn:hover {
- background: color-mix(in srgb, var(--slide-text) 6%, transparent);
-}
-.ok-bubble-btn.is-active {
- background: color-mix(in srgb, var(--slide-accent-strong) 12%, transparent);
- color: var(--slide-accent-strong);
-}
-.ok-bubble-icon {
- width: 0.9375rem;
- height: 0.9375rem;
-}
-
-/* Block-type dropdown trigger (left of the format buttons) */
-.ok-bubble-blocktype {
- display: inline-flex;
- align-items: center;
- gap: 0.25rem;
- height: 1.75rem;
- padding: 0 0.375rem;
- border-radius: 0.375rem;
- font-size: 0.8125rem;
- font-weight: 500;
- white-space: nowrap;
- color: var(--slide-text);
- cursor: pointer;
- transition: background-color 0.12s ease;
-}
-.ok-bubble-blocktype:hover {
- background: color-mix(in srgb, var(--slide-text) 6%, transparent);
-}
-.ok-bubble-sep {
- width: 1px;
- align-self: stretch;
- margin: 0.25rem 0.0625rem;
- background: var(--slide-border);
-}
-
-.ok-slash-empty {
- width: 200px;
- padding: 0.5rem 0.625rem;
- border-radius: 0.625rem;
- background: var(--slide-bg-elevated);
- border: 1px solid var(--slide-border);
- font-size: 0.8125rem;
- color: var(--slide-muted);
-}
-
-/* Intro reveal — as each step writes content into the editor, the freshly-added
- block eases in (the agent "writing" effect). `setContent` diffs the DOM, so a
- CSS animation fires only when a node is first created and never replays on the
- reused, unchanged nodes. We target exactly the revealed content — the
- Highlights heading, each new task, and the chart — so the static intro text
- (which mounts first) doesn't animate. `> :last-child` is avoided because the
- editor keeps a trailing empty paragraph there. Scoped to `[data-revealing]`
- (intro only) so live edits don't animate; motion-gated for reduced motion.
- Kept last in the file so the base h2 / li / embed rules above retain ascending
- specificity (these `[data-revealing] …` selectors are higher-specificity). */
-@media (prefers-reduced-motion: no-preference) {
- [data-revealing] .ok-editor-prose h2,
- [data-revealing] .ok-editor-prose li:last-child,
- [data-revealing] .ok-editor-prose .ok-embed-preview {
- animation: ok-block-reveal 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
- }
-}
-@keyframes ok-block-reveal {
- from {
- opacity: 0;
- transform: translateY(7px);
- }
-}
diff --git a/docs/src/components/ok-editor/ok-editor.tsx b/docs/src/components/ok-editor/ok-editor.tsx
deleted file mode 100644
index 8899a28d9..000000000
--- a/docs/src/components/ok-editor/ok-editor.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-'use client';
-
-import type { EditorView } from '@codemirror/view';
-import { MarkdownManager, sharedExtensions } from '@inkeep/open-knowledge-core';
-import type { Editor } from '@tiptap/core';
-import { Placeholder, UndoRedo } from '@tiptap/extensions';
-import { EditorContent, useEditor, useEditorState } from '@tiptap/react';
-import dynamic from 'next/dynamic';
-import {
- createContext,
- type ReactNode,
- type RefObject,
- use,
- useEffect,
- useRef,
- useState,
-} from 'react';
-import { createPortal } from 'react-dom';
-import './ok-editor.css';
-import { OkBubbleMenu } from './bubble-menu';
-import { BlockDragHandle } from './drag-handle';
-import { type EditorMode, ModeToggle } from './mode-toggle';
-import { PreviewCodeBlock } from './preview-code-block';
-import { HERO_SEED_MARKDOWN } from './seed';
-import { SlashCommand } from './slash-command';
-
-const SourceEditor = dynamic(() => import('./source-editor').then((m) => m.SourceEditor), {
- ssr: false,
-});
-
-const markdownManager = new MarkdownManager({ extensions: sharedExtensions });
-const extensions = [
- ...sharedExtensions.map((ext) => (ext.name === 'codeBlock' ? PreviewCodeBlock : ext)),
- UndoRedo,
- SlashCommand,
- BlockDragHandle,
- Placeholder.configure({ placeholder: "Type '/' for commands", showOnlyCurrent: true }),
-];
-
-export function parseEditorMarkdown(markdown: string) {
- return markdownManager.parse(markdown);
-}
-
-export interface EditorDocStats {
- words: number;
- chars: number;
- tokens: number;
-}
-
-const EMPTY_STATS: EditorDocStats = { words: 0, chars: 0, tokens: 0 };
-
-function computeDocStats(editor: Editor): EditorDocStats {
- let text = '';
- let chars = 0;
- editor.state.doc.descendants((node) => {
- if (node.type.name === 'codeBlock') {
- const meta = typeof node.attrs.meta === 'string' ? node.attrs.meta : '';
- if (node.attrs.language === 'html' && /\bpreview\b/.test(meta)) return false;
- }
- if (node.isText) {
- const s = node.text ?? '';
- text += s;
- chars += s.length;
- return true;
- }
- if (node.type.isBlock && text.length > 0 && !text.endsWith('\n')) text += '\n';
- return true;
- });
- const words = text.trim() ? (text.trim().match(/\S+/g)?.length ?? 0) : 0;
- return { words, chars, tokens: Math.ceil(chars / 4) };
-}
-
-export function useEditorDocStats(): EditorDocStats {
- const { editor } = useOkEditor();
- return (
- useEditorState({
- editor,
- selector: ({ editor }) => (editor ? computeDocStats(editor) : EMPTY_STATS),
- }) ?? EMPTY_STATS
- );
-}
-
-interface OkEditorContextValue {
- editor: Editor | null;
- mode: EditorMode;
- switchMode: (mode: EditorMode) => void;
- sourceDoc: string;
- sourceViewRef: RefObject;
-}
-
-const OkEditorContext = createContext(null);
-
-export function useOkEditor(): OkEditorContextValue {
- const ctx = use(OkEditorContext);
- if (!ctx) throw new Error('OkEditor components must be rendered inside ');
- return ctx;
-}
-
-export function OkEditorProvider({
- children,
- initialMarkdown,
- frontmatter,
-}: {
- children: ReactNode;
- initialMarkdown?: string;
- /** Optional YAML frontmatter block (`---\n…\n---`) revealed above the body in
- * Markdown mode. The body editor stays frontmatter-free (the property panel is
- * its display chrome); this only round-trips through the source view. */
- frontmatter?: string;
-}) {
- const [mode, setMode] = useState('visual');
- const [sourceDoc, setSourceDoc] = useState('');
- const sourceViewRef = useRef(null);
- const [initialContent] = useState(() =>
- markdownManager.parse(initialMarkdown ?? HERO_SEED_MARKDOWN),
- );
-
- const editor = useEditor({
- extensions,
- content: initialContent,
- editable: true,
- immediatelyRender: false,
- editorProps: {
- attributes: { class: 'ok-editor-prose focus:outline-none', spellcheck: 'false' },
- },
- });
-
- function switchMode(next: EditorMode) {
- if (next === mode || !editor) return;
- if (next === 'source') {
- const body = markdownManager.serialize(editor.getJSON());
- setSourceDoc(frontmatter ? `${frontmatter}\n\n${body}` : body);
- } else {
- const raw = sourceViewRef.current?.state.doc.toString() ?? sourceDoc;
- const body = raw.replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n)*/, '');
- editor.commands.setContent(markdownManager.parse(body));
- }
- setMode(next);
- }
-
- return (
-
- {children}
-
- );
-}
-
-export function OkEditorModeToggle() {
- const { mode, switchMode } = useOkEditor();
- return ;
-}
-
-export function OkEditorBody() {
- const { editor, mode, sourceDoc, sourceViewRef } = useOkEditor();
-
- const visualHostRef = useRef(null);
- const [visualHost, setVisualHost] = useState(null);
- useEffect(() => {
- setVisualHost(visualHostRef.current);
- }, []);
-
- return (
- <>
-
- {visualHost &&
- createPortal(
- // biome-ignore lint/plugin/no-unportaled-editor-content: single non-pooled editor portalled into an exclusively-owned target — the rule's canonical shape; no Activity pool / editor cache in this marketing preview.
- ,
- visualHost,
- )}
- {editor && mode === 'visual' && }
- {mode === 'source' && }
- >
- );
-}
diff --git a/docs/src/components/ok-editor/preview-code-block.tsx b/docs/src/components/ok-editor/preview-code-block.tsx
deleted file mode 100644
index 0c114fd9a..000000000
--- a/docs/src/components/ok-editor/preview-code-block.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-'use client';
-
-import { CodeBlockFidelity, PREVIEW_THEME_TOKENS } from '@inkeep/open-knowledge-core';
-import {
- NodeViewContent,
- type NodeViewProps,
- NodeViewWrapper,
- ReactNodeViewRenderer,
-} from '@tiptap/react';
-import { Trash2 } from 'lucide-react';
-
-const THEME_VARS = PREVIEW_THEME_TOKENS.map((t) => `${t.name}:${t.light}`).join(';');
-
-const CHART_PALETTE = [
- '--chart-1:#93c5fd',
- '--chart-2:#60a5fa',
- '--chart-3:#3784ff',
- '--chart-4:#2563eb',
- '--chart-5:#1d4ed8',
-].join(';');
-
-function buildSrcDoc(html: string): string {
- return ` ${html}`;
-}
-
-const DEFAULT_PREVIEW_HEIGHT = 230;
-
-function previewHeight(meta: string): number {
- const m = /\bh=(\d+)\b/.exec(meta);
- return m ? Number(m[1]) : DEFAULT_PREVIEW_HEIGHT;
-}
-
-function PreviewCodeBlockView({ node, deleteNode }: NodeViewProps) {
- const language = typeof node.attrs.language === 'string' ? node.attrs.language : '';
- const meta = typeof node.attrs.meta === 'string' ? node.attrs.meta : '';
- const isPreview = language === 'html' && /\bpreview\b/.test(meta);
-
- if (isPreview) {
- return (
-
-
- {/* The embed isn't editable, but it can be removed. Hover-revealed
- delete button (top-right); mousedown-preventDefault keeps the editor
- from grabbing a selection before the click deletes the node. */}
- e.preventDefault()}
- onClick={() => deleteNode()}
- >
-
-
- {/* The code text stays in the document (round-trips to markdown) but is
- hidden while the preview renders. */}
-
-
-
-
- );
- }
-
- return (
-
-
-
-
-
- );
-}
-
-export const PreviewCodeBlock = CodeBlockFidelity.extend({
- addNodeView() {
- return ReactNodeViewRenderer(PreviewCodeBlockView);
- },
-});
diff --git a/docs/src/components/ok-editor/seed.ts b/docs/src/components/ok-editor/seed.ts
deleted file mode 100644
index 8067d83b5..000000000
--- a/docs/src/components/ok-editor/seed.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-const HERO_INTRO = `# Launch week recap
-
-v2.0 went public on June 3 — the end of a quiet QA window and the start of launch week.
-
-47 PRs merged across the cycle. Activity stayed close to zero through QA, then spiked on launch day as the announcement went live.`;
-
-const HERO_TASKS = [
- '- [ ] Shipped v2.0 to public on Jun 3',
- '- [ ] 1.4k new signups in the first 24 hours',
- '- [ ] Hit #1 on Product Hunt and front of Hacker News',
-];
-
-export const HERO_FRONTMATTER = {
- title: 'Launch week recap',
- tags: ['launch', 'retro', 'v2'],
-} as const;
-
-export const HERO_FRONTMATTER_YAML = [
- '---',
- `title: ${HERO_FRONTMATTER.title}`,
- 'tags:',
- ...HERO_FRONTMATTER.tags.map((t) => ` - ${t}`),
- '---',
-].join('\n');
-
-const HERO_CHART_HTML = `
-
PRs merged per day · last 30 days
-
-
-
`;
-
-export function heroRevealMarkdown(step: number): string {
- let md = HERO_INTRO;
- if (step >= 1) md += '\n\n## Highlights';
- const tasks = step <= 1 ? 0 : Math.min(HERO_TASKS.length, step - 1);
- if (tasks > 0) md += `\n\n${HERO_TASKS.slice(0, tasks).join('\n')}`;
- if (step >= 5) md += `\n\n\`\`\`html preview h=224\n${HERO_CHART_HTML}\n\`\`\``;
- return md;
-}
-
-export const HERO_SEED_MARKDOWN = heroRevealMarkdown(5);
diff --git a/docs/src/components/ok-editor/slash-command.tsx b/docs/src/components/ok-editor/slash-command.tsx
deleted file mode 100644
index 89a3220a6..000000000
--- a/docs/src/components/ok-editor/slash-command.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-'use client';
-
-import { autoUpdate, computePosition, flip, offset, shift } from '@floating-ui/dom';
-import { Extension } from '@tiptap/core';
-import { PluginKey } from '@tiptap/pm/state';
-import { ReactRenderer } from '@tiptap/react';
-import Suggestion, { type SuggestionKeyDownProps, type SuggestionProps } from '@tiptap/suggestion';
-import { filterSlashItems, type SlashItem } from './slash-items';
-import { SlashMenu, type SlashMenuProps } from './slash-menu';
-
-const slashKey = new PluginKey('okSlashCommand');
-
-export const SlashCommand = Extension.create({
- name: 'okSlashCommand',
-
- addProseMirrorPlugins() {
- return [
- Suggestion({
- editor: this.editor,
- pluginKey: slashKey,
- char: '/',
- items: ({ query }) => filterSlashItems(query),
- command: ({ editor, range, props: item }) => {
- editor.chain().focus().deleteRange(range).run();
- item.command(editor);
- },
- render: () => {
- let renderer: ReactRenderer | null = null;
- let popup: HTMLElement | null = null;
- let current: SuggestionProps | null = null;
- let selectedIndex = 0;
- let stopAutoUpdate: (() => void) | null = null;
-
- const anchor = {
- getBoundingClientRect: () => current?.clientRect?.() ?? new DOMRect(),
- get contextElement() {
- return current?.editor.view.dom;
- },
- };
-
- const reposition = () => {
- if (!popup) return;
- computePosition(anchor, popup, {
- strategy: 'fixed',
- placement: 'bottom-start',
- middleware: [offset(6), flip(), shift({ padding: 8 })],
- }).then(({ x, y }) => {
- if (!popup) return;
- popup.style.left = `${x}px`;
- popup.style.top = `${y}px`;
- const caret = current?.clientRect?.();
- const onScreen = !!caret && caret.bottom > 0 && caret.top < window.innerHeight;
- popup.style.visibility = onScreen ? 'visible' : 'hidden';
- });
- };
-
- const update = () => {
- if (!renderer || !current) return;
- renderer.updateProps({
- items: current.items,
- selectedIndex,
- onSelect: current.command,
- onHoverIndex: (i: number) => {
- selectedIndex = i;
- update();
- },
- });
- };
-
- return {
- onStart(props: SuggestionProps) {
- current = props;
- selectedIndex = 0;
- popup = document.createElement('div');
- popup.className = 'ok-slash-popup';
- popup.style.visibility = 'hidden'; // revealed once positioned
- document.body.appendChild(popup);
- renderer = new ReactRenderer(SlashMenu, {
- props: {
- items: props.items,
- selectedIndex,
- onSelect: props.command,
- onHoverIndex: (i: number) => {
- selectedIndex = i;
- update();
- },
- },
- editor: props.editor,
- });
- popup.appendChild(renderer.element);
- stopAutoUpdate = autoUpdate(anchor, popup, reposition);
- },
- onUpdate(props: SuggestionProps) {
- current = props;
- selectedIndex = Math.min(selectedIndex, Math.max(0, props.items.length - 1));
- update();
- reposition();
- },
- onKeyDown({ event }: SuggestionKeyDownProps) {
- if (!current || current.items.length === 0) return false;
- const items = current.items;
- if (event.key === 'ArrowDown') {
- selectedIndex = (selectedIndex + 1) % items.length;
- update();
- return true;
- }
- if (event.key === 'ArrowUp') {
- selectedIndex = (selectedIndex - 1 + items.length) % items.length;
- update();
- return true;
- }
- if (event.key === 'Enter' || event.key === 'Tab') {
- const item = items[selectedIndex];
- if (item) current.command(item);
- return true;
- }
- return false;
- },
- onExit() {
- stopAutoUpdate?.();
- renderer?.destroy();
- popup?.remove();
- stopAutoUpdate = null;
- renderer = null;
- popup = null;
- current = null;
- selectedIndex = 0;
- },
- };
- },
- }),
- ];
- },
-});
diff --git a/docs/src/components/ok-editor/slash-items.tsx b/docs/src/components/ok-editor/slash-items.tsx
deleted file mode 100644
index 6369797c9..000000000
--- a/docs/src/components/ok-editor/slash-items.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-'use client';
-
-import type { Editor } from '@tiptap/react';
-import {
- BarChart3,
- Code2,
- Heading1,
- Heading2,
- Heading3,
- List,
- ListOrdered,
- ListTodo,
- type LucideIcon,
- Minus,
- Quote,
-} from 'lucide-react';
-
-export interface SlashItem {
- name: string;
- label: string;
- icon: LucideIcon;
- aliases?: string[];
- command: (editor: Editor) => void;
-}
-
-const CHART_HTML = `
-
Revenue by region
-
-
-
`;
-
-const SLASH_ITEMS: readonly SlashItem[] = [
- {
- name: 'heading1',
- label: 'Heading 1',
- icon: Heading1,
- aliases: ['h1', 'title'],
- command: (e) => e.chain().focus().toggleHeading({ level: 1 }).run(),
- },
- {
- name: 'heading2',
- label: 'Heading 2',
- icon: Heading2,
- aliases: ['h2'],
- command: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),
- },
- {
- name: 'heading3',
- label: 'Heading 3',
- icon: Heading3,
- aliases: ['h3'],
- command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),
- },
- {
- name: 'bulletList',
- label: 'Bullet List',
- icon: List,
- aliases: ['ul', 'unordered'],
- command: (e) => e.chain().focus().toggleBulletList().run(),
- },
- {
- name: 'orderedList',
- label: 'Numbered List',
- icon: ListOrdered,
- aliases: ['ol', 'ordered', 'numbered'],
- command: (e) => e.chain().focus().toggleOrderedList().run(),
- },
- {
- name: 'taskList',
- label: 'Task List',
- icon: ListTodo,
- aliases: ['todo', 'checklist', 'checkbox'],
- command: (e) => e.chain().focus().toggleTaskList().run(),
- },
- {
- name: 'blockquote',
- label: 'Quote',
- icon: Quote,
- aliases: ['quote'],
- command: (e) => e.chain().focus().toggleBlockquote().run(),
- },
- {
- name: 'codeBlock',
- label: 'Code Block',
- icon: Code2,
- aliases: ['code', 'fence'],
- command: (e) => e.chain().focus().toggleCodeBlock({ language: 'js' }).run(),
- },
- {
- name: 'separator',
- label: 'Separator',
- icon: Minus,
- aliases: ['hr', 'divider', 'rule'],
- command: (e) => e.chain().focus().setHorizontalRule().run(),
- },
- {
- name: 'chart',
- label: 'Chart',
- icon: BarChart3,
- aliases: ['embed', 'graph', 'html'],
- command: (e) =>
- e
- .chain()
- .focus()
- .insertContent({
- type: 'codeBlock',
- attrs: { language: 'html', meta: 'preview h=230' },
- content: [{ type: 'text', text: CHART_HTML }],
- })
- .run(),
- },
-];
-
-export function filterSlashItems(query: string): SlashItem[] {
- if (!query) return [...SLASH_ITEMS];
- const q = query.toLowerCase();
- return SLASH_ITEMS.filter(
- (item) =>
- item.label.toLowerCase().includes(q) ||
- item.name.toLowerCase().includes(q) ||
- item.aliases?.some((a) => a.toLowerCase().includes(q)),
- );
-}
diff --git a/docs/src/components/ok-editor/slash-menu.tsx b/docs/src/components/ok-editor/slash-menu.tsx
deleted file mode 100644
index 71cb72a6d..000000000
--- a/docs/src/components/ok-editor/slash-menu.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-'use client';
-
-import type { SlashItem } from './slash-items';
-
-export interface SlashMenuProps {
- items: SlashItem[];
- selectedIndex: number;
- onSelect: (item: SlashItem) => void;
- onHoverIndex: (index: number) => void;
-}
-
-export function SlashMenu({ items, selectedIndex, onSelect, onHoverIndex }: SlashMenuProps) {
- if (items.length === 0) {
- return (
-
- No results
-
- );
- }
- return (
-
- {items.map((item, i) => {
- const Icon = item.icon;
- return (
- {
- e.preventDefault();
- onSelect(item);
- }}
- onMouseEnter={() => onHoverIndex(i)}
- >
-
- {item.label}
-
- );
- })}
-
- );
-}
diff --git a/docs/src/components/ok-editor/source-editor.tsx b/docs/src/components/ok-editor/source-editor.tsx
deleted file mode 100644
index d129ba181..000000000
--- a/docs/src/components/ok-editor/source-editor.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-'use client';
-
-import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
-import { markdown } from '@codemirror/lang-markdown';
-import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
-import { EditorState } from '@codemirror/state';
-import { EditorView, keymap } from '@codemirror/view';
-import { type RefObject, useEffect, useRef } from 'react';
-
-const cmTheme = EditorView.theme({
- '&': { backgroundColor: 'transparent', color: 'var(--slide-text)' },
- '&.cm-focused': { outline: 'none' },
- '.cm-content': {
- fontFamily: 'var(--font-mono)',
- fontSize: '0.8125rem',
- lineHeight: '1.7',
- padding: '0',
- caretColor: 'var(--slide-text)',
- },
- '.cm-line': { padding: '0' },
- '.cm-cursor': { borderLeftColor: 'var(--slide-text)' },
- '.cm-selectionBackground, &.cm-focused .cm-selectionBackground': {
- backgroundColor: 'color-mix(in oklab, var(--primary) 18%, transparent)',
- },
-});
-
-export function SourceEditor({
- initialDoc,
- viewRef,
-}: {
- initialDoc: string;
- viewRef: RefObject;
-}) {
- const hostRef = useRef(null);
-
- // biome-ignore lint/correctness/useExhaustiveDependencies: mount-once — parent re-mounts with a fresh initialDoc each source-mode entry; viewRef is a stable ref. Reacting would rebuild the CodeMirror view mid-edit.
- useEffect(() => {
- const host = hostRef.current;
- if (!host) return;
- const view = new EditorView({
- parent: host,
- state: EditorState.create({
- doc: initialDoc,
- extensions: [
- history(),
- keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
- markdown(),
- syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
- EditorView.lineWrapping,
- cmTheme,
- ],
- }),
- });
- viewRef.current = view;
- view.focus();
- return () => {
- view.destroy();
- viewRef.current = null;
- };
- }, []);
-
- return
;
-}
diff --git a/docs/src/components/ok-icon.tsx b/docs/src/components/ok-icon.tsx
deleted file mode 100644
index f42423911..000000000
--- a/docs/src/components/ok-icon.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { type SVGProps, useId } from 'react';
-
-export function OkIcon(props: SVGProps) {
- const filterId = useId();
- return (
-
- OpenKnowledge
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/docs/src/app/(home)/section-heading.tsx b/docs/src/components/section-heading.tsx
similarity index 100%
rename from docs/src/app/(home)/section-heading.tsx
rename to docs/src/components/section-heading.tsx
diff --git a/docs/src/components/slide-page.tsx b/docs/src/components/slide-page.tsx
index 3cf42fcd2..ed9107e47 100644
--- a/docs/src/components/slide-page.tsx
+++ b/docs/src/components/slide-page.tsx
@@ -1,7 +1,7 @@
import Link from 'next/link';
import type { ComponentProps, ReactNode } from 'react';
-import { DotTexture } from '@/app/(home)/dot-texture';
-import { SiteFooter } from '@/app/(home)/footer';
+import { DotTexture } from '@/components/dot-texture';
+import { SiteFooter } from '@/components/footer';
import { OkWordmark } from '@/components/ok-wordmark';
import { cn } from '@/lib/utils';
diff --git a/docs/src/components/subscribe-form.tsx b/docs/src/components/subscribe-form.tsx
index 0d4f8d430..7d9197d83 100644
--- a/docs/src/components/subscribe-form.tsx
+++ b/docs/src/components/subscribe-form.tsx
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { z } from 'zod';
-import { MarketingButton } from '@/app/(home)/marketing-button';
+import { MarketingButton } from '@/components/marketing-button';
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
diff --git a/docs/src/components/ui/context-menu.tsx b/docs/src/components/ui/context-menu.tsx
deleted file mode 100644
index 0e7362188..000000000
--- a/docs/src/components/ui/context-menu.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-'use client';
-
-import { ContextMenu as ContextMenuPrimitive } from 'radix-ui';
-import type * as React from 'react';
-
-import { cn } from '@/lib/utils';
-
-function ContextMenu({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function ContextMenuTrigger({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function ContextMenuGroup({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function ContextMenuContent({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- );
-}
-
-function ContextMenuItem({
- className,
- inset,
- variant = 'default',
- ...props
-}: React.ComponentProps & {
- inset?: boolean;
- variant?: 'default' | 'destructive';
-}) {
- return (
-
- );
-}
-
-function ContextMenuLabel({
- className,
- inset,
- ...props
-}: React.ComponentProps & {
- inset?: boolean;
-}) {
- return (
-
- );
-}
-
-function ContextMenuSeparator({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export {
- ContextMenu,
- ContextMenuContent,
- ContextMenuGroup,
- ContextMenuItem,
- ContextMenuLabel,
- ContextMenuSeparator,
- ContextMenuTrigger,
-};
diff --git a/docs/src/components/ui/sheet.tsx b/docs/src/components/ui/sheet.tsx
deleted file mode 100644
index 3f9e7be82..000000000
--- a/docs/src/components/ui/sheet.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-'use client';
-
-import { XIcon } from 'lucide-react';
-import { Dialog as SheetPrimitive } from 'radix-ui';
-import type * as React from 'react';
-
-import { cn } from '@/lib/utils';
-
-function Sheet({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function SheetTrigger({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function SheetClose({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function SheetPortal({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function SheetOverlay({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SheetContent({
- className,
- children,
- side = 'right',
- showCloseButton = true,
- ...props
-}: React.ComponentProps & {
- side?: 'top' | 'right' | 'bottom' | 'left';
- showCloseButton?: boolean;
-}) {
- return (
-
-
-
- {children}
- {showCloseButton && (
-
-
- Close
-
- )}
-
-
- );
-}
-
-function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
- return (
-
- );
-}
-
-function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
- return (
-
- );
-}
-
-function SheetTitle({ className, ...props }: React.ComponentProps) {
- return (
-
- );
-}
-
-function SheetDescription({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-export {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
- SheetTrigger,
-};
diff --git a/docs/src/components/ui/tooltip.tsx b/docs/src/components/ui/tooltip.tsx
deleted file mode 100644
index 7dfde15a1..000000000
--- a/docs/src/components/ui/tooltip.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-'use client';
-
-import { Tooltip as TooltipPrimitive } from 'radix-ui';
-import type * as React from 'react';
-
-import { cn } from '@/lib/utils';
-
-function TooltipProvider({
- delayDuration = 0,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function Tooltip({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function TooltipTrigger({ ...props }: React.ComponentProps) {
- return ;
-}
-
-function TooltipContent({
- className,
- sideOffset = 0,
- children,
- ...props
-}: React.ComponentProps) {
- return (
-
-
- {children}
-
-
-
- );
-}
-
-export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
diff --git a/docs/src/lib/brand-assets.ts b/docs/src/lib/brand-assets.ts
index cfaacc78e..441f49443 100644
--- a/docs/src/lib/brand-assets.ts
+++ b/docs/src/lib/brand-assets.ts
@@ -36,5 +36,3 @@ export const BRAND_ASSETS: BrandAsset[] = [
tile: 'muted',
},
];
-
-export const PRIMARY_BRAND_ASSET = BRAND_ASSETS[0];
diff --git a/docs/src/lib/copy-svg.ts b/docs/src/lib/copy-svg.ts
deleted file mode 100644
index fdcadc025..000000000
--- a/docs/src/lib/copy-svg.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export async function copySvgToClipboard(url: string): Promise {
- const res = await fetch(url);
- if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
- await navigator.clipboard.writeText(await res.text());
-}
diff --git a/docs/src/lib/site.ts b/docs/src/lib/site.ts
index 4dcdefe92..f7bf5c015 100644
--- a/docs/src/lib/site.ts
+++ b/docs/src/lib/site.ts
@@ -30,6 +30,3 @@ export function metaDescription(
export const DOWNLOAD_URL = STABLE_DMG_URL;
export const DOWNLOAD_ROUTE = '/download/stable';
-
-export const EXAMPLE_KB_SHARE_URL =
- 'https://openknowledge.ai/d/AWh0dHBzOi8vZ2l0aHViLmNvbS9pbmtlZXAvdGVjaC1pcG9zL2Jsb2IvbWFpbi9SRUFETUUubWQ';
diff --git a/docs/src/lib/use-is-in-view.ts b/docs/src/lib/use-is-in-view.ts
deleted file mode 100644
index ae89b073c..000000000
--- a/docs/src/lib/use-is-in-view.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client';
-
-import { useEffect, useRef, useState } from 'react';
-
-export function useIsInView(rootMargin = '64px') {
- const ref = useRef(null);
- const [inView, setInView] = useState(false);
- useEffect(() => {
- const el = ref.current;
- if (!el || typeof IntersectionObserver === 'undefined') return;
- const observer = new IntersectionObserver(([entry]) => setInView(entry.isIntersecting), {
- rootMargin,
- threshold: 0,
- });
- observer.observe(el);
- return () => observer.disconnect();
- }, [rootMargin]);
- return [ref, inView] as const;
-}
diff --git a/docs/src/lib/use-prefers-reduced-motion.ts b/docs/src/lib/use-prefers-reduced-motion.ts
deleted file mode 100644
index c8f35b5d3..000000000
--- a/docs/src/lib/use-prefers-reduced-motion.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client';
-
-import { useEffect, useState } from 'react';
-
-export function usePrefersReducedMotion(): boolean {
- const [prefersReduced, setPrefersReduced] = useState(false);
- useEffect(() => {
- const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
- setPrefersReduced(mq.matches);
- const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches);
- mq.addEventListener('change', handler);
- return () => mq.removeEventListener('change', handler);
- }, []);
- return prefersReduced;
-}
diff --git a/docs/src/middleware.test.ts b/docs/src/proxy.test.ts
similarity index 94%
rename from docs/src/middleware.test.ts
rename to docs/src/proxy.test.ts
index 5710df65c..91d2af0ec 100644
--- a/docs/src/middleware.test.ts
+++ b/docs/src/proxy.test.ts
@@ -1,17 +1,17 @@
import { describe, expect, test } from 'bun:test';
import { NextRequest } from 'next/server';
-import { middleware } from './middleware.ts';
+import { proxy } from './proxy.ts';
function run(host: string, pathAndQuery: string) {
const req = new NextRequest(`https://${host}${pathAndQuery}`, { headers: { host } });
- return middleware(req);
+ return proxy(req);
}
const APEX = 'openknowledge.ai';
const WWW = 'www.openknowledge.ai';
const AASA = '/.well-known/apple-app-site-association';
-describe('middleware: www -> apex canonicalization', () => {
+describe('proxy: www -> apex canonicalization', () => {
test('www + AASA passes through (no redirect) so Apple gets a direct 200', () => {
const res = run(WWW, AASA);
expect(res.headers.get('location')).toBeNull();
diff --git a/docs/src/middleware.ts b/docs/src/proxy.ts
similarity index 90%
rename from docs/src/middleware.ts
rename to docs/src/proxy.ts
index effbbcb30..872cabcc9 100644
--- a/docs/src/middleware.ts
+++ b/docs/src/proxy.ts
@@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server';
const APEX_HOST = 'openknowledge.ai';
const WWW_HOST = `www.${APEX_HOST}`;
-export function middleware(request: NextRequest): NextResponse {
+export function proxy(request: NextRequest): NextResponse {
const host = (request.headers.get('host') ?? request.nextUrl.host).split(':')[0];
if (request.nextUrl.pathname.startsWith('/.well-known/')) {