}
- placeholder="Filter applications"
- value={filter}
- onChange={({ target: { value } }) => setFilter(value)}
- />
+
+ onSelectionChange={(key: any) =>
actions.find((action) => action.label === key)?.onSelect()
}
isDisabled={!isAlive}
diff --git a/src/components/Highlight.tsx b/src/components/Highlight.tsx
index 76bd2269..d13905d3 100644
--- a/src/components/Highlight.tsx
+++ b/src/components/Highlight.tsx
@@ -26,7 +26,7 @@ const LineNumbers = styled(StyledPre)(({ theme }) => ({
}))
const StyledHighlight = styled.div(
- (_) => `
+ ({ theme }) => `
pre code.hljs {
display: block;
overflow-x: auto;
@@ -40,21 +40,21 @@ code.hljs {
.hljs ::selection,
.hljs::selection {
background-color: #383a62;
- color: #ebeff0;
+ color: ${theme.colors['code-block-light-grey']};
}
.hljs-comment {
- color: #747b8b;
+ color: ${theme.colors['code-block-dark-grey']};
}
.hljs-tag {
- color: #c5c9d3;
+ color: ${theme.colors['code-block-mid-grey']};
}
.hljs-operator,
.hljs-punctuation,
.hljs-subst {
- color: #ebeff0;
+ color: ${theme.colors['code-block-light-grey']};
}
.hljs-operator {
@@ -67,7 +67,7 @@ code.hljs {
.hljs-selector-tag,
.hljs-template-variable,
.hljs-variable {
- color: #c5c9d3;
+ color: ${theme.colors['code-block-mid-grey']};
}
.hljs-attr,
@@ -77,30 +77,36 @@ code.hljs {
.hljs-symbol,
.hljs-variable.constant_ {
color: #969af8;
+ color: ${theme.colors['code-block-purple']};
+
}
.hljs-class .hljs-title,
.hljs-title,
.hljs-title.class_ {
- color: #7075f5;
+ color: ${theme.colors['code-block-dark-purple']};
+
}
.hljs-strong {
font-weight: 700;
- color: #7075f5;
+ color: ${theme.colors['code-block-dark-purple']};
+
}
.hljs-addition,
.hljs-code,
.hljs-string,
.hljs-title.class_.inherited__ {
color: #8fd6ff;
+ color: ${theme.colors['code-block-mid-blue']};
+
}
.hljs-built_in,
.hljs-doctag,
.hljs-keyword.hljs-atrule,
.hljs-quote,
.hljs-regexp {
- color: #c2e9ff;
+ color: ${theme.colors['code-block-light-blue']};
}
.hljs-attribute,
@@ -108,7 +114,7 @@ code.hljs {
.hljs-section,
.hljs-title.function_,
.ruby .hljs-property {
- color: #3cecaf;
+ color: ${theme.colors[`code-block-dark-green`]};
}
.diff .hljs-meta,
@@ -116,6 +122,7 @@ code.hljs {
.hljs-template-tag,
.hljs-type {
color: #fff48f;
+ color: ${theme.colors[`code-block-yellow`]};
}
.hljs-emphasis {
@@ -127,6 +134,7 @@ code.hljs {
.hljs-meta .hljs-keyword,
.hljs-meta .hljs-string {
color: #99f5d5;
+ color: ${theme.colors[`code-block-light-green`]};
}
.hljs-meta .hljs-keyword,
diff --git a/src/components/InlineCode.tsx b/src/components/InlineCode.tsx
index ec03bd00..77a421df 100644
--- a/src/components/InlineCode.tsx
+++ b/src/components/InlineCode.tsx
@@ -40,8 +40,8 @@ const parentFillLevelToBorderColor: {
// consistent when INLINE_CODE_MIN_PX changes.
const PADDING_EMS = 0.1669
-const Code = styled.code<{ parentFillLevel: FillLevel }>(
- ({ theme, parentFillLevel }) => ({
+const CodeElt = styled.code<{ $parentFillLevel: FillLevel }>(
+ ({ theme, $parentFillLevel: parentFillLevel }) => ({
...theme.partials.text.inlineCode,
border: theme.borders.default,
borderRadius: theme.borderRadiuses.large,
@@ -73,9 +73,9 @@ const InlineCode = forwardRef
>(
return (
<>
-
diff --git a/src/components/contexts/ColorModeProvider.tsx b/src/components/contexts/ColorModeProvider.tsx
new file mode 100644
index 00000000..5659e852
--- /dev/null
+++ b/src/components/contexts/ColorModeProvider.tsx
@@ -0,0 +1,32 @@
+import { ThemeProvider as HonorableThemeProvider } from 'honorable'
+import { type ComponentProps } from 'react'
+
+import styled, { ThemeProvider as StyledThemeProvider } from 'styled-components'
+
+import {
+ COLOR_THEME_KEY,
+ type ColorMode,
+ honorableThemeDark,
+ honorableThemeLight,
+ styledThemeDark,
+ styledThemeLight,
+} from '../../theme'
+
+const Wrapper = styled.div``
+
+export function ColorModeProvider({
+ mode = 'dark',
+ ...props
+}: { mode: ColorMode } & ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/index.ts b/src/index.ts
index 8b6993f7..20083a74 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -125,9 +125,18 @@ export {
useSetBreadcrumbs,
type Breadcrumb,
} from './components/contexts/BreadcrumbsContext'
+export { ColorModeProvider } from './components/contexts/ColorModeProvider'
// Theme
-export { default as theme, styledTheme } from './theme'
+export {
+ honorableThemeDark as theme,
+ styledTheme,
+ styledThemeLight,
+ honorableThemeLight,
+ styledThemeDark,
+} from './theme'
+export type { SemanticColorKey, SemanticColorCssVar } from './theme/colors'
+export { semanticColorKeys, semanticColorCssVars } from './theme/colors'
export { default as GlobalStyle } from './GlobalStyle'
// Utils
diff --git a/src/stories/Card.stories.tsx b/src/stories/Card.stories.tsx
index 21695e8e..a13a2888 100644
--- a/src/stories/Card.stories.tsx
+++ b/src/stories/Card.stories.tsx
@@ -1,6 +1,8 @@
import { Flex } from 'honorable'
import { type ComponentProps } from 'react'
+import { useTheme } from 'styled-components'
+
import { type FillLevel } from '../components/contexts/FillLevelContext'
import { Card } from '../index'
@@ -65,38 +67,45 @@ function FillLevelTemplate({
selected,
width,
}: { width: number } & CardProps) {
+ const theme = useTheme()
+
return (
-
- {fillLevels.map((fillLevel) => (
-
- fillLevel="{fillLevel}"
-
-
+
+
fill-zero
+
+ {fillLevels.map((fillLevel) => (
-
-
- Each Card background should be one level lighter than its parent,
- but not exceed fill-three
-
-
+ fillLevel=
+ {fillLevel === undefined ? 'undefined' : `"${fillLevel}"`}
+ {!fillLevel && ` (1-3 determined by context)`}
+
+
+
+
+
+ Each Card background should be one level lighter than its
+ parent, but not exceed fill-three
+
+
+
-
- ))}
-
+ ))}
+
+
)
}
diff --git a/src/stories/Colors.tsx b/src/stories/Colors.tsx
new file mode 100644
index 00000000..fda1d123
--- /dev/null
+++ b/src/stories/Colors.tsx
@@ -0,0 +1,123 @@
+import styled, { useTheme } from 'styled-components'
+import { partition } from 'lodash-es'
+
+import { Flex } from 'honorable'
+
+import Divider from '../components/Divider'
+
+import { FlexWrap } from './FlexWrap'
+import { FilledBox } from './FilledBox'
+import { ItemLabel } from './ItemLabel'
+
+const ColorBox = styled(FilledBox)<{ $colorKey: string | number }>(
+ ({ theme, $colorKey }) => ({
+ boxShadow: theme.boxShadows.moderate,
+ backgroundColor: (theme.colors as any)[$colorKey],
+ })
+)
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const ColorBoxWrap = styled.div((_) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ width: '64px',
+}))
+
+export function Colors({
+ title,
+ colors,
+}: {
+ title: string
+ colors: [string | number, string][]
+}) {
+ return (
+
+
+
+ {colors.map(([key]) => (
+
+
+ {key}
+
+ ))}
+
+
+ )
+}
+
+function Template() {
+ const theme = useTheme()
+ const colors = { ...theme.colors }
+
+ const colorEntries = Object.entries(colors).filter(
+ (x): x is [(typeof x)[0], string] => typeof x[1] === 'string'
+ )
+ const [fills, rest1] = partition(colorEntries, (key) =>
+ `${key}`.startsWith('fill')
+ )
+ const [borders, rest2] = partition(rest1, (key) =>
+ `${key}`.startsWith('border')
+ )
+ const [text, rest3] = partition(rest2, (key) => `${key}`.startsWith('text'))
+ const [icons, rest4] = partition(rest3, (key) => `${key}`.startsWith('icon'))
+ const [codeBlock, rest5] = partition(rest4, (key) =>
+ `${key}`.startsWith('code-block')
+ )
+ const [cloudShell, rest6] = partition(rest5, (key) =>
+ `${key}`.startsWith('cloud-shell')
+ )
+ const [action, rest7] = partition(rest6, (key) =>
+ `${key}`.startsWith('action')
+ )
+
+ const misc = rest7
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const Exp = Template.bind({})
+
+Exp.args = {}
+export default Exp
diff --git a/src/stories/FilledBox.tsx b/src/stories/FilledBox.tsx
new file mode 100644
index 00000000..279caaa0
--- /dev/null
+++ b/src/stories/FilledBox.tsx
@@ -0,0 +1,9 @@
+import styled from 'styled-components'
+
+export const FilledBox = styled.div<{ $bgColor?: string }>(
+ ({ theme, $bgColor }) => ({
+ width: '64px',
+ height: '64px',
+ backgroundColor: $bgColor || theme.colors['fill-one'],
+ })
+)
diff --git a/src/stories/FlexWrap.tsx b/src/stories/FlexWrap.tsx
new file mode 100644
index 00000000..0b82487f
--- /dev/null
+++ b/src/stories/FlexWrap.tsx
@@ -0,0 +1,7 @@
+import styled from 'styled-components'
+
+export const FlexWrap = styled.div(({ theme }) => ({
+ display: 'flex',
+ flexWrap: 'wrap',
+ gap: theme.spacing.large,
+}))
diff --git a/src/stories/ItemLabel.tsx b/src/stories/ItemLabel.tsx
new file mode 100644
index 00000000..2a9ac4c6
--- /dev/null
+++ b/src/stories/ItemLabel.tsx
@@ -0,0 +1,6 @@
+import styled from 'styled-components'
+
+export const ItemLabel = styled.div(({ theme }) => ({
+ ...theme.partials.text.caption,
+ marginTop: theme.spacing.xxsmall,
+}))
diff --git a/src/stories/Typography.tsx b/src/stories/Typography.tsx
new file mode 100644
index 00000000..3e294a7f
--- /dev/null
+++ b/src/stories/Typography.tsx
@@ -0,0 +1,147 @@
+import styled from 'styled-components'
+
+import Divider from '../components/Divider'
+
+import { type styledTheme } from '..'
+
+const SemanticText = styled.div<{
+ $typeStyle?: keyof typeof styledTheme.partials.text
+}>(({ theme, $typeStyle: typeStyle }) => ({
+ ...theme.partials.text[typeStyle],
+ marginBottom: theme.spacing.large,
+}))
+
+export function Typography({
+ exampleText: txt = 'Lorem ipsum dolor sit amet',
+}: {
+ exampleText: string
+}) {
+ return (
+ <>
+ H1 - {txt}
+ H2 - {txt}
+ H3 - {txt}
+ H4 - {txt}
+ Title 1 - {txt}
+ Title 2 - {txt}
+ Subtitle 1 - {txt}
+ Subtitle 2 - {txt}
+ Body 1 (Bold) - {txt}
+ Body 1 - {txt}
+ Body 2 (Bold) - {txt}
+ Body 2 - {txt}
+
+ Body 2 Loose Line Height - {txt}
+
+ Caption - {txt}
+ Badge Label - {txt}
+ Large Button - {txt}
+ Small Button - {txt}
+ Overline - {txt}
+ Code - {txt}
+ >
+ )
+}
+const MktgText = styled.div<{
+ $typeStyle?: keyof typeof styledTheme.partials.marketingText
+}>(({ theme, $typeStyle: typeStyle }) => ({
+ ...theme.partials.marketingText[typeStyle],
+ display: 'block',
+ marginBottom: theme.spacing.large,
+}))
+const MarketingInlineLink = styled.a(({ theme }) => ({
+ ...theme.partials.marketingText.inlineLink,
+}))
+
+export function MarketingTypography({
+ exampleText: txt = 'Lorem ipsum dolor sit amet',
+}: {
+ exampleText: string
+}) {
+ return (
+ <>
+
+ Big Header (
+ Inline link) - {txt}
+
+
+ Hero 1 (Inline link)
+ - {txt}
+
+
+ Hero 2 (Inline link)
+ - {txt}
+
+
+ Title 1 (Inline link
+ ) - {txt}
+
+
+ Title 2 (Inline link
+ ) - {txt}
+
+
+ Subtitle 1 (
+ Inline link) - {txt}
+
+
+ Subtitle 2 (
+ Inline link) - {txt}
+
+
+ Body 1 (Bold) (
+ Inline link) - {txt}
+
+
+ Body 1 (Inline link)
+ - {txt}
+
+
+ Body 2 (Bold) (
+ Inline link) - {txt}
+
+
+ Body 2 (Inline link)
+ - {txt}
+
+ Standalone link - {txt}
+
+ Component text (
+ Inline link) - {txt}
+
+ Component link - {txt}
+
+ Small component link - {txt}
+
+
+ Label (Inline link)
+ - {txt}
+
+ Nav link - {txt}
+ >
+ )
+}
+
+function Template({ exampleText }: { exampleText?: string }) {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
+
+const Exp = Template.bind({})
+
+export default Exp
+Exp.args = {
+ exampleText: 'Lorem ipsum dolor sit amet',
+}
diff --git a/src/stories/_SemanticSystem.stories.tsx b/src/stories/_SemanticSystem.stories.tsx
index 78d55a2f..0bc141f7 100644
--- a/src/stories/_SemanticSystem.stories.tsx
+++ b/src/stories/_SemanticSystem.stories.tsx
@@ -1,47 +1,35 @@
import styled, { useTheme } from 'styled-components'
import { type FillLevel } from '../components/contexts/FillLevelContext'
-import { type styledTheme } from '..'
import Divider from '../components/Divider'
+import { baseSpacing } from '../theme/spacing'
-const fillLevelToBGColor: Record = {
- 0: 'fill-zero',
- 1: 'fill-one',
- 2: 'fill-two',
- 3: 'fill-three',
-}
+import { ItemLabel } from './ItemLabel'
+import { FilledBox } from './FilledBox'
+import { FlexWrap } from './FlexWrap'
export default {
title: 'Semantic System',
component: null,
}
-const ItemLabel = styled.div(({ theme }) => ({
- ...theme.partials.text.caption,
- marginTop: theme.spacing.xxsmall,
-}))
+const fillLevelToBGColor: Record = {
+ 0: 'fill-zero',
+ 1: 'fill-one',
+ 2: 'fill-two',
+ 3: 'fill-three',
+}
const BlockWrapper = styled.div(({ theme }) => ({
marginBottom: theme.spacing.large,
}))
-const FilledBox = styled.div(({ theme }) => ({
- width: '64px',
- height: '64px',
- backgroundColor: theme.colors['fill-one'],
-}))
-
-function Template({ exampleText }: { exampleText?: string }) {
+function Template() {
return (
<>
-
-
-
-
-
-
>
)
}
-const ColorBox = styled(FilledBox)<{ color: string }>(({ theme, color }) => ({
- boxShadow: theme.boxShadows.moderate,
- backgroundColor: (theme.colors as any)[color],
-}))
-
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const ColorBoxWrap = styled.div((_p) => ({
- display: 'flex',
- flexDirection: 'column',
- alignItems: 'center',
- width: '64px',
-}))
-
-const FlexWrap = styled.div(({ theme }) => ({
- display: 'flex',
- flexWrap: 'wrap',
- gap: theme.spacing.large,
-}))
-
-function Colors() {
- const theme = useTheme()
-
- const colors = { ...theme.colors }
-
- delete colors.blue
- delete colors.grey
- delete colors.green
- delete colors.yellow
- delete colors.red
-
- return (
-
- {Object.entries(colors).map(([key]) => (
-
-
- {key}
-
- ))}
-
- )
-}
-
const ShadowedBox = styled(FilledBox)<{ shadow: string }>(
({ theme, shadow }) => ({ boxShadow: (theme.boxShadows as any)[shadow] })
)
@@ -142,20 +78,24 @@ function Shadows() {
)
}
-const RadiusedBox = styled(FilledBox)<{ radius: 'medium' | 'large' }>(
- ({ theme, radius }) => ({
+const RadiusedBox = styled(FilledBox)<{ $radius: 'medium' | 'large' }>(
+ ({ theme, $radius: radius }) => ({
borderRadius: theme.borderRadiuses[radius],
})
)
function BoxRadiuses() {
const radii: ('medium' | 'large')[] = ['medium', 'large']
+ const theme = useTheme()
return (
{radii.map((key) => (
-
+
{key}
))}
@@ -259,161 +199,32 @@ function Scrollbars() {
)
}
-const SpacingBox = styled.div<{ space: string }>(({ theme, space }) => ({
+const SpacingBox = styled.div<{ $space: number }>(({ theme, $space }) => ({
borderRadius: 0,
backgroundColor: theme.colors['action-primary'],
margin: 0,
- paddingRight: (theme.spacing as any)[space],
- paddingTop: (theme.spacing as any)[space],
+ paddingRight: $space,
+ paddingTop: $space,
width: 'min-content',
}))
function Spacing() {
return (
<>
- {[
- 'xxxsmall',
- 'xxsmall',
- 'xsmall',
- 'small',
- 'medium',
- 'large',
- 'xlarge',
- 'xxlarge',
- 'xxxlarge',
- 'xxxxlarge',
- 'xxxxxlarge',
- ].map((key) => (
+ {Object.entries(baseSpacing).map(([key, val]) => (
-
- {key}
+
+
+ {key}: {val}px
+
))}
>
)
}
-const SemanticText = styled.div<{
- typeStyle?: keyof typeof styledTheme.partials.text
-}>(({ theme, typeStyle }) => ({
- ...theme.partials.text[typeStyle],
- marginBottom: theme.spacing.large,
-}))
+export const Miscellaneous = Template.bind({})
+Miscellaneous.args = {}
-function Typography({
- exampleText: txt = 'Lorem ipsum dolor sit amet',
-}: {
- exampleText: string
-}) {
- return (
- <>
- H1 - {txt}
- H2 - {txt}
- H3 - {txt}
- H4 - {txt}
- Title 1 - {txt}
- Title 2 - {txt}
- Subtitle 1 - {txt}
- Subtitle 2 - {txt}
- Body 1 (Bold) - {txt}
- Body 1 - {txt}
- Body 2 (Bold) - {txt}
- Body 2 - {txt}
-
- Body 2 Loose Line Height - {txt}
-
- Caption - {txt}
- Badge Label - {txt}
- Large Button - {txt}
- Small Button - {txt}
- Overline - {txt}
- Code - {txt}
- >
- )
-}
-
-const MktgText = styled.div<{
- typeStyle?: keyof typeof styledTheme.partials.marketingText
-}>(({ theme, typeStyle }) => ({
- ...theme.partials.marketingText[typeStyle],
- display: 'block',
- marginBottom: theme.spacing.large,
-}))
-
-const MarketingInlineLink = styled.a(({ theme }) => ({
- ...theme.partials.marketingText.inlineLink,
-}))
-
-function MarketingTypography({
- exampleText: txt = 'Lorem ipsum dolor sit amet',
-}: {
- exampleText: string
-}) {
- return (
- <>
-
- Big Header (
- Inline link) - {txt}
-
-
- Hero 1 (Inline link)
- - {txt}
-
-
- Hero 2 (Inline link)
- - {txt}
-
-
- Title 1 (Inline link
- ) - {txt}
-
-
- Title 2 (Inline link
- ) - {txt}
-
-
- Subtitle 1 (
- Inline link) - {txt}
-
-
- Subtitle 2 (
- Inline link) - {txt}
-
-
- Body 1 (Bold) (
- Inline link) - {txt}
-
-
- Body 1 (Inline link)
- - {txt}
-
-
- Body 2 (Bold) (
- Inline link) - {txt}
-
-
- Body 2 (Inline link)
- - {txt}
-
- Standalone link - {txt}
-
- Component text (
- Inline link) - {txt}
-
- Component link - {txt}
-
- Small component link - {txt}
-
-
- Label (Inline link)
- - {txt}
-
- Nav link - {txt}
- >
- )
-}
-
-export const SemanticSystem = Template.bind({})
-SemanticSystem.args = {
- exampleText: 'Lorem ipsum dolor sit amet',
-}
+export { default as Colors } from './Colors'
+export { default as Typography } from './Typography'
diff --git a/src/theme.tsx b/src/theme.tsx
index 29af0610..98e2e189 100644
--- a/src/theme.tsx
+++ b/src/theme.tsx
@@ -2,15 +2,13 @@ import { mergeTheme } from 'honorable'
import defaultTheme from 'honorable-theme-default'
import mapperRecipe from 'honorable-recipe-mapper'
-import {
- blue,
- green,
- grey,
- purple,
- red,
- semanticColors,
- yellow,
-} from './theme/colors'
+import { useState } from 'react'
+
+import { useMutationObserver } from '@react-hooks-library/core'
+
+import { semanticColorsDark } from './theme/colors-semantic-dark'
+import { semanticColorsLight } from './theme/colors-semantic-light'
+import { baseColors } from './theme/colors-base'
import { spacing } from './theme/spacing'
import { fontFamilies } from './theme/fonts'
import { textPartials } from './theme/text'
@@ -20,14 +18,20 @@ import {
borderWidths,
borders,
} from './theme/borders'
-import { boxShadows } from './theme/boxShadows'
+import { getBoxShadows } from './theme/boxShadows'
import { scrollBar } from './theme/scrollBar'
import { zIndexes } from './theme/zIndexes'
-import { focusPartials } from './theme/focus'
+import { getFocusPartials } from './theme/focus'
import { resetPartials } from './theme/resets'
import { marketingTextPartials } from './theme/marketingText'
import gradients from './theme/gradients'
+export const COLOR_THEME_KEY = 'theme-mode'
+
+export const COLOR_MODES = ['light', 'dark'] as const
+export type ColorMode = (typeof COLOR_MODES)[number]
+export const DEFAULT_COLOR_MODE: ColorMode = 'dark'
+
export type StringObj = { [key: string]: string | StringObj }
const spacers = {
@@ -53,731 +57,807 @@ const portals = {
},
}
-const baseTheme = {
- name: 'Plural',
- mode: 'dark',
- breakpoints: {
- // We'll add mobile breakpoints later
- desktopSmall: 1000,
- desktop: 1280,
- desktopLarge: 1440,
- },
- colors: {
- // Base palette,
- blue,
- grey,
- green,
- yellow,
- red,
- purple,
- // Semantic colors,
- ...semanticColors,
- },
-}
+const colorsDark = {
+ ...baseColors,
+ ...semanticColorsDark,
+} as const
-const honorableTheme = mergeTheme(defaultTheme, {
- ...baseTheme,
- stylesheet: {
- html: [
- {
- fontSize: 14,
- fontFamily: fontFamilies.sans,
- backgroundColor: 'fill-zero',
- },
- ],
- '::placeholder': [
- {
- color: 'text-xlight',
- },
- ],
- },
- global: [
- /* Spacing */
- mapperRecipe('gap', spacing),
- ...Object.entries(spacers).map(
- ([key, nextKeys]) =>
- (props: any) =>
- props[key] !== null &&
- typeof props[key] !== 'undefined' &&
- Object.fromEntries(
- nextKeys.map((nextKey) => [
- nextKey,
- (spacing as any)[props[key]] || props[key],
- ])
- )
- ),
- ({ fill }: any) =>
- fill === true && {
- // === true to prevent the `fill` css property to apply here
- width: '100%',
- height: '100%',
- },
- /* Border radiuses */
- mapperRecipe('borderRadius', borderRadiuses),
- /* Shadows */
- mapperRecipe('boxShadow', boxShadows),
- /* Texts */
- ({ h1 }: any) => h1 && textPartials.h1,
- ({ h2 }: any) => h2 && textPartials.h2,
- ({ h3 }: any) => h3 && textPartials.h3,
- ({ h4 }: any) => h4 && textPartials.h4,
- ({ title1 }: any) => title1 && textPartials.title1,
- ({ title2 }: any) => title2 && textPartials.title2,
- ({ subtitle1 }: any) => subtitle1 && textPartials.subtitle1,
- ({ subtitle2 }: any) => subtitle2 && textPartials.subtitle2,
- ({ body1, body2, bold }: any) => ({
- ...(body1 && textPartials.body1),
- ...(body2 && textPartials.body2),
- ...((body1 || body2) && bold && textPartials.bodyBold),
- }),
- ({ body2LooseLineHeight, bold }: any) => ({
- ...(body2LooseLineHeight && textPartials.body2LooseLineHeight),
- ...(body2LooseLineHeight && bold && textPartials.bodyBold),
- }),
- ({ caption }: any) => caption && textPartials.caption,
- ({ badgeLabel }: any) => badgeLabel && textPartials.badgeLabel,
- ({ buttonMedium }: any) => buttonMedium && textPartials.buttonMedium,
- ({ buttonLarge }: any) => buttonLarge && textPartials.buttonLarge,
- ({ buttonSmall }: any) => buttonSmall && textPartials.buttonSmall,
- ({ overline }: any) => overline && textPartials.overline,
- ({ truncate }: any) => truncate && textPartials.truncate,
- /* Deprecated */
- ({ body0 }: any) =>
- body0 && {
- fontSize: 18,
- lineHeight: '28px',
- },
- /* Deprecated */
- ({ font }: any) =>
- font === 'action' && {
- fontFamily: 'Monument',
- letterSpacing: 1,
- fontWeight: 500,
- },
- /* deprecated in favor of _hover */
- ({ hoverIndicator }: any) =>
- hoverIndicator && {
- '&:hover': {
- backgroundColor: hoverIndicator,
+const colorsLight = {
+ ...baseColors,
+ ...semanticColorsLight,
+} as const
+
+const getBaseTheme = ({ mode }: { mode: ColorMode }) =>
+ ({
+ name: 'Plural',
+ mode,
+ breakpoints: {
+ // We'll add mobile breakpoints later
+ desktopSmall: 1000,
+ desktop: 1280,
+ desktopLarge: 1440,
+ },
+ } as const)
+
+const getHonorableThemeProps = ({ mode }: { mode: ColorMode }) => {
+ const boxShadows = getBoxShadows({ mode })
+ const focusPartials = getFocusPartials({ mode })
+
+ return {
+ stylesheet: {
+ html: [
+ {
+ fontSize: 14,
+ fontFamily: fontFamilies.sans,
+ backgroundColor: 'fill-zero',
+ },
+ ],
+ '::placeholder': [
+ {
+ color: 'text-xlight',
},
- },
- ],
- A: {
- Root: [
- {
- color: 'text',
- },
- ({ inline }: any) => inline && textPartials.inlineLink,
- ],
- },
- Accordion: {
- Root: [
- ({ ghost }: any) =>
- ghost && {
- border: 'none',
- elevation: 0,
- backgroundColor: 'inherit',
+ ],
+ },
+ global: [
+ /* Spacing */
+ mapperRecipe('gap', spacing),
+ ...Object.entries(spacers).map(
+ ([key, nextKeys]) =>
+ (props: any) =>
+ props[key] !== null &&
+ typeof props[key] !== 'undefined' &&
+ Object.fromEntries(
+ nextKeys.map((nextKey) => [
+ nextKey,
+ (spacing as any)[props[key]] || props[key],
+ ])
+ )
+ ),
+ ({ fill }: any) =>
+ fill === true && {
+ // === true to prevent the `fill` css property to apply here
+ width: '100%',
+ height: '100%',
+ },
+ /* Border radiuses */
+ mapperRecipe('borderRadius', borderRadiuses),
+ /* Shadows */
+ mapperRecipe('boxShadow', boxShadows),
+ /* Texts */
+ ({ h1 }: any) => h1 && textPartials.h1,
+ ({ h2 }: any) => h2 && textPartials.h2,
+ ({ h3 }: any) => h3 && textPartials.h3,
+ ({ h4 }: any) => h4 && textPartials.h4,
+ ({ title1 }: any) => title1 && textPartials.title1,
+ ({ title2 }: any) => title2 && textPartials.title2,
+ ({ subtitle1 }: any) => subtitle1 && textPartials.subtitle1,
+ ({ subtitle2 }: any) => subtitle2 && textPartials.subtitle2,
+ ({ body1, body2, bold }: any) => ({
+ ...(body1 && textPartials.body1),
+ ...(body2 && textPartials.body2),
+ ...((body1 || body2) && bold && textPartials.bodyBold),
+ }),
+ ({ body2LooseLineHeight, bold }: any) => ({
+ ...(body2LooseLineHeight && textPartials.body2LooseLineHeight),
+ ...(body2LooseLineHeight && bold && textPartials.bodyBold),
+ }),
+ ({ caption }: any) => caption && textPartials.caption,
+ ({ badgeLabel }: any) => badgeLabel && textPartials.badgeLabel,
+ ({ buttonMedium }: any) => buttonMedium && textPartials.buttonMedium,
+ ({ buttonLarge }: any) => buttonLarge && textPartials.buttonLarge,
+ ({ buttonSmall }: any) => buttonSmall && textPartials.buttonSmall,
+ ({ overline }: any) => overline && textPartials.overline,
+ ({ truncate }: any) => truncate && textPartials.truncate,
+ /* Deprecated */
+ ({ body0 }: any) =>
+ body0 && {
+ fontSize: 18,
+ lineHeight: '28px',
+ },
+ /* Deprecated */
+ ({ font }: any) =>
+ font === 'action' && {
+ fontFamily: 'Monument',
+ letterSpacing: 1,
+ fontWeight: 500,
+ },
+ /* deprecated in favor of _hover */
+ ({ hoverIndicator }: any) =>
+ hoverIndicator && {
+ '&:hover': {
+ backgroundColor: hoverIndicator,
+ },
},
],
- },
- Avatar: {
- Root: [
- {
- backgroundColor: 'action-primary',
- borderRadius: 4, // TODO 3 or 6
- fontWeight: 400,
- },
- ],
- },
- Button: {
- Root: [
- {
- buttonMedium: true,
- display: 'flex',
- borderRadius: 'normal',
- backgroundColor: 'action-primary',
- border: '1px solid action-primary',
- paddingTop: spacing.xsmall - 1,
- paddingBottom: spacing.xsmall - 1,
- paddingRight: spacing.medium - 1,
- paddingLeft: spacing.medium - 1,
- _focus: {
- outline: 'none',
- },
- _focusVisible: {
- ...focusPartials.button,
- },
- ':hover': {
- backgroundColor: 'action-primary-hover',
- border: '1px solid action-primary-hover',
- },
- ':active': {
- backgroundColor: 'action-primary',
- border: '1px solid action-primary',
+ A: {
+ Root: [
+ {
+ color: 'text',
},
- ':disabled': {
- color: 'text-primary-disabled',
- backgroundColor: 'action-primary-disabled',
- border: '1px solid action-primary-disabled',
- ':hover': {
- backgroundColor: 'action-primary-disabled',
- border: '1px solid action-primary-disabled',
+ ({ inline }: any) => inline && textPartials.inlineLink,
+ ],
+ },
+ Accordion: {
+ Root: [
+ ({ ghost }: any) =>
+ ghost && {
+ border: 'none',
+ elevation: 0,
+ backgroundColor: 'inherit',
},
+ ],
+ },
+ Avatar: {
+ Root: [
+ {
+ backgroundColor: 'action-primary',
+ borderRadius: 4, // TODO 3 or 6
+ fontWeight: 400,
},
- },
- ({ secondary }: any) =>
- secondary && {
- color: 'text-light',
- backgroundColor: 'transparent',
- border: '1px solid border-input',
+ ],
+ },
+ Button: {
+ Root: [
+ {
+ buttonMedium: true,
+ display: 'flex',
+ borderRadius: 'normal',
+ backgroundColor: 'action-primary',
+ border: '1px solid action-primary',
+ paddingTop: spacing.xsmall - 1,
+ paddingBottom: spacing.xsmall - 1,
+ paddingRight: spacing.medium - 1,
+ paddingLeft: spacing.medium - 1,
+ _focus: {
+ outline: 'none',
+ },
+ _focusVisible: {
+ ...focusPartials.button,
+ },
':hover': {
- color: 'text',
- backgroundColor: 'action-input-hover',
- border: '1px solid border-input',
+ backgroundColor: 'action-primary-hover',
+ border: '1px solid action-primary-hover',
},
':active': {
- color: 'text',
- backgroundColor: 'transparent',
- border: '1px solid border-input',
- },
- ':focus-visible': {
- color: 'text',
- backgroundColor: 'action-input-hover',
+ backgroundColor: 'action-primary',
+ border: '1px solid action-primary',
},
':disabled': {
- color: 'text-disabled',
+ color: 'text-primary-disabled',
+ backgroundColor: 'action-primary-disabled',
+ border: '1px solid action-primary-disabled',
+ ':hover': {
+ backgroundColor: 'action-primary-disabled',
+ border: '1px solid action-primary-disabled',
+ },
+ },
+ },
+ ({ secondary }: any) =>
+ secondary && {
+ color: 'text-light',
backgroundColor: 'transparent',
border: '1px solid border-input',
':hover': {
+ color: 'text',
+ backgroundColor: 'action-input-hover',
+ border: '1px solid border-input',
+ },
+ ':active': {
+ color: 'text',
backgroundColor: 'transparent',
border: '1px solid border-input',
},
+ ':focus-visible': {
+ color: 'text',
+ backgroundColor: 'action-input-hover',
+ },
+ ':disabled': {
+ color: 'text-disabled',
+ backgroundColor: 'transparent',
+ border: '1px solid border-input',
+ ':hover': {
+ backgroundColor: 'transparent',
+ border: '1px solid border-input',
+ },
+ },
},
- },
- ({ tertiary }: any) =>
- tertiary && {
- color: 'text-light',
- backgroundColor: 'transparent',
- border: '1px solid transparent',
- ':hover': {
- color: 'text',
- backgroundColor: 'action-input-hover',
- border: '1px solid transparent',
- },
- ':active': {
- color: 'text',
- backgroundColor: 'transparent',
- border: '1px solid transparent',
- },
- ':focus-visible': {
- color: 'text',
- backgroundColor: 'action-input-hover',
- },
- ':disabled': {
- color: 'text-disabled',
+ ({ tertiary }: any) =>
+ tertiary && {
+ color: 'text-light',
backgroundColor: 'transparent',
border: '1px solid transparent',
':hover': {
+ color: 'text',
+ backgroundColor: 'action-input-hover',
+ border: '1px solid transparent',
+ },
+ ':active': {
+ color: 'text',
backgroundColor: 'transparent',
border: '1px solid transparent',
},
+ ':focus-visible': {
+ color: 'text',
+ backgroundColor: 'action-input-hover',
+ },
+ ':disabled': {
+ color: 'text-disabled',
+ backgroundColor: 'transparent',
+ border: '1px solid transparent',
+ ':hover': {
+ backgroundColor: 'transparent',
+ border: '1px solid transparent',
+ },
+ },
},
- },
- ({ tertiary, padding }: any) =>
- tertiary &&
- padding === 'none' && {
- color: 'text-light',
- backgroundColor: 'transparent',
- border: '1px solid transparent',
- paddingHorizontal: '0',
- ':hover': {
- backgroundColor: 'transparent',
- textDecoration: 'underline',
- },
- ':active': {
- textDecoration: 'underline',
- },
- ':focus-visible': {
+ ({ tertiary, padding }: any) =>
+ tertiary &&
+ padding === 'none' && {
+ color: 'text-light',
backgroundColor: 'transparent',
- textDecoration: 'underline',
- },
- },
- ({ destructive }: any) =>
- destructive && {
- color: 'text-danger',
- backgroundColor: 'transparent',
- border: '1px solid border-danger',
- ':hover': {
- backgroundColor: 'action-input-hover',
- border: '1px solid border-danger',
+ border: '1px solid transparent',
+ paddingHorizontal: '0',
+ ':hover': {
+ backgroundColor: 'transparent',
+ textDecoration: 'underline',
+ },
+ ':active': {
+ textDecoration: 'underline',
+ },
+ ':focus-visible': {
+ backgroundColor: 'transparent',
+ textDecoration: 'underline',
+ },
},
- ':active': {
+ ({ destructive }: any) =>
+ destructive && {
+ color: 'text-danger',
backgroundColor: 'transparent',
border: '1px solid border-danger',
- },
- ':focus-visible': {
- backgroundColor: 'action-input-hover',
- },
- ':disabled': {
- color: 'text-disabled',
- backgroundColor: 'transparent',
- border: '1px solid border-disabled',
':hover': {
+ backgroundColor: 'action-input-hover',
+ border: '1px solid border-danger',
+ },
+ ':active': {
+ backgroundColor: 'transparent',
+ border: '1px solid border-danger',
+ },
+ ':focus-visible': {
+ backgroundColor: 'action-input-hover',
+ },
+ ':disabled': {
+ color: 'text-disabled',
backgroundColor: 'transparent',
border: '1px solid border-disabled',
+ ':hover': {
+ backgroundColor: 'transparent',
+ border: '1px solid border-disabled',
+ },
},
},
- },
- ({ floating }: any) =>
- floating && {
- color: 'text-light',
- backgroundColor: 'fill-two',
- border: '1px solid border-input',
- // boxShadow isn't getting set when placed in the root here,
- // but using the '&' prop gets around it
- '&': {
- boxShadow: boxShadows.slight,
- },
- ':hover': {
- color: 'text',
- backgroundColor: 'fill-two-hover',
- border: '1px solid border-input',
- boxShadow: boxShadows.moderate,
- },
- ':active': {
- color: 'text',
- backgroundColor: 'fill-two-hover',
- border: '1px solid border-input',
- },
- ':focus-visible': {
- color: 'text',
- backgroundColor: 'fill-two-selected',
- },
- ':disabled': {
- color: 'text-disabled',
- backgroundColor: 'transparent',
+ ({ floating }: any) =>
+ floating && {
+ color: 'text-light',
+ backgroundColor: 'fill-two',
border: '1px solid border-input',
+ // boxShadow isn't getting set when placed in the root here,
+ // but using the '&' prop gets around it
+ '&': {
+ boxShadow: boxShadows.slight,
+ },
':hover': {
+ color: 'text',
+ backgroundColor: 'fill-two-hover',
+ border: '1px solid border-input',
+ boxShadow: boxShadows.moderate,
+ },
+ ':active': {
+ color: 'text',
+ backgroundColor: 'fill-two-hover',
+ border: '1px solid border-input',
+ },
+ ':focus-visible': {
+ color: 'text',
+ backgroundColor: 'fill-two-selected',
+ },
+ ':disabled': {
+ color: 'text-disabled',
backgroundColor: 'transparent',
border: '1px solid border-input',
+ ':hover': {
+ backgroundColor: 'transparent',
+ border: '1px solid border-input',
+ },
},
},
- },
- ({ large }: any) =>
- large && {
- buttonLarge: true,
- paddingTop: spacing.small - 1,
- paddingBottom: spacing.small - 1,
- paddingRight: spacing.large - 1,
- paddingLeft: spacing.large - 1,
- },
- ({ small }: any) =>
- small && {
- buttonSmall: true,
- paddingTop: spacing.xxsmall - 1,
- paddingBottom: spacing.xxsmall - 1,
- paddingRight: spacing.medium - 1,
- paddingLeft: spacing.medium - 1,
- minHeight: 32,
- },
- ],
- StartIcon: [
- {
- margin: '0 12px 0 0 !important',
- },
- ({ large }: any) =>
- large && {
- margin: '0 16px 0 0 !important',
- },
- ({ small }: any) =>
- small && {
+ ({ large }: any) =>
+ large && {
+ buttonLarge: true,
+ paddingTop: spacing.small - 1,
+ paddingBottom: spacing.small - 1,
+ paddingRight: spacing.large - 1,
+ paddingLeft: spacing.large - 1,
+ },
+ ({ small }: any) =>
+ small && {
+ buttonSmall: true,
+ paddingTop: spacing.xxsmall - 1,
+ paddingBottom: spacing.xxsmall - 1,
+ paddingRight: spacing.medium - 1,
+ paddingLeft: spacing.medium - 1,
+ minHeight: 32,
+ },
+ ],
+ StartIcon: [
+ {
margin: '0 12px 0 0 !important',
},
- ],
- EndIcon: [
- {
- margin: '0 0 0 12px !important',
- },
- ({ large }: any) =>
- large && {
- margin: '0 0 0 16px !important',
- },
- ({ small }: any) =>
- small && {
+ ({ large }: any) =>
+ large && {
+ margin: '0 16px 0 0 !important',
+ },
+ ({ small }: any) =>
+ small && {
+ margin: '0 12px 0 0 !important',
+ },
+ ],
+ EndIcon: [
+ {
margin: '0 0 0 12px !important',
},
- ],
- },
- ButtonGroup: {
- Root: [
- {
- border: '1px solid border',
- borderRadius: 4,
- '& > button': {
- border: '1px solid transparent',
- },
- overflow: 'hidden',
- },
- ({ direction }: any) =>
- direction === 'row' && {
+ ({ large }: any) =>
+ large && {
+ margin: '0 0 0 16px !important',
+ },
+ ({ small }: any) =>
+ small && {
+ margin: '0 0 0 12px !important',
+ },
+ ],
+ },
+ ButtonGroup: {
+ Root: [
+ {
+ border: '1px solid border',
+ borderRadius: 4,
'& > button': {
- borderLeft: '1px solid border',
- '&:first-of-type': {
- borderLeft: '1px solid transparent',
+ border: '1px solid transparent',
+ },
+ overflow: 'hidden',
+ },
+ ({ direction }: any) =>
+ direction === 'row' && {
+ '& > button': {
+ borderLeft: '1px solid border',
+ '&:first-of-type': {
+ borderLeft: '1px solid transparent',
+ },
},
},
- },
- ({ direction }: any) =>
- direction === 'column' && {
- '& > button': {
- borderTop: '1px solid border',
- '&:first-of-type': {
- borderTop: '1px solid transparent',
+ ({ direction }: any) =>
+ direction === 'column' && {
+ '& > button': {
+ borderTop: '1px solid border',
+ '&:first-of-type': {
+ borderTop: '1px solid transparent',
+ },
},
},
- },
- ],
- },
- Checkbox: {
- Root: [
- ({ small }: any) =>
- small && {
- '> span': {
- borderWidth: '.75px',
+ ],
+ },
+ Checkbox: {
+ Root: [
+ ({ small }: any) =>
+ small && {
+ '> span': {
+ borderWidth: '.75px',
+ },
},
+ ],
+ Control: [
+ {
+ width: 24,
+ height: 24,
+ borderRadius: 'normal',
+ },
+ ({ small }: any) =>
+ small && {
+ width: 16,
+ height: 16,
+ },
+ ],
+ },
+ H1: {
+ Root: [
+ {
+ fontFamily: 'Monument',
},
- ],
- Control: [
- {
- width: 24,
- height: 24,
- borderRadius: 'normal',
- },
- ({ small }: any) =>
- small && {
- width: 16,
- height: 16,
- },
- ],
- },
- H1: {
- Root: [
- {
- fontFamily: 'Monument',
- },
- ],
- },
- H2: {
- Root: [
- {
- fontFamily: 'Monument',
- },
- ],
- },
- H3: {
- Root: [
- {
- fontFamily: 'Monument',
- },
- ],
- },
- H4: {
- Root: [
- {
- fontFamily: 'Monument',
- },
- ],
- },
- H5: {
- Root: [
- {
- fontFamily: 'Monument',
- },
- ],
- },
- H6: {
- Root: [
- {
- fontFamily: 'Monument',
- },
- ],
- },
- Input: {
- Root: [
- {
- body2: true,
- display: 'flex',
- overflow: 'hidden',
- justifyContent: 'space-between',
- align: 'center',
- height: 'auto',
- minHeight: 'auto',
- width: 'auto',
- paddingTop: 0,
- paddingBottom: 0,
- paddingRight: 'medium',
- paddingLeft: 'medium',
- border: '1px solid border-input',
- borderRadius: 'normal',
- _focusWithin: {
- borderColor: 'border-outline-focused',
- },
- },
- ({ valid }: any) =>
- valid && {
- borderColor: 'border-outline',
+ ],
+ },
+ H2: {
+ Root: [
+ {
+ fontFamily: 'Monument',
},
- ({ error }: any) =>
- error && {
- borderColor: 'border-danger',
+ ],
+ },
+ H3: {
+ Root: [
+ {
+ fontFamily: 'Monument',
},
- ({ small }: any) =>
- small && {
- caption: true,
+ ],
+ },
+ H4: {
+ Root: [
+ {
+ fontFamily: 'Monument',
},
- ({ disabled }: any) =>
- disabled && {
- backgroundColor: 'transparent',
- color: 'text-disabled',
- borderColor: 'border-disabled',
+ ],
+ },
+ H5: {
+ Root: [
+ {
+ fontFamily: 'Monument',
},
- ],
- InputBase: [
- {
- width: '100%',
- flex: '1 1',
- height: '38px',
- lineHeight: '38px',
- color: 'text',
- _placeholder: {
- color: 'text-xlight',
+ ],
+ },
+ H6: {
+ Root: [
+ {
+ fontFamily: 'Monument',
},
- },
- ({ small }: any) =>
- small && {
- height: '30px',
- lineHeight: '30px',
- },
- ({ large }: any) =>
- large && {
- height: '46px',
- lineHeight: '46px',
- },
- ({ disabled }: any) =>
- disabled && {
- backgroundColor: 'transparent',
- color: 'text-disabled',
- _placeholder: {
- color: 'text-disabled',
+ ],
+ },
+ Input: {
+ Root: [
+ {
+ body2: true,
+ display: 'flex',
+ overflow: 'hidden',
+ justifyContent: 'space-between',
+ align: 'center',
+ height: 'auto',
+ minHeight: 'auto',
+ width: 'auto',
+ paddingTop: 0,
+ paddingBottom: 0,
+ paddingRight: 'medium',
+ paddingLeft: 'medium',
+ border: '1px solid border-input',
+ borderRadius: 'normal',
+ _focusWithin: {
+ borderColor: 'border-outline-focused',
},
},
- ],
- StartIcon: [
- {
- marginRight: 'xsmall',
- },
- ({ disabled }: any) =>
- disabled && {
- '& *': {
+ ({ valid }: any) =>
+ valid && {
+ borderColor: 'border-outline',
+ },
+ ({ error }: any) =>
+ error && {
+ borderColor: 'border-danger',
+ },
+ ({ small }: any) =>
+ small && {
+ caption: true,
+ },
+ ({ disabled }: any) =>
+ disabled && {
+ backgroundColor: 'transparent',
color: 'text-disabled',
+ borderColor: 'border-disabled',
+ },
+ ],
+ InputBase: [
+ {
+ width: '100%',
+ flex: '1 1',
+ height: '38px',
+ lineHeight: '38px',
+ color: 'text',
+ _placeholder: {
+ color: 'text-xlight',
},
},
- ],
- EndIcon: [
- {
- marginLeft: 'small',
- },
- ({ disabled }: any) =>
- disabled && {
- '& *': {
+ ({ small }: any) =>
+ small && {
+ height: '30px',
+ lineHeight: '30px',
+ },
+ ({ large }: any) =>
+ large && {
+ height: '46px',
+ lineHeight: '46px',
+ },
+ ({ disabled }: any) =>
+ disabled && {
+ backgroundColor: 'transparent',
color: 'text-disabled',
+ _placeholder: {
+ color: 'text-disabled',
+ },
},
+ ],
+ StartIcon: [
+ {
+ marginRight: 'xsmall',
+ },
+ ({ disabled }: any) =>
+ disabled && {
+ '& *': {
+ color: 'text-disabled',
+ },
+ },
+ ],
+ EndIcon: [
+ {
+ marginLeft: 'small',
+ },
+ ({ disabled }: any) =>
+ disabled && {
+ '& *': {
+ color: 'text-disabled',
+ },
+ },
+ ],
+ },
+ Menu: {
+ Root: [
+ {
+ paddingTop: '4px',
+ paddingBottom: '4px',
+ backgroundColor: 'fill-two',
+ border: '1px solid border',
+ borderRadius: 'normal',
+ boxShadow: 'moderate',
+ elevation: 0, // reset from honorable-theme-default
},
- ],
- },
- Menu: {
- Root: [
- {
- paddingTop: '4px',
- paddingBottom: '4px',
- backgroundColor: 'fill-two',
- border: '1px solid border',
- borderRadius: 'normal',
- boxShadow: 'moderate',
- elevation: 0, // reset from honorable-theme-default
- },
- ],
- },
- MenuItem: {
- Root: [
- {
- '& > div': {
- borderTop: '1px solid border-fill-two',
- },
- '&:first-of-type > div': {
- borderTop: 'none',
- },
- },
- ],
- Children: [
- {
- padding: '8px 16px',
- },
- ({ active }: any) =>
- active && {
- backgroundColor: 'fill-two-hover',
- borderColor: 'fill-two-hover',
+ ],
+ },
+ MenuItem: {
+ Root: [
+ {
+ '& > div': {
+ borderTop: '1px solid border-fill-two',
+ },
+ '&:first-of-type > div': {
+ borderTop: 'none',
+ },
},
- ],
- },
- Modal: {
- Root: [
- {
- backgroundColor: 'fill-one',
- border: '1px solid border',
- boxShadow: 'modal',
- paddingTop: 'large',
- paddingRight: 'large',
- paddingBottom: 'large',
- paddingLeft: 'large',
- },
- ],
- Backdrop: [
- {
- backgroundColor: 'modal-backdrop',
- zIndex: zIndexes.modal,
- },
- ],
- },
- Select: {
- Root: [
- {
- border: '1px solid border-input',
- },
- ],
- },
- Spinner: {
- Root: [
- {
- '&:before': {
- borderTop: '2px solid white',
+ ],
+ Children: [
+ {
+ padding: '8px 16px',
},
- },
- ],
- },
- Switch: {
- Root: [
- ({ checked }: any) => ({
- padding: 8,
- paddingLeft: 0,
- color: checked ? 'text' : 'action-link-inactive',
- '> div:first-of-type': {
- backgroundColor: checked ? 'action-primary' : 'transparent',
+ ({ active }: any) =>
+ active && {
+ backgroundColor: 'fill-two-hover',
+ borderColor: 'fill-two-hover',
+ },
+ ],
+ },
+ Modal: {
+ Root: [
+ {
+ backgroundColor: 'fill-one',
+ border: '1px solid border',
+ boxShadow: 'modal',
+ paddingTop: 'large',
+ paddingRight: 'large',
+ paddingBottom: 'large',
+ paddingLeft: 'large',
+ },
+ ],
+ Backdrop: [
+ {
+ backgroundColor: 'modal-backdrop',
+ zIndex: zIndexes.modal,
+ },
+ ],
+ },
+ Select: {
+ Root: [
+ {
border: '1px solid border-input',
- '> span': {
- backgroundColor: checked
- ? 'action-link-active'
- : 'action-link-inactive',
+ },
+ ],
+ },
+ Spinner: {
+ Root: [
+ {
+ '&:before': {
+ borderTop: '2px solid white',
},
},
- ':hover': {
- color: 'text',
+ ],
+ },
+ Switch: {
+ Root: [
+ ({ checked }: any) => ({
+ padding: 8,
+ paddingLeft: 0,
+ color: checked ? 'text' : 'action-link-inactive',
'> div:first-of-type': {
- backgroundColor: checked
- ? 'action-primary-hover'
- : 'action-input-hover',
+ backgroundColor: checked ? 'action-primary' : 'transparent',
border: '1px solid border-input',
'> span': {
backgroundColor: checked
? 'action-link-active'
- : 'action-link-active',
+ : 'action-link-inactive',
+ },
+ },
+ ':hover': {
+ color: 'text',
+ '> div:first-of-type': {
+ backgroundColor: checked
+ ? 'action-primary-hover'
+ : 'action-input-hover',
+ border: '1px solid border-input',
+ '> span': {
+ backgroundColor: checked
+ ? 'action-link-active'
+ : 'action-link-active',
+ },
},
},
+ }),
+ ],
+ Control: [
+ {
+ width: 42,
+ height: 24,
+ borderRadius: 12,
+ '&:hover': {
+ boxShadow: 'none',
+ },
},
- }),
- ],
- Control: [
- {
- width: 42,
- height: 24,
- borderRadius: 12,
- '&:hover': {
- boxShadow: 'none',
+ ],
+ Handle: [
+ ({ checked }: any) => ({
+ width: 16,
+ height: 16,
+ borderRadius: '50%',
+ top: 3,
+ left: checked ? 'calc(100% - 16px - 3px)' : 3,
+ }),
+ ],
+ },
+ Tooltip: {
+ Root: [
+ {
+ caption: true,
+ backgroundColor: 'fill-two',
+ paddingVertical: 'xxsmall',
+ paddingHorizontal: 'xsmall',
+ borderRadius: 'medium',
+ boxShadow: 'moderate',
+ color: 'text-light',
},
- },
- ],
- Handle: [
- ({ checked }: any) => ({
- width: 16,
- height: 16,
- borderRadius: '50%',
- top: 3,
- left: checked ? 'calc(100% - 16px - 3px)' : 3,
- }),
- ],
- },
- Tooltip: {
- Root: [
- {
- caption: true,
- backgroundColor: 'fill-two',
- paddingVertical: 'xxsmall',
- paddingHorizontal: 'xsmall',
- borderRadius: 'medium',
- boxShadow: 'moderate',
- color: 'text-light',
- },
- ],
- Arrow: [
- {
- backgroundColor: 'fill-two',
- borderRadius: '1px',
- top: '50%',
- left: 0,
- transformOrigin: '50% 50%',
- transform:
- 'translate(calc(-50% + 1px), -50%) scaleY(0.77) rotate(45deg)',
- },
- ],
- },
- Ul: {
- Root: [
- {
- marginBlockStart: 0,
- marginBlockEnd: 0,
- paddingInlineStart: 24,
- },
- ],
- },
+ ],
+ Arrow: [
+ {
+ backgroundColor: 'fill-two',
+ borderRadius: '1px',
+ top: '50%',
+ left: 0,
+ transformOrigin: '50% 50%',
+ transform:
+ 'translate(calc(-50% + 1px), -50%) scaleY(0.77) rotate(45deg)',
+ },
+ ],
+ },
+ Ul: {
+ Root: [
+ {
+ marginBlockStart: 0,
+ marginBlockEnd: 0,
+ paddingInlineStart: 24,
+ },
+ ],
+ },
+ }
+}
+
+export const honorableThemeDark = mergeTheme(defaultTheme, {
+ ...getBaseTheme({ mode: 'dark' }),
+ colors: colorsDark,
+ ...getHonorableThemeProps({ mode: 'dark' }),
})
-export default honorableTheme
+export const honorableThemeLight = mergeTheme(defaultTheme, {
+ ...getBaseTheme({ mode: 'light' }),
+ colors: colorsLight,
+ ...getHonorableThemeProps({ mode: 'light' }),
+})
-export const styledTheme = {
- ...baseTheme,
- ...{
- spacing,
- boxShadows,
- borderRadiuses,
- fontFamilies,
- borders,
- borderStyles,
- borderWidths,
- zIndexes,
- portals,
- gradients,
- partials: {
- text: textPartials,
- marketingText: marketingTextPartials,
- focus: focusPartials,
- scrollBar,
- reset: resetPartials,
- dropdown: {
- arrowTransition: ({ isOpen = false }) => ({
- transition: 'transform 0.1s ease',
- transform: `scaleY(${isOpen ? -1 : 1})`,
- }),
- },
+const getStyledTheme = ({ mode }: { mode: ColorMode }) =>
+ ({
+ ...getBaseTheme({ mode }),
+ ...{
+ spacing,
+ boxShadows: getBoxShadows({ mode }),
+ borderRadiuses,
+ fontFamilies,
+ borders,
+ borderStyles,
+ borderWidths,
+ zIndexes,
+ portals,
+ gradients,
+ partials: {
+ text: textPartials,
+ marketingText: marketingTextPartials,
+ focus: getFocusPartials({ mode }),
+ scrollBar,
+ reset: resetPartials,
+ dropdown: {
+ arrowTransition: ({ isOpen = false }) => ({
+ transition: 'transform 0.1s ease',
+ transform: `scaleY(${isOpen ? -1 : 1})`,
+ }),
+ },
+ },
+ colors: mode === 'dark' ? colorsDark : colorsLight,
},
- },
+ } as const)
+
+export const styledThemeDark = getStyledTheme({ mode: 'dark' })
+
+export const styledThemeLight = {
+ ...getStyledTheme({ mode: 'light' }),
+ colors: colorsLight,
+} as const
+
+// Deprecate these later?
+export const styledTheme = styledThemeDark
+export default honorableThemeDark
+
+export const setThemeColorMode = (
+ mode: ColorMode,
+ {
+ dataAttrName = COLOR_THEME_KEY,
+ element = document?.documentElement,
+ }: {
+ dataAttrName?: string
+ element?: HTMLElement
+ } = {}
+) => {
+ if (!element) {
+ return
+ }
+ localStorage.setItem(dataAttrName, mode)
+ element.setAttribute(`data-${dataAttrName}`, mode)
+}
+
+export const useThemeColorMode = ({
+ dataAttrName = COLOR_THEME_KEY,
+ defaultMode = 'dark',
+ element = document?.documentElement,
+}: {
+ dataAttrName?: string
+ defaultMode?: ColorMode
+ element?: HTMLElement
+} = {}) => {
+ const attrName = `data-${dataAttrName}`
+ const [thisTheme, setThisTheme] = useState(
+ element?.getAttribute(attrName) || defaultMode
+ )
+
+ useMutationObserver(
+ element,
+ (mutations) => {
+ mutations.forEach((mutation) => {
+ if (
+ mutation?.attributeName === attrName &&
+ mutation.target instanceof HTMLElement
+ ) {
+ setThisTheme(mutation.target.getAttribute(attrName) || defaultMode)
+ }
+ })
+ },
+ { attributeFilter: [attrName] }
+ )
+
+ return thisTheme
}
diff --git a/src/theme/borders.ts b/src/theme/borders.ts
index 1df94e77..3b9f6f79 100644
--- a/src/theme/borders.ts
+++ b/src/theme/borders.ts
@@ -1,6 +1,6 @@
import { type CSSProperties } from 'react'
-import { semanticColors } from './colors'
+import { semanticColorCssVars } from './colors'
export const borderWidths = {
default: 1,
@@ -12,12 +12,12 @@ export const borderStyles = {
} as const satisfies Record
export const borders = {
- default: `${borderWidths.default}px ${borderStyles.default} ${semanticColors.border}`,
- 'fill-one': `${borderWidths.default}px ${borderStyles.default} ${semanticColors.border}`,
- 'fill-two': `${borderWidths.default}px ${borderStyles.default} ${semanticColors['border-fill-two']}`,
- 'fill-three': `${borderWidths.default}px ${borderStyles.default} ${semanticColors['border-input']}`,
- input: `${borderWidths.default}px ${borderStyles.default} ${semanticColors['border-input']}`,
- 'outline-focused': `${borderWidths.default}px ${borderStyles.default} ${semanticColors['border-outline-focused']}`,
+ default: `${borderWidths.default}px ${borderStyles.default} ${semanticColorCssVars.border}`,
+ 'fill-one': `${borderWidths.default}px ${borderStyles.default} ${semanticColorCssVars.border}`,
+ 'fill-two': `${borderWidths.default}px ${borderStyles.default} ${semanticColorCssVars['border-fill-two']}`,
+ 'fill-three': `${borderWidths.default}px ${borderStyles.default} ${semanticColorCssVars['border-input']}`,
+ input: `${borderWidths.default}px ${borderStyles.default} ${semanticColorCssVars['border-input']}`,
+ 'outline-focused': `${borderWidths.default}px ${borderStyles.default} ${semanticColorCssVars['border-outline-focused']}`,
} as const satisfies Record
export const borderRadiuses = {
diff --git a/src/theme/boxShadows.ts b/src/theme/boxShadows.ts
index e22ad1e3..4d10c2c4 100644
--- a/src/theme/boxShadows.ts
+++ b/src/theme/boxShadows.ts
@@ -3,15 +3,30 @@ import { type CSSProperties } from 'react'
import { borderWidths } from './borders'
-import { grey, semanticColors } from './colors'
+import { semanticColorCssVars } from './colors'
+import { semanticColorsDark as cDark } from './colors-semantic-dark'
+import { semanticColorsLight as cLight } from './colors-semantic-light'
-export const boxShadows = {
- slight: `0px 2px 4px ${chroma(grey[950]).alpha(0.14)}, 0px 2px 7px ${chroma(
- grey[950]
- ).alpha(0.18)}`,
- moderate: `0px 3px 6px ${chroma(grey[950]).alpha(
- 0.2
- )}, 0px 10px 20px ${chroma(grey[950]).alpha(0.3)}`,
- modal: `0px 20px 50px ${chroma(grey[950]).alpha(0.6)}`,
- focused: `0px 0px 0px ${borderWidths.focus}px ${semanticColors['border-outline-focused']}`,
-} as const satisfies Record
+export const getBoxShadows = ({ mode }: { mode: 'dark' | 'light' }) =>
+ ({
+ ...(mode === 'dark'
+ ? {
+ slight: `0px 2px 4px ${chroma(cDark['shadow-default']).alpha(
+ 0.14
+ )}, 0px 2px 7px ${chroma(cDark['shadow-default']).alpha(0.18)}`,
+ moderate: `0px 3px 6px ${chroma(cDark['shadow-default']).alpha(
+ 0.2
+ )}, 0px 10px 20px ${chroma(cDark['shadow-default']).alpha(0.3)}`,
+ modal: `0px 20px 50px ${chroma(cDark['shadow-default']).alpha(0.6)}`,
+ }
+ : {
+ slight: `0px 2px 4px ${chroma(cLight['shadow-default']).alpha(
+ 0.14
+ )}, 0px 2px 7px ${chroma(cLight['shadow-default']).alpha(0.18)}`,
+ moderate: `0px 3px 6px ${chroma(cLight['shadow-default']).alpha(
+ 0.2
+ )}, 0px 10px 20px ${chroma(cLight['shadow-default']).alpha(0.3)}`,
+ modal: `0px 20px 50px ${chroma(cLight['shadow-default']).alpha(0.6)}`,
+ }),
+ focused: `0px 0px 0px ${borderWidths.focus}px ${semanticColorCssVars['border-outline-focused']}`,
+ } as const satisfies Record)
diff --git a/src/theme/colors-base.ts b/src/theme/colors-base.ts
new file mode 100644
index 00000000..1f7c494d
--- /dev/null
+++ b/src/theme/colors-base.ts
@@ -0,0 +1,111 @@
+import chroma from 'chroma-js'
+import { type CSSProperties } from 'styled-components'
+
+export const grey = {
+ 950: '#0E1015',
+ 900: '#171A21',
+ 875: '#1B1F27',
+ 850: '#21242C',
+ 825: '#252932',
+ 800: '#2A2E37',
+ 775: '#303540',
+ 750: '#383D47',
+ 725: '#3D424D',
+ 700: '#454954',
+ 675: '#4C505C',
+ 600: '#5D626F',
+ 500: '#747B8B',
+ 400: '#A1A5B0',
+ 300: '#B2B7C3',
+ 200: '#C5C9D3',
+ 100: '#DFE2E7',
+ 50: '#EBEFF0',
+} as const satisfies Record
+
+export const purple = {
+ 950: '#020318',
+ 900: '#030530',
+ 850: '#050847',
+ 800: '#070A5F',
+ 700: '#0A0F8F',
+ 600: '#0D14BF',
+ 500: '#111AEE',
+ 400: '#4A51F2',
+ 350: '#5D63F4',
+ 300: '#747AF6',
+ 200: '#9FA3F9',
+ 100: '#CFD1FC',
+ 50: '#F1F1FE',
+} as const satisfies Record
+
+export const blue = {
+ 950: '#001019',
+ 900: '#002033',
+ 850: '#00304D',
+ 800: '#004166',
+ 700: '#006199',
+ 600: '#0081CC',
+ 500: '#06A0F9',
+ 400: '#33B4FF',
+ 350: '#4DBEFF',
+ 300: '#66C7FF',
+ 200: '#99DAFF',
+ 100: '#C2E9FF',
+ 50: '#F0F9FF',
+} as const satisfies Record
+
+export const green = {
+ 950: '#032117',
+ 900: '#053827',
+ 850: '#074F37',
+ 800: '#0A6B4A',
+ 700: '#0F996A',
+ 600: '#13C386',
+ 500: '#17E8A0',
+ 400: '#3CECAF',
+ 300: '#6AF1C2',
+ 200: '#99F5D5',
+ 100: '#C7FAE8',
+ 50: '#F1FEF9',
+} as const satisfies Record
+
+export const yellow = {
+ 950: '#242000',
+ 900: '#3D2F00',
+ 850: '#574500',
+ 800: '#756200',
+ 700: '#A88C00',
+ 600: '#D6BA00',
+ 500: '#FFE500',
+ 400: '#FFEB33',
+ 300: '#FFF170',
+ 200: '#FFF59E',
+ 100: '#FFF9C2',
+ 50: '#FFFEF0',
+} as const satisfies Record
+
+export const red = {
+ 950: '#130205',
+ 900: '#200308',
+ 850: '#38060E',
+ 800: '#660A19',
+ 700: '#8B0E23',
+ 600: '#BA1239',
+ 500: '#E81748',
+ 400: '#E95374',
+ 300: '#F2788D',
+ 200: '#F599A8',
+ 100: '#FAC7D0',
+ 50: '#FFF0F2',
+} as const satisfies Record
+
+export const baseColors = {
+ // Base palette,
+ blue,
+ grey,
+ green,
+ yellow,
+ red,
+ purple,
+ 'modal-backdrop': `${chroma(grey[900]).alpha(0.6)}`,
+} as const
diff --git a/src/theme/colors-cloudshell-dark.ts b/src/theme/colors-cloudshell-dark.ts
new file mode 100644
index 00000000..af330838
--- /dev/null
+++ b/src/theme/colors-cloudshell-dark.ts
@@ -0,0 +1,22 @@
+import { type CSSProperties } from 'styled-components'
+
+import { prefixKeys } from '../utils/ts-utils'
+
+export const colorsCloudShellDark = prefixKeys(
+ {
+ [`mid-grey`]: '#C5C9D3',
+ [`dark-grey`]: '#747B8B',
+ [`dark-red`]: '#F2788D',
+ [`light-red`]: '#F599A8',
+ [`green`]: '#3CECAF',
+ [`dark-yellow`]: '#3CECAF',
+ [`light-yellow`]: '#FFF9C2',
+ [`blue`]: '#8FD6FF',
+ [`dark-lilac`]: '#BE5EEB',
+ [`light-lilac`]: '#D596F4',
+ [`dark-purple`]: '#7075F5',
+ [`light-purple`]: '#969AF8',
+ [`light-grey`]: '#969AF8',
+ } as const satisfies Record,
+ 'cloud-shell-'
+)
diff --git a/src/theme/colors-cloudshell-light.ts b/src/theme/colors-cloudshell-light.ts
new file mode 100644
index 00000000..bb3b2381
--- /dev/null
+++ b/src/theme/colors-cloudshell-light.ts
@@ -0,0 +1 @@
+export { colorsCloudShellDark as colorsCloudShellLight } from './colors-cloudshell-dark'
diff --git a/src/theme/colors-codeblock-dark.ts b/src/theme/colors-codeblock-dark.ts
new file mode 100644
index 00000000..a0d263e8
--- /dev/null
+++ b/src/theme/colors-codeblock-dark.ts
@@ -0,0 +1,19 @@
+import { type CSSProperties } from 'styled-components'
+
+import { prefixKeys } from '../utils/ts-utils'
+
+export const colorsCodeBlockDark = prefixKeys(
+ {
+ [`light-green`]: '#99F5D5',
+ [`dark-grey`]: '#747B8B',
+ [`purple`]: '#969AF8',
+ [`mid-blue`]: '#8FD6FF',
+ [`yellow`]: '#FFF48F',
+ [`mid-grey`]: '#C5C9D3',
+ [`dark-green`]: '#3CECAF',
+ [`dark-purple`]: '#7075F5',
+ [`light-grey`]: '#EBEFF0',
+ [`light-blue`]: '#C2E9FF',
+ } as const satisfies Record,
+ 'code-block-'
+)
diff --git a/src/theme/colors-codeblock-light.ts b/src/theme/colors-codeblock-light.ts
new file mode 100644
index 00000000..efae346e
--- /dev/null
+++ b/src/theme/colors-codeblock-light.ts
@@ -0,0 +1 @@
+export { colorsCodeBlockDark as colorsCodeBlockLight } from './colors-codeblock-dark'
diff --git a/src/theme/colors-semantic-dark.ts b/src/theme/colors-semantic-dark.ts
new file mode 100644
index 00000000..81c5cb20
--- /dev/null
+++ b/src/theme/colors-semantic-dark.ts
@@ -0,0 +1,116 @@
+import chroma from 'chroma-js'
+import { type CSSProperties } from 'styled-components'
+
+import { blue, green, grey, purple, red, yellow } from './colors-base'
+import { colorsCloudShellDark } from './colors-cloudshell-dark'
+import { colorsCodeBlockDark } from './colors-codeblock-dark'
+
+export const semanticColorsDark = {
+ // Fill
+ //
+ // fill-zero
+ 'fill-zero': grey[900],
+ 'fill-zero-hover': grey[875],
+ 'fill-zero-selected': grey[825],
+ // fill-one
+ 'fill-one': grey[850],
+ 'fill-one-hover': grey[825],
+ 'fill-one-selected': grey[775],
+ // fill-two
+ 'fill-two': grey[800],
+ 'fill-two-hover': grey[775],
+ 'fill-two-selected': grey[725],
+ // fill-three
+ 'fill-three': grey[750],
+ 'fill-three-hover': grey[725],
+ 'fill-three-selected': grey[675],
+ 'fill-primary': purple[400],
+ 'fill-primary-hover': purple[350],
+
+ // Action
+ //
+ // primary
+ 'action-primary': purple[400],
+ 'action-primary-hover': purple[350],
+ 'action-primary-disabled': grey[825],
+ // link
+ 'action-link-inactive': grey[200],
+ 'action-link-active': grey[50],
+ // link-inline
+ 'action-link-inline': blue[200],
+ 'action-link-inline-hover': blue[100],
+ 'action-link-inline-visited': purple[300],
+ 'action-link-inline-visited-hover': purple[200],
+ 'action-input-hover': `${chroma('#E9ECF0').alpha(0.04)}`,
+
+ // Border
+ //
+ border: grey[800],
+ 'border-input': grey[700],
+ 'border-fill-two': grey[750],
+ 'border-fill-three': grey[700],
+ 'border-disabled': grey[700],
+ 'border-outline-focused': blue[300],
+ 'border-primary': purple[300],
+ 'border-secondary': blue[400],
+ 'border-success': green[300],
+ 'border-warning': yellow[200],
+ 'border-danger': red[300],
+ 'border-selected': grey[100],
+ 'border-info': blue[300],
+
+ // Text
+ //
+ text: grey[50],
+ 'text-light': grey[200],
+ 'text-xlight': grey[400],
+ 'text-long-form': grey[300],
+ 'text-disabled': grey[700],
+ 'text-input-disabled': grey[500],
+ 'text-primary-accent': blue[200],
+ 'text-primary-disabled': grey[500],
+ 'text-success': green[500],
+ 'text-success-light': green[200],
+ 'text-warning': yellow[400],
+ 'text-warning-light': yellow[100],
+ 'text-danger': red[400],
+ 'text-danger-light': red[200],
+
+ // Icon
+ //
+ 'icon-default': grey[100],
+ 'icon-light': grey[200],
+ 'icon-xlight': grey[400],
+ 'icon-disabled': grey[700],
+ 'icon-primary': purple[300],
+ 'icon-secondary': blue[400],
+ 'icon-info': blue[200],
+ 'icon-success': green[200],
+ 'icon-warning': yellow[100],
+ 'icon-danger': red[200],
+ 'icon-danger-critical': red[400],
+
+ // Marketing
+ //
+ 'marketing-white': '#FFFFFF',
+ 'marketing-black': '#000000',
+
+ // Shadows
+ //
+ 'shadow-default': grey[950],
+
+ // Code blocks
+ //
+ ...colorsCodeBlockDark,
+
+ // Cloud shell
+ //
+ ...colorsCloudShellDark,
+
+ // Deprecated (Remove after all 'error' colors converted to 'danger' in app)
+ //
+ 'border-error': red[300],
+ 'text-error': red[400],
+ 'text-error-light': red[200],
+ 'icon-error': red[200],
+} as const satisfies Record
diff --git a/src/theme/colors-semantic-light.ts b/src/theme/colors-semantic-light.ts
new file mode 100644
index 00000000..67689be7
--- /dev/null
+++ b/src/theme/colors-semantic-light.ts
@@ -0,0 +1,125 @@
+import chroma from 'chroma-js'
+import { type CSSProperties } from 'styled-components'
+
+import { blue, green, grey, purple, red, yellow } from './colors-base'
+import { colorsCloudShellLight } from './colors-cloudshell-light'
+import { colorsCodeBlockLight } from './colors-codeblock-light'
+import { type semanticColorsDark } from './colors-semantic-dark'
+
+export const semanticColorsLight = {
+ // Fill
+ //
+ // fill-zero
+ 'fill-zero': '#F9FAFB',
+ 'fill-zero-hover': grey[50],
+ 'fill-zero-selected': grey[100],
+ // fill-one
+ 'fill-one': '#FFFFFF',
+ 'fill-one-hover': grey[100],
+ 'fill-one-selected': grey[200],
+ // fill-two
+ 'fill-two': '#F0F4F5',
+ 'fill-two-hover': grey[200],
+ 'fill-two-selected': '#A9AFBC',
+ // fill-three
+ 'fill-three': grey[100],
+ 'fill-three-hover': grey[400],
+ 'fill-three-selected': grey[300],
+ // primary
+ 'fill-primary': purple[400],
+ 'fill-primary-hover': purple[350],
+
+ // Action
+ //
+ // primary
+ 'action-primary': purple[400],
+ 'action-primary-hover': purple[350],
+ 'action-primary-disabled': grey[100],
+ // link
+ 'action-link-inactive': '#A9AFBC',
+ 'action-link-active': blue[600],
+ 'action-link-inline': blue[700],
+ // link-inline
+
+ // Check with design team that this is correct
+ 'action-link-inline-hover': blue[600],
+
+ 'action-link-inline-visited': purple[300],
+ 'action-link-inline-visited-hover': purple[200],
+ // input
+ 'action-input-hover': `${chroma(grey[950]).alpha(0.04)}`, // text color w/ alpha
+
+ // Border
+ //
+ border: grey[100],
+ 'border-input': grey[300],
+ 'border-fill-two': grey[200],
+ 'border-fill-three': grey[400],
+ 'border-disabled': grey[200],
+ 'border-outline-focused': blue[400],
+ 'border-primary': purple[400],
+ 'border-secondary': blue[400],
+ 'border-success': green[500],
+ 'border-warning': '#FFF175',
+ 'border-danger': '#ED4578',
+ 'border-selected': grey[100],
+ 'border-info': blue[300],
+
+ // Text
+ //
+ text: grey[950],
+ 'text-light': '#5D626F',
+ 'text-xlight': '#A9AFBC',
+ 'text-long-form': grey[300],
+ 'text-disabled': grey[200],
+ 'text-input-disabled': grey[200],
+ 'text-primary-accent': '#38B6FF',
+ 'text-primary-disabled': grey[500],
+ 'text-success': green[700],
+ 'text-success-light': green[600],
+ 'text-warning': yellow[500],
+ 'text-warning-light': '#FFE500',
+ 'text-danger': '#ED4578',
+ 'text-danger-light': red[300],
+
+ // Icon
+ //
+ 'icon-default': grey[600],
+ 'icon-light': grey[500],
+ 'icon-xlight': grey[400],
+ 'icon-disabled': grey[100],
+ 'icon-primary': purple[300],
+ 'icon-secondary': blue[400],
+ 'icon-info': blue[200],
+ 'icon-success': green[700],
+ 'icon-warning': '#FF9900',
+ 'icon-danger': red[300],
+ 'icon-danger-critical': '#ED4578',
+
+ // Marketing
+ //
+ 'marketing-white': '#000000',
+ 'marketing-black': '#FFFFFF',
+
+ // Shadows
+ //
+ 'shadow-default': grey[950],
+
+ // Code-blocks
+ //
+ ...colorsCodeBlockLight,
+
+ // Cloud shell
+ //
+ ...colorsCloudShellLight,
+
+ // Deprecated (Remove after all 'error' colors converted to 'danger' in app)
+ //
+ 'border-error': red[300],
+ 'text-error': 'blue',
+ 'text-error-light': 'blue',
+ 'icon-error': 'blue',
+} as const satisfies Record<
+ keyof typeof semanticColorsDark,
+ CSSProperties['color']
+>
diff --git a/src/theme/colors.ts b/src/theme/colors.ts
index a92dbc44..095c8418 100644
--- a/src/theme/colors.ts
+++ b/src/theme/colors.ts
@@ -1,184 +1,25 @@
-import chroma from 'chroma-js'
-import { type CSSProperties } from 'react'
+import { type Entries } from 'type-fest'
-export const grey = {
- 950: '#0E1015',
- 900: '#171A21',
- 875: '#1B1F27',
- 850: '#21242C',
- 825: '#252932',
- 800: '#2A2E37',
- 775: '#303540',
- 750: '#383D47',
- 725: '#3D424D',
- 700: '#454954',
- 675: '#4C505C',
- 600: '#5D626F',
- 500: '#747B8B',
- 400: '#A1A5B0',
- 300: '#B2B7C3',
- 200: '#C5C9D3',
- 100: '#DFE2E7',
- 50: '#EBEFF0',
-} as const satisfies Record
+import { affixKeysToValues } from '../utils/ts-utils'
-export const purple = {
- 950: '#020318',
- 900: '#030530',
- 850: '#050847',
- 800: '#070A5F',
- 700: '#0A0F8F',
- 600: '#0D14BF',
- 500: '#111AEE',
- 400: '#4A51F2',
- 350: '#5D63F4',
- 300: '#747AF6',
- 200: '#9FA3F9',
- 100: '#CFD1FC',
- 50: '#F1F1FE',
-} as const satisfies Record
+import { semanticColorsDark } from './colors-semantic-dark'
-export const blue = {
- 950: '#001019',
- 900: '#002033',
- 850: '#00304D',
- 800: '#004166',
- 700: '#006199',
- 600: '#0081CC',
- 500: '#06A0F9',
- 400: '#33B4FF',
- 350: '#4DBEFF',
- 300: '#66C7FF',
- 200: '#99DAFF',
- 100: '#C2E9FF',
- 50: '#F0F9FF',
-} as const satisfies Record
+export const semanticColorKeys = (
+ Object.entries(semanticColorsDark) as Entries
+).map(([key]) => key)
-export const green = {
- 950: '#032117',
- 900: '#053827',
- 850: '#074F37',
- 800: '#0A6B4A',
- 700: '#0F996A',
- 600: '#13C386',
- 500: '#17E8A0',
- 400: '#3CECAF',
- 300: '#6AF1C2',
- 200: '#99F5D5',
- 100: '#C7FAE8',
- 50: '#F1FEF9',
-} as const satisfies Record
+export type SemanticColorKey = keyof typeof semanticColorsDark
-export const yellow = {
- 950: '#242000',
- 900: '#3D2F00',
- 850: '#574500',
- 800: '#756200',
- 700: '#A88C00',
- 600: '#D6BA00',
- 500: '#FFE500',
- 400: '#FFEB33',
- 300: '#FFF170',
- 200: '#FFF59E',
- 100: '#FFF9C2',
- 50: '#FFFEF0',
-} as const satisfies Record
+const cssVarPrefix = '--color-'
-export const red = {
- 950: '#130205',
- 900: '#200308',
- 850: '#38060E',
- 800: '#660A19',
- 700: '#8B0E23',
- 600: '#BA1239',
- 500: '#E81748',
- 400: '#E95374',
- 300: '#F2788D',
- 200: '#F599A8',
- 100: '#FAC7D0',
- 50: '#FFF0F2',
-} as const satisfies Record
+export function colorKeyToCssVar(key: T): string {
+ return `${cssVarPrefix}${key}` as `${typeof cssVarPrefix}${T}`
+}
-const baseColors = {
- // Fill
- 'fill-zero': grey[900],
- 'fill-zero-hover': grey[875],
- 'fill-zero-selected': grey[825],
- 'fill-one': grey[850],
- 'fill-one-hover': grey[825],
- 'fill-one-selected': grey[775],
- 'fill-two': grey[800],
- 'fill-two-hover': grey[775],
- 'fill-two-selected': grey[725],
- 'fill-three': grey[750],
- 'fill-three-hover': grey[725],
- 'fill-three-selected': grey[675],
- 'fill-primary': purple[400],
- 'fill-primary-hover': purple[350],
- // Action
- 'action-primary': purple[400],
- 'action-primary-hover': purple[350],
- 'action-primary-disabled': grey[825],
- 'action-link-inactive': grey[200],
- 'action-link-active': grey[50],
- 'action-link-inline': blue[200],
- 'action-link-inline-hover': blue[100],
- 'action-link-inline-visited': purple[300],
- 'action-link-inline-visited-hover': purple[200],
- 'action-input-hover': `${chroma('#E9ECF0').alpha(0.04)}`,
- // Border
- border: grey[800],
- 'border-input': grey[700],
- 'border-fill-two': grey[750],
- 'border-fill-three': grey[700],
- 'border-disabled': grey[700],
- 'border-outline-focused': blue[300],
- 'border-primary': purple[300],
- 'border-secondary': blue[400],
- 'border-success': green[300],
- 'border-warning': yellow[200],
- 'border-danger': red[300],
- 'border-selected': grey[100],
- 'border-info': blue[300],
- // Content
- text: grey[50],
- 'text-light': grey[200],
- 'text-xlight': grey[400],
- 'text-long-form': grey[300],
- 'text-disabled': grey[700], // deprecated
- 'text-input-disabled': grey[500],
- 'text-primary-accent': blue[200], // deprecated maybe?
- 'text-primary-disabled': grey[500],
- 'text-success': green[500],
- 'text-success-light': green[200],
- 'text-warning': yellow[400],
- 'text-warning-light': yellow[100],
- 'text-danger': red[400],
- 'text-danger-light': red[200],
- // Icon
- 'icon-default': grey[100],
- 'icon-light': grey[200],
- 'icon-xlight': grey[400],
- 'icon-disabled': grey[700],
- 'icon-primary': purple[300],
- 'icon-secondary': blue[400],
- 'icon-info': blue[200],
- 'icon-success': green[200],
- 'icon-warning': yellow[100],
- 'icon-danger': red[200],
- 'icon-danger-critical': red[400],
- // Deprecated (Remove after all 'error' colors converted to 'danger' in app)
- 'border-error': red[300],
- 'text-error': red[400],
- 'text-error-light': red[200],
- 'icon-error': red[200],
- // Marketing
- 'marketing-white': '#FFFFFF',
- 'marketing-black': '#000000',
-} as const satisfies Record
+export const semanticColorCssVars = affixKeysToValues(semanticColorsDark, {
+ prefix: `var(${cssVarPrefix}`,
+ suffix: `)`,
+})
-export const semanticColors = {
- ...baseColors,
- // Modals
- 'modal-backdrop': `${chroma(baseColors['fill-zero']).alpha(0.6)}`,
-} as const satisfies Record
+export type SemanticColorCssVar =
+ (typeof semanticColorCssVars)[keyof typeof semanticColorCssVars]
diff --git a/src/theme/editor.ts b/src/theme/editor.ts
index 6a19a59c..e2a188ba 100644
--- a/src/theme/editor.ts
+++ b/src/theme/editor.ts
@@ -1,4 +1,4 @@
-import { semanticColors } from './colors'
+import { semanticColorsDark as semanticColors } from './colors-semantic-dark'
export const editorTheme = {
inherit: true,
diff --git a/src/theme/focus.ts b/src/theme/focus.ts
index 7d4f78ed..3dbca41f 100644
--- a/src/theme/focus.ts
+++ b/src/theme/focus.ts
@@ -1,31 +1,37 @@
import { type CSSObject } from '../types'
-import { boxShadows } from './boxShadows'
+import { type ColorMode } from '../theme'
+
+import { getBoxShadows } from './boxShadows'
import { borderWidths } from './borders'
-import { semanticColors } from './colors'
+import { semanticColorCssVars } from './colors'
+
+export const getFocusPartials = ({ mode }: { mode: ColorMode }) => {
+ const boxShadows = getBoxShadows({ mode })
-export const focusPartials = {
- default: {
- outline: 'none',
- boxShadow: boxShadows.focused,
- },
- outline: {
- outline: `${borderWidths.focus}px solid ${semanticColors['border-outline-focused']}`,
- },
- button: {
- outline: `1px solid ${semanticColors['border-outline-focused']}`,
- outlineOffset: '-1px',
- },
- insetAbsolute: {
- outline: 'none',
- position: 'absolute',
- content: "''",
- pointerEvents: 'none',
- top: `${borderWidths.focus}px`,
- right: `${borderWidths.focus}px`,
- left: `${borderWidths.focus}px`,
- bottom: `${borderWidths.focus}px`,
- boxShadow: boxShadows.focused,
- },
-} as const satisfies Record
+ return {
+ default: {
+ outline: 'none',
+ boxShadow: boxShadows.focused,
+ },
+ outline: {
+ outline: `${borderWidths.focus}px solid ${semanticColorCssVars['border-outline-focused']}`,
+ },
+ button: {
+ outline: `1px solid ${semanticColorCssVars['border-outline-focused']}`,
+ outlineOffset: '-1px',
+ },
+ insetAbsolute: {
+ outline: 'none',
+ position: 'absolute',
+ content: "''",
+ pointerEvents: 'none',
+ top: `${borderWidths.focus}px`,
+ right: `${borderWidths.focus}px`,
+ left: `${borderWidths.focus}px`,
+ bottom: `${borderWidths.focus}px`,
+ boxShadow: boxShadows.focused,
+ },
+ } as const satisfies Record
+}
diff --git a/src/theme/marketingText.ts b/src/theme/marketingText.ts
index 985eb57e..b5189ff9 100644
--- a/src/theme/marketingText.ts
+++ b/src/theme/marketingText.ts
@@ -1,7 +1,7 @@
import { type CSSObject } from '../types'
import { fontFamilies } from './fonts'
-import { semanticColors } from './colors'
+import { semanticColorCssVars } from './colors'
const body1 = {
fontFamily: fontFamilies.sans,
@@ -9,7 +9,7 @@ const body1 = {
lineHeight: '140%',
fontWeight: 300,
letterSpacing: '0.25px',
- color: semanticColors['text-xlight'],
+ color: semanticColorCssVars['text-xlight'],
} as const satisfies CSSObject
const body2 = {
@@ -18,12 +18,12 @@ const body2 = {
lineHeight: '160%',
fontWeight: 300,
letterSpacing: '0.5px',
- color: semanticColors['text-xlight'],
+ color: semanticColorCssVars['text-xlight'],
} as const satisfies CSSObject
const bodyBold = {
fontWeight: 700,
- color: semanticColors['text-light'],
+ color: semanticColorCssVars['text-light'],
} as const satisfies CSSObject
const marketingTextPartials = {
@@ -33,7 +33,7 @@ const marketingTextPartials = {
lineHeight: '120%',
fontWeight: 500,
letterSpacing: 0,
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
hero1: {
fontFamily: fontFamilies.sansHero,
@@ -41,7 +41,7 @@ const marketingTextPartials = {
lineHeight: '120%',
fontWeight: 700,
letterSpacing: 0,
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
hero2: {
fontFamily: fontFamilies.sansHero,
@@ -49,7 +49,7 @@ const marketingTextPartials = {
lineHeight: '125%',
fontWeight: 500,
letterSpacing: 0,
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
title1: {
fontFamily: fontFamilies.sansHero,
@@ -57,7 +57,7 @@ const marketingTextPartials = {
lineHeight: '140%',
fontWeight: 500,
letterSpacing: '0.25px',
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
title2: {
fontFamily: fontFamilies.sansHero,
@@ -65,7 +65,7 @@ const marketingTextPartials = {
lineHeight: '140%',
fontWeight: 500,
letterSpacing: '0.25px',
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
subtitle1: {
fontFamily: fontFamilies.sans,
@@ -73,7 +73,7 @@ const marketingTextPartials = {
lineHeight: '140%',
fontWeight: 600,
letterSpacing: '0.25px',
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
subtitle2: {
fontFamily: fontFamilies.sans,
@@ -81,7 +81,7 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: 600,
letterSpacing: '0.25px',
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
},
body1,
body2,
@@ -95,13 +95,13 @@ const marketingTextPartials = {
...bodyBold,
},
inlineLink: {
- color: semanticColors['action-link-inline'],
+ color: semanticColorCssVars['action-link-inline'],
textDecoration: 'underline',
'&:hover': {
- color: semanticColors['action-link-inline-hover'],
+ color: semanticColorCssVars['action-link-inline-hover'],
},
'&:visited, &:active': {
- color: semanticColors['action-link-inline-visited'],
+ color: semanticColorCssVars['action-link-inline-visited'],
},
},
navLink: {
@@ -110,9 +110,9 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: '300',
letterSpacing: '0.5px',
- color: semanticColors['text-light'],
+ color: semanticColorCssVars['text-light'],
'&:hover': {
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
textDecoration: 'underline',
},
},
@@ -122,7 +122,7 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: 500,
letterSpacing: '0.5px',
- color: semanticColors.text,
+ color: semanticColorCssVars.text,
cursor: 'pointer',
'&:hover': {
textDecoration: 'underline',
@@ -134,7 +134,7 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: 300,
letterSpacing: '0.5px',
- color: semanticColors['text-xlight'],
+ color: semanticColorCssVars['text-xlight'],
},
componentLink: {
fontFamily: fontFamilies.sans,
@@ -142,7 +142,7 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: 600,
letterSpacing: '0.25px',
- color: semanticColors['text-light'],
+ color: semanticColorCssVars['text-light'],
cursor: 'pointer',
'&:hover': {
textDecoration: 'underline',
@@ -154,7 +154,7 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: 400,
letterSpacing: '0.25px',
- color: semanticColors['text-light'],
+ color: semanticColorCssVars['text-light'],
cursor: 'pointer',
'&:hover': {
textDecoration: 'underline',
@@ -166,7 +166,7 @@ const marketingTextPartials = {
lineHeight: '150%',
fontWeight: 300,
letterSpacing: '1px',
- color: semanticColors['text-xlight'],
+ color: semanticColorCssVars['text-xlight'],
textTransform: 'uppercase',
},
} as const satisfies Record
diff --git a/src/theme/scrollBar.ts b/src/theme/scrollBar.ts
index c7eb553d..92e4da3e 100644
--- a/src/theme/scrollBar.ts
+++ b/src/theme/scrollBar.ts
@@ -4,15 +4,17 @@ import { type StringObj } from '../theme'
import { type FillLevel } from '../components/contexts/FillLevelContext'
-import { semanticColors } from './colors'
+import { semanticColorCssVars } from './colors'
export const scrollBar = ({ fillLevel }: { fillLevel: FillLevel }) => {
const trackColor =
- fillLevel >= 2 ? semanticColors['fill-three'] : semanticColors['fill-two']
+ fillLevel >= 2
+ ? semanticColorCssVars['fill-three']
+ : semanticColorCssVars['fill-two']
const barColor =
fillLevel >= 2
- ? semanticColors['text-xlight']
- : semanticColors['fill-three']
+ ? semanticColorCssVars['text-xlight']
+ : semanticColorCssVars['fill-three']
const barWidth = 6
const barRadius = barWidth / 2
diff --git a/src/theme/spacing.ts b/src/theme/spacing.ts
index 7559523a..c7e9d230 100644
--- a/src/theme/spacing.ts
+++ b/src/theme/spacing.ts
@@ -1,25 +1,30 @@
+import { type PrefixKeys } from '../utils/ts-utils'
+
+export const baseSpacing = {
+ xxxsmall: 2, // 1/8 * 16
+ xxsmall: 4, // 1/4 * 16
+ xsmall: 8, // 1/2 * 16
+ small: 12, // 3/4 * 16
+ medium: 16, // 1 * 16
+ large: 24, // 1.5 * 16
+ xlarge: 32, // 2 * 16
+ xxlarge: 48, // 3 * 16
+ xxxlarge: 64, // 4 * 16
+ xxxxlarge: 96, // 6 * 16
+ xxxxxlarge: 128, // 8 * 16
+ xxxxxxlarge: 192, // 12 * 16
+} as const satisfies Record
+
+const negativePrefix = 'minus-' as const
+const negativeSpacing = Object.fromEntries(
+ Object.entries(baseSpacing).map((key, val) => [
+ `${negativePrefix}${key}`,
+ -val,
+ ])
+) as PrefixKeys
+
export const spacing = {
- 'minus-xxxxxlarge': -128,
- 'minus-xxxxlarge': -96,
- 'minus-xxxlarge': -64,
- 'minus-xxlarge': -48,
- 'minus-xlarge': -32,
- 'minus-large': -24,
- 'minus-medium': -16,
- 'minus-small': -12,
- 'minus-xsmall': -8,
- 'minus-xxsmall': -4,
- 'minus-xxxsmall': -2,
none: 0,
- xxxsmall: 2,
- xxsmall: 4,
- xsmall: 8,
- small: 12,
- medium: 16,
- large: 24,
- xlarge: 32,
- xxlarge: 48,
- xxxlarge: 64,
- xxxxlarge: 96,
- xxxxxlarge: 128,
+ ...baseSpacing,
+ ...negativeSpacing,
} as const satisfies Record
diff --git a/src/theme/text.ts b/src/theme/text.ts
index 87df2ba9..93692fcb 100644
--- a/src/theme/text.ts
+++ b/src/theme/text.ts
@@ -1,7 +1,7 @@
import { type CSSObject } from '../types'
import { fontFamilies } from './fonts'
-import { semanticColors } from './colors'
+import { semanticColorCssVars } from './colors'
export const INLINE_CODE_EMS = 0.8
export const INLINE_CODE_MIN_PX = 12
@@ -156,15 +156,15 @@ const textPartials = {
textOverflow: 'ellipsis',
},
inlineLink: {
- color: semanticColors['action-link-inline'],
+ color: semanticColorCssVars['action-link-inline'],
textDecoration: 'underline',
'&:hover': {
- color: semanticColors['action-link-inline-hover'],
+ color: semanticColorCssVars['action-link-inline-hover'],
},
'&:visited, &:active': {
- color: semanticColors['action-link-inline-visited'],
+ color: semanticColorCssVars['action-link-inline-visited'],
'&:hover': {
- color: semanticColors['action-link-inline-visited-hover'],
+ color: semanticColorCssVars['action-link-inline-visited-hover'],
},
},
},
diff --git a/src/types/fromEntries.d.ts b/src/types/fromEntries.d.ts
new file mode 100644
index 00000000..80c9cbe5
--- /dev/null
+++ b/src/types/fromEntries.d.ts
@@ -0,0 +1,17 @@
+export type ArrayElement = A extends readonly (infer T)[] ? T : never
+
+type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable }
+
+type Cast = X extends Y ? X : Y
+
+type FromEntries = T extends [infer Key, any][]
+ ? { [K in Cast]: Extract, [K, any]>[1] } // <- Change here `string` -> `PropertyKey`.
+ : { [key in string]: any }
+
+export type FromEntriesWithReadOnly = FromEntries>
+
+declare global {
+ interface ObjectConstructor {
+ fromEntries(obj: T): FromEntriesWithReadOnly
+ }
+}
diff --git a/src/utils/ts-utils.ts b/src/utils/ts-utils.ts
new file mode 100644
index 00000000..0492bd80
--- /dev/null
+++ b/src/utils/ts-utils.ts
@@ -0,0 +1,81 @@
+export type PrefixString<
+ T extends string,
+ Prefix extends string
+> = `${Prefix}${T}`
+
+export type PrefixKeys = {
+ [key in keyof T as PrefixString]: Val extends void
+ ? T[key]
+ : Val
+}
+
+export type SuffixString<
+ T extends string,
+ Suffix extends string
+> = `${T}${Suffix}`
+
+export type SuffixKeys = {
+ [key in keyof T as SuffixString]: Val extends void
+ ? T[key]
+ : Val
+}
+
+export function prefixKeys(
+ obj: T,
+ prefix: Prefix
+) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, val]) => [`${prefix}${key}`, val])
+ ) as PrefixKeys
+}
+
+export function suffixKeys(
+ obj: T,
+ suffix: Suffix
+) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, val]) => [`${key}${suffix}`, val])
+ ) as SuffixKeys
+}
+
+export type AffixValues<
+ T extends Record,
+ Prefix extends string = '',
+ Suffix extends string = ''
+> = {
+ [key in keyof T]: `${Prefix}${T[key]}${Suffix}`
+}
+
+export function affixValues<
+ T extends Record,
+ Prefix extends string = '',
+ Suffix extends string = ''
+>(obj: T, { prefix, suffix }: { prefix?: Prefix; suffix?: Suffix }) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, val]) => [
+ key,
+ `${prefix || ''}${val}${suffix || ''}`,
+ ])
+ ) as AffixValues
+}
+
+export type AffixKeyToValue<
+ T extends Record,
+ Prefix extends string = '',
+ Suffix extends string = ''
+> = {
+ [key in keyof T]: `${Prefix}${key & string}${Suffix}`
+}
+
+export function affixKeysToValues<
+ T extends Record,
+ Prefix extends string = '',
+ Suffix extends string = ''
+>(obj: T, { prefix, suffix }: { prefix?: Prefix; suffix?: Suffix }) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key]) => [
+ key,
+ `${prefix || ''}${key}${suffix || ''}`,
+ ])
+ ) as AffixKeyToValue
+}
diff --git a/yarn.lock b/yarn.lock
index 693d6d08..4f49857f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4240,6 +4240,7 @@ __metadata:
"@monaco-editor/react": 4.5.1
"@pluralsh/eslint-config-typescript": 2.5.41
"@react-aria/utils": 3.17.0
+ "@react-hooks-library/core": 0.5.1
"@react-stately/utils": 3.6.0
"@react-types/shared": 3.18.1
"@storybook/addon-actions": 7.0.18
@@ -4308,6 +4309,7 @@ __metadata:
storybook: 7.0.18
styled-components: 5.3.11
styled-container-query: 1.3.5
+ type-fest: 3.11.1
typescript: 4.9.5
use-immer: 0.9.0
usehooks-ts: 2.9.1
@@ -5168,6 +5170,24 @@ __metadata:
languageName: node
linkType: hard
+"@react-hooks-library/core@npm:0.5.1":
+ version: 0.5.1
+ resolution: "@react-hooks-library/core@npm:0.5.1"
+ dependencies:
+ "@react-hooks-library/shared": 0.5.1
+ peerDependencies:
+ react: ">=16.9.0"
+ checksum: d334e5705cf2a12ce6344cb31489c40ae891529f121c558730908aec86a43658b03b8ec4c94ace408ad88f640b0ca760365cbca2966369429f349c9102011a4c
+ languageName: node
+ linkType: hard
+
+"@react-hooks-library/shared@npm:0.5.1":
+ version: 0.5.1
+ resolution: "@react-hooks-library/shared@npm:0.5.1"
+ checksum: 8c1271d440c7e1cff32bda700e230223c6f7e1c9cbc0784fa641ab0b29fafccb2c539f37705a24d55b3db758da491d55b23dd63d27f4c7469eff99553a3812e7
+ languageName: node
+ linkType: hard
+
"@react-spring/animated@npm:~9.7.2":
version: 9.7.2
resolution: "@react-spring/animated@npm:9.7.2"
@@ -19133,6 +19153,13 @@ __metadata:
languageName: node
linkType: hard
+"type-fest@npm:3.11.1":
+ version: 3.11.1
+ resolution: "type-fest@npm:3.11.1"
+ checksum: 33be49e3b671c2ff3b5e320ef8c160c488205b08ab7631369116909a1baf2aebfcf45234c045e6902b8aa35730ac2bfd0655ea9e0fe3f8d26af9d99a16b07abd
+ languageName: node
+ linkType: hard
+
"type-fest@npm:^0.16.0":
version: 0.16.0
resolution: "type-fest@npm:0.16.0"