feat: add open in shadcn playground integration#262
feat: add open in shadcn playground integration#262ephraimduncan wants to merge 2 commits intojnsahaj:mainfrom
Conversation
|
@ephraimduncan is attempting to deploy a commit to the Vercel Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a flow to save and open themes in the shadcn Playground: UI button and logo, dialog action handlers to open/save for Playground, and a post-login action type plus auth dialog copy for the Playground sign-in flow. (48 words) Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Editor as Theme Editor
participant Dialog as Dialog Actions
participant Auth as Auth Dialog
participant Playground as Playground
User->>Editor: Click "Open in Playground"
Editor->>Dialog: handleOpenInPlayground(themeId)
alt Authenticated & no unsaved changes
Dialog->>Playground: openInPlayground(themeId)
else Authenticated & unsaved changes
Dialog->>Dialog: saveTheme(openInPlaygroundAfterSave=true)
Dialog->>Playground: openInPlayground(savedThemeId)
else Not authenticated
Dialog->>Auth: show auth dialog (SAVE_THEME_FOR_PLAYGROUND)
User->>Auth: Sign in
Auth->>Dialog: post-login action SAVE_THEME_FOR_PLAYGROUND
Dialog->>Dialog: saveTheme()
Dialog->>Playground: openInPlayground(savedThemeId)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
hooks/use-dialog-actions.tsx (2)
146-173:handleSaveClickoptions are mutually exclusive booleans — consider using a discriminated type.Three boolean flags (
shareAfterSave,openInV0AfterSave,openInPlaygroundAfterSave) where only one should be truthy at a time is fragile. If two are accidentally set, the lastifwins silently. A union type like{ afterSave?: "share" | "v0" | "playground" }would make the intent clearer and prevent misuse.Not blocking — the current pattern already existed for share/v0 and this just extends it.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-dialog-actions.tsx` around lines 146 - 173, The three boolean flags in handleSaveClick (shareAfterSave, openInV0AfterSave, openInPlaygroundAfterSave) should be replaced with a discriminated option like options?: { afterSave?: "share" | "v0" | "playground" } so only one outcome can be specified; update handleSaveClick to read options.afterSave and call setPendingAction("share"|"v0"|"playground") and openAuthDialog("signin", correspondingAction) for unauthenticated flows, remove the multiple if checks, ensure setSaveDialogOpen(true) remains, and update callers to pass the new afterSave value.
277-310:openInPlaygroundandhandleOpenInPlaygroundlargely duplicateopenInV0andhandleOpenInV0.The two pairs of functions share identical structure — theme URL construction, preset lookup, saved-preset logic, and the handle-or-save-first pattern. Consider extracting a shared helper to reduce duplication. Not urgent, but worth noting before a third integration is added.
♻️ Sketch of a shared helper
+type ExternalTarget = "v0" | "playground"; + +const externalTargetConfig: Record<ExternalTarget, { + event: string; + buildUrl: (themeUrl: string, themeName: string) => string; +}> = { + v0: { + event: "OPEN_IN_V0", + buildUrl: (themeUrl, themeName) => { + const title = `"${themeName}" from tweakcn`; + return `https://v0.dev/chat/api/open?url=${encodeURIComponent(themeUrl)}&title=${encodeURIComponent(title)}`; + }, + }, + playground: { + event: "OPEN_IN_PLAYGROUND", + buildUrl: (themeUrl) => + `https://play.blocks.so/api/open?url=${encodeURIComponent(themeUrl)}`, + }, +}; + +const openInExternal = (target: ExternalTarget, id?: string) => { + const presetId = id ?? themeState.preset; + if (!presetId) return; + const currentPreset = getPreset(presetId); + const isSavedPreset = id ? true : !!currentPreset && currentPreset.source === "SAVED"; + const themeName = currentPreset?.label || presetId; + const config = externalTargetConfig[target]; + + posthog.capture(config.event, { theme_id: presetId, theme_name: themeName, is_saved: isSavedPreset }); + + const themeUrl = isSavedPreset + ? `https://tweakcn.com/r/v0/${presetId}` + : `https://tweakcn.com/r/v0/${presetId}.json`; + window.open(config.buildUrl(themeUrl, themeName), "_blank", "noopener,noreferrer"); +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-dialog-actions.tsx` around lines 277 - 310, openInPlayground/handleOpenInPlayground duplicate the logic in openInV0/handleOpenInV0; extract a shared helper (e.g., buildAndOpenThemeInExternalTool) that accepts parameters for baseUrl/template (playground vs v0) and the optional id, uses getPreset and themeState.preset to resolve presetId and themeName, determines isSavedPreset by checking currentPreset.source === "SAVED", calls posthog.capture with the same payload fields, constructs the appropriate themeUrl and final URL, and calls window.open; then replace openInPlayground and openInV0 with thin wrappers that call the helper, and make handleOpenInPlayground/handleOpenInV0 keep the same save-if-modified flow by calling handleSaveClick({ openInPlaygroundAfterSave: true }) or the equivalent flag before delegating to the shared helper (preserving hasThemeChangedFromCheckpoint and handleSaveClick usage).components/editor/theme-preview-panel.tsx (1)
142-152: Consider adding a text label alongside the Playground logo for discoverability.The V0 logo is widely recognized, but the Playground logo (two diagonal strokes) may not be immediately identifiable to users. The tooltip helps, but the inline button label just shows "Open in" + an icon. Consider adding a short text like "Playground" next to the icon for clarity — or confirm this is intentional per design.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/editor/theme-preview-panel.tsx` around lines 142 - 152, The inline button currently shows "Open in" plus only the PlaygroundLogo which may be ambiguous; update the Button inside TooltipWrapper (the element that calls handleOpenInPlayground with themeId) to include a short visible text label like "Playground" next to the PlaygroundLogo for clarity and accessibility—e.g., keep the existing span with className "flex items-center…" and append the label text after the icon, ensuring styling (spacing and group-hover behavior) matches the existing layout and that the TooltipWrapper and onClick handler remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@hooks/use-dialog-actions.tsx`:
- Around line 277-296: Add a short clarifying comment inside the
openInPlayground function explaining why the /r/v0/ route is used for themeUrl:
note that the playground (play.blocks.so) expects the v0 registry payload format
and not the shadcn registry schema generated by
generateThemeRegistryItemFromStyles, and that generateV0RegistryPayload is the
endpoint that produces that format; place this comment immediately above the
themeUrl assignment (referencing themeUrl and isSavedPreset) so future
maintainers understand the intentional coupling.
---
Nitpick comments:
In `@components/editor/theme-preview-panel.tsx`:
- Around line 142-152: The inline button currently shows "Open in" plus only the
PlaygroundLogo which may be ambiguous; update the Button inside TooltipWrapper
(the element that calls handleOpenInPlayground with themeId) to include a short
visible text label like "Playground" next to the PlaygroundLogo for clarity and
accessibility—e.g., keep the existing span with className "flex items-center…"
and append the label text after the icon, ensuring styling (spacing and
group-hover behavior) matches the existing layout and that the TooltipWrapper
and onClick handler remain unchanged.
In `@hooks/use-dialog-actions.tsx`:
- Around line 146-173: The three boolean flags in handleSaveClick
(shareAfterSave, openInV0AfterSave, openInPlaygroundAfterSave) should be
replaced with a discriminated option like options?: { afterSave?: "share" | "v0"
| "playground" } so only one outcome can be specified; update handleSaveClick to
read options.afterSave and call setPendingAction("share"|"v0"|"playground") and
openAuthDialog("signin", correspondingAction) for unauthenticated flows, remove
the multiple if checks, ensure setSaveDialogOpen(true) remains, and update
callers to pass the new afterSave value.
- Around line 277-310: openInPlayground/handleOpenInPlayground duplicate the
logic in openInV0/handleOpenInV0; extract a shared helper (e.g.,
buildAndOpenThemeInExternalTool) that accepts parameters for baseUrl/template
(playground vs v0) and the optional id, uses getPreset and themeState.preset to
resolve presetId and themeName, determines isSavedPreset by checking
currentPreset.source === "SAVED", calls posthog.capture with the same payload
fields, constructs the appropriate themeUrl and final URL, and calls
window.open; then replace openInPlayground and openInV0 with thin wrappers that
call the helper, and make handleOpenInPlayground/handleOpenInV0 keep the same
save-if-modified flow by calling handleSaveClick({ openInPlaygroundAfterSave:
true }) or the equivalent flag before delegating to the shared helper
(preserving hasThemeChangedFromCheckpoint and handleSaveClick usage).
There was a problem hiding this comment.
🧹 Nitpick comments (2)
hooks/use-dialog-actions.tsx (2)
146-173: Theoptionsif-chain allows multiple flags to be true simultaneously, with last-writer-wins semantics.Currently callers only ever set one flag, so this is fine in practice. If a future caller accidentally sets two flags (e.g.,
{ shareAfterSave: true, openInPlaygroundAfterSave: true }),pendingActionsilently picks the last one checked. Anelse ifchain or early-return would make the mutual exclusivity explicit and prevent subtle bugs.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-dialog-actions.tsx` around lines 146 - 173, In handleSaveClick, the current if-chain that calls setPendingAction for options.shareAfterSave, options.openInV0AfterSave, and options.openInPlaygroundAfterSave allows multiple flags to be true and the last one wins; change this to make the intent explicit by making those checks mutually exclusive (e.g., convert to an if / else if / else if chain or perform early-returns after setting setPendingAction) so only one pendingAction ("share", "v0", or "playground") can ever be set; update any related logic around setSaveDialogOpen and openAuthDialog usage in handleSaveClick to preserve current behavior when session is falsy.
277-296: Extract shared logic betweenopenInV0andopenInPlayground.Both helpers share identical preset-resolution,
isSavedPresetderivation, themeName resolution, and analytics-capture logic (lines 240-261 vs 277-296). The only differences are the analytics event name ("OPEN_IN_V0" vs "OPEN_IN_PLAYGROUND"), the route path (/r/v0/vs/r/themes/), and the final URL target. A small shared helper (e.g.,resolveThemeUrl) could reduce this duplication.Not urgent since there are only two integrations today.
Regarding the
titleparameter:v0.dev/chat/api/opendocuments support forurland optionaltokenparameters, but no public documentation exists forplay.blocks.so/api/open(the endpoint does not appear in the public shadcn-play repository). Without access to the endpoint's handler or documentation, it is unclear whetherplay.blocks.so/api/opensupports atitleparameter. If the endpoint is maintained, consider checking its implementation or documentation to determine if including a title would improve user experience.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@hooks/use-dialog-actions.tsx` around lines 277 - 296, Both openInV0 and openInPlayground duplicate preset resolution, isSavedPreset derivation, themeName resolution and analytics capture; extract that shared logic into a small helper (e.g., resolveThemeMeta or resolveThemeUrlMeta) that takes an optional id and a base route/format and returns { presetId, isSavedPreset, themeName, themeUrl } (use getPreset and themeState.preset inside it) and/or a separate helper to emit analytics via posthog.capture; then update openInV0 and openInPlayground to call this helper, pass the differing pieces (event name and route/format) and only perform the final window.open call with the returned themeUrl. Reference: openInV0, openInPlayground, getPreset, themeState.preset, posthog.capture.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@hooks/use-dialog-actions.tsx`:
- Around line 146-173: In handleSaveClick, the current if-chain that calls
setPendingAction for options.shareAfterSave, options.openInV0AfterSave, and
options.openInPlaygroundAfterSave allows multiple flags to be true and the last
one wins; change this to make the intent explicit by making those checks
mutually exclusive (e.g., convert to an if / else if / else if chain or perform
early-returns after setting setPendingAction) so only one pendingAction
("share", "v0", or "playground") can ever be set; update any related logic
around setSaveDialogOpen and openAuthDialog usage in handleSaveClick to preserve
current behavior when session is falsy.
- Around line 277-296: Both openInV0 and openInPlayground duplicate preset
resolution, isSavedPreset derivation, themeName resolution and analytics
capture; extract that shared logic into a small helper (e.g., resolveThemeMeta
or resolveThemeUrlMeta) that takes an optional id and a base route/format and
returns { presetId, isSavedPreset, themeName, themeUrl } (use getPreset and
themeState.preset inside it) and/or a separate helper to emit analytics via
posthog.capture; then update openInV0 and openInPlayground to call this helper,
pass the differing pieces (event name and route/format) and only perform the
final window.open call with the returned themeUrl. Reference: openInV0,
openInPlayground, getPreset, themeState.preset, posthog.capture.
Summary
Changes
components/editor/theme-preview-panel.tsx— AddPlaygroundLogoSVG and playground button next to the v0 buttonhooks/use-dialog-actions.tsx— AddopenInPlayground,handleOpenInPlayground, and pending action handling for"playground"hooks/use-post-login-action.ts— AddSAVE_THEME_FOR_PLAYGROUNDto the post-login action type unionapp/(auth)/components/auth-dialog.tsx— Add contextual auth dialog copy for the playground actionSummary by CodeRabbit