previewcn 0.1.0 → 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.
- package/dist/index.js +1818 -6
- 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
|
|
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
|
-
`)}`,
|
|
4
|
+
`)}`,r=[...e.matchAll(oe)];if(r.length===0)return `${t.join(`
|
|
5
5
|
`)}
|
|
6
6
|
|
|
7
|
-
${e}`;let r
|
|
8
|
-
`+t+e.slice(
|
|
9
|
-
`);let e=[],
|
|
10
|
-
|
|
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();
|