-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: add hero ui theme builder #5903
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| /node_modules |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| const keyChecker = { | ||
| 50: 950, | ||
| 100: 900, | ||
| 200: 800, | ||
| 300: 700, | ||
| 400: 600, | ||
| 500: 500, | ||
| 600: 400, | ||
| 700: 300, | ||
| 800: 200, | ||
| 900: 100, | ||
| 950: 50, | ||
| }; | ||
| function darkGenerator(light) { | ||
| let dark = {}; | ||
| Object.entries(light).forEach(([key, value]) => { | ||
| dark[keyChecker[key]] = value; | ||
| }); | ||
| return dark; | ||
| } | ||
|
|
||
| module.exports = darkGenerator; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| const shadesOf = require("./shades-gen"); | ||
| const darkGenerator = require("./dark-gen"); | ||
|
|
||
| function formatterAndGenerator({ | ||
| defaultColor, | ||
| primaryColor, | ||
| secondaryColor, | ||
| successColor, | ||
| warningColor, | ||
| dangerColor, | ||
| }) { | ||
|
Comment on lines
+4
to
+11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation for color parameters. The function doesn't validate that the color inputs are valid hex strings before passing them to Add validation at the start of the function: function formatterAndGenerator({
defaultColor,
primaryColor,
secondaryColor,
successColor,
warningColor,
dangerColor,
}) {
// Validate all colors are provided
const colors = { defaultColor, primaryColor, secondaryColor, successColor, warningColor, dangerColor };
for (const [name, value] of Object.entries(colors)) {
if (!value || typeof value !== 'string') {
throw new Error(`${name} is required and must be a string`);
}
}🤖 Prompt for AI Agents |
||
| const lightColors = { | ||
| default: { | ||
| DEFAULT: defaultColor, | ||
| ...shadesOf(defaultColor), | ||
| }, | ||
| primary: { | ||
| DEFAULT: primaryColor, | ||
| ...shadesOf(primaryColor), | ||
| }, | ||
| secondary: { | ||
| DEFAULT: secondaryColor, | ||
| ...shadesOf(secondaryColor), | ||
| }, | ||
| success: { | ||
| DEFAULT: successColor, | ||
| ...shadesOf(successColor), | ||
| }, | ||
| warning: { | ||
| DEFAULT: warningColor, | ||
| ...shadesOf(warningColor), | ||
| }, | ||
| danger: { | ||
| DEFAULT: dangerColor, | ||
| ...shadesOf(dangerColor), | ||
| }, | ||
| }; | ||
|
|
||
| const darkColors = { | ||
| default: { | ||
| DEFAULT: defaultColor, | ||
| ...darkGenerator(shadesOf(defaultColor)), | ||
| }, | ||
| primary: { | ||
| DEFAULT: primaryColor, | ||
| ...darkGenerator(shadesOf(primaryColor)), | ||
| }, | ||
| secondary: { | ||
| DEFAULT: secondaryColor, | ||
| ...darkGenerator(shadesOf(secondaryColor)), | ||
| }, | ||
| success: { | ||
| DEFAULT: successColor, | ||
| ...darkGenerator(shadesOf(successColor)), | ||
| }, | ||
| warning: { | ||
| DEFAULT: warningColor, | ||
| ...darkGenerator(shadesOf(warningColor)), | ||
| }, | ||
| danger: { | ||
| DEFAULT: dangerColor, | ||
| ...darkGenerator(shadesOf(dangerColor)), | ||
| }, | ||
| }; | ||
|
|
||
| return { | ||
| light: lightColors, | ||
| dark: darkColors, | ||
| }; | ||
| } | ||
|
|
||
| module.exports = formatterAndGenerator; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const fs = require("fs"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const prompt = require("prompt-sync")(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const formatterAndGenerator = require("./formatter-and-generator"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let themeNameInput = prompt("Enter theme name: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!themeNameInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| themeNameInput = prompt("Enter theme name: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let defaultColorInput = prompt("Enter default colors: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!defaultColorInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultColorInput = prompt("Enter default colors: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let primaryColorInput = prompt("Enter primary HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!primaryColorInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| primaryColorInput = prompt("Enter primary HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let secondaryColorInput = prompt("Enter secondary HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!secondaryColorInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| secondaryColorInput = prompt("Enter secondary HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let successColorInput = prompt("Enter success HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!successColorInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| successColorInput = prompt("Enter success HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let warningColorInput = prompt("Enter warning HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!warningColorInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| warningColorInput = prompt("Enter warning HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let dangerColorInput = prompt("Enter danger HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| while (!dangerColorInput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| dangerColorInput = prompt("Enter danger HEX: "); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+6
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add input validation and reduce code duplication. The input collection has several issues:
Consider refactoring with a helper function: function promptWithValidation(message, validator) {
let input;
while (true) {
input = prompt(message).trim();
if (!input) {
console.log("This field is required.");
continue;
}
if (validator && !validator(input)) {
console.log("Invalid input format.");
continue;
}
return input;
}
}
function isValidHex(value) {
return /^#?[0-9A-Fa-f]{6}$|^#?[0-9A-Fa-f]{3}$/.test(value);
}
// Usage:
const themeNameInput = promptWithValidation("Enter theme name: ");
const defaultColorInput = promptWithValidation("Enter default HEX color: ", isValidHex);
const primaryColorInput = promptWithValidation("Enter primary HEX: ", isValidHex);
// ... etc🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = await formatterAndGenerator({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultColor: defaultColorInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| primaryColor: primaryColorInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| secondaryColor: secondaryColorInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| successColor: successColorInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| warningColor: warningColorInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| dangerColor: dangerColorInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unnecessary await. The Apply this diff: - const result = await formatterAndGenerator({
+ const result = formatterAndGenerator({
defaultColor: defaultColorInput,
primaryColor: primaryColorInput,
secondaryColor: secondaryColorInput,
successColor: successColorInput,
warningColor: warningColorInput,
dangerColor: dangerColorInput,
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `${themeNameInput}-light.json`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.stringify({ light: result.light }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fs.writeFileSync( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| `${themeNameInput}-dark.json`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.stringify({ dark: result.dark }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+50
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling, formatting, and user feedback. The file write operations have several issues:
Apply this diff: + try {
fs.writeFileSync(
`${themeNameInput}-light.json`,
- JSON.stringify({ light: result.light })
+ JSON.stringify({ light: result.light }, null, 2)
);
fs.writeFileSync(
`${themeNameInput}-dark.json`,
- JSON.stringify({ dark: result.dark })
+ JSON.stringify({ dark: result.dark }, null, 2)
);
+ console.log(`\n✓ Theme files created successfully:`);
+ console.log(` - ${themeNameInput}-light.json`);
+ console.log(` - ${themeNameInput}-dark.json`);
+ } catch (error) {
+ console.error(`\n✗ Error writing theme files: ${error.message}`);
+ process.exit(1);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| main(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "name": "hero-theme-builder", | ||
| "version": "1.0.0", | ||
| "description": "CLI to generate HeroUI-compatible themes for Tailwind CSS. Light/dark JSON from color inputs. By alimor.ir", | ||
| "main": "index.js", | ||
| "scripts": { | ||
| "start": "node index.js", | ||
| "dev": "nodemon index.js" | ||
| }, | ||
| "author": { | ||
| "name": "Ali Mortazavi", | ||
| "email": "[email protected]", | ||
| "url": "https://github.com/alimortazavi-pr" | ||
| }, | ||
| "keywords": [ | ||
| "heroui", | ||
| "tailwind", | ||
| "theme-generator", | ||
| "cli", | ||
| "node.js", | ||
| "react", | ||
| "next.js", | ||
| "colors" | ||
| ], | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "prompt-sync": "^4.2.0" | ||
| }, | ||
| "devDependencies": { | ||
| "nodemon": "^3.0.1" | ||
| }, | ||
| "engines": { | ||
| "node": ">=14.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,18 @@ | ||||||||||||||
| Installation | ||||||||||||||
|
|
||||||||||||||
| Clone your repo: git clone https://github.com/alimortazavi-pr/nextui-theme-builder.git | ||||||||||||||
| cd into folder. | ||||||||||||||
| npm install (adds prompt-sync for inputs; I skipped nodemon for now—dev it raw). | ||||||||||||||
|
Comment on lines
+3
to
+5
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix repository reference and markdown formatting. The repository URL references "nextui-theme-builder" but this PR is for "heroui". Additionally, the bare URL should be formatted as a proper markdown link. Apply this diff: -Clone your repo: git clone https://github.com/alimortazavi-pr/nextui-theme-builder.git
+Clone your repo: `git clone https://github.com/heroui-inc/heroui.git`
cd into folder.
-npm install (adds prompt-sync for inputs; I skipped nodemon for now—dev it raw).
+Run `npm install` in the theme-builder directory to install dependencies (prompt-sync for interactive inputs).📝 Committable suggestion
Suggested change
🧰 Tools🪛 markdownlint-cli2 (0.18.1)3-3: Bare URL used (MD034, no-bare-urls) 🤖 Prompt for AI Agents |
||||||||||||||
|
|
||||||||||||||
| Usage | ||||||||||||||
| Run npm start. Prompts: | ||||||||||||||
|
|
||||||||||||||
| Theme name: e.g., "myheroapp" | ||||||||||||||
| Default colors: e.g., "white black" (light bg/text, dark bg/text) | ||||||||||||||
| Primary HEX: e.g., "#3b82f6" (blue) | ||||||||||||||
| Secondary HEX: e.g., "#6b7280" | ||||||||||||||
| Success HEX: e.g., "#10b981" | ||||||||||||||
| Warning HEX: e.g., "#f59e0b" | ||||||||||||||
| Danger HEX: e.g., "#ef4444" | ||||||||||||||
|
|
||||||||||||||
| Outputs: myheroapp-light.json and myheroapp-dark.json. Each has the theme object. Import to tailwind.config.js: | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| function shadesOf(hex) { | ||
| const baseColor = hexToRgbArray(hex); | ||
| const black = [0, 0, 0]; | ||
| const white = [255, 255, 255]; | ||
|
|
||
| let shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]; | ||
|
|
||
| let result = {}; | ||
|
|
||
| for (let shade of shades) { | ||
| const originalShade = shade; | ||
|
|
||
| if (shade === 500) { | ||
| result[shade] = hex; | ||
| continue; | ||
| } | ||
|
|
||
| let isDarkShade = shade > 500; | ||
| if (isDarkShade) shade -= 500; | ||
|
|
||
| const percentage = shade / 500; | ||
| const startColor = isDarkShade ? black : baseColor; | ||
| const endColor = isDarkShade ? baseColor : white; | ||
|
|
||
| result[originalShade] = getColor(percentage, startColor, endColor); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| function hexToRgbArray(hex) { | ||
| const originalHex = hex; | ||
|
|
||
| hex = hex.replace("#", ""); | ||
| if (hex.length === 3) hex = hex + hex; | ||
|
|
||
| const r = hex.substring(0, 2); | ||
| const g = hex.substring(2, 4); | ||
| const b = hex.substring(4, 6); | ||
|
|
||
| const rgb = [r, g, b].map((channel) => { | ||
| try { | ||
| channel = parseInt(channel, 16); | ||
| if (channel < 0 || channel > 255) throw new Error(); | ||
| return channel; | ||
| } catch { | ||
| throw new Error(`Invalid hex color provided: ${originalHex}`); | ||
| } | ||
| }); | ||
|
|
||
| return rgb; | ||
| } | ||
|
Comment on lines
+31
to
+52
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix critical bug in hex color expansion. Line 35 has a logic error: Apply this diff: hex = hex.replace("#", "");
- if (hex.length === 3) hex = hex + hex;
+ if (hex.length === 3) {
+ hex = hex.split('').map(c => c + c).join('');
+ }
+
+ if (hex.length !== 6) {
+ throw new Error(`Invalid hex color provided: ${originalHex}`);
+ }🤖 Prompt for AI Agents |
||
|
|
||
| function getColor(percentage, start, end) { | ||
| const rgb = end.map((channel, index) => { | ||
| return Math.round(channel + percentage * (start[index] - channel)); | ||
| }); | ||
|
|
||
| const hex = | ||
| "#" + | ||
| rgb | ||
| .map((channel) => { | ||
| const component = channel.toString(16); | ||
| if (component.length === 1) return "0" + component; | ||
| return component; | ||
| }) | ||
| .join(""); | ||
| return hex; | ||
| } | ||
|
|
||
| module.exports = shadesOf; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add input validation and handle undefined keys.
The function has no safeguards:
lightis an object or has expected keyslightcontains keys not inkeyChecker(like "DEFAULT"),dark[undefined] = valueoccurs, which is a bugApply this diff:
function darkGenerator(light) { + if (!light || typeof light !== 'object') { + throw new Error('darkGenerator expects an object'); + } let dark = {}; Object.entries(light).forEach(([key, value]) => { + // Skip non-numeric keys like "DEFAULT" + if (!(key in keyChecker)) { + dark[key] = value; + return; + } dark[keyChecker[key]] = value; }); return dark; }📝 Committable suggestion
🤖 Prompt for AI Agents