previewcn 0.1.1 → 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.
- package/dist/index.js +268 -161
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {Command}from'commander';import*as
|
|
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
|
-
`)}`,r=[...e.matchAll(
|
|
6
|
+
`)}`,r=[...e.matchAll(re)];if(r.length===0)return `${t.join(`
|
|
5
7
|
`)}
|
|
6
8
|
|
|
7
|
-
${e}`;let
|
|
8
|
-
`+t+e.slice(r)}async function
|
|
9
|
-
`);let e=process.cwd(),t=[],o=await
|
|
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";
|
|
10
12
|
|
|
11
13
|
import { colorPresets } from "./presets/colors";
|
|
12
14
|
|
|
@@ -53,9 +55,9 @@ export function ColorPicker({ value, onChange }: ColorPickerProps) {
|
|
|
53
55
|
</div>
|
|
54
56
|
);
|
|
55
57
|
}
|
|
56
|
-
`}function
|
|
58
|
+
`}function T(){return `"use client";
|
|
57
59
|
|
|
58
|
-
import { useState } from "react";
|
|
60
|
+
import { useEffect, useRef, useState } from "react";
|
|
59
61
|
|
|
60
62
|
import type { ThemeConfig } from "./theme-applier";
|
|
61
63
|
import { copyToClipboard, generateExportCss } from "./css-export";
|
|
@@ -103,6 +105,15 @@ function CheckIcon() {
|
|
|
103
105
|
|
|
104
106
|
export function CssExportButton({ config }: CssExportButtonProps) {
|
|
105
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
|
+
}, []);
|
|
106
117
|
|
|
107
118
|
const exportCss = generateExportCss(config);
|
|
108
119
|
const isDisabled = !exportCss;
|
|
@@ -115,7 +126,10 @@ export function CssExportButton({ config }: CssExportButtonProps) {
|
|
|
115
126
|
|
|
116
127
|
if (success) {
|
|
117
128
|
setCopied(true);
|
|
118
|
-
|
|
129
|
+
if (resetTimerRef.current) {
|
|
130
|
+
clearTimeout(resetTimerRef.current);
|
|
131
|
+
}
|
|
132
|
+
resetTimerRef.current = setTimeout(() => setCopied(false), 2000);
|
|
119
133
|
}
|
|
120
134
|
};
|
|
121
135
|
|
|
@@ -142,7 +156,7 @@ export function CssExportButton({ config }: CssExportButtonProps) {
|
|
|
142
156
|
</button>
|
|
143
157
|
);
|
|
144
158
|
}
|
|
145
|
-
`}function
|
|
159
|
+
`}function S(){return `// CSS Export utilities for generating shadcn/ui compatible CSS
|
|
146
160
|
|
|
147
161
|
import { getColorPreset } from "./presets/colors";
|
|
148
162
|
import { getThemePreset } from "./presets/theme-presets";
|
|
@@ -234,7 +248,7 @@ export async function copyToClipboard(text: string): Promise<boolean> {
|
|
|
234
248
|
}
|
|
235
249
|
}
|
|
236
250
|
}
|
|
237
|
-
`}function
|
|
251
|
+
`}function F(){return `"use client";
|
|
238
252
|
|
|
239
253
|
import { lazy, Suspense, useState } from "react";
|
|
240
254
|
|
|
@@ -269,7 +283,7 @@ export function PreviewcnDevtools() {
|
|
|
269
283
|
|
|
270
284
|
return <DevtoolsInner />;
|
|
271
285
|
}
|
|
272
|
-
`}function
|
|
286
|
+
`}function R(){return `"use client";
|
|
273
287
|
|
|
274
288
|
import { useState } from "react";
|
|
275
289
|
|
|
@@ -280,6 +294,11 @@ type FontSelectorProps = {
|
|
|
280
294
|
onChange: (font: string) => void;
|
|
281
295
|
};
|
|
282
296
|
|
|
297
|
+
type FontMenuProps = {
|
|
298
|
+
value: string | null;
|
|
299
|
+
onSelect: (fontValue: string) => void;
|
|
300
|
+
};
|
|
301
|
+
|
|
283
302
|
function ChevronDownIcon() {
|
|
284
303
|
return (
|
|
285
304
|
<svg
|
|
@@ -298,12 +317,50 @@ function ChevronDownIcon() {
|
|
|
298
317
|
);
|
|
299
318
|
}
|
|
300
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
|
+
|
|
301
349
|
export function FontSelector({ value, onChange }: FontSelectorProps) {
|
|
302
350
|
const [isOpen, setIsOpen] = useState(false);
|
|
303
351
|
|
|
304
352
|
const selectedFont = fontPresets.find((f) => f.value === value);
|
|
305
353
|
const displayLabel = selectedFont?.label ?? "Select font...";
|
|
306
354
|
|
|
355
|
+
const handleToggle = () => {
|
|
356
|
+
setIsOpen((open) => !open);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const handleSelect = (fontValue: string) => {
|
|
360
|
+
onChange(fontValue);
|
|
361
|
+
setIsOpen(false);
|
|
362
|
+
};
|
|
363
|
+
|
|
307
364
|
return (
|
|
308
365
|
<div
|
|
309
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"}\`}
|
|
@@ -314,7 +371,7 @@ export function FontSelector({ value, onChange }: FontSelectorProps) {
|
|
|
314
371
|
</label>
|
|
315
372
|
<div className="relative z-[1]">
|
|
316
373
|
<button
|
|
317
|
-
onClick={
|
|
374
|
+
onClick={handleToggle}
|
|
318
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"
|
|
319
376
|
style={{ boxShadow: "inset 0 1px 0 oklch(1 0 0 / 0.04)" }}
|
|
320
377
|
aria-expanded={isOpen}
|
|
@@ -324,39 +381,13 @@ export function FontSelector({ value, onChange }: FontSelectorProps) {
|
|
|
324
381
|
</button>
|
|
325
382
|
|
|
326
383
|
{isOpen && (
|
|
327
|
-
<
|
|
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>
|
|
384
|
+
<FontMenu value={value} onSelect={handleSelect} />
|
|
354
385
|
)}
|
|
355
386
|
</div>
|
|
356
387
|
</div>
|
|
357
388
|
);
|
|
358
389
|
}
|
|
359
|
-
`}function
|
|
390
|
+
`}function N(){return `"use client";
|
|
360
391
|
|
|
361
392
|
type ModeToggleProps = {
|
|
362
393
|
value: boolean | null;
|
|
@@ -450,9 +481,9 @@ export function ModeToggle({ value, onChange }: ModeToggleProps) {
|
|
|
450
481
|
</div>
|
|
451
482
|
);
|
|
452
483
|
}
|
|
453
|
-
`}function
|
|
484
|
+
`}function E(){return `"use client";
|
|
454
485
|
|
|
455
|
-
import { useEffect, useRef } from "react";
|
|
486
|
+
import { useEffect, useRef, type ReactNode } from "react";
|
|
456
487
|
|
|
457
488
|
import { useThemeState } from "./use-theme-state";
|
|
458
489
|
import { applyTheme } from "./theme-applier";
|
|
@@ -467,6 +498,32 @@ type PanelProps = {
|
|
|
467
498
|
onClose: () => void;
|
|
468
499
|
};
|
|
469
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
|
+
|
|
470
527
|
function CloseIcon() {
|
|
471
528
|
return (
|
|
472
529
|
<svg
|
|
@@ -505,7 +562,6 @@ function RotateCcwIcon() {
|
|
|
505
562
|
);
|
|
506
563
|
}
|
|
507
564
|
|
|
508
|
-
// Inject keyframes for animations
|
|
509
565
|
const keyframesStyle = \`
|
|
510
566
|
@keyframes previewcn-slide-in-right {
|
|
511
567
|
from { transform: translateX(100%); }
|
|
@@ -521,26 +577,43 @@ const keyframesStyle = \`
|
|
|
521
577
|
}
|
|
522
578
|
\`;
|
|
523
579
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
+
};
|
|
534
594
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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)]";
|
|
542
611
|
|
|
543
|
-
|
|
612
|
+
const footerStyle = {
|
|
613
|
+
background: "linear-gradient(0deg, oklch(0.18 0.02 260), transparent)",
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
function usePanelKeyframes() {
|
|
544
617
|
useEffect(() => {
|
|
545
618
|
const styleId = "previewcn-keyframes";
|
|
546
619
|
if (!document.getElementById(styleId)) {
|
|
@@ -550,84 +623,134 @@ export default function Panel({ onClose }: PanelProps) {
|
|
|
550
623
|
document.head.appendChild(style);
|
|
551
624
|
}
|
|
552
625
|
}, []);
|
|
626
|
+
}
|
|
553
627
|
|
|
628
|
+
function PanelSection({ delay, children }: PanelSectionProps) {
|
|
554
629
|
return (
|
|
555
630
|
<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
631
|
style={{
|
|
558
|
-
|
|
559
|
-
|
|
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",
|
|
632
|
+
animation: "previewcn-rise 0.32s ease both",
|
|
633
|
+
animationDelay: \`\${delay}s\`,
|
|
567
634
|
}}
|
|
568
635
|
>
|
|
569
|
-
{
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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>
|
|
636
|
+
{children}
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
589
640
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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>
|
|
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>
|
|
613
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
|
+
}
|
|
614
706
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
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)]"
|
|
621
714
|
>
|
|
622
|
-
<
|
|
623
|
-
<
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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} />
|
|
631
754
|
</div>
|
|
632
755
|
);
|
|
633
756
|
}
|
|
@@ -689,7 +812,7 @@ export function PresetSelector({ value, onChange }: PresetSelectorProps) {
|
|
|
689
812
|
</div>
|
|
690
813
|
);
|
|
691
814
|
}
|
|
692
|
-
`}function
|
|
815
|
+
`}function D(){return `// Color preset data used by the devtools color picker.
|
|
693
816
|
// Kept in a separate file to keep logic files small.
|
|
694
817
|
|
|
695
818
|
export type PresetSpec = {
|
|
@@ -901,7 +1024,7 @@ export const colorPresetSpecs: PresetSpec[] = [
|
|
|
901
1024
|
},
|
|
902
1025
|
},
|
|
903
1026
|
];
|
|
904
|
-
`}function
|
|
1027
|
+
`}function j(){return `// Color presets compatible with shadcn/ui
|
|
905
1028
|
// Uses OKLCH color space for better perceptual uniformity
|
|
906
1029
|
|
|
907
1030
|
import { colorPresetSpecs, type PresetSpec } from "./color-preset-specs";
|
|
@@ -992,7 +1115,7 @@ export const colorPresets: ColorPreset[] =
|
|
|
992
1115
|
export function getColorPreset(name: string): ColorPreset | undefined {
|
|
993
1116
|
return colorPresets.find((p) => p.name === name);
|
|
994
1117
|
}
|
|
995
|
-
`}function
|
|
1118
|
+
`}function M(){return `// Font presets using Google Fonts
|
|
996
1119
|
|
|
997
1120
|
export type FontPreset = {
|
|
998
1121
|
label: string;
|
|
@@ -1077,12 +1200,12 @@ export const fontPresets: FontPreset[] = [
|
|
|
1077
1200
|
export function getFontPreset(value: string): FontPreset | undefined {
|
|
1078
1201
|
return fontPresets.find((f) => f.value === value);
|
|
1079
1202
|
}
|
|
1080
|
-
`}function
|
|
1203
|
+
`}function O(){return `export * from "./colors";
|
|
1081
1204
|
export * from "./color-preset-specs";
|
|
1082
1205
|
export * from "./fonts";
|
|
1083
1206
|
export * from "./radius";
|
|
1084
1207
|
export * from "./theme-presets";
|
|
1085
|
-
`}function
|
|
1208
|
+
`}function L(){return `// Radius presets for border-radius customization
|
|
1086
1209
|
|
|
1087
1210
|
export type RadiusPreset = {
|
|
1088
1211
|
name: string;
|
|
@@ -1370,7 +1493,7 @@ const themePresetByName = new Map(
|
|
|
1370
1493
|
export function getThemePreset(name: string): ThemePreset | undefined {
|
|
1371
1494
|
return themePresetByName.get(name);
|
|
1372
1495
|
}
|
|
1373
|
-
`}function
|
|
1496
|
+
`}function $(){return `"use client";
|
|
1374
1497
|
|
|
1375
1498
|
import { radiusPresets } from "./presets/radius";
|
|
1376
1499
|
|
|
@@ -1453,8 +1576,8 @@ type ThemeColors = {
|
|
|
1453
1576
|
dark: Record<string, string>;
|
|
1454
1577
|
};
|
|
1455
1578
|
|
|
1456
|
-
function
|
|
1457
|
-
const existing = document.getElementById(
|
|
1579
|
+
function getOrCreateStyleElement(id: string): HTMLStyleElement {
|
|
1580
|
+
const existing = document.getElementById(id);
|
|
1458
1581
|
if (existing instanceof HTMLStyleElement) {
|
|
1459
1582
|
return existing;
|
|
1460
1583
|
}
|
|
@@ -1464,7 +1587,7 @@ function getOrCreateThemeColorStyleElement(): HTMLStyleElement {
|
|
|
1464
1587
|
}
|
|
1465
1588
|
|
|
1466
1589
|
const styleEl = document.createElement("style");
|
|
1467
|
-
styleEl.id =
|
|
1590
|
+
styleEl.id = id;
|
|
1468
1591
|
document.head.appendChild(styleEl);
|
|
1469
1592
|
return styleEl;
|
|
1470
1593
|
}
|
|
@@ -1476,7 +1599,7 @@ function serializeCssVars(cssVars: Record<string, string>): string {
|
|
|
1476
1599
|
}
|
|
1477
1600
|
|
|
1478
1601
|
function applyThemeColors(colors: ThemeColors) {
|
|
1479
|
-
const styleEl =
|
|
1602
|
+
const styleEl = getOrCreateStyleElement(THEME_COLOR_STYLE_ID);
|
|
1480
1603
|
|
|
1481
1604
|
const lightCss = serializeCssVars(colors.light);
|
|
1482
1605
|
const darkCss = serializeCssVars(colors.dark);
|
|
@@ -1518,22 +1641,6 @@ export function applyPresetColors(preset: ThemePreset) {
|
|
|
1518
1641
|
applyThemeColors(preset.colors);
|
|
1519
1642
|
}
|
|
1520
1643
|
|
|
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
1644
|
function applyFontConfig(font: ThemePresetFont) {
|
|
1538
1645
|
const { fontFamily, googleFontsUrl, value: fontId } = font;
|
|
1539
1646
|
|
|
@@ -1555,7 +1662,7 @@ function applyFontConfig(font: ThemePresetFont) {
|
|
|
1555
1662
|
|
|
1556
1663
|
// Use multiple strategies to ensure font override works universally
|
|
1557
1664
|
// This covers various Tailwind v4 and next/font configurations
|
|
1558
|
-
const styleEl =
|
|
1665
|
+
const styleEl = getOrCreateStyleElement(THEME_FONT_STYLE_ID);
|
|
1559
1666
|
styleEl.textContent = \`
|
|
1560
1667
|
:root {
|
|
1561
1668
|
--font-sans: \${fontFamily} !important;
|
|
@@ -1632,7 +1739,7 @@ export function clearTheme() {
|
|
|
1632
1739
|
root.style.removeProperty("color-scheme");
|
|
1633
1740
|
root.classList.remove("light", "dark");
|
|
1634
1741
|
}
|
|
1635
|
-
`}function
|
|
1742
|
+
`}function U(){return `"use client";
|
|
1636
1743
|
|
|
1637
1744
|
type TriggerProps = {
|
|
1638
1745
|
onClick: () => void;
|
|
@@ -1679,7 +1786,7 @@ export function Trigger({ onClick }: TriggerProps) {
|
|
|
1679
1786
|
</button>
|
|
1680
1787
|
);
|
|
1681
1788
|
}
|
|
1682
|
-
`}function
|
|
1789
|
+
`}function V(){return `"use client";
|
|
1683
1790
|
|
|
1684
1791
|
import { useCallback, useState } from "react";
|
|
1685
1792
|
|
|
@@ -1817,6 +1924,6 @@ export function useThemeState() {
|
|
|
1817
1924
|
resetTheme,
|
|
1818
1925
|
};
|
|
1819
1926
|
}
|
|
1820
|
-
`}function
|
|
1821
|
-
`}function
|
|
1822
|
-
`);let t=process.cwd(),o=
|
|
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();
|