previewcn 0.1.0 → 0.1.2

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