-
Notifications
You must be signed in to change notification settings - Fork 42
feat: introduce unified fullscreen Tool Playground shell across tools #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
84373f4
f59885f
e5a749c
a754227
f5887e1
dbeacc7
14cc59b
5dfbb59
6cde849
a9f6787
9ef7c34
5f9cda4
d72e44b
53e4151
cb41462
7a03522
a3516b2
3b0676d
d92c50a
b98fe91
39884a7
7298435
3b152b6
35fff54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,164 @@ | ||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import { Button } from "@/components/ui/button"; | ||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||
| DropdownMenu, | ||||||||||||||||||||||||||
| DropdownMenuContent, | ||||||||||||||||||||||||||
| DropdownMenuItem, | ||||||||||||||||||||||||||
| DropdownMenuTrigger, | ||||||||||||||||||||||||||
| } from "@/components/ui/dropdown-menu"; | ||||||||||||||||||||||||||
| import { Input } from "@/components/ui/input"; | ||||||||||||||||||||||||||
| import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; | ||||||||||||||||||||||||||
| import { cn } from "@/lib/utils"; | ||||||||||||||||||||||||||
| import { Copy, Download, EllipsisVertical, RotateCcw } from "lucide-react"; | ||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||
| import toast from "react-hot-toast"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| interface ToolPlaygroundShellProps { | ||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||
| description: string; | ||||||||||||||||||||||||||
| examples: string[]; | ||||||||||||||||||||||||||
| docs: string; | ||||||||||||||||||||||||||
| exportLabel: string; | ||||||||||||||||||||||||||
| exportCode: string; | ||||||||||||||||||||||||||
| children: React.ReactNode; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export function ToolPlaygroundShell({ | ||||||||||||||||||||||||||
| title, | ||||||||||||||||||||||||||
| description, | ||||||||||||||||||||||||||
| examples, | ||||||||||||||||||||||||||
| docs, | ||||||||||||||||||||||||||
| exportLabel, | ||||||||||||||||||||||||||
| exportCode, | ||||||||||||||||||||||||||
| children, | ||||||||||||||||||||||||||
| }: ToolPlaygroundShellProps) { | ||||||||||||||||||||||||||
| const [topTab, setTopTab] = useState("playground"); | ||||||||||||||||||||||||||
| const [drawerOpen, setDrawerOpen] = useState(false); | ||||||||||||||||||||||||||
| const [filter, setFilter] = useState(""); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const copyCode = async () => { | ||||||||||||||||||||||||||
| await navigator.clipboard.writeText(exportCode); | ||||||||||||||||||||||||||
| toast.success("Copied"); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
Comment on lines
+40
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled clipboard API rejection.
🛡️ Proposed fix const copyCode = async () => {
- await navigator.clipboard.writeText(exportCode);
- toast.success("Copied");
+ try {
+ await navigator.clipboard.writeText(exportCode);
+ toast.success("Copied");
+ } catch {
+ toast.error("Failed to copy");
+ }
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <div className="flex h-full min-h-0 flex-col overflow-hidden rounded-2xl border bg-card-bg"> | ||||||||||||||||||||||||||
| <header className="flex h-16 shrink-0 items-center justify-between gap-3 border-b px-4"> | ||||||||||||||||||||||||||
| <div className="min-w-0"> | ||||||||||||||||||||||||||
| <h1 className="truncate font-semibold text-lg">{title}</h1> | ||||||||||||||||||||||||||
| <p className="truncate text-muted-foreground text-xs">{description}</p> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <Tabs value={topTab} onValueChange={setTopTab} className="hidden md:block"> | ||||||||||||||||||||||||||
| <TabsList> | ||||||||||||||||||||||||||
| <TabsTrigger value="playground">Playground</TabsTrigger> | ||||||||||||||||||||||||||
| <TabsTrigger value="examples">Examples</TabsTrigger> | ||||||||||||||||||||||||||
| <TabsTrigger value="docs">Docs</TabsTrigger> | ||||||||||||||||||||||||||
| </TabsList> | ||||||||||||||||||||||||||
| </Tabs> | ||||||||||||||||||||||||||
|
Comment on lines
+53
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mobile users cannot switch between Playground / Examples / Docs tabs. The 🛡️ Proposed fix: add tab options to the mobile dropdown <DropdownMenuContent align="end">
+ <DropdownMenuItem onClick={() => setTopTab("playground")}>Playground</DropdownMenuItem>
+ <DropdownMenuItem onClick={() => setTopTab("examples")}>Examples</DropdownMenuItem>
+ <DropdownMenuItem onClick={() => setTopTab("docs")}>Docs</DropdownMenuItem>
+ <DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setDrawerOpen(true)}>
Export code
</DropdownMenuItem>Also applies to: 73-86 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <div className="hidden items-center gap-2 lg:flex"> | ||||||||||||||||||||||||||
| <Button size="sm" onClick={() => setDrawerOpen(true)}> | ||||||||||||||||||||||||||
| Export code | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
| <Button variant="outline" size="sm" onClick={copyCode}> | ||||||||||||||||||||||||||
| <Copy className="mr-1 h-4 w-4" /> Copy | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
| <Button variant="outline" size="sm" onClick={() => location.reload()}> | ||||||||||||||||||||||||||
| <RotateCcw className="mr-1 h-4 w-4" /> Reset | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <DropdownMenu> | ||||||||||||||||||||||||||
| <DropdownMenuTrigger asChild className="lg:hidden"> | ||||||||||||||||||||||||||
| <Button variant="outline" size="icon"> | ||||||||||||||||||||||||||
| <EllipsisVertical className="h-4 w-4" /> | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
| </DropdownMenuTrigger> | ||||||||||||||||||||||||||
| <DropdownMenuContent align="end"> | ||||||||||||||||||||||||||
| <DropdownMenuItem onClick={() => setDrawerOpen(true)}> | ||||||||||||||||||||||||||
| Export code | ||||||||||||||||||||||||||
| </DropdownMenuItem> | ||||||||||||||||||||||||||
| <DropdownMenuItem onClick={copyCode}>Copy</DropdownMenuItem> | ||||||||||||||||||||||||||
| <DropdownMenuItem onClick={() => location.reload()}>Reset</DropdownMenuItem> | ||||||||||||||||||||||||||
| </DropdownMenuContent> | ||||||||||||||||||||||||||
| </DropdownMenu> | ||||||||||||||||||||||||||
| </header> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <div className="grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[380px_minmax(0,1fr)]"> | ||||||||||||||||||||||||||
| <aside className="min-h-0 border-r"> | ||||||||||||||||||||||||||
| <div className="sticky top-0 z-10 border-b bg-card-bg p-3"> | ||||||||||||||||||||||||||
| <Input | ||||||||||||||||||||||||||
| placeholder="Search presets" | ||||||||||||||||||||||||||
| value={filter} | ||||||||||||||||||||||||||
| onChange={(e) => setFilter(e.target.value)} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| <div className="h-[calc(100%-60px)] overflow-y-auto p-3"> | ||||||||||||||||||||||||||
| {topTab !== "docs" && ( | ||||||||||||||||||||||||||
| <section className="mb-4 space-y-2"> | ||||||||||||||||||||||||||
| <h2 className="font-medium text-sm">Presets / Examples</h2> | ||||||||||||||||||||||||||
| {examples | ||||||||||||||||||||||||||
| .filter((item) => | ||||||||||||||||||||||||||
| item.toLowerCase().includes(filter.toLowerCase()), | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
| .map((item) => ( | ||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||
| key={item} | ||||||||||||||||||||||||||
| className="mb-2 block w-full rounded-md border px-3 py-2 text-left text-sm hover:bg-accent" | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| {item} | ||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||
|
Comment on lines
+107
to
+113
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preset buttons are non-functional — no click handler. Every preset item renders as a 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| <section> | ||||||||||||||||||||||||||
| <h2 className="mb-2 font-medium text-sm"> | ||||||||||||||||||||||||||
| {topTab === "docs" ? "Docs" : "Controls"} | ||||||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||||||
| <div className="rounded-md border bg-background p-3 text-muted-foreground text-sm"> | ||||||||||||||||||||||||||
| {topTab === "docs" | ||||||||||||||||||||||||||
| ? docs | ||||||||||||||||||||||||||
| : "All existing controls are preserved below in the tool panel. Use this sidebar as the single scroll region."} | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </aside> | ||||||||||||||||||||||||||
|
Comment on lines
+89
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sidebar layout may be unusable on mobile viewports. On 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <section className="relative min-h-0 overflow-hidden bg-background/30"> | ||||||||||||||||||||||||||
| <div className="h-full overflow-hidden p-2"> | ||||||||||||||||||||||||||
| <div className="h-full overflow-hidden rounded-xl border bg-background">{children}</div> | ||||||||||||||||||||||||||
|
Comment on lines
+130
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new shell wraps Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||
| "pointer-events-none fixed top-0 right-0 z-[90] h-screen w-full max-w-xl translate-x-full border-l bg-background/95 opacity-0 backdrop-blur transition-all", | ||||||||||||||||||||||||||
| drawerOpen && "pointer-events-auto translate-x-0 opacity-100", | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||
| <div className="flex h-16 items-center justify-between border-b px-4"> | ||||||||||||||||||||||||||
| <h3 className="font-semibold">Export</h3> | ||||||||||||||||||||||||||
| <Button variant="outline" size="sm" onClick={() => setDrawerOpen(false)}> | ||||||||||||||||||||||||||
| Close | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| <div className="h-[calc(100vh-64px)] overflow-y-auto p-4"> | ||||||||||||||||||||||||||
| <p className="mb-2 text-muted-foreground text-xs">{exportLabel}</p> | ||||||||||||||||||||||||||
| <pre className="overflow-x-auto rounded-md border bg-card p-3 text-xs">{exportCode}</pre> | ||||||||||||||||||||||||||
| <div className="mt-3 flex gap-2"> | ||||||||||||||||||||||||||
| <Button size="sm" onClick={copyCode}> | ||||||||||||||||||||||||||
| <Copy className="mr-1 h-4 w-4" /> Copy | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
| <Button size="sm" variant="outline"> | ||||||||||||||||||||||||||
| <Download className="mr-1 h-4 w-4" /> Download | ||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||
|
Comment on lines
+156
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Download button has no The Download button renders but does nothing. Same concern as the preset buttons — users will click it expecting a file download. I can help implement a 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
|
Comment on lines
+137
to
+161
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Export drawer lacks backdrop, keyboard dismiss, and focus trap. The slide-in drawer has no overlay behind it (clicking outside does nothing), no 🛡️ Minimal fix: add overlay + Escape handler+ {/* Overlay */}
+ {drawerOpen && (
+ <div
+ className="fixed inset-0 z-[89] bg-black/40"
+ onClick={() => setDrawerOpen(false)}
+ onKeyDown={(e) => e.key === "Escape" && setDrawerOpen(false)}
+ role="presentation"
+ />
+ )}
<div
className={cn(
"pointer-events-none fixed top-0 right-0 z-[90] h-screen w-full max-w-xl translate-x-full border-l bg-background/95 opacity-0 backdrop-blur transition-all",
drawerOpen && "pointer-events-auto translate-x-0 opacity-100",
)}
>For a proper focus trap, consider using a library like 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 292
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 90
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 90
🏁 Script executed:
rg 'h-\[.*vh\]' ./components/view/mesh-gradient/Repository: ui-layouts/ui-tools
Length of output: 471
🏁 Script executed:
rg 'col-span|ScrollArea|overflow' ./components/view/mesh-gradient/Repository: ui-layouts/ui-tools
Length of output: 1737
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 108
🏁 Script executed:
cat -n ./components/view/mesh-gradient/index.tsx | head -100Repository: ui-layouts/ui-tools
Length of output: 3243
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 2299
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 2671
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 3226
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 4166
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 112
🏁 Script executed:
cat -n ./components/common/tool-playground-shell.tsx | head -150Repository: ui-layouts/ui-tools
Length of output: 6158
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 2739
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 1655
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 2680
🏁 Script executed:
cat -n ./components/view/mesh-gradient/control-panel.tsx | head -30Repository: ui-layouts/ui-tools
Length of output: 1042
🏁 Script executed:
cat -n ./components/view/mesh-gradient/example-gradients.tsx | head -20Repository: ui-layouts/ui-tools
Length of output: 631
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 390
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 121
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 90
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 278
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 3528
🏁 Script executed:
# Check how BackgroundSnippetsGenerator is structured for comparison wc -l ./components/view/background-snippets/index.tsxRepository: ui-layouts/ui-tools
Length of output: 114
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 461
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 278
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 4513
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 335
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 3677
🏁 Script executed:
Repository: ui-layouts/ui-tools
Length of output: 680
Layout breakage:
ShaderGradientGeneratorviewport-height sizing conflicts with shell's overflow-constrained container.The generator renders a full grid layout with fixed
h-[95vh]dimensions (control panel at line 351, preview area at line 371 incomponents/view/mesh-gradient/index.tsx). When wrapped byToolPlaygroundShell, these elements are placed inside ah-full overflow-hiddencontainer (line 132 oftool-playground-shell.tsx), causing the viewport-height content to be clipped. Additionally, the generator's own control panel and example-gradient selector will appear alongside the shell's preset sidebar, creating duplicate control areas.The generator needs either a refactored layout that respects the shell's constraints, or should skip the shell wrapper until the layout is redesigned.
🤖 Prompt for AI Agents