Skip to content

Commit 8837e02

Browse files
authored
Merge pull request #11 from pheralb/next
Copy with Text Morph + Improve Docs Layout
2 parents e178009 + 813c249 commit 8837e02

13 files changed

Lines changed: 312 additions & 8 deletions

File tree

apps/website/src/app/api/docs/og/route.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export async function GET(request: Request) {
2727
const docData = getDocument({
2828
folder: folder,
2929
document: document,
30+
withGenerals: true,
3031
});
3132

3233
if (!docData) {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client";
2+
3+
import {
4+
useMemo,
5+
useId,
6+
useEffect,
7+
useState,
8+
type ComponentProps,
9+
} from "react";
10+
11+
import {
12+
motion,
13+
AnimatePresence,
14+
type Transition,
15+
type Variants,
16+
} from "motion/react";
17+
18+
import { cn } from "@/utils/cn";
19+
import { copyToClipboard } from "@/utils/copy";
20+
21+
interface CopyTextAnimatedProps extends ComponentProps<"button"> {
22+
content: string;
23+
iconSize?: number;
24+
}
25+
26+
export type TextMorphProps = {
27+
children: string;
28+
as?: React.ElementType;
29+
className?: string;
30+
style?: React.CSSProperties;
31+
variants?: Variants;
32+
transition?: Transition;
33+
};
34+
35+
export function TextMorph({
36+
children,
37+
as: Component = "p",
38+
className,
39+
style,
40+
variants,
41+
transition,
42+
}: TextMorphProps) {
43+
const uniqueId = useId();
44+
45+
const characters = useMemo(() => {
46+
const charCounts: Record<string, number> = {};
47+
48+
return children.split("").map((char) => {
49+
const lowerChar = char.toLowerCase();
50+
charCounts[lowerChar] = (charCounts[lowerChar] || 0) + 1;
51+
52+
return {
53+
id: `${uniqueId}-${lowerChar}${charCounts[lowerChar]}`,
54+
label: char === " " ? "\u00A0" : char,
55+
};
56+
});
57+
}, [children, uniqueId]);
58+
59+
const defaultVariants: Variants = {
60+
initial: { opacity: 0 },
61+
animate: { opacity: 1 },
62+
exit: { opacity: 0 },
63+
};
64+
65+
const defaultTransition: Transition = {
66+
type: "spring",
67+
stiffness: 280,
68+
damping: 18,
69+
mass: 0.3,
70+
};
71+
72+
return (
73+
<Component className={cn(className)} aria-label={children} style={style}>
74+
<AnimatePresence mode="popLayout" initial={false}>
75+
{characters.map((character) => (
76+
<motion.span
77+
key={character.id}
78+
layoutId={character.id}
79+
className="inline-block"
80+
aria-hidden="true"
81+
initial="initial"
82+
animate="animate"
83+
exit="exit"
84+
variants={variants || defaultVariants}
85+
transition={transition || defaultTransition}
86+
>
87+
{character.label}
88+
</motion.span>
89+
))}
90+
</AnimatePresence>
91+
</Component>
92+
);
93+
}
94+
95+
const CopyTextMorph = ({
96+
content,
97+
className,
98+
...props
99+
}: CopyTextAnimatedProps) => {
100+
const [isCopied, setIsCopied] = useState<boolean>(false);
101+
102+
useEffect(() => {
103+
if (!isCopied) return;
104+
105+
const timeout = setTimeout(() => {
106+
setIsCopied(false);
107+
}, 2000);
108+
return () => clearTimeout(timeout);
109+
}, [isCopied]);
110+
111+
const handleCopy = async () => {
112+
await copyToClipboard(content);
113+
setIsCopied(true);
114+
};
115+
116+
return (
117+
<button
118+
title="Copy to clipboard"
119+
className={cn(
120+
"cursor-pointer",
121+
"transition-colors duration-200 ease-in-out",
122+
"text-xs text-neutral-700 dark:text-neutral-300 hover:text-neutral-950 dark:hover:text-neutral-50",
123+
"rounded-md bg-neutral-300/60 px-1.5 py-1 dark:bg-neutral-700/60",
124+
"border border-transparent hover:border-neutral-400/60 dark:hover:border-neutral-600/60",
125+
isCopied && "text-neutral-950 dark:text-neutral-50",
126+
className,
127+
)}
128+
onClick={handleCopy}
129+
{...props}
130+
>
131+
<TextMorph>{isCopied ? `Copied` : `Copy`}</TextMorph>
132+
</button>
133+
);
134+
};
135+
136+
export { CopyTextMorph };

apps/website/src/components/docs/show-categories.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ import {
1212
TagIcon,
1313
} from "lucide-react";
1414

15+
import {
16+
Shiki,
17+
SugarHigh,
18+
React,
19+
RadixUI,
20+
BaseUI,
21+
Motion,
22+
} from "@/components/ui/svgs";
23+
1524
import { cn } from "@/utils/cn";
1625
import { Badge, badgeVariants } from "@/components/ui/badge";
1726
import { ExternalLink } from "@/components/ui/external-link";
18-
import { Shiki, SugarHigh, React } from "@/components/ui/svgs";
19-
import { RadixUI } from "../ui/svgs/radix-ui";
20-
import { BaseUI } from "../ui/svgs/base-ui";
2127

2228
const categorySvgs = [
2329
{
@@ -65,6 +71,11 @@ const categorySvgs = [
6571
icon: BaseUI,
6672
url: "https://base-ui.com/",
6773
},
74+
{
75+
name: "Motion",
76+
icon: Motion,
77+
url: "https://motion.dev/",
78+
},
6879
{
6980
name: "Blocks",
7081
icon: BoxIcon,

apps/website/src/components/docs/sidebar-data.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export const SidebarLinksData: SidebarLinks[] = [
9090
title: "Multi Tabs",
9191
href: "/docs/react/blocks/multi-tabs",
9292
},
93+
{
94+
title: "Copy with Text Morph",
95+
href: "/docs/react/blocks/copy-text-morph",
96+
},
9397
{
9498
title: "Persist Package Manager",
9599
href: "/docs/react/blocks/persist-package-manager",

apps/website/src/components/github-link.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import useSWR from "swr";
44

55
import { cn } from "@/utils/cn";
66
import { globals } from "@/globals";
7-
import { StarIcon } from "lucide-react";
87

98
import { GitHub } from "@/components/ui/svgs/github";
109
import { buttonVariants } from "@/components/ui/button";
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
CodeBlock,
3+
CodeBlockContent,
4+
CodeBlockGroup,
5+
CodeBlockHeader,
6+
CodeBlockIcon,
7+
} from "@/components/code-block/code-block";
8+
import { CodeblockShiki } from "@/components/code-block/client/shiki";
9+
import { CopyTextMorph } from "@/components/code-block/blocks/copy-text-morph";
10+
11+
const code = `const copyToClipboard = async (text: string) => {
12+
try {
13+
await navigator.clipboard.writeText(text);
14+
console.log("Text copied to clipboard");
15+
} catch (err) {
16+
console.error("Failed to copy text: ", err);
17+
}
18+
};`;
19+
20+
const CopyMorphExample = () => {
21+
return (
22+
<CodeBlock>
23+
<CodeBlockHeader>
24+
<CodeBlockGroup>
25+
<CodeBlockIcon language="js" />
26+
<span>Copy with Text Morph</span>
27+
</CodeBlockGroup>
28+
<CopyTextMorph content={code} />
29+
</CodeBlockHeader>
30+
<CodeBlockContent>
31+
<CodeblockShiki language="ts" code={code} />
32+
</CodeBlockContent>
33+
</CodeBlock>
34+
);
35+
};
36+
37+
export default CopyMorphExample;

apps/website/src/components/registry/data.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,23 @@ const Blocks: RegistryComponent[] = [
295295
target: "src/components/code-block/blocks/inline-code.tsx",
296296
},
297297
},
298+
{
299+
title: "Blocks - Copy + Text Morph",
300+
fileType: "tsx",
301+
group: "blocks",
302+
fileSource: `${codeblockComponent}/blocks/copy-text-morph.tsx`,
303+
exampleFileSource: `${componentsFolder}/previews/copy-text-morph-example.tsx`,
304+
reactComponent: lazy(
305+
() => import("@/components/previews/copy-text-morph-example"),
306+
),
307+
shadcnRegistry: {
308+
name: "block-copy-text-morph",
309+
type: "registry:block",
310+
dependencies: ["motion"],
311+
registryDependencies: ["shiki-highlighter", "code-block", "client-shiki"],
312+
target: "src/components/code-block/blocks/copy-text-morph.tsx",
313+
},
314+
},
298315
{
299316
title: "Blocks - Select Package Manager",
300317
fileType: "tsx",

apps/website/src/components/ui/sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const Sidebar = ({ children, position }: SidebarProps) => {
2626
return (
2727
<aside
2828
className={cn(
29-
"fixed w-56",
29+
"fixed w-60",
3030
"h-[calc(100vh-3.5rem)]",
3131
"hidden md:block",
3232
"overflow-x-hidden overflow-y-auto",
@@ -72,7 +72,7 @@ const SidebarPageContent = ({
7272
className,
7373
}: SidebarPageContentProps) => {
7474
return (
75-
<main className={cn("ml-0 md:ml-56 lg:ml-56 xl:mx-56", className)}>
75+
<main className={cn("ml-0 md:ml-60 lg:ml-60 xl:mx-60", className)}>
7676
{children}
7777
</main>
7878
);

apps/website/src/components/ui/svgs/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@ export * from "./shiki";
88
export * from "./sugar-high";
99
export * from "./twitter";
1010
export * from "./vite";
11+
export * from "./radix-ui";
12+
export * from "./base-ui";
13+
export * from "./motion";
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type { SVGProps } from "react";
2+
3+
const Motion = (props: SVGProps<SVGSVGElement>) => (
4+
<svg {...props} viewBox="0 0 1103 386">
5+
<path
6+
className="fill-yellow-500 dark:fill-yellow-300"
7+
d="M416.473 0 198.54 385.66H0L170.17 84.522C196.549 37.842 262.377 0 317.203 0Zm486.875 96.415c0-53.249 44.444-96.415 99.27-96.415 54.826 0 99.27 43.166 99.27 96.415 0 53.248-44.444 96.415-99.27 96.415-54.826 0-99.27-43.167-99.27-96.415ZM453.699 0h198.54L434.306 385.66h-198.54Zm234.492 0h198.542L716.56 301.138c-26.378 46.68-92.207 84.522-147.032 84.522h-99.27Z"
8+
/>
9+
</svg>
10+
);
11+
12+
export { Motion };

0 commit comments

Comments
 (0)