previewcn 0.1.0-alpha.7 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1818 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,10 +1,1822 @@
1
1
  #!/usr/bin/env node
2
- import {Command}from'commander';import l from'chalk';import y from'fs/promises';import a from'path';import h from'ora';import F from'prompts';import {spawn}from'child_process';import {detect}from'detect-package-manager';async function u(e){let t={isNextJs:false,isAppRouter:false};try{let n=a.join(e,"package.json"),r=await y.readFile(n,"utf-8"),c=JSON.parse(r);t.isNextJs=!!(c.dependencies?.next||c.devDependencies?.next);}catch{return t}let o=[a.join(e,"app"),a.join(e,"src","app")];for(let n of o)try{if((await y.stat(n)).isDirectory()){t.isAppRouter=!0;break}}catch{}return t}async function f(e){let t=[a.join(e,"app","layout.tsx"),a.join(e,"app","layout.ts"),a.join(e,"app","layout.jsx"),a.join(e,"app","layout.js"),a.join(e,"src","app","layout.tsx"),a.join(e,"src","app","layout.ts"),a.join(e,"src","app","layout.jsx"),a.join(e,"src","app","layout.js")];for(let o of t)try{return await y.access(o),o}catch{}return null}async function P(e){return await O(e,"@previewcn/devtools")}async function O(e,t){let o=await S(e);return o?!!(o.devDependencies?.[t]??o.dependencies?.[t]):false}async function S(e){try{let t=a.join(e,"package.json"),o=await y.readFile(t,"utf-8");return JSON.parse(o)}catch{return null}}var s={info:e=>console.log(l.blue("info"),e),success:e=>console.log(l.green("success"),e),warn:e=>console.log(l.yellow("warn"),e),error:e=>console.log(l.red("error"),e),hint:e=>console.log(l.gray("hint"),e)};var b=/^import[\s\S]*?;\s*$/gm,T='import "@previewcn/devtools/styles.css";',J='import { PreviewcnDevtools } from "@previewcn/devtools";',E='{process.env.NODE_ENV === "development" && <PreviewcnDevtools />}';function M(e,t){if(t.length===0)return e;let o=`
2
+ import {Command}from'commander';import*as x from'fs/promises';import x__default from'fs/promises';import*as l from'path';import l__default from'path';import p from'chalk';import V from'ora';import ue from'prompts';async function g(e){let t={isNextJs:false,isAppRouter:false};try{let r=l__default.join(e,"package.json"),a=await x__default.readFile(r,"utf-8"),n=JSON.parse(a);t.isNextJs=!!(n.dependencies?.next||n.devDependencies?.next);}catch{return t}let o=[l__default.join(e,"app"),l__default.join(e,"src","app")];for(let r of o)try{if((await x__default.stat(r)).isDirectory()){t.isAppRouter=!0;break}}catch{}return t}async function h(e){let t=[l__default.join(e,"app","layout.tsx"),l__default.join(e,"app","layout.ts"),l__default.join(e,"app","layout.jsx"),l__default.join(e,"app","layout.js"),l__default.join(e,"src","app","layout.tsx"),l__default.join(e,"src","app","layout.ts"),l__default.join(e,"src","app","layout.jsx"),l__default.join(e,"src","app","layout.js")];for(let o of t)try{return await x__default.access(o),o}catch{}return null}var s={info:e=>console.log(p.blue("info"),e),success:e=>console.log(p.green("success"),e),warn:e=>console.log(p.yellow("warn"),e),error:e=>console.log(p.red("error"),e),hint:e=>console.log(p.gray("hint"),e)};var oe=/^import[\s\S]*?;\s*$/gm,re="@/components/ui/previewcn",ne='{process.env.NODE_ENV === "development" && <PreviewcnDevtools />}';function se(e){return `import { PreviewcnDevtools } from "${e}";`}function ae(e,t){if(t.length===0)return e;let o=`
3
3
  ${t.join(`
4
- `)}`,n=[...e.matchAll(b)];if(n.length===0)return `${t.join(`
4
+ `)}`,r=[...e.matchAll(oe)];if(r.length===0)return `${t.join(`
5
5
  `)}
6
6
 
7
- ${e}`;let r=n[n.length-1],c=r.index+r[0].length;return e.slice(0,c)+o+e.slice(c)}function L(e,t){let o=e.match(/<body[^>]*>/);if(!o)return e;let n=o.index+o[0].length;return e.slice(0,n)+`
8
- `+t+e.slice(n)}async function x(e){let t=await y.readFile(e,"utf-8"),o=[];t.includes("@previewcn/devtools/styles.css")||o.push(T),t.includes("PreviewcnDevtools")||o.push(J);let n=M(t,o);n.includes("<PreviewcnDevtools")||(n=L(n,E)),await y.writeFile(e,n,"utf-8");}async function D(e){try{return (await y.readFile(e,"utf-8")).includes("PreviewcnDevtools")}catch{return false}}async function j(){s.info(`Running PreviewCN diagnostics...
9
- `);let e=[],t=await u(process.cwd());e.push({name:"Next.js App Router",pass:t.isNextJs&&t.isAppRouter,message:t.isNextJs?t.isAppRouter?"Detected":"App Router not found (using Pages Router?)":"Not a Next.js project"});let o=await P(process.cwd());e.push({name:"@previewcn/devtools package",pass:o,message:o?"Installed":"Not found (run `npx previewcn init`)"});let n=false,r=await f(process.cwd());r&&(n=await D(r)),e.push({name:"PreviewcnDevtools in layout",pass:n,message:n?"Found":"Not found in layout (run `npx previewcn init`)"}),console.log();for(let i of e){let d=i.pass?l.green("\u2713"):l.red("\u2717"),p=i.pass?l.green(i.message):l.red(i.message);console.log(` ${d} ${i.name}: ${p}`);}console.log(),e.every(i=>i.pass)?(s.success("All checks passed! PreviewCN devtools is ready to use."),s.info("Run your dev server and click the theme icon to open the editor.")):s.warn("Some checks failed. Run `npx previewcn init` to set up.");}function g(e){return l.cyan(e)}async function N(e,t){return !!(await F({type:"confirm",name:"value",message:e,initial:t})).value}async function k(e){return await detect({cwd:e})}async function A(e,t,o){let n=V(e,t);return new Promise((r,c)=>{let i=spawn(e,n,{cwd:o,stdio:"pipe"}),d="";i.stderr?.on("data",p=>{d+=p.toString();}),i.on("close",p=>{p===0?r():c(new Error(`Failed to install ${t}: ${d}`));}),i.on("error",p=>{c(p);});})}function V(e,t){return e==="pnpm"?["add","-D",t]:e==="yarn"?["add","-D",t]:e==="bun"?["add","-D",t]:["install","-D",t]}async function C(e){s.info(`Initializing PreviewCN...
10
- `);let t=h("Detecting project type...").start(),o=await u(process.cwd());o.isNextJs||(t.fail("Not a Next.js project"),s.error("PreviewCN requires a Next.js project with App Router."),s.hint("Make sure you're in the root of a Next.js project."),process.exit(1)),o.isAppRouter||(t.fail("App Router not detected"),s.error("PreviewCN requires Next.js App Router (app/ directory)."),s.hint("Create an app/ directory or migrate from pages/ to app/."),process.exit(1)),t.succeed("Next.js App Router project detected");let n=await f(process.cwd());n||(s.error("Could not find app/layout.tsx"),process.exit(1)),s.info(`Found layout at: ${g(a.relative(process.cwd(),n))}`),e.yes||await N("This will install @previewcn/devtools and modify your app layout. Continue?",true)||(s.info("Aborted."),process.exit(0)),await Y(n),console.log(),s.success("PreviewCN devtools initialized successfully!"),console.log(),s.info("Next step:"),console.log(` Run ${g("pnpm dev")} (or your dev command) to start the dev server.`),console.log(` Click the ${g("theme palette icon")} in the bottom-right corner to open the editor.`),console.log();}async function Y(e){let t=process.cwd(),o=h("Installing @previewcn/devtools...").start();try{let r=await k(t);await A(r,"@previewcn/devtools",t),o.succeed("Installed @previewcn/devtools");}catch(r){o.fail("Failed to install @previewcn/devtools"),s.error(r instanceof Error?r.message:String(r)),s.hint("You can install it manually: pnpm add -D @previewcn/devtools");}let n=h("Adding PreviewcnDevtools to layout...").start();try{await x(e),n.succeed("Added PreviewcnDevtools to layout");}catch(r){n.fail("Failed to modify layout for devtools"),s.error(r instanceof Error?r.message:String(r)),s.hint("You may need to add PreviewcnDevtools manually. See documentation.");}}var v=new Command;v.name("previewcn").description("CLI for PreviewCN - real-time shadcn/ui theme editor").version("0.1.0");v.command("init",{isDefault:true}).description("Initialize PreviewCN devtools in your Next.js project").option("-y, --yes","Skip confirmation prompts").action(C);v.command("doctor").description("Check PreviewCN setup and diagnose issues").action(j);v.parse();
7
+ ${e}`;let a=r[r.length-1],n=a.index+a[0].length;return e.slice(0,n)+o+e.slice(n)}function le(e,t){let o=e.match(/<body[^>]*>/);if(!o)return e;let r=o.index+o[0].length;return e.slice(0,r)+`
8
+ `+t+e.slice(r)}async function J(e,t=re){let o=await x__default.readFile(e,"utf-8"),r=[];o.includes("PreviewcnDevtools")||r.push(se(t));let a=ae(o,r);a.includes("<PreviewcnDevtools")||(a=le(a,ne)),await x__default.writeFile(e,a,"utf-8");}async function G(e){try{return (await x__default.readFile(e,"utf-8")).includes("PreviewcnDevtools")}catch{return false}}async function Y(e){let t=l.join(e,"components.json");try{let o=await x.readFile(t,"utf-8"),r=JSON.parse(o);if(r.aliases?.ui)return r.aliases.ui;if(r.aliases?.components)return `${r.aliases.components}/ui`}catch{}return "@/components/ui"}async function d(e){let o=(await Y(e)).replace(/^@\//,"");return l.join(e,o,"previewcn")}async function H(e){return `${await Y(e)}/previewcn`}async function ce(e){try{let t=await d(e),o=l__default.join(t,"index.ts");return await x__default.access(o),!0}catch{return false}}async function K(){s.info(`Running PreviewCN diagnostics...
9
+ `);let e=process.cwd(),t=[],o=await g(e);t.push({name:"Next.js App Router",pass:o.isNextJs&&o.isAppRouter,message:o.isNextJs?o.isAppRouter?"Detected":"App Router not found (using Pages Router?)":"Not a Next.js project"});let r=await ce(e),a=await d(e),n=l__default.relative(e,a);t.push({name:"PreviewCN components",pass:r,message:r?`Found in ${n}`:"Not found (run `npx previewcn init`)"});let c=false,v=await h(e);v&&(c=await G(v)),t.push({name:"PreviewcnDevtools in layout",pass:c,message:c?"Found":"Not found in layout (run `npx previewcn init`)"}),console.log();for(let i of t){let ee=i.pass?p.green("\u2713"):p.red("\u2717"),te=i.pass?p.green(i.message):p.red(i.message);console.log(` ${ee} ${i.name}: ${te}`);}console.log(),t.every(i=>i.pass)?(s.success("All checks passed! PreviewCN devtools is ready to use."),s.info("Run your dev server and click the theme icon to open the editor.")):s.warn("Some checks failed. Run `npx previewcn init` to set up.");}function P(){return `"use client";
10
+
11
+ import { colorPresets } from "./presets/colors";
12
+
13
+ type ColorPickerProps = {
14
+ value: string | null;
15
+ onChange: (colorPreset: string) => void;
16
+ };
17
+
18
+ export function ColorPicker({ value, onChange }: ColorPickerProps) {
19
+ return (
20
+ <div
21
+ className="relative grid gap-2.5 p-3 rounded-xl border border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)]"
22
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
23
+ >
24
+ <label className="block text-[10.5px] font-semibold tracking-[0.16em] uppercase text-[oklch(0.72_0_0)]">
25
+ Theme
26
+ </label>
27
+ <div className="grid grid-cols-6 gap-2">
28
+ {colorPresets.map((preset) => {
29
+ const isSelected = value === preset.name;
30
+ const primaryColor = preset.colors.light.primary;
31
+
32
+ return (
33
+ <button
34
+ key={preset.name}
35
+ onClick={() => onChange(preset.name)}
36
+ className={\`aspect-square rounded-lg border cursor-pointer transition-all duration-[160ms] focus-visible:outline-2 focus-visible:outline-[oklch(0.72_0.15_265)] focus-visible:outline-offset-2 \${
37
+ isSelected
38
+ ? "border-[oklch(0.72_0.15_265)] shadow-[0_0_0_1px_oklch(0.72_0.15_265),0_10px_20px_oklch(0_0_0/0.35)]"
39
+ : "border-[oklch(1_0_0/0.08)] hover:border-[oklch(1_0_0/0.18)]"
40
+ }\`}
41
+ style={{
42
+ backgroundColor: primaryColor,
43
+ boxShadow: isSelected
44
+ ? undefined
45
+ : "inset 0 1px 0 oklch(1 0 0 / 0.04)",
46
+ }}
47
+ aria-label={preset.label}
48
+ title={preset.label}
49
+ />
50
+ );
51
+ })}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+ `}function w(){return `"use client";
57
+
58
+ import { useState } from "react";
59
+
60
+ import type { ThemeConfig } from "./theme-applier";
61
+ import { copyToClipboard, generateExportCss } from "./css-export";
62
+
63
+ type CssExportButtonProps = {
64
+ config: ThemeConfig;
65
+ };
66
+
67
+ function CopyIcon() {
68
+ return (
69
+ <svg
70
+ xmlns="http://www.w3.org/2000/svg"
71
+ width="14"
72
+ height="14"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ strokeWidth="2"
77
+ strokeLinecap="round"
78
+ strokeLinejoin="round"
79
+ >
80
+ <rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
81
+ <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
82
+ </svg>
83
+ );
84
+ }
85
+
86
+ function CheckIcon() {
87
+ return (
88
+ <svg
89
+ xmlns="http://www.w3.org/2000/svg"
90
+ width="14"
91
+ height="14"
92
+ viewBox="0 0 24 24"
93
+ fill="none"
94
+ stroke="currentColor"
95
+ strokeWidth="2"
96
+ strokeLinecap="round"
97
+ strokeLinejoin="round"
98
+ >
99
+ <path d="M20 6 9 17l-5-5" />
100
+ </svg>
101
+ );
102
+ }
103
+
104
+ export function CssExportButton({ config }: CssExportButtonProps) {
105
+ const [copied, setCopied] = useState(false);
106
+
107
+ const exportCss = generateExportCss(config);
108
+ const isDisabled = !exportCss;
109
+ const label = copied ? "Copied!" : "Copy CSS";
110
+
111
+ const handleCopy = async () => {
112
+ if (!exportCss) return;
113
+
114
+ const success = await copyToClipboard(exportCss);
115
+
116
+ if (success) {
117
+ setCopied(true);
118
+ setTimeout(() => setCopied(false), 2000);
119
+ }
120
+ };
121
+
122
+ const baseClass =
123
+ "inline-flex items-center justify-center gap-1.5 min-h-[30px] px-2.5 py-1.5 mr-auto rounded-[10px] border text-xs font-medium tracking-[0.01em] cursor-pointer transition-all duration-[160ms]";
124
+
125
+ const stateClass = copied
126
+ ? "border-[oklch(0.72_0.17_142)] text-[oklch(0.72_0.17_142)]"
127
+ : isDisabled
128
+ ? "opacity-50 cursor-not-allowed border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] text-[oklch(0.96_0_0)]"
129
+ : "border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] text-[oklch(0.96_0_0)] hover:bg-[oklch(0.24_0.02_260/0.95)] hover:border-[oklch(1_0_0/0.18)]";
130
+
131
+ return (
132
+ <button
133
+ onClick={handleCopy}
134
+ className={\`\${baseClass} \${stateClass}\`}
135
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
136
+ disabled={isDisabled}
137
+ aria-label={label}
138
+ title={isDisabled ? "Select a theme first" : "Copy CSS to clipboard"}
139
+ >
140
+ {copied ? <CheckIcon /> : <CopyIcon />}
141
+ <span>{label}</span>
142
+ </button>
143
+ );
144
+ }
145
+ `}function T(){return `// CSS Export utilities for generating shadcn/ui compatible CSS
146
+
147
+ import { getColorPreset } from "./presets/colors";
148
+ import { getThemePreset } from "./presets/theme-presets";
149
+ import type { ThemeConfig } from "./theme-applier";
150
+
151
+ type ThemeColors = {
152
+ light: Record<string, string>;
153
+ dark: Record<string, string>;
154
+ };
155
+
156
+ type ResolvedTheme = {
157
+ colors: ThemeColors;
158
+ radius: string;
159
+ };
160
+
161
+ /**
162
+ * Resolve export-ready theme data based on config priority:
163
+ * 1. colorPreset (if set and found)
164
+ * 2. preset (full theme preset)
165
+ * 3. null (no theme selected)
166
+ */
167
+ function resolveExportTheme(config: ThemeConfig): ResolvedTheme | null {
168
+ const preset = config.preset !== null ? getThemePreset(config.preset) : null;
169
+ const colorPreset =
170
+ config.colorPreset !== null ? getColorPreset(config.colorPreset) : null;
171
+
172
+ const colors = colorPreset?.colors ?? preset?.colors ?? null;
173
+ if (!colors) return null;
174
+
175
+ const radius = config.radius ?? preset?.radius ?? "0.5rem";
176
+ return { colors, radius };
177
+ }
178
+
179
+ /**
180
+ * Format CSS variables with proper indentation
181
+ */
182
+ function formatCssVars(
183
+ cssVars: Record<string, string>,
184
+ indent: string = " "
185
+ ): string {
186
+ return Object.entries(cssVars)
187
+ .map(([key, value]) => \`\${indent}--\${key}: \${value};\`)
188
+ .join("\\n");
189
+ }
190
+
191
+ function formatCssBlock(selector: string, cssVars: Record<string, string>) {
192
+ return \`\${selector} {\\n\${formatCssVars(cssVars)}\\n}\`;
193
+ }
194
+
195
+ /**
196
+ * Generate CSS export string from current theme config
197
+ * Output format is compatible with shadcn/ui globals.css
198
+ */
199
+ export function generateExportCss(config: ThemeConfig): string | null {
200
+ const theme = resolveExportTheme(config);
201
+ if (!theme) return null;
202
+
203
+ const lightVars = { radius: theme.radius, ...theme.colors.light };
204
+
205
+ return \`\${formatCssBlock(":root", lightVars)}\\n\\n\${formatCssBlock(
206
+ ".dark",
207
+ theme.colors.dark
208
+ )}\`;
209
+ }
210
+
211
+ /**
212
+ * Copy text to clipboard
213
+ */
214
+ export async function copyToClipboard(text: string): Promise<boolean> {
215
+ try {
216
+ await navigator.clipboard.writeText(text);
217
+ return true;
218
+ } catch {
219
+ // Fallback for older browsers or non-HTTPS contexts
220
+ try {
221
+ const textArea = document.createElement("textarea");
222
+ textArea.value = text;
223
+ textArea.style.position = "fixed";
224
+ textArea.style.left = "-999999px";
225
+ textArea.style.top = "-999999px";
226
+ document.body.appendChild(textArea);
227
+ textArea.focus();
228
+ textArea.select();
229
+ const result = document.execCommand("copy");
230
+ document.body.removeChild(textArea);
231
+ return result;
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+ }
237
+ `}function S(){return `"use client";
238
+
239
+ import { lazy, Suspense, useState } from "react";
240
+
241
+ import { Trigger } from "./trigger";
242
+
243
+ const Panel = lazy(() => import("./panel"));
244
+
245
+ // Check if we're in development mode
246
+ const IS_DEV = process.env.NODE_ENV === "development";
247
+
248
+ // Inner component that uses hooks
249
+ function DevtoolsInner() {
250
+ const [open, setOpen] = useState(false);
251
+
252
+ return (
253
+ <>
254
+ {!open && <Trigger onClick={() => setOpen(true)} />}
255
+ {open && (
256
+ <Suspense fallback={null}>
257
+ <Panel onClose={() => setOpen(false)} />
258
+ </Suspense>
259
+ )}
260
+ </>
261
+ );
262
+ }
263
+
264
+ export function PreviewcnDevtools() {
265
+ // Production guard - return null in production
266
+ if (!IS_DEV) {
267
+ return null;
268
+ }
269
+
270
+ return <DevtoolsInner />;
271
+ }
272
+ `}function F(){return `"use client";
273
+
274
+ import { useState } from "react";
275
+
276
+ import { fontPresets } from "./presets/fonts";
277
+
278
+ type FontSelectorProps = {
279
+ value: string | null;
280
+ onChange: (font: string) => void;
281
+ };
282
+
283
+ function ChevronDownIcon() {
284
+ return (
285
+ <svg
286
+ xmlns="http://www.w3.org/2000/svg"
287
+ width="14"
288
+ height="14"
289
+ viewBox="0 0 24 24"
290
+ fill="none"
291
+ stroke="currentColor"
292
+ strokeWidth="2"
293
+ strokeLinecap="round"
294
+ strokeLinejoin="round"
295
+ >
296
+ <path d="m6 9 6 6 6-6" />
297
+ </svg>
298
+ );
299
+ }
300
+
301
+ export function FontSelector({ value, onChange }: FontSelectorProps) {
302
+ const [isOpen, setIsOpen] = useState(false);
303
+
304
+ const selectedFont = fontPresets.find((f) => f.value === value);
305
+ const displayLabel = selectedFont?.label ?? "Select font...";
306
+
307
+ return (
308
+ <div
309
+ className={\`relative grid gap-2.5 p-3 rounded-xl border border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] \${isOpen ? "z-30" : "z-0"}\`}
310
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
311
+ >
312
+ <label className="block text-[10.5px] font-semibold tracking-[0.16em] uppercase text-[oklch(0.72_0_0)]">
313
+ Font
314
+ </label>
315
+ <div className="relative z-[1]">
316
+ <button
317
+ onClick={() => setIsOpen(!isOpen)}
318
+ className="inline-flex items-center justify-between gap-1.5 w-full min-h-[30px] px-2.5 py-1.5 rounded-[10px] border border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] text-[oklch(0.96_0_0)] text-xs font-medium tracking-[0.01em] cursor-pointer transition-all duration-[160ms] hover:bg-[oklch(0.24_0.02_260/0.95)] hover:border-[oklch(1_0_0/0.18)] focus-visible:outline-2 focus-visible:outline-[oklch(0.72_0.15_265)] focus-visible:outline-offset-2"
319
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
320
+ aria-expanded={isOpen}
321
+ >
322
+ <span>{displayLabel}</span>
323
+ <ChevronDownIcon />
324
+ </button>
325
+
326
+ {isOpen && (
327
+ <div
328
+ className="absolute top-[calc(100%+6px)] left-0 z-20 w-full p-1.5 rounded-xl bg-[oklch(0.18_0.02_260)] border border-[oklch(1_0_0/0.08)] max-h-[220px] overflow-y-auto"
329
+ style={{
330
+ boxShadow: "0 10px 26px oklch(0 0 0 / 0.45)",
331
+ animation: "previewcn-pop 0.14s ease",
332
+ }}
333
+ >
334
+ {fontPresets.map((font) => {
335
+ const isSelected = value === font.value;
336
+ return (
337
+ <button
338
+ key={font.value}
339
+ onClick={() => {
340
+ onChange(font.value);
341
+ setIsOpen(false);
342
+ }}
343
+ className={\`flex w-full items-center rounded-lg border border-transparent px-2 py-1.5 text-xs text-left text-[oklch(0.96_0_0)] cursor-pointer transition-all duration-[140ms] hover:bg-[oklch(0.24_0.02_260/0.95)] focus-visible:outline-2 focus-visible:outline-[oklch(0.72_0.15_265)] focus-visible:outline-offset-1 \${
344
+ isSelected
345
+ ? "border-[oklch(0.72_0.15_265)] bg-[oklch(0.72_0.15_265/0.18)]"
346
+ : ""
347
+ }\`}
348
+ >
349
+ {font.label}
350
+ </button>
351
+ );
352
+ })}
353
+ </div>
354
+ )}
355
+ </div>
356
+ </div>
357
+ );
358
+ }
359
+ `}function E(){return `"use client";
360
+
361
+ type ModeToggleProps = {
362
+ value: boolean | null;
363
+ onChange: (darkMode: boolean) => void;
364
+ };
365
+
366
+ function SunIcon() {
367
+ return (
368
+ <svg
369
+ xmlns="http://www.w3.org/2000/svg"
370
+ width="16"
371
+ height="16"
372
+ viewBox="0 0 24 24"
373
+ fill="none"
374
+ stroke="currentColor"
375
+ strokeWidth="2"
376
+ strokeLinecap="round"
377
+ strokeLinejoin="round"
378
+ >
379
+ <circle cx="12" cy="12" r="4" />
380
+ <path d="M12 2v2" />
381
+ <path d="M12 20v2" />
382
+ <path d="m4.93 4.93 1.41 1.41" />
383
+ <path d="m17.66 17.66 1.41 1.41" />
384
+ <path d="M2 12h2" />
385
+ <path d="M20 12h2" />
386
+ <path d="m6.34 17.66-1.41 1.41" />
387
+ <path d="m19.07 4.93-1.41 1.41" />
388
+ </svg>
389
+ );
390
+ }
391
+
392
+ function MoonIcon() {
393
+ return (
394
+ <svg
395
+ xmlns="http://www.w3.org/2000/svg"
396
+ width="16"
397
+ height="16"
398
+ viewBox="0 0 24 24"
399
+ fill="none"
400
+ stroke="currentColor"
401
+ strokeWidth="2"
402
+ strokeLinecap="round"
403
+ strokeLinejoin="round"
404
+ >
405
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
406
+ </svg>
407
+ );
408
+ }
409
+
410
+ const baseButtonClass =
411
+ "inline-flex items-center justify-center gap-1.5 w-full min-h-[30px] px-2.5 py-1.5 rounded-[10px] border text-xs font-medium tracking-[0.01em] cursor-pointer transition-all duration-[160ms]";
412
+
413
+ const defaultButtonClass =
414
+ "border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] text-[oklch(0.96_0_0)] hover:bg-[oklch(0.24_0.02_260/0.95)] hover:border-[oklch(1_0_0/0.18)]";
415
+
416
+ const selectedButtonClass =
417
+ "bg-[oklch(0.24_0.02_260/0.95)] border-[oklch(0.72_0.15_265)] shadow-[0_0_0_1px_oklch(0.72_0.15_265),0_10px_24px_oklch(0_0_0/0.35)]";
418
+
419
+ export function ModeToggle({ value, onChange }: ModeToggleProps) {
420
+ const isDark = value ?? false;
421
+
422
+ return (
423
+ <div
424
+ className="relative grid gap-2.5 p-3 rounded-xl border border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)]"
425
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
426
+ >
427
+ <label className="block text-[10.5px] font-semibold tracking-[0.16em] uppercase text-[oklch(0.72_0_0)]">
428
+ Mode
429
+ </label>
430
+ <div className="grid grid-cols-2 gap-2">
431
+ <button
432
+ onClick={() => onChange(false)}
433
+ className={\`\${baseButtonClass} \${!isDark ? selectedButtonClass : defaultButtonClass}\`}
434
+ style={!isDark ? undefined : { boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
435
+ aria-label="Light mode"
436
+ >
437
+ <SunIcon />
438
+ <span>Light</span>
439
+ </button>
440
+ <button
441
+ onClick={() => onChange(true)}
442
+ className={\`\${baseButtonClass} \${isDark ? selectedButtonClass : defaultButtonClass}\`}
443
+ style={isDark ? undefined : { boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
444
+ aria-label="Dark mode"
445
+ >
446
+ <MoonIcon />
447
+ <span>Dark</span>
448
+ </button>
449
+ </div>
450
+ </div>
451
+ );
452
+ }
453
+ `}function N(){return `"use client";
454
+
455
+ import { useEffect, useRef } from "react";
456
+
457
+ import { useThemeState } from "./use-theme-state";
458
+ import { applyTheme } from "./theme-applier";
459
+ import { ColorPicker } from "./color-picker";
460
+ import { CssExportButton } from "./css-export-button";
461
+ import { FontSelector } from "./font-selector";
462
+ import { ModeToggle } from "./mode-toggle";
463
+ import { PresetSelector } from "./preset-selector";
464
+ import { RadiusSelector } from "./radius-selector";
465
+
466
+ type PanelProps = {
467
+ onClose: () => void;
468
+ };
469
+
470
+ function CloseIcon() {
471
+ return (
472
+ <svg
473
+ xmlns="http://www.w3.org/2000/svg"
474
+ width="18"
475
+ height="18"
476
+ viewBox="0 0 24 24"
477
+ fill="none"
478
+ stroke="currentColor"
479
+ strokeWidth="2"
480
+ strokeLinecap="round"
481
+ strokeLinejoin="round"
482
+ >
483
+ <path d="M18 6 6 18" />
484
+ <path d="m6 6 12 12" />
485
+ </svg>
486
+ );
487
+ }
488
+
489
+ function RotateCcwIcon() {
490
+ return (
491
+ <svg
492
+ xmlns="http://www.w3.org/2000/svg"
493
+ width="14"
494
+ height="14"
495
+ viewBox="0 0 24 24"
496
+ fill="none"
497
+ stroke="currentColor"
498
+ strokeWidth="2"
499
+ strokeLinecap="round"
500
+ strokeLinejoin="round"
501
+ >
502
+ <path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
503
+ <path d="M3 3v5h5" />
504
+ </svg>
505
+ );
506
+ }
507
+
508
+ // Inject keyframes for animations
509
+ const keyframesStyle = \`
510
+ @keyframes previewcn-slide-in-right {
511
+ from { transform: translateX(100%); }
512
+ to { transform: translateX(0); }
513
+ }
514
+ @keyframes previewcn-rise {
515
+ from { opacity: 0; transform: translateY(8px); }
516
+ to { opacity: 1; transform: translateY(0); }
517
+ }
518
+ @keyframes previewcn-pop {
519
+ from { opacity: 0; transform: translateY(-4px) scale(0.98); }
520
+ to { opacity: 1; transform: translateY(0) scale(1); }
521
+ }
522
+ \`;
523
+
524
+ export default function Panel({ onClose }: PanelProps) {
525
+ const {
526
+ config,
527
+ setColorPreset,
528
+ setRadius,
529
+ setDarkMode,
530
+ setFont,
531
+ setPresetTheme,
532
+ resetTheme,
533
+ } = useThemeState();
534
+
535
+ // Apply stored theme only when the panel is opened (user-initiated).
536
+ const didApplyOnOpenRef = useRef(false);
537
+ useEffect(() => {
538
+ if (didApplyOnOpenRef.current) return;
539
+ didApplyOnOpenRef.current = true;
540
+ applyTheme(config);
541
+ }, [config]);
542
+
543
+ // Inject keyframes stylesheet once
544
+ useEffect(() => {
545
+ const styleId = "previewcn-keyframes";
546
+ if (!document.getElementById(styleId)) {
547
+ const style = document.createElement("style");
548
+ style.id = styleId;
549
+ style.textContent = keyframesStyle;
550
+ document.head.appendChild(style);
551
+ }
552
+ }, []);
553
+
554
+ return (
555
+ <div
556
+ className="fixed top-0 right-0 z-[99998] flex flex-col w-80 h-dvh overflow-hidden text-[oklch(0.96_0_0)] text-[12.5px] leading-[1.55] tracking-[0.01em] border-l border-[oklch(1_0_0/0.08)]"
557
+ style={{
558
+ fontFamily:
559
+ 'var(--font-sans, "Inter", "Geist", "SF Pro Text", "Segoe UI", sans-serif)',
560
+ background: \`
561
+ radial-gradient(120% 140% at 0% 0%, oklch(0.25 0.05 265 / 0.25), transparent 45%),
562
+ linear-gradient(180deg, oklch(0.18 0.02 260), oklch(0.14 0.02 260))
563
+ \`,
564
+ boxShadow:
565
+ "0 24px 60px oklch(0 0 0 / 0.6), 0 1px 0 oklch(1 0 0 / 0.04) inset",
566
+ animation: "previewcn-slide-in-right 0.3s ease-out",
567
+ }}
568
+ >
569
+ {/* Header */}
570
+ <div
571
+ className="flex items-center justify-between px-4 py-3 border-b border-[oklch(1_0_0/0.08)]"
572
+ style={{
573
+ background: "linear-gradient(180deg, oklch(0.18 0.02 260), transparent)",
574
+ }}
575
+ >
576
+ <div className="flex items-center gap-2">
577
+ <span className="text-[13px] font-semibold tracking-[0.02em]">
578
+ previewcn
579
+ </span>
580
+ </div>
581
+ <button
582
+ onClick={onClose}
583
+ className="inline-flex items-center justify-center size-7 rounded-[10px] border border-transparent bg-transparent text-[oklch(0.72_0_0)] cursor-pointer transition-all duration-[160ms] hover:bg-[oklch(0.2_0.02_260/0.9)] hover:border-[oklch(1_0_0/0.08)] hover:text-[oklch(0.96_0_0)] focus-visible:outline-2 focus-visible:outline-[oklch(0.72_0.15_265)] focus-visible:outline-offset-2"
584
+ aria-label="Close"
585
+ >
586
+ <CloseIcon />
587
+ </button>
588
+ </div>
589
+
590
+ {/* Content */}
591
+ <div
592
+ className="flex-1 overflow-y-auto p-4 grid gap-4"
593
+ style={{
594
+ scrollbarWidth: "thin",
595
+ scrollbarColor: "oklch(1 0 0 / 0.2) transparent",
596
+ }}
597
+ >
598
+ <div style={{ animation: "previewcn-rise 0.32s ease both", animationDelay: "0.02s" }}>
599
+ <PresetSelector value={config.preset} onChange={setPresetTheme} />
600
+ </div>
601
+ <div style={{ animation: "previewcn-rise 0.32s ease both", animationDelay: "0.05s" }}>
602
+ <ColorPicker value={config.colorPreset} onChange={setColorPreset} />
603
+ </div>
604
+ <div style={{ animation: "previewcn-rise 0.32s ease both", animationDelay: "0.08s" }}>
605
+ <RadiusSelector value={config.radius} onChange={setRadius} />
606
+ </div>
607
+ <div style={{ animation: "previewcn-rise 0.32s ease both", animationDelay: "0.11s" }}>
608
+ <FontSelector value={config.font} onChange={setFont} />
609
+ </div>
610
+ <div style={{ animation: "previewcn-rise 0.32s ease both", animationDelay: "0.14s" }}>
611
+ <ModeToggle value={config.darkMode} onChange={setDarkMode} />
612
+ </div>
613
+ </div>
614
+
615
+ {/* Footer */}
616
+ <div
617
+ className="flex items-center justify-end px-4 py-3 border-t border-[oklch(1_0_0/0.08)]"
618
+ style={{
619
+ background: "linear-gradient(0deg, oklch(0.18 0.02 260), transparent)",
620
+ }}
621
+ >
622
+ <CssExportButton config={config} />
623
+ <button
624
+ onClick={resetTheme}
625
+ className="inline-flex items-center justify-center gap-1.5 min-h-[30px] px-2.5 py-1.5 rounded-[10px] border border-transparent bg-transparent text-[oklch(0.72_0_0)] text-xs font-medium tracking-[0.01em] cursor-pointer transition-all duration-[160ms] hover:bg-[oklch(0.2_0.02_260/0.9)] hover:border-[oklch(1_0_0/0.08)] hover:text-[oklch(0.96_0_0)]"
626
+ >
627
+ <RotateCcwIcon />
628
+ <span>Reset</span>
629
+ </button>
630
+ </div>
631
+ </div>
632
+ );
633
+ }
634
+ `}function I(){return `"use client";
635
+
636
+ import { themePresets } from "./presets/theme-presets";
637
+
638
+ type PresetSelectorProps = {
639
+ value: string | null;
640
+ onChange: (preset: string) => void;
641
+ };
642
+
643
+ const baseCardClass =
644
+ "flex flex-col items-center gap-1.5 w-full min-h-[56px] p-2 rounded-[10px] border text-[11px] font-medium cursor-pointer transition-all duration-[160ms] focus-visible:outline-2 focus-visible:outline-[oklch(0.72_0.15_265)] focus-visible:outline-offset-2";
645
+
646
+ const defaultCardClass =
647
+ "border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] text-[oklch(0.96_0_0)] hover:bg-[oklch(0.24_0.02_260/0.95)] hover:border-[oklch(1_0_0/0.18)]";
648
+
649
+ const selectedCardClass =
650
+ "bg-[oklch(0.24_0.02_260/0.95)] border-[oklch(0.72_0.15_265)] shadow-[0_0_0_1px_oklch(0.72_0.15_265),0_10px_24px_oklch(0_0_0/0.35)]";
651
+
652
+ export function PresetSelector({ value, onChange }: PresetSelectorProps) {
653
+ return (
654
+ <div
655
+ className="relative grid gap-2.5 p-3 rounded-xl border border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)]"
656
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
657
+ >
658
+ <span className="block text-[10.5px] font-semibold tracking-[0.16em] uppercase text-[oklch(0.72_0_0)]">
659
+ Presets
660
+ </span>
661
+ <div className="grid grid-cols-3 gap-2">
662
+ {themePresets.map((preset) => {
663
+ const isSelected = value === preset.name;
664
+ // Use light mode primary color for preview
665
+ const primaryColor = preset.colors.light.primary;
666
+ const bgColor = preset.colors.light.background;
667
+
668
+ return (
669
+ <button
670
+ key={preset.name}
671
+ onClick={() => onChange(preset.name)}
672
+ className={\`\${baseCardClass} \${isSelected ? selectedCardClass : defaultCardClass}\`}
673
+ style={isSelected ? undefined : { boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
674
+ aria-label={preset.label}
675
+ aria-pressed={isSelected}
676
+ >
677
+ <div className="flex w-full h-[18px] rounded overflow-hidden border border-[oklch(1_0_0/0.08)]">
678
+ <div
679
+ className="flex-1 border-r border-[oklch(0_0_0/0.1)]"
680
+ style={{ backgroundColor: bgColor }}
681
+ />
682
+ <div className="flex-1" style={{ backgroundColor: primaryColor }} />
683
+ </div>
684
+ <span className="text-[oklch(0.72_0_0)]">{preset.label}</span>
685
+ </button>
686
+ );
687
+ })}
688
+ </div>
689
+ </div>
690
+ );
691
+ }
692
+ `}function R(){return `// Color preset data used by the devtools color picker.
693
+ // Kept in a separate file to keep logic files small.
694
+
695
+ export type PresetSpec = {
696
+ name: string;
697
+ label: string;
698
+ primary: {
699
+ light: string;
700
+ dark: string;
701
+ };
702
+ primaryForeground?: {
703
+ light?: string;
704
+ dark?: string;
705
+ };
706
+ destructive?: {
707
+ light?: string;
708
+ dark?: string;
709
+ };
710
+ };
711
+
712
+ export const colorPresetSpecs: PresetSpec[] = [
713
+ {
714
+ name: "neutral",
715
+ label: "Neutral",
716
+ primary: { light: "oklch(0.556 0 0)", dark: "oklch(0.708 0 0)" },
717
+ primaryForeground: {
718
+ light: "oklch(0.985 0 0)",
719
+ dark: "oklch(0.205 0 0)",
720
+ },
721
+ },
722
+ {
723
+ name: "red",
724
+ label: "Red",
725
+ primary: {
726
+ light: "oklch(0.577 0.245 27.325)",
727
+ dark: "oklch(0.637 0.237 25.331)",
728
+ },
729
+ primaryForeground: {
730
+ light: "oklch(0.971 0.013 17.38)",
731
+ dark: "oklch(0.971 0.013 17.38)",
732
+ },
733
+ destructive: { light: "oklch(0.505 0.213 27.518)" },
734
+ },
735
+ {
736
+ name: "orange",
737
+ label: "Orange",
738
+ primary: {
739
+ light: "oklch(0.646 0.222 41.116)",
740
+ dark: "oklch(0.705 0.213 47.604)",
741
+ },
742
+ primaryForeground: {
743
+ light: "oklch(0.98 0.016 73.684)",
744
+ dark: "oklch(0.98 0.016 73.684)",
745
+ },
746
+ },
747
+ {
748
+ name: "amber",
749
+ label: "Amber",
750
+ primary: { light: "oklch(0.67 0.16 58)", dark: "oklch(0.77 0.16 70)" },
751
+ primaryForeground: {
752
+ light: "oklch(0.99 0.02 95)",
753
+ dark: "oklch(0.28 0.07 46)",
754
+ },
755
+ },
756
+ {
757
+ name: "yellow",
758
+ label: "Yellow",
759
+ primary: {
760
+ light: "oklch(0.852 0.199 91.936)",
761
+ dark: "oklch(0.795 0.184 86.047)",
762
+ },
763
+ primaryForeground: {
764
+ light: "oklch(0.421 0.095 57.708)",
765
+ dark: "oklch(0.421 0.095 57.708)",
766
+ },
767
+ },
768
+ {
769
+ name: "lime",
770
+ label: "Lime",
771
+ primary: {
772
+ light: "oklch(0.65 0.18 132)",
773
+ dark: "oklch(0.77 0.20 131)",
774
+ },
775
+ primaryForeground: {
776
+ light: "oklch(0.99 0.03 121)",
777
+ dark: "oklch(0.27 0.07 132)",
778
+ },
779
+ },
780
+ {
781
+ name: "green",
782
+ label: "Green",
783
+ primary: {
784
+ light: "oklch(0.648 0.2 131.684)",
785
+ dark: "oklch(0.648 0.2 131.684)",
786
+ },
787
+ primaryForeground: {
788
+ light: "oklch(0.986 0.031 120.757)",
789
+ dark: "oklch(0.986 0.031 120.757)",
790
+ },
791
+ },
792
+ {
793
+ name: "emerald",
794
+ label: "Emerald",
795
+ primary: {
796
+ light: "oklch(0.60 0.13 163)",
797
+ dark: "oklch(0.70 0.15 162)",
798
+ },
799
+ primaryForeground: {
800
+ light: "oklch(0.98 0.02 166)",
801
+ dark: "oklch(0.26 0.05 173)",
802
+ },
803
+ },
804
+ {
805
+ name: "teal",
806
+ label: "Teal",
807
+ primary: { light: "oklch(0.60 0.10 185)", dark: "oklch(0.70 0.12 183)" },
808
+ primaryForeground: {
809
+ light: "oklch(0.98 0.01 181)",
810
+ dark: "oklch(0.28 0.04 193)",
811
+ },
812
+ },
813
+ {
814
+ name: "cyan",
815
+ label: "Cyan",
816
+ primary: { light: "oklch(0.61 0.11 222)", dark: "oklch(0.71 0.13 215)" },
817
+ primaryForeground: {
818
+ light: "oklch(0.98 0.02 201)",
819
+ dark: "oklch(0.30 0.05 230)",
820
+ },
821
+ },
822
+ {
823
+ name: "sky",
824
+ label: "Sky",
825
+ primary: { light: "oklch(0.59 0.14 242)", dark: "oklch(0.68 0.15 237)" },
826
+ primaryForeground: {
827
+ light: "oklch(0.98 0.01 237)",
828
+ dark: "oklch(0.29 0.06 243)",
829
+ },
830
+ },
831
+ {
832
+ name: "blue",
833
+ label: "Blue",
834
+ primary: {
835
+ light: "oklch(0.488 0.243 264.376)",
836
+ dark: "oklch(0.42 0.18 266)",
837
+ },
838
+ primaryForeground: {
839
+ light: "oklch(0.97 0.014 254.604)",
840
+ dark: "oklch(0.97 0.014 254.604)",
841
+ },
842
+ },
843
+ {
844
+ name: "indigo",
845
+ label: "Indigo",
846
+ primary: { light: "oklch(0.51 0.23 277)", dark: "oklch(0.59 0.20 277)" },
847
+ primaryForeground: {
848
+ light: "oklch(0.96 0.02 272)",
849
+ dark: "oklch(0.96 0.02 272)",
850
+ },
851
+ },
852
+ {
853
+ name: "violet",
854
+ label: "Violet",
855
+ primary: {
856
+ light: "oklch(0.541 0.281 293.009)",
857
+ dark: "oklch(0.606 0.25 292.717)",
858
+ },
859
+ primaryForeground: {
860
+ light: "oklch(0.969 0.016 293.756)",
861
+ dark: "oklch(0.969 0.016 293.756)",
862
+ },
863
+ },
864
+ {
865
+ name: "purple",
866
+ label: "Purple",
867
+ primary: { light: "oklch(0.56 0.25 302)", dark: "oklch(0.63 0.23 304)" },
868
+ primaryForeground: {
869
+ light: "oklch(0.98 0.01 308)",
870
+ dark: "oklch(0.98 0.01 308)",
871
+ },
872
+ },
873
+ {
874
+ name: "fuchsia",
875
+ label: "Fuchsia",
876
+ primary: { light: "oklch(0.59 0.26 323)", dark: "oklch(0.67 0.26 322)" },
877
+ primaryForeground: {
878
+ light: "oklch(0.98 0.02 320)",
879
+ dark: "oklch(0.98 0.02 320)",
880
+ },
881
+ },
882
+ {
883
+ name: "pink",
884
+ label: "Pink",
885
+ primary: { light: "oklch(0.59 0.22 1)", dark: "oklch(0.66 0.21 354)" },
886
+ primaryForeground: {
887
+ light: "oklch(0.97 0.01 343)",
888
+ dark: "oklch(0.97 0.01 343)",
889
+ },
890
+ },
891
+ {
892
+ name: "rose",
893
+ label: "Rose",
894
+ primary: {
895
+ light: "oklch(0.586 0.253 17.585)",
896
+ dark: "oklch(0.645 0.246 16.439)",
897
+ },
898
+ primaryForeground: {
899
+ light: "oklch(0.969 0.015 12.422)",
900
+ dark: "oklch(0.969 0.015 12.422)",
901
+ },
902
+ },
903
+ ];
904
+ `}function D(){return `// Color presets compatible with shadcn/ui
905
+ // Uses OKLCH color space for better perceptual uniformity
906
+
907
+ import { colorPresetSpecs, type PresetSpec } from "./color-preset-specs";
908
+
909
+ export type ColorPreset = {
910
+ name: string;
911
+ label: string;
912
+ colors: {
913
+ light: Record<string, string>;
914
+ dark: Record<string, string>;
915
+ };
916
+ };
917
+
918
+ // Base neutral colors that all themes share
919
+ const neutralColors = {
920
+ light: {
921
+ background: "oklch(1 0 0)",
922
+ foreground: "oklch(0.145 0 0)",
923
+ card: "oklch(1 0 0)",
924
+ "card-foreground": "oklch(0.145 0 0)",
925
+ popover: "oklch(1 0 0)",
926
+ "popover-foreground": "oklch(0.145 0 0)",
927
+ secondary: "oklch(0.97 0 0)",
928
+ "secondary-foreground": "oklch(0.205 0 0)",
929
+ muted: "oklch(0.97 0 0)",
930
+ "muted-foreground": "oklch(0.556 0 0)",
931
+ accent: "oklch(0.97 0 0)",
932
+ "accent-foreground": "oklch(0.205 0 0)",
933
+ border: "oklch(0.922 0 0)",
934
+ input: "oklch(0.922 0 0)",
935
+ ring: "oklch(0.708 0 0)",
936
+ },
937
+ dark: {
938
+ background: "oklch(0.145 0 0)",
939
+ foreground: "oklch(0.985 0 0)",
940
+ card: "oklch(0.205 0 0)",
941
+ "card-foreground": "oklch(0.985 0 0)",
942
+ popover: "oklch(0.205 0 0)",
943
+ "popover-foreground": "oklch(0.985 0 0)",
944
+ secondary: "oklch(0.269 0 0)",
945
+ "secondary-foreground": "oklch(0.985 0 0)",
946
+ muted: "oklch(0.269 0 0)",
947
+ "muted-foreground": "oklch(0.708 0 0)",
948
+ accent: "oklch(0.269 0 0)",
949
+ "accent-foreground": "oklch(0.985 0 0)",
950
+ border: "oklch(1 0 0 / 10%)",
951
+ input: "oklch(1 0 0 / 15%)",
952
+ ring: "oklch(0.556 0 0)",
953
+ },
954
+ };
955
+
956
+ const defaultDestructive = {
957
+ light: "oklch(0.577 0.245 27.325)",
958
+ dark: "oklch(0.704 0.191 22.216)",
959
+ };
960
+
961
+ const defaultPrimaryForeground = {
962
+ light: "oklch(0.985 0 0)",
963
+ dark: "oklch(0.985 0 0)",
964
+ };
965
+
966
+ function createColorPreset(spec: PresetSpec): ColorPreset {
967
+ return {
968
+ name: spec.name,
969
+ label: spec.label,
970
+ colors: {
971
+ light: {
972
+ ...neutralColors.light,
973
+ primary: spec.primary.light,
974
+ "primary-foreground":
975
+ spec.primaryForeground?.light ?? defaultPrimaryForeground.light,
976
+ destructive: spec.destructive?.light ?? defaultDestructive.light,
977
+ },
978
+ dark: {
979
+ ...neutralColors.dark,
980
+ primary: spec.primary.dark,
981
+ "primary-foreground":
982
+ spec.primaryForeground?.dark ?? defaultPrimaryForeground.dark,
983
+ destructive: spec.destructive?.dark ?? defaultDestructive.dark,
984
+ },
985
+ },
986
+ };
987
+ }
988
+
989
+ export const colorPresets: ColorPreset[] =
990
+ colorPresetSpecs.map(createColorPreset);
991
+
992
+ export function getColorPreset(name: string): ColorPreset | undefined {
993
+ return colorPresets.find((p) => p.name === name);
994
+ }
995
+ `}function j(){return `// Font presets using Google Fonts
996
+
997
+ export type FontPreset = {
998
+ label: string;
999
+ value: string;
1000
+ fontFamily: string;
1001
+ googleFontsUrl: string;
1002
+ };
1003
+
1004
+ export const fontPresets: FontPreset[] = [
1005
+ {
1006
+ label: "Inter",
1007
+ value: "inter",
1008
+ fontFamily: '"Inter", sans-serif',
1009
+ googleFontsUrl:
1010
+ "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
1011
+ },
1012
+ {
1013
+ label: "Noto Sans",
1014
+ value: "noto-sans",
1015
+ fontFamily: '"Noto Sans", sans-serif',
1016
+ googleFontsUrl:
1017
+ "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@400;500;600;700&display=swap",
1018
+ },
1019
+ {
1020
+ label: "Nunito Sans",
1021
+ value: "nunito-sans",
1022
+ fontFamily: '"Nunito Sans", sans-serif',
1023
+ googleFontsUrl:
1024
+ "https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;500;600;700&display=swap",
1025
+ },
1026
+ {
1027
+ label: "Figtree",
1028
+ value: "figtree",
1029
+ fontFamily: '"Figtree", sans-serif',
1030
+ googleFontsUrl:
1031
+ "https://fonts.googleapis.com/css2?family=Figtree:wght@400;500;600;700&display=swap",
1032
+ },
1033
+ {
1034
+ label: "Roboto",
1035
+ value: "roboto",
1036
+ fontFamily: '"Roboto", sans-serif',
1037
+ googleFontsUrl:
1038
+ "https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap",
1039
+ },
1040
+ {
1041
+ label: "Raleway",
1042
+ value: "raleway",
1043
+ fontFamily: '"Raleway", sans-serif',
1044
+ googleFontsUrl:
1045
+ "https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap",
1046
+ },
1047
+ {
1048
+ label: "DM Sans",
1049
+ value: "dm-sans",
1050
+ fontFamily: '"DM Sans", sans-serif',
1051
+ googleFontsUrl:
1052
+ "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap",
1053
+ },
1054
+ {
1055
+ label: "Public Sans",
1056
+ value: "public-sans",
1057
+ fontFamily: '"Public Sans", sans-serif',
1058
+ googleFontsUrl:
1059
+ "https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;500;600;700&display=swap",
1060
+ },
1061
+ {
1062
+ label: "Outfit",
1063
+ value: "outfit",
1064
+ fontFamily: '"Outfit", sans-serif',
1065
+ googleFontsUrl:
1066
+ "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap",
1067
+ },
1068
+ {
1069
+ label: "JetBrains Mono",
1070
+ value: "jetbrains-mono",
1071
+ fontFamily: '"JetBrains Mono", monospace',
1072
+ googleFontsUrl:
1073
+ "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap",
1074
+ },
1075
+ ];
1076
+
1077
+ export function getFontPreset(value: string): FontPreset | undefined {
1078
+ return fontPresets.find((f) => f.value === value);
1079
+ }
1080
+ `}function M(){return `export * from "./colors";
1081
+ export * from "./color-preset-specs";
1082
+ export * from "./fonts";
1083
+ export * from "./radius";
1084
+ export * from "./theme-presets";
1085
+ `}function O(){return `// Radius presets for border-radius customization
1086
+
1087
+ export type RadiusPreset = {
1088
+ name: string;
1089
+ label: string;
1090
+ value: string;
1091
+ };
1092
+
1093
+ export const radiusPresets: RadiusPreset[] = [
1094
+ { name: "none", label: "None", value: "0rem" },
1095
+ { name: "sm", label: "SM", value: "0.3rem" },
1096
+ { name: "md", label: "MD", value: "0.5rem" },
1097
+ { name: "lg", label: "LG", value: "0.625rem" },
1098
+ { name: "xl", label: "XL", value: "0.75rem" },
1099
+ { name: "full", label: "Full", value: "1rem" },
1100
+ ];
1101
+
1102
+ export function getRadiusPreset(name: string): RadiusPreset | undefined {
1103
+ return radiusPresets.find((p) => p.name === name);
1104
+ }
1105
+ `}function A(){return `// Theme presets adapted from tweakcn (Apache License 2.0)
1106
+ // https://github.com/jnsahaj/tweakcn
1107
+ // Original presets by Sahaj Jain
1108
+
1109
+ export type ThemePresetFont = {
1110
+ value: string;
1111
+ fontFamily: string;
1112
+ googleFontsUrl: string;
1113
+ };
1114
+
1115
+ export type ThemePreset = {
1116
+ name: string;
1117
+ label: string;
1118
+ colors: {
1119
+ light: Record<string, string>;
1120
+ dark: Record<string, string>;
1121
+ };
1122
+ radius: string;
1123
+ font?: ThemePresetFont;
1124
+ };
1125
+
1126
+ export const themePresets: ThemePreset[] = [
1127
+ {
1128
+ name: "vercel",
1129
+ label: "Vercel",
1130
+ colors: {
1131
+ light: {
1132
+ background: "oklch(0.99 0 0)",
1133
+ foreground: "oklch(0 0 0)",
1134
+ card: "oklch(1.00 0 0)",
1135
+ "card-foreground": "oklch(0 0 0)",
1136
+ popover: "oklch(0.99 0 0)",
1137
+ "popover-foreground": "oklch(0 0 0)",
1138
+ primary: "oklch(0 0 0)",
1139
+ "primary-foreground": "oklch(1.00 0 0)",
1140
+ secondary: "oklch(0.94 0 0)",
1141
+ "secondary-foreground": "oklch(0 0 0)",
1142
+ muted: "oklch(0.97 0 0)",
1143
+ "muted-foreground": "oklch(0.44 0 0)",
1144
+ accent: "oklch(0.94 0 0)",
1145
+ "accent-foreground": "oklch(0 0 0)",
1146
+ destructive: "oklch(0.63 0.19 23.03)",
1147
+ "destructive-foreground": "oklch(1.00 0 0)",
1148
+ border: "oklch(0.92 0 0)",
1149
+ input: "oklch(0.94 0 0)",
1150
+ ring: "oklch(0 0 0)",
1151
+ "chart-1": "oklch(0.81 0.17 75.35)",
1152
+ "chart-2": "oklch(0.55 0.22 264.53)",
1153
+ "chart-3": "oklch(0.72 0 0)",
1154
+ "chart-4": "oklch(0.92 0 0)",
1155
+ "chart-5": "oklch(0.56 0 0)",
1156
+ sidebar: "oklch(0.99 0 0)",
1157
+ "sidebar-foreground": "oklch(0 0 0)",
1158
+ "sidebar-primary": "oklch(0 0 0)",
1159
+ "sidebar-primary-foreground": "oklch(1.00 0 0)",
1160
+ "sidebar-accent": "oklch(0.94 0 0)",
1161
+ "sidebar-accent-foreground": "oklch(0 0 0)",
1162
+ "sidebar-border": "oklch(0.94 0 0)",
1163
+ "sidebar-ring": "oklch(0 0 0)",
1164
+ },
1165
+ dark: {
1166
+ background: "oklch(0 0 0)",
1167
+ foreground: "oklch(1.00 0 0)",
1168
+ card: "oklch(0.14 0 0)",
1169
+ "card-foreground": "oklch(1.00 0 0)",
1170
+ popover: "oklch(0.18 0 0)",
1171
+ "popover-foreground": "oklch(1.00 0 0)",
1172
+ primary: "oklch(1.00 0 0)",
1173
+ "primary-foreground": "oklch(0 0 0)",
1174
+ secondary: "oklch(0.25 0 0)",
1175
+ "secondary-foreground": "oklch(1.00 0 0)",
1176
+ muted: "oklch(0.23 0 0)",
1177
+ "muted-foreground": "oklch(0.72 0 0)",
1178
+ accent: "oklch(0.32 0 0)",
1179
+ "accent-foreground": "oklch(1.00 0 0)",
1180
+ destructive: "oklch(0.69 0.20 23.91)",
1181
+ "destructive-foreground": "oklch(0 0 0)",
1182
+ border: "oklch(0.26 0 0)",
1183
+ input: "oklch(0.32 0 0)",
1184
+ ring: "oklch(0.72 0 0)",
1185
+ "chart-1": "oklch(0.81 0.17 75.35)",
1186
+ "chart-2": "oklch(0.58 0.21 260.84)",
1187
+ "chart-3": "oklch(0.56 0 0)",
1188
+ "chart-4": "oklch(0.44 0 0)",
1189
+ "chart-5": "oklch(0.92 0 0)",
1190
+ sidebar: "oklch(0.18 0 0)",
1191
+ "sidebar-foreground": "oklch(1.00 0 0)",
1192
+ "sidebar-primary": "oklch(1.00 0 0)",
1193
+ "sidebar-primary-foreground": "oklch(0 0 0)",
1194
+ "sidebar-accent": "oklch(0.32 0 0)",
1195
+ "sidebar-accent-foreground": "oklch(1.00 0 0)",
1196
+ "sidebar-border": "oklch(0.32 0 0)",
1197
+ "sidebar-ring": "oklch(0.72 0 0)",
1198
+ },
1199
+ },
1200
+ radius: "0.5rem",
1201
+ font: {
1202
+ value: "inter",
1203
+ fontFamily: '"Inter", sans-serif',
1204
+ googleFontsUrl:
1205
+ "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap",
1206
+ },
1207
+ },
1208
+ {
1209
+ name: "supabase",
1210
+ label: "Supabase",
1211
+ colors: {
1212
+ light: {
1213
+ background: "#fcfcfc",
1214
+ foreground: "#171717",
1215
+ card: "#fcfcfc",
1216
+ "card-foreground": "#171717",
1217
+ popover: "#fcfcfc",
1218
+ "popover-foreground": "#525252",
1219
+ primary: "#72e3ad",
1220
+ "primary-foreground": "#1e2723",
1221
+ secondary: "#fdfdfd",
1222
+ "secondary-foreground": "#171717",
1223
+ muted: "#ededed",
1224
+ "muted-foreground": "#202020",
1225
+ accent: "#ededed",
1226
+ "accent-foreground": "#202020",
1227
+ destructive: "#ca3214",
1228
+ "destructive-foreground": "#fffcfc",
1229
+ border: "#dfdfdf",
1230
+ input: "#f6f6f6",
1231
+ ring: "#72e3ad",
1232
+ "chart-1": "#72e3ad",
1233
+ "chart-2": "#3b82f6",
1234
+ "chart-3": "#8b5cf6",
1235
+ "chart-4": "#f59e0b",
1236
+ "chart-5": "#10b981",
1237
+ sidebar: "#fcfcfc",
1238
+ "sidebar-foreground": "#707070",
1239
+ "sidebar-primary": "#72e3ad",
1240
+ "sidebar-primary-foreground": "#1e2723",
1241
+ "sidebar-accent": "#ededed",
1242
+ "sidebar-accent-foreground": "#202020",
1243
+ "sidebar-border": "#dfdfdf",
1244
+ "sidebar-ring": "#72e3ad",
1245
+ },
1246
+ dark: {
1247
+ background: "#121212",
1248
+ foreground: "#e2e8f0",
1249
+ card: "#171717",
1250
+ "card-foreground": "#e2e8f0",
1251
+ popover: "#242424",
1252
+ "popover-foreground": "#a9a9a9",
1253
+ primary: "#006239",
1254
+ "primary-foreground": "#dde8e3",
1255
+ secondary: "#242424",
1256
+ "secondary-foreground": "#fafafa",
1257
+ muted: "#1f1f1f",
1258
+ "muted-foreground": "#a2a2a2",
1259
+ accent: "#313131",
1260
+ "accent-foreground": "#fafafa",
1261
+ destructive: "#541c15",
1262
+ "destructive-foreground": "#ede9e8",
1263
+ border: "#292929",
1264
+ input: "#242424",
1265
+ ring: "#4ade80",
1266
+ "chart-1": "#4ade80",
1267
+ "chart-2": "#60a5fa",
1268
+ "chart-3": "#a78bfa",
1269
+ "chart-4": "#fbbf24",
1270
+ "chart-5": "#2dd4bf",
1271
+ sidebar: "#121212",
1272
+ "sidebar-foreground": "#898989",
1273
+ "sidebar-primary": "#006239",
1274
+ "sidebar-primary-foreground": "#dde8e3",
1275
+ "sidebar-accent": "#313131",
1276
+ "sidebar-accent-foreground": "#fafafa",
1277
+ "sidebar-border": "#292929",
1278
+ "sidebar-ring": "#4ade80",
1279
+ },
1280
+ },
1281
+ radius: "0.5rem",
1282
+ font: {
1283
+ value: "outfit",
1284
+ fontFamily: '"Outfit", sans-serif',
1285
+ googleFontsUrl:
1286
+ "https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&display=swap",
1287
+ },
1288
+ },
1289
+ {
1290
+ name: "claude",
1291
+ label: "Claude",
1292
+ colors: {
1293
+ light: {
1294
+ background: "#faf9f5",
1295
+ foreground: "#3d3929",
1296
+ card: "#faf9f5",
1297
+ "card-foreground": "#141413",
1298
+ popover: "#ffffff",
1299
+ "popover-foreground": "#28261b",
1300
+ primary: "#c96442",
1301
+ "primary-foreground": "#ffffff",
1302
+ secondary: "#e9e6dc",
1303
+ "secondary-foreground": "#535146",
1304
+ muted: "#ede9de",
1305
+ "muted-foreground": "#83827d",
1306
+ accent: "#e9e6dc",
1307
+ "accent-foreground": "#28261b",
1308
+ destructive: "#141413",
1309
+ "destructive-foreground": "#ffffff",
1310
+ border: "#dad9d4",
1311
+ input: "#b4b2a7",
1312
+ ring: "#c96442",
1313
+ "chart-1": "#b05730",
1314
+ "chart-2": "#9c87f5",
1315
+ "chart-3": "#ded8c4",
1316
+ "chart-4": "#dbd3f0",
1317
+ "chart-5": "#b4552d",
1318
+ sidebar: "#f5f4ee",
1319
+ "sidebar-foreground": "#3d3d3a",
1320
+ "sidebar-primary": "#c96442",
1321
+ "sidebar-primary-foreground": "#fbfbfb",
1322
+ "sidebar-accent": "#e9e6dc",
1323
+ "sidebar-accent-foreground": "#343434",
1324
+ "sidebar-border": "#ebebeb",
1325
+ "sidebar-ring": "#b5b5b5",
1326
+ },
1327
+ dark: {
1328
+ background: "#262624",
1329
+ foreground: "#c3c0b6",
1330
+ card: "#262624",
1331
+ "card-foreground": "#faf9f5",
1332
+ popover: "#30302e",
1333
+ "popover-foreground": "#e5e5e2",
1334
+ primary: "#d97757",
1335
+ "primary-foreground": "#ffffff",
1336
+ secondary: "#faf9f5",
1337
+ "secondary-foreground": "#30302e",
1338
+ muted: "#1b1b19",
1339
+ "muted-foreground": "#b7b5a9",
1340
+ accent: "#1a1915",
1341
+ "accent-foreground": "#f5f4ee",
1342
+ destructive: "#ef4444",
1343
+ "destructive-foreground": "#ffffff",
1344
+ border: "#3e3e38",
1345
+ input: "#52514a",
1346
+ ring: "#d97757",
1347
+ "chart-1": "#b05730",
1348
+ "chart-2": "#9c87f5",
1349
+ "chart-3": "#1a1915",
1350
+ "chart-4": "#2f2b48",
1351
+ "chart-5": "#b4552d",
1352
+ sidebar: "#1f1e1d",
1353
+ "sidebar-foreground": "#c3c0b6",
1354
+ "sidebar-primary": "#343434",
1355
+ "sidebar-primary-foreground": "#fbfbfb",
1356
+ "sidebar-accent": "#0f0f0e",
1357
+ "sidebar-accent-foreground": "#c3c0b6",
1358
+ "sidebar-border": "#ebebeb",
1359
+ "sidebar-ring": "#b5b5b5",
1360
+ },
1361
+ },
1362
+ radius: "0.5rem",
1363
+ },
1364
+ ];
1365
+
1366
+ const themePresetByName = new Map(
1367
+ themePresets.map((preset) => [preset.name, preset])
1368
+ );
1369
+
1370
+ export function getThemePreset(name: string): ThemePreset | undefined {
1371
+ return themePresetByName.get(name);
1372
+ }
1373
+ `}function L(){return `"use client";
1374
+
1375
+ import { radiusPresets } from "./presets/radius";
1376
+
1377
+ type RadiusSelectorProps = {
1378
+ value: string | null;
1379
+ onChange: (radius: string) => void;
1380
+ };
1381
+
1382
+ const baseButtonClass =
1383
+ "inline-flex flex-col items-center justify-center gap-1.5 w-full min-h-[52px] px-2.5 py-1.5 rounded-[10px] border text-xs font-medium tracking-[0.01em] cursor-pointer transition-all duration-[160ms]";
1384
+
1385
+ const defaultButtonClass =
1386
+ "border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)] text-[oklch(0.96_0_0)] hover:bg-[oklch(0.24_0.02_260/0.95)] hover:border-[oklch(1_0_0/0.18)]";
1387
+
1388
+ const selectedButtonClass =
1389
+ "bg-[oklch(0.24_0.02_260/0.95)] border-[oklch(0.72_0.15_265)] shadow-[0_0_0_1px_oklch(0.72_0.15_265),0_10px_24px_oklch(0_0_0/0.35)]";
1390
+
1391
+ export function RadiusSelector({ value, onChange }: RadiusSelectorProps) {
1392
+ return (
1393
+ <div
1394
+ className="relative grid gap-2.5 p-3 rounded-xl border border-[oklch(1_0_0/0.08)] bg-[oklch(0.2_0.02_260/0.9)]"
1395
+ style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
1396
+ >
1397
+ <label className="block text-[10.5px] font-semibold tracking-[0.16em] uppercase text-[oklch(0.72_0_0)]">
1398
+ Radius
1399
+ </label>
1400
+ <div className="grid grid-cols-3 gap-2">
1401
+ {radiusPresets.map((preset) => {
1402
+ const isSelected = value === preset.value;
1403
+
1404
+ return (
1405
+ <button
1406
+ key={preset.name}
1407
+ onClick={() => onChange(preset.value)}
1408
+ className={\`\${baseButtonClass} \${isSelected ? selectedButtonClass : defaultButtonClass}\`}
1409
+ style={isSelected ? undefined : { boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
1410
+ title={preset.label}
1411
+ >
1412
+ <span
1413
+ className="w-7 h-[18px] border border-[oklch(1_0_0/0.12)]"
1414
+ style={{
1415
+ borderRadius: preset.value,
1416
+ background:
1417
+ "linear-gradient(180deg, oklch(0.8 0 0 / 0.45), oklch(0.6 0 0 / 0.2))",
1418
+ }}
1419
+ />
1420
+ <span className="text-[11px] text-[oklch(0.72_0_0)]">
1421
+ {preset.label}
1422
+ </span>
1423
+ </button>
1424
+ );
1425
+ })}
1426
+ </div>
1427
+ </div>
1428
+ );
1429
+ }
1430
+ `}function B(){return `// Direct DOM theme application (no postMessage needed)
1431
+
1432
+ import { getColorPreset } from "./presets/colors";
1433
+ import { getFontPreset } from "./presets/fonts";
1434
+ import {
1435
+ getThemePreset,
1436
+ type ThemePreset,
1437
+ type ThemePresetFont,
1438
+ } from "./presets/theme-presets";
1439
+
1440
+ const THEME_COLOR_STYLE_ID = "previewcn-devtools-theme-colors";
1441
+ const THEME_FONT_STYLE_ID = "previewcn-devtools-theme-font";
1442
+
1443
+ export type ThemeConfig = {
1444
+ colorPreset: string | null;
1445
+ radius: string | null;
1446
+ darkMode: boolean | null;
1447
+ font: string | null;
1448
+ preset: string | null;
1449
+ };
1450
+
1451
+ type ThemeColors = {
1452
+ light: Record<string, string>;
1453
+ dark: Record<string, string>;
1454
+ };
1455
+
1456
+ function getOrCreateThemeColorStyleElement(): HTMLStyleElement {
1457
+ const existing = document.getElementById(THEME_COLOR_STYLE_ID);
1458
+ if (existing instanceof HTMLStyleElement) {
1459
+ return existing;
1460
+ }
1461
+
1462
+ if (existing) {
1463
+ existing.remove();
1464
+ }
1465
+
1466
+ const styleEl = document.createElement("style");
1467
+ styleEl.id = THEME_COLOR_STYLE_ID;
1468
+ document.head.appendChild(styleEl);
1469
+ return styleEl;
1470
+ }
1471
+
1472
+ function serializeCssVars(cssVars: Record<string, string>): string {
1473
+ return Object.entries(cssVars)
1474
+ .map(([key, value]) => \`--\${key}: \${value};\`)
1475
+ .join(" ");
1476
+ }
1477
+
1478
+ function applyThemeColors(colors: ThemeColors) {
1479
+ const styleEl = getOrCreateThemeColorStyleElement();
1480
+
1481
+ const lightCss = serializeCssVars(colors.light);
1482
+ const darkCss = serializeCssVars(colors.dark);
1483
+
1484
+ styleEl.textContent = \`:root { \${lightCss} } .dark { \${darkCss} }\`;
1485
+ }
1486
+
1487
+ // Apply dark mode class to document
1488
+ export function applyDarkMode(darkMode: boolean) {
1489
+ const root = document.documentElement;
1490
+
1491
+ if (darkMode) {
1492
+ root.classList.remove("light");
1493
+ root.classList.add("dark");
1494
+ } else {
1495
+ root.classList.remove("dark");
1496
+ root.classList.add("light");
1497
+ }
1498
+
1499
+ root.style.colorScheme = darkMode ? "dark" : "light";
1500
+ }
1501
+
1502
+ // Apply radius to document
1503
+ export function applyRadius(radius: string) {
1504
+ const root = document.documentElement;
1505
+ root.style.setProperty("--radius", radius);
1506
+ }
1507
+
1508
+ // Apply color preset to document
1509
+ export function applyColors(colorPresetName: string) {
1510
+ const preset = getColorPreset(colorPresetName);
1511
+ if (!preset) return;
1512
+
1513
+ applyThemeColors(preset.colors);
1514
+ }
1515
+
1516
+ // Apply theme preset colors directly (bypasses colorPreset lookup)
1517
+ export function applyPresetColors(preset: ThemePreset) {
1518
+ applyThemeColors(preset.colors);
1519
+ }
1520
+
1521
+ function getOrCreateFontStyleElement(): HTMLStyleElement {
1522
+ const existing = document.getElementById(THEME_FONT_STYLE_ID);
1523
+ if (existing instanceof HTMLStyleElement) {
1524
+ return existing;
1525
+ }
1526
+
1527
+ if (existing) {
1528
+ existing.remove();
1529
+ }
1530
+
1531
+ const styleEl = document.createElement("style");
1532
+ styleEl.id = THEME_FONT_STYLE_ID;
1533
+ document.head.appendChild(styleEl);
1534
+ return styleEl;
1535
+ }
1536
+
1537
+ function applyFontConfig(font: ThemePresetFont) {
1538
+ const { fontFamily, googleFontsUrl, value: fontId } = font;
1539
+
1540
+ // Validate Google Fonts URL to prevent XSS attacks
1541
+ if (!googleFontsUrl.startsWith("https://fonts.googleapis.com/")) {
1542
+ console.warn("[PreviewCN] Invalid font URL");
1543
+ return;
1544
+ }
1545
+
1546
+ // Inject Google Fonts link if not already present
1547
+ const linkId = \`previewcn-font-\${fontId}\`;
1548
+ if (!document.getElementById(linkId)) {
1549
+ const link = document.createElement("link");
1550
+ link.id = linkId;
1551
+ link.rel = "stylesheet";
1552
+ link.href = googleFontsUrl;
1553
+ document.head.appendChild(link);
1554
+ }
1555
+
1556
+ // Use multiple strategies to ensure font override works universally
1557
+ // This covers various Tailwind v4 and next/font configurations
1558
+ const styleEl = getOrCreateFontStyleElement();
1559
+ styleEl.textContent = \`
1560
+ :root {
1561
+ --font-sans: \${fontFamily} !important;
1562
+ --font-sans-override: \${fontFamily} !important;
1563
+ --font-geist-sans: \${fontFamily} !important;
1564
+ }
1565
+ html, body, .font-sans {
1566
+ font-family: \${fontFamily} !important;
1567
+ }
1568
+ \`;
1569
+ }
1570
+
1571
+ // Apply font to document
1572
+ export function applyFont(fontId: string) {
1573
+ const fontPreset = getFontPreset(fontId);
1574
+ if (!fontPreset) return;
1575
+
1576
+ applyFontConfig(fontPreset);
1577
+ }
1578
+
1579
+ // Apply font from theme preset (using preset's font config directly)
1580
+ export function applyPresetFont(font: ThemePresetFont) {
1581
+ applyFontConfig(font);
1582
+ }
1583
+
1584
+ // Apply a complete theme preset (colors, radius, and optionally font)
1585
+ export function applyPreset(presetName: string) {
1586
+ const preset = getThemePreset(presetName);
1587
+ if (!preset) return;
1588
+
1589
+ applyPresetColors(preset);
1590
+ applyRadius(preset.radius);
1591
+
1592
+ if (preset.font) {
1593
+ applyFontConfig(preset.font);
1594
+ }
1595
+ }
1596
+
1597
+ // Apply full theme config
1598
+ export function applyTheme(config: ThemeConfig) {
1599
+ const preset = config.preset ? getThemePreset(config.preset) : null;
1600
+
1601
+ if (config.colorPreset !== null) {
1602
+ applyColors(config.colorPreset);
1603
+ } else if (preset) {
1604
+ applyPresetColors(preset);
1605
+ }
1606
+
1607
+ const radius = config.radius ?? preset?.radius;
1608
+ if (radius !== null && radius !== undefined) {
1609
+ applyRadius(radius);
1610
+ }
1611
+
1612
+ if (config.darkMode !== null) {
1613
+ applyDarkMode(config.darkMode);
1614
+ }
1615
+
1616
+ if (config.font !== null) {
1617
+ applyFont(config.font);
1618
+ } else if (preset?.font) {
1619
+ applyFontConfig(preset.font);
1620
+ }
1621
+ }
1622
+
1623
+ export function clearTheme() {
1624
+ const colorStyleEl = document.getElementById(THEME_COLOR_STYLE_ID);
1625
+ if (colorStyleEl) colorStyleEl.remove();
1626
+
1627
+ const fontStyleEl = document.getElementById(THEME_FONT_STYLE_ID);
1628
+ if (fontStyleEl) fontStyleEl.remove();
1629
+
1630
+ const root = document.documentElement;
1631
+ root.style.removeProperty("--radius");
1632
+ root.style.removeProperty("color-scheme");
1633
+ root.classList.remove("light", "dark");
1634
+ }
1635
+ `}function $(){return `"use client";
1636
+
1637
+ type TriggerProps = {
1638
+ onClick: () => void;
1639
+ };
1640
+
1641
+ // SVG icon for the trigger button (palette icon)
1642
+ function PaletteIcon() {
1643
+ return (
1644
+ <svg
1645
+ xmlns="http://www.w3.org/2000/svg"
1646
+ width="20"
1647
+ height="20"
1648
+ viewBox="0 0 24 24"
1649
+ fill="none"
1650
+ stroke="currentColor"
1651
+ strokeWidth="2"
1652
+ strokeLinecap="round"
1653
+ strokeLinejoin="round"
1654
+ >
1655
+ <circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
1656
+ <circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
1657
+ <circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
1658
+ <circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
1659
+ <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.555C21.965 6.012 17.461 2 12 2z" />
1660
+ </svg>
1661
+ );
1662
+ }
1663
+
1664
+ export function Trigger({ onClick }: TriggerProps) {
1665
+ return (
1666
+ <button
1667
+ onClick={onClick}
1668
+ className="fixed bottom-4 right-4 z-[99999] inline-flex items-center justify-center size-12 rounded-full border border-[oklch(1_0_0/0.18)] text-[oklch(0.96_0_0)] transition-all duration-[180ms] hover:border-[oklch(1_0_0/0.28)] focus-visible:outline-2 focus-visible:outline-[oklch(0.72_0.15_265)] focus-visible:outline-offset-[3px]"
1669
+ style={{
1670
+ background:
1671
+ "linear-gradient(180deg, oklch(0.23 0.03 260) 0%, oklch(0.16 0.02 260) 100%)",
1672
+ boxShadow:
1673
+ "0 16px 32px oklch(0 0 0 / 0.45), 0 0 0 1px oklch(1 0 0 / 0.04) inset",
1674
+ }}
1675
+ aria-label="Open PreviewCN theme editor"
1676
+ title="PreviewCN Theme Editor"
1677
+ >
1678
+ <PaletteIcon />
1679
+ </button>
1680
+ );
1681
+ }
1682
+ `}function U(){return `"use client";
1683
+
1684
+ import { useCallback, useState } from "react";
1685
+
1686
+ import { getThemePreset } from "./presets/theme-presets";
1687
+ import type { ThemeConfig } from "./theme-applier";
1688
+ import {
1689
+ applyColors,
1690
+ applyDarkMode,
1691
+ applyFont,
1692
+ applyPreset,
1693
+ applyRadius,
1694
+ clearTheme,
1695
+ } from "./theme-applier";
1696
+
1697
+ // LocalStorage key for persisting theme state
1698
+ const STORAGE_KEY = "previewcn-devtools-theme";
1699
+
1700
+ function loadFromStorage(): Partial<ThemeConfig> {
1701
+ if (typeof window === "undefined") return {};
1702
+
1703
+ try {
1704
+ const stored = localStorage.getItem(STORAGE_KEY);
1705
+ if (stored) {
1706
+ return JSON.parse(stored);
1707
+ }
1708
+ } catch {
1709
+ // Ignore parse errors
1710
+ }
1711
+ return {};
1712
+ }
1713
+
1714
+ function saveToStorage(config: ThemeConfig) {
1715
+ if (typeof window === "undefined") return;
1716
+
1717
+ try {
1718
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
1719
+ } catch {
1720
+ // Ignore storage errors
1721
+ }
1722
+ }
1723
+
1724
+ const defaultConfig: ThemeConfig = {
1725
+ colorPreset: null,
1726
+ radius: null,
1727
+ darkMode: null,
1728
+ font: null,
1729
+ preset: null,
1730
+ };
1731
+
1732
+ export function useThemeState() {
1733
+ const [config, setConfigState] = useState<ThemeConfig>(() => ({
1734
+ ...defaultConfig,
1735
+ ...loadFromStorage(),
1736
+ }));
1737
+
1738
+ const updateConfig = useCallback(
1739
+ (updater: (prev: ThemeConfig) => ThemeConfig) => {
1740
+ setConfigState((prev) => {
1741
+ const next = updater(prev);
1742
+ saveToStorage(next);
1743
+ return next;
1744
+ });
1745
+ },
1746
+ []
1747
+ );
1748
+
1749
+ const setColorPreset = useCallback(
1750
+ (colorPreset: string) => {
1751
+ updateConfig((prev) => ({ ...prev, colorPreset }));
1752
+ applyColors(colorPreset);
1753
+ },
1754
+ [updateConfig]
1755
+ );
1756
+
1757
+ const setRadius = useCallback(
1758
+ (radius: string) => {
1759
+ updateConfig((prev) => ({ ...prev, radius }));
1760
+ applyRadius(radius);
1761
+ },
1762
+ [updateConfig]
1763
+ );
1764
+
1765
+ const setDarkMode = useCallback(
1766
+ (darkMode: boolean) => {
1767
+ updateConfig((prev) => ({ ...prev, darkMode }));
1768
+ applyDarkMode(darkMode);
1769
+ },
1770
+ [updateConfig]
1771
+ );
1772
+
1773
+ const setFont = useCallback(
1774
+ (font: string) => {
1775
+ updateConfig((prev) => ({ ...prev, font }));
1776
+ applyFont(font);
1777
+ },
1778
+ [updateConfig]
1779
+ );
1780
+
1781
+ const setPresetTheme = useCallback(
1782
+ (preset: string) => {
1783
+ const themePreset = getThemePreset(preset);
1784
+ if (!themePreset) return;
1785
+
1786
+ updateConfig((prev) => ({
1787
+ ...prev,
1788
+ preset,
1789
+ // Update individual settings to match preset
1790
+ radius: themePreset.radius,
1791
+ font: themePreset.font?.value ?? prev.font,
1792
+ // Clear colorPreset since we're using preset colors
1793
+ colorPreset: null,
1794
+ }));
1795
+ applyPreset(preset);
1796
+ },
1797
+ [updateConfig]
1798
+ );
1799
+
1800
+ const resetTheme = useCallback(() => {
1801
+ // Remove stored config
1802
+ if (typeof window !== "undefined") {
1803
+ localStorage.removeItem(STORAGE_KEY);
1804
+ }
1805
+ clearTheme();
1806
+
1807
+ setConfigState(defaultConfig);
1808
+ }, []);
1809
+
1810
+ return {
1811
+ config,
1812
+ setColorPreset,
1813
+ setRadius,
1814
+ setDarkMode,
1815
+ setFont,
1816
+ setPresetTheme,
1817
+ resetTheme,
1818
+ };
1819
+ }
1820
+ `}function pe(){return `export { PreviewcnDevtools } from "./devtools";
1821
+ `}function X(){return [{path:"index.ts",content:pe()},{path:"devtools.tsx",content:S()},{path:"trigger.tsx",content:$()},{path:"panel.tsx",content:N()},{path:"color-picker.tsx",content:P()},{path:"preset-selector.tsx",content:I()},{path:"radius-selector.tsx",content:L()},{path:"font-selector.tsx",content:F()},{path:"mode-toggle.tsx",content:E()},{path:"css-export-button.tsx",content:w()},{path:"theme-applier.ts",content:B()},{path:"use-theme-state.ts",content:U()},{path:"css-export.ts",content:T()},{path:"presets/index.ts",content:M()},{path:"presets/colors.ts",content:D()},{path:"presets/color-preset-specs.ts",content:R()},{path:"presets/radius.ts",content:O()},{path:"presets/fonts.ts",content:j()},{path:"presets/theme-presets.ts",content:A()}]}function u(e){return p.cyan(e)}async function q(e,t){return !!(await ue({type:"confirm",name:"value",message:e,initial:t})).value}async function Z(e,t){await x.mkdir(e,{recursive:true});for(let o of t){let r=l.join(e,o.path);await x.mkdir(l.dirname(r),{recursive:true}),await x.writeFile(r,o.content,"utf-8");}}async function Q(e){s.info(`Initializing PreviewCN...
1822
+ `);let t=process.cwd(),o=V("Detecting project type...").start(),r=await g(t);r.isNextJs||(o.fail("Not a Next.js project"),s.error("PreviewCN requires a Next.js project with App Router."),s.hint("Make sure you're in the root of a Next.js project."),process.exit(1)),r.isAppRouter||(o.fail("App Router not detected"),s.error("PreviewCN requires Next.js App Router (app/ directory)."),s.hint("Create an app/ directory or migrate from pages/ to app/."),process.exit(1)),o.succeed("Next.js App Router project detected");let a=await h(t);a||(s.error("Could not find app/layout.tsx"),process.exit(1)),s.info(`Found layout at: ${u(l__default.relative(t,a))}`);let n=await d(t),c=await H(t);s.info(`Components will be generated at: ${u(l__default.relative(t,n))}`),e.yes||await q("This will generate PreviewCN components and modify your app layout. Continue?",true)||(s.info("Aborted."),process.exit(0)),await me(n,a,c),console.log(),s.success("PreviewCN devtools initialized successfully!"),console.log(),s.info("Next step:"),console.log(` Run ${u("pnpm dev")} (or your dev command) to start the dev server.`),console.log(` Click the ${u("theme palette icon")} in the bottom-right corner to open the editor.`),console.log();}async function me(e,t,o){let r=V("Generating PreviewCN components...").start();try{let n=X();await Z(e,n),r.succeed(`Generated ${n.length} files in ${l__default.basename(l__default.dirname(e))}/${l__default.basename(e)}`);}catch(n){r.fail("Failed to generate PreviewCN components"),s.error(n instanceof Error?n.message:String(n)),process.exit(1);}let a=V("Adding PreviewcnDevtools to layout...").start();try{await J(t,o),a.succeed("Added PreviewcnDevtools to layout");}catch(n){a.fail("Failed to modify layout for devtools"),s.error(n instanceof Error?n.message:String(n)),s.hint("You may need to add PreviewcnDevtools manually. See documentation.");}}var b=new Command;b.name("previewcn").description("CLI for PreviewCN - real-time shadcn/ui theme editor").version("0.1.0");b.command("init",{isDefault:true}).description("Initialize PreviewCN devtools in your Next.js project").option("-y, --yes","Skip confirmation prompts").action(Q);b.command("doctor").description("Check PreviewCN setup and diagnose issues").action(K);b.parse();