Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions assets/design-system/src/components/contexts/ScalePresetContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from 'react'
import styled from 'styled-components'

import {
DEFAULT_SCALE_PRESET_ID,
SCALE_PRESET_IDS,
isScalePresetId,
scalePresets,
type ScalePresetId,
} from '../../theme/scale-presets'

const STORAGE_KEY = 'plural-prototype-scale'

type ScalePresetContextValue = {
scaleId: ScalePresetId
setScaleId: (id: ScalePresetId) => void
switcherEnabled: boolean
}

const ScalePresetContext = createContext<ScalePresetContextValue>({
scaleId: DEFAULT_SCALE_PRESET_ID,
setScaleId: () => {},
switcherEnabled: false,
})

function readStoredScaleId(): ScalePresetId {
if (typeof window === 'undefined') return DEFAULT_SCALE_PRESET_ID
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (isScalePresetId(stored)) return stored
} catch {
/* ignore */
}
return DEFAULT_SCALE_PRESET_ID
}

function persistScaleId(id: ScalePresetId) {
try {
localStorage.setItem(STORAGE_KEY, id)
} catch {
/* ignore */
}
}

export function ScalePresetProvider({
children,
switcherEnabled = false,
}: {
children: ReactNode
switcherEnabled?: boolean
}) {
const [scaleId, setScaleIdState] = useState<ScalePresetId>(readStoredScaleId)

const setScaleId = useCallback((id: ScalePresetId) => {
setScaleIdState(id)
persistScaleId(id)
}, [])

const effectiveScaleId = switcherEnabled ? scaleId : DEFAULT_SCALE_PRESET_ID

const value = useMemo(
() => ({
scaleId: effectiveScaleId,
setScaleId,
switcherEnabled,
}),
[effectiveScaleId, setScaleId, switcherEnabled]
)

return (
<ScalePresetContext.Provider value={value}>
{children}
</ScalePresetContext.Provider>
)
}

export function useScalePreset() {
return useContext(ScalePresetContext)
}

const Panel = styled.div(({ theme }) => ({
position: 'fixed',
bottom: theme.spacing.medium,
right: theme.spacing.medium,
zIndex: 3000,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing.xxsmall,
padding: theme.spacing.small,
background: theme.colors['fill-two'],
border: theme.borders.default,
borderRadius: theme.borderRadiuses.large,
boxShadow: theme.boxShadows.moderate,
fontSize: theme.partials.text.caption.fontSize,
color: theme.colors.text,
minWidth: 140,
}))

const Title = styled.div(({ theme }) => ({
fontWeight: 600,
letterSpacing: '0.5px',
color: theme.colors['text-xlight'],
marginBottom: theme.spacing.xxxsmall,
}))

const ScaleButton = styled.button<{ $active?: boolean }>(
({ theme, $active }) => ({
textAlign: 'left',
padding: `${theme.spacing.xxsmall}px ${theme.spacing.xsmall}px`,
borderRadius: theme.borderRadiuses.medium,
background: $active ? theme.colors['fill-three'] : 'transparent',
color: $active ? theme.colors.text : theme.colors['text-light'],
border: `1px solid ${$active ? theme.colors['border-fill-three'] : 'transparent'}`,
cursor: 'pointer',
font: 'inherit',
transition: 'background 0.15s ease, color 0.15s ease',
'&:hover': {
background: theme.colors['fill-three-hover'],
color: theme.colors.text,
},
})
)

export function DevScaleLabToggle() {
const { scaleId, setScaleId, switcherEnabled } = useScalePreset()

if (!switcherEnabled) return null

return (
<Panel
role="group"
aria-label="Spacing scale"
>
<Title>Scale</Title>
{SCALE_PRESET_IDS.map((id) => (
<ScaleButton
key={id}
type="button"
$active={scaleId === id}
aria-pressed={scaleId === id}
onClick={() => setScaleId(id)}
>
{scalePresets[id].label}
</ScaleButton>
))}
</Panel>
)
}
10 changes: 8 additions & 2 deletions assets/design-system/src/components/icons/createIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import {
useTheme,
} from 'honorable'
import { type ReactNode } from 'react'
import { useTheme as useStyledTheme } from 'styled-components'

import { DEFAULT_ICON_SIZE } from '../../theme/iconSizes'

type IconBaseProps = {
size?: number | string
Expand All @@ -18,14 +21,17 @@ export type IconProps = HonorableIconProps & IconBaseProps
function createIcon(render: (props: IconBaseProps) => ReactNode) {
function Icon({
ref,
size = 16,
size,
color = 'currentColor',
fullColor,
secondaryColor,
...props
}: IconProps) {
const theme = useTheme()
const styledTheme = useStyledTheme()
const workingColor = theme.utils?.resolveColorString(color)
const resolvedSize =
size ?? styledTheme.iconSizes?.medium ?? DEFAULT_ICON_SIZE

return (
<HonorableIcon
Expand All @@ -35,7 +41,7 @@ function createIcon(render: (props: IconBaseProps) => ReactNode) {
{...props}
>
{render({
size,
size: resolvedSize,
color: workingColor,
secondaryColor,
fullColor,
Expand Down
36 changes: 36 additions & 0 deletions assets/design-system/src/hooks/useScaledThemes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useMemo } from 'react'

import {
getHonorableTheme,
getStyledTheme,
styledThemeLight,
useThemeColorMode,
type ColorMode,
type ScalePresetId,
} from '../theme'

export function useScaledThemes(scaleId: ScalePresetId) {
const colorMode = useThemeColorMode()

const styledTheme = useMemo(() => {
const mode = (colorMode === 'light' ? 'light' : 'dark') as ColorMode
if (mode === 'light') {
return {
...getStyledTheme({ mode: 'light', scaleId }),
colors: styledThemeLight.colors,
}
}
return getStyledTheme({ mode: 'dark', scaleId })
}, [colorMode, scaleId])

const honorableTheme = useMemo(
() =>
getHonorableTheme({
mode: (colorMode === 'light' ? 'light' : 'dark') as ColorMode,
scaleId,
}),
[colorMode, scaleId]
)

return { styledTheme, honorableTheme }
}
13 changes: 13 additions & 0 deletions assets/design-system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,15 +179,28 @@ export * from './components/TreeNavigation'
// Theme
export { default as GlobalStyle } from './GlobalStyle'
export {
DEFAULT_SCALE_PRESET_ID,
SCALE_PRESET_IDS,
getHonorableTheme,
getStyledTheme,
honorableThemeDark,
honorableThemeLight,
isScalePresetId,
scalePresets,
setThemeColorMode,
styledTheme,
styledThemeDark,
styledThemeLight,
honorableThemeDark as theme,
useThemeColorMode,
} from './theme'
export type { ScalePresetId } from './theme'
export {
DevScaleLabToggle,
ScalePresetProvider,
useScalePreset,
} from './components/contexts/ScalePresetContext'
export { useScaledThemes } from './hooks/useScaledThemes'
export type { SemanticBorderKey } from './theme/borders'
export { semanticColorCssVars, semanticColorKeys } from './theme/colors'
export type { SemanticColorCssVar, SemanticColorKey } from './theme/colors'
Expand Down
Loading
Loading