Skip to content

Commit cf1287a

Browse files
Add Branding details screen
1 parent 5e91ada commit cf1287a

6 files changed

Lines changed: 548 additions & 18 deletions

File tree

apps/website/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"lint": "eslint . --max-warnings 0"
99
},
1010
"dependencies": {
11+
"@adobe/leonardo-contrast-colors": "^1.1.0",
1112
"@cloudscape-design/components": "^3.0.706",
1213
"@dxc-technology/halstack-react": "*",
1314
"@emotion/cache": "^11.14.0",

apps/website/screens/theme-generator/ThemeGeneratorConfigPage.tsx

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { useState } from "react";
1+
import { useEffect, useState } from "react";
22
import { DxcContainer, DxcFlex, DxcWizard } from "@dxc-technology/halstack-react";
33
import StepHeading from "./components/StepHeading";
44
import BottomButtons from "./components/BottomButtons";
5-
// import { FileData } from "../../../../packages/lib/src/file-input/types";
5+
import { FileData } from "../../../../packages/lib/src/file-input/types";
6+
import { generateTokens } from "./ThemeGeneratorUtils";
7+
import { type CssColor } from "@adobe/leonardo-contrast-colors";
8+
import { BrandingDetails } from "./steps/BrandingDetails";
69

710
export type Step = 0 | 1 | 2;
811

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

3538
const ThemeGeneratorConfigPage = () => {
3639
const [currentStep, setCurrentStep] = useState<Step>(0);
37-
// Uncomment when implementing the Branding details screen
38-
/** const [colors, setColors] = useState({
39-
primary: "#5f249f",
40-
secondary: "#00b4d8",
41-
tertiary: "#ffa500",
40+
const [colors, setColors] = useState<{
41+
primary: CssColor;
42+
secondary: CssColor;
43+
tertiary: CssColor;
44+
neutral: CssColor;
45+
info: CssColor;
46+
success: CssColor;
47+
error: CssColor;
48+
warning: CssColor;
49+
}>({
50+
primary: "#5F249F",
51+
secondary: "#00B4D8",
52+
tertiary: "#FFA500",
4253
neutral: "#666666",
4354
info: "#0095FF",
4455
success: "#2FD05D",
@@ -51,12 +62,47 @@ const ThemeGeneratorConfigPage = () => {
5162
footerReducedLogo: [] as FileData[],
5263
favicon: [] as FileData[],
5364
});
54-
*/
65+
const [tokens, setTokens] = useState<Record<string, string>>({});
5566

56-
const handleChangeStep = (step: Step) => {
67+
useEffect(() => {
68+
const generateTokensFromColors = () => {
69+
try {
70+
const mappedColors = {
71+
primary: colors.primary,
72+
secondary: colors.secondary,
73+
tertiary: colors.tertiary,
74+
semantic01: colors.info,
75+
semantic02: colors.success,
76+
semantic03: colors.warning,
77+
semantic04: colors.error,
78+
neutral: colors.neutral,
79+
};
80+
81+
const generatedTokens = generateTokens(mappedColors);
82+
83+
setTokens(generatedTokens);
84+
} catch (error) {
85+
console.error("Error generating tokens:", error);
86+
}
87+
};
88+
89+
generateTokensFromColors();
90+
}, [colors]);
91+
92+
const handleChangeStep = (step: 0 | 1 | 2) => {
5793
setCurrentStep(step);
5894
};
5995

96+
const handleExport = () => {
97+
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(tokens, null, 2));
98+
const downloadAnchorNode = document.createElement("a");
99+
downloadAnchorNode.setAttribute("href", dataStr);
100+
downloadAnchorNode.setAttribute("download", "halstack-theme-tokens.json");
101+
document.body.appendChild(downloadAnchorNode);
102+
downloadAnchorNode.click();
103+
downloadAnchorNode.remove();
104+
};
105+
60106
return (
61107
<DxcContainer
62108
height="100%"
@@ -84,11 +130,17 @@ const ThemeGeneratorConfigPage = () => {
84130
>
85131
<DxcFlex direction="column" alignItems="center" gap="var(--spacing-gap-xl)">
86132
<StepHeading title={steps[currentStep].title} subtitle={steps[currentStep].subtitle} />
87-
{currentStep === 0 ? <></> : currentStep === 1 ? <></> : <></>}
133+
{currentStep === 0 ? (
134+
<BrandingDetails colors={colors} onColorsChange={setColors} logos={logos} onLogosChange={setLogos} />
135+
) : currentStep === 1 ? (
136+
<></>
137+
) : (
138+
<></>
139+
)}
88140
</DxcFlex>
89141
</DxcContainer>
90142

91-
<BottomButtons currentStep={currentStep} onChangeStep={handleChangeStep} />
143+
<BottomButtons currentStep={currentStep} onChangeStep={handleChangeStep} onExport={handleExport} />
92144
</DxcFlex>
93145
</DxcContainer>
94146
);
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Color, BackgroundColor, Theme, type CssColor } from "@adobe/leonardo-contrast-colors";
2+
3+
type Tokens = Record<string, string>;
4+
type BaseColors = Record<string, CssColor>;
5+
6+
/**
7+
* Contrast ratios for generating color shades
8+
* Based on WCAG accessibility standards
9+
*/
10+
export const CONTRAST_RATIOS = [1.03, 1.18, 1.34, 1.52, 2.04, 2.79, 4.3, 6.7, 9, 12.46];
11+
12+
/**
13+
* Shade values corresponding to each contrast ratio (50-900)
14+
*/
15+
export const SHADE_VALUES = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
16+
17+
const ALPHA_VALUES = ["1a", "33", "4d", "66", "80", "99", "b2", "cc", "e5"];
18+
19+
/**
20+
* Generate a full color palette (50-900 shades) from a base hex color
21+
* Uses Adobe Leonardo to ensure accessible contrast ratios
22+
*/
23+
24+
export const generatePalette = (hex: CssColor): string[] => {
25+
try {
26+
const bg = new BackgroundColor({
27+
//Definir color de fondo blanco
28+
name: "bg",
29+
colorKeys: ["#fff"],
30+
ratios: CONTRAST_RATIOS,
31+
});
32+
const color = new Color({
33+
//Definir color base
34+
name: "custom",
35+
colorKeys: [hex],
36+
ratios: CONTRAST_RATIOS,
37+
colorSpace: "RGB",
38+
smooth: false,
39+
});
40+
const theme = new Theme({
41+
colors: [color],
42+
backgroundColor: bg,
43+
lightness: 97,
44+
});
45+
46+
return theme.contrastColors[1]?.values?.map((v) => v.value) || [];
47+
} catch (error) {
48+
console.error("Error generating palette for hex:", hex, error);
49+
return [];
50+
}
51+
};
52+
53+
const generateAlphaColorsObject = (neutralPalette: string[]): Tokens => {
54+
const neutralColorsFrom100 = neutralPalette.slice(1, 10);
55+
const alphaObj: Tokens = {};
56+
57+
neutralColorsFrom100.forEach((neutralColor, i) => {
58+
const key = `--color-alpha-${(i + 1) * 100}-a`;
59+
alphaObj[key] = `${neutralColor}${ALPHA_VALUES[i]}`;
60+
});
61+
62+
return alphaObj;
63+
};
64+
65+
const generateAbsolutesObject = (): Tokens => ({
66+
"--color-absolutes-black": "#000000",
67+
"--color-absolutes-white": "#ffffff",
68+
});
69+
70+
const generateTokensObject = (baseColors: BaseColors): Tokens => {
71+
const neutralColor = baseColors.Neutral || "#999999";
72+
const neutralPalette = generatePalette(neutralColor);
73+
//For each base color, generate its palette
74+
//and create an object with the tokens
75+
//in the format color_<name>_<value>
76+
//where <name> is the color name in lowercase
77+
//and <value> is the palette value (50, 100, 200, etc.)
78+
//Example: color_primary_50, color_primary_100,
79+
const baseTokensObj: Tokens = {};
80+
Object.entries(baseColors).forEach(([name, hex]) => {
81+
const palette = generatePalette(hex);
82+
palette.forEach((color, i) => {
83+
const key = `--color-${name.toLowerCase()}-${SHADE_VALUES[i]}`;
84+
baseTokensObj[key] = color;
85+
});
86+
});
87+
88+
const alphaObj = generateAlphaColorsObject(neutralPalette);
89+
const absolutesObj = generateAbsolutesObject();
90+
91+
const allTokensObj = { ...baseTokensObj, ...alphaObj, ...absolutesObj };
92+
93+
return allTokensObj;
94+
};
95+
96+
export const updateCSSVariables = (colorName: string, newHex: CssColor): void => {
97+
const palette = generatePalette(newHex);
98+
palette.forEach((color, i) => {
99+
const tokenName = `color-${colorName.toLowerCase()}-${SHADE_VALUES[i]}`;
100+
document.documentElement.style.setProperty(`--${tokenName}`, color, "important");
101+
});
102+
};
103+
104+
export const generateTokens = (baseColors: BaseColors): Tokens => {
105+
return generateTokensObject(baseColors);
106+
};

apps/website/screens/theme-generator/components/BottomButtons.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ const MAX_STEP: Step = 2;
77
interface BottomButtonsProps {
88
currentStep: Step;
99
onChangeStep: (step: Step) => void;
10+
onExport: () => void;
1011
}
1112

12-
const BottomButtons = ({ currentStep, onChangeStep }: BottomButtonsProps) => {
13+
const BottomButtons = ({ currentStep, onChangeStep, onExport }: BottomButtonsProps) => {
1314
const goToStep = (step: number) => {
1415
if (step >= MIN_STEP && step <= MAX_STEP) {
1516
onChangeStep(step as Step);
@@ -33,12 +34,7 @@ const BottomButtons = ({ currentStep, onChangeStep }: BottomButtonsProps) => {
3334
size={{ height: "medium", width: "fitContent" }}
3435
/>
3536
{currentStep === 2 ? (
36-
<DxcButton
37-
label="Export theme"
38-
//TODO: replace with actual export functionality
39-
onClick={() => console.log("download theme")}
40-
size={{ height: "medium", width: "fitContent" }}
41-
/>
37+
<DxcButton label="Export theme" onClick={onExport} size={{ height: "medium", width: "fitContent" }} />
4238
) : (
4339
<DxcButton
4440
label="Next"
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { useCallback, useRef, useState } from "react";
2+
import { DxcContainer, DxcFlex, DxcPopover, DxcTextInput } from "@dxc-technology/halstack-react";
3+
import styled from "@emotion/styled";
4+
import { SketchPicker } from "react-color";
5+
6+
const ColorBox = styled.button<{ color: string }>`
7+
aspect-ratio: 1 / 1;
8+
height: 72.8px;
9+
border-radius: var(--border-radius-m);
10+
background-color: ${(props) => props.color};
11+
cursor: pointer;
12+
border: none;
13+
padding: var(--spacing-padding-none);
14+
`;
15+
16+
interface ColorCardProps {
17+
label: string;
18+
helperText: string;
19+
color: string;
20+
onChange: (color: string) => void;
21+
}
22+
23+
export const ColorCard = ({ label, helperText, color, onChange }: ColorCardProps) => {
24+
const [isOpen, setIsOpen] = useState(false);
25+
const [inputValue, setInputValue] = useState(color);
26+
const [error, setError] = useState<string | undefined>(undefined);
27+
const buttonRef = useRef<HTMLButtonElement>(null);
28+
29+
const updateColor = useCallback(
30+
(newColor: string) => {
31+
setInputValue(newColor);
32+
onChange(newColor);
33+
setError(undefined);
34+
},
35+
[onChange]
36+
);
37+
38+
const handleInputChange = useCallback(
39+
({ value }: { value: string }) => {
40+
setInputValue(value);
41+
// Solo propagar si es un hexadecimal válido (el patrón lo valida el DxcTextInput)
42+
const hexPattern = /^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$/;
43+
if (hexPattern.test(value)) {
44+
updateColor(value);
45+
}
46+
},
47+
[updateColor]
48+
);
49+
50+
const onBlur = useCallback(
51+
({ value, error }: { value: string; error?: string }) => {
52+
let normalizedValue = value;
53+
if (value && !value.startsWith("#")) {
54+
normalizedValue = "#" + value;
55+
setInputValue(normalizedValue);
56+
57+
const hexPattern = /^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$/;
58+
if (hexPattern.test(normalizedValue)) {
59+
updateColor(normalizedValue);
60+
return;
61+
}
62+
}
63+
setError(error || undefined);
64+
},
65+
[updateColor]
66+
);
67+
68+
return (
69+
<DxcContainer
70+
height="fit-content"
71+
borderRadius="var(--border-radius-l)"
72+
border={{
73+
width: "var(--border-width-s)",
74+
style: "var(--border-style-default)",
75+
color: "var(--border-color-neutral-lighter)",
76+
}}
77+
padding="var(--spacing-padding-s)"
78+
>
79+
<DxcFlex alignItems="stretch" gap="var(--spacing-gap-s)" fullHeight>
80+
<DxcPopover
81+
isOpen={isOpen}
82+
onClose={() => setIsOpen(false)}
83+
popoverContent={
84+
<SketchPicker
85+
styles={{
86+
default: {
87+
picker: {
88+
backgroundColor: "var(--color-bg-neutral-lightest)",
89+
boxShadow: "none ",
90+
},
91+
},
92+
}}
93+
color={color}
94+
disableAlpha={true}
95+
onChange={(newColor) => updateColor(newColor.hex)}
96+
/>
97+
}
98+
hasTip
99+
side="bottom"
100+
asChild
101+
>
102+
<ColorBox onClick={() => setIsOpen((prev) => !prev)} ref={buttonRef} color={color} />
103+
</DxcPopover>
104+
<DxcTextInput
105+
label={label}
106+
helperText={helperText}
107+
value={inputValue}
108+
onChange={handleInputChange}
109+
size="fillParent"
110+
pattern="^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$"
111+
error={error}
112+
onBlur={onBlur}
113+
action={{
114+
icon: "Content_Copy",
115+
onClick: () => {
116+
navigator.clipboard.writeText(color).catch(() => {
117+
alert("Failed attempt to copy the hex value.");
118+
});
119+
},
120+
title: "Copy the hex value",
121+
}}
122+
/>
123+
</DxcFlex>
124+
</DxcContainer>
125+
);
126+
};

0 commit comments

Comments
 (0)