diff --git a/apps/website/pages/foundations/tokens.tsx b/apps/website/pages/foundations/tokens.tsx new file mode 100644 index 000000000..94653d712 --- /dev/null +++ b/apps/website/pages/foundations/tokens.tsx @@ -0,0 +1,13 @@ +import Head from "next/head"; +import TokensPage from "screens/foundations/tokens/TokensPage"; + +const Tokens = () => ( + <> + + Tokens — Halstack Design System + + + +); + +export default Tokens; \ No newline at end of file diff --git a/apps/website/screens/common/pagesList.ts b/apps/website/screens/common/pagesList.ts index 78fb36f02..83fd962f3 100644 --- a/apps/website/screens/common/pagesList.ts +++ b/apps/website/screens/common/pagesList.ts @@ -45,6 +45,7 @@ const foundationsLinks: LinkDetails[] = [ { label: "Layout", path: "/foundations/layout" }, { label: "Spacing", path: "/foundations/spacing" }, { label: "Typography", path: "/foundations/typography" }, + { label: "Tokens", path: "/foundations/tokens" }, ]; const componentsLinks = componentsList as LinkDetails[]; diff --git a/apps/website/screens/components/file-input/code/FileInputCodePage.tsx b/apps/website/screens/components/file-input/code/FileInputCodePage.tsx index 0be192ba0..4825913bb 100644 --- a/apps/website/screens/components/file-input/code/FileInputCodePage.tsx +++ b/apps/website/screens/components/file-input/code/FileInputCodePage.tsx @@ -24,31 +24,54 @@ const sections = [ - mode + accept - 'file' | 'filedrop' | 'dropzone' + string - Available modes of the component. - 'file' + The file types that the component accepts. Its value must be one of all the possible values of the HTML + file input's accept attribute. Please check the documentation{" "} + here. + - - label + buttonLabel string - Text to be placed above the component. + Text to be placed inside the button. - - buttonLabel - string + + + callbackFile + + + + + {"(val: {files: { file: File, error?: string, preview?: string }[], error?: string}) => void"} + + + + This function will be called when the user adds or deletes a file. That is, when the file input's inner + value is modified. The list of files will be sent as a parameter. In this function, the files can be + updated or returned as they come. These files must be passed to the value in order to be shown. - Text to be placed inside the button. - + + disabled + + boolean + + If true, the component will be disabled. + + false + + dropAreaLabel @@ -66,25 +89,21 @@ const sections = [ - - accept + label string - - The file types that the component accepts. Its value must be one of all the possible values of the HTML - file input's accept attribute. Please check the documentation{" "} - here. - + Text to be placed above the component. - - minSize + margin - number + 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin - The minimum file size (in bytes) allowed. If the size of the file does not comply this value, the file - will have an error. + Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left' and + 'right' properties in order to specify different margin sizes. - @@ -100,43 +119,24 @@ const sections = [ - + minSize - - - value - - - - {"{ file: File, error?: string, preview?: string }[]"} + number - An array of files representing the selected files. Each file has the following properties: - + The minimum file size (in bytes) allowed. If the size of the file does not comply this value, the file + will have an error. - - showPreview - - boolean - + mode - If true, if the file is an image, a preview of it will be shown. If not, an icon referring to the file - type will be shown. + 'file' | 'filedrop' | 'dropzone' + Available modes of the component. - false + 'file' @@ -154,42 +154,35 @@ const sections = [ - disabled + optional boolean - If true, the component will be disabled. + If true, the input will be optional, showing '(Optional)' next to the label. false + ref - - - callbackFile - - - - {"(files: { file: File, error?: string, preview?: string }[]) => void"} - - - This function will be called when the user adds or deletes a file. That is, when the file input's inner - value is modified. The list of files will be sent as a parameter. In this function, the files can be - updated or returned as they come. These files must be passed to the value in order to be shown. + {"React.Ref"} + Reference to the component. - - margin + showPreview - 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge' | Margin + boolean - Size of the margin to be applied to the component. You can pass an object with 'top', 'bottom', 'left' and - 'right' properties in order to specify different margin sizes. + If true, if the file is an image, a preview of it will be shown. If not, an icon referring to the file + type will be shown. + + + false - - tabIndex @@ -204,11 +197,30 @@ const sections = [ - ref - {"React.Ref"} + + + value + + + + {"{ file: File, error?: string, preview?: string }[]"} + + + An array of files representing the selected files. Each file has the following properties: + - Reference to the component. - diff --git a/apps/website/screens/foundations/tokens/TokensPage.tsx b/apps/website/screens/foundations/tokens/TokensPage.tsx new file mode 100644 index 000000000..bbae0bef1 --- /dev/null +++ b/apps/website/screens/foundations/tokens/TokensPage.tsx @@ -0,0 +1,321 @@ +import Code from "@/common/Code"; +import DocFooter from "@/common/DocFooter"; +import Image from "@/common/Image"; +import PageHeading from "@/common/PageHeading"; +import DxcQuickNavContainer from "@/common/QuickNavContainer"; +import QuickNavContainerLayout from "@/common/QuickNavContainerLayout"; +import { DxcAlert, DxcBulletedList, DxcFlex, DxcHeading, DxcLink, DxcParagraph } from "@dxc-technology/halstack-react"; +import Link from "next/link"; +import componentTokens from "./images/component_tokens.gif"; + +const sections = [ + { + title: "Introduction", + content: ( + <> + + Design tokens are the single source of truth for all design decisions in the Halstack Design + System. They create a shared language between design and development, ensuring consistency across products + while maintaining flexibility where customization is needed. + + + In Halstack, tokens define core attributes like colors, typography, spacing, and more—serving + as the foundation for scalable, themeable, and reusable component styling. + + + Tokens allow product teams to build faster, with greater consistency and confidence. + + + ), + }, + { + title: "From 2-layer to 3-layer token architecture", + content: ( + <> + + + When Halstack was first established, its token structure followed a two-tier model:{" "} + primitive tokens (the raw foundational values) and component tokens (styles + applied at the component level.) The intermediate alias layer—commonly used to connect + foundations to components—was missing. + + + Over time, this gap led to inconsistencies. Each component seemed to "speak its own language," with naming + conventions, color assignments, and styling logic varying wildly between them. Without clear documentation or + a shared taxonomy, this structure is hard to maintain and harder to scale. + + + ), + subSections: [ + { + title: "Why change", + content: ( + <> + + The absence of a middle layer and a consistent naming strategy cause several challenges: + + + + Inconsistent styles: Components with similar behaviors (e.g., hover states) often used + unrelated token values. + + + Difficult maintenance:Without a systematic link between primitives and components, + updating or theming became unpredictable. + + + Limited reusability: Repeated values were not abstracted into tokens, forcing + duplication and increasing the chance of drift. + + + + ), + }, + { + title: "Where we're going", + content: ( + <> + + The refactor aims to create a three-tier token architecture: + + + + Core: The raw values. + + + Alias: Contextual tokens that translate foundations into semantic meanings. + + + Component: Tokens applied to specific UI components, referencing aliases rather than + hardcoded values. + + + + At this stage, the Core and Alias layers are already in place and + actively used in both design and development workflows. These two layers give us a solid foundation of + structured, semantic tokens that ensure consistency and scalability. + + + The next step is to extend this work into the Component layer, where tokens will be + defined specifically for UI components by referencing existing Alias tokens. This will allow us to + standardize pattern across all Halstack components and give product teams even greater flexibility when + theming. We expect to begin working on this layer in the coming months. + + + ), + }, + ], + }, + { + title: "Structure", + subSections: [ + { + title: "Core", + content: ( + <> + + The Core layer contains the fundamental, non-contextual design values that form the base + of the system. They are not tied to any component or theme; instead, they describe pure design attributes + such as specific colors or type sizes. + + Foundations in Halstack include: + + + + Color + + - Defines the base color palette, organized and measured using the OKLCH color space for accuracy and + accessibility. + + + + Typography + {" "} + - Sets font families, weights, sizes, and line-heights that serve as the system's typographic + foundation. + + + + Spacing + {" "} + - A consistent set of spacing values to control layout and component padding/margins. + + + + Border + {" "} + - Defines border widths, radius, and styles for consistent corner and edge treatment. + + + + Shadow + + - Standardized elevation styles for depth and hierarchy in UI elements. + + + + Height + {" "} + - Predefined vertical dimensions for components and layouts. + + + + ), + }, + { + title: "Alias", + content: ( + <> + + The Alias layer maps core values to semantic meanings that align with the design language + and user context. + + + For example: color/bg/primary/strong → maps to a core token like{" "} + color/primary/700 (#6F4B97) + + + By separating raw values from their semantic role, we not only enable easy theming and quick adjustments + without touching the foundational values, but also establish clear usage expectations: + whether a token should be applied as a background (bg), foreground (fg), or{" "} + border element. This reduces ambiguity and ensures that tokens are applied consistently + across components and interfaces. + + + ), + }, + { + title: "Component", + content: ( + <> + + Component tokens define the styling for each component in Halstack, referencing{" "} + Alias tokens rather than hardcoded values. + + + Example: A button's hover background might use, which itself maps to an alias token, which then points to + a core color. + + This indirection: + + Keeps component styles consistent across the system. + Simplifies global updates. + + Allows products to theme components without rewriting every style rule. + + + Component tokens + + ), + }, + ], + }, + { + title: "Halstack's theming strategy and token layers", + content: ( + <> + + Until now, Halstack has supported two theming strategies—an{" "} + opinionated approach with limited, safe customization, and an{" "} + advanced approach with broader freedom. Several products have already benefited from these + strategies. + + + With the new token architecture, our goal is to continue supporting both levels of theming,{" "} + even though the underlying structure that makes them possible will change. This ensures that product teams can + rely on the same flexibility they know today, while also benefiting from a more scalable and consistent + system. + + + By aligning this strategy with our token architecture: + + + With the Core layer, we can already enable opinionated theming, exposing + tokens like colors, spacing, or typography in a way that is safe and predictable. + + + To unlock Advanced theming, we will rely on the upcoming{" "} + Component layer, which will allow deeper overrides at the component-token level. This + work is still in progress and will be rolled out in the coming months. + + + + + This structured approach ensures that theming in Halstack is flexible but safe—giving teams + the ability to adapt components to their needs while still protecting those{" "} + design decisions that are critical for accessibility such as maintaining proper{" "} + contrast ratios, minimum font sizes, and spacing values. In this way, we preserve both{" "} + brand flexibility and usability standards across all products. + + + ), + }, + { + title: "How tokens move from design to code", + content: ( + <> + + Design tokens in Halstack are created in{" "} + + Figma + + , where they serve as the foundation for all design decisions. From there, they evolve into development-ready + assets, ensuring a seamless bridge between design and code. + + + ), + subSections: [ + { + title: "Tokens in Design", + content: ( + <> + + In Halstack, tokens are defined and maintained as Variables in Figma. These variables + represent values like colors, spacing, typography, and more, and are applied directly in the design of{" "} + + components + {" "} + and interface flows. + + + ), + }, + { + title: "Tokens in Development", + content: ( + <> + + Once validated in Figma, tokens can be exported as structured JSON files, which makes + them consumable by development teams. ..... + + + ), + }, + ], + }, +]; + +export default function TokensPage() { + return ( + <> + + + + + + + + + + + + + ); +} diff --git a/apps/website/screens/foundations/tokens/images/component_tokens.gif b/apps/website/screens/foundations/tokens/images/component_tokens.gif new file mode 100644 index 000000000..ecab8b7c8 Binary files /dev/null and b/apps/website/screens/foundations/tokens/images/component_tokens.gif differ diff --git a/packages/lib/src/file-input/FileInput.stories.tsx b/packages/lib/src/file-input/FileInput.stories.tsx index 14b7c254a..05d1cd72d 100644 --- a/packages/lib/src/file-input/FileInput.stories.tsx +++ b/packages/lib/src/file-input/FileInput.stories.tsx @@ -2,7 +2,6 @@ import { Meta, StoryObj } from "@storybook/react"; import ExampleContainer from "../../.storybook/components/ExampleContainer"; import Title from "../../.storybook/components/Title"; import DxcFileInput from "./FileInput"; -import { userEvent, within } from "@storybook/test"; export default { title: "File Input", @@ -81,6 +80,10 @@ const FileInput = () => ( <DxcFileInput label="File input" value={[]} callbackFile={() => {}} /> </ExampleContainer> + <ExampleContainer> + <Title title="Optional" theme="light" level={4} /> + <DxcFileInput label="File input" value={[]} callbackFile={() => {}} optional /> + </ExampleContainer> <ExampleContainer> <Title title="With label and helper text" theme="light" level={4} /> <DxcFileInput label="File input" helperText="Please select files" value={[]} callbackFile={() => {}} /> diff --git a/packages/lib/src/file-input/FileInput.tsx b/packages/lib/src/file-input/FileInput.tsx index 9109460d4..08aab3317 100644 --- a/packages/lib/src/file-input/FileInput.tsx +++ b/packages/lib/src/file-input/FileInput.tsx @@ -105,7 +105,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( ( { mode = "file", - label = "", + label, buttonLabel, dropAreaLabel, helperText = "", @@ -119,6 +119,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( value, margin, tabIndex = 0, + optional = false, }, ref ): JSX.Element => { @@ -226,10 +227,12 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( return ( <FileInputContainer margin={margin} ref={ref}> - <Label htmlFor={fileInputId} disabled={disabled}> - {label} - </Label> - <HelperText disabled={disabled}>{helperText}</HelperText> + {label && ( + <Label disabled={disabled} hasMargin={!helperText} htmlFor={fileInputId}> + {label} {optional && <span>{translatedLabels.formFields.optionalLabel}</span>} + </Label> + )} + {helperText && <HelperText disabled={disabled}>{helperText}</HelperText>} {mode === "file" ? ( <FileContainer singleFileMode={!multiple && files.length === 1}> <ValueInput @@ -240,6 +243,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( onChange={selectFiles} disabled={disabled} readOnly + required={!optional} /> <DxcButton mode="secondary" @@ -282,6 +286,7 @@ const DxcFileInput = forwardRef<RefType, FileInputPropsType>( onChange={selectFiles} disabled={disabled} readOnly + required={!optional} /> <DragDropArea isDragging={isDragging} diff --git a/packages/lib/src/file-input/types.ts b/packages/lib/src/file-input/types.ts index ab0c39a2e..5c891e114 100644 --- a/packages/lib/src/file-input/types.ts +++ b/packages/lib/src/file-input/types.ts @@ -1,14 +1,14 @@ import { Margin, Space } from "../common/utils"; export type FileData = { - /** - * Selected file. - */ - file: File; /** * Error of the file. If it is defined, it will be shown and the file item will be mark as invalid. */ error?: string; + /** + * Selected file. + */ + file: File; /** * Preview of the file. */ @@ -17,62 +17,73 @@ export type FileData = { type CommonProps = { /** - * Text to be placed above the component. + * The file types that the component accepts. Its value must be one of all the possible values of the HTML file input's accept attribute. */ - label?: string; + accept?: string; /** * Text to be placed inside the button. */ buttonLabel?: string; /** - * Helper text to be placed above the component. + * This function will be called when the user selects or drops a file. The list of files will be sent as a parameter. + * In this function, the files can be updated or returned as they come. These files must be passed to the value in order to be shown. */ - helperText?: string; + callbackFile: (files: FileData[]) => void; /** - * The file types that the component accepts. Its value must be one of all the possible values of the HTML file input's accept attribute. + * If true, the component will be disabled. */ - accept?: string; + disabled?: boolean; /** - * An array of files representing the selected files. + * Helper text to be placed above the component. */ - value: FileData[]; + helperText?: string; /** - * The minimum file size (in bytes) allowed. If the size of the file does not comply the minSize, the file will have an error. + * Text to be placed above the component. */ - minSize?: number; + label?: string; + /** + * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). + * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. + */ + margin?: Space | Margin; /** * The maximum file size (in bytes) allowed. If the size of the file does not comply the maxSize, the file will have an error. */ maxSize?: number; /** - * If true, if the file is an image, a preview of it will be shown. If not, an icon refering to the file type will be shown. + * The minimum file size (in bytes) allowed. If the size of the file does not comply the minSize, the file will have an error. */ - showPreview?: boolean; + minSize?: number; /** * If true, the component allows multiple file items and will show all of them. If false, only one file will be shown, and if there is already one * file selected and a new one is chosen, it will be replaced by the new selected one. */ multiple?: boolean; /** - * If true, the component will be disabled. - */ - disabled?: boolean; - /** - * This function will be called when the user selects or drops a file. The list of files will be sent as a parameter. - * In this function, the files can be updated or returned as they come. These files must be passed to the value in order to be shown. + * If true, the input will be optional, showing '(Optional)' + * next to the label. Otherwise, the field will be considered required and an error will be + * passed as a parameter to the OnBlur and onChange functions when it has + * not been filled. */ - callbackFile: (files: FileData[]) => void; + optional?: boolean; /** - * Size of the margin to be applied to the component ('xxsmall' | 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge' | 'xxlarge'). - * You can pass an object with 'top', 'bottom', 'left' and 'right' properties in order to specify different margin sizes. + * If true, if the file is an image, a preview of it will be shown. If not, an icon refering to the file type will be shown. */ - margin?: Space | Margin; + showPreview?: boolean; /** * Value of the tabindex attribute. */ tabIndex?: number; + /** + * An array of files representing the selected files. + */ + value: FileData[]; }; type DropModeProps = CommonProps & { + /** + * Text to be placed inside the drag and drop zone alongside the button. + */ + dropAreaLabel?: string; /** * Uses one of the available file input modes: * 'file': Files are selected by clicking the button and selecting it through the file explorer. @@ -81,12 +92,12 @@ type DropModeProps = CommonProps & { * The drag and drop area of this mode is bigger than the one of the filedrop mode. */ mode: "filedrop" | "dropzone"; +}; +type FileModeProps = CommonProps & { /** * Text to be placed inside the drag and drop zone alongside the button. */ - dropAreaLabel?: string; -}; -type FileModeProps = CommonProps & { + dropAreaLabel?: never; /** * Uses one of the available file input modes: * 'file': Files are selected by clicking the button and selecting it through the file explorer. @@ -95,10 +106,6 @@ type FileModeProps = CommonProps & { * The drag and drop area of this mode is bigger than the one of the filedrop mode. */ mode?: "file"; - /** - * Text to be placed inside the drag and drop zone alongside the button. - */ - dropAreaLabel?: never; }; /** @@ -112,14 +119,14 @@ type Props = DropModeProps | FileModeProps; * Single file item preview. */ export type FileItemProps = { - fileName?: string; error?: string; + fileName?: string; + onDelete: (fileName: string) => void; + preview: string; showPreview: boolean; singleFileMode: boolean; - preview: string; - type: string; - onDelete: (fileName: string) => void; tabIndex: number; + type: string; }; export default Props; diff --git a/packages/lib/src/file-input/utils.ts b/packages/lib/src/file-input/utils.ts index 7b76761e7..cf0f4e9f4 100644 --- a/packages/lib/src/file-input/utils.ts +++ b/packages/lib/src/file-input/utils.ts @@ -24,4 +24,4 @@ export const isFileIncluded = (file: FileData, fileList: FileData[]) => { lastModified === file.file.lastModified && webkitRelativePath === file.file.webkitRelativePath ); -}; \ No newline at end of file +};