@inklu/docs 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/components.json +25 -0
- package/dist/index.d.mts +271 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +2455 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2368 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shiki.d.mts +10 -0
- package/dist/shiki.d.ts +10 -0
- package/dist/shiki.js +47 -0
- package/dist/shiki.js.map +1 -0
- package/dist/shiki.mjs +22 -0
- package/dist/shiki.mjs.map +1 -0
- package/dist/styles.css +2556 -0
- package/package.json +55 -0
- package/src/components/content/changelog-content.tsx +119 -0
- package/src/components/content/code-block.tsx +111 -0
- package/src/components/content/docs-content.tsx +113 -0
- package/src/components/layout/docs-layout.tsx +192 -0
- package/src/components/layout/site-header.tsx +152 -0
- package/src/components/layout/site-layout.tsx +28 -0
- package/src/components/layout/site-provider.tsx +23 -0
- package/src/components/layout/site-template.tsx +20 -0
- package/src/components/layout/theme-provider.tsx +10 -0
- package/src/components/layout/theme-switcher.tsx +42 -0
- package/src/components/motion-primitives/morph-icon.tsx +97 -0
- package/src/components/motion-primitives/progressive-blur.tsx +64 -0
- package/src/components/motion-primitives/sliding-number.tsx +131 -0
- package/src/components/motion-primitives/text-effect.tsx +290 -0
- package/src/components/motion-primitives/text-morph.tsx +77 -0
- package/src/components/motion-primitives/text-scramble.tsx +87 -0
- package/src/components/shared/command-block.tsx +193 -0
- package/src/components/shared/scroll-fade.tsx +22 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/brand-logo.tsx +68 -0
- package/src/components/ui/button-group.tsx +87 -0
- package/src/components/ui/button.tsx +104 -0
- package/src/components/ui/card.tsx +97 -0
- package/src/components/ui/direction.tsx +6 -0
- package/src/components/ui/hover-card.tsx +51 -0
- package/src/components/ui/item.tsx +201 -0
- package/src/components/ui/separator.tsx +25 -0
- package/src/components/ui/toast.tsx +322 -0
- package/src/components/ui/tooltip.tsx +66 -0
- package/src/hooks/use-copy-to-clipboard.ts +54 -0
- package/src/index.ts +30 -0
- package/src/lib/constants.ts +63 -0
- package/src/lib/motions.ts +21 -0
- package/src/lib/utils/audio.ts +56 -0
- package/src/lib/utils.ts +14 -0
- package/src/shiki.ts +25 -0
- package/src/styles.css +112 -0
- package/src/typeset.css +513 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +11 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@inklu/docs",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.mjs",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"style": "dist/styles.css",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./shiki": {
|
|
18
|
+
"types": "./dist/shiki.d.ts",
|
|
19
|
+
"import": "./dist/shiki.mjs",
|
|
20
|
+
"require": "./dist/shiki.js"
|
|
21
|
+
},
|
|
22
|
+
"./styles.css": "./dist/styles.css"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@base-ui/react": "^1.6.0",
|
|
26
|
+
"@lisse/react": "^0.5.2",
|
|
27
|
+
"@radix-ui/react-icons": "^1.3.2",
|
|
28
|
+
"class-variance-authority": "^0.7.1",
|
|
29
|
+
"clsx": "^2.1.1",
|
|
30
|
+
"motion": "^12.42.2",
|
|
31
|
+
"next-themes": "^0.4.6",
|
|
32
|
+
"react-use-measure": "^2.1.7",
|
|
33
|
+
"shiki": "^4.3.1",
|
|
34
|
+
"tailwind-merge": "^3.6.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"next": "*",
|
|
38
|
+
"react": "*",
|
|
39
|
+
"react-dom": "*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@tailwindcss/cli": "^4",
|
|
43
|
+
"@types/node": "^20.19.43",
|
|
44
|
+
"@types/react": "^19",
|
|
45
|
+
"@types/react-dom": "^19",
|
|
46
|
+
"tailwindcss": "^4",
|
|
47
|
+
"tsup": "^8.0.0",
|
|
48
|
+
"typescript": "^5"
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "tsup && pnpm run build:css",
|
|
52
|
+
"build:css": "tailwindcss -i ./src/styles.css -o ./dist/styles.css",
|
|
53
|
+
"dev": "tsup --watch & pnpm run build:css --watch"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { Badge } from "@/components/ui/badge";
|
|
6
|
+
import { CHANGELOG, type ChangeKind } from "@/lib/constants";
|
|
7
|
+
import { colReveal } from "@/lib/motions";
|
|
8
|
+
import { cn, formatChangelogDate } from "@/lib/utils";
|
|
9
|
+
|
|
10
|
+
function Col({ children }: { children: ReactNode }) {
|
|
11
|
+
const reduced = useReducedMotion() ?? false;
|
|
12
|
+
return (
|
|
13
|
+
<motion.div
|
|
14
|
+
className="mx-auto w-full"
|
|
15
|
+
initial={reduced ? false : "hidden"}
|
|
16
|
+
whileInView="show"
|
|
17
|
+
viewport={{ once: true, margin: "0px 0px -64px 0px" }}
|
|
18
|
+
variants={colReveal}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</motion.div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const KIND_LABEL: Record<ChangeKind, string> = {
|
|
26
|
+
new: "New",
|
|
27
|
+
improved: "Improved",
|
|
28
|
+
fixed: "Fixed",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function KindTag({ kind }: { kind: ChangeKind }) {
|
|
32
|
+
return (
|
|
33
|
+
<Badge variant="secondary" className="align-top mt-0.5">
|
|
34
|
+
{KIND_LABEL[kind]}
|
|
35
|
+
</Badge>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function ChangelogContent() {
|
|
40
|
+
return (
|
|
41
|
+
<div className="w-full">
|
|
42
|
+
<div className="relative mx-auto w-full">
|
|
43
|
+
<main className="typeset typeset-docs mx-auto max-w-(--layout-content-width) w-full">
|
|
44
|
+
{/* Page Header (Title & Date) */}
|
|
45
|
+
<section id="changelog-header" className="scroll-mt-24">
|
|
46
|
+
<Col>
|
|
47
|
+
<h1 className="tracking-tight mb-2">Changelog</h1>
|
|
48
|
+
<p className="text-muted-foreground/60 mb-8 mt-0">
|
|
49
|
+
16 February, 2026
|
|
50
|
+
</p>
|
|
51
|
+
</Col>
|
|
52
|
+
</section>
|
|
53
|
+
|
|
54
|
+
{CHANGELOG.map((entry: (typeof CHANGELOG)[number]) => (
|
|
55
|
+
<section
|
|
56
|
+
key={entry.version}
|
|
57
|
+
id={`v${entry.version}`}
|
|
58
|
+
className="scroll-mt-24"
|
|
59
|
+
>
|
|
60
|
+
<Col>
|
|
61
|
+
<div className="not-typeset mt-16 flex items-center gap-3">
|
|
62
|
+
<Badge
|
|
63
|
+
variant="secondary"
|
|
64
|
+
className="text-foreground align-middle"
|
|
65
|
+
>
|
|
66
|
+
{entry.version === "next" ? "Up next" : `v${entry.version}`}
|
|
67
|
+
</Badge>
|
|
68
|
+
<span className="text-muted-foreground">
|
|
69
|
+
{entry.date
|
|
70
|
+
? formatChangelogDate(entry.date)
|
|
71
|
+
: "In progress"}
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<h2>{entry.title}</h2>
|
|
75
|
+
<p>{entry.summary}</p>
|
|
76
|
+
|
|
77
|
+
<div className="not-typeset mt-4 overflow-hidden rounded-2xl bg-card">
|
|
78
|
+
{entry.changes.map(
|
|
79
|
+
(
|
|
80
|
+
change: (typeof CHANGELOG)[number]["changes"][number],
|
|
81
|
+
i: number,
|
|
82
|
+
) => (
|
|
83
|
+
<div
|
|
84
|
+
key={change.text}
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex gap-3 px-4 py-3",
|
|
87
|
+
i > 0 && "border-t border-border",
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<KindTag kind={change.kind} />
|
|
91
|
+
<span className="text-muted-foreground">
|
|
92
|
+
{change.text}
|
|
93
|
+
</span>
|
|
94
|
+
</div>
|
|
95
|
+
),
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
</Col>
|
|
99
|
+
</section>
|
|
100
|
+
))}
|
|
101
|
+
|
|
102
|
+
<Col>
|
|
103
|
+
<p className="mt-16">
|
|
104
|
+
Full commit history lives on{" "}
|
|
105
|
+
<a
|
|
106
|
+
href="https://github.com/your-username/your-repo"
|
|
107
|
+
target="_blank"
|
|
108
|
+
rel="noopener noreferrer"
|
|
109
|
+
>
|
|
110
|
+
GitHub
|
|
111
|
+
</a>
|
|
112
|
+
.
|
|
113
|
+
</p>
|
|
114
|
+
</Col>
|
|
115
|
+
</main>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { CopyMorphIcon } from "@/components/shared/command-block";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import { anchoredToastManager } from "@/components/ui/toast";
|
|
7
|
+
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard";
|
|
8
|
+
import { cn } from "@/lib/utils";
|
|
9
|
+
import { copy } from "@/lib/utils/audio";
|
|
10
|
+
|
|
11
|
+
export interface CodeBlockProps {
|
|
12
|
+
html: string;
|
|
13
|
+
code: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CodeBlock({ html, code, className }: CodeBlockProps) {
|
|
18
|
+
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
|
19
|
+
const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 1800 });
|
|
20
|
+
|
|
21
|
+
async function handleCopy() {
|
|
22
|
+
const success = await copyToClipboard(code);
|
|
23
|
+
if (success) {
|
|
24
|
+
copy();
|
|
25
|
+
anchoredToastManager.add({
|
|
26
|
+
title: "Copied!",
|
|
27
|
+
positionerProps: {
|
|
28
|
+
anchor: buttonRef.current,
|
|
29
|
+
sideOffset: 6,
|
|
30
|
+
},
|
|
31
|
+
data: {
|
|
32
|
+
tooltipStyle: true,
|
|
33
|
+
},
|
|
34
|
+
timeout: 2000,
|
|
35
|
+
});
|
|
36
|
+
} else {
|
|
37
|
+
anchoredToastManager.add({
|
|
38
|
+
title: "Failed to copy",
|
|
39
|
+
positionerProps: {
|
|
40
|
+
anchor: buttonRef.current,
|
|
41
|
+
sideOffset: 6,
|
|
42
|
+
},
|
|
43
|
+
data: {
|
|
44
|
+
tooltipStyle: true,
|
|
45
|
+
},
|
|
46
|
+
timeout: 2000,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
className={cn(
|
|
54
|
+
"group relative mt-6 overflow-hidden rounded-xl not-typeset",
|
|
55
|
+
"bg-code",
|
|
56
|
+
"transition-colors",
|
|
57
|
+
className,
|
|
58
|
+
)}
|
|
59
|
+
>
|
|
60
|
+
<Button
|
|
61
|
+
ref={buttonRef}
|
|
62
|
+
variant="ghost"
|
|
63
|
+
size="icon-xs"
|
|
64
|
+
onClick={handleCopy}
|
|
65
|
+
className={cn(
|
|
66
|
+
"absolute right-3 top-3 z-20 rounded-lg",
|
|
67
|
+
"bg-transparent",
|
|
68
|
+
"text-muted-foreground",
|
|
69
|
+
"opacity-0",
|
|
70
|
+
"translate-y-1",
|
|
71
|
+
"transition-all duration-200",
|
|
72
|
+
"group-hover:translate-y-0",
|
|
73
|
+
"group-hover:opacity-100",
|
|
74
|
+
"hover:bg-code-foreground/10",
|
|
75
|
+
"hover:text-code-foreground",
|
|
76
|
+
"focus-visible:translate-y-0",
|
|
77
|
+
"focus-visible:opacity-100",
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<CopyMorphIcon copied={isCopied} />
|
|
81
|
+
</Button>
|
|
82
|
+
|
|
83
|
+
<div
|
|
84
|
+
className={cn(
|
|
85
|
+
"[&_pre]:m-0",
|
|
86
|
+
"[&_pre]:overflow-x-auto",
|
|
87
|
+
"[&_pre]:bg-transparent!",
|
|
88
|
+
"[&_pre]:px-5",
|
|
89
|
+
"[&_pre]:py-4",
|
|
90
|
+
"[&_pre]:font-mono",
|
|
91
|
+
"[&_pre]:leading-relaxed",
|
|
92
|
+
|
|
93
|
+
"[&_code]:bg-transparent!",
|
|
94
|
+
"[&_code]:p-0",
|
|
95
|
+
"[&_code]:font-inherit",
|
|
96
|
+
|
|
97
|
+
"[&_.line]:min-h-6",
|
|
98
|
+
|
|
99
|
+
"[&_pre::-webkit-scrollbar]:h-2",
|
|
100
|
+
"[&_pre::-webkit-scrollbar-track]:bg-transparent",
|
|
101
|
+
"[&_pre::-webkit-scrollbar-thumb]:rounded-full",
|
|
102
|
+
"[&_pre::-webkit-scrollbar-thumb]:bg-code-foreground/10",
|
|
103
|
+
"[&_pre::-webkit-scrollbar-thumb:hover]:bg-code-foreground/20",
|
|
104
|
+
)}
|
|
105
|
+
dangerouslySetInnerHTML={{
|
|
106
|
+
__html: html,
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion, useReducedMotion } from "motion/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { CodeBlock } from "@/components/content/code-block";
|
|
6
|
+
import { CommandBlock } from "@/components/shared/command-block";
|
|
7
|
+
import { ScrollFade } from "@/components/shared/scroll-fade";
|
|
8
|
+
import { colReveal } from "@/lib/motions";
|
|
9
|
+
|
|
10
|
+
export function DocsCol({ children }: { children: ReactNode }) {
|
|
11
|
+
const reduced = useReducedMotion() ?? false;
|
|
12
|
+
return (
|
|
13
|
+
<motion.div
|
|
14
|
+
className="mx-auto w-full"
|
|
15
|
+
initial={reduced ? false : "hidden"}
|
|
16
|
+
whileInView="show"
|
|
17
|
+
viewport={{ once: true, margin: "0px 0px -64px 0px" }}
|
|
18
|
+
variants={colReveal}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
</motion.div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function DocsFrame({ children }: { children: ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<div className="relative min-h-screen overflow-x-clip pb-24">
|
|
28
|
+
{/* Top scroll fade */}
|
|
29
|
+
<ScrollFade />
|
|
30
|
+
|
|
31
|
+
{/* Shiki code block styles */}
|
|
32
|
+
<style>{`.docs-code .shiki{margin:0;padding:16px 18px;overflow-x:auto;line-height:1.65;background:transparent !important;font-size:var(--typeset-size)}.docs-code .shiki code{background:transparent;padding:0}`}</style>
|
|
33
|
+
|
|
34
|
+
<div className="w-full">
|
|
35
|
+
<div className="relative mx-auto w-full">
|
|
36
|
+
<main className="typeset typeset-docs mx-auto max-w-(--layout-content-width) w-full">
|
|
37
|
+
{children}
|
|
38
|
+
</main>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
import { TextEffect } from "@/components/motion-primitives/text-effect";
|
|
46
|
+
|
|
47
|
+
export function DocsHeader({
|
|
48
|
+
title,
|
|
49
|
+
date,
|
|
50
|
+
children,
|
|
51
|
+
}: {
|
|
52
|
+
title: string;
|
|
53
|
+
date: string;
|
|
54
|
+
children?: ReactNode;
|
|
55
|
+
}) {
|
|
56
|
+
return (
|
|
57
|
+
<section id="docs-header" className="scroll-mt-24">
|
|
58
|
+
<DocsCol>
|
|
59
|
+
<h1 className="tracking-tight mb-2">
|
|
60
|
+
<TextEffect preset="fade-in-blur" per="word">
|
|
61
|
+
{title}
|
|
62
|
+
</TextEffect>
|
|
63
|
+
</h1>
|
|
64
|
+
<p className="text-muted-foreground/60 mb-8 mt-0">{date}</p>
|
|
65
|
+
{children}
|
|
66
|
+
</DocsCol>
|
|
67
|
+
</section>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function DocsOverview({ children }: { children: ReactNode }) {
|
|
72
|
+
return (
|
|
73
|
+
<section id="overview" className="scroll-mt-24">
|
|
74
|
+
<DocsCol>{children}</DocsCol>
|
|
75
|
+
</section>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function DocsSection({
|
|
80
|
+
id,
|
|
81
|
+
title,
|
|
82
|
+
children,
|
|
83
|
+
}: {
|
|
84
|
+
id: string;
|
|
85
|
+
title?: string;
|
|
86
|
+
children: ReactNode;
|
|
87
|
+
}) {
|
|
88
|
+
return (
|
|
89
|
+
<section id={id} className="scroll-mt-24 mt-12">
|
|
90
|
+
<DocsCol>
|
|
91
|
+
{title && <h2>{title}</h2>}
|
|
92
|
+
{children}
|
|
93
|
+
</DocsCol>
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function DocsInstallation({
|
|
99
|
+
install,
|
|
100
|
+
}: {
|
|
101
|
+
install: { id: string; command: string }[];
|
|
102
|
+
}) {
|
|
103
|
+
return (
|
|
104
|
+
<DocsSection id="installation" title="Installation">
|
|
105
|
+
<p>Install the package using your preferred package manager:</p>
|
|
106
|
+
<CommandBlock items={install} />
|
|
107
|
+
</DocsSection>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function DocsCode({ code, html }: { code: string; html: string }) {
|
|
112
|
+
return <CodeBlock code={code} html={html} />;
|
|
113
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowLeftIcon } from "@radix-ui/react-icons";
|
|
4
|
+
import { motion, useMotionValueEvent, useScroll } from "motion/react";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { SlidingNumber } from "../motion-primitives/sliding-number";
|
|
9
|
+
import { ScrollFade } from "../shared/scroll-fade";
|
|
10
|
+
|
|
11
|
+
export type DocsLayoutProps = {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
links: {
|
|
14
|
+
group: string;
|
|
15
|
+
items: { title: string; url: string; isAnchor: boolean }[];
|
|
16
|
+
}[];
|
|
17
|
+
indexUrl?: string;
|
|
18
|
+
indexLabel?: string;
|
|
19
|
+
header?: React.ReactNode;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function DocsLayout({
|
|
23
|
+
children,
|
|
24
|
+
links,
|
|
25
|
+
indexUrl = "/",
|
|
26
|
+
indexLabel = "Index",
|
|
27
|
+
header,
|
|
28
|
+
}: DocsLayoutProps) {
|
|
29
|
+
const pathname = usePathname();
|
|
30
|
+
const [activeHash, setActiveHash] = React.useState("");
|
|
31
|
+
|
|
32
|
+
const { scrollYProgress } = useScroll();
|
|
33
|
+
const [progress, setProgress] = React.useState(0);
|
|
34
|
+
|
|
35
|
+
useMotionValueEvent(scrollYProgress, "change", (latest) => {
|
|
36
|
+
setProgress(Math.round(latest * 100));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Scrollspy to detect currently active section
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
// Only extract sections for the current page path
|
|
42
|
+
const currentPathLinks = links.flatMap((g) =>
|
|
43
|
+
g.items.filter((i) => i.isAnchor && i.url.startsWith(pathname)),
|
|
44
|
+
);
|
|
45
|
+
const sections = currentPathLinks.map((i) => i.url.split("#")[1]);
|
|
46
|
+
if (!sections.length) {
|
|
47
|
+
setActiveHash("");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const observerOptions = {
|
|
52
|
+
root: null,
|
|
53
|
+
rootMargin: "-20% 0px -60% 0px", // Trigger when the section occupies the main focal area
|
|
54
|
+
threshold: 0,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const observer = new IntersectionObserver((entries) => {
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.isIntersecting) {
|
|
60
|
+
setActiveHash(`#${entry.target.id}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}, observerOptions);
|
|
64
|
+
|
|
65
|
+
for (const id of sections) {
|
|
66
|
+
if (id) {
|
|
67
|
+
const el = document.getElementById(id);
|
|
68
|
+
if (el) observer.observe(el);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Capture initial hash if present
|
|
73
|
+
if (typeof window !== "undefined" && window.location.hash) {
|
|
74
|
+
setActiveHash(window.location.hash);
|
|
75
|
+
} else {
|
|
76
|
+
// default to first
|
|
77
|
+
setActiveHash(`#${sections[0]}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
observer.disconnect();
|
|
82
|
+
};
|
|
83
|
+
}, [links, pathname]);
|
|
84
|
+
|
|
85
|
+
const handleAnchorClick = (
|
|
86
|
+
e: React.MouseEvent<HTMLAnchorElement>,
|
|
87
|
+
url: string,
|
|
88
|
+
) => {
|
|
89
|
+
if (!url.includes("#")) return;
|
|
90
|
+
|
|
91
|
+
const hash = url.split("#")[1];
|
|
92
|
+
const element = document.getElementById(hash);
|
|
93
|
+
|
|
94
|
+
if (element && url.startsWith(pathname)) {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
element.scrollIntoView({ behavior: "smooth" });
|
|
97
|
+
window.history.pushState(null, "", `#${hash}`);
|
|
98
|
+
setActiveHash(`#${hash}`);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isLinkActive = (url: string, isAnchor: boolean) => {
|
|
103
|
+
if (isAnchor) {
|
|
104
|
+
if (!url.startsWith(pathname)) return false;
|
|
105
|
+
const hash = url.split("#")[1];
|
|
106
|
+
if (activeHash) {
|
|
107
|
+
return activeHash === `#${hash}`;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return pathname === url;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<ScrollFade />
|
|
117
|
+
{header}
|
|
118
|
+
<div className="mx-auto w-full max-w-(--layout-max-width) px-6 py-12 md:py-24 flex flex-col md:flex-row gap-10 md:gap-16">
|
|
119
|
+
{/* Sticky Minimalist Sidebar */}
|
|
120
|
+
<aside className="w-full md:w-48 shrink-0 md:sticky md:top-24 md:self-start flex flex-col gap-6 md:gap-8">
|
|
121
|
+
<div className="flex items-center justify-between">
|
|
122
|
+
<Link
|
|
123
|
+
href={indexUrl}
|
|
124
|
+
className="text-sm text-muted-foreground hover:text-foreground flex items-center gap-1.5 transition-opacity duration-200 select-none opacity-60 hover:opacity-100"
|
|
125
|
+
>
|
|
126
|
+
<ArrowLeftIcon className="size-3.5" />
|
|
127
|
+
{indexLabel}
|
|
128
|
+
</Link>
|
|
129
|
+
|
|
130
|
+
<div className="text-xs font-mono text-muted-foreground opacity-50 flex items-center gap-0.5">
|
|
131
|
+
<SlidingNumber value={progress} />%
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
<div className="flex flex-row flex-wrap gap-x-6 gap-y-2 md:flex-col md:gap-y-4">
|
|
135
|
+
{links.map((group) => (
|
|
136
|
+
<div key={group.group} className="flex flex-col gap-1.5 md:gap-2">
|
|
137
|
+
<nav className="flex flex-row flex-wrap gap-x-1 gap-y-1 md:flex-col md:gap-y-1">
|
|
138
|
+
{group.items.map((link) => {
|
|
139
|
+
const active = isLinkActive(link.url, link.isAnchor);
|
|
140
|
+
return (
|
|
141
|
+
<Link
|
|
142
|
+
key={link.title}
|
|
143
|
+
href={link.url}
|
|
144
|
+
onClick={(e) => handleAnchorClick(e, link.url)}
|
|
145
|
+
className={`relative text-sm transition-colors duration-200 px-2 py-1.5 -ml-2 rounded-md ${
|
|
146
|
+
active
|
|
147
|
+
? "text-foreground font-medium"
|
|
148
|
+
: "text-muted-foreground hover:text-foreground"
|
|
149
|
+
}`}
|
|
150
|
+
>
|
|
151
|
+
{active && (
|
|
152
|
+
<motion.div
|
|
153
|
+
layoutId="activeDocsLinkBg"
|
|
154
|
+
className="absolute inset-0 bg-secondary/50 rounded-md"
|
|
155
|
+
transition={{
|
|
156
|
+
type: "spring",
|
|
157
|
+
duration: 0.3,
|
|
158
|
+
bounce: 0,
|
|
159
|
+
}}
|
|
160
|
+
/>
|
|
161
|
+
)}
|
|
162
|
+
<span className="relative z-10">{link.title}</span>
|
|
163
|
+
</Link>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</nav>
|
|
167
|
+
</div>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
</aside>
|
|
171
|
+
|
|
172
|
+
{/* Main Content */}
|
|
173
|
+
<main className="flex-1 min-w-0">{children}</main>
|
|
174
|
+
</div>
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function DocsNavbar({
|
|
180
|
+
logo,
|
|
181
|
+
right,
|
|
182
|
+
}: {
|
|
183
|
+
logo?: React.ReactNode;
|
|
184
|
+
right?: React.ReactNode;
|
|
185
|
+
}) {
|
|
186
|
+
return (
|
|
187
|
+
<header className="sticky top-4 z-(--z-header) mx-auto flex w-full max-w-(--layout-max-width) items-center justify-between px-(--layout-padding) py-3">
|
|
188
|
+
{logo}
|
|
189
|
+
{right}
|
|
190
|
+
</header>
|
|
191
|
+
);
|
|
192
|
+
}
|