gradient-forge 1.0.0 → 1.0.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/cli/index.mjs +132 -62
  2. package/package.json +1 -2
package/cli/index.mjs CHANGED
@@ -567,16 +567,10 @@ const promptSelect = async (title, items, defaultIndex = 0) => {
567
567
  }
568
568
 
569
569
  let index = defaultIndex;
570
- let isFirstRender = true;
570
+ let buffer = "";
571
571
 
572
572
  const render = () => {
573
- if (isFirstRender) {
574
- process.stdout.write("\x1b[s");
575
- isFirstRender = false;
576
- } else {
577
- process.stdout.write("\x1b[u");
578
- process.stdout.write("\x1b[0J");
579
- }
573
+ process.stdout.write("\x1b[2J\x1b[H");
580
574
  log(title);
581
575
  log("");
582
576
  items.forEach((item, i) => {
@@ -588,27 +582,35 @@ const promptSelect = async (title, items, defaultIndex = 0) => {
588
582
  };
589
583
 
590
584
  const onKey = (data) => {
591
- const key = data.toString();
592
- if (key === "\u0003") {
593
- process.stdin.setRawMode(false);
594
- process.stdin.pause();
595
- process.exit(1);
596
- }
597
- if (key === "\r") {
585
+ buffer += data.toString();
586
+
587
+ // Handle escape sequences for arrow keys
588
+ if (buffer.includes("\u001b[A")) {
589
+ // Up arrow
590
+ index = index > 0 ? index - 1 : items.length - 1;
591
+ buffer = "";
592
+ render();
593
+ } else if (buffer.includes("\u001b[B")) {
594
+ // Down arrow
595
+ index = index < items.length - 1 ? index + 1 : 0;
596
+ buffer = "";
597
+ render();
598
+ } else if (buffer.includes("\r") || buffer.includes("\n")) {
599
+ // Enter key
598
600
  process.stdin.setRawMode(false);
599
601
  process.stdin.pause();
600
602
  process.stdin.removeListener("data", onKey);
601
603
  process.stdout.write("\x1b[?25h");
602
604
  resolve(items[index]);
603
605
  return;
604
- }
605
- if (key === "\u001b[A") {
606
- index = (index - 1 + items.length) % items.length;
607
- render();
608
- }
609
- if (key === "\u001b[B") {
610
- index = (index + 1) % items.length;
611
- render();
606
+ } else if (buffer.includes("\u0003")) {
607
+ // Ctrl+C
608
+ process.stdin.setRawMode(false);
609
+ process.stdin.pause();
610
+ process.exit(1);
611
+ } else {
612
+ // Clear buffer after processing
613
+ buffer = "";
612
614
  }
613
615
  };
614
616
 
@@ -685,8 +687,13 @@ type ThemeContextValue = {
685
687
  colorMode: ColorMode;
686
688
  setThemeId: (themeId: ThemeId) => void;
687
689
  setColorMode: (mode: ColorMode) => void;
690
+ themes: { id: ThemeId; label: string }[];
688
691
  };
689
692
 
693
+ const allThemes: { id: ThemeId; label: string }[] = [
694
+ ${themes.map(t => ` { id: "${t.id}", label: "${t.label}" },`).join("\n")}
695
+ ];
696
+
690
697
  const ThemeContext = createContext<ThemeContextValue | null>(null);
691
698
 
692
699
  export const ThemeProvider = ({ children }: { children: ReactNode }) => {
@@ -696,7 +703,7 @@ export const ThemeProvider = ({ children }: { children: ReactNode }) => {
696
703
  useEffect(() => {
697
704
  const root = document.documentElement;
698
705
  root.classList.remove("dark", "light");
699
- root.classList.remove(${themes.map(t => `"${t.id}"`).join(", ")});
706
+ ${themes.map(t => ` root.classList.remove("${t.id}");`).join("\n")}
700
707
 
701
708
  root.classList.add(colorMode, themeId);
702
709
  root.setAttribute("data-theme", themeId);
@@ -707,7 +714,7 @@ export const ThemeProvider = ({ children }: { children: ReactNode }) => {
707
714
  }, [themeId, colorMode]);
708
715
 
709
716
  return (
710
- <ThemeContext.Provider value={{ themeId, colorMode, setThemeId, setColorMode }}>
717
+ <ThemeContext.Provider value={{ themeId, colorMode, setThemeId, setColorMode, themes: allThemes }}>
711
718
  {children}
712
719
  </ThemeContext.Provider>
713
720
  );
@@ -718,6 +725,31 @@ export const useTheme = () => {
718
725
  if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
719
726
  return ctx;
720
727
  };
728
+
729
+ // Simple theme switcher component - drop into your app!
730
+ export function ThemeSwitcher() {
731
+ const { themeId, colorMode, setThemeId, setColorMode, themes } = useTheme();
732
+
733
+ return (
734
+ <div className="flex items-center gap-2 p-2 rounded-lg bg-card border">
735
+ <select
736
+ value={themeId}
737
+ onChange={(e) => setThemeId(e.target.value as ThemeId)}
738
+ className="bg-background text-foreground px-2 py-1 rounded border"
739
+ >
740
+ {themes.map(t => (
741
+ <option key={t.id} value={t.id}>{t.label}</option>
742
+ ))}
743
+ </select>
744
+ <button
745
+ onClick={() => setColorMode(colorMode === "dark" ? "light" : "dark")}
746
+ className="px-2 py-1 rounded bg-primary text-primary-foreground"
747
+ >
748
+ {colorMode === "dark" ? "🌙" : "☀️"}
749
+ </button>
750
+ </div>
751
+ );
752
+ }
721
753
  `;
722
754
  };
723
755
 
@@ -727,41 +759,73 @@ const generateSetupInstructions = (projectRoot, themeId, mode, themeLabel) => {
727
759
  Theme: ${themeLabel}
728
760
  Mode: ${mode}
729
761
 
730
- ## What was created:
731
- - components/theme/theme-engine.ts - Theme definitions
732
- - components/theme/theme-context.tsx - React context provider
733
- - app/gradient-forge.css - Theme CSS variables
734
-
735
- ## Next steps:
762
+ All files created automatically!
763
+ Theme applied!
736
764
 
737
- 1. Import the CSS in your layout:
738
- import "./app/gradient-forge.css";
765
+ Your app now has beautiful gradient themes! 🎉
739
766
 
740
- 2. Wrap your app with ThemeProvider:
741
- import { ThemeProvider } from "./components/theme/theme-context";
742
-
743
- export default function RootLayout({ children }) {
744
- return (
745
- <html lang="en" className="${mode} ${themeId}" data-theme="${themeId}" data-color-mode="${mode}">
746
- <body>
747
- <ThemeProvider>{children}</ThemeProvider>
748
- </body>
749
- </html>
750
- );
751
- }
752
-
753
- 3. Use the theme in your components:
754
- import { useTheme } from "./components/theme/theme-context";
755
-
756
- function MyComponent() {
757
- const { themeId, colorMode } = useTheme();
758
- // ...
759
- }
760
-
761
- ## For more themes, visit: https://gradient-forge.vercel.app
767
+ For more themes, visit: https://gradient-forge.vercel.app
762
768
  `;
763
769
  };
764
770
 
771
+ const autoInjectLayout = (projectRoot, themeId, mode) => {
772
+ const layoutPaths = [
773
+ path.join(projectRoot, "app", "layout.tsx"),
774
+ path.join(projectRoot, "src", "app", "layout.tsx"),
775
+ ];
776
+
777
+ for (const layoutPath of layoutPaths) {
778
+ if (fs.existsSync(layoutPath)) {
779
+ let content = fs.readFileSync(layoutPath, "utf8");
780
+
781
+ // Check if already has ThemeProvider
782
+ if (content.includes("ThemeProvider")) {
783
+ return { success: true, path: layoutPath, message: "Already configured" };
784
+ }
785
+
786
+ // Add import - insert after the first import or at the start
787
+ const importStatement = `import { ThemeProvider } from "./components/theme/theme-context";`;
788
+
789
+ // Find position after the last import line
790
+ const lines = content.split('\n');
791
+ let insertIndex = 0;
792
+ for (let i = 0; i < lines.length; i++) {
793
+ if (lines[i].trim().startsWith('import ') && !lines[i].includes('from "') && !lines[i].includes("from '")) {
794
+ continue;
795
+ }
796
+ if (lines[i].trim().startsWith('import ')) {
797
+ insertIndex = i + 1;
798
+ }
799
+ }
800
+
801
+ // Insert the import
802
+ if (insertIndex > 0 && lines[insertIndex - 1].trim() !== '') {
803
+ lines.splice(insertIndex, 0, importStatement);
804
+ } else {
805
+ lines.splice(insertIndex, 0, importStatement);
806
+ }
807
+ content = lines.join('\n');
808
+
809
+ // Wrap body content with ThemeProvider
810
+ content = content.replace(
811
+ /(<body[^>]*>)([\s\S]*)(<\/body>)/,
812
+ '$1<ThemeProvider>$2</ThemeProvider>$3'
813
+ );
814
+
815
+ // Add theme classes to html tag - use className for JSX
816
+ content = content.replace(
817
+ /<html[^>]*>/,
818
+ `<html lang="en" className="${mode} ${themeId}" data-theme="${themeId}" data-color-mode="${mode}">`
819
+ );
820
+
821
+ fs.writeFileSync(layoutPath, content);
822
+ return { success: true, path: layoutPath };
823
+ }
824
+ }
825
+
826
+ return { success: false, message: "No layout.tsx found" };
827
+ };
828
+
765
829
  const init = async () => {
766
830
  log("");
767
831
  log("🎨 Gradient Forge CLI - Initialize Theme");
@@ -841,17 +905,23 @@ const init = async () => {
841
905
  }
842
906
  }
843
907
 
844
- // Generate setup instructions
845
- const instructions = generateSetupInstructions(projectRoot, themeId, mode, selectedTheme.label);
846
- const readmePath = path.join(projectRoot, "GRADIENT_FORGE_SETUP.md");
847
- fs.writeFileSync(readmePath, instructions);
848
- logSuccess(`Created setup guide: ${path.relative(projectRoot, readmePath)}`);
849
-
908
+ // Auto-inject into layout.tsx
909
+ const layoutResult = autoInjectLayout(projectRoot, themeId, mode);
910
+ if (layoutResult.success) {
911
+ logSuccess(`Updated layout: ${path.relative(projectRoot, layoutResult.path)}`);
912
+ } else {
913
+ logInfo("Note: Could not auto-detect layout.tsx");
914
+ }
915
+
850
916
  log("");
851
917
  log("======================================");
852
918
  logSuccess("Theme setup complete! 🎉");
853
919
  log("");
854
- log(instructions);
920
+ log(`Theme: ${selectedTheme.label} (${mode})`);
921
+ log("");
922
+ log("Your app now has beautiful gradient themes!");
923
+ log("Run: npm run dev");
924
+ log("");
855
925
  };
856
926
 
857
927
  const exportCommand = async () => {
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "gradient-forge",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A production-ready gradient theming framework for shadcn/ui with CLI support",
5
- "main": "index.js",
6
5
  "type": "module",
7
6
  "bin": {
8
7
  "gradient-forge": "./cli/index.mjs"