diff --git a/.changeset/bright-enso-loads.md b/.changeset/bright-enso-loads.md new file mode 100644 index 00000000000..1a9e6e2eae2 --- /dev/null +++ b/.changeset/bright-enso-loads.md @@ -0,0 +1,5 @@ +--- +'@kaizen/components': minor +--- + +feat: add the BrandLoader component diff --git a/packages/components/src/Loading/BrandLoader/BrandLoader.module.css b/packages/components/src/Loading/BrandLoader/BrandLoader.module.css new file mode 100644 index 00000000000..6e8fdaa99a7 --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/BrandLoader.module.css @@ -0,0 +1,86 @@ +@layer kz-components { + .brandLoader { + --brand-loader-size: 72px; + --brand-loader-duration: 3200ms; + + display: flex; + inline-size: var(--brand-loader-size); + block-size: var(--brand-loader-size); + color: var(--color-purple-800); + } + + .reversed { + color: var(--color-white); + } + + .sm { + --brand-loader-size: 48px; + } + + .md { + --brand-loader-size: 72px; + } + + .lg { + --brand-loader-size: 96px; + } + + .slow { + --brand-loader-duration: 4000ms; + } + + .normal { + --brand-loader-duration: 3200ms; + } + + .fast { + --brand-loader-duration: 2400ms; + } + + .icon { + inline-size: 100%; + block-size: 100%; + } + + .enso { + fill: currentcolor; + } + + .brush { + animation: paint var(--brand-loader-duration) var(--animation-easing-function-ease-in-out) + infinite; + fill: none; + stroke: white; + stroke-width: 15; + stroke-linecap: round; + stroke-dasharray: 100 400; + stroke-dashoffset: 101; + } + + @media (prefers-reduced-motion: reduce) { + .brush { + animation: none; + stroke-dashoffset: 0; + } + } +} + +@keyframes paint { + 0% { + stroke-dashoffset: 101; + } + + 42%, + 56% { + stroke-dashoffset: 0; + } + + 78% { + stroke-dashoffset: -70; + } + + 87.5%, + 100% { + stroke-dashoffset: -101; + } +} diff --git a/packages/components/src/Loading/BrandLoader/BrandLoader.tsx b/packages/components/src/Loading/BrandLoader/BrandLoader.tsx new file mode 100644 index 00000000000..5ef1db516cb --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/BrandLoader.tsx @@ -0,0 +1,44 @@ +import React, { forwardRef, type HTMLAttributes } from 'react' +import classnames from 'classnames' +import { VisuallyHidden } from '~components/VisuallyHidden' +import { useReversedColors } from '~components/utils' +import { BrandLoaderIcon } from './subcomponents' +import styles from './BrandLoader.module.css' + +export type BrandLoaderProps = { + accessibilityLabel: string + /** @default "md" */ + size?: 'sm' | 'md' | 'lg' + /** @default "normal" */ + speed?: 'slow' | 'normal' | 'fast' +} & Omit, 'children'> + +/** {@link https://cultureamp.design/?path=/docs/components-loading-states-brandloader--docs Storybook} */ +export const BrandLoader = forwardRef( + ( + { accessibilityLabel, size = 'md', speed = 'normal', className, ...restProps }, + ref, + ): JSX.Element => { + const isReversed = useReversedColors() + + return ( +
+ {accessibilityLabel} + +
+ ) + }, +) + +BrandLoader.displayName = 'BrandLoader' diff --git a/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.mdx b/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.mdx new file mode 100644 index 00000000000..e2eeeb478d8 --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.mdx @@ -0,0 +1,51 @@ +import { Canvas, Controls, Meta } from '@storybook/blocks' +import { KAIOInstallation, LinkTo, ResourceLinks } from '~storybook/components' +import * as BrandLoaderStories from './BrandLoader.stories' + + + +# BrandLoader + + + + + +## Overview + +Use BrandLoader for page- and section-level waits where a prominent branded loading state is +appropriate. For compact waits in buttons, form fields, and content-dense layouts, use + +LoadingSpinner. + +The `accessibilityLabel` is required. Describe what is loading, such as “Loading survey results”. +The label is announced by assistive technology and is not shown visually. + + + + +## Sizes + +Choose `sm` (48px), `md` (72px), or `lg` (96px). The default is `md`. + + + +## Speeds + +Choose `slow` (4s), `normal` (3.2s), or `fast` (2.4s). The default is `normal`. + + + +## Colour + +BrandLoader uses Purple 800 by default. Wrap it in `ReversedColors` on an approved reversed +surface. + + + +## Reduced motion + +When reduced motion is enabled, BrandLoader displays the completed Ensō without animation. diff --git a/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.stickersheet.stories.tsx b/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.stickersheet.stories.tsx new file mode 100644 index 00000000000..d459fbdea2c --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.stickersheet.stories.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { type Meta } from '@storybook/react' +import { ReversedColors } from '~components/utils' +import { StickerSheet, type StickerSheetStory } from '~storybook/components/StickerSheet' +import { BrandLoader } from '../index' + +export default { + title: 'Components/Loading states/BrandLoader', + parameters: { + chromatic: { disable: false, prefersReducedMotion: 'reduce' }, + controls: { disable: true }, + }, +} satisfies Meta + +const StickerSheetTemplate: StickerSheetStory = { + render: ({ isReversed }) => ( + + <> + + + + + + + + + + + + + + + + + ), +} + +export const StickerSheetDefault: StickerSheetStory = { + ...StickerSheetTemplate, + name: 'Sticker Sheet (Default)', +} + +export const StickerSheetReversed: StickerSheetStory = { + ...StickerSheetTemplate, + name: 'Sticker Sheet (Reversed)', + parameters: { + backgrounds: { default: 'Purple 700' }, + }, + args: { isReversed: true }, +} diff --git a/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.stories.tsx b/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.stories.tsx new file mode 100644 index 00000000000..5b60c8b6687 --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/_docs/BrandLoader.stories.tsx @@ -0,0 +1,78 @@ +import React from 'react' +import { type Meta, type StoryObj } from '@storybook/react' +import { ReversedColors } from '~components/utils' +import { BrandLoader } from '../index' + +const meta = { + title: 'Components/Loading states/BrandLoader', + component: BrandLoader, + parameters: { + layout: 'centered', + }, + args: { + accessibilityLabel: 'Loading', + }, +} satisfies Meta + +export default meta + +type Story = StoryObj + +export const Playground: Story = { + parameters: { + docs: { + canvas: { + sourceState: 'shown', + }, + }, + }, +} + +export const Sizes: Story = { + render: (args) => ( + <> + + + + + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export const Speeds: Story = { + render: (args) => ( + <> + + + + + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} + +export const ReversedContext: Story = { + render: (args) => ( + + + + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], +} diff --git a/packages/components/src/Loading/BrandLoader/index.ts b/packages/components/src/Loading/BrandLoader/index.ts new file mode 100644 index 00000000000..426050349bd --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/index.ts @@ -0,0 +1 @@ +export * from './BrandLoader' diff --git a/packages/components/src/Loading/BrandLoader/subcomponents/BrandLoaderIcon.tsx b/packages/components/src/Loading/BrandLoader/subcomponents/BrandLoaderIcon.tsx new file mode 100644 index 00000000000..0a3876b0d46 --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/subcomponents/BrandLoaderIcon.tsx @@ -0,0 +1,31 @@ +import React, { useId } from 'react' +import styles from '../BrandLoader.module.css' + +export const BrandLoaderIcon = (): JSX.Element => { + const maskId = `brand-loader-mask-${useId().replace(/:/g, '')}` + + return ( + + ) +} diff --git a/packages/components/src/Loading/BrandLoader/subcomponents/index.ts b/packages/components/src/Loading/BrandLoader/subcomponents/index.ts new file mode 100644 index 00000000000..d8c215a2d96 --- /dev/null +++ b/packages/components/src/Loading/BrandLoader/subcomponents/index.ts @@ -0,0 +1 @@ +export * from './BrandLoaderIcon' diff --git a/packages/components/src/Loading/index.ts b/packages/components/src/Loading/index.ts index 2a3d473454c..5cb807e9414 100644 --- a/packages/components/src/Loading/index.ts +++ b/packages/components/src/Loading/index.ts @@ -1,3 +1,4 @@ +export * from './BrandLoader' export * from './LoadingGraphic' export * from './LoadingHeading' export * from './LoadingInput'