blodemd 0.0.8 → 0.0.10

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.
Files changed (68) hide show
  1. package/README.md +25 -9
  2. package/dev-server/app/[[...slug]]/page.tsx +1 -0
  3. package/dev-server/app/favicon.ico +0 -0
  4. package/dev-server/next.config.js +11 -13
  5. package/dev-server/package.json +1 -1
  6. package/dev-server/tsconfig.json +3 -0
  7. package/dist/cli.mjs +869 -184
  8. package/dist/cli.mjs.map +1 -1
  9. package/docs/app/globals.css +1 -1
  10. package/docs/components/animate-ui/primitives/buttons/button.tsx +14 -0
  11. package/docs/components/api/api-playground.tsx +255 -80
  12. package/docs/components/api/api-reference.tsx +11 -1
  13. package/docs/components/docs/contextual-menu.tsx +227 -142
  14. package/docs/components/docs/copy-page-menu.tsx +148 -85
  15. package/docs/components/docs/doc-header.tsx +13 -3
  16. package/docs/components/docs/doc-shell.tsx +25 -14
  17. package/docs/components/docs/mobile-nav.tsx +0 -6
  18. package/docs/components/mdx/code-group.tsx +171 -62
  19. package/docs/components/mdx/steps.tsx +1 -1
  20. package/docs/components/mdx/tabs.tsx +131 -26
  21. package/docs/components/ui/copy-button.tsx +122 -0
  22. package/docs/components/ui/input.tsx +0 -1
  23. package/docs/components/ui/search.tsx +241 -132
  24. package/docs/components/ui/site-footer.tsx +39 -0
  25. package/docs/lib/config.ts +7 -0
  26. package/docs/lib/content-root.ts +33 -0
  27. package/docs/lib/content-source.ts +70 -0
  28. package/docs/lib/contextual-options.ts +20 -0
  29. package/docs/lib/docs-runtime.tsx +595 -0
  30. package/docs/lib/edge-config.ts +95 -0
  31. package/docs/lib/env.ts +22 -0
  32. package/docs/lib/openapi-proxy.ts +88 -0
  33. package/docs/lib/platform-config.ts +6 -0
  34. package/docs/lib/routes.ts +39 -0
  35. package/docs/lib/supabase.ts +13 -0
  36. package/docs/lib/tenancy.ts +350 -0
  37. package/docs/lib/tenant-headers.ts +14 -0
  38. package/docs/lib/tenant-static.ts +529 -0
  39. package/docs/lib/tenant-utility-context.ts +62 -0
  40. package/docs/lib/tenants.ts +68 -0
  41. package/docs/lib/use-mobile.ts +19 -0
  42. package/package.json +3 -2
  43. package/packages/@repo/common/dist/index.d.ts +7 -0
  44. package/packages/@repo/common/dist/index.d.ts.map +1 -1
  45. package/packages/@repo/common/dist/index.js +42 -0
  46. package/packages/@repo/common/src/index.ts +50 -0
  47. package/packages/@repo/contracts/dist/project.d.ts +1 -1
  48. package/packages/@repo/contracts/dist/project.js +1 -1
  49. package/packages/@repo/contracts/src/project.ts +1 -1
  50. package/packages/@repo/models/dist/docs-config.d.ts +194 -29
  51. package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
  52. package/packages/@repo/models/dist/docs-config.js +3 -28
  53. package/packages/@repo/models/src/docs-config.ts +5 -31
  54. package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
  55. package/packages/@repo/previewing/dist/blob-source.js +7 -2
  56. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
  57. package/packages/@repo/previewing/dist/fs-source.js +2 -3
  58. package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
  59. package/packages/@repo/previewing/dist/index.js +20 -50
  60. package/packages/@repo/previewing/src/blob-source.ts +7 -4
  61. package/packages/@repo/previewing/src/fs-source.ts +2 -3
  62. package/packages/@repo/previewing/src/index.ts +29 -64
  63. package/packages/@repo/validation/dist/index.d.ts +2 -2
  64. package/packages/@repo/validation/dist/index.d.ts.map +1 -1
  65. package/packages/@repo/validation/dist/index.js +2 -2
  66. package/packages/@repo/validation/package.json +1 -0
  67. package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
  68. package/packages/@repo/validation/src/index.ts +4 -4
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { isValidElement, useCallback, useMemo, useState } from "react";
4
- import type { MouseEvent, ReactElement, ReactNode } from "react";
3
+ import { isValidElement, useCallback, useId, useMemo, useState } from "react";
4
+ import type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react";
5
5
 
