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
48 changes: 32 additions & 16 deletions apps/website/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,18 @@ type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
emotionCache?: EmotionCache;
};
type ApplicationLayoutWrapperProps = {
condition: boolean;
wrapper: (_children: ReactNode) => JSX.Element;
children: ReactNode;
};

const clientSideEmotionCache = createCache({ key: "css", prepend: true });

const ApplicationLayoutWrapper = ({ condition, wrapper, children }: ApplicationLayoutWrapperProps): JSX.Element => (
<>{condition ? wrapper(children) : children}</>
);

export default function App({ Component, pageProps, emotionCache = clientSideEmotionCache }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page);
const componentWithLayout = getLayout(<Component {...pageProps} />);
Expand Down Expand Up @@ -107,23 +116,30 @@ export default function App({ Component, pageProps, emotionCache = clientSideEmo
<Head>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
</Head>
<DxcApplicationLayout
logo={{ src: dxcLogo, alt: "DXC Technology" }}
header={<DxcApplicationLayout.Header />}
sidenav={
<DxcApplicationLayout.Sidenav
navItems={navItems}
appTitle={<SidenavLogo />}
searchBar={{ placeholder: "Search docs", onChange: (value) => setFilter(value) }}
/>
}
<ApplicationLayoutWrapper
condition={!currentPath.includes("/theme-generator")}
wrapper={(children) => (
<DxcApplicationLayout
logo={{ src: dxcLogo, alt: "DXC Technology" }}
header={<DxcApplicationLayout.Header />}
sidenav={
<DxcApplicationLayout.Sidenav
navItems={navItems}
appTitle={<SidenavLogo />}
searchBar={{ placeholder: "Search docs", onChange: (value) => setFilter(value) }}
/>
}
>
<DxcApplicationLayout.Main>
<DxcToastsQueue duration={7000}>
<MainContent>{children}</MainContent>
</DxcToastsQueue>
</DxcApplicationLayout.Main>
</DxcApplicationLayout>
)}
>
<DxcApplicationLayout.Main>
<DxcToastsQueue duration={7000}>
<MainContent>{componentWithLayout}</MainContent>
</DxcToastsQueue>
</DxcApplicationLayout.Main>
</DxcApplicationLayout>
{componentWithLayout}
</ApplicationLayoutWrapper>
</CacheProvider>
);
}
44 changes: 44 additions & 0 deletions apps/website/pages/utilities/theme-generator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import Head from "next/head";
import styled from "@emotion/styled";
import { DxcApplicationLayout, DxcButton, DxcToastsQueue } from "@dxc-technology/halstack-react";
import { dxcLogo } from "@/common/images/dxc_logo";
import ThemeGeneratorPage from "screens/utilities/theme-generator/ThemeGeneratorPage";

