@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
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import type {
|
|
3
|
+
TargetAndTransition,
|
|
4
|
+
Transition,
|
|
5
|
+
Variant,
|
|
6
|
+
Variants,
|
|
7
|
+
} from "motion/react";
|
|
8
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { cn } from "@/lib/utils";
|
|
11
|
+
|
|
12
|
+
export type PresetType = "blur" | "fade-in-blur" | "scale" | "fade" | "slide";
|
|
13
|
+
|
|
14
|
+
export type PerType = "word" | "char" | "line";
|
|
15
|
+
|
|
16
|
+
export type TextEffectProps = {
|
|
17
|
+
children: string;
|
|
18
|
+
per?: PerType;
|
|
19
|
+
as?: keyof React.JSX.IntrinsicElements;
|
|
20
|
+
variants?: {
|
|
21
|
+
container?: Variants;
|
|
22
|
+
item?: Variants;
|
|
23
|
+
};
|
|
24
|
+
className?: string;
|
|
25
|
+
preset?: PresetType;
|
|
26
|
+
delay?: number;
|
|
27
|
+
speedReveal?: number;
|
|
28
|
+
speedSegment?: number;
|
|
29
|
+
trigger?: boolean;
|
|
30
|
+
onAnimationComplete?: () => void;
|
|
31
|
+
onAnimationStart?: () => void;
|
|
32
|
+
segmentWrapperClassName?: string;
|
|
33
|
+
containerTransition?: Transition;
|
|
34
|
+
segmentTransition?: Transition;
|
|
35
|
+
style?: React.CSSProperties;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const defaultStaggerTimes: Record<PerType, number> = {
|
|
39
|
+
char: 0.03,
|
|
40
|
+
word: 0.05,
|
|
41
|
+
line: 0.1,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const defaultContainerVariants: Variants = {
|
|
45
|
+
hidden: { opacity: 0 },
|
|
46
|
+
visible: {
|
|
47
|
+
opacity: 1,
|
|
48
|
+
transition: {
|
|
49
|
+
staggerChildren: 0.05,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
exit: {
|
|
53
|
+
transition: { staggerChildren: 0.05, staggerDirection: -1 },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const defaultItemVariants: Variants = {
|
|
58
|
+
hidden: { opacity: 0 },
|
|
59
|
+
visible: {
|
|
60
|
+
opacity: 1,
|
|
61
|
+
},
|
|
62
|
+
exit: { opacity: 0 },
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const presetVariants: Record<
|
|
66
|
+
PresetType,
|
|
67
|
+
{ container: Variants; item: Variants }
|
|
68
|
+
> = {
|
|
69
|
+
blur: {
|
|
70
|
+
container: defaultContainerVariants,
|
|
71
|
+
item: {
|
|
72
|
+
hidden: { opacity: 0, filter: "blur(12px)" },
|
|
73
|
+
visible: { opacity: 1, filter: "blur(0px)" },
|
|
74
|
+
exit: { opacity: 0, filter: "blur(12px)" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
"fade-in-blur": {
|
|
78
|
+
container: defaultContainerVariants,
|
|
79
|
+
item: {
|
|
80
|
+
hidden: { opacity: 0, y: 8, filter: "blur(12px)" },
|
|
81
|
+
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
|
|
82
|
+
exit: { opacity: 0, y: 8, filter: "blur(12px)" },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
scale: {
|
|
86
|
+
container: defaultContainerVariants,
|
|
87
|
+
item: {
|
|
88
|
+
hidden: { opacity: 0, scale: 0.95 },
|
|
89
|
+
visible: { opacity: 1, scale: 1 },
|
|
90
|
+
exit: { opacity: 0, scale: 0.95 },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
fade: {
|
|
94
|
+
container: defaultContainerVariants,
|
|
95
|
+
item: {
|
|
96
|
+
hidden: { opacity: 0 },
|
|
97
|
+
visible: { opacity: 1 },
|
|
98
|
+
exit: { opacity: 0 },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
slide: {
|
|
102
|
+
container: defaultContainerVariants,
|
|
103
|
+
item: {
|
|
104
|
+
hidden: { opacity: 0, y: 8 },
|
|
105
|
+
visible: { opacity: 1, y: 0 },
|
|
106
|
+
exit: { opacity: 0, y: 8 },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const AnimationComponent: React.FC<{
|
|
112
|
+
segment: string;
|
|
113
|
+
variants: Variants;
|
|
114
|
+
per: "line" | "word" | "char";
|
|
115
|
+
segmentWrapperClassName?: string;
|
|
116
|
+
}> = React.memo(({ segment, variants, per, segmentWrapperClassName }) => {
|
|
117
|
+
const content =
|
|
118
|
+
per === "line" ? (
|
|
119
|
+
<motion.span variants={variants} className="block">
|
|
120
|
+
{segment}
|
|
121
|
+
</motion.span>
|
|
122
|
+
) : per === "word" ? (
|
|
123
|
+
<motion.span
|
|
124
|
+
aria-hidden="true"
|
|
125
|
+
variants={variants}
|
|
126
|
+
className="inline-block whitespace-pre"
|
|
127
|
+
>
|
|
128
|
+
{segment}
|
|
129
|
+
</motion.span>
|
|
130
|
+
) : (
|
|
131
|
+
<motion.span className="inline-block whitespace-pre">
|
|
132
|
+
{segment.split("").map((char, charIndex) => (
|
|
133
|
+
<motion.span
|
|
134
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: Characters are static and don't change order
|
|
135
|
+
key={`char-${charIndex}`}
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
variants={variants}
|
|
138
|
+
className="inline-block whitespace-pre"
|
|
139
|
+
>
|
|
140
|
+
{char}
|
|
141
|
+
</motion.span>
|
|
142
|
+
))}
|
|
143
|
+
</motion.span>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (!segmentWrapperClassName) {
|
|
147
|
+
return content;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const defaultWrapperClassName = per === "line" ? "block" : "inline-block";
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<span className={cn(defaultWrapperClassName, segmentWrapperClassName)}>
|
|
154
|
+
{content}
|
|
155
|
+
</span>
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
AnimationComponent.displayName = "AnimationComponent";
|
|
160
|
+
|
|
161
|
+
const splitText = (text: string, per: PerType) => {
|
|
162
|
+
if (per === "line") return text.split("\n");
|
|
163
|
+
return text.split(/(\s+)/);
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const hasTransition = (
|
|
167
|
+
variant?: Variant,
|
|
168
|
+
): variant is TargetAndTransition & { transition?: Transition } => {
|
|
169
|
+
if (!variant) return false;
|
|
170
|
+
return typeof variant === "object" && "transition" in variant;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const createVariantsWithTransition = (
|
|
174
|
+
baseVariants: Variants,
|
|
175
|
+
transition?: Transition & { exit?: Transition },
|
|
176
|
+
): Variants => {
|
|
177
|
+
if (!transition) return baseVariants;
|
|
178
|
+
|
|
179
|
+
const { exit: _, ...mainTransition } = transition;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
...baseVariants,
|
|
183
|
+
visible: {
|
|
184
|
+
...baseVariants.visible,
|
|
185
|
+
transition: {
|
|
186
|
+
...(hasTransition(baseVariants.visible)
|
|
187
|
+
? baseVariants.visible.transition
|
|
188
|
+
: {}),
|
|
189
|
+
...mainTransition,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
exit: {
|
|
193
|
+
...baseVariants.exit,
|
|
194
|
+
transition: {
|
|
195
|
+
...(hasTransition(baseVariants.exit)
|
|
196
|
+
? baseVariants.exit.transition
|
|
197
|
+
: {}),
|
|
198
|
+
...mainTransition,
|
|
199
|
+
staggerDirection: -1,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export function TextEffect({
|
|
206
|
+
children,
|
|
207
|
+
per = "word",
|
|
208
|
+
as = "p",
|
|
209
|
+
variants,
|
|
210
|
+
className,
|
|
211
|
+
preset = "fade",
|
|
212
|
+
delay = 0,
|
|
213
|
+
speedReveal = 1,
|
|
214
|
+
speedSegment = 1,
|
|
215
|
+
trigger = true,
|
|
216
|
+
onAnimationComplete,
|
|
217
|
+
onAnimationStart,
|
|
218
|
+
segmentWrapperClassName,
|
|
219
|
+
containerTransition,
|
|
220
|
+
segmentTransition,
|
|
221
|
+
style,
|
|
222
|
+
}: TextEffectProps) {
|
|
223
|
+
const segments = splitText(children, per);
|
|
224
|
+
const MotionTag = motion[as as keyof typeof motion] as typeof motion.div;
|
|
225
|
+
|
|
226
|
+
const baseVariants = preset
|
|
227
|
+
? presetVariants[preset]
|
|
228
|
+
: { container: defaultContainerVariants, item: defaultItemVariants };
|
|
229
|
+
|
|
230
|
+
const stagger = defaultStaggerTimes[per] / speedReveal;
|
|
231
|
+
|
|
232
|
+
const baseDuration = 0.3 / speedSegment;
|
|
233
|
+
|
|
234
|
+
const customStagger = hasTransition(variants?.container?.visible ?? {})
|
|
235
|
+
? (variants?.container?.visible as TargetAndTransition).transition
|
|
236
|
+
?.staggerChildren
|
|
237
|
+
: undefined;
|
|
238
|
+
|
|
239
|
+
const customDelay = hasTransition(variants?.container?.visible ?? {})
|
|
240
|
+
? (variants?.container?.visible as TargetAndTransition).transition
|
|
241
|
+
?.delayChildren
|
|
242
|
+
: undefined;
|
|
243
|
+
|
|
244
|
+
const computedVariants = {
|
|
245
|
+
container: createVariantsWithTransition(
|
|
246
|
+
variants?.container || baseVariants.container,
|
|
247
|
+
{
|
|
248
|
+
staggerChildren: customStagger ?? stagger,
|
|
249
|
+
delayChildren: customDelay ?? delay,
|
|
250
|
+
...containerTransition,
|
|
251
|
+
exit: {
|
|
252
|
+
staggerChildren: customStagger ?? stagger,
|
|
253
|
+
staggerDirection: -1,
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
),
|
|
257
|
+
item: createVariantsWithTransition(variants?.item || baseVariants.item, {
|
|
258
|
+
duration: baseDuration,
|
|
259
|
+
...segmentTransition,
|
|
260
|
+
}),
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<AnimatePresence mode="popLayout">
|
|
265
|
+
{trigger && (
|
|
266
|
+
<MotionTag
|
|
267
|
+
initial="hidden"
|
|
268
|
+
animate="visible"
|
|
269
|
+
exit="exit"
|
|
270
|
+
variants={computedVariants.container}
|
|
271
|
+
className={className}
|
|
272
|
+
onAnimationComplete={onAnimationComplete}
|
|
273
|
+
onAnimationStart={onAnimationStart}
|
|
274
|
+
style={style}
|
|
275
|
+
>
|
|
276
|
+
{per !== "line" ? <span className="sr-only">{children}</span> : null}
|
|
277
|
+
{segments.map((segment, index) => (
|
|
278
|
+
<AnimationComponent
|
|
279
|
+
key={`${per}-${index}-${segment}`}
|
|
280
|
+
segment={segment}
|
|
281
|
+
variants={computedVariants.item}
|
|
282
|
+
per={per}
|
|
283
|
+
segmentWrapperClassName={segmentWrapperClassName}
|
|
284
|
+
/>
|
|
285
|
+
))}
|
|
286
|
+
</MotionTag>
|
|
287
|
+
)}
|
|
288
|
+
</AnimatePresence>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import {
|
|
3
|
+
AnimatePresence,
|
|
4
|
+
motion,
|
|
5
|
+
type Transition,
|
|
6
|
+
type Variants,
|
|
7
|
+
} from "motion/react";
|
|
8
|
+
import { useId, useMemo } from "react";
|
|
9
|
+
import { cn } from "@/lib/utils";
|
|
10
|
+
|
|
11
|
+
export type TextMorphProps = {
|
|
12
|
+
children: string;
|
|
13
|
+
as?: React.ElementType;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: React.CSSProperties;
|
|
16
|
+
variants?: Variants;
|
|
17
|
+
transition?: Transition;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function TextMorph({
|
|
21
|
+
children,
|
|
22
|
+
as: Component = "p",
|
|
23
|
+
className,
|
|
24
|
+
style,
|
|
25
|
+
variants,
|
|
26
|
+
transition,
|
|
27
|
+
}: TextMorphProps) {
|
|
28
|
+
const uniqueId = useId();
|
|
29
|
+
|
|
30
|
+
const characters = useMemo(() => {
|
|
31
|
+
const charCounts: Record<string, number> = {};
|
|
32
|
+
|
|
33
|
+
return children.split("").map((char) => {
|
|
34
|
+
const lowerChar = char.toLowerCase();
|
|
35
|
+
charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
|
|
39
|
+
label: char === " " ? "\u00A0" : char,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
}, [children, uniqueId]);
|
|
43
|
+
|
|
44
|
+
const defaultVariants: Variants = {
|
|
45
|
+
initial: { opacity: 0 },
|
|
46
|
+
animate: { opacity: 1 },
|
|
47
|
+
exit: { opacity: 0 },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const defaultTransition: Transition = {
|
|
51
|
+
type: "spring",
|
|
52
|
+
bounce: 0,
|
|
53
|
+
duration: 0.3,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Component className={cn(className)} aria-label={children} style={style}>
|
|
58
|
+
<AnimatePresence mode="popLayout" initial={false}>
|
|
59
|
+
{characters.map((character) => (
|
|
60
|
+
<motion.span
|
|
61
|
+
key={character.id}
|
|
62
|
+
layoutId={character.id}
|
|
63
|
+
className="inline-block"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
initial="initial"
|
|
66
|
+
animate="animate"
|
|
67
|
+
exit="exit"
|
|
68
|
+
variants={variants || defaultVariants}
|
|
69
|
+
transition={transition || defaultTransition}
|
|
70
|
+
>
|
|
71
|
+
{character.label}
|
|
72
|
+
</motion.span>
|
|
73
|
+
))}
|
|
74
|
+
</AnimatePresence>
|
|
75
|
+
</Component>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { type MotionProps, motion } from "motion/react";
|
|
3
|
+
import { type JSX, useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export type TextScrambleProps = {
|
|
6
|
+
children: string;
|
|
7
|
+
duration?: number;
|
|
8
|
+
speed?: number;
|
|
9
|
+
characterSet?: string;
|
|
10
|
+
as?: React.ElementType;
|
|
11
|
+
className?: string;
|
|
12
|
+
trigger?: boolean;
|
|
13
|
+
onScrambleComplete?: () => void;
|
|
14
|
+
} & MotionProps;
|
|
15
|
+
|
|
16
|
+
const defaultChars =
|
|
17
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
18
|
+
|
|
19
|
+
export function TextScramble({
|
|
20
|
+
children,
|
|
21
|
+
duration = 0.8,
|
|
22
|
+
speed = 0.04,
|
|
23
|
+
characterSet = defaultChars,
|
|
24
|
+
className,
|
|
25
|
+
as: Component = "p",
|
|
26
|
+
trigger = true,
|
|
27
|
+
onScrambleComplete,
|
|
28
|
+
...props
|
|
29
|
+
}: TextScrambleProps) {
|
|
30
|
+
const MotionComponent = motion.create(
|
|
31
|
+
Component as keyof JSX.IntrinsicElements,
|
|
32
|
+
);
|
|
33
|
+
const [scrambledText, setScrambledText] = useState<string | null>(null);
|
|
34
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
35
|
+
const text = children;
|
|
36
|
+
const displayText = scrambledText ?? children;
|
|
37
|
+
|
|
38
|
+
const scramble = async () => {
|
|
39
|
+
if (isAnimating) return;
|
|
40
|
+
setIsAnimating(true);
|
|
41
|
+
|
|
42
|
+
const steps = duration / speed;
|
|
43
|
+
let step = 0;
|
|
44
|
+
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
let scrambled = "";
|
|
47
|
+
const progress = step / steps;
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < text.length; i++) {
|
|
50
|
+
if (text[i] === " ") {
|
|
51
|
+
scrambled += " ";
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (progress * text.length > i) {
|
|
56
|
+
scrambled += text[i];
|
|
57
|
+
} else {
|
|
58
|
+
scrambled +=
|
|
59
|
+
characterSet[Math.floor(Math.random() * characterSet.length)];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setScrambledText(scrambled);
|
|
64
|
+
step++;
|
|
65
|
+
|
|
66
|
+
if (step > steps) {
|
|
67
|
+
clearInterval(interval);
|
|
68
|
+
setScrambledText(null);
|
|
69
|
+
setIsAnimating(false);
|
|
70
|
+
onScrambleComplete?.();
|
|
71
|
+
}
|
|
72
|
+
}, speed * 1000);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!trigger) return;
|
|
77
|
+
|
|
78
|
+
scramble();
|
|
79
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: trigger controls the animation
|
|
80
|
+
}, [trigger, scramble]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<MotionComponent className={className} {...props}>
|
|
84
|
+
{displayText}
|
|
85
|
+
</MotionComponent>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
|
4
|
+
import { useRef, useState } from "react";
|
|
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 { deny, tap } from "@/lib/utils/audio";
|
|
10
|
+
import { TextMorph } from "../motion-primitives/text-morph";
|
|
11
|
+
|
|
12
|
+
const SLIDE_SPRING = { type: "spring", stiffness: 520, damping: 34 } as const;
|
|
13
|
+
|
|
14
|
+
const MORPH_EASE = { duration: 0.16, ease: "easeOut" } as const;
|
|
15
|
+
|
|
16
|
+
export function CopyMorphIcon({ copied }: { copied: boolean }) {
|
|
17
|
+
const reduced = useReducedMotion() ?? false;
|
|
18
|
+
const t = reduced ? { duration: 0 } : MORPH_EASE;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<span
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
className="relative grid size-3.5 place-items-center"
|
|
24
|
+
>
|
|
25
|
+
<AnimatePresence initial={false} mode="popLayout">
|
|
26
|
+
{copied ? (
|
|
27
|
+
<motion.svg
|
|
28
|
+
key="check"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
width="15"
|
|
31
|
+
height="15"
|
|
32
|
+
viewBox="0 0 16 16"
|
|
33
|
+
fill="none"
|
|
34
|
+
className="absolute text-foreground"
|
|
35
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
36
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
37
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
38
|
+
transition={t}
|
|
39
|
+
>
|
|
40
|
+
<motion.path
|
|
41
|
+
d="M1.83398 10.0625L6.00065 13.5L14.1673 2.5"
|
|
42
|
+
stroke="currentColor"
|
|
43
|
+
strokeWidth="1.25"
|
|
44
|
+
strokeLinecap="round"
|
|
45
|
+
strokeLinejoin="round"
|
|
46
|
+
initial={{ pathLength: reduced ? 1 : 0 }}
|
|
47
|
+
animate={{ pathLength: 1 }}
|
|
48
|
+
transition={
|
|
49
|
+
reduced ? { duration: 0 } : { duration: 0.18, ease: "easeOut" }
|
|
50
|
+
}
|
|
51
|
+
/>
|
|
52
|
+
</motion.svg>
|
|
53
|
+
) : (
|
|
54
|
+
<motion.svg
|
|
55
|
+
key="copy"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
width="15"
|
|
58
|
+
height="15"
|
|
59
|
+
viewBox="0 0 16 16"
|
|
60
|
+
fill="none"
|
|
61
|
+
className="absolute"
|
|
62
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
63
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
64
|
+
exit={{ opacity: 0, scale: 0.8 }}
|
|
65
|
+
transition={t}
|
|
66
|
+
>
|
|
67
|
+
<path
|
|
68
|
+
d="M10.1667 3.16634H12.8334V14.1663H3.16675V3.16634H5.83341M5.83341 1.83301H10.1667V4.83301H5.83341V1.83301Z"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
strokeWidth="1.25"
|
|
71
|
+
strokeLinecap="round"
|
|
72
|
+
strokeLinejoin="round"
|
|
73
|
+
/>
|
|
74
|
+
</motion.svg>
|
|
75
|
+
)}
|
|
76
|
+
</AnimatePresence>
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function CommandBlock({
|
|
82
|
+
items,
|
|
83
|
+
}: {
|
|
84
|
+
items: { id: string; command: string }[];
|
|
85
|
+
}) {
|
|
86
|
+
const [active, setActive] = useState(0);
|
|
87
|
+
const { copyToClipboard, isCopied } = useCopyToClipboard({ timeout: 2000 });
|
|
88
|
+
const reduced = useReducedMotion() ?? false;
|
|
89
|
+
const cur = items[active];
|
|
90
|
+
const copyButtonRef = useRef<HTMLButtonElement>(null);
|
|
91
|
+
|
|
92
|
+
const handleCopy = async () => {
|
|
93
|
+
const success = await copyToClipboard(cur.command);
|
|
94
|
+
if (success) {
|
|
95
|
+
tap();
|
|
96
|
+
anchoredToastManager.add({
|
|
97
|
+
title: "Copied!",
|
|
98
|
+
positionerProps: {
|
|
99
|
+
anchor: copyButtonRef.current,
|
|
100
|
+
sideOffset: 6,
|
|
101
|
+
},
|
|
102
|
+
data: {
|
|
103
|
+
tooltipStyle: true,
|
|
104
|
+
},
|
|
105
|
+
timeout: 2000,
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
deny();
|
|
109
|
+
anchoredToastManager.add({
|
|
110
|
+
title: "Failed to copy",
|
|
111
|
+
positionerProps: {
|
|
112
|
+
anchor: copyButtonRef.current,
|
|
113
|
+
sideOffset: 6,
|
|
114
|
+
},
|
|
115
|
+
data: {
|
|
116
|
+
tooltipStyle: true,
|
|
117
|
+
},
|
|
118
|
+
timeout: 2000,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="not-typeset mt-6 rounded-2xl border-0 bg-code overflow-hidden">
|
|
125
|
+
{items.length > 1 && (
|
|
126
|
+
<div className="flex items-center gap-0.5 px-3 pb-1.5 pt-3 justify-between">
|
|
127
|
+
<div className="flex items-center gap-0.5 ">
|
|
128
|
+
{items.map((it, i) => {
|
|
129
|
+
const isActive = i === active;
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
key={it.id}
|
|
133
|
+
type="button"
|
|
134
|
+
onClick={() => setActive(i)}
|
|
135
|
+
className={cn(
|
|
136
|
+
"relative cursor-pointer rounded-md px-1.5 py-0.5 font-mono outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring",
|
|
137
|
+
isActive
|
|
138
|
+
? "text-foreground"
|
|
139
|
+
: "text-muted-foreground hover:bg-accent hover:text-foreground",
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
{isActive && (
|
|
143
|
+
<motion.span
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
layoutId="pkg-tab-active"
|
|
146
|
+
transition={reduced ? { duration: 0 } : SLIDE_SPRING}
|
|
147
|
+
className="absolute inset-0 rounded-md bg-accent"
|
|
148
|
+
/>
|
|
149
|
+
)}
|
|
150
|
+
<span className="relative">{it.id}</span>
|
|
151
|
+
</button>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
<Button
|
|
156
|
+
ref={copyButtonRef}
|
|
157
|
+
variant="ghost"
|
|
158
|
+
size="icon"
|
|
159
|
+
onClick={handleCopy}
|
|
160
|
+
className="ml-auto text-muted-foreground hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring"
|
|
161
|
+
title="Copy to clipboard"
|
|
162
|
+
aria-label="Copy install command"
|
|
163
|
+
>
|
|
164
|
+
<CopyMorphIcon copied={isCopied} />
|
|
165
|
+
</Button>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<div className="flex items-center">
|
|
170
|
+
<div className="flex-1 overflow-x-auto">
|
|
171
|
+
<pre className="flex items-center overflow-x-auto px-5 py-4 font-mono text-foreground">
|
|
172
|
+
<code>
|
|
173
|
+
<TextMorph>{cur.command}</TextMorph>
|
|
174
|
+
</code>
|
|
175
|
+
</pre>
|
|
176
|
+
</div>
|
|
177
|
+
{items.length === 1 && (
|
|
178
|
+
<Button
|
|
179
|
+
ref={copyButtonRef}
|
|
180
|
+
variant="ghost"
|
|
181
|
+
size="icon-sm"
|
|
182
|
+
onClick={handleCopy}
|
|
183
|
+
className="mr-2 text-muted-foreground hover:text-foreground focus-visible:ring-1 focus-visible:ring-ring"
|
|
184
|
+
title="Copy to clipboard"
|
|
185
|
+
aria-label="Copy install command"
|
|
186
|
+
>
|
|
187
|
+
<CopyMorphIcon copied={isCopied} />
|
|
188
|
+
</Button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
export function ScrollFade() {
|
|
6
|
+
const [visible, setVisible] = useState(false);
|
|
7
|
+
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const handleScroll = () => setVisible(window.scrollY > 50);
|
|
10
|
+
handleScroll();
|
|
11
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
12
|
+
return () => window.removeEventListener("scroll", handleScroll);
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div
|
|
17
|
+
className={`pointer-events-none fixed inset-x-0 top-0 z-(--z-scroll-fade) h-20 bg-gradient-to-b from-background to-transparent transition-opacity duration-300 ${
|
|
18
|
+
visible ? "opacity-100" : "opacity-0"
|
|
19
|
+
}`}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|