mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-27 19:25:05 +08:00
refactor(themes): refactor theme module to keep multiple themes (#440)
* refactor(themes): refactor theme module to keep multiple themes * chore: migrate APIs to be compatible with new theme system * test: update snapshots * chore: migrate the path of the theme module * feat(themes): append static methods of themes * chore: hide custom theme when no custom content in the context * chore: manually add flush to preload styles in html * docs(themes): update to fit the new theme system
This commit is contained in:
@@ -18,8 +18,10 @@ module.exports = {
|
||||
'components/**/*.{ts,tsx}',
|
||||
'!components/**/styles.{ts,tsx}',
|
||||
'!components/**/*types.{ts,tsx}',
|
||||
'!components/styles/*',
|
||||
'!components/use-theme/*',
|
||||
'!components/use-all-themes/*',
|
||||
'!components/themes/*',
|
||||
'!components/geist-provider/*',
|
||||
'!components/index.ts',
|
||||
'!components/utils/*',
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import useTheme from '../use-theme'
|
||||
import { NormalSizes, NormalTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
import BadgeAnchor from './badge-anchor'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
import { NormalTypes } from '../utils/prop-types'
|
||||
|
||||
type ButtonDropdownColors = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import useTheme from '../use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import { NormalSizes, ButtonTypes } from '../utils/prop-types'
|
||||
import { ButtonGroupContext, ButtonGroupConfig } from './button-group-context'
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
import { NormalSizes, ButtonTypes } from '../utils/prop-types'
|
||||
import { ButtonProps } from './button'
|
||||
import { addColorAlpha } from '../utils/color'
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { useProportions } from '../utils/calculations'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
interface Props {
|
||||
value?: number
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CardTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
export type CardStyles = {
|
||||
color: string
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('CSSBaseline', () => {
|
||||
|
||||
it('should render dark mode correctly', () => {
|
||||
const wrapper = render(
|
||||
<GeistProvider theme={{ type: 'dark' }}>
|
||||
<GeistProvider themeType="dark">
|
||||
<CssBaseline />
|
||||
</GeistProvider>,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import useTheme from '../use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import { DividerAlign, SnippetTypes } from '../utils/prop-types'
|
||||
import { getMargin } from '../spacer/spacer'
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
|
||||
export type DividerTypes = SnippetTypes
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { NormalTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemes } from '../styles/themes'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
|
||||
interface Props {
|
||||
type?: NormalTypes
|
||||
|
||||
@@ -4,16 +4,17 @@ import {
|
||||
GeistUIContextParams,
|
||||
UpdateToastsFunction,
|
||||
} from '../utils/use-geist-ui-context'
|
||||
import ThemeProvider from '../styles/theme-provider'
|
||||
import { ThemeParam } from '../styles/theme-provider/theme-provider'
|
||||
import ThemeProvider from './theme-provider'
|
||||
import useCurrentState from '../utils/use-current-state'
|
||||
import ToastContainer, { ToastWithID } from '../use-toasts/toast-container'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
|
||||
export interface Props {
|
||||
theme?: ThemeParam
|
||||
themes?: Array<GeistUIThemes>
|
||||
themeType?: string | 'dark' | 'light'
|
||||
}
|
||||
|
||||
const GeistProvider: React.FC<PropsWithChildren<Props>> = ({ theme, children }) => {
|
||||
const GeistProvider: React.FC<PropsWithChildren<Props>> = ({ themes, themeType, children }) => {
|
||||
const [toasts, setToasts, toastsRef] = useCurrentState<Array<ToastWithID>>([])
|
||||
const [toastHovering, setToastHovering] = useState<boolean>(false)
|
||||
const updateToasts: UpdateToastsFunction<ToastWithID> = (
|
||||
@@ -40,7 +41,7 @@ const GeistProvider: React.FC<PropsWithChildren<Props>> = ({ theme, children })
|
||||
|
||||
return (
|
||||
<GeistUIContent.Provider value={initialValue}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<ThemeProvider themes={themes} themeType={themeType}>
|
||||
{children}
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
|
||||
44
components/geist-provider/theme-provider.tsx
Normal file
44
components/geist-provider/theme-provider.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react'
|
||||
import Themes from '../themes'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
import { ThemeContext } from '../use-theme/theme-context'
|
||||
import { AllThemesConfig, AllThemesContext } from '../use-all-themes/all-themes-context'
|
||||
|
||||
export interface Props {
|
||||
themeType?: string
|
||||
themes?: Array<GeistUIThemes>
|
||||
}
|
||||
|
||||
const ThemeProvider: React.FC<PropsWithChildren<Props>> = ({
|
||||
children,
|
||||
themeType,
|
||||
themes = [],
|
||||
}) => {
|
||||
const [allThemes, setAllThemes] = useState<AllThemesConfig>({ themes: Themes.getPresets() })
|
||||
|
||||
const currentTheme = useMemo<GeistUIThemes>(() => {
|
||||
const theme = allThemes.themes.find(item => item.type === themeType)
|
||||
if (theme) return theme
|
||||
return Themes.getPresetStaticTheme()
|
||||
}, [allThemes, themeType])
|
||||
|
||||
useEffect(() => {
|
||||
if (!themes?.length) return
|
||||
setAllThemes(last => {
|
||||
const safeThemes = themes.filter(item => Themes.isAvailableThemeType(item.type))
|
||||
const nextThemes = Themes.getPresets().concat(safeThemes)
|
||||
return {
|
||||
...last,
|
||||
themes: nextThemes,
|
||||
}
|
||||
})
|
||||
}, [themes])
|
||||
|
||||
return (
|
||||
<AllThemesContext.Provider value={allThemes}>
|
||||
<ThemeContext.Provider value={currentTheme}>{children}</ThemeContext.Provider>
|
||||
</AllThemesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ThemeProvider
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
|
||||
export type BrowserColors = {
|
||||
color: string
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/// <reference types="styled-jsx" />
|
||||
|
||||
export * from './styles/themes'
|
||||
export * from './themes/presets'
|
||||
export { default as Themes } from './themes'
|
||||
export { default as useTheme } from './use-theme'
|
||||
export { default as useAllThemes } from './use-all-themes'
|
||||
export { default as GeistProvider } from './geist-provider'
|
||||
export { default as CssBaseline } from './css-baseline'
|
||||
export { default as useToasts } from './use-toasts'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NormalSizes, NormalTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
export type InputSize = {
|
||||
heightRatio: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { GeistUIThemes } from '../styles/themes'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
|
||||
interface Props {
|
||||
command?: boolean
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import useTheme from '../use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import { NormalSizes, NormalTypes } from 'components/utils/prop-types'
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
|
||||
interface Props {
|
||||
size?: NormalSizes
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import useTheme from '../use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import { NormalTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemes } from '../styles/themes'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
|
||||
interface Props {
|
||||
type?: NormalTypes
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('Page', () => {
|
||||
|
||||
it('should disable dot style when in dark mode', () => {
|
||||
const wrapper = mount(
|
||||
<GeistProvider theme={{ type: 'dark' }}>
|
||||
<GeistProvider themeType="dark">
|
||||
<Page dotBackdrop />
|
||||
</GeistProvider>,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageSize } from './page'
|
||||
import { NormalSizes } from '../utils/prop-types'
|
||||
import { GeistUIThemesLayout } from '../styles/themes'
|
||||
import { GeistUIThemesLayout } from '../themes/presets'
|
||||
|
||||
export const getPageSize = (size: PageSize, layout: GeistUIThemesLayout): string => {
|
||||
const presets: { [key in NormalSizes]: string } = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { useProportions } from '../utils/calculations'
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
import { NormalTypes } from 'components/utils/prop-types'
|
||||
|
||||
export type ProgressColors = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NormalSizes } from 'components/utils/prop-types'
|
||||
import { GeistUIThemes } from 'components/styles/themes'
|
||||
import { GeistUIThemes } from 'components/themes/presets'
|
||||
|
||||
export interface SelectSize {
|
||||
height: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SnippetTypes } from 'components/utils/prop-types'
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
|
||||
export type SnippetStyles = {
|
||||
color: string
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { NormalSizes } from '../utils/prop-types'
|
||||
import { GeistUIThemes } from '../styles/themes'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
|
||||
interface Props {
|
||||
size?: NormalSizes
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ThemeProvider should merge themes with custom function 1`] = `
|
||||
initialize {
|
||||
"0": Node {
|
||||
"attribs": Object {
|
||||
"class": " ",
|
||||
},
|
||||
"children": Array [
|
||||
Node {
|
||||
"data": "hello",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"name": "p",
|
||||
"namespace": "http://www.w3.org/1999/xhtml",
|
||||
"next": Node {
|
||||
"attribs": Object {},
|
||||
"children": Array [
|
||||
Node {
|
||||
"data": "
|
||||
p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.custom-size {
|
||||
font-size: inherit;
|
||||
}
|
||||
",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"name": "style",
|
||||
"namespace": "http://www.w3.org/1999/xhtml",
|
||||
"next": null,
|
||||
"parent": Node {
|
||||
"children": Array [
|
||||
[Circular],
|
||||
[Circular],
|
||||
],
|
||||
"name": "root",
|
||||
"next": null,
|
||||
"parent": null,
|
||||
"prev": null,
|
||||
"type": "root",
|
||||
},
|
||||
"prev": [Circular],
|
||||
"type": "style",
|
||||
"x-attribsNamespace": Object {},
|
||||
"x-attribsPrefix": Object {},
|
||||
},
|
||||
"parent": Node {
|
||||
"children": Array [
|
||||
[Circular],
|
||||
Node {
|
||||
"attribs": Object {},
|
||||
"children": Array [
|
||||
Node {
|
||||
"data": "
|
||||
p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.custom-size {
|
||||
font-size: inherit;
|
||||
}
|
||||
",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"name": "style",
|
||||
"namespace": "http://www.w3.org/1999/xhtml",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": [Circular],
|
||||
"type": "style",
|
||||
"x-attribsNamespace": Object {},
|
||||
"x-attribsPrefix": Object {},
|
||||
},
|
||||
],
|
||||
"name": "root",
|
||||
"next": null,
|
||||
"parent": null,
|
||||
"prev": null,
|
||||
"type": "root",
|
||||
},
|
||||
"prev": null,
|
||||
"type": "tag",
|
||||
"x-attribsNamespace": Object {
|
||||
"class": undefined,
|
||||
},
|
||||
"x-attribsPrefix": Object {
|
||||
"class": undefined,
|
||||
},
|
||||
},
|
||||
"1": Node {
|
||||
"attribs": Object {},
|
||||
"children": Array [
|
||||
Node {
|
||||
"data": "
|
||||
p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.custom-size {
|
||||
font-size: inherit;
|
||||
}
|
||||
",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"name": "style",
|
||||
"namespace": "http://www.w3.org/1999/xhtml",
|
||||
"next": null,
|
||||
"parent": Node {
|
||||
"children": Array [
|
||||
Node {
|
||||
"attribs": Object {
|
||||
"class": " ",
|
||||
},
|
||||
"children": Array [
|
||||
Node {
|
||||
"data": "hello",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"name": "p",
|
||||
"namespace": "http://www.w3.org/1999/xhtml",
|
||||
"next": [Circular],
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "tag",
|
||||
"x-attribsNamespace": Object {
|
||||
"class": undefined,
|
||||
},
|
||||
"x-attribsPrefix": Object {
|
||||
"class": undefined,
|
||||
},
|
||||
},
|
||||
[Circular],
|
||||
],
|
||||
"name": "root",
|
||||
"next": null,
|
||||
"parent": null,
|
||||
"prev": null,
|
||||
"type": "root",
|
||||
},
|
||||
"prev": Node {
|
||||
"attribs": Object {
|
||||
"class": " ",
|
||||
},
|
||||
"children": Array [
|
||||
Node {
|
||||
"data": "hello",
|
||||
"next": null,
|
||||
"parent": [Circular],
|
||||
"prev": null,
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"name": "p",
|
||||
"namespace": "http://www.w3.org/1999/xhtml",
|
||||
"next": [Circular],
|
||||
"parent": Node {
|
||||
"children": Array [
|
||||
[Circular],
|
||||
[Circular],
|
||||
],
|
||||
"name": "root",
|
||||
"next": null,
|
||||
"parent": null,
|
||||
"prev": null,
|
||||
"type": "root",
|
||||
},
|
||||
"prev": null,
|
||||
"type": "tag",
|
||||
"x-attribsNamespace": Object {
|
||||
"class": undefined,
|
||||
},
|
||||
"x-attribsPrefix": Object {
|
||||
"class": undefined,
|
||||
},
|
||||
},
|
||||
"type": "style",
|
||||
"x-attribsNamespace": Object {},
|
||||
"x-attribsPrefix": Object {},
|
||||
},
|
||||
"_root": [Circular],
|
||||
"length": 2,
|
||||
"options": Object {
|
||||
"decodeEntities": true,
|
||||
"xml": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react'
|
||||
import { render, mount } from 'enzyme'
|
||||
import { deepMergeObject } from '../theme-provider/theme-provider'
|
||||
import DefaultThemes from '../themes/default'
|
||||
import { GeistProvider, GeistUIThemes, Text } from 'components'
|
||||
import { DeepPartial } from 'components/utils/types'
|
||||
|
||||
describe('ThemeProvider', () => {
|
||||
it('should deep merge objects but not add new key', () => {
|
||||
const sourceObject: any = {
|
||||
palette: {
|
||||
success: '#000',
|
||||
},
|
||||
arr: ['array', 10],
|
||||
font: {
|
||||
subFont: {
|
||||
key: 'font',
|
||||
},
|
||||
},
|
||||
}
|
||||
const targetObject: any = {
|
||||
palette: {
|
||||
warning: '#000',
|
||||
success: '#ccc',
|
||||
},
|
||||
arr: [5],
|
||||
font: {
|
||||
first: 'first',
|
||||
},
|
||||
}
|
||||
expect(deepMergeObject(sourceObject, targetObject)).toMatchObject({
|
||||
palette: {
|
||||
success: '#ccc',
|
||||
},
|
||||
arr: [5, 'array', 10],
|
||||
font: {
|
||||
subFont: {
|
||||
key: 'font',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should merge themes with custom function', () => {
|
||||
const customFunc = (): DeepPartial<GeistUIThemes> => {
|
||||
return {
|
||||
...DefaultThemes,
|
||||
palette: {
|
||||
success: '#ccc',
|
||||
},
|
||||
}
|
||||
}
|
||||
const wrapper = render(
|
||||
<GeistProvider theme={customFunc}>
|
||||
<Text type="success">hello</Text>
|
||||
</GeistProvider>,
|
||||
)
|
||||
expect(wrapper).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should warning when using the wrong merge function', () => {
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const customFunc = () => 0 as DeepPartial<GeistUIThemes>
|
||||
const wrapper = mount(
|
||||
<GeistProvider theme={customFunc}>
|
||||
<p>test</p>
|
||||
</GeistProvider>,
|
||||
)
|
||||
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
errorSpy.mockRestore()
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './theme-provider'
|
||||
@@ -1,74 +0,0 @@
|
||||
import React, { PropsWithChildren } from 'react'
|
||||
import useTheme from '../../use-theme'
|
||||
import darkTheme from '../themes/dark'
|
||||
import lightTheme from '../themes/default'
|
||||
import { GeistUIThemes } from '../themes/index'
|
||||
import ThemeContext from '../../use-theme/theme-context'
|
||||
import useWarning from '../../utils/use-warning'
|
||||
import { DeepPartial } from '../../utils/types'
|
||||
|
||||
type PartialTheme = DeepPartial<GeistUIThemes>
|
||||
export type ThemeParam = PartialTheme | ((theme: PartialTheme) => PartialTheme) | undefined
|
||||
|
||||
export interface Props {
|
||||
theme?: ThemeParam
|
||||
}
|
||||
|
||||
export interface MergeObject {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export const isObject = (target: any) => target && typeof target === 'object'
|
||||
|
||||
export const deepMergeObject = <T extends MergeObject>(source: T, target: T): T => {
|
||||
if (!isObject(target) || !isObject(source)) return source
|
||||
|
||||
const sourceKeys = Object.keys(source) as Array<keyof T>
|
||||
let result = {} as T
|
||||
for (const key of sourceKeys) {
|
||||
const sourceValue = source[key]
|
||||
const targetValue = target[key]
|
||||
|
||||
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
|
||||
result[key] = targetValue.concat(sourceValue)
|
||||
} else if (isObject(sourceValue) && isObject(targetValue)) {
|
||||
result[key] = deepMergeObject(sourceValue, { ...targetValue })
|
||||
} else if (targetValue) {
|
||||
result[key] = targetValue
|
||||
} else {
|
||||
result[key] = sourceValue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const mergeTheme = (current: GeistUIThemes, custom: ThemeParam): GeistUIThemes => {
|
||||
if (!custom) return current
|
||||
if (typeof custom === 'function') {
|
||||
const merged = custom(current)
|
||||
if (!merged || typeof merged !== 'object') {
|
||||
useWarning('The theme function must return object value.')
|
||||
}
|
||||
return merged as GeistUIThemes
|
||||
}
|
||||
return deepMergeObject<GeistUIThemes>(current, custom as GeistUIThemes)
|
||||
}
|
||||
|
||||
const switchTheme = (mergedTheme: GeistUIThemes): GeistUIThemes => {
|
||||
const themes: { [key in GeistUIThemes['type']]: GeistUIThemes } = {
|
||||
light: lightTheme,
|
||||
dark: darkTheme,
|
||||
}
|
||||
return { ...mergedTheme, ...themes[mergedTheme.type] }
|
||||
}
|
||||
|
||||
const ThemeProvider: React.FC<PropsWithChildren<Props>> = ({ children, theme }) => {
|
||||
const customTheme = theme
|
||||
const currentTheme = useTheme()
|
||||
const merged = mergeTheme(currentTheme, customTheme)
|
||||
const userTheme = currentTheme.type !== merged.type ? switchTheme(merged) : merged
|
||||
|
||||
return <ThemeContext.Provider value={userTheme}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export default ThemeProvider
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { SnippetTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
interface Props {
|
||||
type?: SnippetTypes
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useTheme from '../use-theme'
|
||||
import { NormalTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
export interface Props {
|
||||
tag: keyof JSX.IntrinsicElements
|
||||
|
||||
3
components/themes/index.ts
Normal file
3
components/themes/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Themes from './themes'
|
||||
|
||||
export default Themes
|
||||
@@ -1,5 +1,3 @@
|
||||
import { ThemeTypes } from '../../utils/prop-types'
|
||||
|
||||
export interface GeistUIThemesPalette {
|
||||
accents_1: string
|
||||
accents_2: string
|
||||
@@ -87,7 +85,7 @@ export interface GeistUIThemesBreakpoints {
|
||||
}
|
||||
|
||||
export interface GeistUIThemes {
|
||||
type: ThemeTypes
|
||||
type: string
|
||||
font: GeistUIThemesFont
|
||||
layout: GeistUIThemesLayout
|
||||
palette: GeistUIThemesPalette
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
GeistUIThemesBreakpoints,
|
||||
GeistUIThemesFont,
|
||||
GeistUIThemesLayout,
|
||||
} from 'components/styles/themes/index'
|
||||
import { GeistUIThemesBreakpoints, GeistUIThemesFont, GeistUIThemesLayout } from './index'
|
||||
|
||||
export const defaultFont: GeistUIThemesFont = {
|
||||
sans:
|
||||
84
components/themes/themes.ts
Normal file
84
components/themes/themes.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { GeistUIThemes } from './presets/index'
|
||||
import { DeepPartial } from '../utils/types'
|
||||
import lightTheme from './presets/default'
|
||||
import darkTheme from './presets/dark'
|
||||
|
||||
export type GeistUserTheme = DeepPartial<GeistUIThemes> & { type: string }
|
||||
|
||||
export const isObject = (target: unknown) => target && typeof target === 'object'
|
||||
|
||||
export const deepDuplicable = <T extends Record<string, unknown>>(source: T, target: T): T => {
|
||||
if (!isObject(target) || !isObject(source)) return source as T
|
||||
|
||||
const sourceKeys = Object.keys(source) as Array<keyof T>
|
||||
let result = {} as any
|
||||
for (const key of sourceKeys) {
|
||||
const sourceValue = source[key]
|
||||
const targetValue = target[key]
|
||||
|
||||
if (Array.isArray(sourceValue) && Array.isArray(targetValue)) {
|
||||
result[key] = targetValue.concat(sourceValue)
|
||||
} else if (isObject(sourceValue) && isObject(targetValue)) {
|
||||
result[key] = deepDuplicable(sourceValue as Record<string, unknown>, {
|
||||
...(targetValue as Record<string, unknown>),
|
||||
})
|
||||
} else if (targetValue) {
|
||||
result[key] = targetValue
|
||||
} else {
|
||||
result[key] = sourceValue
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const getPresets = (): Array<GeistUIThemes> => {
|
||||
return [lightTheme, darkTheme]
|
||||
}
|
||||
|
||||
const getPresetStaticTheme = (): GeistUIThemes => {
|
||||
return lightTheme
|
||||
}
|
||||
|
||||
const isAvailableThemeType = (type?: string): boolean => {
|
||||
if (!type) return false
|
||||
const presetThemes = getPresets()
|
||||
const hasType = presetThemes.find(theme => theme.type === type)
|
||||
return !hasType
|
||||
}
|
||||
|
||||
const isPresetTheme = (themeOrType?: GeistUserTheme | GeistUIThemes | string): boolean => {
|
||||
if (!themeOrType) return false
|
||||
const isType = typeof themeOrType === 'string'
|
||||
const type = isType
|
||||
? (themeOrType as string)
|
||||
: (themeOrType as Exclude<typeof themeOrType, string>).type
|
||||
return !isAvailableThemeType(type)
|
||||
}
|
||||
|
||||
const hasUserCustomTheme = (themes: Array<GeistUIThemes> = []): boolean => {
|
||||
return !!themes.find(item => isAvailableThemeType(item.type))
|
||||
}
|
||||
|
||||
const create = (base: GeistUIThemes, custom: GeistUserTheme): GeistUIThemes => {
|
||||
if (!isAvailableThemeType(custom.type)) {
|
||||
throw new Error('Duplicate or unavailable theme type')
|
||||
}
|
||||
|
||||
return deepDuplicable(base, custom) as GeistUIThemes
|
||||
}
|
||||
|
||||
const createFromDark = (custom: GeistUserTheme) => create(darkTheme, custom)
|
||||
const createFromLight = (custom: GeistUserTheme) => create(lightTheme, custom)
|
||||
|
||||
const Themes = {
|
||||
isPresetTheme,
|
||||
isAvailableThemeType,
|
||||
hasUserCustomTheme,
|
||||
getPresets,
|
||||
getPresetStaticTheme,
|
||||
create,
|
||||
createFromDark,
|
||||
createFromLight,
|
||||
}
|
||||
|
||||
export default Themes
|
||||
@@ -15,7 +15,7 @@ const expectTooltipIsHidden = (wrapper: ReactWrapper) => {
|
||||
describe('Tooltip', () => {
|
||||
it('should render correctly', async () => {
|
||||
const wrapper = mount(
|
||||
<GeistProvider theme={{ type: 'dark' }}>
|
||||
<GeistProvider themeType="dark">
|
||||
<Tooltip text={<p id="test">custom-content</p>}>some tips</Tooltip>
|
||||
</GeistProvider>,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SnippetTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
export type TooltipColors = {
|
||||
bgColor: string
|
||||
|
||||
18
components/use-all-themes/all-themes-context.ts
Normal file
18
components/use-all-themes/all-themes-context.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
import Themes from '../themes/themes'
|
||||
import { GeistUIThemes } from '../themes/presets'
|
||||
|
||||
export type AllThemesConfig = {
|
||||
themes: Array<GeistUIThemes>
|
||||
}
|
||||
|
||||
const defaultAllThemesConfig = {
|
||||
themes: Themes.getPresets(),
|
||||
}
|
||||
|
||||
export const AllThemesContext: React.Context<AllThemesConfig> = React.createContext<
|
||||
AllThemesConfig
|
||||
>(defaultAllThemesConfig)
|
||||
|
||||
export const useAllThemes = (): AllThemesConfig =>
|
||||
React.useContext<AllThemesConfig>(AllThemesContext)
|
||||
3
components/use-all-themes/index.ts
Normal file
3
components/use-all-themes/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { useAllThemes } from './all-themes-context'
|
||||
|
||||
export default useAllThemes
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import useTheme from '../use-theme'
|
||||
import { tuple } from '../utils/prop-types'
|
||||
import { BreakpointsItem, GeistUIThemesBreakpoints } from '../styles/themes'
|
||||
import { BreakpointsItem, GeistUIThemesBreakpoints } from '../themes/presets'
|
||||
|
||||
const breakpoints = tuple('xs', 'sm', 'md', 'lg', 'xl', 'mobile')
|
||||
export type ResponsiveBreakpoint = typeof breakpoints[number]
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { default } from './use-theme'
|
||||
import { useTheme } from './theme-context'
|
||||
|
||||
export default useTheme
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from 'react'
|
||||
import { GeistUIThemes } from '../styles/themes/index'
|
||||
import defaultTheme from '../styles/themes/default'
|
||||
import Themes from '../themes'
|
||||
import { GeistUIThemes } from '../themes/presets/index'
|
||||
|
||||
const ThemeContext: React.Context<GeistUIThemes> = React.createContext<GeistUIThemes>(defaultTheme)
|
||||
const defaultTheme = Themes.getPresetStaticTheme()
|
||||
|
||||
export default ThemeContext
|
||||
export const ThemeContext: React.Context<GeistUIThemes> = React.createContext<GeistUIThemes>(
|
||||
defaultTheme,
|
||||
)
|
||||
export const useTheme = (): GeistUIThemes => React.useContext<GeistUIThemes>(ThemeContext)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import React from 'react'
|
||||
import ThemeContext from './theme-context'
|
||||
import { GeistUIThemes } from '../styles/themes/index'
|
||||
|
||||
const useTheme = (): GeistUIThemes => React.useContext<GeistUIThemes>(ThemeContext)
|
||||
|
||||
export default useTheme
|
||||
@@ -3,7 +3,7 @@ import useTheme from '../use-theme'
|
||||
import { Toast, ToastAction } from './use-toast'
|
||||
import Button from '../button'
|
||||
import { NormalTypes } from '../utils/prop-types'
|
||||
import { GeistUIThemesPalette } from '../styles/themes'
|
||||
import { GeistUIThemesPalette } from '../themes/presets'
|
||||
|
||||
type ToastWithID = Toast & {
|
||||
id: string
|
||||
|
||||
@@ -17,8 +17,6 @@ const normalSizes = tuple('mini', 'small', 'medium', 'large')
|
||||
|
||||
const normalTypes = tuple('default', 'secondary', 'success', 'warning', 'error')
|
||||
|
||||
const themeTypes = tuple('dark', 'light')
|
||||
|
||||
const snippetTypes = tuple('default', 'secondary', 'success', 'warning', 'error', 'dark', 'lite')
|
||||
|
||||
const cardTypes = tuple(
|
||||
@@ -62,8 +60,6 @@ export type NormalSizes = typeof normalSizes[number]
|
||||
|
||||
export type NormalTypes = typeof normalTypes[number]
|
||||
|
||||
export type ThemeTypes = typeof themeTypes[number]
|
||||
|
||||
export type SnippetTypes = typeof snippetTypes[number]
|
||||
|
||||
export type CardTypes = typeof cardTypes[number]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Just customize what you need, deep merge themes by default.
|
||||
*
|
||||
* If you are using TypeScript, please use the following type definition.
|
||||
* If you are using JavaScript, refer to https://github.com/geist-org/react/blob/master/components/styles/themes/default.ts
|
||||
* If you are using JavaScript, refer to https://github.com/geist-org/react/blob/master/components/themes/presets/index.ts
|
||||
*/
|
||||
|
||||
// import {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Avatar, Link, Tooltip, useTheme } from 'components'
|
||||
import { useConfigs } from 'lib/config-context'
|
||||
const GithubURL = 'https://github.com/geist-org/react/blob/master'
|
||||
const host = 'https://contributors.geist-ui.dev/api/users'
|
||||
import { CONTRIBUTORS_URL, GITHUB_URL } from 'lib/constants'
|
||||
const RepoMasterURL = `${GITHUB_URL}/blob/master`
|
||||
|
||||
export interface Contributor {
|
||||
name: string
|
||||
@@ -16,7 +16,7 @@ interface Props {
|
||||
|
||||
const getContributors = async (path: string): Promise<Array<Contributor>> => {
|
||||
try {
|
||||
const response = await fetch(`${host}?path=${path}`)
|
||||
const response = await fetch(`${CONTRIBUTORS_URL}?path=${path}`)
|
||||
if (!response.ok || response.status === 204) return []
|
||||
return response.json()
|
||||
} catch (e) {
|
||||
@@ -28,7 +28,7 @@ const Contributors: React.FC<Props> = ({ path }) => {
|
||||
const theme = useTheme()
|
||||
const { isChinese } = useConfigs()
|
||||
const [users, setUsers] = useState<Array<Contributor>>([])
|
||||
const link = useMemo(() => `${GithubURL}/${path || '/pages'}`, [])
|
||||
const link = useMemo(() => `${RepoMasterURL}/${path || '/pages'}`, [])
|
||||
|
||||
useEffect(() => {
|
||||
let unmount = false
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Button, useTheme, Select, Spacer } from 'components'
|
||||
import { Button, useTheme, Select, Spacer, Themes, useAllThemes } from 'components'
|
||||
import { useConfigs } from 'lib/config-context'
|
||||
import useLocale from 'lib/use-locale'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import MoonIcon from '@geist-ui/react-icons/moon'
|
||||
import SunIcon from '@geist-ui/react-icons/sun'
|
||||
import UserIcon from '@geist-ui/react-icons/user'
|
||||
import {
|
||||
CHINESE_LANGUAGE_IDENT,
|
||||
CUSTOM_THEME_TYPE,
|
||||
ENGLISH_LANGUAGE_IDENT,
|
||||
GITHUB_URL,
|
||||
} from 'lib/constants'
|
||||
|
||||
const Controls: React.FC<unknown> = React.memo(({}) => {
|
||||
const Controls: React.FC<unknown> = React.memo(() => {
|
||||
const theme = useTheme()
|
||||
const { updateCustomTheme, updateChineseState } = useConfigs()
|
||||
const { themes } = useAllThemes()
|
||||
const { switchTheme, updateChineseState } = useConfigs()
|
||||
const { pathname } = useRouter()
|
||||
const { locale } = useLocale()
|
||||
const isChinese = useMemo(() => locale === 'zh-cn', [locale])
|
||||
const isDark = useMemo(() => theme.type === 'dark', [theme.type])
|
||||
const isChinese = useMemo(() => locale === CHINESE_LANGUAGE_IDENT, [locale])
|
||||
const nextLocalePath = useMemo(() => {
|
||||
const nextLocale = isChinese ? 'en-us' : 'zh-cn'
|
||||
const nextLocale = isChinese ? ENGLISH_LANGUAGE_IDENT : CHINESE_LANGUAGE_IDENT
|
||||
return pathname.replace(locale, nextLocale)
|
||||
}, [locale, pathname])
|
||||
const hasCustomTheme = useMemo(() => Themes.hasUserCustomTheme(themes), [themes])
|
||||
|
||||
const switchThemes = (type: 'dark' | 'light') => {
|
||||
updateCustomTheme({ type })
|
||||
const switchThemes = (type: string) => {
|
||||
switchTheme(type)
|
||||
if (typeof window === 'undefined' || !window.localStorage) return
|
||||
window.localStorage.setItem('theme', type)
|
||||
}
|
||||
@@ -28,9 +36,8 @@ const Controls: React.FC<unknown> = React.memo(({}) => {
|
||||
Router.push(nextLocalePath)
|
||||
}
|
||||
const redirectGithub = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.open('https://github.com/geist-org/react')
|
||||
}
|
||||
if (typeof window === 'undefined') return
|
||||
window.open(GITHUB_URL)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -53,7 +60,7 @@ const Controls: React.FC<unknown> = React.memo(({}) => {
|
||||
size="small"
|
||||
pure
|
||||
onChange={switchThemes}
|
||||
value={isDark ? 'dark' : 'light'}
|
||||
value={theme.type}
|
||||
title={isChinese ? '切换主题' : 'Switch Themes'}>
|
||||
<Select.Option value="light">
|
||||
<span className="select-content">
|
||||
@@ -65,6 +72,13 @@ const Controls: React.FC<unknown> = React.memo(({}) => {
|
||||
<MoonIcon size={14} /> {isChinese ? '暗黑' : 'Dark'}
|
||||
</span>
|
||||
</Select.Option>
|
||||
{hasCustomTheme && (
|
||||
<Select.Option value={CUSTOM_THEME_TYPE}>
|
||||
<span className="select-content">
|
||||
<UserIcon size={14} /> {CUSTOM_THEME_TYPE}
|
||||
</span>
|
||||
</Select.Option>
|
||||
)}
|
||||
</Select>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { Text, Spacer, useTheme, Code, useToasts } from 'components'
|
||||
import DefaultTheme from 'components/styles/themes/default'
|
||||
import { isObject, MergeObject } from 'components/styles/theme-provider/theme-provider'
|
||||
import { isObject } from 'components/themes/themes'
|
||||
import { LiveEditor, LiveProvider } from 'react-live'
|
||||
import makeCodeTheme from 'lib/components/playground/code-theme'
|
||||
import useClipboard from 'components/utils/use-clipboard'
|
||||
import CopyIcon from 'components/snippet/snippet-icon'
|
||||
import { useConfigs } from 'lib/config-context'
|
||||
import { CUSTOM_THEME_TYPE } from 'lib/constants'
|
||||
import CopyIcon from 'components/snippet/snippet-icon'
|
||||
import makeCodeTheme from 'lib/components/playground/code-theme'
|
||||
import { Text, Spacer, useTheme, Code, useToasts, Themes, useClipboard } from 'components'
|
||||
|
||||
export const getDeepDifferents = <T extends MergeObject>(source: T, target: T): T => {
|
||||
export const getDeepDifferents = <T,>(source: T, target: T): T => {
|
||||
if (!isObject(target) || !isObject(source)) return target
|
||||
|
||||
const sourceKeys = Object.keys(source) as Array<keyof T>
|
||||
@@ -29,17 +28,21 @@ export const getDeepDifferents = <T extends MergeObject>(source: T, target: T):
|
||||
return result
|
||||
}
|
||||
|
||||
const CustomizationCodes = () => {
|
||||
const CustomizationCodes: React.FC<unknown> = () => {
|
||||
const DefaultTheme = Themes.getPresetStaticTheme()
|
||||
const theme = useTheme()
|
||||
const { isChinese } = useConfigs()
|
||||
const codeTheme = makeCodeTheme(theme)
|
||||
const { copy } = useClipboard()
|
||||
const [, setToast] = useToasts()
|
||||
|
||||
const deepDifferents = useMemo(() => getDeepDifferents(DefaultTheme, theme), [
|
||||
DefaultTheme,
|
||||
theme,
|
||||
])
|
||||
const deepDifferents = useMemo(
|
||||
() => ({
|
||||
...getDeepDifferents(DefaultTheme, theme),
|
||||
type: CUSTOM_THEME_TYPE,
|
||||
}),
|
||||
[DefaultTheme, theme],
|
||||
)
|
||||
const userCodes = useMemo(() => {
|
||||
return `const myTheme = ${JSON.stringify(deepDifferents, null, 2)}
|
||||
|
||||
@@ -48,7 +51,7 @@ const CustomizationCodes = () => {
|
||||
*
|
||||
* export const App = () => {
|
||||
* return (
|
||||
* <GeistProvider theme={myTheme}>
|
||||
* <GeistProvider themes={[myTheme]} themeType="${CUSTOM_THEME_TYPE}">
|
||||
* <CssBaseline />
|
||||
* <YourComponent />
|
||||
* </GeistProvider>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useTheme, GeistUIThemesPalette, Popover } from 'components'
|
||||
import { useTheme, GeistUIThemesPalette, Popover, Themes } from 'components'
|
||||
import { ColorResult, TwitterPicker } from 'react-color'
|
||||
import { useConfigs } from 'lib/config-context'
|
||||
import DefaultTheme from 'components/styles/themes/default'
|
||||
const DefaultTheme = Themes.getPresetStaticTheme()
|
||||
|
||||
interface Props {
|
||||
value?: string
|
||||
|
||||
@@ -3,13 +3,13 @@ import {
|
||||
Text,
|
||||
Button,
|
||||
useTheme,
|
||||
Themes,
|
||||
GeistUIThemesPalette,
|
||||
GeistUIThemesExpressiveness,
|
||||
GeistUIThemesLayout,
|
||||
} from 'components'
|
||||
import EditorColorItem from './editor-color-item'
|
||||
import EditorInputItem from './editor-input-item'
|
||||
import DefaultTheme from 'components/styles/themes/default'
|
||||
import { useConfigs } from 'lib/config-context'
|
||||
|
||||
const basicColors: Array<keyof GeistUIThemesPalette> = [
|
||||
@@ -71,6 +71,7 @@ const gapLayout: Array<keyof GeistUIThemesLayout> = [
|
||||
|
||||
const Editor = () => {
|
||||
const theme = useTheme()
|
||||
const DefaultTheme = Themes.getPresetStaticTheme()
|
||||
const { updateCustomTheme, isChinese } = useConfigs()
|
||||
|
||||
const resetLayout = () => updateCustomTheme({ layout: DefaultTheme.layout })
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GeistUIThemesPalette } from 'components/styles/themes'
|
||||
import { GeistUIThemesPalette } from 'components/themes/presets'
|
||||
|
||||
export type ColorEnum = {
|
||||
[key in keyof GeistUIThemesPalette]?: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import withDefaults from 'components/utils/with-defaults'
|
||||
import useTheme from 'components/use-theme'
|
||||
import { GeistUIThemes } from 'components/styles/themes'
|
||||
import { GeistUIThemes } from 'components/themes/presets'
|
||||
|
||||
interface Props {
|
||||
plain?: number | boolean
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PrismTheme } from 'prism-react-renderer'
|
||||
import { GeistUIThemes } from 'components/styles/themes'
|
||||
import { GeistUIThemes } from 'components/themes/presets'
|
||||
|
||||
const makeCodeTheme = (theme: GeistUIThemes): PrismTheme => ({
|
||||
plain: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { GeistUIThemes } from 'components/styles/themes'
|
||||
import { DeepPartial } from 'components/utils/types'
|
||||
import { GeistUIThemes } from 'components'
|
||||
|
||||
export interface Configs {
|
||||
onThemeChange?: (themes: DeepPartial<GeistUIThemes>) => void
|
||||
@@ -14,6 +14,7 @@ export interface Configs {
|
||||
|
||||
customTheme: DeepPartial<GeistUIThemes>
|
||||
updateCustomTheme: (theme: DeepPartial<GeistUIThemes>) => void
|
||||
switchTheme: (type: string) => void
|
||||
}
|
||||
|
||||
export const defaultConfigs: Configs = {
|
||||
@@ -27,6 +28,7 @@ export const defaultConfigs: Configs = {
|
||||
customTheme: {},
|
||||
updateCustomTheme: () => {},
|
||||
onThemeChange: () => {},
|
||||
switchTheme: () => {},
|
||||
}
|
||||
|
||||
export const ConfigContext = React.createContext<Configs>(defaultConfigs)
|
||||
|
||||
@@ -3,13 +3,13 @@ import withDefaults from 'components/utils/with-defaults'
|
||||
import { ConfigContext, Configs } from 'lib/config-context'
|
||||
import { useRouter } from 'next/router'
|
||||
import { DeepPartial } from 'components/utils/types'
|
||||
import { GeistUIThemes } from 'components/styles/themes'
|
||||
import { deepMergeObject } from 'components/styles/theme-provider/theme-provider'
|
||||
import useCurrentState from 'components/utils/use-current-state'
|
||||
import { GeistUIThemes, Themes } from 'components'
|
||||
import { useTheme } from 'components'
|
||||
import { CHINESE_LANGUAGE_IDENT, CUSTOM_THEME_TYPE } from './constants'
|
||||
|
||||
interface Props {
|
||||
onThemeChange?: (themes: DeepPartial<GeistUIThemes>) => void
|
||||
onThemeTypeChange?: (type: string) => void
|
||||
}
|
||||
|
||||
const defaultProps = {}
|
||||
@@ -17,24 +17,27 @@ const defaultProps = {}
|
||||
export type ConfigProviderProps = Props & typeof defaultProps
|
||||
|
||||
const ConfigProvider: React.FC<React.PropsWithChildren<ConfigProviderProps>> = React.memo(
|
||||
({ onThemeChange, children }) => {
|
||||
({ onThemeChange, onThemeTypeChange, children }) => {
|
||||
const theme = useTheme()
|
||||
const { pathname } = useRouter()
|
||||
const [isChinese, setIsChinese] = useState<boolean>(() => pathname.includes('zh-cn'))
|
||||
const [isChinese, setIsChinese] = useState<boolean>(() =>
|
||||
pathname.includes(CHINESE_LANGUAGE_IDENT),
|
||||
)
|
||||
const [scrollHeight, setScrollHeight] = useState<number>(0)
|
||||
const [tabbarFixed, setTabbarFixed] = useState<boolean>(false)
|
||||
const [customTheme, setCustomTheme, customThemeRef] = useCurrentState<
|
||||
DeepPartial<GeistUIThemes>
|
||||
>(theme)
|
||||
const [customTheme, setCustomTheme] = useState<GeistUIThemes>(theme)
|
||||
|
||||
const updateSidebarScrollHeight = (height: number) => setScrollHeight(height)
|
||||
const updateChineseState = (state: boolean) => setIsChinese(state)
|
||||
const updateTabbarFixed = (state: boolean) => setTabbarFixed(state)
|
||||
const updateCustomTheme = (nextTheme: DeepPartial<GeistUIThemes>) => {
|
||||
const mergedTheme = deepMergeObject(customThemeRef.current, nextTheme)
|
||||
const mergedTheme = Themes.create(theme, { ...nextTheme, type: CUSTOM_THEME_TYPE })
|
||||
setCustomTheme(mergedTheme)
|
||||
onThemeChange && onThemeChange(mergedTheme)
|
||||
}
|
||||
const switchTheme = (type: string) => {
|
||||
onThemeTypeChange && onThemeTypeChange(type)
|
||||
}
|
||||
|
||||
const initialValue = useMemo<Configs>(
|
||||
() => ({
|
||||
@@ -42,6 +45,7 @@ const ConfigProvider: React.FC<React.PropsWithChildren<ConfigProviderProps>> = R
|
||||
isChinese,
|
||||
tabbarFixed,
|
||||
customTheme,
|
||||
switchTheme,
|
||||
updateCustomTheme,
|
||||
updateTabbarFixed,
|
||||
updateChineseState,
|
||||
|
||||
9
lib/constants.ts
Normal file
9
lib/constants.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const CUSTOM_THEME_TYPE = 'Custom'
|
||||
|
||||
export const CHINESE_LANGUAGE_IDENT = 'zh-cn'
|
||||
|
||||
export const ENGLISH_LANGUAGE_IDENT = 'en-us'
|
||||
|
||||
export const GITHUB_URL = 'https://github.com/geist-org/react'
|
||||
|
||||
export const CONTRIBUTORS_URL = 'https://contributors.geist-ui.dev/api/users'
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -6,20 +6,21 @@ import { CssBaseline, GeistProvider, useTheme, GeistUIThemes } from 'components'
|
||||
import Menu from 'lib/components/menu'
|
||||
import ConfigContext from 'lib/config-provider'
|
||||
import useDomClean from 'lib/use-dom-clean'
|
||||
import { DeepPartial } from 'components/utils/types'
|
||||
import 'inter-ui/inter.css'
|
||||
|
||||
const Application: NextPage<AppProps<{}>> = ({ Component, pageProps }) => {
|
||||
const theme = useTheme()
|
||||
const [customTheme, setCustomTheme] = useState<DeepPartial<GeistUIThemes>>({})
|
||||
const themeChangeHandle = (theme: DeepPartial<GeistUIThemes>) => {
|
||||
const [themeType, setThemeType] = useState<string>()
|
||||
const [customTheme, setCustomTheme] = useState<GeistUIThemes>(theme)
|
||||
const themeChangeHandle = (theme: GeistUIThemes) => {
|
||||
setCustomTheme(theme)
|
||||
setThemeType(theme.type)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const theme = window.localStorage.getItem('theme')
|
||||
if (theme !== 'dark') return
|
||||
themeChangeHandle({ type: 'dark' })
|
||||
setThemeType('dark')
|
||||
}, [])
|
||||
useDomClean()
|
||||
|
||||
@@ -62,9 +63,11 @@ const Application: NextPage<AppProps<{}>> = ({ Component, pageProps }) => {
|
||||
content="initial-scale=1, maximum-scale=1, minimum-scale=1, viewport-fit=cover"
|
||||
/>
|
||||
</Head>
|
||||
<GeistProvider theme={customTheme}>
|
||||
<GeistProvider themeType={themeType} themes={[customTheme]}>
|
||||
<CssBaseline />
|
||||
<ConfigContext onThemeChange={themeChangeHandle}>
|
||||
<ConfigContext
|
||||
onThemeChange={themeChangeHandle}
|
||||
onThemeTypeChange={type => setThemeType(type)}>
|
||||
<Menu />
|
||||
<Component {...pageProps} />
|
||||
</ConfigContext>
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document'
|
||||
import Document, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
|
||||
import { CssBaseline } from 'components'
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const initialProps = await Document.getInitialProps(ctx)
|
||||
const styles = CssBaseline.flush()
|
||||
|
||||
return {
|
||||
...initialProps,
|
||||
styles: (
|
||||
<>
|
||||
{initialProps.styles}
|
||||
{styles}
|
||||
</>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
|
||||
@@ -8,7 +8,9 @@ export const meta = {
|
||||
|
||||
## Themes
|
||||
|
||||
`@geist-ui/react` now support **Dark Mode**. You can switch theme at any time through a very simple API, no third-party styles or configs.
|
||||
**Geist UI** now supports a variety of themes, and it is very easy to create or inherit modifications, no third-party styles or configs.
|
||||
|
||||
As a basic option, there are two themes available, `light` and `dark`.
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
@@ -27,20 +29,18 @@ export const meta = {
|
||||
|
||||
1. Make sure `GeistProvider` and `CssBaseline` are already on the root component.
|
||||
|
||||
2. Get the current theme of the page through hook `useTheme`.
|
||||
|
||||
3. Update the value of `theme.type`, and the theme of all components will follow automatically.
|
||||
2. Update the value of `themeType`, and the theme of all components will follow automatically.
|
||||
|
||||
```jsx
|
||||
import { CssBaseline, GeistProvider } from '@geist-ui/react'
|
||||
|
||||
const App = () => {
|
||||
const [themeType, setThemeType] = useState('dark')
|
||||
const [themeType, setThemeType] = useState('light')
|
||||
const switchThemes = () => {
|
||||
setThemeType(lastThemeType => (lastThemeType === 'dark' ? 'light' : 'dark'))
|
||||
setThemeType(last => (last === 'dark' ? 'light' : 'dark'))
|
||||
}
|
||||
return (
|
||||
<GeistProvider theme={{ type: themeType }}>
|
||||
<GeistProvider themeType={themeType}>
|
||||
<CssBaseline />
|
||||
<YourComponent onClick={switchThemes} />
|
||||
</GeistProvider>
|
||||
@@ -52,7 +52,7 @@ const App = () => {
|
||||
|
||||
### Customizing theme
|
||||
|
||||
Customizing a theme is very simple in `@geist-ui/react`, you just need to provide a new theme `Object`,
|
||||
Customizing a theme is very simple in Geist UI, you just need to provide a new theme `Object`,
|
||||
and all the components will change automatically.
|
||||
Here is <Link target="_blank" color href="https://github.com/geist-org/react/tree/master/examples/custom-themes">a complete sample project</Link> for reference.
|
||||
|
||||
@@ -64,22 +64,37 @@ Of course, if a _component_ doesn't use your customized variables, it doesn't ma
|
||||
</Note>
|
||||
|
||||
```jsx
|
||||
import { CssBaseline, GeistProvider } from '@geist-ui/react'
|
||||
import { CssBaseline, GeistProvider, Themes } from '@geist-ui/react'
|
||||
|
||||
const myTheme = {
|
||||
const myTheme1 = Themes.createFromLight({
|
||||
type: 'coolTheme',
|
||||
palette: {
|
||||
success: '#000',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const App = () => (
|
||||
<GeistProvider theme={myTheme}>
|
||||
<GeistProvider themes={[myTheme1]} themeType="coolTheme">
|
||||
<CssBaseline />
|
||||
<YourAppComponent onClick={switchThemes} />
|
||||
</GeistProvider>
|
||||
)
|
||||
```
|
||||
|
||||
Function `Themes.createFromLight` allows you to fork a new theme based on Light Theme,
|
||||
Of course, you can also create a new theme based on the dark style: `Themes.createFromDark`,
|
||||
Or create a theme based on your own theme:
|
||||
|
||||
```jsx
|
||||
const myBaseTheme = { ... }
|
||||
const myTheme2 = Themes.create(myBaseTheme, {
|
||||
type: 'myTheme2',
|
||||
palette: {
|
||||
success: '#000',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
<Spacer y={3} />
|
||||
|
||||
### View all types of Theme definitions
|
||||
@@ -104,7 +119,7 @@ const myPalette: Partial<GeistUIThemesPalette> = {
|
||||
}
|
||||
```
|
||||
|
||||
If you don't use TypeScript, to learn more about preset types, see <Link color target="_blank" href="https://github.com/geist-org/react/blob/master/components/styles/themes/index.ts">here</Link>.
|
||||
If you don't use TypeScript, to learn more about preset types, see <Link color target="_blank" href="https://github.com/geist-org/react/blob/master/components/themes/presets/index.ts">here</Link>.
|
||||
|
||||
<Spacer y={3} />
|
||||
|
||||
@@ -162,4 +177,19 @@ const MyComponent = () => {
|
||||
}
|
||||
```
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
#### Themes APIs
|
||||
|
||||
`Themes` contains some static methods that are useful when working with custom themes:
|
||||
|
||||
- `Themes.create` - create a new theme object.
|
||||
- `Themes.createFromDark` - create a new theme object based on Dark Theme.
|
||||
- `Themes.createFromLight` - create a new theme object based on Light Theme.
|
||||
- `Themes.isPresetTheme` - Check if a theme is the base of Geist UI.
|
||||
- `Themes.isAvailableThemeType` - Check if the name of the theme is available.
|
||||
- `Themes.hasUserCustomTheme` - Check if a list of themes has a custom.
|
||||
- `Themes.getPresets` - Get a default list of themes.
|
||||
- `Themes.getPresetStaticTheme` - Get the theme loaded by Geist UI default.
|
||||
|
||||
export default ({ children }) => <Layout meta={meta}>{children}</Layout>
|
||||
|
||||
@@ -8,8 +8,9 @@ export const meta = {
|
||||
|
||||
## 主题
|
||||
|
||||
`@geist-ui/react` 现在也支持 **暗黑模式**。你可以通过非常简单的 API 随时切换主题,所有的色彩和排版细节都已内置,
|
||||
不需要任何配置与第三方库。
|
||||
**Geist UI** 现在支持多种主题,并能以简单易用的方式随意切换或是创建新的主题,不需要任何配置与第三方库。
|
||||
|
||||
作为基础的选项,Geist UI 内置了两种可用的基础主题,`light` 和 `dark` (亮色和暗色)。
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
@@ -24,24 +25,22 @@ export const meta = {
|
||||
</Note>
|
||||
<Spacer y={1} />
|
||||
|
||||
**现在你可以根据如下所示步骤切换主题:**
|
||||
**你可以参考如下步骤切换主题:**
|
||||
|
||||
1. 确保 `GeistProvider` 与 `CssBaseline` 已经添加至根节点。
|
||||
|
||||
2. (可选的) 通过钩子 `useTheme` 获取所有可用的主题变量。
|
||||
|
||||
3. 更新 `theme.type` 的值,所有的组件都会随之自动变化。
|
||||
2. 更新 `themeType` 的值,所有的组件都会随之自动变化。
|
||||
|
||||
```jsx
|
||||
import { CssBaseline, GeistProvider } from '@geist-ui/react'
|
||||
|
||||
const App = () => {
|
||||
const [themeType, setThemeType] = useState('dark')
|
||||
const [themeType, setThemeType] = useState('light')
|
||||
const switchThemes = () => {
|
||||
setThemeType(lastThemeType => (lastThemeType === 'dark' ? 'light' : 'dark'))
|
||||
setThemeType(last => (last === 'dark' ? 'light' : 'dark'))
|
||||
}
|
||||
return (
|
||||
<GeistProvider theme={{ type: themeType }}>
|
||||
<GeistProvider themeType={themeType}>
|
||||
<CssBaseline />
|
||||
<YourComponent onClick={switchThemes} />
|
||||
</GeistProvider>
|
||||
@@ -53,10 +52,10 @@ const App = () => {
|
||||
|
||||
### 自定义主题
|
||||
|
||||
自定义主题样式在 `@geist-ui/react` 中是非常简单的,你只需要提供一个新的样式对象给 `GeistProvider`,所有的组件都会自然变化以适应你自定义的属性。
|
||||
自定义主题样式在 Geist UI 中非常简单,你只需要提供一个新的样式对象给 `GeistProvider`,所有的组件都会自然变化以适应你自定义的属性。
|
||||
这里有 <Link target="_blank" color href="https://github.com/geist-org/react/tree/master/examples/custom-themes">一个完整的示例项目</Link> 可供参考。
|
||||
|
||||
当然,如果一个组件未使用到你自定义的变量,它不会发生任何变化也不会重新渲染。
|
||||
当然,如果一个组件未使用到你自定义的变量,它不会有任何变化也不会重新渲染。
|
||||
|
||||
<Spacer y={1} />
|
||||
<Note type="warning">
|
||||
@@ -64,22 +63,36 @@ const App = () => {
|
||||
</Note>
|
||||
|
||||
```jsx
|
||||
import { CssBaseline, GeistProvider } from '@geist-ui/react'
|
||||
import { CssBaseline, GeistProvider, Themes } from '@geist-ui/react'
|
||||
|
||||
const myTheme = {
|
||||
const myTheme1 = Themes.createFromLight({
|
||||
type: 'coolTheme',
|
||||
palette: {
|
||||
success: '#000',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const App = () => (
|
||||
<GeistProvider theme={myTheme}>
|
||||
<GeistProvider themes={[myTheme1]} themeType="coolTheme">
|
||||
<CssBaseline />
|
||||
<YourAppComponent onClick={switchThemes} />
|
||||
</GeistProvider>
|
||||
)
|
||||
```
|
||||
|
||||
方法 `Themes.createFromLight` 允许你在 `light` (亮色主题) 的基础上继承与创建一份新的主题,
|
||||
当然,你可以以 `dark` (暗色主题) 为基准创建主题:`Themes.createFromDark`,或是以你自己的、来自社区的主题为基础:
|
||||
|
||||
```jsx
|
||||
const myBaseTheme = { ... }
|
||||
const myTheme2 = Themes.create(myBaseTheme, {
|
||||
type: 'myTheme2',
|
||||
palette: {
|
||||
success: '#000',
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
<Spacer y={3} />
|
||||
|
||||
### 查看主题定义的所有类型
|
||||
@@ -104,7 +117,7 @@ const myPalette: Partial<GeistUIThemesPalette> = {
|
||||
}
|
||||
```
|
||||
|
||||
如果你没有使用 TypeScript,想要了解更多的预置类型,请看<Link color target="_blank" href="https://github.com/geist-org/react/blob/master/components/styles/themes/index.ts">这里的类型声明文件</Link>。
|
||||
如果你没有使用 TypeScript,想要了解更多的预置类型,请看<Link color target="_blank" href="https://github.com/geist-org/react/blob/master/components/themes/presets/index.ts">这里的类型声明文件</Link>。
|
||||
|
||||
<Spacer y={3} />
|
||||
|
||||
@@ -163,4 +176,19 @@ const MyComponent = () => {
|
||||
}
|
||||
```
|
||||
|
||||
<Spacer y={2} />
|
||||
|
||||
#### Themes APIs
|
||||
|
||||
`Themes` 包含了一些静态方法 (纯函数),这在你自定义主题时会很有用:
|
||||
|
||||
- `Themes.create` - 创建一个新主题。
|
||||
- `Themes.createFromDark` - 以暗色模式为基础创建新主题。
|
||||
- `Themes.createFromLight` - 以亮色模式为基础创建新主题。
|
||||
- `Themes.isPresetTheme` - 检查一个主题对象是否来自 Geist UI。
|
||||
- `Themes.isAvailableThemeType` - 检查一个主题名是否可用。(是否重复)
|
||||
- `Themes.hasUserCustomTheme` - 检查一组主题中是否包含自定义的主题。
|
||||
- `Themes.getPresets` - 获取一组由 Geist UI 内置的默认主题。
|
||||
- `Themes.getPresetStaticTheme` - 获取一个由 Geist UI 内置的默认主题 (默认加载的)。
|
||||
|
||||
export default ({ children }) => <Layout meta={meta}>{children}</Layout>
|
||||
|
||||
Reference in New Issue
Block a user