Skip to content

Commit

Permalink
feat(theme): add theme hook and make storybook use internal theming
Browse files Browse the repository at this point in the history
  • Loading branch information
vassbence committed Jul 23, 2024
1 parent 42ba13c commit dfefb6b
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 70 deletions.
36 changes: 22 additions & 14 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import type { Preview } from '@storybook/react'
import '../src/assets/global.css'
import * as React from 'react'
import { KeyboardEventProvider } from '../src/hooks/useHadKeyboardEvent'
import { Provider } from '../src/components/Provider/index.js'

const preview: Preview = {
tags: ['autodocs'],
parameters: {
backgrounds: {
default: 'system',
values: [
{
name: 'system',
value: 'var(--neutral-inverted-100)',
},
],
backgrounds: { disable: true },
viewport: { disable: true },
},
globalTypes: {
theme: {
defaultValue: 'dark',
toolbar: {
title: 'Theme',
items: [
{ value: 'light', title: 'Light', icon: 'sun' },
{ value: 'dark', title: 'Dark', icon: 'moon' },
],
dynamicTitle: true,
},
},
},
decorators: (Story) => (
<KeyboardEventProvider>
<Story />
</KeyboardEventProvider>
),
decorators: (Story, context) => {
return (
<Provider forcedTheme={context.globals.theme}>
<Story />
</Provider>
)
},
}

export default preview
106 changes: 52 additions & 54 deletions src/assets/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,60 +60,58 @@
--black-5: #09090b0d;
}

