Skip to content

Commit 6e09f2e

Browse files
authored
Merge pull request #2417 from dxc-technology/Mil4n0r/theme-generator-preview
[doc] Theme generator Preview (Step 2)
2 parents ba8c1c4 + 5030da0 commit 6e09f2e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+24508
-26
lines changed

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { useRef, 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 ThemeGeneratorPreviewPage from "./ThemeGeneratorPreviewPage";
6+
// import { FileData } from "../../../../packages/lib/src/file-input/types";
7+
58
import { BrandingDetails } from "./steps/BrandingDetails";
69
import { generateTokens, handleExport } from "./utils";
710
import { Colors, FileData, Step } from "./types";
@@ -88,7 +91,7 @@ const ThemeGeneratorConfigPage = () => {
8891
case 0:
8992
return <BrandingDetails colors={colors} onColorsChange={setColors} logos={logos} onLogosChange={setLogos} />;
9093
case 1:
91-
return <></>;
94+
return <ThemeGeneratorPreviewPage tokens={tokens} logos={logos} />;
9295
case 2:
9396
return <ReviewDetails generatedTokens={tokens} logos={logos} />;
9497
}
@@ -119,7 +122,7 @@ const ThemeGeneratorConfigPage = () => {
119122
boxSizing="border-box"
120123
margin={{ left: "auto", right: "auto" }}
121124
>
122-
<DxcFlex direction="column" alignItems="center" gap="var(--spacing-gap-xl)">
125+
<DxcFlex direction="column" alignItems="center" gap="var(--spacing-gap-xl)" fullHeight>
123126
<StepHeading title={steps[currentStep].title} subtitle={steps[currentStep].subtitle} />
124127
{renderStepContent()}
125128
</DxcFlex>
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import {
2+
DxcButton,
3+
DxcContainer,
4+
DxcFlex,
5+
DxcSelect,
6+
DxcToggleGroup,
7+
DxcTypography,
8+
HalstackProvider,
9+
} from "@dxc-technology/halstack-react";
10+
import { ReactNode, SVGProps, useMemo, useState } from "react";
11+
import componentsList from "../common/componentsList.json";
12+
import { componentsRegistry, examplesRegistry } from "screens/utilities/theme-generator/componentsRegistry";
13+
import styled from "@emotion/styled";
14+
import { ComponentItem, Logos } from "./types";
15+
16+
// TODO: Both of these types should be exported by @dxc-technology/halstack-react when they are available
17+
type SVG = ReactNode & SVGProps<SVGSVGElement>;
18+
19+
type ListOptionType = {
20+
/**
21+
* Element used as the icon that will be placed before the option label.
22+
* It can be an inline SVG or Material Symbol name. If the url option
23+
* is the chosen one, take into account that the component's
24+
* color styling tokens will not be applied to the image.
25+
*/
26+
icon?: string | SVG;
27+
/**
28+
* Label of the option to be shown in the select's listbox.
29+
*/
30+
label: string;
31+
/**
32+
* Value of the option. It should be unique and
33+
* not an empty string, which is reserved to the empty option added
34+
* by optional prop.
35+
*/
36+
value: string;
37+
};
38+
39+
const informationIcon = (
40+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
41+
<path
42+
d="M11 7H13V9H11V7ZM11 11H13V17H11V11ZM12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM12 20C7.59 20 4 16.41 4 12C4 7.59 7.59 4 12 4C16.41 4 20 7.59 20 12C20 16.41 16.41 20 12 20Z"
43+
fill="#494949"
44+
/>
45+
</svg>
46+
);
47+
48+
const exampleOptions = [
49+
{
50+
label: "Application example",
51+
value: "/examples/application",
52+
icon: "settings",
53+
},
54+
{
55+
label: "Dashboard example",
56+
value: "/examples/dashboard",
57+
icon: "dashboard",
58+
},
59+
{
60+
label: "Form example",
61+
value: "/examples/form",
62+
icon: "description",
63+
},
64+
];
65+
66+
const componentsExceptions = [
67+
"/components/application-layout",
68+
"/components/bleed",
69+
"/components/bulleted-list",
70+
"/components/card",
71+
"/components/container",
72+
"/components/dialog",
73+
"/components/flex",
74+
"/components/footer",
75+
"/components/grid",
76+
"/components/header",
77+
"/components/heading",
78+
"/components/image",
79+
"/components/inset",
80+
"/components/paragraph",
81+
"/components/popover",
82+
"/components/sidenav",
83+
"/components/typography",
84+
];
85+
86+
const mapToSelectGroups = (data: ComponentItem[]) => {
87+
const collectOptions = (items: ComponentItem[]): ListOptionType[] => {
88+
return items.flatMap((item) => {
89+
const current: ListOptionType[] =
90+
item.path && !componentsExceptions.includes(item.path)
91+
? [
92+
{
93+
label: item.label,
94+
value: item.path,
95+
icon: item.icon,
96+
},
97+
]
98+
: [];
99+
100+
const nested = item.links ? collectOptions(item.links) : [];
101+
102+
return [...current, ...nested];
103+
});
104+
};
105+
106+
return data.map((category) => ({
107+
label: category.label,
108+
options: collectOptions(category.links ?? []),
109+
}));
110+
};
111+
112+
const ThemeGeneratorPreviewPage = ({ tokens, logos }: { tokens: Record<string, string>; logos: Logos }) => {
113+
const [mode, setMode] = useState<"components" | "examples">("components");
114+
115+
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
116+
const [selectedExample, setSelectedExample] = useState<string>("");
117+
118+
const componentOptions = useMemo(() => {
119+
return mapToSelectGroups(componentsList as ComponentItem[]);
120+
}, []);
121+
122+
const getLabelFromValue = (value: string) =>
123+
componentOptions.flatMap((group) => group.options).find((opt) => opt.value === value)?.label;
124+
125+
const displayedPreview = useMemo(() => {
126+
if (mode === "components") {
127+
return selectedComponents.map((component) => {
128+
const ComponentPreview = componentsRegistry[component as keyof typeof componentsRegistry];
129+
return ComponentPreview ? (
130+
<DxcFlex direction="column" gap="var(--spacing-gap-s)" key={component}>
131+
<DxcTypography
132+
color="var(--color-fg-neutral-strongest)"
133+
fontFamily="var(--typography-font-family)"
134+
fontSize="var(--typography-title-s)"
135+
fontWeight="var(--typography-title-bold)"
136+
>
137+
{getLabelFromValue(component)}
138+
</DxcTypography>
139+
<ComponentPreview key={component} />
140+
</DxcFlex>
141+
) : null;
142+
});
143+
}
144+
145+
if (mode === "examples") {
146+
const ExamplePreview = examplesRegistry[selectedExample as keyof typeof examplesRegistry];
147+
return ExamplePreview ? <ExamplePreview logos={logos} key={selectedExample} /> : null;
148+
}
149+
150+
return null;
151+
}, [mode, selectedComponents, selectedExample]);
152+
153+
return (
154+
<DxcContainer width="100%" height="100%">
155+
<DxcFlex direction="column" gap="var(--spacing-gap-s)" fullHeight>
156+
<DxcFlex direction="row" justifyContent="space-between" alignItems="center">
157+
<DxcToggleGroup
158+
options={[
159+
{ label: "Components", icon: "category", value: 1 },
160+
{ label: "Layout examples", icon: "dashboard", value: 2 },
161+
]}
162+
value={mode === "components" ? 1 : 2}
163+
onChange={(value: number) => setMode(value === 1 ? "components" : "examples")}
164+
/>
165+
{mode === "components" && (
166+
<DxcSelect
167+
placeholder="Select components"
168+
options={componentOptions}
169+
multiple
170+
value={selectedComponents}
171+
onChange={({ value }) => {
172+
setSelectedComponents(value);
173+
}}
174+
enableSelectAll
175+
searchable
176+
size="small"
177+
/>
178+
)}
179+
180+
{mode === "examples" && (
181+
<DxcSelect
182+
placeholder="Select examples"
183+
options={exampleOptions}
184+
value={selectedExample}
185+
onChange={({ value }) => {
186+
setSelectedExample(value);
187+
}}
188+
searchable
189+
/>
190+
)}
191+
</DxcFlex>
192+
{mode === "examples" && (
193+
<DxcFlex gap="var(--spacing-gap-xs)">
194+
{informationIcon}
195+
<DxcTypography
196+
color="var(--color-fg-neutral-strongest)"
197+
fontSize="var(--typography-label-m)"
198+
fontWeight="var(--typography-label-regular)"
199+
>
200+
Some components are presentational examples. The layouts shown are for demonstration purposes only and do
201+
not represent actual components.
202+
</DxcTypography>
203+
</DxcFlex>
204+
)}
205+
{/* TODO: Turn this into a separate componente called PreviewArea or similar? */}
206+
<DxcContainer
207+
borderRadius="var(--border-radius-l)"
208+
border={{
209+
width: "var(--border-width-s)",
210+
color: "var(--border-color-neutral-medium)",
211+
style: "var(--border-style-default)",
212+
}}
213+
background={{ color: "var(--color-bg-neutral-lightest)" }}
214+
padding="var(--spacing-padding-s)"
215+
height="100%"
216+
>
217+
{(mode === "components" && selectedComponents.length > 0) || (mode === "examples" && !!selectedExample) ? (
218+
<DxcFlex direction="column" gap="var(--spacing-gap-ml)" fullHeight>
219+
<DxcFlex justifyContent="flex-end">
220+
<DxcButton
221+
icon="filled_delete"
222+
size={{ height: "medium" }}
223+
title="Delete selection"
224+
onClick={() => {
225+
if (mode === "components") {
226+
setSelectedComponents([]);
227+
} else {
228+
setSelectedExample("");
229+
}
230+
}}
231+
mode="secondary"
232+
semantic="error"
233+
disabled={mode === "components" ? selectedComponents.length === 0 : !selectedExample}
234+
/>
235+
</DxcFlex>
236+
<PreviewAreaContainer>
237+
<HalstackProvider opinionatedTheme={tokens}>
238+
<DxcFlex direction="column" gap="var(--spacing-gap-l)">
239+
{displayedPreview}
240+
</DxcFlex>
241+
</HalstackProvider>
242+
</PreviewAreaContainer>
243+
</DxcFlex>
244+
) : (
245+
<DxcFlex alignItems="center" justifyContent="center" fullHeight>
246+
<DxcTypography
247+
color="var(--color-fg-neutral-dark)"
248+
fontFamily="var(--typography-font-family)"
249+
fontSize="var(--typography-body-s)"
250+
fontWeight="var(--typography-body-regular)"
251+
>
252+
Select {mode === "components" ? "a component" : "an example"} to preview
253+
</DxcTypography>
254+
</DxcFlex>
255+
)}
256+
</DxcContainer>
257+
</DxcFlex>
258+
</DxcContainer>
259+
);
260+
};
261+
262+
const PreviewAreaContainer = styled.div`
263+
flex: 1 1 0;
264+
overflow: auto;
265+
`;
266+
267+
export default ThemeGeneratorPreviewPage;

apps/website/screens/theme-generator/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,11 @@ export type BrandingDetailsSection = {
5454
icon: ReactNode;
5555
fields: Field[];
5656
};
57+
58+
export type ComponentItem = {
59+
label: string;
60+
icon?: string;
61+
path?: string;
62+
status?: string;
63+
links?: ComponentItem[];
64+
};
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
AccordionPreview,
3+
AlertPreview,
4+
AvatarPreview,
5+
BadgePreview,
6+
BreadcrumbsPreview,
7+
ButtonPreview,
8+
CheckboxPreview,
9+
ChipPreview,
10+
ContextualMenuPreview,
11+
DataGridPreview,
12+
DateInputPreview,
13+
DividerPreview,
14+
DropdownPreview,
15+
FileInputPreview,
16+
LinkPreview,
17+
NavTabsPreview,
18+
NumberInputPreview,
19+
PaginatorPreview,
20+
PasswordInputPreview,
21+
ProgressBarPreview,
22+
QuickNavPreview,
23+
RadioGroupPreview,
24+
ResultsetTablePreview,
25+
SelectPreview,
26+
SliderPreview,
27+
SpinnerPreview,
28+
StatusLightPreview,
29+
SwitchPreview,
30+
TablePreview,
31+
TabsPreview,
32+
TextareaPreview,
33+
TextInputPreview,
34+
ToastPreview,
35+
ToggleGroupPreview,
36+
TooltipPreview,
37+
WizardPreview,
38+
} from "./previews/components";
39+
import { ApplicationPreview, DashboardPreview, FormPreview } from "./previews/examples";
40+
41+
export const componentsRegistry = {
42+
"/components/accordion": AccordionPreview,
43+
"/components/avatar": AvatarPreview,
44+
"/components/divider": DividerPreview,
45+
"/components/wizard": WizardPreview,
46+
"/components/data-grid": DataGridPreview,
47+
"/components/paginator": PaginatorPreview,
48+
"/components/resultset-table": ResultsetTablePreview,
49+
"/components/table": TablePreview,
50+
"/components/alert": AlertPreview,
51+
"/components/progress-bar": ProgressBarPreview,
52+
"/components/spinner": SpinnerPreview,
53+
"/components/toast": ToastPreview,
54+
"/components/tooltip": TooltipPreview,
55+
"/components/button": ButtonPreview,
56+
"/components/checkbox": CheckboxPreview,
57+
"/components/date-input": DateInputPreview,
58+
"/components/file-input": FileInputPreview,
59+
"/components/number-input": NumberInputPreview,
60+
"/components/password-input": PasswordInputPreview,
61+
"/components/radio-group": RadioGroupPreview,
62+
"/components/select": SelectPreview,
63+
"/components/slider": SliderPreview,
64+
"/components/switch": SwitchPreview,
65+
"/components/text-input": TextInputPreview,
66+
"/components/textarea": TextareaPreview,
67+
"/components/toggle-group": ToggleGroupPreview,
68+
"/components/breadcrumbs": BreadcrumbsPreview,
69+
"/components/contextual-menu": ContextualMenuPreview,
70+
"/components/dropdown": DropdownPreview,
71+
"/components/link": LinkPreview,
72+
"/components/nav-tabs": NavTabsPreview,
73+
"/components/quick-nav": QuickNavPreview,
74+
"/components/tabs": TabsPreview,
75+
"/components/badge": BadgePreview,
76+
"/components/chip": ChipPreview,
77+
"/components/status-light": StatusLightPreview,
78+
};
79+
80+
export const examplesRegistry = {
81+
"/examples/application": ApplicationPreview,
82+
"/examples/dashboard": DashboardPreview,
83+
"/examples/form": FormPreview,
84+
};

0 commit comments

Comments
 (0)