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

Feat: theme builder options config #2955

Merged
merged 3 commits into from
Nov 7, 2024
Merged
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
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
Loading