previewcn 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +279 -161
  2. 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 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=`
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(oe)];if(r.length===0)return `${t.join(`
6
+ `)}`,r=[...e.matchAll(re)];if(r.length===0)return `${t.join(`
5
7
  `)}
6
8
 
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";
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 w(){return `"use client";
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
- setTimeout(() => setCopied(false), 2000);
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 T(){return `// CSS Export utilities for generating shadcn/ui compatible CSS
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 S(){return `"use client";
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 F(){return `"use client";
286
+ `}function N(){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={() => setIsOpen(!isOpen)}
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
- <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>
384
+ <FontMenu value={value} onSelect={handleSelect} />
354
385
  )}
355
386
  </div>
356
387
  </div>
357
388
  );
358
389
  }
359
- `}function E(){return `"use client";
390
+ `}function R(){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 N(){return `"use client";
484
+ `}function E(){return `"use client";
454
485
 
455
- import { useEffect, useRef } from "react";
486
+ import { useEffect, 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
- export default function Panel({ onClose }: PanelProps) {
525
- const {
526
- config,
527
- setColorPreset,
528
- setRadius,
529
- setDarkMode,
530
- setFont,
531
- setPresetTheme,
532
- resetTheme,
533
- } = useThemeState();
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
- // 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]);
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
- // Inject keyframes stylesheet once
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,132 @@ 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
- 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",
632
+ animation: "previewcn-rise 0.32s ease both",
633
+ animationDelay: \`\${delay}s\`,
567
634
  }}
568
635
  >
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>
636
+ {children}
637
+ </div>
638
+ );
639
+ }
589
640
 
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>
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
- {/* 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
- }}
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
- <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>
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
+ // Apply stored theme only when the panel opens
734
+ useEffect(() => {
735
+ applyTheme(config);
736
+ }, []);
737
+
738
+ usePanelKeyframes();
739
+
740
+ return (
741
+ <div className={panelClassName} style={panelStyle}>
742
+ <PanelHeader onClose={onClose} />
743
+ <PanelContent
744
+ config={config}
745
+ setColorPreset={setColorPreset}
746
+ setRadius={setRadius}
747
+ setDarkMode={setDarkMode}
748
+ setFont={setFont}
749
+ setPresetTheme={setPresetTheme}
750
+ />
751
+ <PanelFooter config={config} onReset={resetTheme} />
631
752
  </div>
632
753
  );
633
754
  }
@@ -689,7 +810,7 @@ export function PresetSelector({ value, onChange }: PresetSelectorProps) {
689
810
  </div>
690
811
  );
691
812
  }
692
- `}function R(){return `// Color preset data used by the devtools color picker.
813
+ `}function D(){return `// Color preset data used by the devtools color picker.
693
814
  // Kept in a separate file to keep logic files small.
694
815
 
695
816
  export type PresetSpec = {
@@ -707,6 +828,10 @@ export type PresetSpec = {
707
828
  light?: string;
708
829
  dark?: string;
709
830
  };
831
+ destructiveForeground?: {
832
+ light?: string;
833
+ dark?: string;
834
+ };
710
835
  };
711
836
 
712
837
  export const colorPresetSpecs: PresetSpec[] = [
@@ -901,7 +1026,7 @@ export const colorPresetSpecs: PresetSpec[] = [
901
1026
  },
902
1027
  },
903
1028
  ];
904
- `}function D(){return `// Color presets compatible with shadcn/ui
1029
+ `}function j(){return `// Color presets compatible with shadcn/ui
905
1030
  // Uses OKLCH color space for better perceptual uniformity
906
1031
 
907
1032
  import { colorPresetSpecs, type PresetSpec } from "./color-preset-specs";
@@ -963,6 +1088,11 @@ const defaultPrimaryForeground = {
963
1088
  dark: "oklch(0.985 0 0)",
964
1089
  };
965
1090
 
1091
+ const defaultDestructiveForeground = {
1092
+ light: "oklch(0.985 0 0)",
1093
+ dark: "oklch(0.985 0 0)",
1094
+ };
1095
+
966
1096
  function createColorPreset(spec: PresetSpec): ColorPreset {
967
1097
  return {
968
1098
  name: spec.name,
@@ -974,6 +1104,8 @@ function createColorPreset(spec: PresetSpec): ColorPreset {
974
1104
  "primary-foreground":
975
1105
  spec.primaryForeground?.light ?? defaultPrimaryForeground.light,
976
1106
  destructive: spec.destructive?.light ?? defaultDestructive.light,
1107
+ "destructive-foreground":
1108
+ spec.destructiveForeground?.light ?? defaultDestructiveForeground.light,
977
1109
  },
978
1110
  dark: {
979
1111
  ...neutralColors.dark,
@@ -981,6 +1113,8 @@ function createColorPreset(spec: PresetSpec): ColorPreset {
981
1113
  "primary-foreground":
982
1114
  spec.primaryForeground?.dark ?? defaultPrimaryForeground.dark,
983
1115
  destructive: spec.destructive?.dark ?? defaultDestructive.dark,
1116
+ "destructive-foreground":
1117
+ spec.destructiveForeground?.dark ?? defaultDestructiveForeground.dark,
984
1118
  },
985
1119
  },
986
1120
  };
@@ -992,7 +1126,7 @@ export const colorPresets: ColorPreset[] =
992
1126
  export function getColorPreset(name: string): ColorPreset | undefined {
993
1127
  return colorPresets.find((p) => p.name === name);
994
1128
  }
995
- `}function j(){return `// Font presets using Google Fonts
1129
+ `}function M(){return `// Font presets using Google Fonts
996
1130
 
997
1131
  export type FontPreset = {
998
1132
  label: string;
@@ -1077,12 +1211,12 @@ export const fontPresets: FontPreset[] = [
1077
1211
  export function getFontPreset(value: string): FontPreset | undefined {
1078
1212
  return fontPresets.find((f) => f.value === value);
1079
1213
  }
1080
- `}function M(){return `export * from "./colors";
1214
+ `}function O(){return `export * from "./colors";
1081
1215
  export * from "./color-preset-specs";
1082
1216
  export * from "./fonts";
1083
1217
  export * from "./radius";
1084
1218
  export * from "./theme-presets";
1085
- `}function O(){return `// Radius presets for border-radius customization
1219
+ `}function L(){return `// Radius presets for border-radius customization
1086
1220
 
1087
1221
  export type RadiusPreset = {
1088
1222
  name: string;
@@ -1370,7 +1504,7 @@ const themePresetByName = new Map(
1370
1504
  export function getThemePreset(name: string): ThemePreset | undefined {
1371
1505
  return themePresetByName.get(name);
1372
1506
  }
1373
- `}function L(){return `"use client";
1507
+ `}function $(){return `"use client";
1374
1508
 
1375
1509
  import { radiusPresets } from "./presets/radius";
1376
1510
 
@@ -1453,8 +1587,8 @@ type ThemeColors = {
1453
1587
  dark: Record<string, string>;
1454
1588
  };
1455
1589
 
1456
- function getOrCreateThemeColorStyleElement(): HTMLStyleElement {
1457
- const existing = document.getElementById(THEME_COLOR_STYLE_ID);
1590
+ function getOrCreateStyleElement(id: string): HTMLStyleElement {
1591
+ const existing = document.getElementById(id);
1458
1592
  if (existing instanceof HTMLStyleElement) {
1459
1593
  return existing;
1460
1594
  }
@@ -1464,7 +1598,7 @@ function getOrCreateThemeColorStyleElement(): HTMLStyleElement {
1464
1598
  }
1465
1599
 
1466
1600
  const styleEl = document.createElement("style");
1467
- styleEl.id = THEME_COLOR_STYLE_ID;
1601
+ styleEl.id = id;
1468
1602
  document.head.appendChild(styleEl);
1469
1603
  return styleEl;
1470
1604
  }
@@ -1476,7 +1610,7 @@ function serializeCssVars(cssVars: Record<string, string>): string {
1476
1610
  }
1477
1611
 
1478
1612
  function applyThemeColors(colors: ThemeColors) {
1479
- const styleEl = getOrCreateThemeColorStyleElement();
1613
+ const styleEl = getOrCreateStyleElement(THEME_COLOR_STYLE_ID);
1480
1614
 
1481
1615
  const lightCss = serializeCssVars(colors.light);
1482
1616
  const darkCss = serializeCssVars(colors.dark);
@@ -1518,22 +1652,6 @@ export function applyPresetColors(preset: ThemePreset) {
1518
1652
  applyThemeColors(preset.colors);
1519
1653
  }
1520
1654
 
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
1655
  function applyFontConfig(font: ThemePresetFont) {
1538
1656
  const { fontFamily, googleFontsUrl, value: fontId } = font;
1539
1657
 
@@ -1555,7 +1673,7 @@ function applyFontConfig(font: ThemePresetFont) {
1555
1673
 
1556
1674
  // Use multiple strategies to ensure font override works universally
1557
1675
  // This covers various Tailwind v4 and next/font configurations
1558
- const styleEl = getOrCreateFontStyleElement();
1676
+ const styleEl = getOrCreateStyleElement(THEME_FONT_STYLE_ID);
1559
1677
  styleEl.textContent = \`
1560
1678
  :root {
1561
1679
  --font-sans: \${fontFamily} !important;
@@ -1632,7 +1750,7 @@ export function clearTheme() {
1632
1750
  root.style.removeProperty("color-scheme");
1633
1751
  root.classList.remove("light", "dark");
1634
1752
  }
1635
- `}function $(){return `"use client";
1753
+ `}function U(){return `"use client";
1636
1754
 
1637
1755
  type TriggerProps = {
1638
1756
  onClick: () => void;
@@ -1679,7 +1797,7 @@ export function Trigger({ onClick }: TriggerProps) {
1679
1797
  </button>
1680
1798
  );
1681
1799
  }
1682
- `}function U(){return `"use client";
1800
+ `}function V(){return `"use client";
1683
1801
 
1684
1802
  import { useCallback, useState } from "react";
1685
1803
 
@@ -1817,6 +1935,6 @@ export function useThemeState() {
1817
1935
  resetTheme,
1818
1936
  };
1819
1937
  }
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();
1938
+ `}function Pe(){return `export { PreviewcnDevtools } from "./devtools";
1939
+ `}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:N()},{path:"mode-toggle.tsx",content:R()},{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...
1940
+ `);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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "previewcn",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },