diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..25fc973 --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# TODO + +## Default theme + +- [x] Replace sidebar logo placeholder (`CubeIcon`) with configurable logo from `config.logo` (light/dark) +- [x] Add optional `icon` field per content dir and API in `chronicle.yaml` (accepts URL or inline SVG string) +- [x] "Open in AI" dropdown (Copy/View MD, Open in ChatGPT/Claude) in subNav +- [x] Sidebar nesting supports 2 levels; 3rd+ level items are ignored diff --git a/bun.lock b/bun.lock index 84ca123..729f6aa 100644 --- a/bun.lock +++ b/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "chronicle", + "dependencies": { + "std-env": "^4.0.0", + }, }, "packages/chronicle": { "name": "@raystack/chronicle", @@ -22,7 +25,7 @@ "@opentelemetry/resources": "^2.6.1", "@opentelemetry/sdk-metrics": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", - "@raystack/apsara": "1.0.0-rc.3", + "@raystack/apsara": "1.0.0-rc.4", "@shikijs/rehype": "^4.0.2", "@vitejs/plugin-react": "^6.0.1", "chalk": "^5.6.2", @@ -235,43 +238,9 @@ "@oxc-project/types": ["@oxc-project/types@0.120.0", "", {}, "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], - - "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], - - "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], - - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], - - "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], - - "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], - "@radix-ui/react-icons": ["@radix-ui/react-icons@1.3.2", "", { "peerDependencies": { "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g=="], - "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - - "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], - - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - - "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], - - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - - "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], - - "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], - - "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], - - "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - - "@raystack/apsara": ["@raystack/apsara@1.0.0-rc.3", "", { "dependencies": { "@base-ui/react": "^1.3.0", "@base-ui/utils": "^0.2.6", "@radix-ui/react-icons": "^1.3.2", "@tanstack/match-sorter-utils": "^8.8.4", "@tanstack/react-table": "^8.9.2", "@tanstack/react-virtual": "^3.13.13", "@tanstack/table-core": "^8.9.2", "class-variance-authority": "^0.7.1", "cmdk": "^1.1.1", "color": "^5.0.0", "dayjs": "^1.11.11", "prism-react-renderer": "^2.4.1", "react-day-picker": "^9.6.7" }, "peerDependencies": { "@types/react": "^19", "react": "^19", "react-dom": "^19" }, "optionalPeers": ["@types/react"] }, "sha512-h7HfNAPDKDxp93D1AMkWYOwfgcaG7A2CXmzAioozZ3TLK2evx3cgOiFk3YfN5rSv8OMTU0d99zgYrv3s/TpWpA=="], + "@raystack/apsara": ["@raystack/apsara@1.0.0-rc.4", "", { "dependencies": { "@base-ui/react": "^1.3.0", "@base-ui/utils": "^0.2.6", "@radix-ui/react-icons": "^1.3.2", "@tanstack/match-sorter-utils": "^8.8.4", "@tanstack/react-table": "^8.9.2", "@tanstack/react-virtual": "^3.13.13", "@tanstack/table-core": "^8.9.2", "class-variance-authority": "^0.7.1", "color": "^5.0.0", "dayjs": "^1.11.11", "prism-react-renderer": "^2.4.1", "react-day-picker": "^9.6.7" }, "peerDependencies": { "@types/react": "^19", "react": "^19", "react-dom": "^19" }, "optionalPeers": ["@types/react"] }, "sha512-RYt1URkQjfQINYGoN9r4mkmm4Vvul1UKkqiUqN+EoNgl2BNK0fPJq+x3pmW1RDardjsxgeT4AmdZfsuumVn+uw=="], "@raystack/chronicle": ["@raystack/chronicle@workspace:packages/chronicle"], @@ -449,8 +418,6 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], @@ -485,8 +452,6 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], - "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="], "collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="], @@ -621,8 +586,6 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], "dompurify": ["dompurify@3.3.2", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ=="], @@ -679,8 +642,6 @@ "fumadocs-mdx": ["fumadocs-mdx@14.3.1", "", { "dependencies": { "@mdx-js/mdx": "^3.1.1", "@standard-schema/spec": "^1.1.0", "chokidar": "^5.0.0", "esbuild": "^0.28.0", "estree-util-value-to-estree": "^3.5.0", "js-yaml": "^4.1.1", "mdast-util-mdx": "^3.0.0", "mdast-util-to-markdown": "^2.1.2", "picocolors": "^1.1.1", "picomatch": "^4.0.4", "tinyexec": "^1.1.1", "tinyglobby": "^0.2.16", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "vfile": "^6.0.3", "zod": "^4.3.6" }, "peerDependencies": { "@types/mdast": "*", "@types/mdx": "*", "@types/react": "*", "fumadocs-core": "^15.0.0 || ^16.0.0", "mdast-util-directive": "*", "next": "^15.3.0 || ^16.0.0", "react": "^19.2.0", "vite": "6.x.x || 7.x.x || 8.x.x" }, "optionalPeers": ["@types/mdast", "@types/mdx", "@types/react", "mdast-util-directive", "next", "react", "vite"], "bin": { "fumadocs-mdx": "dist/bin.js" } }, "sha512-0u2eXvYrZtrJB14y6fDhP0hhxLgmH8JOmRv6IVHALt5MqR9JIJxV5LJYlho8g8CJXRE8w12rVNFZN0rtUVAqGw=="], - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], - "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], @@ -965,14 +926,8 @@ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], - "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], - - "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.13.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA=="], - "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], @@ -1119,10 +1074,6 @@ "unstorage": ["unstorage@2.0.0-alpha.7", "", { "peerDependencies": { "@azure/app-configuration": "^1.11.0", "@azure/cosmos": "^4.9.1", "@azure/data-tables": "^13.3.2", "@azure/identity": "^4.13.0", "@azure/keyvault-secrets": "^4.10.0", "@azure/storage-blob": "^12.31.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.13.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.36.2", "@vercel/blob": ">=0.27.3", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1.0.1", "aws4fetch": "^1.0.20", "chokidar": "^4 || ^5", "db0": ">=0.3.4", "idb-keyval": "^6.2.2", "ioredis": "^5.9.3", "lru-cache": "^11.2.6", "mongodb": "^6 || ^7", "ofetch": "*", "uploadthing": "^7.7.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "chokidar", "db0", "idb-keyval", "ioredis", "lru-cache", "mongodb", "ofetch", "uploadthing"] }, "sha512-ELPztchk2zgFJnakyodVY3vJWGW9jy//keJ32IOJVGUMyaPydwcA1FtVvWqT0TNRch9H+cMNEGllfVFfScImog=="], - "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], - - "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -1159,16 +1110,6 @@ "@antfu/install-pkg/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], - "@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - - "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - "color-convert/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], "color-string/color-name": ["color-name@2.1.0", "", {}, "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg=="], diff --git a/docs/chronicle.yaml b/docs/chronicle.yaml index 627ae8f..305a659 100644 --- a/docs/chronicle.yaml +++ b/docs/chronicle.yaml @@ -7,7 +7,7 @@ content: label: Docs theme: - name: paper + name: default navigation: links: @@ -18,16 +18,7 @@ search: enabled: true placeholder: Search docs... -llms: - enabled: true - telemetry: enabled: true preset: "vercel" - -footer: - copyright: "© 2026 Raystack. All rights reserved." - links: - - label: GitHub - href: https://github.com/raystack/chronicle diff --git a/docs/content/docs/components.mdx b/docs/content/docs/components.mdx index 97f9e31..4f9937d 100644 --- a/docs/content/docs/components.mdx +++ b/docs/content/docs/components.mdx @@ -78,8 +78,8 @@ Tabbed content panels using Apsara's `Tabs` component. ````mdx - npm - bun + npm + bun diff --git a/examples/basic/chronicle.yaml b/examples/basic/chronicle.yaml index 2282d33..b61a74b 100644 --- a/examples/basic/chronicle.yaml +++ b/examples/basic/chronicle.yaml @@ -38,10 +38,3 @@ analytics: googleAnalytics: measurementId: G-XXXXXXXXXX -footer: - copyright: "© 2024 Chronicle. All rights reserved." - links: - - label: GitHub - href: https://github.com/raystack/chronicle - - label: Documentation - href: / diff --git a/examples/versioned/chronicle.yaml b/examples/versioned/chronicle.yaml index 818fae1..0a55cb0 100644 --- a/examples/versioned/chronicle.yaml +++ b/examples/versioned/chronicle.yaml @@ -37,6 +37,3 @@ versions: search: enabled: true placeholder: Search... - -llms: - enabled: true diff --git a/package.json b/package.json index 5ccda8b..c433f52 100644 --- a/package.json +++ b/package.json @@ -17,5 +17,8 @@ "build:examples:basic": "./packages/chronicle/bin/chronicle.js build --config examples/basic/chronicle.yaml", "dev:examples:versioned": "./packages/chronicle/bin/chronicle.js dev --config examples/versioned/chronicle.yaml", "build:examples:versioned": "./packages/chronicle/bin/chronicle.js build --config examples/versioned/chronicle.yaml" + }, + "dependencies": { + "std-env": "^4.0.0" } } diff --git a/packages/chronicle/package.json b/packages/chronicle/package.json index d7a86d8..e9dffd5 100644 --- a/packages/chronicle/package.json +++ b/packages/chronicle/package.json @@ -42,7 +42,7 @@ "@opentelemetry/resources": "^2.6.1", "@opentelemetry/sdk-metrics": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", - "@raystack/apsara": "1.0.0-rc.3", + "@raystack/apsara": "1.0.0-rc.4", "@shikijs/rehype": "^4.0.2", "@vitejs/plugin-react": "^6.0.1", "chalk": "^5.6.2", diff --git a/packages/chronicle/src/components/ui/breadcrumbs.tsx b/packages/chronicle/src/components/ui/breadcrumbs.tsx index 6babcd7..c7724e1 100644 --- a/packages/chronicle/src/components/ui/breadcrumbs.tsx +++ b/packages/chronicle/src/components/ui/breadcrumbs.tsx @@ -3,6 +3,7 @@ import { Breadcrumb } from '@raystack/apsara' import { getBreadcrumbItems } from 'fumadocs-core/breadcrumb' import type { Root } from 'fumadocs-core/page-tree' +import { Link as RouterLink } from 'react-router' interface BreadcrumbsProps { slug: string[] @@ -18,11 +19,12 @@ export function Breadcrumbs({ slug, tree }: BreadcrumbsProps) { return ( {items.flatMap((item, index) => { + const isCurrent = index === items.length - 1 const breadcrumbItem = ( } > {item.name} diff --git a/packages/chronicle/src/components/ui/client-theme-switcher.tsx b/packages/chronicle/src/components/ui/client-theme-switcher.tsx index 34f9051..b504410 100644 --- a/packages/chronicle/src/components/ui/client-theme-switcher.tsx +++ b/packages/chronicle/src/components/ui/client-theme-switcher.tsx @@ -1,18 +1,35 @@ 'use client' -import { ThemeSwitcher } from '@raystack/apsara' -import { useState, useEffect } from 'react' +import { MoonIcon, SunIcon } from '@heroicons/react/24/outline' +import { IconButton, useTheme } from '@raystack/apsara' +import { useEffect, useState } from 'react' interface ClientThemeSwitcherProps { size?: number } -export function ClientThemeSwitcher({ size }: ClientThemeSwitcherProps) { +export function ClientThemeSwitcher({ size = 16 }: ClientThemeSwitcherProps) { const [isClient, setIsClient] = useState(false) + const { resolvedTheme, setTheme } = useTheme() useEffect(() => { setIsClient(true) }, []) - return isClient ? : null + if (!isClient) return null + + const isDark = resolvedTheme === 'dark' + return ( + setTheme(isDark ? 'light' : 'dark')} + > + {isDark ? ( + + ) : ( + + )} + + ) } diff --git a/packages/chronicle/src/components/ui/footer.module.css b/packages/chronicle/src/components/ui/footer.module.css deleted file mode 100644 index b40bc91..0000000 --- a/packages/chronicle/src/components/ui/footer.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.footer { - border-top: 1px solid var(--rs-color-border-base-primary); - padding: var(--rs-space-5) var(--rs-space-7); -} - -.container { - max-width: 1200px; - margin: 0 auto; - width: 100%; -} - -.copyright { - color: var(--rs-color-foreground-base-secondary); -} - -.links { - flex-wrap: wrap; -} - -.link { - color: var(--rs-color-foreground-base-secondary); - font-size: 14px; -} - -.link:hover { - color: var(--rs-color-foreground-base-primary); -} diff --git a/packages/chronicle/src/components/ui/footer.tsx b/packages/chronicle/src/components/ui/footer.tsx deleted file mode 100644 index 0f3c35f..0000000 --- a/packages/chronicle/src/components/ui/footer.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Flex, Link, Text } from "@raystack/apsara"; -import type { FooterConfig } from "@/types"; -import styles from "./footer.module.css"; - -interface FooterProps { - config?: FooterConfig; -} - -export function Footer({ config }: FooterProps) { - return ( -
- - {config?.copyright && ( - - {config.copyright} - - )} - {config?.links && config.links.length > 0 && ( - - {config.links.map((link) => ( - - {link.label} - - ))} - - )} - -
- ); -} diff --git a/packages/chronicle/src/components/ui/search.module.css b/packages/chronicle/src/components/ui/search.module.css index 086e7f4..d36228a 100644 --- a/packages/chronicle/src/components/ui/search.module.css +++ b/packages/chronicle/src/components/ui/search.module.css @@ -1,63 +1,38 @@ -.trigger { - gap: 8px; - color: var(--rs-color-foreground-base-secondary); - cursor: pointer; -} - -.kbd { - padding: 2px 6px; - border-radius: 4px; - border: 1px solid var(--rs-color-border-base-primary); - font-size: 12px; -} - .dialogContent { + border-radius: var(--rs-radius-4); + padding: 0px; + width: 80%; max-width: 600px; - padding: 0; - min-height: 0; position: fixed; top: 20%; - left: 50%; - transform: translateX(-50%); - border-radius: var(--rs-radius-4); } .input { - flex: 1; - border: none; - outline: none; - background: transparent; - font-size: 16px; - color: var(--rs-color-foreground-base-primary); + font-size: var(--rs-font-size-small); } .list { - max-height: 300px; - overflow: auto; - padding: 0; + max-height: 400px; } -.visuallyHidden { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; +.list :global([cmdk-group-heading]) { + color: var(--rs-color-foreground-base-tertiary); + font-size: var(--rs-font-size-mini); + font-weight: var(--rs-font-weight-medium); + letter-spacing: var(--rs-letter-spacing-mini); + padding: var(--rs-space-6) var(--rs-space-5) var(--rs-space-3); } .item { - padding: 12px 16px; + height: 32px; + padding: var(--rs-space-3); + gap: var(--rs-space-3); + border-radius: var(--rs-radius-2); cursor: pointer; - border-radius: 6px; } .item[data-selected="true"] { - background: var(--rs-color-background-base-secondary); - color: var(--rs-color-foreground-accent-primary-hover); + background: var(--rs-color-background-base-primary-hover); } .itemContent { diff --git a/packages/chronicle/src/components/ui/search.tsx b/packages/chronicle/src/components/ui/search.tsx index 370c4ce..c2a3e7d 100644 --- a/packages/chronicle/src/components/ui/search.tsx +++ b/packages/chronicle/src/components/ui/search.tsx @@ -1,6 +1,9 @@ -import { DocumentIcon, HashtagIcon } from '@heroicons/react/24/outline'; -import { Button, Command, Dialog, Text } from '@raystack/apsara'; -import { cx } from 'class-variance-authority'; +import { + DocumentIcon, + HashtagIcon, + MagnifyingGlassIcon +} from '@heroicons/react/24/outline'; +import { Command, IconButton, Text } from '@raystack/apsara'; import type { SortedResult } from 'fumadocs-core/search'; import { useDocsSearch } from 'fumadocs-core/search/client'; import { useCallback, useEffect, useState } from 'react'; @@ -9,21 +12,6 @@ import { MethodBadge } from '@/components/api/method-badge'; import { usePageContext } from '@/lib/page-context'; import styles from './search.module.css'; -function SearchShortcutKey({ className }: { className?: string }) { - const [key, setKey] = useState('⌘'); - - useEffect(() => { - const isMac = navigator.platform?.toUpperCase().includes('MAC'); - setKey(isMac ? '⌘' : 'Ctrl'); - }, []); - - return ( - - {key} K - - ); -} - interface SearchProps { className?: string; } @@ -67,31 +55,28 @@ export function Search({ className }: SearchProps) { return ( <> - - - - - - Search documentation - - + + + + + + } value={search} - onValueChange={setSearch} + onChange={(e) => setSearch(e.target.value)} className={styles.input} /> - + {query.isLoading && Loading...} {!query.isLoading && search.length > 0 && @@ -101,12 +86,13 @@ export function Search({ className }: SearchProps) { {!query.isLoading && search.length === 0 && results.length > 0 && ( - + + Suggestions {results.slice(0, 8).map((result: SortedResult) => ( onSelect(result.url)} + onClick={() => onSelect(result.url)} className={styles.item} >
@@ -126,7 +112,7 @@ export function Search({ className }: SearchProps) { onSelect(result.url)} + onClick={() => onSelect(result.url)} className={styles.item} >
@@ -155,10 +141,10 @@ export function Search({ className }: SearchProps) {
))} - + - -
+ + ); } diff --git a/packages/chronicle/src/lib/config.ts b/packages/chronicle/src/lib/config.ts index c6dabd3..78bd3b6 100644 --- a/packages/chronicle/src/lib/config.ts +++ b/packages/chronicle/src/lib/config.ts @@ -35,6 +35,7 @@ export interface ContentRoot { versionLabel: string | null contentDir: string contentLabel: string + contentIcon?: string fsPath: string urlPrefix: string } @@ -45,6 +46,7 @@ export function getLatestContentRoots(config: ChronicleConfig): ContentRoot[] { versionLabel: config.latest?.label ?? null, contentDir: c.dir, contentLabel: c.label, + contentIcon: c.icon, fsPath: `content/${c.dir}`, urlPrefix: `/${c.dir}`, })) @@ -62,6 +64,7 @@ export function getVersionContentRoots( versionLabel: version.label, contentDir: c.dir, contentLabel: c.label, + contentIcon: c.icon, fsPath: `versions/${version.dir}/${c.dir}`, urlPrefix: `/${version.dir}/${c.dir}`, })) @@ -78,6 +81,7 @@ export interface LandingEntry { label: string href: string contentDir: string + icon?: string } export function getLandingEntries( @@ -93,6 +97,7 @@ export function getLandingEntries( label: r.contentLabel, href: r.urlPrefix, contentDir: r.contentDir, + icon: r.contentIcon, })) } diff --git a/packages/chronicle/src/lib/head.tsx b/packages/chronicle/src/lib/head.tsx index ebdc967..02fdab6 100644 --- a/packages/chronicle/src/lib/head.tsx +++ b/packages/chronicle/src/lib/head.tsx @@ -6,9 +6,10 @@ export interface HeadProps { description?: string; config: ChronicleConfig; jsonLd?: Record; + markdownHref?: string; } -export function Head({ title, description, config, jsonLd }: HeadProps) { +export function Head({ title, description, config, jsonLd, markdownHref }: HeadProps) { const { pathname } = useLocation(); const fullTitle = `${title} | ${config.site.title}`; const ogParams = new URLSearchParams({ title }); @@ -24,6 +25,14 @@ export function Head({ title, description, config, jsonLd }: HeadProps) { {fullTitle} {description && } {canonical && } + {markdownHref && ( + + )} {config.url && ( <> diff --git a/packages/chronicle/src/lib/page-context.tsx b/packages/chronicle/src/lib/page-context.tsx index 9487a9d..a5069af 100644 --- a/packages/chronicle/src/lib/page-context.tsx +++ b/packages/chronicle/src/lib/page-context.tsx @@ -10,21 +10,14 @@ import type { ApiSpec } from '@/lib/openapi'; import { resolveRoute, RouteType } from '@/lib/route-resolver'; import type { VersionContext } from '@/lib/version-source'; import { LATEST_CONTEXT } from '@/lib/version-source'; -import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types'; +import type { ChronicleConfig, Frontmatter, Page, Root, TableOfContents } from '@/types'; export type MdxLoader = (relativePath: string) => Promise<{ content: ReactNode; toc: TableOfContents }>; -interface PageData { - slug: string[]; - frontmatter: Frontmatter; - content: ReactNode; - toc: TableOfContents; -} - interface PageContextValue { config: ChronicleConfig; tree: Root; - page: PageData | null; + page: Page | null; errorStatus: number | null; apiSpecs: ApiSpec[]; version: VersionContext; @@ -54,18 +47,18 @@ export function usePageContext(): PageContextValue { interface PageProviderProps { initialConfig: ChronicleConfig; initialTree: Root; - initialPage: PageData | null; + initialPage: Page | null; initialApiSpecs: ApiSpec[]; initialVersion: VersionContext; loadMdx: MdxLoader; children: ReactNode; } -function getInitialErrorStatus( - page: PageData | null, - config: ChronicleConfig, - pathname: string, -): number | null { +function isApisRoute(pathname: string): boolean { + return pathname === '/apis' || pathname.startsWith('/apis/'); +} + +function getInitialErrorStatus(page: Page | null, config: ChronicleConfig, pathname: string): number | null { if (page) return null; const route = resolveRoute(pathname, config); if (route.type === RouteType.ApiIndex || route.type === RouteType.ApiPage) return null; @@ -85,10 +78,8 @@ export function PageProvider({ }: PageProviderProps) { const { pathname } = useLocation(); const [tree] = useState(initialTree); - const [page, setPage] = useState(initialPage); - const [errorStatus, setErrorStatus] = useState( - getInitialErrorStatus(initialPage, initialConfig, pathname), - ); + const [page, setPage] = useState(initialPage); + const [errorStatus, setErrorStatus] = useState(getInitialErrorStatus(initialPage, initialConfig, pathname)); const [apiSpecs, setApiSpecs] = useState(initialApiSpecs); const [version, setVersion] = useState(initialVersion); const [currentPath, setCurrentPath] = useState(pathname); @@ -140,12 +131,12 @@ export function PageProvider({ } return res.json(); }) - .then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string } | undefined) => { + .then(async (data: { frontmatter: Frontmatter; relativePath: string; originalPath?: string; prev?: Page['prev']; next?: Page['next'] } | undefined) => { if (cancelled.current || !data) return; const { content, toc } = await loadMdx(data.originalPath || data.relativePath); if (cancelled.current) return; setErrorStatus(null); - setPage({ slug: route.slug, frontmatter: data.frontmatter, content, toc }); + setPage({ slug: route.slug, frontmatter: data.frontmatter, content, toc, prev: data.prev, next: data.next }); }) .catch(() => { if (!cancelled.current) { diff --git a/packages/chronicle/src/lib/source.ts b/packages/chronicle/src/lib/source.ts index e528c8d..85014dd 100644 --- a/packages/chronicle/src/lib/source.ts +++ b/packages/chronicle/src/lib/source.ts @@ -1,8 +1,8 @@ import { loader } from 'fumadocs-core/source'; +import { flattenTree } from 'fumadocs-core/page-tree'; import type { Root, Node, Folder } from 'fumadocs-core/page-tree'; import type { MDXContent } from 'mdx/types'; import type { TableOfContents } from 'fumadocs-core/toc'; -import type { Frontmatter } from '@/types'; import { getLatestContentRoots, getVersionContentRoots, @@ -14,6 +14,7 @@ import { resolveVersionFromUrl, type VersionContext, } from './version-source'; +import type { Frontmatter, PageNav, PageNavLink } from '@/types'; const CONTENT_PREFIX = '../../.content/'; @@ -168,6 +169,35 @@ export function getVersionContextForUrl(url: string): VersionContext { export type { VersionContext } from './version-source'; +function titleFromUrl(url: string): string { + if (url === '/') return 'Home'; + const last = url.split('/').filter(Boolean).pop(); + if (!last) return 'Home'; + return last + .split('-') + .map(w => w.charAt(0).toUpperCase() + w.slice(1)) + .join(' '); +} + +export async function getPageNav(slug: string[], tree?: Root): Promise { + const resolvedTree = tree ?? (await getPageTree()); + const pages = flattenTree(resolvedTree.children); + const url = slug.length === 0 ? '/' : `/${slug.join('/')}`; + const i = pages.findIndex(p => p.url === url); + if (i < 0) return { prev: null, next: null }; + const toLink = (p: (typeof pages)[number]): PageNavLink => ({ + url: p.url, + title: + typeof p.name === 'string' && p.name.length > 0 + ? p.name + : titleFromUrl(p.url) + }); + return { + prev: i > 0 ? toLink(pages[i - 1]) : null, + next: i < pages.length - 1 ? toLink(pages[i + 1]) : null + }; +} + export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter { const d = page.data as Record; return { diff --git a/packages/chronicle/src/pages/DocsPage.tsx b/packages/chronicle/src/pages/DocsPage.tsx index e1710b4..8dfb6ad 100644 --- a/packages/chronicle/src/pages/DocsPage.tsx +++ b/packages/chronicle/src/pages/DocsPage.tsx @@ -16,6 +16,7 @@ export function DocsPage({ slug }: DocsPageProps) { const { Page } = getTheme(config.theme?.name); const pageUrl = config.url ? `${config.url}/${slug.join('/')}` : undefined; + const markdownHref = `/${slug.join('/')}.md`; return ( <> @@ -23,6 +24,7 @@ export function DocsPage({ slug }: DocsPageProps) { title={page.frontmatter.title} description={page.frontmatter.description} config={config} + markdownHref={markdownHref} jsonLd={{ '@context': 'https://schema.org', '@type': 'Article', @@ -32,12 +34,7 @@ export function DocsPage({ slug }: DocsPageProps) { }} /> diff --git a/packages/chronicle/src/server/App.tsx b/packages/chronicle/src/server/App.tsx index 16db22e..65117c0 100644 --- a/packages/chronicle/src/server/App.tsx +++ b/packages/chronicle/src/server/App.tsx @@ -40,7 +40,7 @@ export function App() { ) : ( - + {isLanding ? : } )} diff --git a/packages/chronicle/src/server/api/page.ts b/packages/chronicle/src/server/api/page.ts index bfb43aa..568d6aa 100644 --- a/packages/chronicle/src/server/api/page.ts +++ b/packages/chronicle/src/server/api/page.ts @@ -1,5 +1,5 @@ import { defineHandler, HTTPError } from 'nitro'; -import { getPage, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; +import { getPage, getPageNav, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; export default defineHandler(async event => { const slugParam = event.url.searchParams.get('slug') ?? ''; @@ -10,9 +10,13 @@ export default defineHandler(async event => { throw new HTTPError({ status: 404, message: 'Page not found' }); } + const nav = await getPageNav(slug); + return { frontmatter: extractFrontmatter(page, slug[slug.length - 1]), relativePath: getRelativePath(page), originalPath: getOriginalPath(page), + prev: nav.prev, + next: nav.next, }; }); diff --git a/packages/chronicle/src/server/entry-client.tsx b/packages/chronicle/src/server/entry-client.tsx index 141ae5f..985cce3 100644 --- a/packages/chronicle/src/server/entry-client.tsx +++ b/packages/chronicle/src/server/entry-client.tsx @@ -8,7 +8,7 @@ import { getApiConfigsForVersion } from '@/lib/config'; import { PageProvider } from '@/lib/page-context'; import { resolveRoute, RouteType } from '@/lib/route-resolver'; import { resolveVersionFromUrl, type VersionContext } from '@/lib/version-source'; -import type { ChronicleConfig, Frontmatter, Root, TableOfContents } from '@/types'; +import type { ChronicleConfig, Frontmatter, PageNavLink, Root, TableOfContents } from '@/types'; import type { ApiSpec } from '@/lib/openapi'; import type { ReactNode } from 'react'; import { App } from './App'; @@ -18,9 +18,11 @@ interface EmbeddedData { tree: Root; slug: string[]; version: VersionContext; - frontmatter: Frontmatter; - relativePath: string; - originalPath?: string; + frontmatter: Frontmatter | null; + relativePath: string | null; + originalPath: string | null; + prev: PageNavLink | null; + next: PageNavLink | null; } const defaultConfig: ChronicleConfig = { @@ -79,10 +81,12 @@ async function hydrate() { : []; const mdxPath = embedded?.originalPath || embedded?.relativePath; - const page = mdxPath + const page = embedded && mdxPath && embedded.frontmatter ? { - slug: embedded!.slug, - frontmatter: embedded!.frontmatter, + slug: embedded.slug, + frontmatter: embedded.frontmatter, + prev: embedded.prev, + next: embedded.next, ...(await loadMdxModule(mdxPath)), } : null; diff --git a/packages/chronicle/src/server/entry-server.tsx b/packages/chronicle/src/server/entry-server.tsx index e5721d7..6fd5fe1 100644 --- a/packages/chronicle/src/server/entry-server.tsx +++ b/packages/chronicle/src/server/entry-server.tsx @@ -9,7 +9,7 @@ import { getApiConfigsForVersion, loadConfig } from '@/lib/config'; import { loadApiSpecs } from '@/lib/openapi'; import { PageProvider } from '@/lib/page-context'; import { resolveRoute, RouteType } from '@/lib/route-resolver'; -import { getPage, getPageTree, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; +import { getPageTree, getPage, getPageNav, loadPageModule, extractFrontmatter, getRelativePath, getOriginalPath } from '@/lib/source'; import { useNitroApp } from 'nitro/app'; import { App } from './App'; @@ -45,6 +45,7 @@ export default { getPageTree(), route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null), ]); + const nav = page ? await getPageNav(pageSlug, tree) : { prev: null, next: null }; const relativePath = page ? getRelativePath(page) : null; const originalPath = page ? getOriginalPath(page) : null; @@ -58,6 +59,8 @@ export default { ? React.createElement(mdxModule.default, { components: mdxComponents }) : null, toc: mdxModule?.toc ?? [], + prev: nav.prev, + next: nav.next, } : null; @@ -69,6 +72,8 @@ export default { frontmatter: pageData?.frontmatter ?? null, relativePath, originalPath, + prev: pageData?.prev ?? null, + next: pageData?.next ?? null, }; const safeJson = JSON.stringify(embeddedData).replace(/ { const pathname = event.path || event.req.url?.split('?')[0] || ''; if (!pathname.endsWith('.md')) return; - const config = loadConfig(); - if (!config.llms?.enabled) { - throw new HTTPError({ status: 404, message: 'Not Found' }); - } - const stripped = pathname.replace(/\.md$/, ''); const parts = stripped === '/index' || stripped === '/' ? [] diff --git a/packages/chronicle/src/server/routes/[version]/llms.txt.ts b/packages/chronicle/src/server/routes/[version]/llms.txt.ts index b0e8603..80997a1 100644 --- a/packages/chronicle/src/server/routes/[version]/llms.txt.ts +++ b/packages/chronicle/src/server/routes/[version]/llms.txt.ts @@ -7,10 +7,6 @@ import { extractFrontmatter, getPagesForVersion } from '@/lib/source'; export default defineHandler(async event => { const config = loadConfig(); - if (!config.llms?.enabled) { - throw new HTTPError({ status: 404, message: 'Not Found' }); - } - const versionDir = getRouterParam(event, 'version'); const version = config.versions?.find(v => v.dir === versionDir); if (!version) { diff --git a/packages/chronicle/src/server/routes/llms.txt.ts b/packages/chronicle/src/server/routes/llms.txt.ts index a0939f6..2d150cf 100644 --- a/packages/chronicle/src/server/routes/llms.txt.ts +++ b/packages/chronicle/src/server/routes/llms.txt.ts @@ -1,4 +1,4 @@ -import { defineHandler, HTTPError } from 'nitro'; +import { defineHandler } from 'nitro'; import { loadConfig } from '@/lib/config'; import { buildLlmsTxt } from '@/lib/llms'; import { extractFrontmatter, getPagesForVersion } from '@/lib/source'; @@ -7,10 +7,6 @@ import { LATEST_CONTEXT } from '@/lib/version-source'; export default defineHandler(async event => { const config = loadConfig(); - if (!config.llms?.enabled) { - throw new HTTPError({ status: 404, message: 'Not Found' }); - } - const pages = await getPagesForVersion(LATEST_CONTEXT); const body = buildLlmsTxt( config, diff --git a/packages/chronicle/src/themes/default/Layout.module.css b/packages/chronicle/src/themes/default/Layout.module.css index dadbe5d..911a157 100644 --- a/packages/chronicle/src/themes/default/Layout.module.css +++ b/packages/chronicle/src/themes/default/Layout.module.css @@ -1,79 +1,226 @@ .layout { + --navbar-height: 48px; min-height: 100vh; } -.header { - border-bottom: 1px solid var(--rs-color-border-base-primary); -} - -.search { - margin-left: var(--rs-space-5); -} - .body { flex: 1; } .sidebar { - width: 260px; + width: 262px; + flex: 0 0 262px; + display: flex; + flex-direction: column; position: sticky; top: 0; height: 100vh; + background: var(--rs-color-background-base-secondary); } -.content { - flex: 1; - padding: var(--rs-space-9); +.sidebarLogo { + width: 28px; + height: 28px; + object-fit: contain; } -.sidebarList { - list-style: none; +.configIcon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: currentColor; +} + +.configIcon svg { + width: 100%; + height: 100%; + display: block; +} + +.sidebar[data-position='left'] { + border-right: none; padding: 0; - margin: 0; } -.separator { - height: 1px; - background: var(--rs-color-border-base-primary); - margin: var(--rs-space-3) 0; +.sidebarNavbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--rs-space-3); + width: 100%; + height: var(--navbar-height); + padding: 0 var(--rs-space-5); + flex-shrink: 0; + backdrop-filter: blur(1px); +} + +.sidebarNavLogo { + color: var(--rs-color-foreground-base-primary); +} + +.sidebarNavActions { + display: flex; + align-items: center; + gap: var(--rs-space-3); +} + +.sidebarMain { + padding: var(--rs-space-7) var(--rs-space-5); + gap: 0; + min-height: 0; + overflow-y: auto; + scrollbar-width: none; +} + +.topLinks { + width: 100%; + margin-bottom: var(--rs-space-7); +} + +.topLinkItem { + color: var(--rs-color-foreground-base-tertiary); +} + +.topLinkText { + color: var(--rs-color-foreground-base-tertiary); +} + +.topLinkItem[data-active='true'] { + background: transparent; + color: var(--rs-color-foreground-base-primary); +} + +.topLinkItem[data-active='true'] .topLinkText { + color: var(--rs-color-foreground-base-primary); +} + +.sidebarFooter { + display: flex; + align-items: center; + gap: var(--rs-space-3); + height: var(--navbar-height); + box-sizing: border-box; + padding: 0 var(--rs-space-5); + border-top: 0.5px solid var(--rs-color-border-base-primary); + background: var(--rs-color-background-base-secondary); + backdrop-filter: blur(7.5px); +} + +.sidebarMain::-webkit-scrollbar { + display: none; } -.folder { - margin-bottom: var(--rs-space-3); +.sidebarHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--rs-space-3); + height: var(--navbar-height); + box-sizing: border-box; + margin-bottom: 0; + padding: 0 var(--rs-space-5); + background: var(--rs-color-background-base-secondary); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + border-radius: 0; + backdrop-filter: blur(1px); +} + +.mainArea { + flex: 1; + min-width: 0; } -.folderLabel { - font-weight: 500; - font-size: 0.875rem; - color: var(--rs-color-text-base-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; +.cardWrapper { + flex: 1; + display: flex; + padding: 0 0 var(--rs-space-2) 0; + min-height: 0; + background: var(--rs-color-background-base-secondary); +} + +.card { + flex: 1; + display: flex; + flex-direction: column; + border-left: 0.5px solid var(--rs-color-border-base-primary); + box-shadow: var(--rs-shadow-soft); + overflow: visible; +} + +.subNav { + display: flex; + align-items: center; + justify-content: space-between; + height: var(--navbar-height); + padding: var(--rs-space-4) var(--rs-space-7); + background: var(--rs-color-background-base-primary); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + backdrop-filter: blur(1px); +} + +.subNavLeft { + min-width: 0; +} + +.content { + flex: 1; + padding: var(--rs-space-9) var(--rs-space-7); + background: var(--rs-color-background-base-primary); } -.folder > .sidebarList { - margin-top: var(--rs-space-2); +.groupItems { padding-left: var(--rs-space-4); + padding-bottom: var(--rs-space-3); + gap: 0; +} + +.navGroup { + margin-top: 0; } -.navButton { +.navGroup[data-depth='0'] { + margin-top: var(--rs-space-7); +} + +.navGroup .navGroupHeader { + margin: 0; +} + +.navGroup[data-depth='1'] .navGroupHeader { + height: auto; +} + +.navGroup[data-depth='1'] .navGroupLabel { + color: var(--rs-color-foreground-base-primary); +} + +.navGroupTrigger { display: flex; + padding: var(--rs-space-3); align-items: center; + gap: var(--rs-space-3); + align-self: stretch; + width: 100%; height: 32px; - padding: 0 var(--rs-space-4); - border: 1px solid var(--rs-color-border-base-primary); border-radius: var(--rs-radius-2); - font-size: var(--rs-font-size-small); - font-weight: var(--rs-font-weight-medium); - color: var(--rs-color-foreground-base-primary); - text-decoration: none; + box-sizing: border-box; } -.navButton:hover { - background: var(--rs-color-background-base-primary-hover); +.navGroupLabel { + flex: 1; + text-align: left; + color: var(--rs-color-foreground-base-secondary); + font-family: var(--rs-font-body); + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); } -.groupItems { - padding-left: var(--rs-space-4); +.navGroupChevron { + margin-left: auto; } .page { diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 9017c61..00674b7 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -1,23 +1,27 @@ -import { RectangleStackIcon } from '@heroicons/react/24/outline'; import { - Button, - Flex, - Headline, - Link, - Navbar, - Sidebar -} from '@raystack/apsara'; + ArrowLeftIcon, + ArrowRightIcon, + CodeBracketSquareIcon, + RectangleStackIcon, + DocumentTextIcon, + Squares2X2Icon +} from '@heroicons/react/24/outline'; +import { Flex, IconButton, Sidebar } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; -import { useEffect, useRef } from 'react'; -import { Link as RouterLink, useLocation } from 'react-router'; +import { useEffect, useMemo, useRef } from 'react'; +import { Link as RouterLink, useLocation, useNavigate } from 'react-router'; import { MethodBadge } from '@/components/api/method-badge'; import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher'; -import { Footer } from '@/components/ui/footer'; import { Search } from '@/components/ui/search'; +import { Breadcrumbs } from '@/components/ui/breadcrumbs'; +import { getLandingEntries } from '@/lib/config'; +import { getActiveContentDir } from '@/lib/navigation'; +import { usePageContext } from '@/lib/page-context'; import type { Node } from 'fumadocs-core/page-tree'; import type { ThemeLayoutProps } from '@/types'; -import { ContentDirButtons } from './ContentDirButtons'; import styles from './Layout.module.css'; +import { OpenInAI } from './OpenInAI'; +import { SidebarLogo } from './SidebarLogo'; import { VersionSwitcher } from './VersionSwitcher'; const iconMap: Record = { @@ -29,6 +33,24 @@ const iconMap: Record = { 'method-patch': }; +function renderConfigIcon( + icon: string | undefined, + alt: string, + fallback: React.ReactNode +): React.ReactNode { + if (!icon) return fallback; + if (icon.trim().startsWith(' + ); + } + return {alt}; +} + let savedScrollTop = 0; export function Layout({ @@ -39,7 +61,23 @@ export function Layout({ classNames }: ThemeLayoutProps) { const { pathname } = useLocation(); + const navigate = useNavigate(); + const { page, version } = usePageContext(); const scrollRef = useRef(null); + const isApiRoute = pathname === '/apis' || pathname.startsWith('/apis/'); + const isApiBase = (basePath: string) => + pathname === basePath || pathname.startsWith(`${basePath}/`); + const { prev, next } = page ?? { prev: null, next: null }; + + const contentEntries = getLandingEntries(config, version.dir); + const activeContentDir = getActiveContentDir(pathname, config); + const apiEntries = config.api ?? []; + const showTopLinks = contentEntries.length + apiEntries.length > 1; + + const slug = useMemo( + () => (pathname === '/' ? [] : pathname.split('/').filter(Boolean)), + [pathname] + ); useEffect(() => { const el = scrollRef.current; @@ -61,40 +99,6 @@ export function Layout({ return ( - - - - - {config.site.title} - - - - - - - - {config.api?.map(api => ( - - {api.name} API - - ))} - {config.navigation?.links?.map(link => ( - - {link.label} - - ))} - {config.search?.enabled && } - - - - {hideSidebar ? null : ( - + + + + {config.search?.enabled && } + + + + + {showTopLinks ? ( +
+ {contentEntries.map(entry => ( + + )} + classNames={{ root: styles.topLinkItem, text: styles.topLinkText }} + render={} + > + {entry.label} + + ))} + {apiEntries.map(api => ( + + )} + classNames={{ root: styles.topLinkItem, text: styles.topLinkText }} + render={} + > + {api.name} API + + ))} +
+ ) : null} {tree.children.map((item, i) => ( ))}
+ {config.versions?.length ? ( + + + + ) : null}
)} -
- {children} -
+ +
+
+ +
+ {children} +
+
+
+
-