Skip to content

Commit 8620107

Browse files
kylemclarenclaude
andcommitted
Split copy page button into ButtonGroup with dropdown
- Add shadcn ButtonGroup and Separator components - Refactor CopyPageButton to use split button pattern: - Left: immediate copy action - Right: dropdown menu for AI assistant options (ChatGPT, Claude) - Reduce button size with smaller text and icons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 01138a9 commit 8620107

File tree

5 files changed

+209
-54
lines changed

5 files changed

+209
-54
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"@radix-ui/react-collapsible": "^1.1.12",
2222
"@radix-ui/react-dropdown-menu": "^2.1.16",
2323
"@radix-ui/react-select": "^2.2.6",
24+
"@radix-ui/react-separator": "^1.1.8",
2425
"@radix-ui/react-slot": "^1.2.4",
2526
"@radix-ui/react-switch": "^1.2.6",
2627
"@radix-ui/react-tabs": "^1.1.13",

pnpm-lock.yaml

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/react/CopyPageButton.tsx

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
import { ArrowUpRight, Check, ChevronDown, Copy } from 'lucide-react';
1+
import { ArrowUpRight, Check, Copy, Ellipsis } from 'lucide-react';
22
import * as React from 'react';
33
import { Button } from '@/components/ui/button';
4+
import {
5+
ButtonGroup,
6+
ButtonGroupSeparator,
7+
} from '@/components/ui/button-group';
48
import {
59
DropdownMenu,
610
DropdownMenuContent,
@@ -65,58 +69,52 @@ export function CopyPageButton({
6569
};
6670

6771
return (
68-
<DropdownMenu>
69-
<DropdownMenuTrigger asChild>
70-
<Button variant="outline" size="sm" className="gap-1.5">
71-
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
72-
<span className="hidden sm:inline">Copy page</span>
73-
<ChevronDown className="size-3 opacity-50" />
74-
</Button>
75-
</DropdownMenuTrigger>
76-
<DropdownMenuContent align="end" className="w-64">
77-
<DropdownMenuItem
78-
onClick={handleCopy}
79-
className="flex flex-col items-start gap-0.5 py-2"
80-
>
81-
<div className="flex items-center gap-2">
82-
{copied ? (
83-
<Check className="size-4 text-emerald-500" />
84-
) : (
85-
<Copy className="size-4" />
86-
)}
87-
<span className="font-medium">Copy page</span>
88-
</div>
89-
<span className="text-xs text-muted-foreground pl-6">
90-
Copy page as Markdown for LLMs
91-
</span>
92-
</DropdownMenuItem>
93-
<DropdownMenuItem
94-
onClick={handleOpenChatGPT}
95-
className="flex flex-col items-start gap-0.5 py-2"
96-
>
97-
<div className="flex items-center gap-2">
98-
<ChatGPTIcon />
99-
<span className="font-medium">Open in ChatGPT</span>
100-
<ArrowUpRight className="size-3 opacity-50" />
101-
</div>
102-
<span className="text-xs text-muted-foreground pl-6">
103-
Ask questions about this page
104-
</span>
105-
</DropdownMenuItem>
106-
<DropdownMenuItem
107-
onClick={handleOpenClaude}
108-
className="flex flex-col items-start gap-0.5 py-2"
109-
>
110-
<div className="flex items-center gap-2">
111-
<ClaudeIcon />
112-
<span className="font-medium">Open in Claude</span>
113-
<ArrowUpRight className="size-3 opacity-50" />
114-
</div>
115-
<span className="text-xs text-muted-foreground pl-6">
116-
Ask questions about this page
117-
</span>
118-
</DropdownMenuItem>
119-
</DropdownMenuContent>
120-
</DropdownMenu>
72+
<ButtonGroup>
73+
<Button
74+
variant="outline"
75+
size="sm"
76+
className="gap-1 !text-xs h-7"
77+
onClick={handleCopy}
78+
>
79+
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
80+
<span className="hidden sm:inline">Copy page</span>
81+
</Button>
82+
<ButtonGroupSeparator />
83+
<DropdownMenu>
84+
<DropdownMenuTrigger asChild>
85+
<Button variant="outline" size="sm" className="px-1.5 h-7">
86+
<Ellipsis className="size-3" />
87+
</Button>
88+
</DropdownMenuTrigger>
89+
<DropdownMenuContent align="end" className="w-64">
90+
<DropdownMenuItem
91+
onClick={handleOpenChatGPT}
92+
className="flex flex-col items-start gap-0.5 py-2"
93+
>
94+
<div className="flex items-center gap-2">
95+
<ChatGPTIcon />
96+
<span className="font-medium">Open in ChatGPT</span>
97+
<ArrowUpRight className="size-3 opacity-50" />
98+
</div>
99+
<span className="text-xs text-muted-foreground pl-6">
100+
Ask questions about this page
101+
</span>
102+
</DropdownMenuItem>
103+
<DropdownMenuItem
104+
onClick={handleOpenClaude}
105+
className="flex flex-col items-start gap-0.5 py-2"
106+
>
107+
<div className="flex items-center gap-2">
108+
<ClaudeIcon />
109+
<span className="font-medium">Open in Claude</span>
110+
<ArrowUpRight className="size-3 opacity-50" />
111+
</div>
112+
<span className="text-xs text-muted-foreground pl-6">
113+
Ask questions about this page
114+
</span>
115+
</DropdownMenuItem>
116+
</DropdownMenuContent>
117+
</DropdownMenu>
118+
</ButtonGroup>
121119
);
122120
}

src/components/ui/button-group.tsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Slot } from '@radix-ui/react-slot';
2+
import { cva, type VariantProps } from 'class-variance-authority';
3+
import { Separator } from '@/components/ui/separator';
4+
import { cn } from '@/lib/utils';
5+
6+
const buttonGroupVariants = cva(
7+
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
8+
{
9+
variants: {
10+
orientation: {
11+
horizontal:
12+
'[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none',
13+
vertical:
14+
'flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none',
15+
},
16+
},
17+
defaultVariants: {
18+
orientation: 'horizontal',
19+
},
20+
},
21+
);
22+
23+
function ButtonGroup({
24+
className,
25+
orientation,
26+
...props
27+
}: React.ComponentProps<'div'> & VariantProps<typeof buttonGroupVariants>) {
28+
return (
29+
// biome-ignore lint/a11y/useSemanticElements: div with role="group" is intentional for flexible button group styling
30+
<div
31+
role="group"
32+
data-slot="button-group"
33+
data-orientation={orientation}
34+
className={cn(buttonGroupVariants({ orientation }), className)}
35+
{...props}
36+
/>
37+
);
38+
}
39+
40+
function ButtonGroupText({
41+
className,
42+
asChild = false,
43+
...props
44+
}: React.ComponentProps<'div'> & {
45+
asChild?: boolean;
46+
}) {
47+
const Comp = asChild ? Slot : 'div';
48+
49+
return (
50+
<Comp
51+
className={cn(
52+
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
53+
className,
54+
)}
55+
{...props}
56+
/>
57+
);
58+
}
59+
60+
function ButtonGroupSeparator({
61+
className,
62+
orientation = 'vertical',
63+
...props
64+
}: React.ComponentProps<typeof Separator>) {
65+
return (
66+
<Separator
67+
data-slot="button-group-separator"
68+
orientation={orientation}
69+
className={cn(
70+
'bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto',
71+
className,
72+
)}
73+
{...props}
74+
/>
75+
);
76+
}
77+
78+
export {
79+
ButtonGroup,
80+
ButtonGroupSeparator,
81+
ButtonGroupText,
82+
buttonGroupVariants,
83+
};

src/components/ui/separator.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as SeparatorPrimitive from '@radix-ui/react-separator';
2+
import type * as React from 'react';
3+
4+
import { cn } from '@/lib/utils';
5+
6+
function Separator({
7+
className,
8+
orientation = 'horizontal',
9+
decorative = true,
10+
...props
11+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
12+
return (
13+
<SeparatorPrimitive.Root
14+
data-slot="separator"
15+
decorative={decorative}
16+
orientation={orientation}
17+
className={cn(
18+
'bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
19+
className,
20+
)}
21+
{...props}
22+
/>
23+
);
24+
}
25+
26+
export { Separator };

0 commit comments

Comments
 (0)