Skip to content

feat: add open in shadcn playground integration#262

Open
ephraimduncan wants to merge 2 commits intojnsahaj:mainfrom
ephraimduncan:feature/open-in-playground
Open

feat: add open in shadcn playground integration#262
ephraimduncan wants to merge 2 commits intojnsahaj:mainfrom
ephraimduncan:feature/open-in-playground

Conversation

@ephraimduncan
Copy link
Copy Markdown

@ephraimduncan ephraimduncan commented Feb 17, 2026

Summary

  • Add "Open in Playground" button to the theme editor preview panel, allowing users to open their theme in the shadcn playground
  • Wire up the full save-if-needed and auth-if-needed flow (mirrors the existing "Open in v0" integration)
  • Add contextual copy for auth and save dialogs when the playground action is pending

Changes

  • components/editor/theme-preview-panel.tsx — Add PlaygroundLogo SVG and playground button next to the v0 button
  • hooks/use-dialog-actions.tsx — Add openInPlayground, handleOpenInPlayground, and pending action handling for "playground"
  • hooks/use-post-login-action.ts — Add SAVE_THEME_FOR_PLAYGROUND to the post-login action type union
  • app/(auth)/components/auth-dialog.tsx — Add contextual auth dialog copy for the playground action

Summary by CodeRabbit

  • New Features
    • Added "Open in shadcn Playground" button in the theme preview for instant opening of themes in the official playground.
    • Allow saving theme changes and automatically opening them in the Playground after save.
    • Added a sign-in prompt that appears when users need to sign in to save a theme and open it in the Playground.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 17, 2026

@ephraimduncan is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Auth dialog copy
app/(auth)/components/auth-dialog.tsx
Added SAVE_THEME_FOR_PLAYGROUND case to contextual copy (title and description) shown by the auth dialog.
Editor UI
components/editor/theme-preview-panel.tsx
Added local PlaygroundLogo component and an "Open in shadcn playground" button wired to handleOpenInPlayground(themeId); destructures new dialog action.
Dialog actions / orchestration
hooks/use-dialog-actions.tsx
Introduced pending action "playground", openInPlaygroundAfterSave option, handleOpenInPlayground(id?), internal openInPlayground(id?), and flow to save then open in Playground (analytics, URL resolution). Updated exported context type signatures.
Post-login action type
hooks/use-post-login-action.ts
Added new PostLoginActionType variant: SAVE_THEME_FOR_PLAYGROUND.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • jnsahaj

Poem

🐰 I hopped on a button and whisked with delight,
Saved a theme in a burrow then launched it in light.
Playgrounds of shadcn, with colors to play,
Rabbits and pixels now dance through the day. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add open in shadcn playground integration' clearly and specifically describes the main change in the changeset—adding an 'Open in Playground' feature that mirrors the existing v0 integration.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
hooks/use-dialog-actions.tsx (2)

146-173: handleSaveClick options 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 last if wins 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: openInPlayground and handleOpenInPlayground largely duplicate openInV0 and handleOpenInV0.

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).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
hooks/use-dialog-actions.tsx (2)

146-173: The options if-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 }), pendingAction silently picks the last one checked. An else if chain 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 between openInV0 and openInPlayground.

Both helpers share identical preset-resolution, isSavedPreset derivation, 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 title parameter: v0.dev/chat/api/open documents support for url and optional token parameters, but no public documentation exists for play.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 whether play.blocks.so/api/open supports a title parameter. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant