Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/bright-enso-loads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@kaizen/components': minor
---

feat: add the BrandLoader component
86 changes: 86 additions & 0 deletions packages/components/src/Loading/BrandLoader/BrandLoader.module.css
Original file line number Diff line number Diff line change
@@ -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;
}
}
44 changes: 44 additions & 0 deletions packages/components/src/Loading/BrandLoader/BrandLoader.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAttributes<HTMLDivElement>, 'children'>

/** {@link https://cultureamp.design/?path=/docs/components-loading-states-brandloader--docs Storybook} */
export const BrandLoader = forwardRef<HTMLDivElement, BrandLoaderProps>(
(
{ accessibilityLabel, size = 'md', speed = 'normal', className, ...restProps },
ref,
): JSX.Element => {
const isReversed = useReversedColors()

return (
<div
ref={ref}
className={classnames(
styles.brandLoader,
styles[size],
styles[speed],
isReversed && styles.reversed,
className,
)}
{...restProps}
role="status"
>
<VisuallyHidden>{accessibilityLabel}</VisuallyHidden>
<BrandLoaderIcon />
</div>
)
},
)

BrandLoader.displayName = 'BrandLoader'
51 changes: 51 additions & 0 deletions packages/components/src/Loading/BrandLoader/_docs/BrandLoader.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Canvas, Controls, Meta } from '@storybook/blocks'
import { KAIOInstallation, LinkTo, ResourceLinks } from '~storybook/components'
import * as BrandLoaderStories from './BrandLoader.stories'

<Meta of={BrandLoaderStories} />

# BrandLoader

<ResourceLinks
sourceCode="https://github.com/cultureamp/kaizen-design-system/tree/main/packages/components/src/Loading/BrandLoader"
figma=""
designGuidelines=""
/>

<KAIOInstallation exportNames="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

<LinkTo pageId="components-loading-states-loadingspinner">LoadingSpinner</LinkTo>.

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.

<Canvas of={BrandLoaderStories.Playground} />
<Controls of={BrandLoaderStories.Playground} />

## Sizes

Choose `sm` (48px), `md` (72px), or `lg` (96px). The default is `md`.

<Canvas of={BrandLoaderStories.Sizes} />

## Speeds

Choose `slow` (4s), `normal` (3.2s), or `fast` (2.4s). The default is `normal`.

<Canvas of={BrandLoaderStories.Speeds} />

## Colour

BrandLoader uses Purple 800 by default. Wrap it in `ReversedColors` on an approved reversed
surface.

<Canvas of={BrandLoaderStories.ReversedContext} />

## Reduced motion

When reduced motion is enabled, BrandLoader displays the completed Ensō without animation.
Original file line number Diff line number Diff line change
@@ -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 }) => (
<ReversedColors isReversed={isReversed}>
<>
<StickerSheet isReversed={isReversed} title="Size" headers={['Small', 'Medium', 'Large']}>
<StickerSheet.Row>
<BrandLoader accessibilityLabel="Loading" size="sm" />
<BrandLoader accessibilityLabel="Loading" size="md" />
<BrandLoader accessibilityLabel="Loading" size="lg" />
</StickerSheet.Row>
</StickerSheet>
<StickerSheet isReversed={isReversed} title="Speed" headers={['Slow', 'Normal', 'Fast']}>
<StickerSheet.Row>
<BrandLoader accessibilityLabel="Loading" speed="slow" />
<BrandLoader accessibilityLabel="Loading" speed="normal" />
<BrandLoader accessibilityLabel="Loading" speed="fast" />
</StickerSheet.Row>
</StickerSheet>
</>
</ReversedColors>
),
}

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 },
}
Original file line number Diff line number Diff line change
@@ -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<typeof BrandLoader>

export default meta

type Story = StoryObj<typeof meta>

export const Playground: Story = {
parameters: {
docs: {
canvas: {
sourceState: 'shown',
},
},
},
}

export const Sizes: Story = {
render: (args) => (
<>
<BrandLoader {...args} size="sm" />
<BrandLoader {...args} size="md" />
<BrandLoader {...args} size="lg" />
</>
),
decorators: [
(Story) => (
<div className="flex items-center gap-24">
<Story />
</div>
),
],
}

export const Speeds: Story = {
render: (args) => (
<>
<BrandLoader {...args} speed="slow" />
<BrandLoader {...args} speed="normal" />
<BrandLoader {...args} speed="fast" />
</>
),
decorators: [
(Story) => (
<div className="flex items-center gap-24">
<Story />
</div>
),
],
}