6
6
  import { cn } from "@/lib/utils";
7
7
 
@@ -9,49 +9,97 @@ interface CodeGroupProps {
9
9
  children: ReactNode;
10
10
  }
11
11
 
12
- export const CodeGroup = ({ children }: CodeGroupProps) => {
13
- const items = useMemo(() => {
14
- const nodes = Array.isArray(children) ? children : [children];
15
- return nodes.filter((child): child is ReactElement =>
16
- isValidElement(child)
17
- );
18
- }, [children]);
12
+ interface CodeGroupItemProps {
13
+ "data-rehype-pretty-code-title"?: string;
14
+ children?: ReactNode;
15
+ }
16
+
17
+ interface ResolvedCodeItem {
18
+ element: ReactElement<CodeGroupItemProps>;
19
+ key: string;
20
+ label: string;
21
+ }
22
+
23
+ const clampTabIndex = (index: number, total: number) => {
24
+ if (total <= 0) {
25
+ return 0;
26
+ }
19
27
 
28
+ return Math.min(Math.max(index, 0), total - 1);
29
+ };
30
+
31
+ const sanitizeDomId = (value: string) =>
32
+ value.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
33
+
34
+ const toNodeArray = (children: ReactNode): ReactNode[] =>
35
+ Array.isArray(children) ? children.flatMap(toNodeArray) : [children];
36
+
37
+ const getCodeLabel = (
38
+ element: ReactElement<CodeGroupItemProps>,
39
+ index: number
40
+ ) => {
41
+ const title = element.props["data-rehype-pretty-code-title"];
42
+ if (title) {
43
+ return title;
44
+ }
45
+
46
+ const pre = toNodeArray(element.props.children).find(
47
+ (child) => isValidElement(child) && child.type === "pre"
48
+ );
49
+ if (isValidElement<{ className?: string }>(pre)) {
50
+ const languageClass = pre.props.className
51
+ ?.split(" ")
52
+ .find((className: string) => className.startsWith("language-"));
53
+ if (languageClass) {
54
+ return languageClass.replace("language-", "");
55
+ }
56
+ }
57
+
58
+ return `Tab ${index + 1}`;
59
+ };
60
+
61
+ const resolveCodeItems = (children: ReactNode): ResolvedCodeItem[] =>
62
+ toNodeArray(children)
63
+ .filter((child): child is ReactElement<CodeGroupItemProps> =>
64
+ isValidElement<CodeGroupItemProps>(child)
65
+ )
66
+ .map((element, index) => {
67
+ const label = getCodeLabel(element, index);
68
+ const key = sanitizeDomId(
69
+ String(
70
+ element.key ??
71
+ element.props["data-rehype-pretty-code-title"] ??
72
+ `code-${index + 1}`
73
+ )
74
+ );
75
+
76
+ return {
77
+ element,
78
+ key,
79
+ label,
80
+ };
81
+ });
82
+
83
+ export const CodeGroup = ({ children }: CodeGroupProps) => {
84
+ const items = useMemo(() => resolveCodeItems(children), [children]);
20
85
  const [active, setActive] = useState(0);
86
+ const activeIndex = clampTabIndex(active, items.length);
87
+ const activeItem = items[activeIndex];
88
+ const tabsId = useId();
21
89
 
22
- const labels = useMemo(
23
- () =>
24
- items.map((item) => {
25
- if (
26
- !isValidElement<{
27
- "data-rehype-pretty-code-title"?: string;
28
- children?: ReactNode;
29
- }>(item)
30
- ) {
31
- return "Code";
32
- }
33
- const title = item.props["data-rehype-pretty-code-title"];
34
- if (title) {
35
- return title;
36
- }
37
- const itemChildren = Array.isArray(item.props.children)
38
- ? item.props.children
39
- : [item.props.children];
40
- const pre = itemChildren.find(
41
- (c: unknown) => isValidElement(c) && c.type === "pre"
42
- );
43
- if (isValidElement<{ className?: string }>(pre)) {
44
- const lang = pre.props.className
45
- ?.split(" ")
46
- .find((c: string) => c.startsWith("language-"))
47
- ?.replace("language-", "");
48
- if (lang) {
49
- return lang;
50
- }
51
- }
52
- return `Tab ${items.indexOf(item) + 1}`;
53
- }),
54
- [items]
90
+ const getTabId = useCallback(
91
+ (index: number) => `${tabsId}-${items[index]?.key ?? index}-tab`,
92
+ [items, tabsId]
93
+ );
94
+ const getPanelId = useCallback(
95
+ (index: number) => `${tabsId}-${items[index]?.key ?? index}-panel`,
96
+ [items, tabsId]
97
+ );
98
+ const focusTab = useCallback(
99
+ (index: number) => {
100
+ document.querySelector<HTMLElement>(`[id="${getTabId(index)}"]`)?.focus();
101
+ },
102
+ [getTabId]
55
103
  );
56
104
 
57
105
  const handleTabClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
@@ -59,36 +107,97 @@ export const CodeGroup = ({ children }: CodeGroupProps) => {
59
107
  setActive(index);
60
108
  }, []);
61
109
 
62
- if (items.length <= 1) {
110
+ const handleTabKeyDown = useCallback(
111
+ (event: KeyboardEvent<HTMLButtonElement>) => {
112
+ if (!items.length) {
113
+ return;
114
+ }
115
+
116
+ const index = Number(event.currentTarget.dataset.index ?? activeIndex);
117
+ const lastIndex = items.length - 1;
118
+ let nextIndex: number | null = null;
119
+
120
+ switch (event.key) {
121
+ case "ArrowDown":
122
+ case "ArrowRight": {
123
+ nextIndex = index === lastIndex ? 0 : index + 1;
124
+ break;
125
+ }
126
+ case "ArrowLeft":
127
+ case "ArrowUp": {
128
+ nextIndex = index === 0 ? lastIndex : index - 1;
129
+ break;
130
+ }
131
+ case "End": {
132
+ nextIndex = lastIndex;
133
+ break;
134
+ }
135
+ case "Home": {
136
+ nextIndex = 0;
137
+ break;
138
+ }
139
+ default: {
140
+ return;
141
+ }
142
+ }
143
+
144
+ event.preventDefault();
145
+ setActive(nextIndex);
146
+ focusTab(nextIndex);
147
+ },
148
+ [activeIndex, focusTab, items.length]
149
+ );
150
+
151
+ if (!activeItem) {
63
152
  return children as ReactElement;
64
153
  }
65
154
 
155
+ if (items.length === 1) {
156
+ return activeItem.element;
157
+ }
158
+
66
159
  return (
67
160
  <div className="my-4 overflow-hidden rounded-xl border border-border bg-code">
68
161
  <div
162
+ aria-orientation="horizontal"
69
163
  className="flex gap-1 border-b border-border bg-muted/50 px-2 pt-2"
70
164
  role="tablist"
71
165
  >
72
- {labels.map((label, index) => (
73
- <button
74
- aria-selected={index === active}
75
- className={cn(
76
- "rounded-t-md border-b-2 px-3 py-1.5 font-mono text-xs transition-colors",
77
- index === active
78
- ? "border-primary text-foreground"
79
- : "border-transparent text-muted-foreground hover:text-foreground"
80
- )}
81
- data-index={index}
82
- key={label}
83
- onClick={handleTabClick}
84
- role="tab"
85
- type="button"
86
- >
87
- {label}
88
- </button>
89
- ))}
166
+ {items.map((item, index) => {
167
+ const isSelected = index === activeIndex;
168
+
169
+ return (
170
+ <button
171
+ aria-controls={getPanelId(index)}
172
+ aria-selected={isSelected}
173
+ className={cn(
174
+ "rounded-t-md border-b-2 px-3 py-1.5 font-mono text-xs transition-colors",
175
+ isSelected
176
+ ? "border-primary text-foreground"
177
+ : "border-transparent text-muted-foreground hover:text-foreground"
178
+ )}
179
+ data-index={index}
180
+ id={getTabId(index)}
181
+ key={item.key}
182
+ onClick={handleTabClick}
183
+ onKeyDown={handleTabKeyDown}
184
+ role="tab"
185
+ tabIndex={isSelected ? 0 : -1}
186
+ type="button"
187
+ >
188
+ {item.label}
189
+ </button>
190
+ );
191
+ })}
192
+ </div>
193
+ <div
194
+ aria-labelledby={getTabId(activeIndex)}
195
+ id={getPanelId(activeIndex)}
196
+ role="tabpanel"
197
+ tabIndex={0}
198
+ >
199
+ {activeItem.element}
90
200
  </div>
91
- <div role="tabpanel">{items[active]}</div>
92
201
  </div>
93
202
  );
94
203
  };
@@ -24,7 +24,7 @@ export const Step = ({
24
24
  const anchorId = id ?? title.toLowerCase().replaceAll(/\s+/g, "-");
25
25
 
26
26
  return (
27
- <div className="relative pb-8 pl-10 last:pb-0" id={anchorId}>
27
+ <div className="relative pb-8 pl-8 sm:pl-10 last:pb-0" id={anchorId}>
28
28
  <div
29
29
  aria-hidden
30
30
  className="absolute left-0 flex size-7 items-center justify-center rounded-full border border-border bg-muted font-mono text-xs font-medium"
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
- import { isValidElement, useCallback, useMemo, useState } from "react";
4
- import type { MouseEvent, ReactElement, ReactNode } from "react";
3
+ import { isValidElement, useCallback, useId, useMemo, useState } from "react";
4
+ import type { KeyboardEvent, MouseEvent, ReactElement, ReactNode } from "react";
5
5
 
6
6
  import { cn } from "@/lib/utils";
7
7
 
@@ -13,9 +13,11 @@ interface TabProps {
13
13
  children: ReactNode;
14
14
  }
15
15
 
16
- export const Tab = ({ children }: TabProps) => (
17
- <div className="p-4">{children}</div>
18
- );
16
+ interface ResolvedTabItem {
17
+ element: ReactElement<TabProps>;
18
+ key: string;
19
+ label: string;
20
+ }
19
21
 
20
22
  interface TabsProps {
21
23
  children: ReactNode;
@@ -23,26 +25,116 @@ interface TabsProps {
23
25
  borderBottom?: boolean;
24
26
  }
25
27
 
28
+ export const Tab = ({ children }: TabProps) => (
29
+ <div className="p-4">{children}</div>
30
+ );
31
+
32
+ const clampTabIndex = (index: number, total: number) => {
33
+ if (total <= 0) {
34
+ return 0;
35
+ }
36
+
37
+ return Math.min(Math.max(index, 0), total - 1);
38
+ };
39
+
40
+ const sanitizeDomId = (value: string) =>
41
+ value.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
42
+
43
+ const toNodeArray = (children: ReactNode): ReactNode[] =>
44
+ Array.isArray(children) ? children.flatMap(toNodeArray) : [children];
45
+
46
+ const resolveTabItems = (children: ReactNode): ResolvedTabItem[] =>
47
+ toNodeArray(children)
48
+ .filter((child): child is ReactElement<TabProps> =>
49
+ isValidElement<TabProps>(child)
50
+ )
51
+ .map((element, index) => {
52
+ const label =
53
+ element.props.title ?? element.props.label ?? `Tab ${index + 1}`;
54
+ const key = sanitizeDomId(
55
+ String(element.props.id ?? element.key ?? `tab-${index + 1}`)
56
+ );
57
+
58
+ return {
59
+ element,
60
+ key,
61
+ label,
62
+ };
63
+ });
64
+
26
65
  export const Tabs = ({
27
66
  children,
28
67
  defaultTabIndex = 0,
29
68
  borderBottom,
30
69
  }: TabsProps) => {
31
- const items = useMemo(() => {
32
- const nodes = Array.isArray(children) ? children : [children];
33
- return nodes.filter((child): child is ReactElement<TabProps> =>
34
- isValidElement<TabProps>(child)
35
- );
36
- }, [children]);
70
+ const items = useMemo(() => resolveTabItems(children), [children]);
71
+ const [active, setActive] = useState<number | null>(null);
72
+ const activeIndex = clampTabIndex(active ?? defaultTabIndex, items.length);
73
+ const activeItem = items[activeIndex];
74
+ const tabsId = useId();
75
+
76
+ const getTabId = useCallback(
77
+ (index: number) => `${tabsId}-${items[index]?.key ?? index}-tab`,
78
+ [items, tabsId]
79
+ );
80
+ const getPanelId = useCallback(
81
+ (index: number) => `${tabsId}-${items[index]?.key ?? index}-panel`,
82
+ [items, tabsId]
83
+ );
84
+ const focusTab = useCallback(
85
+ (index: number) => {
86
+ document.querySelector<HTMLElement>(`[id="${getTabId(index)}"]`)?.focus();
87
+ },
88
+ [getTabId]
89
+ );
37
90
 
38
- const [active, setActive] = useState(defaultTabIndex);
39
- const activeItem = items[active];
40
91
  const handleTabClick = useCallback((event: MouseEvent<HTMLButtonElement>) => {
41
92
  const index = Number(event.currentTarget.dataset.index ?? "0");
42
93
  setActive(index);
43
94
  }, []);
44
95
 
45
- if (!items.length) {
96
+ const handleTabKeyDown = useCallback(
97
+ (event: KeyboardEvent<HTMLButtonElement>) => {
98
+ if (!items.length) {
99
+ return;
100
+ }
101
+
102
+ const index = Number(event.currentTarget.dataset.index ?? activeIndex);
103
+ const lastIndex = items.length - 1;
104
+ let nextIndex: number | null = null;
105
+
106
+ switch (event.key) {
107
+ case "ArrowDown":
108
+ case "ArrowRight": {
109
+ nextIndex = index === lastIndex ? 0 : index + 1;
110
+ break;
111
+ }
112
+ case "ArrowLeft":
113
+ case "ArrowUp": {
114
+ nextIndex = index === 0 ? lastIndex : index - 1;
115
+ break;
116
+ }
117
+ case "End": {
118
+ nextIndex = lastIndex;
119
+ break;
120
+ }
121
+ case "Home": {
122
+ nextIndex = 0;
123
+ break;
124
+ }
125
+ default: {
126
+ return;
127
+ }
128
+ }
129
+
130
+ event.preventDefault();
131
+ setActive(nextIndex);
132
+ focusTab(nextIndex);
133
+ },
134
+ [activeIndex, focusTab, items.length]
135
+ );
136
+
137
+ if (!activeItem) {
46
138
  return null;
47
139
  }
48
140
 
@@ -53,35 +145,48 @@ export const Tabs = ({
53
145
  borderBottom && "border-b-2"
54
146
  )}
55
147
  >
56
- <div className="flex gap-2 bg-muted p-2" role="tablist">
148
+ <div
149
+ aria-orientation="horizontal"
150
+ className="flex gap-2 bg-muted p-2"
151
+ role="tablist"
152
+ >
57
153
  {items.map((item, index) => {
58
- const tabLabel =
59
- item.props.title ?? item.props.label ?? `Tab ${index + 1}`;
154
+ const isSelected = index === activeIndex;
155
+
60
156
  return (
61
157
  <button
62
- aria-selected={index === active}
158
+ aria-controls={getPanelId(index)}
159
+ aria-selected={isSelected}
63
160
  className={cn(
64
- "inline-flex items-center gap-1.5 rounded-full border-none bg-transparent px-3 py-2 text-sm cursor-pointer transition-colors",
65
- index === active
161
+ "inline-flex cursor-pointer items-center gap-1.5 rounded-full border-none bg-transparent px-3 py-2 text-sm transition-colors",
162
+ isSelected
66
163
  ? "bg-primary text-primary-foreground"
67
164
  : "text-muted-foreground hover:text-foreground"
68
165
  )}
69
166
  data-index={index}
70
- key={String(item.key ?? tabLabel)}
167
+ id={getTabId(index)}
168
+ key={item.key}
71
169
  onClick={handleTabClick}
170
+ onKeyDown={handleTabKeyDown}
72
171
  role="tab"
172
+ tabIndex={isSelected ? 0 : -1}
73
173
  type="button"
74
174
  >
75
- {item.props.icon ? (
76
- <span className="shrink-0">{item.props.icon}</span>
175
+ {item.element.props.icon ? (
176
+ <span className="shrink-0">{item.element.props.icon}</span>
77
177
  ) : null}
78
- {tabLabel}
178
+ {item.label}
79
179
  </button>
80
180
  );
81
181
  })}
82
182
  </div>
83
- <div className="p-4" role="tabpanel">
84
- {activeItem}
183
+ <div
184
+ aria-labelledby={getTabId(activeIndex)}
185
+ id={getPanelId(activeIndex)}
186
+ role="tabpanel"
187
+ tabIndex={0}
188
+ >
189
+ {activeItem.element}
85
190
  </div>
86
191
  </div>
87
192
  );
@@ -0,0 +1,122 @@
1
+ "use client";
2
+
3
+ import {
4
+ Checkmark1Icon as CheckIcon,
5
+ CopySimpleIcon as CopyIcon,
6
+ } from "blode-icons-react";
7
+ import { cva } from "class-variance-authority";
8
+ import type { VariantProps } from "class-variance-authority";
9
+ import { AnimatePresence, motion } from "motion/react";
10
+ import { useCallback } from "react";
11
+ import type { MouseEvent } from "react";
12
+
13
+ import { Button as ButtonPrimitive } from "@/components/animate-ui/primitives/buttons/button";
14
+ import type { ButtonProps as ButtonPrimitiveProps } from "@/components/animate-ui/primitives/buttons/button";
15
+ import { useControlledState } from "@/hooks/use-controlled-state";
16
+ import { cn } from "@/lib/utils";
17
+
18
+ const buttonVariants = cva(
19
+ "flex shrink-0 items-center justify-center rounded-md outline-none transition-[box-shadow,_color,_background-color,_border-color,_outline-color,_text-decoration-color,_fill,_stroke] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
20
+ {
21
+ defaultVariants: {
22
+ size: "default",
23
+ variant: "default",
24
+ },
25
+ variants: {
26
+ size: {
27
+ default: "size-9",
28
+ lg: "size-10 rounded-md",
29
+ sm: "size-8 rounded-md",
30
+ xs: "size-7 rounded-md [&_svg:not([class*='size-'])]:size-3.5",
31
+ },
32
+ variant: {
33
+ accent: "bg-accent text-accent-foreground shadow-xs hover:bg-accent/90",
34
+ default:
35
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
36
+ destructive:
37
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
38
+ ghost:
39
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
40
+ link: "text-primary underline-offset-4 hover:underline",
41
+ outline:
42
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
43
+ secondary:
44
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
45
+ },
46
+ },
47
+ }
48
+ );
49
+
50
+ type CopyButtonProps = Omit<ButtonPrimitiveProps, "children"> &
51
+ VariantProps<typeof buttonVariants> & {
52
+ content: string;
53
+ copied?: boolean;
54
+ delay?: number;
55
+ onCopiedChange?: (copied: boolean, content?: string) => void;
56
+ };
57
+
58
+ const CopyButton = ({
59
+ className,
60
+ content,
61
+ copied,
62
+ onCopiedChange,
63
+ onClick,
64
+ variant,
65
+ size,
66
+ delay = 3000,
67
+ ...props
68
+ }: CopyButtonProps) => {
69
+ const [isCopied, setIsCopied] = useControlledState({
70
+ onChange: onCopiedChange,
71
+ value: copied,
72
+ });
73
+
74
+ const handleCopy = useCallback(
75
+ async (e: MouseEvent<HTMLButtonElement>) => {
76
+ onClick?.(e);
77
+ if (copied) {
78
+ return;
79
+ }
80
+ if (content) {
81
+ try {
82
+ await navigator.clipboard.writeText(content);
83
+ setIsCopied(true);
84
+ onCopiedChange?.(true, content);
85
+ setTimeout(() => {
86
+ setIsCopied(false);
87
+ onCopiedChange?.(false);
88
+ }, delay);
89
+ } catch (error) {
90
+ console.error("Error copying command", error);
91
+ }
92
+ }
93
+ },
94
+ [onClick, copied, content, setIsCopied, onCopiedChange, delay]
95
+ );
96
+
97
+ const Icon = isCopied ? CheckIcon : CopyIcon;
98
+
99
+ return (
100
+ <ButtonPrimitive
101
+ className={cn(buttonVariants({ className, size, variant }))}
102
+ data-slot="copy-button"
103
+ onClick={handleCopy}
104
+ {...props}
105
+ >
106
+ <AnimatePresence mode="popLayout">
107
+ <motion.span
108
+ animate={{ filter: "blur(0px)", opacity: 1, scale: 1 }}
109
+ data-slot="copy-button-icon"
110
+ exit={{ filter: "blur(4px)", opacity: 0.4, scale: 0 }}
111
+ initial={false}
112
+ key={isCopied ? "check" : "copy"}
113
+ transition={{ duration: 0.25 }}
114
+ >
115
+ <Icon />
116
+ </motion.span>
117
+ </AnimatePresence>
118
+ </ButtonPrimitive>
119
+ );
120
+ };
121
+
122
+ export { CopyButton };
@@ -72,7 +72,6 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
72
72
  clearClassName
73
73
  )}
74
74
  onClick={onClear}
75
- tabIndex={-1}
76
75
  type="button"
77
76
  >
78
77
  <CircleXFilledIcon className="size-5 text-muted-foreground/50" />