diff --git a/.changeset/cyan-pans-sit.md b/.changeset/cyan-pans-sit.md new file mode 100644 index 00000000000..c08939c1f05 --- /dev/null +++ b/.changeset/cyan-pans-sit.md @@ -0,0 +1,5 @@ +--- +"@hashintel/ds-components": patch +--- + +New Button component added to design system diff --git a/libs/@hashintel/ds-components/scripts/figma-variables.json b/libs/@hashintel/ds-components/scripts/figma-variables.json index daeeac7cdfb..ccbe3de5b66 100644 --- a/libs/@hashintel/ds-components/scripts/figma-variables.json +++ b/libs/@hashintel/ds-components/scripts/figma-variables.json @@ -1666,6 +1666,11 @@ "type": "fontSize", "resolvedType": "FLOAT" }, + "xxs": { + "value": 10, + "type": "fontSize", + "resolvedType": "FLOAT" + }, "xs": { "value": 12, "type": "fontSize", diff --git a/libs/@hashintel/ds-components/src/components/Button/button.recipe.ts b/libs/@hashintel/ds-components/src/components/Button/button.recipe.ts new file mode 100644 index 00000000000..ccd8216b6f8 --- /dev/null +++ b/libs/@hashintel/ds-components/src/components/Button/button.recipe.ts @@ -0,0 +1,728 @@ +import { sva } from "@hashintel/ds-helpers/css"; + +export const styles = sva({ + slots: ["button", "loadingContainer", "loadingContent", "iconText"], + base: { + button: { + "--button-border-width": "1px", + cursor: "pointer", + display: "inline-block", + fontWeight: "medium", + border: "var(--button-border-width) solid", + textAlign: "center", + transition: + "[background 0.15s ease, color 0.15s ease, border 0.15s ease]", + "&:focus-visible": { + outline: "2px solid", + }, + '&[aria-disabled="true"]': { + cursor: "auto", + }, + }, + loadingContainer: { + position: "absolute", + inset: "0", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + loadingContent: { visibility: "hidden", display: "flex" }, + iconText: {}, + }, + variants: { + size: { + xxs: { + button: { + paddingX: "2", + paddingY: "[1px]", + borderRadius: "md", + textStyle: "xxs", + minWidth: `[calc(1em * ${1.6} * var(--leading-factor, 1) + 1px * 2 + var(--button-border-width) * 2)]`, + "--button-icon-margin": "var(--spacing-1)", + }, + }, + xs: { + button: { + paddingX: "2", + paddingY: "0", + borderRadius: "md", + textStyle: "xs", + minWidth: `[calc(1em * ${1.6} * var(--leading-factor, 1) + var(--button-border-width) * 2)]`, + "--button-icon-margin": "var(--spacing-1)", + }, + }, + sm: { + button: { + paddingX: "2", + paddingY: "0.5", + borderRadius: "lg", + textStyle: "sm", + minWidth: `[calc(1em * ${1.6} * var(--leading-factor, 1) + var(--spacing-0\\.5) * 2 + var(--button-border-width) * 2)]`, + "--button-icon-margin": "var(--spacing-1\\.5)", + }, + }, + md: { + button: { + paddingX: "3", + paddingY: "1", + borderRadius: "lg", + textStyle: "base", + minWidth: `[calc(1em * ${1.5} * var(--leading-factor, 1) + var(--spacing-1) * 2 + var(--button-border-width) * 2)]`, + "--button-icon-margin": "var(--spacing-2)", + }, + }, + lg: { + button: { + paddingX: "4", + paddingY: "2", + borderRadius: "lg", + textStyle: "base", + minWidth: `[calc(1em * ${1.5} * var(--leading-factor, 1) + var(--spacing-2) * 2 + var(--button-border-width) * 2)]`, + "--button-icon-margin": "var(--spacing-2)", + }, + }, + }, + shape: { + default: {}, + round: { button: { borderRadius: "full" } }, + }, + tone: { + neutral: { + button: { + "&:focus-visible": { + outlineColor: "black.a60", + }, + }, + }, + brand: { + button: { + "&:focus-visible": { + outlineColor: "blue.a60", + }, + }, + }, + error: { + button: { + "&:focus-visible": { + outlineColor: "red.a60", + }, + }, + }, + }, + variant: { + solid: {}, + subtle: {}, + ghost: { + button: { + background: "[transparent]", + borderColor: "[transparent]", + }, + }, + link: { + button: { + display: "inline", + padding: "0 !important", + border: "0 !important", + background: "[none !important]", + textAlign: "[inherit !important]", + minWidth: "0 !important", + fontWeight: "semibold", + "&:not([aria-disabled=true]):hover": { + textDecoration: "underline", + }, + "&:focus-visible": { + outlineOffset: "0.5", + }, + }, + }, + linkSubtle: { + button: { + display: "inline", + padding: "0 !important", + border: "0 !important", + background: "[none !important]", + textAlign: "[inherit !important]", + minWidth: "0 !important", + fontWeight: "semibold", + "&:focus-visible": { + outlineOffset: "0.5", + }, + }, + }, + }, + isLoading: { + true: { button: { position: "relative" } }, + }, + hasIcon: { + true: { + button: { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + }, + }, + }, + hasIconLeft: { + true: { iconText: { marginLeft: "var(--button-icon-margin)" } }, + }, + hasIconRight: { + true: { iconText: { marginRight: "var(--button-icon-margin)" } }, + }, + isDisabled: { + true: {}, + }, + isPressed: { + true: {}, + }, + isIconOnly: { + true: {}, + }, + }, + compoundVariants: [ + // ── Icon-only (square) ── + { + size: "xxs", + isIconOnly: true, + css: { + button: { paddingX: "0" }, + }, + }, + { + size: "xs", + isIconOnly: true, + css: { + button: { paddingX: "0" }, + }, + }, + { + size: "sm", + isIconOnly: true, + css: { + button: { paddingX: "0.5" }, + }, + }, + { + size: "md", + isIconOnly: true, + css: { + button: { paddingX: "1" }, + }, + }, + { + size: "lg", + isIconOnly: true, + css: { + button: { paddingX: "2" }, + }, + }, + // ── Solid + Neutral ── + { + variant: "solid", + tone: "neutral", + css: { + button: { + background: "neutral.s120", + borderColor: "neutral.s120", + color: "fg.onSolid", + "&:not([aria-disabled=true]):hover": { + background: "neutral.s110", + borderColor: "neutral.s110", + }, + }, + }, + }, + { + variant: "solid", + tone: "neutral", + isPressed: true, + css: { + button: { + background: "neutral.s115", + borderColor: "neutral.s115", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.35)]", + }, + }, + }, + { + variant: "solid", + tone: "neutral", + isDisabled: true, + css: { + button: { + color: "neutral.s20", + background: "neutral.s80", + borderColor: "neutral.s80", + }, + }, + }, + + // ── Solid + Brand (blue) ── + { + variant: "solid", + tone: "brand", + css: { + button: { + background: "blue.s90", + borderColor: "blue.s90", + color: "fg.onSolid", + "&:not([aria-disabled=true]):hover": { + background: "blue.s85", + borderColor: "blue.s85", + }, + }, + }, + }, + { + variant: "solid", + tone: "brand", + isPressed: true, + css: { + button: { + background: "blue.s95", + borderColor: "blue.s95", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.15)]", + }, + }, + }, + { + variant: "solid", + tone: "brand", + isDisabled: true, + css: { + button: { + background: "blue.s60", + borderColor: "blue.s60", + }, + }, + }, + + // ── Solid + Error (red) ── + { + variant: "solid", + tone: "error", + css: { + button: { + background: "red.s90", + borderColor: "red.s90", + color: "fg.onSolid", + "&:not([aria-disabled=true]):hover": { + background: "red.s85", + borderColor: "red.s85", + }, + }, + }, + }, + { + variant: "solid", + tone: "error", + isPressed: true, + css: { + button: { + background: "red.s95", + borderColor: "red.s95", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.15)]", + }, + }, + }, + { + variant: "solid", + tone: "error", + isDisabled: true, + css: { + button: { + background: "red.s60", + borderColor: "red.s60", + }, + }, + }, + // ── Subtle (neutral) ── + { + variant: "subtle", + tone: "neutral", + css: { + button: { + background: "neutral.a00", + borderColor: "neutral.a60", + color: "neutral.s120", + "&:not([aria-disabled=true]):hover": { + background: "neutral.a20", + borderColor: "neutral.a80", + }, + }, + }, + }, + { + variant: "subtle", + tone: "neutral", + isPressed: true, + css: { + button: { + background: "neutral.a05", + color: "neutral.s115", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.05)]", + }, + }, + }, + { + variant: "subtle", + tone: "neutral", + isDisabled: true, + css: { + button: { + background: "neutral.a20", + borderColor: "neutral.a50", + color: "neutral.s80", + }, + }, + }, + // ── Subtle + Brand ── + { + variant: "subtle", + tone: "brand", + css: { + button: { + background: "blue.a20", + borderColor: "blue.a60", + color: "blue.s90", + "&:not([aria-disabled=true]):hover": { + background: "blue.a30", + borderColor: "blue.a70", + }, + }, + }, + }, + { + variant: "subtle", + tone: "brand", + isPressed: true, + css: { + button: { + color: "blue.s85", + background: "blue.a25", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.05)]", + }, + }, + }, + { + variant: "subtle", + tone: "brand", + isDisabled: true, + css: { + button: { + background: "blue.a20", + borderColor: "blue.a40", + color: "blue.s70", + }, + }, + }, + // ── Subtle + Error (red) ── + { + variant: "subtle", + tone: "error", + css: { + button: { + background: "red.a20", + borderColor: "red.a60", + color: "red.s90", + "&:not([aria-disabled=true]):hover": { + background: "red.a25", + borderColor: "red.a70", + }, + }, + }, + }, + { + variant: "subtle", + tone: "error", + isPressed: true, + css: { + button: { + color: "red.s85", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.05)]", + }, + }, + }, + { + variant: "subtle", + tone: "error", + isDisabled: true, + css: { + button: { + background: "red.a15", + borderColor: "red.a30", + color: "red.s70", + }, + }, + }, + + // ── Ghost (neutral) ── + { + variant: "ghost", + tone: "neutral", + css: { + button: { + color: "neutral.s120", + "&:not([aria-disabled=true]):hover": { + background: "neutral.a20", + borderColor: "neutral.a60", + }, + }, + }, + }, + { + variant: "ghost", + tone: "neutral", + isPressed: true, + css: { + button: { + color: "neutral.s115", + background: "neutral.a10", + borderColor: "neutral.a50", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.05)]", + }, + }, + }, + { + variant: "ghost", + tone: "neutral", + isDisabled: true, + css: { + button: { + color: "neutral.s70", + }, + }, + }, + + // ── Ghost (brand) ── + { + variant: "ghost", + tone: "brand", + css: { + button: { + color: "blue.s105", + "&:not([aria-disabled=true]):hover": { + background: "blue.a30", + borderColor: "blue.a70", + }, + }, + }, + }, + { + variant: "ghost", + tone: "brand", + isPressed: true, + css: { + button: { + color: "blue.s85", + background: "blue.a25", + borderColor: "blue.a50", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.05)]", + }, + }, + }, + { + variant: "ghost", + tone: "brand", + isDisabled: true, + css: { + button: { + color: "blue.s70", + }, + }, + }, + // ── Ghost (error) ── + { + variant: "ghost", + tone: "error", + css: { + button: { + color: "red.s105", + "&:not([aria-disabled=true]):hover": { + background: "red.a25", + borderColor: "red.a70", + }, + }, + }, + }, + { + variant: "ghost", + tone: "error", + isPressed: true, + css: { + button: { + color: "red.s85", + background: "red.a20", + borderColor: "red.a60", + boxShadow: "[inset 0 2px 4px rgba(0,0,0,0.05)]", + }, + }, + }, + { + variant: "ghost", + tone: "error", + isDisabled: true, + css: { + button: { + color: "red.s70", + }, + }, + }, + + // ── Link ── + { + variant: "link", + isPressed: true, + css: { + button: { + textDecoration: "underline", + }, + }, + }, + { + variant: "link", + isDisabled: true, + css: { + button: { + opacity: 0.4, + }, + }, + }, + // ── Link (neutral) ── + { + variant: "link", + tone: "neutral", + css: { + button: { + color: "[inherit]", + }, + }, + }, + // ── Link (brand) ── + { + variant: "link", + tone: "brand", + css: { + button: { + color: "blue.s105", + }, + }, + }, + // ── Link (error) ── + { + variant: "link", + tone: "error", + css: { + button: { + color: "red.s100", + }, + }, + }, + + // ── Link Subtle ── + { + variant: "linkSubtle", + isDisabled: true, + css: { + button: { + opacity: 0.4, + }, + }, + }, + // ── Link Subtle (neutral) ── + { + variant: "linkSubtle", + tone: "neutral", + css: { + button: { + color: "neutral.s120", + "&:not([aria-disabled=true]):hover": { + color: "neutral.s100", + }, + }, + }, + }, + { + variant: "linkSubtle", + tone: "neutral", + isPressed: true, + css: { + button: { + color: "neutral.s110", + }, + }, + }, + // ── Link (brand) ── + { + variant: "linkSubtle", + tone: "brand", + css: { + button: { + color: "blue.s105", + "&:not([aria-disabled=true]):hover": { + color: "blue.s80", + }, + }, + }, + }, + { + variant: "linkSubtle", + tone: "brand", + isPressed: true, + css: { + button: { + color: "blue.s115", + }, + }, + }, + // ── Link (error) ── + { + variant: "linkSubtle", + tone: "error", + css: { + button: { + color: "red.s100", + "&:not([aria-disabled=true]):hover": { + color: "red.s80", + }, + }, + }, + }, + { + variant: "linkSubtle", + tone: "error", + isPressed: true, + css: { + button: { + color: "red.s115", + }, + }, + }, + { + variant: "link", + size: "md", + css: { + button: { + fontSize: "[inherit !important]", + lineHeight: "[inherit !important]", + letterSpacing: "inherit !important", + }, + }, + }, + { + variant: "linkSubtle", + size: "md", + css: { + button: { + fontSize: "[inherit !important]", + lineHeight: "[inherit !important]", + letterSpacing: "inherit !important", + }, + }, + }, + ], + defaultVariants: { + size: "md", + tone: "neutral", + variant: "solid", + }, +}); diff --git a/libs/@hashintel/ds-components/src/components/Button/button.stories.tsx b/libs/@hashintel/ds-components/src/components/Button/button.stories.tsx index 06e1b207d8e..b181eb485b5 100644 --- a/libs/@hashintel/ds-components/src/components/Button/button.stories.tsx +++ b/libs/@hashintel/ds-components/src/components/Button/button.stories.tsx @@ -1,264 +1,285 @@ +import { css } from "@hashintel/ds-helpers/css"; import type { Story, StoryDefault } from "@ladle/react"; +import { Fragment, useState } from "react"; -import { Button, type ButtonProps } from "./button"; +import { formInputSizes } from "../../util/form-shared"; +import { Icon, iconNames } from "../Icon/icon"; +import { + Button as ButtonComponent, + type ButtonElementProps, + iconSizeMap, + type Tone, + type Variant, +} from "./button"; + +const variants: Variant[] = ["solid", "subtle", "ghost", "link", "linkSubtle"]; +const tones: Tone[] = ["neutral", "brand", "error"]; export default { - title: "Legacy/Button", + title: "Components/Button", + parameters: { + layout: "centered", + }, argTypes: { variant: { - control: { type: "select" }, - options: ["primary", "secondary", "ghost"], - description: "The variant style of the button", + control: { + type: "select", + options: variants, + }, }, - colorScheme: { - control: { type: "select" }, - options: ["brand", "neutral", "critical"], - description: "The color scheme of the button", + tone: { + control: { + type: "select", + options: [undefined, ...tones], + }, }, size: { - control: { type: "select" }, - options: ["xs", "sm", "md", "lg"], - description: "The size of the button", + control: { + type: "select", + options: formInputSizes, + }, + }, + iconName: { + control: { + type: "select", + options: [undefined, ...iconNames], + }, }, - isLoading: { + iconPosition: { + control: { + type: "select", + options: ["left", "right"], + }, + }, + loading: { control: { type: "boolean" }, - description: "Whether the button is in a loading state", }, disabled: { control: { type: "boolean" }, - description: "Whether the button is disabled", + }, + pressed: { + control: { type: "boolean" }, + }, + shape: { + control: { + type: "select", + options: ["default", "round"], + }, }, }, args: { children: "Button", - variant: "primary", - colorScheme: "brand", + variant: "solid", size: "md", + disabled: false, + loading: false, }, -} satisfies StoryDefault; - -// Primary Variants -export const PrimaryBrand: Story = (args) => + + + + + ))} + + ))} + + ); }; -// Secondary Variants -export const SecondaryBrand: Story = (args) => ( - + + + + {size} + + + ))} + + ))} + ); -SecondaryCritical.args = { - variant: "secondary", - colorScheme: "critical", - children: "Secondary Critical", -}; - -// Ghost Variants -export const GhostBrand: Story = (args) => - - - - -); Sizes.parameters = { - controls: { exclude: ["size"] }, -}; - -// States -export const Loading: Story = (args) => - - + + } + suffix={} + onClick={() => {}} + > + Both Icons + + + } + suffix={} + onClick={() => {}} + tooltip="Icon only with prefix and suffix" + > + {undefined} + + {/* eslint-enable */} + + ))} + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any -- same union spread issue */} + - + ))} + +); -
-

Secondary

-
- - - -
-
+WithIcon.parameters = { + controls: { exclude: ["iconName", "iconPosition", "variant", "loading"] }, +}; -
-

Ghost

-
- - -
-
- -
-

Sizes

-
- - - - -
-
- -
-

States

-
- - -
-
- + ))} + ); -AllVariants.parameters = { - controls: { disable: true }, -}; diff --git a/libs/@hashintel/ds-components/src/components/Button/button.tsx b/libs/@hashintel/ds-components/src/components/Button/button.tsx index eb53704066f..955bd93869e 100644 --- a/libs/@hashintel/ds-components/src/components/Button/button.tsx +++ b/libs/@hashintel/ds-components/src/components/Button/button.tsx @@ -1,434 +1,244 @@ -import { css, cva, cx } from "@hashintel/ds-helpers/css"; -import type { ReactNode } from "react"; +/* eslint-disable react/destructuring-assignment, react/button-has-type, @typescript-eslint/prefer-nullish-coalescing */ +import { cx } from "@hashintel/ds-helpers/css"; +import type { ExclusifyUnion, RequireAtLeastOne } from "type-fest"; -export interface ButtonProps - extends Omit, "type"> { - /** The variant style of the button */ - variant?: "primary" | "secondary" | "ghost" | "error"; - /** The color scheme of the button */ - colorScheme?: "brand" | "neutral" | "critical" | "subtle"; - /** The size of the button */ - size?: "xs" | "sm" | "md" | "lg"; - /** Whether the button is in a loading state */ - isLoading?: boolean; - /** Optional icon to display on the left */ - iconLeft?: ReactNode; - /** Optional icon to display on the right */ - iconRight?: ReactNode; - /** Button type */ +import type { FormInputSize } from "../../util/form-shared"; +import { Icon, type IconName } from "../Icon/icon"; +import { LoadingSpinner } from "../Loading/loading-spinner"; +import { styles } from "./button.recipe"; + +export type Variant = "solid" | "subtle" | "ghost" | "link" | "linkSubtle"; +export type Tone = "neutral" | "brand" | "error"; // success, warning, etc + +type SharedButtonProps = + { + className?: string; + /** The overall style of the button */ + variant?: Variant; + /** Sets the color treatment of the button for destructive actions. */ + tone?: Tone; + /** The size (height) of the button */ + size?: FormInputSize; + /** The shape of the button. Non default shapes should VERY rarely be used */ + shape?: "default" | "round"; + /** Whether the button is in a loading state */ + loading?: boolean; + /** Whether the button is in a pressed/active state */ + pressed?: boolean; + disabled?: boolean; + tabIndex?: number; + onClick?: React.ButtonHTMLAttributes["onClick"]; + onMouseDown?: React.ButtonHTMLAttributes["onMouseDown"]; + onMouseUp?: React.ButtonHTMLAttributes["onMouseUp"]; + onMouseEnter?: React.ButtonHTMLAttributes["onMouseEnter"]; + onMouseLeave?: React.ButtonHTMLAttributes["onMouseLeave"]; + onKeyDown?: React.ButtonHTMLAttributes["onKeyDown"]; + onFocus?: React.ButtonHTMLAttributes["onFocus"]; + onBlur?: React.ButtonHTMLAttributes["onBlur"]; + } & RequireAtLeastOne<{ + tooltip?: string; + children?: React.ReactNode; + }> & + React.AriaAttributes; + +/** We support 2 apis for button icons, a simple api that maps directly to icon names + * or a more customizable api for more complex use cases */ +type ButtonIconProps = ExclusifyUnion< + | { + /** Optional icon to display */ + iconName?: IconName; + /** Whether the icon should be on the left or right */ + iconPosition?: "left" | "right"; + } + | { + /** Optional element to include at the beginning of a button */ + prefix?: React.ReactNode; + /** Optional element to include at the end of a button */ + suffix?: React.ReactNode; + } +>; + +type ButtonElementOnlyProps = { + /** Button type - defaults to "button" */ type?: "button" | "submit" | "reset"; -} + href?: never; + target?: never; + download?: never; + ref?: React.Ref; +} & RequireAtLeastOne<{ + onClick: React.ButtonHTMLAttributes["onClick"]; + type: "submit" | "reset"; +}>; -const LoadingSpinner = () => ( - - - -); +export type AnchorElementOnlyProps = { + href: string; + target?: "_blank"; + download?: boolean; + type?: never; + ref?: React.Ref; +}; -const buttonRecipe = cva({ - base: { - position: "relative", - display: "inline-flex", - alignItems: "center", - justifyContent: "center", - fontFamily: "body", - fontWeight: "medium", - lineHeight: "none", - cursor: "pointer", - overflow: "hidden", - whiteSpace: "nowrap", - _disabled: { - cursor: "not-allowed", - opacity: 0.4, - }, - }, - variants: { - variant: { - primary: {}, - secondary: {}, - ghost: { - borderWidth: "1px", - borderStyle: "solid", - borderColor: "[transparent]", - }, - error: {}, - }, - colorScheme: { - brand: {}, - neutral: {}, - critical: {}, - subtle: {}, - }, - size: { - xs: { - height: "6", - paddingX: "2", - paddingY: "1.5", - fontSize: "xs", - borderRadius: "md", - }, - sm: { - height: "7", - paddingX: "2", - paddingY: "1.5", - fontSize: "sm", - borderRadius: "lg", - }, - md: { - height: "8", - paddingX: "2.5", - paddingY: "2", - fontSize: "sm", - borderRadius: "lg", - }, - lg: { - height: "10", - paddingX: "3.5", - paddingY: "2.5", - fontSize: "base", - borderRadius: "xl", - }, - }, - isLoading: { - true: { - opacity: 0.4, - pointerEvents: "none", - }, - false: {}, - }, - }, - compoundVariants: [ - // Primary + Brand - { - variant: "primary", - colorScheme: "brand", - css: { - backgroundColor: "blue.bg.solid", - color: "blue.fg.onSolid", - _hover: { - backgroundColor: "blue.bg.solid.hover", - }, - _active: { - backgroundColor: "blue.bg.solid.active", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "blue.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Primary + Neutral - { - variant: "primary", - colorScheme: "neutral", - css: { - backgroundColor: "bg.solid", - color: "fg.onSolid", - _hover: { - backgroundColor: "bg.solid.hover", - }, - _active: { - backgroundColor: "bg.solid.active", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "neutral.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Primary + Critical - { - variant: "primary", - colorScheme: "critical", - css: { - backgroundColor: "status.error.bg.solid", - color: "status.error.fg.onSolid", - _hover: { - backgroundColor: "status.error.bg.solid.hover", - }, - _active: { - backgroundColor: "status.error.bg.solid.active", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "red.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Secondary + Brand - { - variant: "secondary", - colorScheme: "brand", - css: { - backgroundColor: "bg.subtle", - borderWidth: "1px", - borderStyle: "solid", - borderColor: "blue.bg.solid", - color: "blue.fg.link", - _hover: { - backgroundColor: "blue.bg.subtle.hover", - borderColor: "blue.bg.solid.hover", - color: "blue.fg.link.hover", - }, - _active: { - backgroundColor: "blue.bg.subtle.active", - borderColor: "blue.bg.solid.active", - color: "blue.fg.link.hover", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "blue.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Secondary + Neutral - { - variant: "secondary", - colorScheme: "neutral", - css: { - backgroundColor: "bg.subtle", - borderWidth: "1px", - borderStyle: "solid", - borderColor: "bd.solid", - color: "fg.body", - _hover: { - backgroundColor: "bg.subtle.hover", - borderColor: "bd.solid.hover", - }, - _active: { - backgroundColor: "bg.subtle.active", - borderColor: "bd.solid.hover", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "neutral.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Secondary + Critical - { - variant: "secondary", - colorScheme: "critical", - css: { - backgroundColor: "bg.subtle", - borderWidth: "1px", - borderStyle: "solid", - borderColor: "status.error.bg.solid", - color: "status.error.fg.heading", - _hover: { - backgroundColor: "status.error.bg.subtle.hover", - borderColor: "status.error.bg.solid.hover", - color: "status.error.fg.heading", - }, - _active: { - backgroundColor: "status.error.bg.subtle.active", - borderColor: "status.error.bg.solid.active", - color: "status.error.fg.heading", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "red.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Ghost + Brand - { - variant: "ghost", - colorScheme: "brand", - css: { - backgroundColor: "[transparent]", - color: "blue.fg.link", - _hover: { - backgroundColor: "blue.bg.subtle.hover", - borderColor: "blue.bd.solid", - color: "blue.fg.link.hover", - }, - _active: { - backgroundColor: "blue.bg.subtle.active", - borderColor: "blue.bd.solid", - color: "blue.fg.link.hover", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "blue.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Ghost + Neutral - { - variant: "ghost", - colorScheme: "neutral", - css: { - backgroundColor: "[transparent]", - color: "fg.muted", - _hover: { - backgroundColor: "bg.subtle.hover", - borderColor: "bd.solid.hover", - color: "fg.heading", - }, - _active: { - backgroundColor: "bg.subtle.active", - borderColor: "bd.solid.hover", - color: "fg.heading", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "neutral.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Ghost + Critical - { - variant: "ghost", - colorScheme: "critical", - css: { - backgroundColor: "[transparent]", - color: "status.error.fg.heading", - _hover: { - backgroundColor: "status.error.bg.subtle.hover", - borderColor: "status.error.bd.solid", - color: "status.error.fg.heading", - }, - _active: { - backgroundColor: "status.error.bg.subtle.active", - borderColor: "status.error.bd.solid", - color: "status.error.fg.heading", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "red.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Error + Critical (solid red) - { - variant: "error", - colorScheme: "critical", - css: { - backgroundColor: "status.error.bg.solid", - color: "status.error.fg.onSolid", - _hover: { - backgroundColor: "status.error.bg.solid.hover", - }, - _active: { - backgroundColor: "status.error.bg.solid.active", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "red.s30", - outlineOffset: "[2px]", - }, - }, - }, - // Error + Subtle (light bg, red text) - { - variant: "error", - colorScheme: "subtle", - css: { - backgroundColor: "status.error.bg.subtle", - color: "status.error.fg.heading", - _hover: { - backgroundColor: "status.error.bg.subtle.hover", - }, - _active: { - backgroundColor: "status.error.bg.subtle.active", - }, - _focusVisible: { - outline: "[2px solid]", - outlineColor: "red.s30", - outlineOffset: "[2px]", - }, - }, - }, - ], - defaultVariants: { - variant: "primary", - colorScheme: "brand", - size: "md", - isLoading: false, - }, -}); +export type ButtonElementProps = ButtonElementOnlyProps & + SharedButtonProps & + ButtonIconProps; +export type AnchorElementProps = AnchorElementOnlyProps & + SharedButtonProps & + ButtonIconProps; +export type ButtonProps = ButtonElementProps | AnchorElementProps; -const contentGap = { - xs: "1", - sm: "1", - md: "1", - lg: "1.5", -} as const; +export const iconSizeMap: Record = { + xxs: "xs", + xs: "xs", + sm: "sm", + md: "sm", + lg: "md", +}; -export const Button: React.FC = ({ - className, - children, - variant = "primary", - colorScheme = "brand", - size = "md", - isLoading = false, - iconLeft, - iconRight, - disabled, - ...props -}) => { - const isDisabled = disabled ?? isLoading; +const loadingSizeMap: Record = { + xxs: "xs", + xs: "xs", + sm: "sm", + md: "md", + lg: "md", +}; - return ( - ); }; diff --git a/libs/@hashintel/ds-components/src/components/Icon/icon.recipe.ts b/libs/@hashintel/ds-components/src/components/Icon/icon.recipe.ts index 3c12361349b..247bf715e31 100644 --- a/libs/@hashintel/ds-components/src/components/Icon/icon.recipe.ts +++ b/libs/@hashintel/ds-components/src/components/Icon/icon.recipe.ts @@ -10,6 +10,7 @@ export const styles = cva({ }, variants: { size: { + xxs: { "--icon-size": "8px" }, xs: { "--icon-size": "12px" }, sm: { "--icon-size": "16px" }, md: { "--icon-size": "24px" }, diff --git a/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.recipe.ts b/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.recipe.ts index 64234361fe4..78bac4daf33 100644 --- a/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.recipe.ts +++ b/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.recipe.ts @@ -2,19 +2,28 @@ import { cva } from "@hashintel/ds-helpers/css"; export const styles = cva({ base: { - animation: "rotateLeft 1.1s infinite linear", width: "var(--loading-spinner-size)", height: "var(--loading-spinner-size)", }, variants: { size: { + xxs: { "--loading-spinner-size": "8px" }, xs: { "--loading-spinner-size": "12px" }, sm: { "--loading-spinner-size": "16px" }, md: { "--loading-spinner-size": "24px" }, lg: { "--loading-spinner-size": "32px" }, }, + variant: { + default: { + animation: "rotateLeft 1.1s infinite linear", + }, + bars: { + animation: "rotateRight 1.0s steps(12) infinite", + }, + }, }, defaultVariants: { size: "md", + variant: "default", }, }); diff --git a/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.stories.tsx b/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.stories.tsx index 5cf701dbc6f..6b50e6a9e41 100644 --- a/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.stories.tsx +++ b/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.stories.tsx @@ -2,10 +2,13 @@ import { css } from "@hashintel/ds-helpers/css"; import type { Story, StoryDefault } from "@ladle/react"; import { formInputSizes } from "../../util/form-shared"; +import type { LoadingSpinnerVariant } from "./loading-spinner"; import { LoadingSpinner } from "./loading-spinner"; type LoadingSpinnerProps = React.ComponentProps; +const variants: LoadingSpinnerVariant[] = ["default", "bars"]; + export default { title: "Components/LoadingSpinner", parameters: { @@ -17,9 +20,15 @@ export default { options: formInputSizes, description: "The size of the spinner", }, + variant: { + control: { type: "select" }, + options: variants, + description: "The visual variant of the spinner", + }, }, args: { size: "md", + variant: "default", }, } satisfies StoryDefault; @@ -27,16 +36,80 @@ export const Default: Story = (args) => (
- {formInputSizes.map((size) => ( - - ))} +
+ {formInputSizes.map((size) => ( + + ))} +
+
+ {formInputSizes.map((size) => ( + + ))} +
); Default.parameters = { controls: { exclude: ["size"] }, }; + +export const Bars: Story = (args) => ( +
+
+ {formInputSizes.map((size) => ( + + ))} +
+
+ {formInputSizes.map((size) => ( + + ))} +
+
+); + +Bars.parameters = { + controls: { exclude: ["size", "variant"] }, +}; diff --git a/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.tsx b/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.tsx index 02fc15919f7..76ea7bdb462 100644 --- a/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.tsx +++ b/libs/@hashintel/ds-components/src/components/Loading/loading-spinner.tsx @@ -4,18 +4,64 @@ import { useId } from "react"; import type { FormInputSize } from "../../util/form-shared"; import { styles } from "./loading-spinner.recipe"; +export type LoadingSpinnerVariant = "default" | "bars"; + +const spokes = [ + { rotation: 0, opacity: 1 }, + { rotation: 30, opacity: 0.93 }, + { rotation: 60, opacity: 0.83 }, + { rotation: 90, opacity: 0.73 }, + { rotation: 120, opacity: 0.63 }, + { rotation: 150, opacity: 0.53 }, + { rotation: 180, opacity: 0.43 }, + { rotation: 210, opacity: 0.33 }, + { rotation: 240, opacity: 0.25 }, + { rotation: 270, opacity: 0.2 }, + { rotation: 300, opacity: 0.15 }, + { rotation: 330, opacity: 0.1 }, +] as const; + export const LoadingSpinner = ({ size = "md", + variant = "default", className, }: { size?: FormInputSize; + variant?: LoadingSpinnerVariant; className?: string; }) => { const gradientId = useId(); - const bg = "color-mix(in oklab, currentColor, white 85%)"; + + if (variant === "bars") { + return ( + + {spokes.map((spoke) => ( + + ))} + + ); + } + + const bg = "color-mix(in oklab, currentColor, transparent 85%)"; return ( - + - + diff --git a/libs/@hashintel/ds-components/src/components/Tooltip/tooltip.stories.tsx b/libs/@hashintel/ds-components/src/components/Tooltip/tooltip.stories.tsx index fc51388e25f..ff4de81cd1f 100644 --- a/libs/@hashintel/ds-components/src/components/Tooltip/tooltip.stories.tsx +++ b/libs/@hashintel/ds-components/src/components/Tooltip/tooltip.stories.tsx @@ -1,3 +1,4 @@ +import { css } from "@hashintel/ds-helpers/css"; import type { Story, StoryDefault } from "@ladle/react"; import { Button } from "../Button/button"; @@ -28,7 +29,7 @@ export const Default: Story = () => (
- @@ -85,10 +86,9 @@ export const AllPositions: Story = () => ( ) : ( diff --git a/libs/@hashintel/ds-components/src/main.ts b/libs/@hashintel/ds-components/src/main.ts index 7f4f576f284..f19a3c7099a 100644 --- a/libs/@hashintel/ds-components/src/main.ts +++ b/libs/@hashintel/ds-components/src/main.ts @@ -3,7 +3,10 @@ export { Badge, type BadgeProps } from "./components/Badge/badge"; export { Button, type ButtonProps } from "./components/Button/button"; export { Checkbox, type CheckboxProps } from "./components/Checkbox/checkbox"; export { Icon, type IconName, iconNames } from "./components/Icon/icon"; -export { LoadingSpinner } from "./components/Loading/loading-spinner"; +export { + LoadingSpinner, + type LoadingSpinnerVariant, +} from "./components/Loading/loading-spinner"; export { RadioGroup, type RadioGroupOption, diff --git a/libs/@hashintel/ds-components/src/preset.ts b/libs/@hashintel/ds-components/src/preset.ts index 3ed171783d1..c07db2e97a4 100644 --- a/libs/@hashintel/ds-components/src/preset.ts +++ b/libs/@hashintel/ds-components/src/preset.ts @@ -135,6 +135,13 @@ export function createPreset(options?: PresetOptions) { }, }, textStyles: { + xxs: { + value: { + fontSize: "{fontSizes.xxs}", + lineHeight: "calc(1em * 1.6 * var(--leading-factor, 1))", + letterSpacing: "0.01em", + }, + }, xs: { value: { fontSize: "{fontSizes.xs}", diff --git a/libs/@hashintel/ds-components/src/util/form-shared.ts b/libs/@hashintel/ds-components/src/util/form-shared.ts index 932c4aa8192..76838fe8faf 100644 --- a/libs/@hashintel/ds-components/src/util/form-shared.ts +++ b/libs/@hashintel/ds-components/src/util/form-shared.ts @@ -1,2 +1,2 @@ -export const formInputSizes = ["xs", "sm", "md", "lg"] as const; +export const formInputSizes = ["xxs", "xs", "sm", "md", "lg"] as const; export type FormInputSize = (typeof formInputSizes)[number]; diff --git a/libs/@hashintel/ds-components/turbo.json b/libs/@hashintel/ds-components/turbo.json index e847fe05b5b..13983a080f1 100644 --- a/libs/@hashintel/ds-components/turbo.json +++ b/libs/@hashintel/ds-components/turbo.json @@ -19,10 +19,7 @@ }, "codegen": { "dependsOn": ["^build"], - "outputs": [ - "src/preset/theme/**/*.gen.ts", - "../ds-helpers/styled-system/**" - ] + "outputs": ["src/preset/**/*.gen.ts", "../ds-helpers/styled-system/**"] }, "dev": { "cache": false, @@ -41,6 +38,9 @@ "lint:tsc": { "dependsOn": ["codegen"] }, + "fix": { + "dependsOn": ["codegen"] + }, "preview:ladle": { "dependsOn": ["build:ladle"] }, diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/components/import-error-dialog.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/components/import-error-dialog.tsx index ebd6967be50..eadc41bf58a 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/components/import-error-dialog.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/components/import-error-dialog.tsx @@ -34,16 +34,12 @@ export const ImportErrorDialog = ({ - - diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx index 6d0245f84dc..13695d5d558 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/PropertiesPanel/differential-equation-properties/subviews/main.tsx @@ -237,16 +237,16 @@ const DiffEqMainContent: React.FC = () => {
@@ -292,8 +292,8 @@ const PlaceMainContent: React.FC = () => { {place.differentialEquationId && (
diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-experiment-drawer.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-experiment-drawer.tsx index a72b117d654..43225d9973f 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-experiment-drawer.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-experiment-drawer.tsx @@ -198,19 +198,15 @@ export const CreateExperimentDrawer = ({ - diff --git a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-metric-drawer.tsx b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-metric-drawer.tsx index f4224763a81..a37ada5309e 100644 --- a/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-metric-drawer.tsx +++ b/libs/@hashintel/petrinaut/src/ui/views/Editor/panels/SimulateView/create-metric-drawer.tsx @@ -62,17 +62,12 @@ const CreateMetricFooter = ({ return ( - -