Skip to content

Commit 84d2026

Browse files
rilromGermanJablo
andauthored
feat: preselected theme (#8354)
This PR implements the ability to attempt to force the use of light/dark theme in the admin panel. While I am a big advocate for the benefits that dark mode can bring to UX, it does not always suit a clients branding needs. Open to discussion on whether we consider this a suitable feature for the platform. Please feel free to add to this PR as needed. TODO: - [x] Implement tests (I'm open to guidance on this from the Payload team as currently it doesn't look like it's possible to adjust the payload config file on the fly - meaning it can't be easily placed in the admin folder tests). --------- Co-authored-by: Germán Jabloñski <43938777+GermanJablo@users.noreply.github.com>
1 parent 4b0351f commit 84d2026

File tree

10 files changed

+43
-10
lines changed

10 files changed

+43
-10
lines changed

docs/admin/overview.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ The following options are available:
9898
| **`livePreview`** | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). |
9999
| **`meta`** | Base metadata to use for the Admin Panel. [More details](./metadata). |
100100
| **`routes`** | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). |
101+
| **`theme`** | Restrict the Admin Panel theme to use only one of your choice. Default is `all`.
101102
| **`user`** | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). |
102103

103104
<Banner type="success">

packages/next/src/utilities/getRequestTheme.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ type GetRequestLanguageArgs = {
1212
const acceptedThemes: Theme[] = ['dark', 'light']
1313

1414
export const getRequestTheme = ({ config, cookies, headers }: GetRequestLanguageArgs): Theme => {
15+
if (config.admin.theme !== 'all' && acceptedThemes.includes(config.admin.theme)) {
16+
return config.admin.theme
17+
}
18+
1519
const themeCookie = cookies.get(`${config.cookiePrefix || 'payload'}-theme`)
1620

1721
const themeFromCookie: Theme = (

packages/next/src/views/Account/Settings/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { I18n } from '@payloadcms/translations'
2-
import type { LanguageOptions } from 'payload'
2+
import type { Config, LanguageOptions } from 'payload'
33

44
import { FieldLabel } from '@payloadcms/ui'
55
import React from 'react'
@@ -14,8 +14,9 @@ export const Settings: React.FC<{
1414
readonly className?: string
1515
readonly i18n: I18n
1616
readonly languageOptions: LanguageOptions
17+
readonly theme: Config['admin']['theme']
1718
}> = (props) => {
18-
const { className, i18n, languageOptions } = props
19+
const { className, i18n, languageOptions, theme } = props
1920

2021
return (
2122
<div className={[baseClass, className].filter(Boolean).join(' ')}>
@@ -24,7 +25,7 @@ export const Settings: React.FC<{
2425
<FieldLabel field={null} htmlFor="language-select" label={i18n.t('general:language')} />
2526
<LanguageSelector languageOptions={languageOptions} />
2627
</div>
27-
<ToggleTheme />
28+
{theme === 'all' && <ToggleTheme />}
2829
</div>
2930
)
3031
}

packages/next/src/views/Account/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export const Account: React.FC<AdminViewProps> = async ({
3838
} = initPageResult
3939

4040
const {
41-
admin: { components: { views: { Account: CustomAccountComponent } = {} } = {}, user: userSlug },
41+
admin: {
42+
components: { views: { Account: CustomAccountComponent } = {} } = {},
43+
theme,
44+
user: userSlug,
45+
},
4246
routes: { api },
4347
serverURL,
4448
} = config
@@ -85,7 +89,7 @@ export const Account: React.FC<AdminViewProps> = async ({
8589

8690
return (
8791
<DocumentInfoProvider
88-
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} />}
92+
AfterFields={<Settings i18n={i18n} languageOptions={languageOptions} theme={theme} />}
8993
apiURL={`${serverURL}${api}/${userSlug}${user?.id ? `/${user.id}` : ''}`}
9094
collectionSlug={userSlug}
9195
docPermissions={docPermissions}

packages/payload/src/config/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const defaults: Omit<Config, 'db' | 'editor' | 'secret'> = {
2525
reset: '/reset',
2626
unauthorized: '/unauthorized',
2727
},
28+
theme: 'all',
2829
},
2930
bin: [],
3031
collections: [],

packages/payload/src/config/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,12 @@ export type Config = {
818818
/** The route for the unauthorized page. */
819819
unauthorized?: string
820820
}
821+
/**
822+
* Restrict the Admin Panel theme to use only one of your choice
823+
*
824+
* @default 'all' // The theme can be configured by users
825+
*/
826+
theme?: 'all' | 'dark' | 'light'
821827
/** The slug of a Collection that you want to be used to log in to the Admin dashboard. */
822828
user?: string
823829
}

packages/ui/src/providers/Root/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const RootProvider: React.FC<Props> = ({
8888
<ModalProvider classPrefix="payload" transTime={0} zIndex="var(--z-modal)">
8989
<AuthProvider permissions={permissions} user={user}>
9090
<PreferencesProvider>
91-
<ThemeProvider cookiePrefix={config.cookiePrefix} theme={theme}>
91+
<ThemeProvider theme={theme}>
9292
<ParamsProvider>
9393
<LocaleProvider>
9494
<StepNavProvider>

packages/ui/src/providers/Theme/index.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use client'
22
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'
33

4+
import { useConfig } from '../Config/index.js'
5+
46
export type Theme = 'dark' | 'light'
57

68
export type ThemeContext = {
@@ -55,20 +57,26 @@ export const defaultTheme = 'light'
5557

5658
export const ThemeProvider: React.FC<{
5759
children?: React.ReactNode
58-
cookiePrefix?: string
5960
theme?: Theme
60-
}> = ({ children, cookiePrefix, theme: initialTheme }) => {
61-
const cookieKey = `${cookiePrefix || 'payload'}-theme`
61+
}> = ({ children, theme: initialTheme }) => {
62+
const { config } = useConfig()
63+
64+
const preselectedTheme = config.admin.theme
65+
const cookieKey = `${config.cookiePrefix || 'payload'}-theme`
6266

6367
const [theme, setThemeState] = useState<Theme>(initialTheme || defaultTheme)
6468

6569
const [autoMode, setAutoMode] = useState<boolean>()
6670

6771
useEffect(() => {
72+
if (preselectedTheme !== 'all') {
73+
return
74+
}
75+
6876
const { theme, themeFromCookies } = getTheme(cookieKey)
6977
setThemeState(theme)
7078
setAutoMode(!themeFromCookies)
71-
}, [cookieKey])
79+
}, [preselectedTheme, cookieKey])
7280

7381
const setTheme = useCallback(
7482
(themeToSet: 'auto' | Theme) => {

test/admin-root/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default buildConfigWithDefaults({
2121
importMap: {
2222
baseDir: path.resolve(dirname),
2323
},
24+
theme: 'dark',
2425
},
2526
cors: ['http://localhost:3000', 'http://localhost:3001'],
2627
globals: [MenuGlobal],

test/admin-root/e2e.spec.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,11 @@ test.describe('Admin Panel (Root)', () => {
9898
await expect(favicons.nth(1)).toHaveAttribute('media', '(prefers-color-scheme: dark)')
9999
await expect(favicons.nth(1)).toHaveAttribute('href', /\/payload-favicon-light\.[a-z\d]+\.png/)
100100
})
101+
102+
test('config.admin.theme should restrict the theme', async () => {
103+
await page.goto(url.account)
104+
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark')
105+
await expect(page.locator('#field-theme')).toBeHidden()
106+
await expect(page.locator('#field-theme-auto')).toBeHidden()
107+
})
101108
})

0 commit comments

Comments
 (0)