export const ReversedContext: Story = {
render: (args) => (
<ReversedColors>
<BrandLoader {...args} />
</ReversedColors>
),
decorators: [
(Story) => (
<div className="rounded-8 bg-purple-700 p-32">
<Story />
</div>
),
],
}
1 change: 1 addition & 0 deletions packages/components/src/Loading/BrandLoader/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BrandLoader'
Original file line number Diff line number Diff line change
@@ -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 (
<svg
className={styles.icon}
aria-hidden="true"
viewBox="-2 -2 40 40"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<mask id={maskId}>
<rect x="-2" y="-2" width="40" height="40" fill="black" />
<path
className={styles.brush}
pathLength={100}
d="M 25.5 7.5 A 13 13 0 1 0 26.5 27 L 31.5 20.5"
/>
</mask>
</defs>
<path
className={styles.enso}
mask={`url(#${maskId})`}
d="m25.7678 20.2359c.0963.1604.1295.3509.0931.5343-.34 1.5449-1.1673 2.9397-2.3598 3.979-.0654.0495-.1204.1114-.1619.1821-.0149.0324-.0226.0676-.0226.1032 0 .0357.0077.0709.0226.1032.0238.0277.0538.0495.0875.0635.0338.0141.0703.0201.1068.0175.1727-.046.3396-.1112.4978-.1943.6995-.3973 1.3175-.9233 1.8215-1.5503.3036-.3683.6031-.7367.8905-1.1172.5412-.7515.9417-1.5949 1.182-2.4893.2185-.7245.4047-1.4572.6314-2.1817.0926-.2657.2049-.5241.336-.7731.0527-.0767.1138-.1473.1821-.2105h.0567c.0566.1902.1255.3724.1659.5667.1059.611.1209 1.2343.0446 1.8498-.084.801-.2481 1.5915-.4898 2.3598-.1693.4722-.3978.921-.68 1.3357-.7093 1.1691-1.547 2.2552-2.4975 3.2382-.7002.6762-1.4572 1.291-2.2627 1.8377-.0587.0301-.1121.0698-.1578.1174-.0471.0567-.0716.1287-.0688.2024.0169.0329.0416.0611.0719.0823s.0654.0346.1021.0391c.1693-.0011.3379-.0228.5019-.0648.7479-.2155 1.4474-.5728 2.0603-1.0524 1.7144-1.2639 3.1067-2.9141 4.0639-4.8168.61-1.2414 1.0055-2.5771 1.1698-3.9506.0985-.867.108-1.7418.0284-2.6107-.0695-.4453-.0558-.8996.0404-1.3398.0255-.1009.0622-.1987.1093-.2915.1093-.2105.2672-.2226.3724 0 .1107.2107.1924.4354.2429.6679.2222 1.1134.2917 2.2518.2064 3.3839-.1571 1.8393-.6195 3.6395-1.3681 5.3268-.7582 1.7373-1.8966 3.2824-3.3313 4.5213-.4655.4048-.9107.8096-1.3924 1.1901-.5785.4206-1.1983.7814-1.8498 1.0766-1.4375.7142-2.9674 1.2251-4.5456 1.5179-.9673.1679-1.9496.2331-2.9306.1943-.4816 0-.9674-.0323-1.449-.0566-1.089-.0603-2.1634-.2785-3.1897-.6477-1.4861-.5171-2.9145-1.1873-4.2622-1.9995-1.57169-.9637-2.91214-2.2613-3.9263-3.8009-.50066-.724-.93679-1.4906-1.30336-2.291-.01869-.0475-.04876-.0897-.08758-.1229-.03882-.0331-.0852-.0563-.13505-.0673-.26618-.0662-.51381-.1921-.72419-.3681-.21037-.176-.37797-.3976-.49013-.6479-.300143-.733-.533165-1.4916-.696204-2.2667-.296643-1.2513-.4554524-2.5312-.473586-3.817.0121573-.8141.0865841-1.626.222626-2.4287.153645-1.0906.425079-2.1614.809544-3.1936.34003-.90334.81793-1.74854 1.4167-2.50556.34811-.43715.6355-.92693.9755-1.37218 2.15791-2.85201 5.2474-4.85724 8.73093-5.666816.6719-.161909 1.3439-.331915 2.0239-.457395.6071-.113336 1.2143-.186194 1.8376-.242863.7205-.0647632 1.441-.12143121 2.1656-.12952666 2.9067-.04047474 5.7542.82380266 8.1481 2.47316066.2728.165.5186.37117.7285.61121.1551.18889.2583.41487.2996.65573.016.05667.0484.10735.0931.14572.5707.55049 1.1779 1.07264 1.7041 1.66361.8362.91359 1.4414 2.01427 1.7648 3.20985.1482.48288.1998.99023.1519 1.49306-.048.5028-.1945.9913-.4312 1.4375-.3959.6665-.9526 1.2232-1.6191 1.6191-.7415.5045-1.631.7454-2.5258.684-.8365-.0382-1.6305-.3791-2.2343-.9593-.6344-.6592-1.3104-1.2771-2.0239-1.8498-.4695-.3562-.9553-.6921-1.445-1.0119-.5921-.35579-1.248-.59231-1.9308-.69621-.7767-.14207-1.5677-.18964-2.3558-.14167-1.7229.03283-3.3899.61734-4.7561 1.66768-1.0238.7735-1.86646 1.7612-2.46906 2.8941-.34127.6874-.55651 1.4305-.6355 2.1939-.0659.4709-.10374.9453-.11333 1.4207.03818 1.3552.39157 2.6829 1.03217 3.8777.47332.9269 1.10322 1.7649 1.86192 2.4773.7057.6341 1.4875 1.178 2.3275 1.619.956.5183 2.0118.826 3.0965.9027 1.4244.1487 2.8632-.0672 4.1813-.6274.6317-.297 1.2134-.6902 1.7243-1.1658.8824-.7589 1.6226-1.6691 2.1858-2.6877.2105-.3683.3562-.7731.5343-1.1616.0526-.1174.1133-.2348.174-.3481.0371-.055.0806-.1052.1296-.1498"
/>
</svg>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BrandLoaderIcon'
1 change: 1 addition & 0 deletions packages/components/src/Loading/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './BrandLoader'
export * from './LoadingGraphic'
export * from './LoadingHeading'
export * from './LoadingInput'
Expand Down
Loading