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.
- package/README.md +25 -9
- package/dev-server/app/[[...slug]]/page.tsx +1 -0
- package/dev-server/app/favicon.ico +0 -0
- package/dev-server/next.config.js +11 -13
- package/dev-server/package.json +1 -1
- package/dev-server/tsconfig.json +3 -0
- package/dist/cli.mjs +869 -184
- package/dist/cli.mjs.map +1 -1
- package/docs/app/globals.css +1 -1
- package/docs/components/animate-ui/primitives/buttons/button.tsx +14 -0
- package/docs/components/api/api-playground.tsx +255 -80
- package/docs/components/api/api-reference.tsx +11 -1
- package/docs/components/docs/contextual-menu.tsx +227 -142
- package/docs/components/docs/copy-page-menu.tsx +148 -85
- package/docs/components/docs/doc-header.tsx +13 -3
- package/docs/components/docs/doc-shell.tsx +25 -14
- package/docs/components/docs/mobile-nav.tsx +0 -6
- package/docs/components/mdx/code-group.tsx +171 -62
- package/docs/components/mdx/steps.tsx +1 -1
- package/docs/components/mdx/tabs.tsx +131 -26
- package/docs/components/ui/copy-button.tsx +122 -0
- package/docs/components/ui/input.tsx +0 -1
- package/docs/components/ui/search.tsx +241 -132
- package/docs/components/ui/site-footer.tsx +39 -0
- package/docs/lib/config.ts +7 -0
- package/docs/lib/content-root.ts +33 -0
- package/docs/lib/content-source.ts +70 -0
- package/docs/lib/contextual-options.ts +20 -0
- package/docs/lib/docs-runtime.tsx +595 -0
- package/docs/lib/edge-config.ts +95 -0
- package/docs/lib/env.ts +22 -0
- package/docs/lib/openapi-proxy.ts +88 -0
- package/docs/lib/platform-config.ts +6 -0
- package/docs/lib/routes.ts +39 -0
- package/docs/lib/supabase.ts +13 -0
- package/docs/lib/tenancy.ts +350 -0
- package/docs/lib/tenant-headers.ts +14 -0
- package/docs/lib/tenant-static.ts +529 -0
- package/docs/lib/tenant-utility-context.ts +62 -0
- package/docs/lib/tenants.ts +68 -0
- package/docs/lib/use-mobile.ts +19 -0
- package/package.json +3 -2
- package/packages/@repo/common/dist/index.d.ts +7 -0
- package/packages/@repo/common/dist/index.d.ts.map +1 -1
- package/packages/@repo/common/dist/index.js +42 -0
- package/packages/@repo/common/src/index.ts +50 -0
- package/packages/@repo/contracts/dist/project.d.ts +1 -1
- package/packages/@repo/contracts/dist/project.js +1 -1
- package/packages/@repo/contracts/src/project.ts +1 -1
- package/packages/@repo/models/dist/docs-config.d.ts +194 -29
- package/packages/@repo/models/dist/docs-config.d.ts.map +1 -1
- package/packages/@repo/models/dist/docs-config.js +3 -28
- package/packages/@repo/models/src/docs-config.ts +5 -31
- package/packages/@repo/previewing/dist/blob-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/blob-source.js +7 -2
- package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/fs-source.js +2 -3
- package/packages/@repo/previewing/dist/index.d.ts.map +1 -1
- package/packages/@repo/previewing/dist/index.js +20 -50
- package/packages/@repo/previewing/src/blob-source.ts +7 -4
- package/packages/@repo/previewing/src/fs-source.ts +2 -3
- package/packages/@repo/previewing/src/index.ts +29 -64
- package/packages/@repo/validation/dist/index.d.ts +2 -2
- package/packages/@repo/validation/dist/index.d.ts.map +1 -1
- package/packages/@repo/validation/dist/index.js +2 -2
- package/packages/@repo/validation/package.json +1 -0
- package/packages/@repo/validation/src/{mintlify-docs-schema.json → blodemd-docs-schema.json} +346 -1794
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
23
|
-
() =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
index
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
17
|
-
<
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
59
|
-
|
|
154
|
+
const isSelected = index === activeIndex;
|
|
155
|
+
|
|
60
156
|
return (
|
|
61
157
|
<button
|
|
62
|
-
aria-
|
|
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
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
178
|
+
{item.label}
|
|
79
179
|
</button>
|
|
80
180
|
);
|
|
81
181
|
})}
|
|
82
182
|
</div>
|
|
83
|
-
<div
|
|
84
|
-
{
|
|
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 };
|