diff --git a/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSection.tsx b/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSection.tsx index 74b0068ed..d95dd138f 100644 --- a/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSection.tsx +++ b/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSection.tsx @@ -32,7 +32,7 @@ export function ConfiguratorSection(props: Props) { paddingTop={24} flexGrow={1} overflowY="auto" - key={props.endStep ? "endStep" : props.currentStep} + key={props.endStep ? "endStep" : props.singleStep ? "singleStep" : props.currentStep} > diff --git a/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSectionSteps.tsx b/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSectionSteps.tsx index 087e070a5..ed1193b26 100644 --- a/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSectionSteps.tsx +++ b/packages/configuration-builder/src/ConfiguratorSection/ConfiguratorSectionSteps.tsx @@ -8,41 +8,55 @@ import { } from "@buildo/bento-design-system"; import { useTranslation } from "react-i18next"; -export type Props = { - steps: readonly T[]; - stepLabel: (step: T) => LocalizedString; - currentStep: T; - onStepChange: (step: T) => void; - onComplete: () => void; - onCancel: () => void; - children: Children; -}; +export type Props = ( + | { + singleStep?: never; + steps: readonly T[]; + stepLabel: (step: T) => LocalizedString; + currentStep: T; + onStepChange: (step: T) => void; + } + | { + singleStep: true; + } +) & { onComplete: () => void; onCancel: () => void; children: Children }; export function ConfiguratorSectionSteps(props: Props) { const { t } = useTranslation(); - const currentStepIndex = props.steps.indexOf(props.currentStep); + const currentStepIndex = props.singleStep ? 0 : props.steps.indexOf(props.currentStep); const onNext = () => { - props.onStepChange(props.steps[currentStepIndex + 1]); + if (!props.singleStep) { + props.onStepChange(props.steps[currentStepIndex + 1]); + } }; const onBack = () => { - props.onStepChange(props.steps[currentStepIndex - 1]); + if (!props.singleStep) { + props.onStepChange(props.steps[currentStepIndex - 1]); + } }; return ( - ({ label: props.stepLabel(step) }))} - currentStep={currentStepIndex} - /> + {!props.singleStep && ( + ({ label: props.stepLabel(step) }))} + currentStep={currentStepIndex} + /> + )} - {props.stepLabel(props.currentStep)} + {!props.singleStep && ( + {props.stepLabel(props.currentStep)} + )} {props.children} ; +export type ElevationConfig = { + x: number; + y: number; + blur: number; + color: ColorToken; +}; + export type ThemeConfig = { colors: { brand: BrandColors; @@ -54,10 +61,11 @@ export type ThemeConfig = { pink: ColorConfig; }; }; + elevations: Record<"small" | "medium" | "large", ElevationConfig>; tokens: TokensConfig; }; -export type ThemeSection = "colors" | "tokens"; +export type ThemeSection = "colors" | "tokens" | "elevations"; type ConfiguratorStatus = { theme: ThemeConfig; @@ -77,7 +85,7 @@ export const defaultTokens: TokensConfig = { backgroundColor: { backgroundPrimary: colorToken("white"), backgroundSecondary: colorToken("Neutral-1"), - backgroundOverlay: colorToken("Neutral-20", 20), + backgroundOverlay: colorToken("Neutral-20", 40), backgroundPrimaryInverse: colorToken("Neutral-90"), backgroundSecondaryInverse: colorToken("Neutral-80"), backgroundInteractive: colorToken("Interactive-40"), @@ -92,7 +100,7 @@ export const defaultTokens: TokensConfig = { foregroundColor: { foregroundPrimary: colorToken("Neutral-90"), foregroundSecondary: colorToken("Neutral-50"), - foregroundPrimaryInverse: colorToken("Neutral-1"), + foregroundPrimaryInverse: colorToken("white"), foregroundSecondaryInverse: colorToken("Neutral-30"), foregroundInteractive: colorToken("Interactive-40"), foregroundInformative: colorToken("Informative-30"), @@ -121,8 +129,8 @@ export const defaultTokens: TokensConfig = { primaryTransparentHoverBackground: colorToken("Interactive-10", 40), primaryTransparentFocusBackground: colorToken("Interactive-10", 40), dangerSolidEnabledBackground: colorToken("Negative-40"), - dangerSolidHoverBackground: colorToken("Negative-40"), - dangerSolidFocusBackground: colorToken("Negative-40"), + dangerSolidHoverBackground: colorToken("Negative-60"), + dangerSolidFocusBackground: colorToken("Negative-60"), dangerTransparentEnabledBackground: colorToken("white", 0), dangerTransparentHoverBackground: colorToken("Negative-40", 10), dangerTransparentFocusBackground: colorToken("Negative-40", 10), @@ -130,8 +138,8 @@ export const defaultTokens: TokensConfig = { secondarySolidHoverBackground: colorToken("Neutral-20"), secondarySolidFocusBackground: colorToken("Neutral-20"), secondaryTransparentEnabledBackground: colorToken("white", 0), - secondaryTransparentHoverBackground: colorToken("Neutral-10", 40), - secondaryTransparentFocusBackground: colorToken("Neutral-10", 40), + secondaryTransparentHoverBackground: colorToken("Neutral-20", 40), + secondaryTransparentFocusBackground: colorToken("Neutral-20", 40), disabledSolidBackground: colorToken("Neutral-20", 20), disabledTransparentBackground: colorToken("white", 0), }, @@ -168,7 +176,7 @@ export const defaultTokens: TokensConfig = { outlineColor: { outlineInteractive: colorToken("Interactive-40"), outlineDecorative: colorToken("Neutral-20"), - outlineContainer: colorToken("Neutral-40", 20), + outlineContainer: colorToken("Neutral-5"), outlineInputEnabled: colorToken("Neutral-40"), outlineInputHover: colorToken("Neutral-60"), outlineInputFocus: colorToken("Interactive-40"), @@ -258,11 +266,32 @@ export function ConfiguratorStatusProvider(props: { children: Children }) { }, }, tokens: defaultTokens, + elevations: { + small: { + x: 0, + y: 4, + blur: 8, + color: { colorKey: "black", alpha: 16 }, + }, + medium: { + x: 0, + y: 8, + blur: 16, + color: { colorKey: "black", alpha: 16 }, + }, + large: { + x: 0, + y: 16, + blur: 32, + color: { colorKey: "black", alpha: 16 }, + }, + }, }); const [sections, setSections] = useState({ colors: false, tokens: false, + elevations: false, }); return ( diff --git a/packages/configuration-builder/src/ElevationsSection/ElevationCard.tsx b/packages/configuration-builder/src/ElevationsSection/ElevationCard.tsx new file mode 100644 index 000000000..dcb05159c --- /dev/null +++ b/packages/configuration-builder/src/ElevationsSection/ElevationCard.tsx @@ -0,0 +1,63 @@ +import { Body, Card, FormRow, Inset, NumberField, Stack, Title } from "@buildo/bento-design-system"; +import { ElevationConfig } from "../ConfiguratorStatusContext"; +import { Playground } from "../TokensSection/Playground"; +import { Form } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { ColorSelector } from "../TokensSection/ColorSelector"; + +type Props = { + elevation: "small" | "medium" | "large"; + config: ElevationConfig; + onChange: (elevation: ElevationConfig) => void; +}; + +export function ElevationCard(props: Props) { + const { t } = useTranslation(); + return ( + + + + + + <> + + + + +
+ + + {t(`Elevation.${props.elevation}`)} + {t(`Elevation.${props.elevation}Description`)} + + + props.onChange({ ...props.config, x })} + /> + props.onChange({ ...props.config, y })} + /> + props.onChange({ ...props.config, blur })} + /> + + + props.onChange({ ...props.config, color })} + /> + + +
+
+
+
+ ); +} diff --git a/packages/configuration-builder/src/ElevationsSection/ElevationsSection.tsx b/packages/configuration-builder/src/ElevationsSection/ElevationsSection.tsx new file mode 100644 index 000000000..2a83159b9 --- /dev/null +++ b/packages/configuration-builder/src/ElevationsSection/ElevationsSection.tsx @@ -0,0 +1,50 @@ +import { ConfiguratorSection } from "../ConfiguratorSection/ConfiguratorSection"; +import { useConfiguratorStatusContext } from "../ConfiguratorStatusContext"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { SectionCompleted } from "./SectionCompleted"; +import { Columns } from "@buildo/bento-design-system"; +import { ElevationCard } from "./ElevationCard"; + +export function ElevationsSection() { + const { theme, setTheme, completeSection } = useConfiguratorStatusContext(); + const [completed, setCompleted] = useState(false); + const { t } = useTranslation(); + const navigate = useNavigate(); + + if (completed) { + return ( + + + + ); + } + + return ( + navigate("/theme")} + onComplete={() => { + setCompleted(true); + completeSection("elevations"); + }} + > + + {(Object.keys(theme.elevations) as (keyof typeof theme.elevations)[]).map((elevation) => ( + { + setTheme({ + ...theme, + elevations: { ...theme.elevations, [elevation]: elevationConfig }, + }); + }} + /> + ))} + + + ); +} diff --git a/packages/configuration-builder/src/ElevationsSection/SectionCompleted.tsx b/packages/configuration-builder/src/ElevationsSection/SectionCompleted.tsx new file mode 100644 index 000000000..5db0693d2 --- /dev/null +++ b/packages/configuration-builder/src/ElevationsSection/SectionCompleted.tsx @@ -0,0 +1,28 @@ +import { ButtonLink, Feedback, Inline, Stack } from "@buildo/bento-design-system"; +import { useTranslation } from "react-i18next"; +import { IconConfetti } from "../PhosphorIcons"; + +export function SectionCompleted() { + const { t } = useTranslation(); + return ( + + + + + + + + ); +} diff --git a/packages/configuration-builder/src/MyTheme.tsx b/packages/configuration-builder/src/MyTheme.tsx index 7d889bab8..f0b1b65b8 100644 --- a/packages/configuration-builder/src/MyTheme.tsx +++ b/packages/configuration-builder/src/MyTheme.tsx @@ -126,7 +126,8 @@ export function MyTheme() { name={t("Theme.Foundations.Elevations.title")} description={t("Theme.Foundations.Elevations.description")} icon={IconSubtract} - disabled + kind={sections.elevations ? "done" : "todo"} + onClick={() => navigate("/theme/elevations")} /> , + }, { path: "/theme/tokens", element: , diff --git a/packages/configuration-builder/src/utils/preview.ts b/packages/configuration-builder/src/utils/preview.ts index 10d91d1da..eae038084 100644 --- a/packages/configuration-builder/src/utils/preview.ts +++ b/packages/configuration-builder/src/utils/preview.ts @@ -3,7 +3,7 @@ import { useConfiguratorStatusContext } from "../ConfiguratorStatusContext"; import { ColorToken, colorTokenToValue as _colorTokenToValue } from "./paletteUtils"; export function useConfiguredTheme(): BentoTheme & object { - const { tokens: _tokens, colors } = useConfiguratorStatusContext().theme; + const { tokens: _tokens, colors, elevations } = useConfiguratorStatusContext().theme; const tokens = _tokens as Record>; const colorTokenToValue = _colorTokenToValue(colors); @@ -70,6 +70,15 @@ export function useConfiguredTheme(): BentoTheme & object { outlineInputFocus: theme.outlineColor?.outlineInputFocus ? `inset 0px 0px 0px 2px ${theme.outlineColor.outlineInputFocus}` : undefined, + elevationSmall: `${elevations.small.x}px ${elevations.small.y}px ${ + elevations.small.blur + }px ${colorTokenToValue(elevations.small.color)}`, + elevationMedium: `${elevations.medium.x}px ${elevations.medium.y}px ${ + elevations.medium.blur + }px ${colorTokenToValue(elevations.medium.color)}`, + elevationLarge: `${elevations.large.x}px ${elevations.large.y}px ${ + elevations.large.blur + }px ${colorTokenToValue(elevations.large.color)}`, }, }; } diff --git a/packages/configuration-builder/src/utils/useConfigGeneratorTS.ts b/packages/configuration-builder/src/utils/useConfigGeneratorTS.ts index a86f9a719..7d35b64c0 100644 --- a/packages/configuration-builder/src/utils/useConfigGeneratorTS.ts +++ b/packages/configuration-builder/src/utils/useConfigGeneratorTS.ts @@ -1,4 +1,5 @@ -import { useConfiguratorStatusContext } from "../ConfiguratorStatusContext"; +import { match } from "ts-pattern"; +import { ElevationConfig, useConfiguratorStatusContext } from "../ConfiguratorStatusContext"; import { ColorToken, colorTokenToValue as _colorTokenToValue } from "./paletteUtils"; import prettier from "prettier/standalone"; import parserTypescript from "prettier/parser-typescript"; @@ -11,9 +12,24 @@ function colorTokenToVarName(colorToken: ColorToken): string { return `${tokenPart}_${colorToken.alpha}`; } +function elevationToVarName(elevation: "small" | "medium" | "large"): string { + return match(elevation) + .with("small", () => "elevationSmall") + .with("medium", () => "elevationMedium") + .with("large", () => "elevationLarge") + .exhaustive(); +} + export function useConfigGeneratorTS(): () => string { - const { tokens, colors } = useConfiguratorStatusContext().theme; + const { tokens, colors, elevations } = useConfiguratorStatusContext().theme; const colorTokenToValue = _colorTokenToValue(colors); + + function elevationToValue(elevation: ElevationConfig): string { + return `${elevation.x}px ${elevation.y}px ${elevation.blur}px \${${colorTokenToVarName( + elevation.color + )}}`; + } + return () => { const prelude = `import { BentoTheme } from "@buildo/bento-design-system";`; @@ -26,6 +42,12 @@ export function useConfigGeneratorTS(): () => string { } }); }); + Object.entries(elevations).forEach(([_, elevation]) => { + const rgba = colorTokenToValue(elevation.color); + if (rgba) { + usedColors[colorTokenToVarName(elevation.color)] = rgba; + } + }); const colorConsts = Object.entries(usedColors) .reduce((acc, [colorKey, color]) => { @@ -41,7 +63,25 @@ export function useConfigGeneratorTS(): () => string { }); themeCode += "},"; }); - themeCode += "};"; + themeCode += `boxShadow: {`; + Object.entries(tokens.outlineColor).forEach(([key, colorToken]) => { + themeCode += `${key}: \`inset 0px 0px 0px 1px \${${colorTokenToVarName(colorToken)}}\`,`; + }); + themeCode += `outlineInteractiveBottom: \`inset 0px 0px -1px 0px \${${colorTokenToVarName( + tokens.outlineColor.outlineInteractive + )}}\`,`; + themeCode += `outlineDecorativeBottom: \`inset 0px 0px -1px 0px \${${colorTokenToVarName( + tokens.outlineColor.outlineDecorative + )}}\`,`; + themeCode += `outlineNegativeStrong: \`inset 0px 0px 0px 2px \${${colorTokenToVarName( + tokens.outlineColor.outlineNegative + )}}\`,`; + Object.entries(elevations).forEach(([key, value]) => { + themeCode += `${elevationToVarName( + key as "small" | "medium" | "large" + )}: \`${elevationToValue(value)}\`,`; + }); + themeCode += `}};`; return prettier.format([prelude, colorConsts, themeCode].join("\n\n"), { parser: "typescript",