Skip to content
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
1 change: 1 addition & 0 deletions apps/website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"lint": "eslint . --max-warnings 0"
},
"dependencies": {
"@adobe/leonardo-contrast-colors": "^1.1.0",
"@cloudscape-design/components": "^3.0.706",
"@dxc-technology/halstack-react": "*",
"@emotion/cache": "^11.14.0",
Expand Down
74 changes: 63 additions & 11 deletions apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { DxcContainer, DxcFlex, DxcWizard } from "@dxc-technology/halstack-react";
import StepHeading from "./components/StepHeading";
import BottomButtons from "./components/BottomButtons";
// import { FileData } from "../../../../packages/lib/src/file-input/types";
import { FileData } from "../../../../packages/lib/src/file-input/types";
import { generateTokens } from "./ThemeGeneratorUtils";
import { type CssColor } from "@adobe/leonardo-contrast-colors";
import { BrandingDetails } from "./steps/BrandingDetails";

export type Step = 0 | 1 | 2;

Expand Down Expand Up @@ -34,11 +37,19 @@ const wizardSteps = steps.map(({ label, description }) => ({

const ThemeGeneratorConfigPage = () => {
const [currentStep, setCurrentStep] = useState<Step>(0);
// Uncomment when implementing the Branding details screen
/** const [colors, setColors] = useState({
primary: "#5f249f",
secondary: "#00b4d8",
tertiary: "#ffa500",
const [colors, setColors] = useState<{
primary: CssColor;
secondary: CssColor;
tertiary: CssColor;
neutral: CssColor;
info: CssColor;
success: CssColor;
error: CssColor;
warning: CssColor;
}>({
primary: "#5F249F",
secondary: "#00B4D8",
tertiary: "#FFA500",
neutral: "#666666",
info: "#0095FF",
success: "#2FD05D",
Expand All @@ -51,12 +62,47 @@ const ThemeGeneratorConfigPage = () => {
footerReducedLogo: [] as FileData[],
favicon: [] as FileData[],
});
*/
const [tokens, setTokens] = useState<Record<string, string>>({});

const handleChangeStep = (step: Step) => {
useEffect(() => {
const generateTokensFromColors = () => {
try {
const mappedColors = {
primary: colors.primary,
secondary: colors.secondary,
tertiary: colors.tertiary,
semantic01: colors.info,
semantic02: colors.success,
semantic03: colors.warning,
semantic04: colors.error,
neutral: colors.neutral,
};

const generatedTokens = generateTokens(mappedColors);

setTokens(generatedTokens);
} catch (error) {
console.error("Error generating tokens:", error);
}
};

generateTokensFromColors();
}, [colors]);

const handleChangeStep = (step: 0 | 1 | 2) => {
setCurrentStep(step);
};

const handleExport = () => {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(tokens, null, 2));
const downloadAnchorNode = document.createElement("a");
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "halstack-theme-tokens.json");
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
};

return (
<DxcContainer
height="100%"
Expand Down Expand Up @@ -84,11 +130,17 @@ const ThemeGeneratorConfigPage = () => {
>
<DxcFlex direction="column" alignItems="center" gap="var(--spacing-gap-xl)">
<StepHeading title={steps[currentStep].title} subtitle={steps[currentStep].subtitle} />
{currentStep === 0 ? <></> : currentStep === 1 ? <></> : <></>}
{currentStep === 0 ? (
<BrandingDetails colors={colors} onColorsChange={setColors} logos={logos} onLogosChange={setLogos} />
) : currentStep === 1 ? (
<></>
) : (
<></>
)}
</DxcFlex>
</DxcContainer>

<BottomButtons currentStep={currentStep} onChangeStep={handleChangeStep} />
<BottomButtons currentStep={currentStep} onChangeStep={handleChangeStep} onExport={handleExport} />
</DxcFlex>
</DxcContainer>
);
Expand Down
4 changes: 2 additions & 2 deletions apps/website/screens/theme-generator/ThemeGeneratorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const ThemeGeneratorPage = () => {
</DxcTypography>
</DxcContainer>
</DxcFlex>
<Link href="" passHref legacyBehavior>
<Link href="/theme-generator/configuration" passHref legacyBehavior>
<DxcLink icon="arrow_forward" iconPosition="after">
Start your theme
</DxcLink>
Expand Down Expand Up @@ -139,7 +139,7 @@ const ThemeGeneratorPage = () => {
</DxcContainer>
</DxcFlex>
<DxcFlex gap="var(--spacing-gap-l)">
<Link href="" passHref legacyBehavior>
<Link href="/theme-generator/configuration" passHref legacyBehavior>
<DxcLink icon="arrow_forward" iconPosition="after">
Open Theme Generator
</DxcLink>
Expand Down
106 changes: 106 additions & 0 deletions apps/website/screens/theme-generator/ThemeGeneratorUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Color, BackgroundColor, Theme, type CssColor } from "@adobe/leonardo-contrast-colors";

type Tokens = Record<string, string>;
type BaseColors = Record<string, CssColor>;

/**
* Contrast ratios for generating color shades
* Based on WCAG accessibility standards
*/
export const CONTRAST_RATIOS = [1.03, 1.18, 1.34, 1.52, 2.04, 2.79, 4.3, 6.7, 9, 12.46];

/**
* Shade values corresponding to each contrast ratio (50-900)
*/
export const SHADE_VALUES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];

const ALPHA_VALUES = ["1a", "33", "4d", "66", "80", "99", "b2", "cc", "e5"];

/**
* Generate a full color palette (50-900 shades) from a base hex color
* Uses Adobe Leonardo to ensure accessible contrast ratios
*/

export const generatePalette = (hex: CssColor): string[] => {
try {
const bg = new BackgroundColor({
//Definir color de fondo blanco
name: "bg",
colorKeys: ["#fff"],
ratios: CONTRAST_RATIOS,
});
const color = new Color({
//Definir color base
name: "custom",
colorKeys: [hex],
ratios: CONTRAST_RATIOS,
colorSpace: "RGB",
smooth: false,
});
const theme = new Theme({
colors: [color],
backgroundColor: bg,
lightness: 97,
});

return theme.contrastColors[1]?.values?.map((v) => v.value) || [];
} catch (error) {
console.error("Error generating palette for hex:", hex, error);
return [];
}
};

const generateAlphaColorsObject = (neutralPalette: string[]): Tokens => {
const neutralColorsFrom100 = neutralPalette.slice(1, 10);
const alphaObj: Tokens = {};

neutralColorsFrom100.forEach((neutralColor, i) => {
const key = `--color-alpha-${(i + 1) * 100}-a`;
alphaObj[key] = `${neutralColor}${ALPHA_VALUES[i]}`;
});

return alphaObj;
};

const generateAbsolutesObject = (): Tokens => ({
"--color-absolutes-black": "#000000",
"--color-absolutes-white": "#ffffff",
});

const generateTokensObject = (baseColors: BaseColors): Tokens => {
const neutralColor = baseColors.Neutral || "#999999";
const neutralPalette = generatePalette(neutralColor);
//For each base color, generate its palette
//and create an object with the tokens
//in the format color_<name>_<value>
//where <name> is the color name in lowercase
//and <value> is the palette value (50, 100, 200, etc.)
//Example: color_primary_50, color_primary_100,
const baseTokensObj: Tokens = {};
Object.entries(baseColors).forEach(([name, hex]) => {
const palette = generatePalette(hex);
palette.forEach((color, i) => {
const key = `--color-${name.toLowerCase()}-${SHADE_VALUES[i]}`;
baseTokensObj[key] = color;
});
});

const alphaObj = generateAlphaColorsObject(neutralPalette);
const absolutesObj = generateAbsolutesObject();

const allTokensObj = { ...baseTokensObj, ...alphaObj, ...absolutesObj };

return allTokensObj;
};

export const updateCSSVariables = (colorName: string, newHex: CssColor): void => {
const palette = generatePalette(newHex);
palette.forEach((color, i) => {
const tokenName = `color-${colorName.toLowerCase()}-${SHADE_VALUES[i]}`;
document.documentElement.style.setProperty(`--${tokenName}`, color, "important");
});
};

export const generateTokens = (baseColors: BaseColors): Tokens => {
return generateTokensObject(baseColors);
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ const MAX_STEP: Step = 2;
interface BottomButtonsProps {
currentStep: Step;
onChangeStep: (step: Step) => void;
onExport: () => void;
}

const BottomButtons = ({ currentStep, onChangeStep }: BottomButtonsProps) => {
const BottomButtons = ({ currentStep, onChangeStep, onExport }: BottomButtonsProps) => {
const goToStep = (step: number) => {
if (step >= MIN_STEP && step <= MAX_STEP) {
onChangeStep(step as Step);
Expand All @@ -33,12 +34,7 @@ const BottomButtons = ({ currentStep, onChangeStep }: BottomButtonsProps) => {
size={{ height: "medium", width: "fitContent" }}
/>
{currentStep === 2 ? (
<DxcButton
label="Export theme"
//TODO: replace with actual export functionality
onClick={() => console.log("download theme")}
size={{ height: "medium", width: "fitContent" }}
/>
<DxcButton label="Export theme" onClick={onExport} size={{ height: "medium", width: "fitContent" }} />
) : (
<DxcButton
label="Next"
Expand Down
126 changes: 126 additions & 0 deletions apps/website/screens/theme-generator/components/ColorCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useCallback, useRef, useState } from "react";
import { DxcContainer, DxcFlex, DxcPopover, DxcTextInput } from "@dxc-technology/halstack-react";
import styled from "@emotion/styled";
import { SketchPicker } from "react-color";

const ColorBox = styled.button<{ color: string }>`
aspect-ratio: 1 / 1;
height: 72.8px;
border-radius: var(--border-radius-m);
background-color: ${(props) => props.color};
cursor: pointer;
border: none;
padding: var(--spacing-padding-none);
`;

interface ColorCardProps {
label: string;
helperText: string;
color: string;
onChange: (color: string) => void;
}

export const ColorCard = ({ label, helperText, color, onChange }: ColorCardProps) => {
const [isOpen, setIsOpen] = useState(false);
const [inputValue, setInputValue] = useState(color);
const [error, setError] = useState<string | undefined>(undefined);
const buttonRef = useRef<HTMLButtonElement>(null);

const updateColor = useCallback(
(newColor: string) => {
setInputValue(newColor);
onChange(newColor);
setError(undefined);
},
[onChange]
);

const handleInputChange = useCallback(
({ value }: { value: string }) => {
setInputValue(value);
// Solo propagar si es un hexadecimal válido (el patrón lo valida el DxcTextInput)
const hexPattern = /^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$/;
if (hexPattern.test(value)) {
updateColor(value);
}
},
[updateColor]
);

const onBlur = useCallback(
({ value, error }: { value: string; error?: string }) => {
let normalizedValue = value;
if (value && !value.startsWith("#")) {
normalizedValue = "#" + value;
setInputValue(normalizedValue);

const hexPattern = /^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$/;
if (hexPattern.test(normalizedValue)) {
updateColor(normalizedValue);
return;
}
}
setError(error || undefined);
},
[updateColor]
);

return (
<DxcContainer
height="fit-content"
borderRadius="var(--border-radius-l)"
border={{
width: "var(--border-width-s)",
style: "var(--border-style-default)",
color: "var(--border-color-neutral-lighter)",
}}
padding="var(--spacing-padding-s)"
>
<DxcFlex alignItems="stretch" gap="var(--spacing-gap-s)" fullHeight>
<DxcPopover
isOpen={isOpen}
onClose={() => setIsOpen(false)}
popoverContent={
<SketchPicker
styles={{
default: {
picker: {
backgroundColor: "var(--color-bg-neutral-lightest)",
boxShadow: "none ",
},
},
}}
color={color}
disableAlpha={true}
onChange={(newColor) => updateColor(newColor.hex)}
/>
}
hasTip
side="bottom"
asChild
>
<ColorBox onClick={() => setIsOpen((prev) => !prev)} ref={buttonRef} color={color} />
</DxcPopover>
<DxcTextInput
label={label}
helperText={helperText}
value={inputValue}
onChange={handleInputChange}
size="fillParent"
pattern="^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$"
error={error}
onBlur={onBlur}
action={{
icon: "Content_Copy",
onClick: () => {
navigator.clipboard.writeText(color).catch(() => {
alert("Failed attempt to copy the hex value.");
});
},
title: "Copy the hex value",
}}
/>
</DxcFlex>
</DxcContainer>
);
};
Loading
Loading