Skip to content

Commit

Permalink
Feat: theme builder options config (#2955)
Browse files Browse the repository at this point in the history
  • Loading branch information
ramenhog authored Nov 7, 2024
1 parent 3f41ff5 commit 442e1a5
Show file tree
Hide file tree
Showing 11 changed files with 293 additions and 127 deletions.
55 changes: 55 additions & 0 deletions demo/ts/components/theme-builder/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from "react";
import clsx from "clsx";
import { FaChevronDown } from "react-icons/fa";

type AccordionProps = {
id: string;
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
};

const Accordion = ({
id,
title,
children,
defaultOpen = false,
}: AccordionProps) => {
const [isOpen, setIsOpen] = React.useState(defaultOpen);

const toggleAccordion = () => {
setIsOpen(!isOpen);
};

return (
<div id={id} className="group">
<h2 id={`${id}-heading`}>
<button
type="button"
className={clsx(
"flex items-center justify-between w-full px-5 py-3 text-sm font-bold rtl:text-right text-gray-500 border border-b-0 border-gray-200 gap-3 group-last:border-b",
{ "group-last:border-b-0": isOpen },
)}
aria-expanded="true"
aria-controls={`${id}-body`}
onClick={toggleAccordion}
>
<span>{title}</span>
<FaChevronDown
className={clsx("w-3 h-3 shrink-0", { "rotate-180 ": isOpen })}
/>
</button>
</h2>
<div
id={`${id}-body`}
className={isOpen ? "block" : "hidden"}
aria-labelledby={`${id}-heading`}
>
<div className="p-5 border border-b-0 border-gray-200 group-last:border-b">
{children}
</div>
</div>
</div>
);
};
export default Accordion;
10 changes: 5 additions & 5 deletions demo/ts/components/theme-builder/color-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type ColorPickerProps = {
label?: string;
color: string;
id: string;
onColorChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onColorChange: (color: string) => void;
showColorName?: boolean;
};

Expand All @@ -21,7 +21,7 @@ const ColorPicker = ({

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (onColorChange) {
onColorChange(event);
onColorChange(event.target.value);
}
};

Expand All @@ -33,7 +33,7 @@ const ColorPicker = ({
</label>
)}
<div
className={clsx("relative inline-flex rounded-full group", {
className={clsx("relative inline-flex rounded-full group/swatch", {
"border-2 border-gray-200 p-0.5 cursor-pointer justify-between bg-gray-100":
showColorName,
})}
Expand All @@ -55,7 +55,7 @@ const ColorPicker = ({
/>
{!showColorName && (
<div
className={`absolute top-0 left-0 w-full h-full text-white flex justify-center items-center text-xl rounded-full opacity-0 group-hover:opacity-100 ${
className={`absolute top-0 left-0 w-full h-full text-white flex justify-center items-center text-xl rounded-full opacity-0 group-hover/swatch:opacity-100 ${
isPickerOpen ? "opacity-100" : ""
}`}
>
Expand All @@ -82,7 +82,7 @@ const ColorPicker = ({
)}
<input
id={id}
className={`absolute top-0 left-0 w-full h-full cursor-pointer opacity-0 z-10 group-hover:border-currentColor ${
className={`absolute top-0 left-0 w-full h-full cursor-pointer opacity-0 z-10 group-hover/swatch:border-currentColor ${
isPickerOpen ? "border-currentColor" : ""
}`}
type="color"
Expand Down
9 changes: 5 additions & 4 deletions demo/ts/components/theme-builder/color-scale-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Select from "./select";
import ColorPicker from "./color-picker";

export type ColorChangeArgs = {
event: React.ChangeEvent<HTMLInputElement>;
newColor: string;
index: number;
colorScale: string;
};
Expand All @@ -13,7 +13,7 @@ type ColorScaleOptionsProps = {
palette?: VictoryThemeDefinition["palette"];
activeColorScale?: ColorScalePropType;
onColorChange: (args: ColorChangeArgs) => void;
onColorScaleChange: (event: React.ChangeEvent<HTMLSelectElement>) => void;
onColorScaleChange: (colorScale: string) => void;
};

const colorScales = [
Expand Down Expand Up @@ -57,16 +57,17 @@ const ColorScaleOptions = ({
onChange={onColorScaleChange}
options={colorScales}
label="Color Scale"
className="mb-5"
/>
<div className="flex flex-wrap gap-3 mb-5">
{palette?.[activeColorScale as string]?.map((color, i) => (
<ColorPicker
key={i}
color={color}
id={`color-${i}`}
onColorChange={(event) =>
onColorChange={(newColor) =>
onColorChange({
event,
newColor,
index: i,
colorScale: activeColorScale as string,
})
Expand Down
95 changes: 95 additions & 0 deletions demo/ts/components/theme-builder/config-mapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from "react";
import optionsConfig from "./options-config";
import Accordion from "./accordion";
import Select from "./select";
import Slider from "./slider";
import ColorPicker from "./color-picker";
import ColorScaleOptions from "./color-scale-options";
import { getConfigValue } from "./utils";

const ConfigMapper = ({
themeConfig,
activeColorScale,
updateThemeConfig,
handleColorScaleChange,
}) => {
const handleColorChange = ({ newColor, index, colorScale }) => {
const updatedColors = themeConfig?.palette?.[colorScale]?.map((color, i) =>
i === index ? newColor : color,
);
updateThemeConfig(`palette.${colorScale}`, updatedColors);
};

return (
<>
{optionsConfig.map((section, index) => (
<Accordion
key={section.title}
title={section.title}
id={section.title}
defaultOpen={index === 0}
>
{section.fields.map((field) => {
if (field.type === "colorScale") {
return (
<ColorScaleOptions
key={field.label}
activeColorScale={activeColorScale}
palette={themeConfig?.palette}
onColorChange={handleColorChange}
onColorScaleChange={handleColorScaleChange}
/>
);
}
const configValue = getConfigValue(themeConfig, field.path);
if (field.type === "slider") {
return (
<Slider
id={field.label}
key={field.label}
label={field.label}
value={configValue as number}
unit={field.unit}
onChange={(newValue) =>
updateThemeConfig(field.path, newValue)
}
/>
);
}
if (field.type === "select") {
return (
<Select
id={field.label}
key={field.label}
label={field.label}
value={configValue as string}
options={field.options}
onChange={(newValue) =>
updateThemeConfig(field.path, newValue)
}
/>
);
}
if (field.type === "colorPicker") {
return (
<ColorPicker
id={field.label}
key={field.label}
label={field.label}
color={configValue as string}
onColorChange={(newColor) =>
updateThemeConfig(field.path, newColor)
}
showColorName
/>
);
}
return null;
})}
</Accordion>
))}
</>
);
};

export default ConfigMapper;
2 changes: 1 addition & 1 deletion demo/ts/components/theme-builder/config-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { VictoryThemeDefinition } from "victory-core/lib";
import { VictoryThemeDefinition } from "victory-core";
import Button from "./button";
import { Prism, SyntaxHighlighterProps } from "react-syntax-highlighter";

Expand Down
73 changes: 20 additions & 53 deletions demo/ts/components/theme-builder/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React from "react";
import "./tailwind.css";

import {
ColorScalePropType,
LabelProps,
VictoryTheme,
VictoryThemeDefinition,
} from "victory-core";
Expand All @@ -10,13 +11,11 @@ import { VictoryAxis } from "victory-axis";
import { VictoryStack } from "victory-stack";
import { VictoryBar } from "victory-bar";
import { VictoryArea } from "victory-area";
import ColorScaleOptions, { ColorChangeArgs } from "./color-scale-options";
import Select from "./select";
import ConfigPreview from "./config-preview";
import Button from "./button";

import "./tailwind.css";
import LabelOptions from "./label-options";
import ConfigMapper from "./config-mapper";
import { setNestedConfigValue } from "./utils";

export type ThemeOption = {
name: string;
Expand Down Expand Up @@ -83,50 +82,24 @@ const ThemeBuilder = () => {
const [showThemeConfigPreview, setShowThemeConfigPreview] =
React.useState(false);

const handleThemeSelect = (event: React.ChangeEvent<HTMLSelectElement>) => {
const themeName = event.target.value;
const handleThemeSelect = (themeName: string) => {
const theme = themes.find((t) => t.name === themeName);
setBaseTheme(theme);
setCustomThemeConfig({ ...theme?.config });
};

const handleLabelConfigChange = (newLabelConfig: Partial<LabelProps>) => {
if (customThemeConfig) {
const updatedConfig = {
...customThemeConfig,
axis: {
...customThemeConfig.axis,
style: {
...customThemeConfig.axis?.style,
axisLabel: {
...customThemeConfig.axis?.style?.axisLabel,
...newLabelConfig,
},
},
},
};
setCustomThemeConfig(updatedConfig as VictoryThemeDefinition);
}
};

const handleColorChange = ({ event, index, colorScale }: ColorChangeArgs) => {
const newColor = event.target.value;
const updatedConfig = {
...customThemeConfig,
palette: {
...customThemeConfig?.palette,
[colorScale]: customThemeConfig?.palette?.[colorScale]?.map(
(color, i) => (i === index ? newColor : color),
),
},
};
const updateCustomThemeConfig = (path: string, newValue: unknown) => {
if (!customThemeConfig) return;
const updatedConfig = setNestedConfigValue(
customThemeConfig,
path,
newValue,
);
setCustomThemeConfig(updatedConfig);
};

const handleColorScaleChange = (
event: React.ChangeEvent<HTMLSelectElement>,
) => {
setActiveColorScale(event.target.value as ColorScalePropType);
const handleColorScaleChange = (colorScale: string) => {
setActiveColorScale(colorScale as ColorScalePropType);
};

const handleThemeConfigPreviewOpen = () => {
Expand All @@ -139,7 +112,7 @@ const ThemeBuilder = () => {

return (
<div className="flex flex-row flex-wrap items-start justify-start w-full">
<aside className="relative flex flex-col h-lvh w-[300px] border-r border-gray-200">
<aside className="relative flex flex-col h-lvh w-[350px] border-r border-gray-200">
<div className="grow overflow-y-auto p-4 pb-[100px]">
<h2 className="mb-0 text-lg font-bold">Customize Your Theme</h2>
<p className="text-sm mb-4 text-gray-300">
Expand All @@ -154,18 +127,12 @@ const ThemeBuilder = () => {
/>
{customThemeConfig && (
<section>
<h2 className="text-lg font-bold mb-4">Customization Options</h2>
<ColorScaleOptions
<h2 className="text-lg font-bold my-4">Customization Options</h2>
<ConfigMapper
themeConfig={customThemeConfig}
activeColorScale={activeColorScale}
palette={customThemeConfig.palette}
onColorChange={handleColorChange}
onColorScaleChange={handleColorScaleChange}
/>
<LabelOptions
labelConfig={
customThemeConfig.axis?.style?.axisLabel as LabelProps
}
onLabelConfigChange={handleLabelConfigChange}
handleColorScaleChange={handleColorScaleChange}
updateThemeConfig={updateCustomThemeConfig}
/>
</section>
)}
Expand Down
Loading

0 comments on commit 442e1a5

Please sign in to comment.