const MainContent = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-gap-xs);
padding: var(--spacing-padding-none) var(--spacing-padding-ml);
box-sizing: border-box;
padding-top: var(--spacing-padding-s);
`;

const Index = () => {
return (
<>
<Head>
<title>Theme generator — Halstack Design System</title>
</Head>
<DxcApplicationLayout logo={{ src: dxcLogo, alt: "DXC Technology" }} header={<DxcApplicationLayout.Header />}>
<DxcApplicationLayout.Main>
<DxcToastsQueue duration={7000}>
<MainContent>
<DxcButton
label="Back to site"
icon="chevron_left"
mode="tertiary"
size={{ height: "large", width: "fitContent" }}
/>
<ThemeGeneratorPage />
</MainContent>
</DxcToastsQueue>
</DxcApplicationLayout.Main>
</DxcApplicationLayout>
</>
);
};

export default Index;
5 changes: 5 additions & 0 deletions apps/website/screens/common/pagesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ const utilitiesLinks: LinkDetails[] = [
path: "/utilities/halstack-provider",
icon: "integration_instructions",
},
{
label: "Theme generator",
path: "/utilities/theme-generator",
icon: "colorize",
},
];

const principlesLinks: LinkDetails[] = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import styled from "@emotion/styled";
import { ThemeSettings } from "screens/utilities/theme-generator/components/ThemeSettings";
import { BottomButtons } from "screens/utilities/theme-generator/components/BottomButtons";
import { PreviewArea } from "./components/PreviewArea";
import { PreviewSidenav } from "./components/PreviewSidenav";
import { DxcFlex } from "@dxc-technology/halstack-react";
import { ThemeGeneratorProvider, useThemeGenerator } from "./context/ThemeGeneratorContext";

const PreviewWrapper = styled.div`
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-gap-ml);
padding: var(--spacing-padding-l);
`;

const ThemeGeneratorContent = () => {
const { currentStep, setCurrentStep, activeComponents, setActiveComponents } = useThemeGenerator();

return (
<DxcFlex alignItems="flex-start" gap="var(--spacing-gap-ml)" alignSelf="stretch">
<PreviewSidenav />

<PreviewWrapper>
<ThemeSettings />

<PreviewArea components={activeComponents} setActiveComponents={setActiveComponents} />

<BottomButtons currentStep={currentStep} setCurrentStep={setCurrentStep} />
</PreviewWrapper>
</DxcFlex>
);
};

const ThemeGeneratorPage = () => {
return (
<ThemeGeneratorProvider>
<ThemeGeneratorContent />
</ThemeGeneratorProvider>
);
};

export default ThemeGeneratorPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DxcButton } from "@dxc-technology/halstack-react";
import styled from "@emotion/styled";

const ButtonsWrapper = styled.div`
width: 100%;
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-gap-s);
`;

export const BottomButtons = ({
currentStep,
setCurrentStep,
}: {
currentStep: 0 | 1 | 2;
setCurrentStep: React.Dispatch<React.SetStateAction<0 | 1 | 2>>;
}) => {
const handlePrevious = () => {
setCurrentStep((prev) => (prev > 0 ? ((prev - 1) as 0 | 1 | 2) : prev));
};

const handleNext = () => {
setCurrentStep((prev) => (prev < 2 ? ((prev + 1) as 0 | 1 | 2) : prev));
};

return (
<ButtonsWrapper>
<DxcButton label="Previous" icon="arrow_back" mode="secondary" onClick={handlePrevious} />
{currentStep === 2 ? (
<DxcButton label="Export" icon="download" onClick={handleNext} />
) : (
<DxcButton label="Next" icon="arrow_forward" onClick={handleNext} />
)}
</ButtonsWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { useCallback, useRef, useState } from "react";
import { DxcHeading, DxcPopover, DxcTextInput } from "@dxc-technology/halstack-react";
import styled from "@emotion/styled";
import { SketchPicker } from "react-color";

const CardWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-gap-s);
padding: var(--spacing-padding-xs);
border-radius: var(--border-radius-m);
background-color: var(--color-bg-neutral-lightest);
box-shadow: var(--shadow-100);
`;

const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-gap-xs);
`;

const ColorBox = styled.button<{ color: string }>`
width: 124px;
height: 32px;
border-radius: var(--border-radius-s);
background-color: ${(props) => props.color};
cursor: pointer;

border: none;
`;

const ColorText = styled.div`
width: 124px;
display: flex;
justify-content: center;
align-items: center;
`;

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

const handleChange = useCallback(
(newColor: { hex: string }) => {
setInputValue(newColor.hex);
onChange(newColor.hex);
},
[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)) {
onChange(value);
setError("");
}
},
[onChange]
);

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)) {
onChange(normalizedValue);
setError("");
return;
}
}
setError(error || "");
},
[onChange]
);

return (
<CardWrapper>
<ContentWrapper>
<DxcHeading level={4} text={label} />
<DxcPopover
isOpen={isOpen}
onClose={() => setIsOpen(false)}
popoverContent={
<SketchPicker
styles={{
default: {
picker: {
backgroundColor: "var(--color-bg-neutral-lightest)",
},
},
}}
color={color}
disableAlpha={true}
onChange={handleChange}
/>
}
hasTip
side="bottom"
asChild
>
<ColorBox onClick={() => setIsOpen((prev) => !prev)} ref={buttonRef} color={color} />
</DxcPopover>
<ColorText>
<DxcTextInput
value={inputValue}
onChange={handleInputChange}
size="fillParent"
pattern="^#[0-9A-Fa-f]{3}$|^#[0-9A-Fa-f]{6}$"
error={error}
onBlur={onBlur}
/>
</ColorText>
</ContentWrapper>
</CardWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { DxcButton, DxcFlex, DxcHeading } from "@dxc-technology/halstack-react";
import styled from "@emotion/styled";
import { ReactNode } from "react";

const ComponentPreviewWrapper = styled.div`
display: flex;
align-items: center;
gap: var(--spacing-gap-xxl);
`;

export const ComponentPreview = ({
component,
onDelete,
}: {
component: { name: string; preview: ReactNode };
onDelete: () => void;
}) => {
return (
<ComponentPreviewWrapper>
<DxcFlex direction="column" alignItems="flex-start" gap="var(--spacing-gap-s)">
<DxcHeading level={5} text={component.name} />
{component.preview}
</DxcFlex>
<DxcButton icon="delete" mode="tertiary" semantic="error" onClick={onDelete} />
</ComponentPreviewWrapper>
);
};
Loading