= {
+ inter: {family: "Inter", type: "sans-serif"},
+ roboto: {family: "Roboto", type: "sans-serif"},
+ outfit: {family: "Outfit", type: "sans-serif"},
+ lora: {family: "Lora", type: "serif"},
+};
+
+function getFontStyle(fontName: FontName) {
+ const config = FONT_CONFIGS[fontName];
+
+ return config ? {fontFamily: `'${config.family}', ${config.type}`} : {};
+}
+
+export function ShowcaseComponent({children, id, name}: ShowcaseComponentProps) {
+ const {font} = useThemeBuilder();
+ const style = getFontStyle(font);
+
+ return (
+
+
{name}
+
+
{children}
+
+ );
+}
diff --git a/apps/docs/components/themes/components/showcase/avatar.tsx b/apps/docs/components/themes/components/showcase/avatar.tsx
new file mode 100644
index 0000000000..33d4cace00
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/avatar.tsx
@@ -0,0 +1,102 @@
+import {cloneElement} from "react";
+import {AvatarProps, Avatar as HeroUIAvatar} from "@heroui/react";
+import {clsx} from "@heroui/shared-utils";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {Border, HeroUIScaling} from "../../types";
+import {getBorderWidth} from "../../utils/shared";
+
+type Color = AvatarProps["color"];
+type Radius = AvatarProps["radius"];
+
+const SectionBase = ({
+ color,
+ radius,
+ isDisabled,
+ className,
+}: {
+ color?: Color;
+ radius?: Radius;
+ isDisabled?: boolean;
+ className?: string;
+}) => {
+ return (
+
+ {color}
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+ borderWidthValue,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+ borderWidthValue: Border;
+}) => {
+ let className = "h-10 w-10";
+ const border = getBorderWidth(borderWidthValue);
+ const borderClassName = border <= 2 ? `ring-${border} ring-offset-2` : `ring ring-offset-2`;
+
+ switch (scaling) {
+ case 90: {
+ className = clsx("h-6 w-6", borderClassName);
+ break;
+ }
+ case 95: {
+ className = clsx("h-8 w-8", borderClassName);
+ break;
+ }
+ case 100: {
+ className = clsx("h-10 w-10", borderClassName);
+ break;
+ }
+ case 105: {
+ className = clsx("h-12 w-12", borderClassName);
+ break;
+ }
+ case 110: {
+ className = clsx("h-14 w-14", borderClassName);
+ break;
+ }
+ }
+
+ return (
+
+ {cloneElement(, {color, radius, className, isDisabled: false})}
+ {cloneElement(, {color, radius, className, isDisabled: true})}
+
+ );
+};
+
+export const Avatar = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling, borderWidthValue} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/breadcrumbs.tsx b/apps/docs/components/themes/components/showcase/breadcrumbs.tsx
new file mode 100644
index 0000000000..e935561b5d
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/breadcrumbs.tsx
@@ -0,0 +1,113 @@
+import {
+ BreadcrumbsProps,
+ Breadcrumbs as HeroUIBreadcrumbs,
+ BreadcrumbItem as HeroUIBreadcrumbsItem,
+} from "@heroui/react";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {Border} from "../../types";
+
+type Color = BreadcrumbsProps["color"];
+
+const SectionBase = ({
+ color,
+ variant,
+ isDisabled,
+ classNames,
+}: {
+ color?: BreadcrumbsProps["color"];
+ variant?: BreadcrumbsProps["variant"];
+ isDisabled?: boolean;
+ classNames?: any;
+}) => {
+ const items = ["Home", "Music", "Artist", "Album", "Song"];
+
+ return (
+
+ {items.map((item, index) => (
+ {item}
+ ))}
+
+ );
+};
+
+const Section = ({
+ color,
+ scaling,
+ borderWidthValue,
+}: {
+ color: Color;
+ scaling: number;
+ borderWidthValue: Border;
+}) => {
+ const variants = ["bordered", "light", "solid", "solid"];
+ const disabled = [false, false, false, true];
+ let classNames = {base: "text-small"};
+
+ let borderClass = "border-medium";
+
+ if (borderWidthValue === "thin") {
+ borderClass = "border-small";
+ } else if (borderWidthValue === "thick") {
+ borderClass = "border-large";
+ }
+
+ switch (scaling) {
+ case 90: {
+ classNames = {base: "text-[0.7rem]"};
+ break;
+ }
+ case 95: {
+ classNames = {base: "text-tiny"};
+ break;
+ }
+ case 100: {
+ classNames = {base: "text-small p-0.5"};
+ break;
+ }
+ case 105: {
+ classNames = {base: "text-medium p-1"};
+ break;
+ }
+ case 110: {
+ classNames = {base: "text-large p-1.5"};
+ break;
+ }
+ }
+
+ return (
+
+ {variants.map((variant, idx) => (
+
+ ))}
+
+ );
+};
+
+export const BreadCrumbs = () => {
+ const colors: Color[] = ["foreground", "primary", "secondary", "success", "warning", "danger"];
+ const {scaling, borderWidthValue} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/button.tsx b/apps/docs/components/themes/components/showcase/button.tsx
new file mode 100644
index 0000000000..f479115857
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/button.tsx
@@ -0,0 +1,114 @@
+import {cloneElement} from "react";
+import {ButtonProps, Button as HeroUIButton} from "@heroui/react";
+import {clsx} from "@heroui/shared-utils";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {Border} from "../../types";
+
+type Color = ButtonProps["color"];
+type Radius = ButtonProps["radius"];
+type Variant = ButtonProps["variant"];
+
+const SectionBase = ({
+ color,
+ radius,
+ isDisabled,
+ variant,
+ className,
+}: {
+ color?: Color;
+ radius?: Radius;
+ isDisabled?: boolean;
+ variant?: Variant;
+ className?: string;
+}) => {
+ return (
+
+ {color}
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+ borderWidthValue,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: number;
+ borderWidthValue: Border;
+}) => {
+ const variants = ["solid", "shadow", "bordered", "flat", "faded", "ghost"];
+
+ let borderClass = "border-medium";
+
+ if (borderWidthValue === "thin") {
+ borderClass = "border-small";
+ } else if (borderWidthValue === "thick") {
+ borderClass = "border-large";
+ }
+
+ const scalingClasses = {
+ 90: "px-4 min-w-12 h-8 text-[0.7rem]",
+ 95: "px-5 min-w-14 h-9 text-tiny",
+ 100: "px-6 min-w-16 h-10 text-small",
+ 105: "px-7 min-w-18 h-11 text-medium",
+ 110: "px-8 min-w-20 h-12 text-medium",
+ };
+ const className = scalingClasses[scaling] || scalingClasses[100];
+
+ return (
+
+ {variants.map((variant) => (
+
+ ))}
+ {cloneElement(, {
+ color,
+ radius,
+ className,
+ isDisabled: true,
+ variant: "solid",
+ })}
+
+ );
+};
+
+export const Button = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling, borderWidthValue} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/checkbox.tsx b/apps/docs/components/themes/components/showcase/checkbox.tsx
new file mode 100644
index 0000000000..84ff298a9f
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/checkbox.tsx
@@ -0,0 +1,113 @@
+import {cloneElement} from "react";
+import {CheckboxProps, Checkbox as HeroUICheckbox} from "@heroui/react";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {HeroUIScaling} from "../../types";
+
+type Color = CheckboxProps["color"];
+type Radius = CheckboxProps["radius"];
+
+const SectionBase = ({
+ color,
+ isDisabled,
+ radius,
+ classNames,
+}: {
+ color?: Color;
+ isDisabled?: boolean;
+ radius?: Radius;
+ classNames?: any;
+}) => {
+ return (
+
+ {color}
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+}) => {
+ let classNames = {
+ wrapper: "h-6 w-6",
+ icon: "w-5 h-4",
+ label: "text-medium",
+ };
+
+ switch (scaling) {
+ case 90: {
+ classNames = {
+ wrapper: "h-5 w-5",
+ icon: "w-4 h-3",
+ label: "text-small",
+ };
+ break;
+ }
+ case 95: {
+ classNames = {
+ wrapper: "h-5 w-5",
+ icon: "w-4 h-3",
+ label: "text-medium",
+ };
+ break;
+ }
+ case 100: {
+ classNames = {
+ wrapper: "h-6 w-6",
+ icon: "w-4 h-3",
+ label: "text-medium",
+ };
+ break;
+ }
+ case 105: {
+ classNames = {
+ wrapper: "h-6 w-6",
+ icon: "w-4 h-3",
+ label: "text-large",
+ };
+ break;
+ }
+ case 110: {
+ classNames = {
+ wrapper: "h-7 w-7",
+ icon: "w-5 h-4",
+ label: "text-large",
+ };
+ break;
+ }
+ }
+
+ return (
+
+ {cloneElement(, {color, radius, classNames, isDisabled: false})}
+ {cloneElement(, {color, radius, classNames, isDisabled: true})}
+
+ );
+};
+
+export const Checkbox = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/chip.tsx b/apps/docs/components/themes/components/showcase/chip.tsx
new file mode 100644
index 0000000000..5631242922
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/chip.tsx
@@ -0,0 +1,128 @@
+import {cloneElement} from "react";
+import {ChipProps, Chip as HeroUIChip} from "@heroui/react";
+import {clsx} from "@heroui/shared-utils";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {Border, HeroUIScaling} from "../../types";
+
+type Color = ChipProps["color"];
+type Radius = ChipProps["radius"];
+type Variant = ChipProps["variant"];
+
+const SectionBase = ({
+ color,
+ isDisabled,
+ radius,
+ variant,
+ className,
+}: {
+ color?: Color;
+ isDisabled?: boolean;
+ radius?: Radius;
+ variant?: Variant;
+ className?: string;
+}) => {
+ return (
+
+ {color}
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+ borderWidthValue,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+ borderWidthValue: Border;
+}) => {
+ const variants = ["solid", "bordered", "light", "flat", "faded", "shadow"];
+
+ let borderClass = "border-medium";
+
+ if (borderWidthValue === "thin") {
+ borderClass = "border-small";
+ } else if (borderWidthValue === "thick") {
+ borderClass = "border-large";
+ }
+
+ let className = "px-1 h-6 text-tiny";
+
+ switch (scaling) {
+ case 90: {
+ className = "h-5 text-tiny";
+ break;
+ }
+ case 95: {
+ className = "h-6 text-tiny";
+ break;
+ }
+ case 100: {
+ className = "px-1 h-7 text-tiny";
+ break;
+ }
+ case 105: {
+ className = "px-2 h-8 text-medium";
+ break;
+ }
+ case 110: {
+ className = "px-3 h-8 text-medium";
+ break;
+ }
+ }
+
+ return (
+
+ {variants.map((variant, idx) =>
+ cloneElement(, {
+ color,
+ className: clsx(
+ className,
+ variant === "bordered" || variant === "faded" ? borderClass : "",
+ ),
+ variant,
+ isDisabled: false,
+ radius,
+ }),
+ )}
+ {cloneElement(, {
+ color,
+ className,
+ variant: "solid",
+ isDisabled: true,
+ radius,
+ })}
+
+ );
+};
+
+export const Chip = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling, borderWidthValue} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/code.tsx b/apps/docs/components/themes/components/showcase/code.tsx
new file mode 100644
index 0000000000..ab8ce3dfdb
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/code.tsx
@@ -0,0 +1,78 @@
+import {CodeProps, Code as HeroUICode} from "@heroui/react";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {HeroUIScaling} from "../../types";
+
+type Color = CodeProps["color"];
+type Radius = CodeProps["radius"];
+
+const SectionBase = ({
+ color,
+ radius,
+ className,
+}: {
+ color?: Color;
+ radius?: Radius;
+ className?: string;
+}) => {
+ return (
+
+ npm install @heroui/react
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+}) => {
+ let className = "p-0 px-3 text-tiny";
+
+ switch (scaling) {
+ case 90: {
+ className = "p-2 px-3 text-tiny";
+ break;
+ }
+ case 95: {
+ className = "p-2 px-4 text-tiny";
+ break;
+ }
+ case 100: {
+ className = "p-2 px-4 text-medium";
+ break;
+ }
+ case 105: {
+ className = "p-3 px-5 text-medium";
+ break;
+ }
+ case 110: {
+ className = "p-2 px-8 text-medium";
+ break;
+ }
+ }
+
+ return (
+
+
+
+ );
+};
+
+export const Code = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/index.tsx b/apps/docs/components/themes/components/showcase/index.tsx
new file mode 100644
index 0000000000..9f30f7f5e8
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/index.tsx
@@ -0,0 +1,30 @@
+import {showcaseId} from "../../constants";
+
+import {Avatar} from "./avatar";
+import {BreadCrumbs} from "./breadcrumbs";
+import {Button} from "./button";
+import {Checkbox} from "./checkbox";
+import {Chip} from "./chip";
+import {Code} from "./code";
+import {InputComponent} from "./input";
+import {PopoverComponent} from "./popover";
+import {SwitchComponent} from "./switch";
+import {TabsComponent} from "./tabs";
+
+export function Showcase() {
+ return (
+
+
Theme Generator
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/docs/components/themes/components/showcase/input.tsx b/apps/docs/components/themes/components/showcase/input.tsx
new file mode 100644
index 0000000000..7ff0b7fbcc
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/input.tsx
@@ -0,0 +1,125 @@
+import {InputProps, Input} from "@heroui/react";
+import {clsx} from "@heroui/shared-utils";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {Border, HeroUIScaling} from "../../types";
+
+type Color = InputProps["color"];
+type Radius = InputProps["radius"];
+type Variant = InputProps["variant"];
+
+const SectionBase = ({
+ color,
+ isDisabled,
+ radius,
+ variant,
+ classNames,
+}: {
+ color?: Color;
+ isDisabled?: boolean;
+ radius?: Radius;
+ variant?: Variant;
+ classNames?: any;
+}) => {
+ return (
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+ borderWidthValue,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+ borderWidthValue: Border;
+}) => {
+ const variants = ["flat", "bordered", "faded", "underlined"];
+ let classNames = {base: "h-10 w-[340px]", label: "text-small"};
+
+ let borderClass = "border-medium";
+
+ if (borderWidthValue === "thin") {
+ borderClass = "border-small";
+ } else if (borderWidthValue === "thick") {
+ borderClass = "border-large";
+ }
+
+ switch (scaling) {
+ case 90: {
+ classNames = {base: "h-8 w-[300px]", label: "text-tiny"};
+ break;
+ }
+ case 95: {
+ classNames = {base: "h-8 w-[320px]", label: "text-tiny"};
+ break;
+ }
+ case 100: {
+ classNames = {base: "h-10 w-[340px]", label: "text-small"};
+ break;
+ }
+ case 105: {
+ classNames = {base: "h-12 w-[360px]", label: "text-medium"};
+ break;
+ }
+ case 110: {
+ classNames = {base: "h-12 w-[380px]", label: "text-medium"};
+ break;
+ }
+ }
+
+ return (
+
+ {variants.map((variant, idx) => (
+
+ ))}
+
+
+ );
+};
+
+export const InputComponent = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling, borderWidthValue} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/popover.tsx b/apps/docs/components/themes/components/showcase/popover.tsx
new file mode 100644
index 0000000000..aeb3386887
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/popover.tsx
@@ -0,0 +1,86 @@
+import {PopoverProps, Popover, PopoverTrigger, PopoverContent, Button} from "@heroui/react";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {HeroUIScaling} from "../../types";
+
+type Color = PopoverProps["color"];
+type Radius = PopoverProps["radius"];
+
+const SectionBase = ({
+ color,
+ radius,
+ className,
+}: {
+ color?: Color;
+ radius?: Radius;
+ className?: string;
+}) => {
+ return (
+
+
+
+
+
+
+
Popover Content
+
This is the popover content
+
+
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+}) => {
+ let className = "text-small px-1";
+
+ switch (scaling) {
+ case 90: {
+ className = "px-1 py-2 text-tiny";
+ break;
+ }
+ case 95: {
+ className = "text-tiny px-2 py-3";
+ break;
+ }
+ case 100: {
+ className = "text-small px-2 py-3";
+ break;
+ }
+ case 105: {
+ className = "text-small px-3 py-3";
+ break;
+ }
+ case 110: {
+ className = "text-medium px-3 py-3";
+ break;
+ }
+ }
+
+ return (
+
+
+
+ );
+};
+
+export const PopoverComponent = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/switch.tsx b/apps/docs/components/themes/components/showcase/switch.tsx
new file mode 100644
index 0000000000..f70b619466
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/switch.tsx
@@ -0,0 +1,96 @@
+import {cloneElement} from "react";
+import {SwitchProps, Switch} from "@heroui/react";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {HeroUIScaling} from "../../types";
+
+type Color = SwitchProps["color"];
+
+const SectionBase = ({
+ color,
+ isDisabled,
+ classNames,
+}: {
+ color?: Color;
+ isDisabled?: boolean;
+ classNames?: {
+ wrapper?: string;
+ thumb?: string;
+ };
+}) => {
+ return (
+
+ );
+};
+
+const Section = ({color, scaling}: {color: Color; scaling: HeroUIScaling}) => {
+ let classNames = {
+ wrapper: "w-12 h-7",
+ thumb: "w-5 h-5",
+ };
+
+ switch (scaling) {
+ case 90: {
+ classNames = {
+ wrapper: "w-8 h-4",
+ thumb: "w-2 h-2 group-data-[selected=true]:ms-4",
+ };
+ break;
+ }
+ case 95: {
+ classNames = {
+ wrapper: "w-10 h-6",
+ thumb: "w-3 h-3",
+ };
+ break;
+ }
+ case 100: {
+ classNames = {
+ wrapper: "w-12 h-7",
+ thumb: "w-4 h-4 group-data-[selected=true]:ms-6",
+ };
+ break;
+ }
+ case 105: {
+ classNames = {
+ wrapper: "w-14 h-8",
+ thumb: "w-5 h-5 group-data-[selected=true]:ms-7",
+ };
+ break;
+ }
+ case 110: {
+ classNames = {
+ wrapper: "w-16 h-9",
+ thumb: "w-6 h-6 group-data-[selected=true]:ms-8",
+ };
+ break;
+ }
+ }
+
+ return (
+
+ {cloneElement(, {color, classNames, isDisabled: false})}
+ {cloneElement(, {color, classNames, isDisabled: true})}
+
+ );
+};
+
+export const SwitchComponent = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {scaling} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/components/showcase/tabs.tsx b/apps/docs/components/themes/components/showcase/tabs.tsx
new file mode 100644
index 0000000000..9c4ea5aca6
--- /dev/null
+++ b/apps/docs/components/themes/components/showcase/tabs.tsx
@@ -0,0 +1,130 @@
+import {cloneElement} from "react";
+import {TabsProps, Tabs, Tab} from "@heroui/react";
+
+import {ShowcaseComponent} from "../showcase-component";
+import {useThemeBuilder} from "../../provider";
+import {Border, HeroUIScaling} from "../../types";
+
+type Color = TabsProps["color"];
+type Radius = TabsProps["radius"];
+type Variant = TabsProps["variant"];
+
+const SectionBase = ({
+ color,
+ isDisabled,
+ radius,
+ variant,
+ classNames,
+}: {
+ color?: Color;
+ isDisabled?: boolean;
+ radius?: Radius;
+ variant?: Variant;
+ classNames?: any;
+}) => {
+ return (
+
+
+
+
+
+ );
+};
+
+const Section = ({
+ color,
+ radius,
+ scaling,
+ borderWidthValue,
+}: {
+ color: Color;
+ radius: Radius;
+ scaling: HeroUIScaling;
+ borderWidthValue: Border;
+}) => {
+ const variants = ["solid", "bordered", "light", "underlined"];
+
+ let borderClass = "border-medium";
+
+ if (borderWidthValue === "thin") {
+ borderClass = "border-small";
+ } else if (borderWidthValue === "thick") {
+ borderClass = "border-large";
+ }
+
+ let classNames = {tab: "text-tiny px-2 h-6"};
+
+ switch (scaling) {
+ case 90: {
+ classNames = {tab: "text-tiny px-2 h-6"};
+ break;
+ }
+ case 95: {
+ classNames = {tab: "text-tiny px-2 h-7"};
+ break;
+ }
+ case 100: {
+ classNames = {tab: "text-tiny px-3 h-7"};
+ break;
+ }
+ case 105: {
+ classNames = {tab: "text-medium px-3 h-8"};
+ break;
+ }
+ case 110: {
+ classNames = {tab: "text-medium px-4 h-9"};
+ break;
+ }
+ }
+
+ return (
+
+ {variants.map((variant, idx) =>
+ cloneElement(, {
+ color,
+ variant,
+ classNames: {
+ ...classNames,
+ tabList: variant === "bordered" ? borderClass : "",
+ },
+ isDisabled: false,
+ radius,
+ }),
+ )}
+ {cloneElement(, {
+ color,
+ classNames,
+ variant: "solid",
+ isDisabled: true,
+ radius,
+ })}
+
+ );
+};
+
+export const TabsComponent = () => {
+ const colors: Color[] = ["default", "primary", "secondary", "success", "warning", "danger"];
+ const {radiusValue, scaling, borderWidthValue} = useThemeBuilder();
+
+ return (
+
+ {colors.map((color, idx) => (
+
+ ))}
+
+ );
+};
diff --git a/apps/docs/components/themes/constants.ts b/apps/docs/components/themes/constants.ts
new file mode 100644
index 0000000000..5ff0d0473f
--- /dev/null
+++ b/apps/docs/components/themes/constants.ts
@@ -0,0 +1,111 @@
+import {colors} from "@heroui/theme";
+
+import {ConfigColors, Config, ConfigLayout} from "./types";
+
+// Colors
+export const defaultDarkColorWeight = 20;
+export const defaultLightColorWeight = 17.5;
+export const colorWeight = 17.5;
+
+// Regex
+export const floatNumberPattern = /^\d+(\.\d*)?$/;
+
+// Elements ids
+export const colorsId = "th-colors";
+export const defaultColorsId = "th-default-colors";
+export const baseColorsId = "th-base-colors";
+export const otherColorsId = "th-other-colors";
+export const showcaseId = "th-showcase";
+export const contentShowcaseId = "th-content-showcase";
+export const contentColorsId = "th-content-colors";
+
+// Local storage
+export const configKey = "config";
+export const syncThemesKey = "sync-themes";
+
+// Theme configuration
+export const initialLightTheme: ConfigColors = {
+ defaultColor: {
+ default: "#d4d4d8",
+ },
+ baseColor: {
+ primary: colors.blue[500],
+ secondary: colors.purple[500],
+ success: colors.green[500],
+ warning: colors.yellow[500],
+ danger: colors.red[500],
+ },
+ layoutColor: {
+ foreground: colors.black,
+ background: colors.white,
+ focus: colors.blue[500],
+ overlay: colors.black,
+ },
+ contentColor: {
+ content1: colors.white,
+ content2: colors.zinc[100],
+ content3: colors.zinc[200],
+ content4: colors.zinc[300],
+ },
+};
+
+export const initialDarkTheme: ConfigColors = {
+ defaultColor: {
+ default: colors.zinc[700],
+ },
+ baseColor: {
+ primary: colors.blue[500],
+ secondary: colors.purple[500],
+ success: colors.green[500],
+ warning: colors.yellow[500],
+ danger: colors.red[500],
+ },
+ layoutColor: {
+ foreground: colors.white,
+ background: colors.black,
+ focus: colors.blue[500],
+ overlay: colors.white,
+ },
+ contentColor: {
+ content1: colors.zinc[900],
+ content2: colors.zinc[800],
+ content3: colors.zinc[700],
+ content4: colors.zinc[600],
+ },
+};
+
+export const initialLayout: ConfigLayout = {
+ fontSize: {
+ tiny: "0.75",
+ small: "0.875",
+ medium: "1",
+ large: "1.125",
+ },
+ lineHeight: {
+ tiny: "1",
+ small: "1.25",
+ medium: "1.5",
+ large: "1.75",
+ },
+ radius: {
+ small: "0.5",
+ medium: "0.75",
+ large: "0.875",
+ },
+ borderWidth: {
+ small: "1",
+ medium: "2",
+ large: "3",
+ },
+ otherParams: {
+ disabledOpacity: "0.5",
+ dividerWeight: "1",
+ hoverOpacity: "0.9",
+ },
+};
+
+export const initialConfig: Config = {
+ light: initialLightTheme,
+ dark: initialDarkTheme,
+ layout: initialLayout,
+};
diff --git a/apps/docs/components/themes/css-vars.ts b/apps/docs/components/themes/css-vars.ts
new file mode 100644
index 0000000000..3a240bd5a1
--- /dev/null
+++ b/apps/docs/components/themes/css-vars.ts
@@ -0,0 +1,157 @@
+import {readableColor} from "color2k";
+
+import {colorsId, baseColorsId, showcaseId, otherColorsId, defaultColorsId} from "./constants";
+import {ColorPickerType, Config, ConfigLayout, ThemeType, ThemeColor} from "./types";
+import {generateThemeColor, hexToHsl} from "./utils/colors";
+
+export function setCssColor(colorType: ColorPickerType, value: string, theme: ThemeType) {
+ const baseColorEl = document.getElementById(colorsId);
+ const commonColorsEl = document.getElementById(baseColorsId);
+ const showcaseEl = document.getElementById(showcaseId);
+ const defaultColorEl = document.getElementById(defaultColorsId);
+ const themeColor = generateThemeColor(value, colorType, theme);
+
+ if (!baseColorEl || !commonColorsEl || !showcaseEl || !defaultColorEl) {
+ // eslint-disable-next-line no-console
+ console.error("One or more required elements are missing from the DOM.");
+
+ return;
+ }
+
+ Object.keys(themeColor).forEach((key) => {
+ const value = hexToHsl(themeColor[key as keyof ThemeColor]);
+
+ if (key === "DEFAULT") {
+ baseColorEl.style.setProperty(`--heroui-${colorType}`, value);
+ commonColorsEl.style.setProperty(`--heroui-${colorType}`, value);
+ showcaseEl.style.setProperty(`--heroui-${colorType}`, value);
+ defaultColorEl.style.setProperty(`--heroui-${colorType}`, value);
+ } else {
+ baseColorEl.style.setProperty(`--heroui-${colorType}-${key}`, value);
+ commonColorsEl.style.setProperty(`--heroui-${colorType}-${key}`, value);
+ showcaseEl.style.setProperty(`--heroui-${colorType}-${key}`, value);
+ defaultColorEl.style.setProperty(`--heroui-${colorType}`, value);
+ }
+ });
+}
+
+export function setCssBackground(value: string) {
+ const showcaseEl = document.getElementById(showcaseId);
+ const baseColor = document.getElementById(baseColorsId);
+ const hslValue = hexToHsl(value);
+
+ baseColor?.style.setProperty("--heroui-background", hslValue);
+ showcaseEl?.style.setProperty("--heroui-background", hslValue);
+}
+
+export function setCssFontSize(type: keyof ConfigLayout["fontSize"], value: string) {
+ const el = document.getElementById(showcaseId);
+
+ el?.style.setProperty(`--heroui-font-size-${type}`, `${value}rem`);
+}
+
+export function setCssLineHeight(type: keyof ConfigLayout["lineHeight"], value: string) {
+ const el = document.getElementById(showcaseId);
+
+ el?.style.setProperty(`--heroui-line-height-${type}`, `${value}rem`);
+}
+
+export function setCssRadius(type: keyof ConfigLayout["radius"], value: string) {
+ const el = document.getElementById(showcaseId);
+
+ el?.style.setProperty(`--heroui-radius-${type}`, `${value}rem`);
+}
+
+export function setCssBorderWidth(type: keyof ConfigLayout["borderWidth"], value: string) {
+ const el = document.getElementById(showcaseId);
+
+ el?.style.setProperty(`--heroui-border-width-${type}`, `${value}px`);
+}
+
+export function setCssContentColor(level: 1 | 2 | 3 | 4, value: string) {
+ const showcaseEl = document.getElementById(showcaseId);
+ const baseColorEl = document.getElementById(baseColorsId);
+ const hslValue = hexToHsl(value);
+
+ showcaseEl?.style.setProperty(`--heroui-content${level}`, hslValue);
+ showcaseEl?.style.setProperty(
+ `--heroui-content${level}-foreground`,
+ hexToHsl(readableColor(value)),
+ );
+ baseColorEl?.style.setProperty(`--heroui-content${level}`, hslValue);
+ baseColorEl?.style.setProperty(
+ `--heroui-content${level}-foreground`,
+ hexToHsl(readableColor(value)),
+ );
+}
+
+export function setCssOtherColor(
+ type: "background" | "foreground" | "focus" | "overlay",
+ value: string,
+) {
+ const showcaseEl = document.getElementById(showcaseId);
+ const otherColors = document.getElementById(otherColorsId);
+ const hslValue = hexToHsl(value);
+
+ otherColors?.style.setProperty(`--heroui-${type}`, hslValue);
+ showcaseEl?.style.setProperty(`--heroui-${type}`, hslValue);
+}
+
+export function setOtherCssParams(type: keyof ConfigLayout["otherParams"], value: string) {
+ const el = document.getElementById(showcaseId);
+
+ if (!el) return;
+
+ switch (type) {
+ case "disabledOpacity":
+ el.style.setProperty("--heroui-disabled-opacity", value);
+ break;
+ case "dividerWeight":
+ el.style.setProperty("--heroui-divider-weight", `${value}px`);
+ break;
+ case "hoverOpacity":
+ el.style.setProperty("--heroui-hover-opacity", value);
+ break;
+ }
+}
+
+export function setAllCssVars(config: Config, theme: ThemeType) {
+ if (!config[theme] || !config[theme].baseColor || !config[theme].layoutColor || !config.layout) {
+ // eslint-disable-next-line no-console
+ console.error("Invalid configuration or theme provided.");
+
+ return;
+ }
+
+ setCssColor("default", config[theme].defaultColor.default, theme);
+ setCssColor("primary", config[theme].baseColor.primary, theme);
+ setCssColor("secondary", config[theme].baseColor.secondary, theme);
+ setCssColor("success", config[theme].baseColor.success, theme);
+ setCssColor("warning", config[theme].baseColor.warning, theme);
+ setCssColor("danger", config[theme].baseColor.danger, theme);
+ setCssColor("foreground", config[theme].layoutColor.foreground, theme);
+ setCssContentColor(1, config[theme].contentColor.content1);
+ setCssContentColor(2, config[theme].contentColor.content2);
+ setCssContentColor(3, config[theme].contentColor.content3);
+ setCssContentColor(4, config[theme].contentColor.content4);
+ setCssBackground(config[theme].layoutColor.background);
+ setCssFontSize("tiny", config.layout.fontSize.tiny);
+ setCssFontSize("small", config.layout.fontSize.small);
+ setCssFontSize("medium", config.layout.fontSize.medium);
+ setCssFontSize("large", config.layout.fontSize.large);
+ setCssLineHeight("tiny", config.layout.lineHeight.tiny);
+ setCssLineHeight("small", config.layout.lineHeight.small);
+ setCssLineHeight("medium", config.layout.lineHeight.medium);
+ setCssLineHeight("large", config.layout.lineHeight.large);
+ setCssRadius("small", config.layout.radius.small);
+ setCssRadius("medium", config.layout.radius.medium);
+ setCssRadius("large", config.layout.radius.large);
+ setCssBorderWidth("small", config.layout.borderWidth.small);
+ setCssBorderWidth("medium", config.layout.borderWidth.medium);
+ setCssBorderWidth("large", config.layout.borderWidth.large);
+ setCssOtherColor("focus", config[theme].layoutColor.focus);
+ setCssOtherColor("overlay", config[theme].layoutColor.overlay);
+ setOtherCssParams("disabledOpacity", config.layout.otherParams.disabledOpacity);
+ setOtherCssParams("dividerWeight", config.layout.otherParams.dividerWeight);
+ setOtherCssParams("hoverOpacity", config.layout.otherParams.hoverOpacity);
+}
diff --git a/apps/docs/components/themes/index.tsx b/apps/docs/components/themes/index.tsx
new file mode 100644
index 0000000000..12c2c96285
--- /dev/null
+++ b/apps/docs/components/themes/index.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import {useEffect, useState} from "react";
+
+import Configuration from "./components/configuration";
+import {Showcase} from "./components/showcase";
+import ThemeBuilderProvider from "./provider";
+
+export function ThemeBuilder() {
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ return isClient ? (
+
+
+
+
+ ) : null;
+}
diff --git a/apps/docs/components/themes/provider.tsx b/apps/docs/components/themes/provider.tsx
new file mode 100644
index 0000000000..9bd43d92ed
--- /dev/null
+++ b/apps/docs/components/themes/provider.tsx
@@ -0,0 +1,346 @@
+import {useState, createContext, useContext} from "react";
+import {useLocalStorage} from "usehooks-ts";
+
+import {configKey, initialConfig} from "./constants";
+import {
+ ConfigColors,
+ Config,
+ ConfigLayout,
+ ThemeType,
+ Radius,
+ TemplateType,
+ FontType,
+ HeroUIScaling,
+ Border,
+} from "./types";
+
+export interface ThemeBuilderContextProps {
+ config: Config;
+ radiusValue: Radius;
+ borderWidthValue: Border;
+ templateTheme: TemplateType;
+ font: FontType;
+ scaling: HeroUIScaling;
+ resetConfig: (theme: ThemeType, sync: boolean) => Config;
+ setLayoutColor: (
+ newConfig: Partial,
+ theme: ThemeType,
+ sync: boolean,
+ ) => void;
+ setContentColor: (newConfig: Partial, theme: ThemeType) => void;
+ setBorderWidth: (newConfig: Partial) => void;
+ setBaseColor: (
+ newConfig: Partial,
+ theme: ThemeType,
+ sync: boolean,
+ ) => void;
+ setDefaultColor: (
+ newConfig: Partial,
+ theme: ThemeType,
+ sync: boolean,
+ ) => void;
+ setConfiguration: (newConfig: Config, theme: ThemeType, sync: boolean) => void;
+ setLineHeight: (newConfig: Partial) => void;
+ setFontSize: (newConfig: Partial) => void;
+ setOtherParams: (newConfig: Partial) => void;
+ setRadius: (newConfig: Partial) => void;
+ setRadiusValue: (radius: Radius) => void;
+ setBorderWidthValue: (borderWidth: Border) => void;
+ setTemplateTheme: (theme: TemplateType) => void;
+ setFont: (font: FontType) => void;
+ setScaling: (scale: HeroUIScaling) => void;
+}
+
+const ThemeBuilderContext = createContext({
+ config: initialConfig,
+ radiusValue: "md",
+ borderWidthValue: "thick",
+ templateTheme: "heroui",
+ font: "inter",
+ scaling: 100,
+ resetConfig: () => initialConfig,
+ setLayoutColor: () => {},
+ setBorderWidth: () => {},
+ setBaseColor: () => {},
+ setConfiguration: () => {},
+ setLineHeight: () => {},
+ setFontSize: () => {},
+ setOtherParams: () => {},
+ setRadius: () => {},
+ setDefaultColor: () => {},
+ setContentColor: () => {},
+ setRadiusValue: () => {},
+ setBorderWidthValue: () => {},
+ setTemplateTheme: () => {},
+ setFont: () => {},
+ setScaling: () => {},
+});
+
+interface ThemeBuilderProviderProps {
+ children: React.ReactNode;
+}
+
+export default function ThemeBuilderProvider({children}: ThemeBuilderProviderProps) {
+ const [lsConfig] = useLocalStorage(configKey, initialConfig);
+ const [config, setConfig] = useState(lsConfig);
+ const [radiusValue, setRadiusValue] = useState("sm");
+ const [borderWidthValue, setBorderWidthValue] = useState("thin");
+ const [templateTheme, setTemplateTheme] = useState("heroui");
+ const [font, setFont] = useState("inter");
+ const [scaling, setScaling] = useState(100);
+
+ const setConfiguration = (newConfig: Config, theme: ThemeType, sync: boolean) => {
+ setConfig((prev) =>
+ sync
+ ? newConfig
+ : {
+ ...prev,
+ [theme]: newConfig[theme],
+ },
+ );
+ };
+
+ const resetConfig = (theme: ThemeType, sync: boolean) => {
+ let newConfig = initialConfig;
+
+ setConfig((prev) => {
+ newConfig = sync
+ ? newConfig
+ : {
+ ...prev,
+ [theme]: newConfig[theme],
+ layout: newConfig.layout,
+ };
+
+ return newConfig;
+ });
+
+ return newConfig;
+ };
+
+ const setBaseColor = (
+ newConfig: Partial,
+ theme: ThemeType,
+ sync: boolean,
+ ) => {
+ setConfig((prev) =>
+ sync
+ ? {
+ ...prev,
+ light: {
+ ...prev.light,
+ baseColor: {
+ ...prev.light.baseColor,
+ ...newConfig,
+ },
+ },
+ dark: {
+ ...prev.dark,
+ baseColor: {
+ ...prev.dark.baseColor,
+ ...newConfig,
+ },
+ },
+ }
+ : {
+ ...prev,
+ [theme]: {
+ ...prev[theme],
+ baseColor: {
+ ...prev[theme].baseColor,
+ ...newConfig,
+ },
+ },
+ },
+ );
+ };
+
+ const setDefaultColor = (
+ newConfig: Partial,
+ theme: ThemeType,
+ sync: boolean,
+ ) => {
+ setConfig((prev) =>
+ sync
+ ? {
+ ...prev,
+ light: {
+ ...prev.light,
+ defaultColor: {
+ ...prev.light.defaultColor,
+ ...newConfig,
+ },
+ },
+ dark: {
+ ...prev.dark,
+ defaultColor: {
+ ...prev.dark.defaultColor,
+ ...newConfig,
+ },
+ },
+ }
+ : {
+ ...prev,
+ [theme]: {
+ ...prev[theme],
+ defaultColor: {
+ ...prev[theme].defaultColor,
+ ...newConfig,
+ },
+ },
+ },
+ );
+ };
+
+ const setContentColor = (newConfig: Partial, theme: ThemeType) => {
+ setConfig((prev) => ({
+ ...prev,
+ [theme]: {
+ ...prev[theme],
+ contentColor: {
+ ...prev[theme].contentColor,
+ ...newConfig,
+ },
+ },
+ }));
+ };
+
+ const setLayoutColor = (
+ newConfig: Partial,
+ theme: ThemeType,
+ sync: boolean,
+ ) => {
+ setConfig((prev) =>
+ sync
+ ? {
+ ...prev,
+ light: {
+ ...prev.light,
+ otherColor: {
+ ...prev.light.layoutColor,
+ ...newConfig,
+ },
+ },
+ dark: {
+ ...prev.dark,
+ otherColor: {
+ ...prev.dark.layoutColor,
+ ...newConfig,
+ },
+ },
+ }
+ : {
+ ...prev,
+ [theme]: {
+ ...prev[theme],
+ otherColor: {
+ ...prev[theme].layoutColor,
+ ...newConfig,
+ },
+ },
+ },
+ );
+ };
+
+ const setBorderWidth = (newBorderWidths: Partial) =>
+ setConfig((prev) => ({
+ ...prev,
+ layout: {
+ ...prev.layout,
+ borderWidth: {
+ ...prev.layout.borderWidth,
+ ...newBorderWidths,
+ },
+ },
+ }));
+
+ const setLineHeight = (newLineHeights: Partial) =>
+ setConfig((prev) => ({
+ ...prev,
+ layout: {
+ ...prev.layout,
+ lineHeight: {
+ ...prev.layout.lineHeight,
+ ...newLineHeights,
+ },
+ },
+ }));
+
+ const setFontSize = (newFontSizes: Partial) =>
+ setConfig((prev) => ({
+ ...prev,
+ layout: {
+ ...prev.layout,
+ fontSize: {
+ ...prev.layout.fontSize,
+ ...newFontSizes,
+ },
+ },
+ }));
+
+ const setRadius = (newRadius: Partial) =>
+ setConfig((prev) => ({
+ ...prev,
+ layout: {
+ ...prev.layout,
+ radius: {
+ ...prev.layout.radius,
+ ...newRadius,
+ },
+ },
+ }));
+
+ const setOtherParams = (newOtherParams: Partial) =>
+ setConfig((prev) => ({
+ ...prev,
+ layout: {
+ ...prev.layout,
+ otherParams: {
+ ...prev.layout.otherParams,
+ ...newOtherParams,
+ },
+ },
+ }));
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Create a custom hook to use the ThemeBuilderContext
+export function useThemeBuilder(): ThemeBuilderContextProps {
+ const context = useContext(ThemeBuilderContext);
+
+ if (!context) {
+ throw new Error("useThemeBuilder must be used within a ThemeBuilderProvider");
+ }
+
+ return context;
+}
diff --git a/apps/docs/components/themes/templates/coffee.ts b/apps/docs/components/themes/templates/coffee.ts
new file mode 100644
index 0000000000..798fa574c3
--- /dev/null
+++ b/apps/docs/components/themes/templates/coffee.ts
@@ -0,0 +1,56 @@
+import {colors} from "@heroui/theme";
+
+import {initialLayout} from "../constants";
+import {Config} from "../types";
+
+export const coffee: Config = {
+ light: {
+ defaultColor: {
+ default: "#b4afa8",
+ },
+ baseColor: {
+ primary: "#db924b",
+ secondary: "#5a8486",
+ success: "#9db787",
+ warning: "#ffd25f",
+ danger: "#fc9581",
+ },
+ layoutColor: {
+ foreground: "#a27225",
+ background: "#fffbf6",
+ overlay: colors.black,
+ focus: "#db924b",
+ },
+ contentColor: {
+ content1: "#fff2e0",
+ content2: "#ffe9cc",
+ content3: "#ffe0b8",
+ content4: "#ffd7a3",
+ },
+ },
+ dark: {
+ defaultColor: {
+ default: "#413841",
+ },
+ baseColor: {
+ primary: "#db924b",
+ secondary: "#5a8486",
+ success: "#9db787",
+ warning: "#ffd25f",
+ danger: "#fc9581",
+ },
+ layoutColor: {
+ foreground: "#c59f60",
+ background: "#20161F",
+ focus: "#db924b",
+ overlay: colors.white,
+ },
+ contentColor: {
+ content1: "#2c1f2b",
+ content2: "#3e2b3c",
+ content3: "#50374d",
+ content4: "#62435f",
+ },
+ },
+ layout: initialLayout,
+};
diff --git a/apps/docs/components/themes/templates/emerald.ts b/apps/docs/components/themes/templates/emerald.ts
new file mode 100644
index 0000000000..748edb86a3
--- /dev/null
+++ b/apps/docs/components/themes/templates/emerald.ts
@@ -0,0 +1,54 @@
+import {initialDarkTheme, initialLayout, initialLightTheme} from "../constants";
+import {Config} from "../types";
+
+export const emerald: Config = {
+ light: {
+ defaultColor: {
+ default: "#b9c9be",
+ },
+ baseColor: {
+ primary: "#66cc8a",
+ secondary: "#377cfb",
+ success: "#00a96e",
+ warning: "#ffbe00",
+ danger: "#ff5861",
+ },
+ layoutColor: {
+ foreground: "#004c1b",
+ background: "#f6fffa",
+ focus: "#66cc8a",
+ overlay: initialLightTheme.layoutColor.overlay,
+ },
+ contentColor: {
+ content1: "#e0f5e8",
+ content2: "#c2ebd0",
+ content3: "#a3e0b9",
+ content4: "#85d6a1",
+ },
+ },
+ dark: {
+ defaultColor: {
+ default: "#485248",
+ },
+ baseColor: {
+ primary: "#66cc8a",
+ secondary: "#377cfb",
+ success: "#00a96e",
+ warning: "#ffbe00",
+ danger: "#ff5861",
+ },
+ layoutColor: {
+ foreground: "#99d2ad",
+ background: "#010b06",
+ focus: "#66cc8a",
+ overlay: initialDarkTheme.layoutColor.overlay,
+ },
+ contentColor: {
+ content1: "#14291c",
+ content2: "#295237",
+ content3: "#3d7a53",
+ content4: "#52a36e",
+ },
+ },
+ layout: initialLayout,
+};
diff --git a/apps/docs/components/themes/templates/heroui.ts b/apps/docs/components/themes/templates/heroui.ts
new file mode 100644
index 0000000000..7a63f5ee78
--- /dev/null
+++ b/apps/docs/components/themes/templates/heroui.ts
@@ -0,0 +1,8 @@
+import {initialDarkTheme, initialLayout, initialLightTheme} from "../constants";
+import {Config} from "../types";
+
+export const heroui: Config = {
+ light: initialLightTheme,
+ dark: initialDarkTheme,
+ layout: initialLayout,
+};
diff --git a/apps/docs/components/themes/templates/index.ts b/apps/docs/components/themes/templates/index.ts
new file mode 100644
index 0000000000..f1cd48c2e4
--- /dev/null
+++ b/apps/docs/components/themes/templates/index.ts
@@ -0,0 +1,11 @@
+import {Template} from "../types";
+
+import {coffee} from "./coffee";
+import {emerald} from "./emerald";
+import {heroui} from "./heroui";
+
+export const templates: Template[] = [
+ {label: "HeroUI", name: "heroui", value: heroui},
+ {label: "Coffee", name: "coffee", value: coffee},
+ {label: "Emerald", name: "emerald", value: emerald},
+];
diff --git a/apps/docs/components/themes/types.ts b/apps/docs/components/themes/types.ts
new file mode 100644
index 0000000000..504d7c5d90
--- /dev/null
+++ b/apps/docs/components/themes/types.ts
@@ -0,0 +1,129 @@
+// Colors
+export interface ColorShades {
+ 50: string;
+ 100: string;
+ 200: string;
+ 300: string;
+ 400: string;
+ 500: string;
+ 600: string;
+ 700: string;
+ 800: string;
+ 900: string;
+}
+
+export type ColorPickerType =
+ | "background"
+ | "content1"
+ | "content2"
+ | "content3"
+ | "content4"
+ | "danger"
+ | "default"
+ | "divider"
+ | "focus"
+ | "foreground"
+ | "overlay"
+ | "primary"
+ | "secondary"
+ | "success"
+ | "warning";
+
+// HeroUI component props
+export type Color = "default" | "primary" | "secondary" | "success" | "warning" | "danger";
+export type Size = "sm" | "md" | "lg";
+export type Variant =
+ | "dot"
+ | "solid"
+ | "faded"
+ | "bordered"
+ | "light"
+ | "flat"
+ | "ghost"
+ | "shadow"
+ | "underlined";
+export type Radius = "none" | "sm" | "md" | "lg" | "full";
+export type HeroUIScaling = 90 | 95 | 100 | 105 | 110;
+export type Border = "thin" | "medium" | "thick";
+export type FontName = "inter" | "roboto" | "outfit" | "lora";
+
+// Themes
+export type ThemeType = "light" | "dark";
+
+export interface ThemeColor extends ColorShades {
+ foreground: string;
+ DEFAULT: string;
+}
+
+// Configuration
+export interface Config {
+ light: ConfigColors;
+ dark: ConfigColors;
+ layout: ConfigLayout;
+}
+
+export interface ConfigColors {
+ defaultColor: {
+ default: string;
+ };
+ baseColor: {
+ primary: string;
+ secondary: string;
+ success: string;
+ warning: string;
+ danger: string;
+ };
+ layoutColor: {
+ foreground: string;
+ background: string;
+ focus: string;
+ overlay: string;
+ };
+ contentColor: {
+ content1: string;
+ content2: string;
+ content3: string;
+ content4: string;
+ };
+}
+
+export interface ConfigLayout {
+ fontSize: {
+ tiny: string;
+ small: string;
+ medium: string;
+ large: string;
+ };
+ lineHeight: {
+ tiny: string;
+ small: string;
+ medium: string;
+ large: string;
+ };
+ radius: {
+ small: string;
+ medium: string;
+ large: string;
+ };
+ borderWidth: {
+ small: string;
+ medium: string;
+ large: string;
+ };
+ otherParams: {
+ disabledOpacity: string;
+ dividerWeight: string;
+ hoverOpacity: string;
+ };
+}
+
+// Templates
+export interface Template {
+ label: string;
+ name: TemplateType;
+ value: Config;
+}
+
+export type TemplateType = "coffee" | "emerald" | "heroui";
+
+export type FontType = "inter" | "roboto" | "outfit" | "lora";
diff --git a/apps/docs/components/themes/utils/colors.ts b/apps/docs/components/themes/utils/colors.ts
new file mode 100644
index 0000000000..da19cd3b87
--- /dev/null
+++ b/apps/docs/components/themes/utils/colors.ts
@@ -0,0 +1,138 @@
+import {swapColorValues} from "@heroui/theme/src/utils/object";
+import {readableColor} from "color2k";
+import Values from "values.js";
+
+import {ColorShades, ThemeType, ThemeColor, ColorPickerType} from "../types";
+import {colorWeight, defaultDarkColorWeight, defaultLightColorWeight} from "../constants";
+
+/**
+ * Convert color values to RGB
+ */
+export function colorValuesToRgb(value: Values) {
+ return `rgba(${value.rgb.join(", ")}, ${value.alpha})`;
+}
+
+/**
+ * Generate theme color
+ */
+export function generateThemeColor(
+ color: string,
+ type: ColorPickerType,
+ theme: ThemeType,
+): ThemeColor {
+ const values = new Values(color);
+ const colorWeight = getColorWeight(type, theme);
+ const colorValues = values.all(colorWeight);
+ let shades = colorValues.slice(0, colorValues.length - 1).reduce((acc, shadeValue, index) => {
+ (acc as any)[index === 0 ? 50 : index * 100] = rgbToHex(shadeValue.rgb);
+
+ return acc;
+ }, {} as ColorShades);
+
+ return {
+ ...((theme === "light" ? shades : swapColorValues(shades)) as ColorShades),
+ foreground: readableColor(shades[500]),
+ DEFAULT: shades[500],
+ };
+}
+
+/**
+ * Convert hex color to HSL
+ */
+export function hexToHsl(hex: string) {
+ // Convert hex to RGB first
+ const [r, g, b] = hexToRgb(hex);
+
+ // Normalize RGB values
+ const normalizedR = r / 255;
+ const normalizedG = g / 255;
+ const normalizedB = b / 255;
+
+ // Find the maximum and minimum values of R, G, B
+ const max = Math.max(normalizedR, normalizedG, normalizedB);
+ const min = Math.min(normalizedR, normalizedG, normalizedB);
+
+ // Calculate the lightness
+ const lightness = (max + min) / 2;
+
+ // If the maximum and minimum are equal, there is no saturation
+ if (max === min) {
+ return `${0} ${0}% ${lightness * 100}%`;
+ }
+
+ // Calculate the saturation
+ let saturation = 0;
+
+ if (lightness < 0.5) {
+ saturation = (max - min) / (max + min);
+ } else {
+ saturation = (max - min) / (2 - max - min);
+ }
+
+ // Calculate the hue
+ let hue;
+
+ if (max === normalizedR) {
+ hue = (normalizedG - normalizedB) / (max - min);
+ } else if (max === normalizedG) {
+ hue = 2 + (normalizedB - normalizedR) / (max - min);
+ } else {
+ hue = 4 + (normalizedR - normalizedG) / (max - min);
+ }
+
+ hue *= 60;
+ if (hue < 0) hue += 360;
+
+ return `${hue.toFixed(2)} ${(saturation * 100).toFixed(2)}% ${(lightness * 100).toFixed(2)}%`;
+}
+
+/**
+ * Get the color weight
+ */
+export function getColorWeight(colorType: ColorPickerType, theme: ThemeType) {
+ if (colorType === "default") {
+ return theme === "dark" ? defaultDarkColorWeight : defaultLightColorWeight;
+ }
+
+ return colorWeight;
+}
+
+/**
+ * Convert RGB value to hex
+ */
+function rgbValueToHex(c: number) {
+ const hex = c.toString(16);
+
+ return hex.length == 1 ? "0" + hex : hex;
+}
+
+/**
+ * Convert RGB to hex
+ */
+function rgbToHex([r, g, b]: number[]): string {
+ return "#" + rgbValueToHex(r) + rgbValueToHex(g) + rgbValueToHex(b);
+}
+
+/**
+ * Convert hex color to RGB
+ */
+function hexToRgb(hex: string): number[] {
+ // Convert hex to RGB first
+ let r = 0,
+ g = 0,
+ b = 0;
+
+ if (hex.length === 4 || hex.length === 5) {
+ r = parseInt(hex[1] + hex[1], 16);
+ g = parseInt(hex[2] + hex[2], 16);
+ b = parseInt(hex[3] + hex[3], 16);
+ } else if (hex.length === 7 || hex.length === 9) {
+ r = parseInt(hex.slice(1, 3), 16);
+ g = parseInt(hex.slice(3, 5), 16);
+ b = parseInt(hex.slice(5, 7), 16);
+ } else {
+ throw new Error("Invalid hex color format");
+ }
+
+ return [r, g, b];
+}
diff --git a/apps/docs/components/themes/utils/config.ts b/apps/docs/components/themes/utils/config.ts
new file mode 100644
index 0000000000..e049ac27a2
--- /dev/null
+++ b/apps/docs/components/themes/utils/config.ts
@@ -0,0 +1,59 @@
+import {HeroUIPluginConfig} from "@heroui/theme";
+import {readableColor} from "color2k";
+
+import {Config, ThemeType} from "../types";
+
+import {generateThemeColor} from "./colors";
+function generateLayoutConfig(config: Config): HeroUIPluginConfig["layout"] {
+ return {
+ disabledOpacity: config.layout.otherParams.disabledOpacity,
+ };
+}
+
+function generateThemeColorsConfig(config: Config, theme: ThemeType) {
+ return {
+ default: generateThemeColor(config[theme].defaultColor.default, "default", "light"),
+ primary: generateThemeColor(config[theme].baseColor.primary, "primary", "light"),
+ secondary: generateThemeColor(config[theme].baseColor.secondary, "secondary", "light"),
+ success: generateThemeColor(config[theme].baseColor.success, "success", "light"),
+ warning: generateThemeColor(config[theme].baseColor.warning, "warning", "light"),
+ danger: generateThemeColor(config[theme].baseColor.danger, "danger", "light"),
+ background: config[theme].layoutColor.background,
+ foreground: generateThemeColor(config[theme].layoutColor.foreground, "foreground", "light"),
+ content1: {
+ DEFAULT: config[theme].contentColor.content1,
+ foreground: readableColor(config[theme].contentColor.content1),
+ },
+ content2: {
+ DEFAULT: config[theme].contentColor.content2,
+ foreground: readableColor(config[theme].contentColor.content2),
+ },
+ content3: {
+ DEFAULT: config[theme].contentColor.content3,
+ foreground: readableColor(config[theme].contentColor.content3),
+ },
+ content4: {
+ DEFAULT: config[theme].contentColor.content4,
+ foreground: readableColor(config[theme].contentColor.content4),
+ },
+ focus: config[theme].layoutColor.focus,
+ overlay: config[theme].layoutColor.overlay,
+ };
+}
+
+/**
+ * Generate plugin configuration
+ */
+export function generatePluginConfig(config: Config): HeroUIPluginConfig {
+ return {
+ themes: {
+ light: {
+ colors: generateThemeColorsConfig(config, "light"),
+ },
+ dark: {
+ colors: generateThemeColorsConfig(config, "dark"),
+ },
+ },
+ layout: generateLayoutConfig(config),
+ };
+}
diff --git a/apps/docs/components/themes/utils/shared.ts b/apps/docs/components/themes/utils/shared.ts
new file mode 100644
index 0000000000..e4b2b14997
--- /dev/null
+++ b/apps/docs/components/themes/utils/shared.ts
@@ -0,0 +1,30 @@
+import {Border} from "../types";
+
+/**
+ * Copy data to clipboard
+ * @param data
+ */
+export function copyData(data: string) {
+ navigator.clipboard.writeText(data);
+}
+
+/**
+ * Stringify data
+ *
+ * @param data
+ * @returns
+ */
+export function stringifyData(data: unknown) {
+ return JSON.stringify(data, null, 2);
+}
+
+export function getBorderWidth(data: Border) {
+ if (data === "thin") {
+ return 1;
+ }
+ if (data === "medium") {
+ return 2;
+ }
+
+ return 4;
+}
diff --git a/apps/docs/hooks/use-previous.ts b/apps/docs/hooks/use-previous.ts
new file mode 100644
index 0000000000..0309125961
--- /dev/null
+++ b/apps/docs/hooks/use-previous.ts
@@ -0,0 +1,18 @@
+import {useEffect, useRef} from "react";
+
+/**
+ * Holds the previous value of the provided [value] parameter
+ *
+ * @param value
+ * @returns
+ */
+function usePrevious(value: T) {
+ const ref = useRef();
+
+ useEffect(() => {
+ ref.current = value;
+ }, [value]);
+
+ return ref.current;
+}
+export default usePrevious;
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 5b5bee021e..a762ca0cd9 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -101,7 +101,9 @@
"unified": "^11.0.5",
"unist-util-visit": "5.0.0",
"usehooks-ts": "3.1.0",
- "zustand": "5.0.1"
+ "zustand": "5.0.1",
+ "react-colorful": "^5.6.1",
+ "values.js": "^2.1.1"
},
"devDependencies": {
"@docusaurus/utils": "2.0.0-beta.3",
diff --git a/apps/docs/tailwind.config.js b/apps/docs/tailwind.config.js
index 9668d14742..7485609481 100644
--- a/apps/docs/tailwind.config.js
+++ b/apps/docs/tailwind.config.js
@@ -345,7 +345,15 @@ module.exports = {
maxWidth: {
"8xl": "90rem", // 1440px
},
- },
+ utilities: {
+ '.scrollbar-hide': {
+ 'scrollbar-width': 'none',
+ '&::-webkit-scrollbar': {
+ display: 'none',
+ },
+ }
+ },
+ }
},
plugins: [
heroui({
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f8f7af6282..32458dfcf4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -457,6 +457,9 @@ importers:
react:
specifier: 18.2.0
version: 18.2.0
+ react-colorful:
+ specifier: ^5.6.1
+ version: 5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
@@ -523,6 +526,9 @@ importers:
usehooks-ts:
specifier: 3.1.0
version: 3.1.0(react@18.2.0)
+ values.js:
+ specifier: ^2.1.1
+ version: 2.1.1
zustand:
specifier: 5.0.1
version: 5.0.1(@types/react@18.2.8)(react@18.2.0)(use-sync-external-store@1.4.0(react@18.2.0))
@@ -10798,6 +10804,10 @@ packages:
header-case@2.0.4:
resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==}
+ hex-rgb@4.3.0:
+ resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==}
+ engines: {node: '>=6'}
+
homedir-polyfill@1.0.3:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
engines: {node: '>=0.10.0'}
@@ -12373,6 +12383,9 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
+ mix-css-color@0.2.0:
+ resolution: {integrity: sha512-mZugANySFPE21tjELbQddhC6HAZNzqp7gDxmW8fJFURSWtJ0nuXU26dyrb/1AR6ZYxdEAtW2bbWT9QnRtI6Jzg==}
+
mixin-deep@1.3.2:
resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==}
engines: {node: '>=0.10.0'}
@@ -12853,6 +12866,12 @@ packages:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse-css-color@0.1.2:
+ resolution: {integrity: sha512-z7v/tf0edGsnlm9VONQtH+u/YVrdUqZXrSBzqM13scef8Abl2VyZfYsZaJoyb/AyY4SIxtoJChSQ4MURHfY3Sg==}
+
+ parse-css-color@0.2.0:
+ resolution: {integrity: sha512-uWQyuOe+SMxnUgHf4mjdn2C/YzA1tOW+uU8Z2UiV3qnao9ZFnvYeyzeoU7TNv8NLIJo0PiRkETW48QNJZ4IA9g==}
+
parse-entities@2.0.0:
resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==}
@@ -13252,6 +13271,9 @@ packages:
resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==}
engines: {node: '>=12.20'}
+ pure-color@1.3.0:
+ resolution: {integrity: sha512-QFADYnsVoBMw1srW7OVKEYjG+MbIa49s54w1MA1EDY6r2r/sTcKKYqRX1f4GYvnXP7eN/Pe9HFcX+hwzmrXRHA==}
+
pure-rand@6.1.0:
resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==}
@@ -13296,6 +13318,12 @@ packages:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true
+ react-colorful@5.6.1:
+ resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
+ peerDependencies:
+ react: 18.2.0
+ react-dom: 18.2.0
+
react-devtools-inline@4.4.0:
resolution: {integrity: sha512-ES0GolSrKO8wsKbsEkVeiR/ZAaHQTY4zDh1UW8DImVmm8oaGLl3ijJDvSGe+qDRKPZdPRnDtWWnSvvrgxXdThQ==}
@@ -14971,6 +14999,9 @@ packages:
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+ values.js@2.1.1:
+ resolution: {integrity: sha512-pI6dKW3kv7BR/WzS0NvIuxegeH1r8gk8y6BuXrIYGVccynmUsUZPhSpTfkt39VBHyciD7WZi+lM+7Zyamowzeg==}
+
vfile-location@5.0.3:
resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==}
@@ -24002,6 +24033,8 @@ snapshots:
capital-case: 1.0.4
tslib: 2.8.1
+ hex-rgb@4.3.0: {}
+
homedir-polyfill@1.0.3:
dependencies:
parse-passwd: 1.0.0
@@ -26093,6 +26126,11 @@ snapshots:
mitt@3.0.1: {}
+ mix-css-color@0.2.0:
+ dependencies:
+ parse-css-color: 0.1.2
+ pure-color: 1.3.0
+
mixin-deep@1.3.2:
dependencies:
for-in: 1.0.2
@@ -26745,6 +26783,16 @@ snapshots:
dependencies:
callsites: 3.1.0
+ parse-css-color@0.1.2:
+ dependencies:
+ color-name: 1.1.4
+ hex-rgb: 4.3.0
+
+ parse-css-color@0.2.0:
+ dependencies:
+ color-name: 1.1.4
+ hex-rgb: 4.3.0
+
parse-entities@2.0.0:
dependencies:
character-entities: 1.2.4
@@ -27187,6 +27235,8 @@ snapshots:
dependencies:
escape-goat: 4.0.0
+ pure-color@1.3.0: {}
+
pure-rand@6.1.0: {}
querystring@0.2.0: {}
@@ -27229,6 +27279,11 @@ snapshots:
minimist: 1.2.8
strip-json-comments: 2.0.1
+ react-colorful@5.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+
react-devtools-inline@4.4.0:
dependencies:
es6-symbol: 3.1.4
@@ -29308,6 +29363,12 @@ snapshots:
validate-npm-package-name@5.0.1: {}
+ values.js@2.1.1:
+ dependencies:
+ mix-css-color: 0.2.0
+ parse-css-color: 0.2.0
+ pure-color: 1.3.0
+
vfile-location@5.0.3:
dependencies:
'@types/unist': 3.0.3