diff --git a/apps/docs/app/themes/page.tsx b/apps/docs/app/themes/page.tsx new file mode 100644 index 0000000000..17cd5544c5 --- /dev/null +++ b/apps/docs/app/themes/page.tsx @@ -0,0 +1,9 @@ +import {ThemeBuilder} from "@/components/themes"; + +export default function ThemesPage() { + return ( +
+ +
+ ); +} diff --git a/apps/docs/components/navbar.tsx b/apps/docs/components/navbar.tsx index 816f0cbc0a..599f9ee0c6 100644 --- a/apps/docs/components/navbar.tsx +++ b/apps/docs/components/navbar.tsx @@ -235,6 +235,17 @@ export const Navbar: FC = ({children, routes, mobileRoutes = [], sl > Figma + {" "} + + handlePressNavbarItem("Themes", "/themes")} + > + Themes + {/* hide feedback and changelog at this moment */} {/* diff --git a/apps/docs/components/themes/components/color-picker.tsx b/apps/docs/components/themes/components/color-picker.tsx new file mode 100644 index 0000000000..946dc336e3 --- /dev/null +++ b/apps/docs/components/themes/components/color-picker.tsx @@ -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 ( +
+ onClose(selectedColor)} + onOpenChange={setIsOpen} + > + + + + +
+
+ {selectedColorValues + ?.slice(0, selectedColorValues.length - 1) + .map((colorValue, index: number) => ( +
+
+ {index === 0 ? 50 : index * 100} +
+ ))} +
+ + +
+ + + +
+ ); +} + +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; + } +} diff --git a/apps/docs/components/themes/components/config-section.tsx b/apps/docs/components/themes/components/config-section.tsx new file mode 100644 index 0000000000..8822244fc3 --- /dev/null +++ b/apps/docs/components/themes/components/config-section.tsx @@ -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 ( +
+ {title} +
+ {children} +
+
+ ); +} diff --git a/apps/docs/components/themes/components/configuration/actions.tsx b/apps/docs/components/themes/components/configuration/actions.tsx new file mode 100644 index 0000000000..43b42efb4b --- /dev/null +++ b/apps/docs/components/themes/components/configuration/actions.tsx @@ -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 ( +
+ + + + + + + + + +
+ ); +} diff --git a/apps/docs/components/themes/components/configuration/base-colors.tsx b/apps/docs/components/themes/components/configuration/base-colors.tsx new file mode 100644 index 0000000000..c09c14a197 --- /dev/null +++ b/apps/docs/components/themes/components/configuration/base-colors.tsx @@ -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 ( + + setCssBackground(hexColor)} + onClose={(hexColor) => setBaseColor({background: hexColor}, theme)} + onCopy={(theme) => copyBaseColorConfig(config, "background", theme)} + /> + setCssColor("foreground", hexColor, theme)} + onClose={(hexColor) => setBaseColor({foreground: hexColor}, theme)} + onCopy={(theme) => copyBaseColorConfig(config, "foreground", theme)} + /> + setCssContentColor(1, hexColor)} + onClose={(hexColor) => setBaseColor({content1: hexColor}, theme)} + onCopy={(theme) => copyBaseColorConfig(config, "content1", theme)} + /> + setCssContentColor(2, hexColor)} + onClose={(hexColor) => setBaseColor({content2: hexColor}, theme)} + onCopy={(theme) => copyBaseColorConfig(config, "content2", theme)} + /> + setCssContentColor(3, hexColor)} + onClose={(hexColor) => setBaseColor({content3: hexColor}, theme)} + onCopy={(theme) => copyBaseColorConfig(config, "content3", theme)} + /> + setCssContentColor(4, hexColor)} + onClose={(hexColor) => setBaseColor({content4: hexColor}, theme)} + onCopy={(theme) => copyBaseColorConfig(config, "content4", theme)} + /> + + ); +} diff --git a/apps/docs/components/themes/components/configuration/border-widths.tsx b/apps/docs/components/themes/components/configuration/border-widths.tsx new file mode 100644 index 0000000000..eccd819872 --- /dev/null +++ b/apps/docs/components/themes/components/configuration/border-widths.tsx @@ -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 ( + + handleChange("small", value)} + /> + handleChange("medium", value)} + /> + handleChange("large", value)} + /> + + ); +} diff --git a/apps/docs/components/themes/components/configuration/brand-colors.tsx b/apps/docs/components/themes/components/configuration/brand-colors.tsx new file mode 100644 index 0000000000..b5f1371e76 --- /dev/null +++ b/apps/docs/components/themes/components/configuration/brand-colors.tsx @@ -0,0 +1,76 @@ +import {colorsId} from "../../constants"; +import {setCssColor} from "../../css-vars"; +import {useThemeBuilder} from "../../provider"; +import {Config, ThemeType} from "../../types"; +import {copyBrandColorConfig} from "../../utils/config"; +import {ColorPicker} from "../color-picker"; +import {ConfigSection} from "../config-section"; + +interface BrandColorsProps { + config: Config; + syncIcon: React.ReactNode; + syncThemes: boolean; + theme: ThemeType; +} + +export function BrandColors({config, syncIcon, syncThemes, theme}: BrandColorsProps) { + const {setBrandColor} = useThemeBuilder(); + + return ( + + setCssColor("default", hexColor, theme)} + onClose={(hexColor) => setBrandColor({default: hexColor}, theme, false)} + onCopy={(theme) => copyBrandColorConfig(config, "default", theme)} + /> + setCssColor("primary", hexColor, theme)} + onClose={(hexColor) => setBrandColor({primary: hexColor}, theme, syncThemes)} + onCopy={(theme) => copyBrandColorConfig(config, "primary", theme)} + /> + setCssColor("secondary", hexColor, theme)} + onClose={(hexColor) => setBrandColor({secondary: hexColor}, theme, syncThemes)} + onCopy={(theme) => copyBrandColorConfig(config, "secondary", theme)} + /> + setCssColor("success", hexColor, theme)} + onClose={(hexColor) => setBrandColor({success: hexColor}, theme, syncThemes)} + onCopy={(theme) => copyBrandColorConfig(config, "success", theme)} + /> + setCssColor("warning", hexColor, theme)} + onClose={(hexColor) => setBrandColor({warning: hexColor}, theme, syncThemes)} + onCopy={(theme) => copyBrandColorConfig(config, "warning", theme)} + /> + setCssColor("danger", hexColor, theme)} + onClose={(hexColor) => setBrandColor({danger: hexColor}, theme, syncThemes)} + onCopy={(theme) => copyBrandColorConfig(config, "danger", theme)} + /> + + ); +} diff --git a/apps/docs/components/themes/components/configuration/font-sizes.tsx b/apps/docs/components/themes/components/configuration/font-sizes.tsx new file mode 100644 index 0000000000..ad472d8168 --- /dev/null +++ b/apps/docs/components/themes/components/configuration/font-sizes.tsx @@ -0,0 +1,41 @@ +import {setCssFontSize} from "../../css-vars"; +import {useThemeBuilder} from "../../provider"; +import {Config, ConfigLayout} from "../../types"; +import {ConfigSection} from "../config-section"; +import {NumberInput} from "../number-input"; + +interface FontSizesProps { + config: Config; +} + +export function FontSizes({config}: FontSizesProps) { + return ( + + + + + + + ); +} + +interface FontSizeInputProps { + label: string; + type: keyof ConfigLayout["fontSize"]; + value: string; +} + +function FontSizeInput({label, type, value}: FontSizeInputProps) { + const {setFontSize} = useThemeBuilder(); + + return ( + { + setFontSize({[type]: value}); + setCssFontSize(type, value); + }} + /> + ); +} diff --git a/apps/docs/components/themes/components/configuration/index.tsx b/apps/docs/components/themes/components/configuration/index.tsx new file mode 100644 index 0000000000..22b0643a36 --- /dev/null +++ b/apps/docs/components/themes/components/configuration/index.tsx @@ -0,0 +1,127 @@ +import {useEffect, useState} from "react"; +import {Card, CardBody, Switch} from "@nextui-org/react"; +import {useTheme} from "next-themes"; +import {useLocalStorage} from "usehooks-ts"; +import {Icon} from "@iconify/react/dist/offline"; +import LinkSquareIcon from "@iconify/icons-solar/link-square-linear"; + +import {useThemeBuilder} from "../../provider"; +import {Config, Template, ThemeType} from "../../types"; +import {configKey, syncThemesKey, initialConfig} from "../../constants"; +import {SelectTemplate} from "../select-template"; +import {generatePluginConfig} from "../../utils/config"; +import {setAllCssVars} from "../../css-vars"; + +import {BrandColors} from "./brand-colors"; +import {BaseColors} from "./base-colors"; +import {OtherColors} from "./other-colors"; +import {FontSizes} from "./font-sizes"; +import {LineHeights} from "./line-heights"; +import {Radiuses} from "./radiuses"; +import {BorderWidths} from "./border-widths"; +import {Other} from "./other"; +import {Actions} from "./actions"; + +import usePrevious from "@/hooks/use-previous"; + +export default function Configuration() { + const [selectedTemplate, setSelectedTemplate] = useState