Skip to content
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

docs(themes): add theme generator #3707

Closed
wants to merge 15 commits into from
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
9 changes: 9 additions & 0 deletions apps/docs/app/themes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {ThemeBuilder} from "@/components/themes";

export default function ThemesPage() {
return (
<div className="flex flex-col md:flex-row gap-6 max-w-7xl mx-auto mt-12">
<ThemeBuilder />
</div>
);
}
11 changes: 11 additions & 0 deletions apps/docs/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,17 @@ export const Navbar: FC<NavbarProps> = ({children, routes, mobileRoutes = [], sl
>
Figma
</NextLink>
</NavbarItem>{" "}
<NavbarItem>
<NextLink
className={navLinkClasses}
color="foreground"
data-active={pathname.includes("themes")}
href="/themes"
onClick={() => handlePressNavbarItem("Themes", "/themes")}
>
Themes
</NextLink>
</NavbarItem>
{/* hide feedback and changelog at this moment */}
{/* <NavbarItem>
Expand Down
142 changes: 142 additions & 0 deletions apps/docs/components/themes/components/color-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import {useEffect, useState} from "react";
import {Button, Popover, PopoverContent, PopoverTrigger} from "@nextui-org/react";
import {HexColorInput, HexColorPicker} from "react-colorful";
import Values from "values.js";
import {readableColor} from "color2k";
import waterDrop from "@iconify/icons-solar/waterdrop-linear";
import {Icon} from "@iconify/react/dist/offline";
import {useTheme} from "next-themes";
import {clsx} from "@nextui-org/shared-utils";

import {ColorPickerType, ThemeType} from "../types";
import {colorValuesToRgb, getColorWeight} from "../utils/colors";

import {CopyButton} from "./copy-button";

interface ColorPickerProps {
hexColor: string;
icon?: React.ReactNode;
label: string;
type: ColorPickerType;
onChange: (hexColor: string) => void;
onClose: (hexColor: string) => void;
onCopy: (theme: ThemeType) => void;
}

export function ColorPicker({
hexColor,
icon,
label,
type,
onChange,
onClose,
onCopy,
}: ColorPickerProps) {
const [selectedColor, setSelectedColor] = useState(hexColor);

const [isOpen, setIsOpen] = useState(false);
const theme = useTheme().theme as ThemeType;
const selectedColorWeight = getColorWeight(type, theme);
const selectedColorValues = new Values(selectedColor).all(selectedColorWeight);

function handleChange(updatedHexColor: string) {
onChange(updatedHexColor);
setSelectedColor(updatedHexColor);
}

/**
* Update the selected color when the popover is opened.
*/
useEffect(() => {
setSelectedColor(hexColor);
}, [hexColor, isOpen]);

return (
<div className="flex">
<Popover
isOpen={isOpen}
placement="bottom"
onClose={() => onClose(selectedColor)}
onOpenChange={setIsOpen}
>
<PopoverTrigger>
<Button
fullWidth
className={clsx(getColor(type), "rounded-r-none")}
size="sm"
style={{
color: ["background", "foreground", "focus", "overlay"].includes(type)
? readableColor(selectedColor)
: undefined,
}}
>
<Icon className="text-lg" icon={waterDrop} />
{label} {icon}
</Button>
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col gap-2 max-w-48 my-2">
<div className="grid grid-cols-5 gap-2">
{selectedColorValues
?.slice(0, selectedColorValues.length - 1)
.map((colorValue, index: number) => (
<div key={index} className="flex flex-col items-center">
<div
className="h-6 w-6 rounded"
style={{backgroundColor: colorValuesToRgb(colorValue)}}
/>
<span className="text-xs mt-1">{index === 0 ? 50 : index * 100}</span>
</div>
))}
</div>
<HexColorPicker className="!w-full" color={selectedColor} onChange={handleChange} />
<HexColorInput
prefixed
className="px-2 py-1 w-full rounded-md"
color={selectedColor}
onChange={handleChange}
/>
</div>
</PopoverContent>
</Popover>
<CopyButton className="rounded-l-none" size="sm" variant="flat" onCopy={onCopy} />
</div>
);
}

function getColor(type: ColorPickerType) {
switch (type) {
case "primary":
return "bg-primary text-primary-foreground";
case "secondary":
return "bg-secondary text-secondary-foreground";
case "success":
return "bg-success text-success-foreground";
case "warning":
return "bg-warning text-warning-foreground";
case "danger":
return "bg-danger text-danger-foreground";
case "background":
return "bg-background text-foreground";
case "foreground":
return "bg-foreground text-black";
case "default":
return "bg-default";
case "content1":
return "bg-content1 text-content1-foreground";
case "content2":
return "bg-content2 text-content2-foreground";
case "content3":
return "bg-content3 text-content3-foreground";
case "content4":
return "bg-content4 text-content4-foreground";
case "divider":
return "bg-divider";
case "focus":
return "bg-focus";
case "overlay":
return "bg-overlay";
default:
return undefined;
}
}
24 changes: 24 additions & 0 deletions apps/docs/components/themes/components/config-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {clsx} from "@nextui-org/shared-utils";

interface ConfigurationSectionProps {
children: React.ReactNode;
cols?: number;
id?: string;
title: string;
}

export function ConfigSection({children, cols = 2, id, title}: ConfigurationSectionProps) {
return (
<div id={id}>
<span className="font-semibold">{title}</span>
<div
className={clsx("grid flex-wrap gap-4 mt-2", {
"grid-cols-2": cols === 2,
"grid-cols-3": cols === 3,
})}
>
{children}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {useState} from "react";
import {Button, Tooltip} from "@nextui-org/react";
import {Icon} from "@iconify/react/dist/offline";
import SunIcon from "@iconify/icons-solar/sun-linear";
import MoonIcon from "@iconify/icons-solar/moon-linear";
import CopyIcon from "@iconify/icons-solar/copy-linear";
import UndoLeftIcon from "@iconify/icons-solar/undo-left-linear";
import CheckCircleIcon from "@iconify/icons-solar/check-circle-linear";

import {ThemeType} from "../../types";

interface ActionsProps {
theme: ThemeType;
onCopy: () => unknown;
onResetTheme: () => void;
onToggleTheme: () => void;
}

export function Actions({theme, onCopy, onResetTheme, onToggleTheme}: ActionsProps) {
const [copied, setCopied] = useState(false);
const isLight = theme === "light";

/**
* Handle the copying of the configuration.
*/
function handleCopyConfig() {
navigator.clipboard.writeText(JSON.stringify(onCopy(), null, 2));

setCopied(true);
setTimeout(() => setCopied(false), 1500);
}

return (
<div className="flex gap-2">
<Tooltip content={isLight ? "Dark" : "Light"}>
<Button isIconOnly color="secondary" size="sm" variant="flat" onClick={onToggleTheme}>
{isLight ? (
<Icon className="text-lg" icon={MoonIcon} />
) : (
<Icon className="text-lg" icon={SunIcon} />
)}
</Button>
</Tooltip>
<Tooltip content="Reset theme">
<Button isIconOnly color="secondary" size="sm" variant="flat" onClick={onResetTheme}>
<Icon className="text-lg" icon={UndoLeftIcon} />
</Button>
</Tooltip>
<Tooltip content="Copy configuration">
<Button isIconOnly color="secondary" size="sm" variant="flat" onClick={handleCopyConfig}>
{copied ? (
<Icon className="text-lg" icon={CheckCircleIcon} />
) : (
<Icon className="text-lg" icon={CopyIcon} />
)}
</Button>
</Tooltip>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {baseColorsId} from "../../constants";
import {setCssBackground, setCssColor, setCssContentColor} from "../../css-vars";
import {useThemeBuilder} from "../../provider";
import {Config, ThemeType} from "../../types";
import {copyBaseColorConfig} from "../../utils/config";
import {ColorPicker} from "../color-picker";
import {ConfigSection} from "../config-section";

interface BaseColorsProps {
config: Config;
theme: ThemeType;
}

export function BaseColors({config, theme}: BaseColorsProps) {
const {setBaseColor} = useThemeBuilder();

return (
<ConfigSection id={baseColorsId} title="Base colors">
<ColorPicker
hexColor={config[theme].baseColor.background}
label="Background"
type="background"
onChange={(hexColor) => setCssBackground(hexColor)}
onClose={(hexColor) => setBaseColor({background: hexColor}, theme)}
onCopy={(theme) => copyBaseColorConfig(config, "background", theme)}
/>
<ColorPicker
hexColor={config[theme].baseColor.foreground}
label="Foreground"
type="foreground"
onChange={(hexColor) => setCssColor("foreground", hexColor, theme)}
onClose={(hexColor) => setBaseColor({foreground: hexColor}, theme)}
onCopy={(theme) => copyBaseColorConfig(config, "foreground", theme)}
/>
<ColorPicker
hexColor={config[theme].baseColor.content1}
label="Content 1"
type="content1"
onChange={(hexColor) => setCssContentColor(1, hexColor)}
onClose={(hexColor) => setBaseColor({content1: hexColor}, theme)}
onCopy={(theme) => copyBaseColorConfig(config, "content1", theme)}
/>
<ColorPicker
hexColor={config[theme].baseColor.content2}
label="Content 2"
type="content2"
onChange={(hexColor) => setCssContentColor(2, hexColor)}
onClose={(hexColor) => setBaseColor({content2: hexColor}, theme)}
onCopy={(theme) => copyBaseColorConfig(config, "content2", theme)}
/>
<ColorPicker
hexColor={config[theme].baseColor.content3}
label="Content 3"
type="content3"
onChange={(hexColor) => setCssContentColor(3, hexColor)}
onClose={(hexColor) => setBaseColor({content3: hexColor}, theme)}
onCopy={(theme) => copyBaseColorConfig(config, "content3", theme)}
/>
<ColorPicker
hexColor={config[theme].baseColor.content4}
label="Content 4"
type="content4"
onChange={(hexColor) => setCssContentColor(4, hexColor)}
onClose={(hexColor) => setBaseColor({content4: hexColor}, theme)}
onCopy={(theme) => copyBaseColorConfig(config, "content4", theme)}
/>
</ConfigSection>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {setCssBorderWidth} from "../../css-vars";
import {useThemeBuilder} from "../../provider";
import {Config} from "../../types";
import {ConfigSection} from "../config-section";
import {NumberInput} from "../number-input";

interface BorderWidthsProps {
config: Config;
}

export function BorderWidths({config}: BorderWidthsProps) {
const {setBorderWidth} = useThemeBuilder();

const handleChange = (key: keyof Config["layout"]["borderWidth"], value: string) => {
setBorderWidth({[key]: value});
setCssBorderWidth(key, value);
};

return (
<ConfigSection cols={3} title="Border width (px)">
<NumberInput
label="Small"
value={config.layout.borderWidth.small}
onChange={(value) => handleChange("small", value)}
/>
<NumberInput
label="Medium"
value={config.layout.borderWidth.medium}
onChange={(value) => handleChange("medium", value)}
/>
<NumberInput
label="Large"
value={config.layout.borderWidth.large}
onChange={(value) => handleChange("large", value)}
/>
</ConfigSection>
);
}
Loading