@media (prefers-color-scheme: dark) {
:root {
--neutral-100: #f4f4f5;
--neutral-80: #f4f4f5cc;
--neutral-60: #f4f4f566;
--neutral-20: #f4f4f533;
--neutral-20-adjusted: #f4f4f533;
--neutral-10: #f4f4f51a;
--neutral-10-adjusted: #f4f4f51a;
--neutral-10-background: #f4f4f51a;
--neutral-5: #f4f4f50d;
--neutral-inverted-100: #09090b;
--neutral-inverted-80: #09090bcc;
--neutral-inverted-60: #09090b99;
--neutral-inverted-20: #09090b33;
--neutral-inverted-10: #09090b1a;
--neutral-inverted-5: #09090b0d;
--red-100: #ef4444;
--red-80: #ef4444cc;
--red-60: #ef444499;
--red-20: #ef444433;
--red-10: #ef44441a;
--red-5: #ef44440d;
--amber-100: #f59e0b;
--amber-80: #f59e0bcc;
--amber-60: #f59e0b99;
--amber-20: #f59e0b33;
--amber-10: #f59e0b1a;
--amber-5: #f59e0b0d;
--indigo-100: #7c7ef4;
--indigo-80: #7c7ef4cc;
--indigo-60: #7c7ef499;
--indigo-20: #7c7ef433;
--indigo-10: #7c7ef41a;
--indigo-5: #7c7ef40d;
--green-100: #10b981;
--green-80: #10b981cc;
--green-60: #10b98199;
--green-20: #10b98133;
--green-10: #10b9811a;
--green-5: #10b9810d;
--white-100: #f4f4f5;
--white-80: #f4f4f5cc;
--white-60: #f4f4f599;
--white-20: #f4f4f533;
--white-10: #f4f4f51a;
--white-5: #f4f4f50d;
--black-100: #09090b;
--black-80: #09090bcc;
--black-60: #09090b99;
--black-20: #09090b33;
--black-10: #09090b1a;
--black-5: #09090b0d;
}
[data-theme='dark'] {
--neutral-100: #f4f4f5;
--neutral-80: #f4f4f5cc;
--neutral-60: #f4f4f566;
--neutral-20: #f4f4f533;
--neutral-20-adjusted: #f4f4f533;
--neutral-10: #f4f4f51a;
--neutral-10-adjusted: #f4f4f51a;
--neutral-10-background: #f4f4f51a;
--neutral-5: #f4f4f50d;
--neutral-inverted-100: #09090b;
--neutral-inverted-80: #09090bcc;
--neutral-inverted-60: #09090b99;
--neutral-inverted-20: #09090b33;
--neutral-inverted-10: #09090b1a;
--neutral-inverted-5: #09090b0d;
--red-100: #ef4444;
--red-80: #ef4444cc;
--red-60: #ef444499;
--red-20: #ef444433;
--red-10: #ef44441a;
--red-5: #ef44440d;
--amber-100: #f59e0b;
--amber-80: #f59e0bcc;
--amber-60: #f59e0b99;
--amber-20: #f59e0b33;
--amber-10: #f59e0b1a;
--amber-5: #f59e0b0d;
--indigo-100: #7c7ef4;
--indigo-80: #7c7ef4cc;
--indigo-60: #7c7ef499;
--indigo-20: #7c7ef433;
--indigo-10: #7c7ef41a;
--indigo-5: #7c7ef40d;
--green-100: #10b981;
--green-80: #10b981cc;
--green-60: #10b98199;
--green-20: #10b98133;
--green-10: #10b9811a;
--green-5: #10b9810d;
--white-100: #f4f4f5;
--white-80: #f4f4f5cc;
--white-60: #f4f4f599;
--white-20: #f4f4f533;
--white-10: #f4f4f51a;
--white-5: #f4f4f50d;
--black-100: #09090b;
--black-80: #09090bcc;
--black-60: #09090b99;
--black-20: #09090b33;
--black-10: #09090b1a;
--black-5: #09090b0d;
}

*,
Expand Down
4 changes: 2 additions & 2 deletions src/components/KeyHint/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { colors } from '../../utils/colors.js'
import { radius } from '../../utils/radius.js'
import { useIsMac } from '../../hooks/useIsMac.js'
Expand Down Expand Up @@ -91,7 +91,7 @@ function KeyHint({
const triggerFnRef = useRef(onTrigger)
const isMac = useIsMac()

useLayoutEffect(() => {
useEffect(() => {
triggerFnRef.current = onTrigger
})

Expand Down
19 changes: 19 additions & 0 deletions src/components/Provider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ReactNode } from 'react'
import { KeyboardEventProvider } from '../../hooks/useHadKeyboardEvent.js'
import { ThemeProvider, ThemeProviderProps } from '../../hooks/useTheme.js'

type ProviderProps = {
children: ReactNode
forcedTheme: ThemeProviderProps['forcedTheme']
}

function Provider({ children, forcedTheme }: ProviderProps) {
return (
<ThemeProvider forcedTheme={forcedTheme}>
<KeyboardEventProvider>{children}</KeyboardEventProvider>
</ThemeProvider>
)
}

export type { ProviderProps }
export { Provider }
58 changes: 58 additions & 0 deletions src/hooks/useTheme.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState, useEffect, createContext, useContext } from 'react'

const LOCAL_STORAGE_KEY = 'based-ui-theme'
const DEFAULT_THEME: Theme = 'light'
const ATTRIBUTE_NAME = 'data-theme'
// This script is necessary because we want to set the data-theme attribute before the first render
// so that light/dark does not flash
// It should be placed somewhere in the body of the document, since it's sync it will stop the document parsing and so a render won't happen which is exactly what we are looking for
export const THEME_PREVENT_FLASH_SCRIPT_SRC = `<script>!function(){try{var t=localStorage.getItem("${LOCAL_STORAGE_KEY}");document.documentElement.setAttribute("${ATTRIBUTE_NAME}",null!=t?t:"${DEFAULT_THEME}")}catch(t){}}();</script>`

type Theme = 'light' | 'dark'

type UseThemeProps = {
theme: Theme
setTheme: React.Dispatch<React.SetStateAction<Theme>>
}

const defaultContext = {
setTheme: () => {},
theme: DEFAULT_THEME,
}

const ThemeContext = createContext<UseThemeProps>(defaultContext)

export function useTheme() {
return useContext(ThemeContext)
}

export type ThemeProviderProps = {
children: React.ReactNode
forcedTheme?: Theme // needed for storybook, where we don't control the theme switcher button
}

export function ThemeProvider({ children, forcedTheme }: ThemeProviderProps) {
const [theme, setTheme] = useState(() => {
let theme

try {
theme = localStorage.getItem(LOCAL_STORAGE_KEY)
} catch {}

return forcedTheme ?? ((theme ?? DEFAULT_THEME) as Theme)
})

useEffect(() => {
document.documentElement.setAttribute(ATTRIBUTE_NAME, forcedTheme ?? theme)

try {
localStorage.setItem(LOCAL_STORAGE_KEY, forcedTheme ?? theme)
} catch {}
}, [theme, forcedTheme])

return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
)
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export * from './components/Counter/index.js'
export * from './components/KeyHint/index.js'
export * from './components/TextAreaInput/index.js'
export * from './components/NumberInput/index.js'
export * from './components/Provider/index.js'
export * from './hooks/useHadKeyboardEvent.js'
export * from './hooks/useTheme.js'
export * from './utils/colors.js'
export * from './utils/radius.js'

0 comments on commit dfefb6b

Please sign in to comment.