diff --git a/react/src/Button/Button.stories.tsx b/react/src/Button/Button.stories.tsx
index 3020d802..1139c67d 100644
--- a/react/src/Button/Button.stories.tsx
+++ b/react/src/Button/Button.stories.tsx
@@ -191,3 +191,60 @@ LinkPrimary.args = {
variant: 'link',
colorScheme: 'brand.primary',
}
+
+export const Sizes = () => (
+
+ Solid
+ Outline
+ Clear
+ Reverse
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/react/src/Toolbar/Toolbar.stories.tsx b/react/src/Toolbar/Toolbar.stories.tsx
new file mode 100644
index 00000000..59d65423
--- /dev/null
+++ b/react/src/Toolbar/Toolbar.stories.tsx
@@ -0,0 +1,50 @@
+import { Spacer, Text } from '@chakra-ui/react'
+import { Meta, StoryFn } from '@storybook/react'
+
+import { BxsTimeFive, BxUpload } from '~/icons'
+
+import { Toolbar, ToolbarProps } from './Toolbar'
+import { ToolbarButton } from './ToolbarButton'
+import { ToolbarDivider } from './ToolbarDivider'
+import { ToolbarGroup } from './ToolbarGroup'
+import { ToolbarIconButton } from './ToolbarIconButton'
+
+export default {
+ title: 'Components/Toolbar',
+ component: Toolbar,
+ decorators: [],
+ tags: ['autodocs'],
+} as Meta
+
+const Template: StoryFn = ({ children, ...args }) => {
+ return (
+
+ 1 item selected
+
+ {children}
+
+ }>
+ Download
+
+ }>
+ Move
+
+
+ } aria-label="Upload" />
+
+ Cancel
+
+
+ )
+}
+export const TemplateExample = Template.bind({})
+
+export const NeutralColorScheme = Template.bind({})
+NeutralColorScheme.args = {
+ colorScheme: 'neutral',
+}
+
+export const SizeXs = Template.bind({})
+SizeXs.args = {
+ size: 'xs',
+}
diff --git a/react/src/Toolbar/Toolbar.tsx b/react/src/Toolbar/Toolbar.tsx
new file mode 100644
index 00000000..5e231515
--- /dev/null
+++ b/react/src/Toolbar/Toolbar.tsx
@@ -0,0 +1,35 @@
+import { PropsWithChildren } from 'react'
+import {
+ createStylesContext,
+ Flex,
+ FlexProps,
+ ThemingProps,
+ useMultiStyleConfig,
+} from '@chakra-ui/react'
+
+import { ToolbarProvider } from './ToolbarContext'
+
+const [ToolbarStylesProvider, useToolbarStyles] = createStylesContext('Toolbar')
+
+export { useToolbarStyles }
+
+export interface ToolbarProps extends PropsWithChildren, FlexProps {
+ colorScheme?: 'main' | 'neutral' | 'sub'
+ size?: ThemingProps<'Toolbar'>['size']
+}
+
+/**
+ * Container for the toolbar.
+ */
+export const Toolbar = ({ children, ...props }: ToolbarProps): JSX.Element => {
+ const styles = useMultiStyleConfig('Toolbar', props)
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/react/src/Toolbar/ToolbarButton.tsx b/react/src/Toolbar/ToolbarButton.tsx
new file mode 100644
index 00000000..e782718c
--- /dev/null
+++ b/react/src/Toolbar/ToolbarButton.tsx
@@ -0,0 +1,11 @@
+import { Button, ButtonProps } from '~/Button'
+
+import { useToolbarButtonProps } from './utils/useToolbarButtonProps'
+
+export type ToolbarButtonProps = ButtonProps
+
+export const ToolbarButton = (props: ToolbarButtonProps): JSX.Element => {
+ const toolbarButtonProps = useToolbarButtonProps()
+
+ return
+}
diff --git a/react/src/Toolbar/ToolbarContext.tsx b/react/src/Toolbar/ToolbarContext.tsx
new file mode 100644
index 00000000..c6a242e7
--- /dev/null
+++ b/react/src/Toolbar/ToolbarContext.tsx
@@ -0,0 +1,33 @@
+import { createContext, FC, PropsWithChildren, useContext } from 'react'
+import { ThemingProps } from '@chakra-ui/react'
+
+export interface ToolbarContextProps {
+ colorScheme?: 'main' | 'neutral' | 'sub'
+ size?: ThemingProps<'Toolbar'>['size']
+}
+
+export type ToolbarContextReturn = Required
+
+const ToolbarContext = createContext(
+ undefined,
+)
+
+export const ToolbarProvider: FC> = ({
+ children,
+ colorScheme = 'sub',
+ size = 'md',
+}) => {
+ return (
+
+ {children}
+
+ )
+}
+
+export const useToolbarContext = () => {
+ const context = useContext(ToolbarContext)
+ if (!context) {
+ throw new Error('useToolbar must be used within a ToolbarProvider')
+ }
+ return context
+}
diff --git a/react/src/Toolbar/ToolbarDivider.tsx b/react/src/Toolbar/ToolbarDivider.tsx
new file mode 100644
index 00000000..d9c662d8
--- /dev/null
+++ b/react/src/Toolbar/ToolbarDivider.tsx
@@ -0,0 +1,11 @@
+import { Divider, DividerProps } from '@chakra-ui/react'
+
+import { useToolbarStyles } from './Toolbar'
+
+export type ToolbarDividerProps = DividerProps
+
+export const ToolbarDivider = (props: ToolbarDividerProps): JSX.Element => {
+ const styles = useToolbarStyles()
+
+ return
+}
diff --git a/react/src/Toolbar/ToolbarGroup.tsx b/react/src/Toolbar/ToolbarGroup.tsx
new file mode 100644
index 00000000..b09e6e69
--- /dev/null
+++ b/react/src/Toolbar/ToolbarGroup.tsx
@@ -0,0 +1,19 @@
+import { Wrap, WrapProps } from '@chakra-ui/react'
+
+import { useToolbarStyles } from './Toolbar'
+
+export type ToolbarGroupProps = WrapProps
+
+export const ToolbarGroup = (props: ToolbarGroupProps): JSX.Element => {
+ const styles = useToolbarStyles()
+ return (
+
+ )
+}
diff --git a/react/src/Toolbar/ToolbarIconButton.tsx b/react/src/Toolbar/ToolbarIconButton.tsx
new file mode 100644
index 00000000..57fde50e
--- /dev/null
+++ b/react/src/Toolbar/ToolbarIconButton.tsx
@@ -0,0 +1,13 @@
+import { IconButton, IconButtonProps } from '~/IconButton'
+
+import { useToolbarButtonProps } from './utils/useToolbarButtonProps'
+
+export type ToolbarIconButtonProps = IconButtonProps
+
+export const ToolbarIconButton = (
+ props: ToolbarIconButtonProps,
+): JSX.Element => {
+ const toolbarButtonProps = useToolbarButtonProps()
+
+ return
+}
diff --git a/react/src/Toolbar/index.ts b/react/src/Toolbar/index.ts
new file mode 100644
index 00000000..03b48f98
--- /dev/null
+++ b/react/src/Toolbar/index.ts
@@ -0,0 +1,5 @@
+export * from './Toolbar'
+export * from './ToolbarButton'
+export * from './ToolbarDivider'
+export * from './ToolbarGroup'
+export * from './ToolbarIconButton'
diff --git a/react/src/Toolbar/utils/useToolbarButtonProps.ts b/react/src/Toolbar/utils/useToolbarButtonProps.ts
new file mode 100644
index 00000000..8994e015
--- /dev/null
+++ b/react/src/Toolbar/utils/useToolbarButtonProps.ts
@@ -0,0 +1,42 @@
+import { useMemo } from 'react'
+import { useBreakpointValue } from '@chakra-ui/react'
+
+import { ButtonProps } from '~/Button'
+import { layerStyles } from '~/theme/layerStyles'
+
+import { useToolbarContext } from '../ToolbarContext'
+
+export const useToolbarButtonProps = (): ButtonProps => {
+ const { colorScheme, size } = useToolbarContext()
+
+ const toolbarBreakpointSize = useBreakpointValue(
+ typeof size === 'string' ? { base: size } : size,
+ )
+
+ const toolbarSize = useMemo(() => {
+ switch (toolbarBreakpointSize) {
+ case 'xs':
+ return 'xs'
+ default:
+ return 'sm'
+ }
+ }, [toolbarBreakpointSize])
+
+ const toolbarButtonStyleProps: Partial = useMemo(() => {
+ switch (colorScheme) {
+ case 'main':
+ case 'sub':
+ return {
+ colorScheme: 'inverse',
+ _focusVisible: layerStyles.focusRing.inverse._focusVisible,
+ }
+ default:
+ return { colorScheme }
+ }
+ }, [colorScheme])
+
+ return {
+ ...toolbarButtonStyleProps,
+ size: toolbarSize,
+ }
+}
diff --git a/react/src/index.ts b/react/src/index.ts
index 662e4d25..5dd692e6 100644
--- a/react/src/index.ts
+++ b/react/src/index.ts
@@ -37,4 +37,5 @@ export * from './theme'
export * from './Tile'
export * from './Toast'
export * from './Toggle'
+export * from './Toolbar'
export * from './Tooltip'
diff --git a/react/src/theme/components/Badge.ts b/react/src/theme/components/Badge.ts
index 11d6ae8e..da0a1a39 100644
--- a/react/src/theme/components/Badge.ts
+++ b/react/src/theme/components/Badge.ts
@@ -1,7 +1,7 @@
import { defineStyle } from '@chakra-ui/react'
import { getColor, SystemStyleObject } from '@chakra-ui/theme-tools'
-import { meetsWcagAaRatio } from '~/theme/utils/contrast'
+import { getContrastColor } from '~/theme/utils/contrast'
import { textStyles } from '../textStyles'
@@ -27,10 +27,7 @@ const variantSolid = defineStyle((props) => {
const bgColor = getColor(theme, solidBgTokenMap[c] ?? `${c}.500`)
let textColor = getColor(theme, 'base.content.inverse')
- const hasSufficientContrast = meetsWcagAaRatio(textColor, bgColor)
- if (!hasSufficientContrast) {
- textColor = 'base.content.default'
- }
+ textColor = getContrastColor(textColor, bgColor, 'base.content.default')
return {
bg: bgColor,
diff --git a/react/src/theme/components/Button.ts b/react/src/theme/components/Button.ts
index 4ada464b..74e02ead 100644
--- a/react/src/theme/components/Button.ts
+++ b/react/src/theme/components/Button.ts
@@ -4,7 +4,7 @@ import { merge } from 'lodash'
import { layerStyles } from '../layerStyles'
import { textStyles } from '../textStyles'
-import { meetsWcagAaRatio } from '../utils'
+import { getContrastColor } from '../utils'
import { hexToRgba } from '../utils/hexToRgba'
import { Link } from './Link'
@@ -53,14 +53,13 @@ const genVariantSolidColours = ({
}
}
}
- const hasSufficientContrast = meetsWcagAaRatio(
+ // Note that using the fallback content colour for the button text could still result in bad contrast.
+ color = getContrastColor(
getColor(theme, color),
getColor(theme, solidVariantProps.bg),
+ 'base.content.default',
)
- // Note that using the default content colour for the button text could still result in bad contrast.
- if (!hasSufficientContrast) {
- color = 'base.content.default'
- }
+
return { ...solidVariantProps, color }
}
@@ -296,18 +295,22 @@ const baseStyle = defineStyle({
const sizes = {
xs: defineStyle({
+ ...textStyles['subhead-2'],
minH: '2.25rem',
minW: '2.25rem',
}),
sm: defineStyle({
+ ...textStyles['subhead-1'],
minH: '2.5rem',
minW: '2.5rem',
}),
md: defineStyle({
+ ...textStyles['subhead-1'],
minH: '2.75rem',
minW: '2.75rem',
}),
lg: defineStyle({
+ ...textStyles['subhead-1'],
minH: '3rem',
minW: '3rem',
}),
diff --git a/react/src/theme/components/Toolbar.ts b/react/src/theme/components/Toolbar.ts
new file mode 100644
index 00000000..0c4a7a69
--- /dev/null
+++ b/react/src/theme/components/Toolbar.ts
@@ -0,0 +1,94 @@
+import { createMultiStyleConfigHelpers } from '@chakra-ui/react'
+import { anatomy } from '@chakra-ui/theme-tools'
+
+const parts = anatomy('toolbar').parts('container', 'group', 'divider')
+
+const { defineMultiStyleConfig, definePartsStyle } =
+ createMultiStyleConfigHelpers(parts.keys)
+
+const baseStyle = definePartsStyle({
+ container: {
+ textStyle: 'subhead-2',
+ },
+ divider: {
+ borderColor: 'base.divider.medium',
+ },
+})
+
+const getSolidVariantContainerStyles = (c: string) => {
+ switch (c) {
+ case 'sub':
+ case 'main':
+ return {
+ bg: `interaction.${c}.default`,
+ color: 'white',
+ }
+ case 'neutral':
+ return {
+ bg: 'interaction.neutral-subtle.default',
+ color: 'base.content.default',
+ }
+ default:
+ return {
+ bg: 'base.bg.default',
+ }
+ }
+}
+
+const variantSolid = definePartsStyle((props) => {
+ const { colorScheme: c } = props
+
+ return {
+ container: {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ ...getSolidVariantContainerStyles(c),
+ },
+ }
+})
+
+const variants = {
+ solid: variantSolid,
+}
+
+const sizes = {
+ xs: definePartsStyle({
+ container: {
+ gap: '0.5rem',
+ h: '3rem',
+ py: '0.375rem',
+ pl: '1rem',
+ pr: '0.25rem',
+ },
+ }),
+ sm: definePartsStyle({
+ container: {
+ gap: '0.5rem',
+ h: '3.5rem',
+ py: '0.625rem',
+ pl: '1rem',
+ pr: '0.25rem',
+ },
+ }),
+ md: definePartsStyle({
+ container: {
+ gap: '0.5rem',
+ h: '3.5rem',
+ py: '0.625rem',
+ pl: '1rem',
+ pr: '0.25rem',
+ },
+ }),
+}
+
+export const Toolbar = defineMultiStyleConfig({
+ baseStyle,
+ variants,
+ sizes,
+ defaultProps: {
+ colorScheme: 'sub',
+ variant: 'solid',
+ size: 'md',
+ },
+})
diff --git a/react/src/theme/components/index.ts b/react/src/theme/components/index.ts
index f31432f8..56e9a8c8 100644
--- a/react/src/theme/components/index.ts
+++ b/react/src/theme/components/index.ts
@@ -38,6 +38,7 @@ import { Textarea } from './Textarea'
import { Tile } from './Tile'
import { Toast } from './Toast'
import { Toggle } from './Toggle'
+import { Toolbar } from './Toolbar'
import { Tooltip } from './Tooltip'
export const components = {
@@ -81,5 +82,6 @@ export const components = {
Tile,
Toast,
Toggle,
+ Toolbar,
Tooltip,
}
diff --git a/react/src/theme/utils/contrast.ts b/react/src/theme/utils/contrast.ts
index f0dc38d6..1d57938c 100644
--- a/react/src/theme/utils/contrast.ts
+++ b/react/src/theme/utils/contrast.ts
@@ -94,3 +94,14 @@ export const getContrast = (
return (Math.max(lum1, lum2) + LUM_FLARE) / (Math.min(lum1, lum2) + LUM_FLARE)
}
+
+export const getContrastColor = (
+ fg: string,
+ bg: string,
+ fallback: string,
+): string => {
+ if (meetsWcagAaRatio(fg, bg)) {
+ return fg
+ }
+ return fallback
+}