-
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 2 commits
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 |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ import { ShaderGradientGenerator } from "@/components/view/mesh-gradient"; | |
| import { siteConfig } from "@/lib/utils"; | ||
| import type { Metadata } from "next"; | ||
| import React from "react"; | ||
| import { ToolPlaygroundShell } from "@/components/common/tool-playground-shell"; | ||
| export const metadata: Metadata = { | ||
| title: "Mesh-Gradient Generator", | ||
| description: | ||
|
|
@@ -52,10 +53,17 @@ export const metadata: Metadata = { | |
|
|
||
| function page() { | ||
| return ( | ||
| <> | ||
| <ToolPlaygroundShell | ||
| title="Mesh Gradient Generator" | ||
| description="Build fluid mesh gradients with draggable color points." | ||
| examples={["Aurora", "Sunset", "Neon", "Pastel"]} | ||
| docs="Use Playground for live editing, Examples to load quick presets, and Export to copy production-ready output." | ||
| exportLabel="CSS" | ||
| exportCode={`/* export output from active tool */`} | ||
| > | ||
| <ShaderGradientGenerator /> | ||
| </> | ||
| </ToolPlaygroundShell> | ||
|
Comment on lines
+56
to
+65
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. 🧩 Analysis chain🏁 Script executed: find . -type f -name "*.tsx" -o -name "*.ts" | grep -i "mesh-gradient" | head -20Repository: ui-layouts/ui-tools Length of output: 292 🏁 Script executed: rg -n 'h-\[.*vh\]' --type=tsx --type=ts -g '*/mesh-gradient/*'Repository: ui-layouts/ui-tools Length of output: 90 🏁 Script executed: rg -n 'col-span|ScrollArea|overflow' --type=tsx --type=ts -g '*/mesh-gradient/*'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: wc -l ./components/view/mesh-gradient/index.tsxRepository: 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: cat -n ./components/view/mesh-gradient/index.tsx | sed -n '100,200p'Repository: ui-layouts/ui-tools Length of output: 2299 🏁 Script executed: cat -n ./app/\(tools\)/mesh-gradients/page.tsxRepository: ui-layouts/ui-tools Length of output: 2671 🏁 Script executed: cat -n ./components/view/mesh-gradient/index.tsx | sed -n '200,300p'Repository: ui-layouts/ui-tools Length of output: 3226 🏁 Script executed: cat -n ./components/view/mesh-gradient/index.tsx | sed -n '250,350p'Repository: ui-layouts/ui-tools Length of output: 4166 🏁 Script executed: wc -l ./components/common/tool-playground-shell.tsxRepository: 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: cat -n ./components/view/mesh-gradient/index.tsx | sed -n '343,400p'Repository: ui-layouts/ui-tools Length of output: 2739 🏁 Script executed: cat -n ./components/view/mesh-gradient/index.tsx | sed -n '348,380p'Repository: ui-layouts/ui-tools Length of output: 1655 🏁 Script executed: cat -n ./components/view/mesh-gradient/index.tsx | sed -n '400,460p'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: cat -n ./components/common/tool-playground-shell.tsx | sed -n '128,135p'Repository: ui-layouts/ui-tools Length of output: 390 🏁 Script executed: # Check if there's a render of this page to verify actual layout behavior
git log --oneline -20 -- ./app/\(tools\)/mesh-gradients/page.tsx 2>/dev/null | head -5Repository: ui-layouts/ui-tools Length of output: 121 🏁 Script executed: # Search for other tools using ToolPlaygroundShell to compare patterns
rg 'ToolPlaygroundShell' --type=tsx -lRepository: ui-layouts/ui-tools Length of output: 90 🏁 Script executed: # Check other tool pages to see if they have similar issues
fd 'page.tsx' --path '*/tools/*' --type f | head -10Repository: ui-layouts/ui-tools Length of output: 278 🏁 Script executed: # Get context on one other tool page for comparison
cat -n ./app/\(tools\)/*/page.tsx 2>/dev/null | head -80Repository: 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: # Peek at BackgroundSnippetsGenerator to see if it has similar viewport-height issues
rg 'h-\[.*vh\]' ./components/view/background-snippets/ | head -20Repository: ui-layouts/ui-tools Length of output: 461 🏁 Script executed: # Check other tool generators for comparison
fd 'index.tsx' --path '*/view/*' | head -15Repository: ui-layouts/ui-tools Length of output: 278 🏁 Script executed: # Check structure of BackgroundSnippetsGenerator
cat -n ./components/view/background-snippets/index.tsx | head -100Repository: ui-layouts/ui-tools Length of output: 4513 🏁 Script executed: # Check what other view components exist
fd 'index.tsx' ./components/viewRepository: ui-layouts/ui-tools Length of output: 335 🏁 Script executed: # Check if BackgroundSnippetsGenerator also uses grid layout like ShaderGradientGenerator
cat -n ./components/view/background-snippets/index.tsx | sed -n '150,250p'Repository: ui-layouts/ui-tools Length of output: 3677 🏁 Script executed: # Look for the actual grid structure in BackgroundSnippetsGenerator
rg -n 'lg:grid|lg:col-span|grid-cols' ./components/view/background-snippets/index.tsxRepository: ui-layouts/ui-tools Length of output: 680 Layout breakage: The generator renders a full grid layout with fixed 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 |
||
| ); | ||
| } | ||
|
|
||
| export default page; | ||
| export default page; | ||
| 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> | ||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.