cyhip-dynamic-themes 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 CyberHippie-io
3
+ Copyright (c) 2024 Kassio Rodrigues Ferreira
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,2 +1,186 @@
1
1
  # cyhip-dynamic-themes
2
- Dynamic Color Themes for React Apps
2
+
3
+ ![cyhip-dynamic-themes minzip package size](https://img.shields.io/bundlephobia/minzip/cyhip-dynamic-themes)
4
+ [![Version](https://img.shields.io/npm/v/cyhip-dynamic-themes.svg?colorB=green)](https://www.npmjs.com/package/cyhip-dynamic-themes)
5
+
6
+ **Dynamically switch color themes in your React + TypeScript + Tailwind CSS apps.**
7
+
8
+ ## Features
9
+
10
+ - **Dynamic Color Theming**: Utilize OKLCH colors for vibrant and accurate color representations.
11
+ - **Dark Mode Support**: Easily switch between light and dark modes across your custom themes.
12
+
13
+ Inspired by the excellent [article](https://evilmartians.com/chronicles/better-dynamic-themes-in-tailwind-with-oklch-color-magic) by Dan Kozlov and Travis Turner, this package integrates seamlessly with their [tw-dynamic-themes](https://github.com/dkzlv/tw-dynamic-themes) library to manage dynamic CSS variables effectively.
14
+
15
+ ## Installation
16
+
17
+ To install the package, run:
18
+
19
+ ```bash
20
+ npm install cyhip-dynamic-themes
21
+ # or
22
+ yarn add cyhip-dynamic-themes
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ### Prerequisites
28
+
29
+ Ensure you have Tailwind CSS installed in your project and a `tailwind.config.ts `file in your root directory.
30
+
31
+ ### Initialize Theme basic files
32
+
33
+ In your project's root directory, run:
34
+
35
+ ```bash
36
+ npx cyhip-dynamic-themes init
37
+ ```
38
+
39
+ This command generates the following files in the `/themes/` folder:
40
+
41
+ ```bash
42
+ /themes/
43
+ ├── hue-palettes.ts # To set your available hue-based colors.
44
+ ├── root.css # Main CSS file for styling.
45
+ ├── theme-colors.ts # Includes color definitions for Tailwind.
46
+ └── theme-switcher.tsx # Example component for theme switching.
47
+ ```
48
+
49
+ ### Update `tailwind.config.ts`
50
+
51
+ To enable dynamic colors and dark mode, modify your `tailwind.config.ts` as follows:
52
+
53
+ ```ts
54
+ // tailwind.config.ts
55
+ import type { Config } from "tailwindcss";
56
+ import { themeColors } from "./src/themes/theme-colors";
57
+
58
+ export default {
59
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
60
+ darkMode: "class",
61
+ theme: {
62
+ extend: {
63
+ colors: themeColors,
64
+ },
65
+ },
66
+ plugins: [],
67
+ } satisfies Config;
68
+ ```
69
+
70
+ **Note:** After updating the configuration, restart your application to apply the changes.
71
+
72
+ ### Import `root.css`
73
+
74
+ To apply CSS styles linked to the defined themes, add `/themes/root.css` to your root TSX file:
75
+
76
+ ```tsx
77
+ // src/main.tsx
78
+ import { StrictMode } from "react";
79
+ import { createRoot } from "react-dom/client";
80
+ import App from "./App.tsx";
81
+
82
+ // Import CSS
83
+ import "./themes/root.css";
84
+
85
+ createRoot(document.getElementById("root")!).render(
86
+ <StrictMode>
87
+ <App />
88
+ </StrictMode>
89
+ );
90
+ ```
91
+
92
+ ### Switching Themes Dynamically
93
+
94
+ Switching the main color palette can be done using the `ThemeSwitcher` component. Here's a basic example to illustrate its use:
95
+
96
+ ```tsx
97
+ // App.tsx
98
+ import { ThemeSwitcher } from "./themes/theme-switcher";
99
+ function App() {
100
+ return (
101
+ <>
102
+ <main className="h-screen flex flex-col justify-center items-center gap-y-14">
103
+ <h1 className="text-4xl font-bold text-center">
104
+ Cyhip Dynamic Themes - Basic Usage
105
+ </h1>
106
+ <ThemeSwitcher />
107
+ <div className="bg-accent-200/40 dark:bg-accent-700/40 grid grid-cols-1 gap-6 p-4">
108
+ <button className="bg-primary text-primary-foreground px-5 py-2 shadow rounded-sm font-medium mx-auto">
109
+ Button
110
+ </button>
111
+ <samp className="bg-accent-950/80 text-accent-100/90 text-sm rounded-sm px-4 py-1 shadow">
112
+ className="bg-primary text-primary-foreground ..."
113
+ </samp>
114
+ </div>
115
+ </main>
116
+ </>
117
+ );
118
+ }
119
+
120
+ export default App;
121
+ ```
122
+
123
+ Check the `/templates/theme-switcher.tsx` component to see how to initialize and alternate themes.
124
+
125
+ Finally, take a look on the last example and see how we can combine the accent variable with tailwind classes like `bg-accent-<value>` and `text-accent-<value>`.
126
+
127
+ ## Defining Color Palettes Based on Hue
128
+
129
+ You can add or modify hue palettes by visiting [OKLCH Color Preview](https://oklch.com/). To change your hue values, edit the `/themes/hue-palettes.ts` file:
130
+
131
+ ```ts
132
+ // themes/hue-palettes.ts
133
+ /**
134
+ * HUE THEMES
135
+ * Define the available color palettes here!
136
+ */
137
+
138
+ const hueScheme: Record<string, string> = {
139
+ monoCromatic: "-1",
140
+ blue: "250",
141
+ green: "150",
142
+ orange: "35",
143
+ pink: "0",
144
+ purple: "316",
145
+ };
146
+
147
+ export { hueScheme };
148
+ ```
149
+
150
+ ## API
151
+
152
+ ### `useColorTheme(hue: string, darkMode: boolean)`
153
+
154
+ A custom hook that manages the application of color themes based on the provided HUE value and dark mode setting.
155
+
156
+ - **Note**: Dispatches a custom event themeChange when the theme changes.
157
+
158
+ ### `getThemeProperties(hue: string, darkMode: boolean)`
159
+
160
+ Defines CSS class and style properties based on the provided HUE value and dark mode setting.
161
+
162
+ - **Parameters:**
163
+
164
+ - `hue`: A string representing the hue value. If -1, the theme is monochromatic.
165
+
166
+ - `darkMode`: A boolean indicating if dark mode is active.
167
+
168
+ - **Returns:**
169
+
170
+ - An object containing:
171
+ - `className`: A string for dark mode ("dark") or an empty string for light mode.
172
+ - `style`: A record of dynamically generated CSS variables for accent colors.
173
+
174
+ ### `currentAccentValue(variableName: string)`
175
+
176
+ Retrieves the current value of a specified CSS variable from the root element.
177
+
178
+ - **Parameters**:
179
+
180
+ - `variableName`: The name of the CSS variable to retrieve (e.g., "--accent-500").
181
+
182
+ - **Returns**: The OKLCH color value or `null` if not available.
183
+
184
+ ## License
185
+
186
+ This project is licensed under the MIT License. See the [LICENSE](./LICENSE) file for details.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
+ export { version } from "./__version__";
1
2
  export { consistentChroma } from "./lib/tw-dynamic-themes/runtime";
2
3
  export { dynamicTwClasses } from "./lib/tw-dynamic-themes/twPlugin";
3
4
  export { hueScheme as defaultHueScheme } from "./src/hue-scheme";
4
5
  export { currentAccentValue, getThemeProperties } from "./src/theme-helpers";
5
6
  export { useColorTheme } from "./src/theme-hook";
6
- export { version } from "./__version__";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
+ export { version } from "./__version__";
1
2
  export { consistentChroma } from "./lib/tw-dynamic-themes/runtime";
2
3
  export { dynamicTwClasses } from "./lib/tw-dynamic-themes/twPlugin";
3
4
  export { hueScheme as defaultHueScheme } from "./src/hue-scheme";
4
5
  export { currentAccentValue, getThemeProperties } from "./src/theme-helpers";
5
6
  export { useColorTheme } from "./src/theme-hook";
6
- export { version } from "./__version__";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyhip-dynamic-themes",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Tailwind-powered dynamic color themes for React apps.",
5
5
  "author": "@KassioRF, @CyberHippie-io",
6
6
  "license": "MIT",
@@ -20,22 +20,30 @@
20
20
  "README.md",
21
21
  "LICENSE"
22
22
  ],
23
+ "type": "module",
23
24
  "main": "./dist/index.js",
24
25
  "module": "./dist/index.esm.js",
25
26
  "types": "./dist/index.d.ts",
27
+ "bin": {
28
+ "cyhip-dynamic-themes": "./dist/src/cli.js"
29
+ },
26
30
  "scripts": {
27
31
  "build": "tsc --project tsconfig.json",
28
32
  "start": "tsx src/index.ts",
29
- "prepublishOnly": "yarn build"
33
+ "prepublishOnly": "pnpm build"
30
34
  },
31
35
  "devDependencies": {
32
36
  "@types/culori": "^2.1.1",
37
+ "@types/node": "^22.7.7",
33
38
  "@types/react": "^18.3.11",
39
+ "commander": "^12.1.0",
34
40
  "tsx": "^4.19.1",
35
41
  "typescript": "^5.6.3"
36
42
  },
37
43
  "dependencies": {
44
+ "chalk": "^5.3.0",
38
45
  "culori": "^4.0.1",
46
+ "inquirer": "^12.0.0",
39
47
  "react": "^18.3.1",
40
48
  "react-dom": "^18.3.1"
41
49
  },
@@ -0,0 +1,2 @@
1
+ export default huePalettes;
2
+ declare const huePalettes: "\n/**\n * HUE THEMES\n *\n * Define the available color palettes here!\n *\n * The palettes are based on HUE values.\n *\n * To add or modify a HUE palette, explore and preview colors at:\n * https://oklch.com/#70,0.1,250,100\n *\n */\n\nconst hueScheme: Record<string, string> = {\n monoCromatic: \"-1\",\n blue: \"250\",\n green: \"150\",\n orange: \"35\",\n pink: \"0\",\n purple: \"316\",\n};\n\nexport { hueScheme };\n";
@@ -0,0 +1,2 @@
1
+ var huePalettes = "\n/**\n * HUE THEMES\n *\n * Define the available color palettes here!\n *\n * The palettes are based on HUE values.\n *\n * To add or modify a HUE palette, explore and preview colors at:\n * https://oklch.com/#70,0.1,250,100\n *\n */\n\nconst hueScheme: Record<string, string> = {\n monoCromatic: \"-1\",\n blue: \"250\",\n green: \"150\",\n orange: \"35\",\n pink: \"0\",\n purple: \"316\",\n};\n\nexport { hueScheme };\n";
2
+ export default huePalettes;
@@ -0,0 +1,2 @@
1
+ export default huePalettes;
2
+ declare const huePalettes: "\n/**\n * HUE THEMES\n *\n * Define the available color palettes here!\n *\n * The palettes are based on HUE values.\n *\n * To add or modify a HUE palette, explore and preview colors at:\n * https://oklch.com/#70,0.1,250,100\n *\n */\n\nconst hueScheme: Record<string, string> = {\n monoCromatic: \"-1\",\n blue: \"250\",\n green: \"150\",\n orange: \"35\",\n pink: \"0\",\n purple: \"316\",\n};\n\nexport { hueScheme };\n";
@@ -0,0 +1,2 @@
1
+ var huePalettes = "\n/**\n * HUE THEMES\n *\n * Define the available color palettes here!\n *\n * The palettes are based on HUE values.\n *\n * To add or modify a HUE palette, explore and preview colors at:\n * https://oklch.com/#70,0.1,250,100\n *\n */\n\nconst hueScheme: Record<string, string> = {\n monoCromatic: \"-1\",\n blue: \"250\",\n green: \"150\",\n orange: \"35\",\n pink: \"0\",\n purple: \"316\",\n};\n\nexport { hueScheme };\n";
2
+ export default huePalettes;
@@ -0,0 +1,2 @@
1
+ export default rootCss;
2
+ declare const rootCss: "\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* \n * This is how your global.css or index.css should look like. \n *\n * To see how to apply this variables declared here on tailwind\n * check ./theme-colors.ts properties examples.\n * \n */\n\n@layer base {\n :root {\n /* oklch vars - Applied by dynamic accent colors */\n --background: oklch(var(--accent-100));\n --foreground: oklch(var(--accent-900));\n --primary: oklch(var(--accent-500));\n --primary-foreground: oklch(var(--accent-50));\n --secondary: oklch(var(--accent-800));\n --secondary-foreground: oklch(var(--accent-200));\n --ring: oklch(var(--accent-500));\n --box-shadow: oklch(var(--accent-800) / 0.15);\n --border: oklch(var(--accent-900) / 0.2);\n /* hsl vars */\n --muted: hsl(240 4.8% 95.9%);\n --muted-foreground: hsl(240 3.8% 45%);\n --input: hsl(240 5.9% 80%);\n }\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n @apply outline-accent-500 dark:outline-accent-400;\n }\n *,\n :after,\n :before {\n border-color: theme(\"colors.border\");\n }\n}\n\n.dark {\n /* oklch vars - Applied by dynamic accent colors */\n --background: oklch(var(--accent-900));\n --foreground: oklch(var(--accent-100));\n --primary: oklch(var(--accent-500));\n --primary-foreground: oklch(var(--accent-50));\n --box-shadow: oklch(var(--accent-100) / 0.15);\n --border: oklch(var(--accent-100) / 0.4);\n\n /* hsl vars */\n --muted: hsl(240 4.8% 30%);\n --muted-foreground: hsl(240 3.8% 70%);\n}\n\nbody {\n background-color: var(--background);\n color: var(--foreground);\n}\n";
@@ -0,0 +1,2 @@
1
+ var rootCss = "\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n/* \n * This is how your global.css or index.css should look like. \n *\n * To see how to apply this variables declared here on tailwind\n * check ./theme-colors.ts properties examples.\n * \n */\n\n@layer base {\n :root {\n /* oklch vars - Applied by dynamic accent colors */\n --background: oklch(var(--accent-100));\n --foreground: oklch(var(--accent-900));\n --primary: oklch(var(--accent-500));\n --primary-foreground: oklch(var(--accent-50));\n --secondary: oklch(var(--accent-800));\n --secondary-foreground: oklch(var(--accent-200));\n --ring: oklch(var(--accent-500));\n --box-shadow: oklch(var(--accent-800) / 0.15);\n --border: oklch(var(--accent-900) / 0.2);\n /* hsl vars */\n --muted: hsl(240 4.8% 95.9%);\n --muted-foreground: hsl(240 3.8% 45%);\n --input: hsl(240 5.9% 80%);\n }\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n @apply outline-accent-500 dark:outline-accent-400;\n }\n *,\n :after,\n :before {\n border-color: theme(\"colors.border\");\n }\n}\n\n.dark {\n /* oklch vars - Applied by dynamic accent colors */\n --background: oklch(var(--accent-900));\n --foreground: oklch(var(--accent-100));\n --primary: oklch(var(--accent-500));\n --primary-foreground: oklch(var(--accent-50));\n --box-shadow: oklch(var(--accent-100) / 0.15);\n --border: oklch(var(--accent-100) / 0.4);\n\n /* hsl vars */\n --muted: hsl(240 4.8% 30%);\n --muted-foreground: hsl(240 3.8% 70%);\n}\n\nbody {\n background-color: var(--background);\n color: var(--foreground);\n}\n";
2
+ export default rootCss;
@@ -0,0 +1,2 @@
1
+ export default themeColors;
2
+ declare const themeColors: "\n\n/**\n * COLORS\n * \n * You can use this on tailwindcss.config.ts as follows:\n * \n * import type { Config } from \"tailwindcss\";\n * import { themeColors } from \"./src/themes/theme-colors\";\n *\n * export default {\n * content: [\"./index.html\", \".\\src\\**\\*.{js,ts,jsx,tsx}\"],\n * darkMode: \"class\",\n * theme: {\n * extend: {\n * colors: themeColors,\n * },\n * },\n * plugins: [],\n * } satisfies Config;\n *\n * \n */\n\nimport colors from \"tailwindcss/colors\";\nimport { dynamicTwClasses } from \"cyhip-dynamic-themes\";\n\nexport const themeColors = {\n // accent vars to allow dynamic color changes\n accent: dynamicTwClasses(\"accent\", 250),\n // static colors as you wish...\n white: colors.white,\n destructive: colors.red,\n success: colors.green,\n /**\n * You can customize this css vars based on accent values.\n * Take a look at root.css\n */\n background: \"var(--background)\",\n foreground: \"var(--foreground)\",\n primary: {\n DEFAULT: \"var(--primary)\",\n foreground: \"var(--primary-foreground)\",\n },\n secondary: {\n DEFAULT: \"var(--secondary)\",\n foreground: \"var(--foreground)\",\n },\n muted: {\n DEFAULT: \"var(--muted)\",\n foreground: \"var(--muted-foreground)\",\n },\n border: \"var(--border)\",\n ring: \"var(--ring)\",\n input: \"var(--input)\",\n};";
@@ -0,0 +1,2 @@
1
+ var themeColors = "\n\n/**\n * COLORS\n * \n * You can use this on tailwindcss.config.ts as follows:\n * \n * import type { Config } from \"tailwindcss\";\n * import { themeColors } from \"./src/themes/theme-colors\";\n *\n * export default {\n * content: [\"./index.html\", \".\\src\\**\\*.{js,ts,jsx,tsx}\"],\n * darkMode: \"class\",\n * theme: {\n * extend: {\n * colors: themeColors,\n * },\n * },\n * plugins: [],\n * } satisfies Config;\n *\n * \n */\n\nimport colors from \"tailwindcss/colors\";\nimport { dynamicTwClasses } from \"cyhip-dynamic-themes\";\n\nexport const themeColors = {\n // accent vars to allow dynamic color changes\n accent: dynamicTwClasses(\"accent\", 250),\n // static colors as you wish...\n white: colors.white,\n destructive: colors.red,\n success: colors.green,\n /**\n * You can customize this css vars based on accent values.\n * Take a look at root.css\n */\n background: \"var(--background)\",\n foreground: \"var(--foreground)\",\n primary: {\n DEFAULT: \"var(--primary)\",\n foreground: \"var(--primary-foreground)\",\n },\n secondary: {\n DEFAULT: \"var(--secondary)\",\n foreground: \"var(--foreground)\",\n },\n muted: {\n DEFAULT: \"var(--muted)\",\n foreground: \"var(--muted-foreground)\",\n },\n border: \"var(--border)\",\n ring: \"var(--ring)\",\n input: \"var(--input)\",\n};";
2
+ export default themeColors;
@@ -0,0 +1,2 @@
1
+ export default themeSwitcher;
2
+ declare const themeSwitcher: "\n// \"use client\"; // enable this if you are using Nextjs\n/**\n * ThemeSwitcher component\n *\n * A simlpe example about how you can define a ThemeSwitcher component.\n *\n */\n\nimport { consistentChroma, useColorTheme } from \"cyhip-dynamic-themes\";\nimport { forwardRef, HTMLAttributes, useEffect, useState } from \"react\";\nimport { hueScheme } from \"./hue-palettes\";\n\n/**\n * This methods are used only to build a gradient sample based on the hue value.\n * Used for a visual referrence as a \"icon\" of the theme on the buttons.\n */\nconst buildThemeSample = (hue: string, monoCromatic: boolean = false) => {\n const oklchA = 'oklch(' + consistentChroma(4, +hue, monoCromatic) + ')';\n const oklchB = 'oklch(' + consistentChroma(5, +hue, monoCromatic) + ')';\n const oklchC = 'oklch(' + consistentChroma(6, +hue, monoCromatic) + ')';\n const gradient = 'linear-gradient(70deg, ' + oklchA + ', ' + oklchB + ', ' + oklchC + ')';\n return gradient;\n};\n\nconst availableThemes: Record<string, string> = Object.keys(hueScheme).reduce(\n (acc, key) => {\n const value = hueScheme[key];\n acc[key] = buildThemeSample(value, value === \"-1\");\n return acc;\n },\n {} as Record<string, string>\n);\n\nconst ThemeSwitcher = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement>\n>(({ ...props }, ref) => {\n /** To initialize here you can manage cookie values to reminder user preferences. */\n const [isMounted, setIsMounted] = useState(false);\n\n const [darkMode, setDarkMode] = useState(false);\n const [hue, setHue] = useState(hueScheme.blue);\n const { setTheme } = useColorTheme(hue, darkMode);\n\n useEffect(() => {\n if (!isMounted) return;\n setTheme(hue, darkMode);\n }, [hue, darkMode, setTheme, isMounted]);\n\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n return (\n <>\n <div\n ref={ref}\n className=\"bg-accent-200/40 dark:bg-accent-700/40 grid grid-cols-3 rounded-sm gap-2 p-6\"\n {...props}\n >\n {Object.keys(availableThemes).map((key) => (\n <button\n key={key}\n className=\"bg-background px-4 py-1 text-sm font-medium rounded-ms border flex space-x-2 hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary\"\n onClick={() => setHue(hueScheme[key])}\n >\n <span\n className=\"w-4 h-4 rounded-full\"\n style={{\n background: availableThemes[key],\n }}\n ></span>\n <span>{key}</span>\n </button>\n ))}\n <div className=\"col-span-3 grid grid-cols-2 gap-x-2 mx-auto\">\n <button\n className=\"bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary\"\n onClick={() => setDarkMode(false)}\n >\n Light\n </button>\n <button\n className=\"bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary\"\n onClick={() => setDarkMode(true)}\n >\n Dark\n </button>\n </div>\n </div>\n </>\n );\n});\n\nThemeSwitcher.displayName = \"ThemeSwitcher\";\n\nexport { ThemeSwitcher };\n\n";
@@ -0,0 +1,2 @@
1
+ var themeSwitcher = "\n// \"use client\"; // enable this if you are using Nextjs\n/**\n * ThemeSwitcher component\n *\n * A simlpe example about how you can define a ThemeSwitcher component.\n *\n */\n\nimport { consistentChroma, useColorTheme } from \"cyhip-dynamic-themes\";\nimport { forwardRef, HTMLAttributes, useEffect, useState } from \"react\";\nimport { hueScheme } from \"./hue-palettes\";\n\n/**\n * This methods are used only to build a gradient sample based on the hue value.\n * Used for a visual referrence as a \"icon\" of the theme on the buttons.\n */\nconst buildThemeSample = (hue: string, monoCromatic: boolean = false) => {\n const oklchA = 'oklch(' + consistentChroma(4, +hue, monoCromatic) + ')';\n const oklchB = 'oklch(' + consistentChroma(5, +hue, monoCromatic) + ')';\n const oklchC = 'oklch(' + consistentChroma(6, +hue, monoCromatic) + ')';\n const gradient = 'linear-gradient(70deg, ' + oklchA + ', ' + oklchB + ', ' + oklchC + ')';\n return gradient;\n};\n\nconst availableThemes: Record<string, string> = Object.keys(hueScheme).reduce(\n (acc, key) => {\n const value = hueScheme[key];\n acc[key] = buildThemeSample(value, value === \"-1\");\n return acc;\n },\n {} as Record<string, string>\n);\n\nconst ThemeSwitcher = forwardRef<\n HTMLDivElement,\n HTMLAttributes<HTMLDivElement>\n>(({ ...props }, ref) => {\n /** To initialize here you can manage cookie values to reminder user preferences. */\n const [isMounted, setIsMounted] = useState(false);\n\n const [darkMode, setDarkMode] = useState(false);\n const [hue, setHue] = useState(hueScheme.blue);\n const { setTheme } = useColorTheme(hue, darkMode);\n\n useEffect(() => {\n if (!isMounted) return;\n setTheme(hue, darkMode);\n }, [hue, darkMode, setTheme, isMounted]);\n\n useEffect(() => {\n setIsMounted(true);\n }, []);\n\n return (\n <>\n <div\n ref={ref}\n className=\"bg-accent-200/40 dark:bg-accent-700/40 grid grid-cols-3 rounded-sm gap-2 p-6\"\n {...props}\n >\n {Object.keys(availableThemes).map((key) => (\n <button\n key={key}\n className=\"bg-background px-4 py-1 text-sm font-medium rounded-ms border flex space-x-2 hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary\"\n onClick={() => setHue(hueScheme[key])}\n >\n <span\n className=\"w-4 h-4 rounded-full\"\n style={{\n background: availableThemes[key],\n }}\n ></span>\n <span>{key}</span>\n </button>\n ))}\n <div className=\"col-span-3 grid grid-cols-2 gap-x-2 mx-auto\">\n <button\n className=\"bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary\"\n onClick={() => setDarkMode(false)}\n >\n Light\n </button>\n <button\n className=\"bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary\"\n onClick={() => setDarkMode(true)}\n >\n Dark\n </button>\n </div>\n </div>\n </>\n );\n});\n\nThemeSwitcher.displayName = \"ThemeSwitcher\";\n\nexport { ThemeSwitcher };\n\n";
2
+ export default themeSwitcher;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
13
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ import chalk from "chalk";
39
+ import fs from "fs";
40
+ import inquirer from "inquirer";
41
+ import path from "path";
42
+ import huePalettes from "./_templates/hue-scheme.js";
43
+ import rootCss from "./_templates/root-css.js";
44
+ import themeColors from "./_templates/theme-colors.js";
45
+ import themeSwitcher from "./_templates/theme-switcher.js";
46
+ var initThemesDirectory = function (themesDir) {
47
+ if (!fs.existsSync(themesDir)) {
48
+ fs.mkdirSync(themesDir);
49
+ fs.writeFileSync(path.join(themesDir, "hue-palettes.ts"), huePalettes.trim());
50
+ fs.writeFileSync(path.join(themesDir, "root.css"), rootCss.trim());
51
+ fs.writeFileSync(path.join(themesDir, "theme-colors.ts"), themeColors.trim());
52
+ fs.writeFileSync(path.join(themesDir, "theme-switcher.tsx"), themeSwitcher.trim());
53
+ console.log(chalk.green("Default files created in \"".concat(themesDir, "\".")));
54
+ console.log(chalk.cyan(" 1. Update your tailwind.conf.ts as described in /themes/theme-colors.ts"));
55
+ console.log(chalk.cyan(" 2. Import /themes/root.css into the base tsx of your application, e.g.: [ Main.tsx or App.tsx or Layout.tsx ]"));
56
+ console.log(chalk.cyan(" 3. Add the /themes/theme-switcher.tsx component to your application and to see how it works."));
57
+ }
58
+ else {
59
+ console.error(chalk.red("A \"/themes/ folder already exists, it was not possible to initialize the files."));
60
+ }
61
+ };
62
+ var checkSrcDirectory = function () {
63
+ var srcPath = path.join(process.cwd(), "src");
64
+ return fs.existsSync(srcPath) ? "src" : ".";
65
+ };
66
+ var askDirectory = function () { return __awaiter(void 0, void 0, void 0, function () {
67
+ var srcExists, selectedDir;
68
+ return __generator(this, function (_a) {
69
+ switch (_a.label) {
70
+ case 0:
71
+ srcExists = checkSrcDirectory();
72
+ return [4 /*yield*/, inquirer.prompt([
73
+ {
74
+ type: "list",
75
+ name: "selectedDir",
76
+ message: "Where would you like to initialize the themes directory?",
77
+ choices: [
78
+ {
79
+ name: "./src (if exists)",
80
+ value: "src",
81
+ disabled: !fs.existsSync(path.join(process.cwd(), "src")),
82
+ },
83
+ { name: "./ (project root)", value: "." },
84
+ ],
85
+ default: srcExists,
86
+ },
87
+ ])];
88
+ case 1:
89
+ selectedDir = (_a.sent()).selectedDir;
90
+ return [2 /*return*/, selectedDir];
91
+ }
92
+ });
93
+ }); };
94
+ var runInit = function () { return __awaiter(void 0, void 0, void 0, function () {
95
+ var directory, themesDir;
96
+ return __generator(this, function (_a) {
97
+ switch (_a.label) {
98
+ case 0: return [4 /*yield*/, askDirectory()];
99
+ case 1:
100
+ directory = _a.sent();
101
+ themesDir = path.join(process.cwd(), directory, "themes");
102
+ initThemesDirectory(themesDir);
103
+ return [2 /*return*/];
104
+ }
105
+ });
106
+ }); };
107
+ var _a = process.argv, command = _a[2];
108
+ if (command === "init") {
109
+ runInit();
110
+ }
@@ -0,0 +1,8 @@
1
+ import { HTMLAttributes } from "react";
2
+ declare const ThemeSwitcher: import("react").ForwardRefExoticComponent<HTMLAttributes<HTMLDivElement> & {
3
+ hueScheme: Record<string, string>;
4
+ availableThemes: Record<string, string>;
5
+ setHue: (hue: string) => void;
6
+ setDarkMode: (darkMode: boolean) => void;
7
+ } & import("react").RefAttributes<HTMLDivElement>>;
8
+ export { ThemeSwitcher };
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __rest = (this && this.__rest) || function (s, e) {
14
+ var t = {};
15
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
16
+ t[p] = s[p];
17
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
18
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
19
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
20
+ t[p[i]] = s[p[i]];
21
+ }
22
+ return t;
23
+ };
24
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
25
+ import { forwardRef } from "react";
26
+ var ThemeSwitcher = forwardRef(function (_a, ref) {
27
+ var hueScheme = _a.hueScheme, availableThemes = _a.availableThemes, setHue = _a.setHue, setDarkMode = _a.setDarkMode, className = _a.className, props = __rest(_a, ["hueScheme", "availableThemes", "setHue", "setDarkMode", "className"]);
28
+ return (_jsx(_Fragment, { children: _jsxs("div", __assign({ ref: ref, className: "bg-accent-200/40 dark:bg-accent-700/40 grid grid-cols-3 rounded-sm gap-2 p-6" }, props, { children: [Object.keys(availableThemes).map(function (key) { return (_jsxs("button", { className: "bg-background px-4 py-1 text-sm font-medium rounded-ms border flex space-x-2 hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary", onClick: function () { return setHue(hueScheme[key]); }, children: [_jsx("span", { className: "w-4 h-4 rounded-full", style: {
29
+ background: availableThemes[key],
30
+ } }), _jsx("span", { children: key })] }, key)); }), _jsxs("div", { className: "col-span-3 grid grid-cols-2 gap-x-2 mx-auto", children: [_jsx("button", { className: "bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary", onClick: function () { return setDarkMode(false); }, children: "Light" }), _jsx("button", { className: "bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary", onClick: function () { return setDarkMode(true); }, children: "Dark" })] })] })) }));
31
+ });
32
+ ThemeSwitcher.displayName = "ThemeSwitcher";
33
+ export { ThemeSwitcher };
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ import { forwardRef } from "react";
14
+ var ThemeSwitcher = forwardRef(function (_a, ref) {
15
+ var hueScheme = _a.hueScheme, availableThemes = _a.availableThemes, setHue = _a.setHue, setDarkMode = _a.setDarkMode, className = _a.className, props = __rest(_a, ["hueScheme", "availableThemes", "setHue", "setDarkMode", "className"]);
16
+ return (<>
17
+ <div ref={ref} className="bg-accent-200/40 dark:bg-accent-700/40 grid grid-cols-3 rounded-sm gap-2 p-6" {...props}>
18
+ {Object.keys(availableThemes).map(function (key) { return (<button key={key} className="bg-background px-4 py-1 text-sm font-medium rounded-ms border flex space-x-2 hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary" onClick={function () { return setHue(hueScheme[key]); }}>
19
+ <span className="w-4 h-4 rounded-full" style={{
20
+ background: availableThemes[key],
21
+ }}></span>
22
+ <span>{key}</span>
23
+ </button>); })}
24
+ <div className="col-span-3 grid grid-cols-2 gap-x-2 mx-auto">
25
+ <button className="bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary" onClick={function () { return setDarkMode(false); }}>
26
+ Light
27
+ </button>
28
+ <button className="bg-background border px-4 py-1 text-sm font-medium rounded-ms hover:ring-1 hover:ring-offset-2 hover:ring-offset-background hover:ring-primary" onClick={function () { return setDarkMode(true); }}>
29
+ Dark
30
+ </button>
31
+ </div>
32
+ </div>
33
+ </>);
34
+ });
35
+ ThemeSwitcher.displayName = "ThemeSwitcher";
36
+ export { ThemeSwitcher };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyhip-dynamic-themes",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Tailwind-powered dynamic color themes for React apps.",
5
5
  "author": "@KassioRF, @CyberHippie-io",
6
6
  "license": "MIT",
@@ -20,17 +20,25 @@
20
20
  "README.md",
21
21
  "LICENSE"
22
22
  ],
23
+ "type": "module",
23
24
  "main": "./dist/index.js",
24
25
  "module": "./dist/index.esm.js",
25
26
  "types": "./dist/index.d.ts",
27
+ "bin": {
28
+ "cyhip-dynamic-themes": "./dist/src/cli.js"
29
+ },
26
30
  "devDependencies": {
27
31
  "@types/culori": "^2.1.1",
32
+ "@types/node": "^22.7.7",
28
33
  "@types/react": "^18.3.11",
34
+ "commander": "^12.1.0",
29
35
  "tsx": "^4.19.1",
30
36
  "typescript": "^5.6.3"
31
37
  },
32
38
  "dependencies": {
39
+ "chalk": "^5.3.0",
33
40
  "culori": "^4.0.1",
41
+ "inquirer": "^12.0.0",
34
42
  "react": "^18.3.1",
35
43
  "react-dom": "^18.3.1"
36
44
  },