chore(ui): migrates theme provider
This commit is contained in:
1
packages/dev/.env.example
Normal file
1
packages/dev/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
PAYLOAD_SECRET=ls3nd09adff9cmlq71j
|
||||
@@ -3,6 +3,8 @@ import { RootProvider } from '@payloadcms/ui/providers'
|
||||
import { SanitizedConfig } from 'payload/types'
|
||||
import { createClientConfig } from '../../createClientConfig'
|
||||
|
||||
import '@payloadcms/ui/scss/app.scss'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Next.js',
|
||||
description: 'Generated by Next.js',
|
||||
|
||||
@@ -3,21 +3,24 @@ import React from 'react'
|
||||
import { RenderCustomComponent } from '@payloadcms/ui/elements'
|
||||
import { DefaultDashboard } from '@payloadcms/ui/views'
|
||||
|
||||
export const Dashboard = ({ config: configPromise }: { config: Promise<SanitizedConfig> }) =>
|
||||
async function () {
|
||||
const config = await configPromise
|
||||
export const Dashboard = async ({
|
||||
config: configPromise,
|
||||
}: {
|
||||
config: Promise<SanitizedConfig>
|
||||
}) => {
|
||||
const config = await configPromise
|
||||
|
||||
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
|
||||
const CustomDashboardComponent = config.admin.components?.views?.Dashboard
|
||||
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={
|
||||
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined
|
||||
}
|
||||
DefaultComponent={DefaultDashboard}
|
||||
componentProps={{
|
||||
config,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<RenderCustomComponent
|
||||
CustomComponent={
|
||||
typeof CustomDashboardComponent === 'function' ? CustomDashboardComponent : undefined
|
||||
}
|
||||
DefaultComponent={DefaultDashboard}
|
||||
componentProps={{
|
||||
config,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ import { afterChange } from '../../fields/hooks/afterChange'
|
||||
import { afterRead } from '../../fields/hooks/afterRead'
|
||||
import { beforeChange } from '../../fields/hooks/beforeChange'
|
||||
import { beforeValidate } from '../../fields/hooks/beforeValidate'
|
||||
import { generateFileData } from '../../uploads/generateFileData'
|
||||
import { unlinkTempFiles } from '../../uploads/unlinkTempFiles'
|
||||
import { uploadFiles } from '../../uploads/uploadFiles'
|
||||
// import { generateFileData } from '../../uploads/generateFileData'
|
||||
// import { unlinkTempFiles } from '../../uploads/unlinkTempFiles'
|
||||
// import { uploadFiles } from '../../uploads/uploadFiles' TODO: this was temporarily commented out bc it throws Sharp compilation errors in Next.js
|
||||
import { commitTransaction } from '../../utilities/commitTransaction'
|
||||
import { initTransaction } from '../../utilities/initTransaction'
|
||||
import { killTransaction } from '../../utilities/killTransaction'
|
||||
@@ -123,17 +123,18 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// Generate data for all files and sizes
|
||||
// /////////////////////////////////////
|
||||
|
||||
const { data: newFileData, files: filesToUpload } = await generateFileData({
|
||||
collection,
|
||||
config,
|
||||
data,
|
||||
overwriteExistingFiles,
|
||||
req,
|
||||
throwOnMissingFile:
|
||||
!shouldSaveDraft && collection.config.upload.filesRequiredOnCreate !== false,
|
||||
})
|
||||
// TODO: this was temporarily commented out bc it throws Sharp compilation errors in Next.js
|
||||
// const { data: newFileData, files: filesToUpload } = await generateFileData({
|
||||
// collection,
|
||||
// config,
|
||||
// data,
|
||||
// overwriteExistingFiles,
|
||||
// req,
|
||||
// throwOnMissingFile:
|
||||
// !shouldSaveDraft && collection.config.upload.filesRequiredOnCreate !== false,
|
||||
// })
|
||||
|
||||
data = newFileData
|
||||
// data = newFileData
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeValidate - Fields
|
||||
@@ -208,7 +209,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!collectionConfig.upload.disableLocalStorage) {
|
||||
await uploadFiles(payload, filesToUpload, req.t)
|
||||
// await uploadFiles(payload, filesToUpload, req.t) // TODO: this was temporarily commented out bc it throws Sharp compilation errors in Next.js
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
@@ -356,7 +357,7 @@ async function create<TSlug extends keyof GeneratedTypes['collections']>(
|
||||
result,
|
||||
})
|
||||
|
||||
await unlinkTempFiles({ collectionConfig, config, req })
|
||||
// await unlinkTempFiles({ collectionConfig, config, req }) // TODO: this was temporarily commented out bc it throws Sharp compilation errors in Next.js
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
|
||||
@@ -20,7 +20,6 @@ import type {
|
||||
Validate,
|
||||
} from './config/types'
|
||||
|
||||
import canUseDOM from '../utilities/canUseDOM'
|
||||
import { getIDType } from '../utilities/getIDType'
|
||||
import { isNumber } from '../utilities/isNumber'
|
||||
import { isValidID } from '../utilities/isValidID'
|
||||
@@ -53,7 +52,7 @@ export const text: Validate<unknown, unknown, TextField> = (
|
||||
|
||||
export const password: Validate<unknown, unknown, TextField> = (
|
||||
value: string,
|
||||
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
|
||||
{ config, maxLength: fieldMaxLength, minLength, required, t },
|
||||
) => {
|
||||
let maxLength: number
|
||||
|
||||
@@ -85,7 +84,7 @@ export const email: Validate<unknown, unknown, EmailField> = (value: string, { r
|
||||
|
||||
export const textarea: Validate<unknown, unknown, TextareaField> = (
|
||||
value: string,
|
||||
{ config, maxLength: fieldMaxLength, minLength, payload, required, t },
|
||||
{ config, maxLength: fieldMaxLength, minLength, required, t },
|
||||
) => {
|
||||
let maxLength: number
|
||||
|
||||
@@ -248,7 +247,7 @@ const validateFilterOptions: Validate = async (
|
||||
value,
|
||||
{ id, data, filterOptions, payload, relationTo, req, siblingData, t, user },
|
||||
) => {
|
||||
if (!canUseDOM && typeof filterOptions !== 'undefined' && value) {
|
||||
if (typeof filterOptions !== 'undefined' && value) {
|
||||
const options: {
|
||||
[collection: string]: (number | string)[]
|
||||
} = {}
|
||||
@@ -343,7 +342,7 @@ export const upload: Validate<unknown, unknown, UploadField> = (value: string, o
|
||||
return options.t('validation:required')
|
||||
}
|
||||
|
||||
if (!canUseDOM && typeof value !== 'undefined' && value !== null) {
|
||||
if (typeof value !== 'undefined' && value !== null) {
|
||||
const idField = options?.config?.collections
|
||||
?.find((collection) => collection.slug === options.relationTo)
|
||||
?.fields?.find((field) => fieldAffectsData(field) && field.name === 'id')
|
||||
@@ -378,7 +377,7 @@ export const relationship: Validate<unknown, unknown, RelationshipField> = async
|
||||
}
|
||||
}
|
||||
|
||||
if (!canUseDOM && typeof value !== 'undefined' && value !== null) {
|
||||
if (typeof value !== 'undefined' && value !== null) {
|
||||
const values = Array.isArray(value) ? value : [value]
|
||||
|
||||
const invalidRelationships = values.filter((val) => {
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"import": "./src/exports/*.ts",
|
||||
"require": "./src/exports/*.ts",
|
||||
"types": "./src/exports/*.ts"
|
||||
}
|
||||
},
|
||||
"./scss/app.scss": "./src/scss/app.scss"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
@@ -54,7 +55,8 @@
|
||||
"import": "./dist/exports/*.ts",
|
||||
"require": "./dist/exports/*.ts",
|
||||
"types": "./dist/exports/*.d.ts"
|
||||
}
|
||||
},
|
||||
"./scss/app.scss": "./dist/scss/app.scss"
|
||||
},
|
||||
"main": "./dist/index.js",
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import type { Locale } from 'payload/config'
|
||||
|
||||
import { findLocaleFromCode } from 'payload/utilities' // TODO: import this from 'payload/utilities' when it's available
|
||||
import { findLocaleFromCode } from 'payload/dist/utilities/findLocaleFromCode' // TODO: import this from 'payload/utilities' when it's available
|
||||
import { useAuth } from '../Auth'
|
||||
import { useConfig } from '../Config'
|
||||
import { usePreferences } from '../Preferences'
|
||||
|
||||
@@ -13,7 +13,6 @@ import { LocaleProvider } from '../Locale'
|
||||
import StepNav, { StepNavProvider } from '../../elements/StepNav'
|
||||
import { LoadingOverlayProvider } from '../../elements/LoadingOverlay'
|
||||
import { NavProvider } from '../../elements/Nav/context'
|
||||
import '../../scss/app.scss'
|
||||
|
||||
type Props = {
|
||||
config: ClientConfig
|
||||
|
||||
@@ -1,68 +1,57 @@
|
||||
'use client'
|
||||
import React, { createContext, useCallback, useContext, useState } from 'react'
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
|
||||
export type ThemeContext = {
|
||||
autoMode: boolean
|
||||
setTheme: (theme: Theme) => void
|
||||
theme: Theme
|
||||
}
|
||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import canUseDOM from '../../utilities/canUseDOM'
|
||||
import { Theme, ThemeContext } from './types'
|
||||
import { getImplicitPreference, themeIsValid } from './utilities'
|
||||
|
||||
const initialContext: ThemeContext = {
|
||||
autoMode: true,
|
||||
setTheme: () => null,
|
||||
theme: 'light',
|
||||
}
|
||||
|
||||
const Context = createContext(initialContext)
|
||||
|
||||
const localStorageKey = 'payload-theme'
|
||||
export const themeLocalStorageKey = 'payload-theme'
|
||||
|
||||
const getTheme = () => {
|
||||
let theme: Theme
|
||||
const themeFromStorage = window.localStorage.getItem(localStorageKey)
|
||||
|
||||
if (themeFromStorage === 'light' || themeFromStorage === 'dark') {
|
||||
theme = themeFromStorage
|
||||
} else {
|
||||
theme =
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
return theme
|
||||
}
|
||||
export const defaultTheme = 'light'
|
||||
|
||||
export const ThemeProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
|
||||
const [theme, setThemeState] = useState<Theme>(getTheme)
|
||||
const [autoMode, setAutoMode] = useState(() => {
|
||||
const themeFromStorage = window.localStorage.getItem(localStorageKey)
|
||||
return !themeFromStorage
|
||||
})
|
||||
const [theme, setThemeState] = useState<Theme | undefined>(
|
||||
canUseDOM ? (document.documentElement.getAttribute('data-theme') as Theme) : undefined,
|
||||
)
|
||||
|
||||
const setTheme = useCallback((themeToSet: 'auto' | Theme) => {
|
||||
if (themeToSet === 'light' || themeToSet === 'dark') {
|
||||
const setTheme = useCallback((themeToSet: Theme | null) => {
|
||||
if (themeToSet === null) {
|
||||
window.localStorage.removeItem(themeLocalStorageKey)
|
||||
const implicitPreference = getImplicitPreference()
|
||||
document.documentElement.setAttribute('data-theme', implicitPreference || '')
|
||||
if (implicitPreference) setThemeState(implicitPreference)
|
||||
} else {
|
||||
setThemeState(themeToSet)
|
||||
setAutoMode(false)
|
||||
window.localStorage.setItem(localStorageKey, themeToSet)
|
||||
window.localStorage.setItem(themeLocalStorageKey, themeToSet)
|
||||
document.documentElement.setAttribute('data-theme', themeToSet)
|
||||
} else if (themeToSet === 'auto') {
|
||||
const existingThemeFromStorage = window.localStorage.getItem(localStorageKey)
|
||||
if (existingThemeFromStorage) window.localStorage.removeItem(localStorageKey)
|
||||
const themeFromOS =
|
||||
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light'
|
||||
document.documentElement.setAttribute('data-theme', themeFromOS)
|
||||
setAutoMode(true)
|
||||
setThemeState(themeFromOS)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <Context.Provider value={{ autoMode, setTheme, theme }}>{children}</Context.Provider>
|
||||
useEffect(() => {
|
||||
let themeToSet: Theme = defaultTheme
|
||||
const preference = window.localStorage.getItem(themeLocalStorageKey)
|
||||
|
||||
if (themeIsValid(preference)) {
|
||||
themeToSet = preference
|
||||
} else {
|
||||
const implicitPreference = getImplicitPreference()
|
||||
|
||||
if (implicitPreference) {
|
||||
themeToSet = implicitPreference
|
||||
}
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-theme', themeToSet)
|
||||
setThemeState(themeToSet)
|
||||
}, [])
|
||||
|
||||
return <Context.Provider value={{ setTheme, theme }}>{children}</Context.Provider>
|
||||
}
|
||||
|
||||
export const useTheme = (): ThemeContext => useContext(Context)
|
||||
|
||||
6
packages/ui/src/providers/Theme/types.ts
Normal file
6
packages/ui/src/providers/Theme/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type Theme = 'dark' | 'light'
|
||||
|
||||
export type ThemeContext = {
|
||||
setTheme: (theme: Theme) => void
|
||||
theme: Theme
|
||||
}
|
||||
17
packages/ui/src/providers/Theme/utilities.ts
Normal file
17
packages/ui/src/providers/Theme/utilities.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Theme } from './types'
|
||||
|
||||
export function themeIsValid(string: string | null): string is Theme {
|
||||
return string ? ['light', 'dark'].includes(string) : false
|
||||
}
|
||||
|
||||
export const getImplicitPreference = (): Theme | null => {
|
||||
const mediaQuery = '(prefers-color-scheme: dark)'
|
||||
const mql = window.matchMedia(mediaQuery)
|
||||
const hasImplicitPreference = typeof mql.matches === 'boolean'
|
||||
|
||||
if (hasImplicitPreference) {
|
||||
return mql.matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -68,6 +68,12 @@ html {
|
||||
@extend %body;
|
||||
background: var(--theme-bg);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
opacity: 0;
|
||||
|
||||
&[data-theme='dark'],
|
||||
&[data-theme='light'] {
|
||||
opacity: initial;
|
||||
}
|
||||
|
||||
&[data-theme='dark'] {
|
||||
--theme-bg: var(--theme-elevation-0);
|
||||
|
||||
@@ -21,11 +21,11 @@ export const DefaultDashboard: React.FC<{
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<Gutter className={`${baseClass}__wrap`}>
|
||||
{/* {Array.isArray(beforeDashboard) &&
|
||||
beforeDashboard.map((Component, i) => <Component key={i} />)} */}
|
||||
{/* <DefaultDashboardClient /> */}
|
||||
{/* {Array.isArray(afterDashboard) &&
|
||||
afterDashboard.map((Component, i) => <Component key={i} />)} */}
|
||||
{Array.isArray(beforeDashboard) &&
|
||||
beforeDashboard.map((Component, i) => <Component key={i} />)}
|
||||
<DefaultDashboardClient />
|
||||
{Array.isArray(afterDashboard) &&
|
||||
afterDashboard.map((Component, i) => <Component key={i} />)}
|
||||
</Gutter>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": [
|
||||
"src/scss/app.scss",
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.d.ts",
|
||||
|
||||
Reference in New Issue
Block a user