Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions theme-builder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/node_modules
22 changes: 22 additions & 0 deletions theme-builder/dark-gen.js
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;
}
Comment on lines +14 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add input validation and handle undefined keys.

The function has no safeguards:

  1. No input validation - Doesn't check if light is an object or has expected keys
  2. Silent undefined assignment - If light contains keys not in keyChecker (like "DEFAULT"), dark[undefined] = value occurs, which is a bug

Apply 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function darkGenerator(light) {
let dark = {};
Object.entries(light).forEach(([key, value]) => {
dark[keyChecker[key]] = value;
});
return dark;
}
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;
}
🤖 Prompt for AI Agents
In theme-builder/dark-gen.js around lines 14 to 20, the darkGenerator lacks
input validation and can assign to undefined when a light key is not present in
keyChecker; validate that the input light is a non-null object (throw or return
an empty object for invalid input), iterate Object.entries only after that
check, and for each [key,value] ensure keyChecker has that key (use
Object.prototype.hasOwnProperty.call(keyChecker,key) or key in keyChecker)
before assigning to dark[keyChecker[key]]; skip unknown keys (or log/warn) so
you never set dark[undefined], and preserve existing behavior for valid
mappings.


module.exports = darkGenerator;
72 changes: 72 additions & 0 deletions theme-builder/formatter-and-generator.js
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add input validation for color parameters.

The function doesn't validate that the color inputs are valid hex strings before passing them to shadesOf, which will throw errors for invalid inputs.

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
In theme-builder/formatter-and-generator.js around lines 4 to 11, the function
accepts color params but does not validate them before calling shadesOf, so
invalid values will throw; add input validation at the start that collects all
color params into an object, ensures each is present and a string, and validates
each against a hex color regex (accepting #RGB or #RRGGBB); for any
missing/invalid value throw a descriptive Error like "<name> is required and
must be a valid hex color (e.g. #RRGGBB)" so callers get clear feedback instead
of shields failing inside shadesOf.

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;
60 changes: 60 additions & 0 deletions theme-builder/index.js
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add input validation and reduce code duplication.

The input collection has several issues:

  1. No HEX color validation - The tool accepts any string for color inputs, which will cause errors downstream when shadesOf attempts to parse invalid hex values.
  2. Code duplication - Seven nearly-identical validation loops.
  3. No input trimming - Whitespace in inputs could cause issues.
  4. Confusing prompt - Line 11 says "default colors" (plural) but expects a single hex value.

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
In theme-builder/index.js around lines 6 to 39, the input collection loops are
duplicated, do not trim inputs, mislabel the default color prompt, and accept
invalid hex values; refactor by extracting a helper like
promptWithValidation(message, validator) that trims input, requires non-empty
responses, and uses a hex validator for color fields, update prompt text to
"Enter default HEX color:" and apply the helper to themeName (no validator) and
all color fields (use isValidHex that accepts 3- or 6-digit hex with optional
leading #), and replace the repeated while-loops with calls to this helper to
reduce duplication and ensure proper validation.


const result = await formatterAndGenerator({
defaultColor: defaultColorInput,
primaryColor: primaryColorInput,
secondaryColor: secondaryColorInput,
successColor: successColorInput,
warningColor: warningColorInput,
dangerColor: dangerColorInput,
});
Comment on lines +41 to +48
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unnecessary await.

The formatterAndGenerator function is not async (as seen in formatter-and-generator.js), so awaiting it is unnecessary and misleading.

Apply this diff:

-  const result = await formatterAndGenerator({
+  const result = formatterAndGenerator({
     defaultColor: defaultColorInput,
     primaryColor: primaryColorInput,
     secondaryColor: secondaryColorInput,
     successColor: successColorInput,
     warningColor: warningColorInput,
     dangerColor: dangerColorInput,
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const result = await formatterAndGenerator({
defaultColor: defaultColorInput,
primaryColor: primaryColorInput,
secondaryColor: secondaryColorInput,
successColor: successColorInput,
warningColor: warningColorInput,
dangerColor: dangerColorInput,
});
const result = formatterAndGenerator({
defaultColor: defaultColorInput,
primaryColor: primaryColorInput,
secondaryColor: secondaryColorInput,
successColor: successColorInput,
warningColor: warningColorInput,
dangerColor: dangerColorInput,
});
🤖 Prompt for AI Agents
In theme-builder/index.js around lines 41 to 48, the code uses "await" when
calling formatterAndGenerator even though that function is synchronous; remove
the unnecessary await and call formatterAndGenerator directly (assign its return
value to result without awaiting) so the call reflects the actual synchronous
behavior and eliminates misleading async usage.


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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling, formatting, and user feedback.

The file write operations have several issues:

  1. No error handling - fs.writeFileSync can throw (permission errors, disk full, etc.)
  2. Poor formatting - JSON output lacks indentation, making it hard to read
  3. No user feedback - Users don't know if files were created successfully
  4. Blocking I/O - Synchronous file operations in an async function

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs.writeFileSync(
`${themeNameInput}-light.json`,
JSON.stringify({ light: result.light })
);
fs.writeFileSync(
`${themeNameInput}-dark.json`,
JSON.stringify({ dark: result.dark })
);
try {
fs.writeFileSync(
`${themeNameInput}-light.json`,
JSON.stringify({ light: result.light }, null, 2)
);
fs.writeFileSync(
`${themeNameInput}-dark.json`,
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);
}
🤖 Prompt for AI Agents
In theme-builder/index.js around lines 50 to 57, the synchronous
fs.writeFileSync calls should be replaced with asynchronous, non-blocking writes
using fs.promises.writeFile inside a try/catch; format JSON with indentation
(e.g., JSON.stringify(obj, null, 2)) before writing, wrap both write operations
in await calls (or Promise.all) to avoid blocking, and on success emit user
feedback (console.log or process.stdout.write) indicating file paths written; on
error catch and log the full error (including error.message), and either rethrow
or exit with a non-zero code so failures are visible to the caller.

}

main();
35 changes: 35 additions & 0 deletions theme-builder/package.json
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"
}
}
18 changes: 18 additions & 0 deletions theme-builder/readme.md
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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).
Clone your repo: `git clone https://github.com/heroui-inc/heroui.git`
cd into folder.
Run `npm install` in the theme-builder directory to install dependencies (prompt-sync for interactive inputs).
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

3-3: Bare URL used

(MD034, no-bare-urls)

🤖 Prompt for AI Agents
In theme-builder/readme.md around lines 3 to 5, update the repository reference
from "nextui-theme-builder" to "heroui" and convert the bare clone URL into a
proper Markdown link; replace the raw URL with a markdown link to
https://github.com/alimortazavi-pr/heroui and keep the accompanying instructions
(cd into folder, npm install) unchanged, optionally wrapping the shell commands
in inline code formatting for clarity.


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:
71 changes: 71 additions & 0 deletions theme-builder/shades-gen.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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix critical bug in hex color expansion.

Line 35 has a logic error: hex = hex + hex for a 3-character hex like "abc" produces "abcabc" (6 chars but wrong), when it should produce "aabbcc".

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
theme-builder/shades-gen.js around lines 31 to 52, the 3-character hex expansion
uses `hex = hex + hex` which transforms "abc" into "abcabc" instead of "aabbcc";
replace that line with logic that expands each of the three characters by
repeating it (e.g., map each char to char+char and join) so 3-char inputs become
6-char RRGGBB strings before parsing, leaving the rest of hexToRgbArray
unchanged and preserving error handling.


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;