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.
Files changed (2) hide show
  1. package/dist/index.js +268 -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 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={() => 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 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 N(){return `"use client";
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
- 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,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
- 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
+ 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 R(){return `// Color preset data used by the devtools color picker.
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 D(){return `// Color presets compatible with shadcn/ui
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 j(){return `// Font presets using Google Fonts
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 M(){return `export * from "./colors";
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 O(){return `// Radius presets for border-radius customization
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 L(){return `"use client";
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 getOrCreateThemeColorStyleElement(): HTMLStyleElement {
1457
- const existing = document.getElementById(THEME_COLOR_STYLE_ID);
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 = THEME_COLOR_STYLE_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 = getOrCreateThemeColorStyleElement();
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 = getOrCreateFontStyleElement();
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 $(){return `"use client";
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 U(){return `"use client";
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 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();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "previewcn",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "engines": {
5
5
  "node": ">=18"
6
6
  },