@kilivi/payloadcms-theme-management 1.1.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +318 -847
  2. package/dist/components/AppearanceSessionControls.d.ts +15 -0
  3. package/dist/components/AppearanceSessionControls.d.ts.map +1 -0
  4. package/dist/components/AppearanceSessionControls.js +152 -0
  5. package/dist/components/AppearanceSessionControls.js.map +1 -0
  6. package/dist/components/ColorModeToggle.css +42 -0
  7. package/dist/components/ColorModeToggle.d.ts +40 -0
  8. package/dist/components/ColorModeToggle.d.ts.map +1 -0
  9. package/dist/components/ColorModeToggle.js +111 -0
  10. package/dist/components/ColorModeToggle.js.map +1 -0
  11. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  12. package/dist/components/ServerThemeInjector.js +41 -14
  13. package/dist/components/ServerThemeInjector.js.map +1 -1
  14. package/dist/components/ThemeLivePreviewSync.d.ts +1 -0
  15. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  16. package/dist/components/ThemeLivePreviewSync.js +1 -1
  17. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  18. package/dist/components/ThemeSwitcher.d.ts +36 -0
  19. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  20. package/dist/components/ThemeSwitcher.js +89 -0
  21. package/dist/components/ThemeSwitcher.js.map +1 -0
  22. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  23. package/dist/components/typographyPreviewUtils.js +8 -1
  24. package/dist/components/typographyPreviewUtils.js.map +1 -1
  25. package/dist/constants/devicePreviews.d.ts +35 -0
  26. package/dist/constants/devicePreviews.d.ts.map +1 -0
  27. package/dist/constants/devicePreviews.js +43 -0
  28. package/dist/constants/devicePreviews.js.map +1 -0
  29. package/dist/constants/themeFonts.js +9 -9
  30. package/dist/constants/themeFonts.js.map +1 -1
  31. package/dist/extended-presets.d.ts +0 -4
  32. package/dist/extended-presets.d.ts.map +1 -1
  33. package/dist/extended-presets.js +1 -18
  34. package/dist/extended-presets.js.map +1 -1
  35. package/dist/extended-themes.d.ts.map +1 -1
  36. package/dist/extended-themes.js +428 -0
  37. package/dist/extended-themes.js.map +1 -1
  38. package/dist/fields/AccessibilityAuditField.d.ts +2 -0
  39. package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
  40. package/dist/fields/AccessibilityAuditField.js +303 -0
  41. package/dist/fields/AccessibilityAuditField.js.map +1 -0
  42. package/dist/fields/AppearancePreviewField.d.ts +2 -0
  43. package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
  44. package/dist/fields/AppearancePreviewField.js +763 -0
  45. package/dist/fields/AppearancePreviewField.js.map +1 -0
  46. package/dist/fields/ColorPickerField.d.ts.map +1 -1
  47. package/dist/fields/ColorPickerField.js +89 -85
  48. package/dist/fields/ColorPickerField.js.map +1 -1
  49. package/dist/fields/FontSelectField.d.ts +6 -0
  50. package/dist/fields/FontSelectField.d.ts.map +1 -1
  51. package/dist/fields/FontSelectField.js +195 -122
  52. package/dist/fields/FontSelectField.js.map +1 -1
  53. package/dist/fields/PaletteGeneratorField.d.ts +7 -0
  54. package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
  55. package/dist/fields/PaletteGeneratorField.js +327 -0
  56. package/dist/fields/PaletteGeneratorField.js.map +1 -0
  57. package/dist/fields/StylePresetField.d.ts +3 -0
  58. package/dist/fields/StylePresetField.d.ts.map +1 -0
  59. package/dist/fields/StylePresetField.js +462 -0
  60. package/dist/fields/StylePresetField.js.map +1 -0
  61. package/dist/fields/ThemeColorPickerField.css +115 -35
  62. package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
  63. package/dist/fields/ThemeColorPickerField.js +204 -70
  64. package/dist/fields/ThemeColorPickerField.js.map +1 -1
  65. package/dist/fields/ThemeExportField.d.ts +6 -0
  66. package/dist/fields/ThemeExportField.d.ts.map +1 -0
  67. package/dist/fields/ThemeExportField.js +202 -0
  68. package/dist/fields/ThemeExportField.js.map +1 -0
  69. package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
  70. package/dist/fields/ThemePresetImportField.js +56 -25
  71. package/dist/fields/ThemePresetImportField.js.map +1 -1
  72. package/dist/fields/ThemePreviewField.css +382 -5
  73. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  74. package/dist/fields/ThemePreviewField.js +268 -170
  75. package/dist/fields/ThemePreviewField.js.map +1 -1
  76. package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
  77. package/dist/fields/ThemeTokenSelectField.js +278 -80
  78. package/dist/fields/ThemeTokenSelectField.js.map +1 -1
  79. package/dist/fields/colorFieldHelpers.d.ts +90 -0
  80. package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
  81. package/dist/fields/colorFieldHelpers.js +133 -0
  82. package/dist/fields/colorFieldHelpers.js.map +1 -0
  83. package/dist/fields/colorModeFields.d.ts +1 -1
  84. package/dist/fields/colorModeFields.d.ts.map +1 -1
  85. package/dist/fields/colorModeFields.js +49 -7
  86. package/dist/fields/colorModeFields.js.map +1 -1
  87. package/dist/fields/themeConfigurationField.d.ts +8 -1
  88. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  89. package/dist/fields/themeConfigurationField.js +961 -29
  90. package/dist/fields/themeConfigurationField.js.map +1 -1
  91. package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
  92. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
  93. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
  94. package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
  95. package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
  96. package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
  97. package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
  98. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
  99. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
  100. package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
  101. package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
  102. package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
  103. package/dist/globals/ThemeSettings/config.js +76 -79
  104. package/dist/globals/ThemeSettings/config.js.map +1 -1
  105. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
  106. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
  107. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
  108. package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
  109. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
  110. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
  111. package/dist/hooks/useAppearanceSession.d.ts +50 -0
  112. package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
  113. package/dist/hooks/useAppearanceSession.js +91 -0
  114. package/dist/hooks/useAppearanceSession.js.map +1 -0
  115. package/dist/hooks/useThemeTranslations.d.ts +13 -0
  116. package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
  117. package/dist/hooks/useThemeTranslations.js +22 -0
  118. package/dist/hooks/useThemeTranslations.js.map +1 -0
  119. package/dist/i18n.d.ts +53 -0
  120. package/dist/i18n.d.ts.map +1 -0
  121. package/dist/i18n.js +53 -0
  122. package/dist/i18n.js.map +1 -0
  123. package/dist/index.d.ts +10 -4
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +130 -32
  126. package/dist/index.js.map +1 -1
  127. package/dist/payload-types.d.ts +24 -0
  128. package/dist/payload-types.d.ts.map +1 -1
  129. package/dist/payload-types.js.map +1 -1
  130. package/dist/presets.d.ts +8 -0
  131. package/dist/presets.d.ts.map +1 -1
  132. package/dist/presets.js.map +1 -1
  133. package/dist/providers/Theme/index.d.ts.map +1 -1
  134. package/dist/providers/Theme/index.js +75 -11
  135. package/dist/providers/Theme/index.js.map +1 -1
  136. package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
  137. package/dist/providers/Theme/themeConfig.js +104 -0
  138. package/dist/providers/Theme/themeConfig.js.map +1 -1
  139. package/dist/providers/Theme/types.d.ts +1 -1
  140. package/dist/providers/Theme/types.d.ts.map +1 -1
  141. package/dist/providers/Theme/types.js +10 -1
  142. package/dist/providers/Theme/types.js.map +1 -1
  143. package/dist/style-presets.d.ts +66 -0
  144. package/dist/style-presets.d.ts.map +1 -0
  145. package/dist/style-presets.js +556 -0
  146. package/dist/style-presets.js.map +1 -0
  147. package/dist/translations.d.ts +159 -1
  148. package/dist/translations.d.ts.map +1 -1
  149. package/dist/translations.js +310 -9
  150. package/dist/translations.js.map +1 -1
  151. package/dist/tweakcn-special-themes.d.ts.map +1 -1
  152. package/dist/tweakcn-special-themes.js +585 -9
  153. package/dist/tweakcn-special-themes.js.map +1 -1
  154. package/dist/types/css.d.js +3 -0
  155. package/dist/types/css.d.js.map +1 -0
  156. package/dist/types.d.ts +30 -0
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/types.js +1 -1
  159. package/dist/types.js.map +1 -1
  160. package/dist/utilities/getThemeSettings.d.ts.map +1 -1
  161. package/dist/utilities/getThemeSettings.js +2 -14
  162. package/dist/utilities/getThemeSettings.js.map +1 -1
  163. package/dist/utils/contrast.d.ts +63 -0
  164. package/dist/utils/contrast.d.ts.map +1 -0
  165. package/dist/utils/contrast.js +158 -0
  166. package/dist/utils/contrast.js.map +1 -0
  167. package/dist/utils/exportTokens.d.ts +30 -0
  168. package/dist/utils/exportTokens.d.ts.map +1 -0
  169. package/dist/utils/exportTokens.js +183 -0
  170. package/dist/utils/exportTokens.js.map +1 -0
  171. package/dist/utils/generatePalette.d.ts +46 -0
  172. package/dist/utils/generatePalette.d.ts.map +1 -0
  173. package/dist/utils/generatePalette.js +127 -0
  174. package/dist/utils/generatePalette.js.map +1 -0
  175. package/dist/utils/generateThemeCSS.d.ts.map +1 -1
  176. package/dist/utils/generateThemeCSS.js +2 -40
  177. package/dist/utils/generateThemeCSS.js.map +1 -1
  178. package/dist/utils/livePreviewThemeRuntime.d.ts +16 -2
  179. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  180. package/dist/utils/livePreviewThemeRuntime.js +37 -14
  181. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  182. package/dist/utils/localizedText.d.ts +8 -0
  183. package/dist/utils/localizedText.d.ts.map +1 -0
  184. package/dist/utils/localizedText.js +21 -0
  185. package/dist/utils/localizedText.js.map +1 -0
  186. package/dist/utils/resolveThemeConfiguration.d.ts +45 -1
  187. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  188. package/dist/utils/resolveThemeConfiguration.js +12 -5
  189. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  190. package/dist/utils/themeColors.d.ts +3 -0
  191. package/dist/utils/themeColors.d.ts.map +1 -1
  192. package/dist/utils/themeColors.js +30 -1
  193. package/dist/utils/themeColors.js.map +1 -1
  194. package/dist/utils/themeHtmlAttributes.d.ts +32 -12
  195. package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
  196. package/dist/utils/themeHtmlAttributes.js +38 -5
  197. package/dist/utils/themeHtmlAttributes.js.map +1 -1
  198. package/dist/utils/themeUtils.d.ts +3 -5
  199. package/dist/utils/themeUtils.d.ts.map +1 -1
  200. package/dist/utils/themeUtils.js +416 -6
  201. package/dist/utils/themeUtils.js.map +1 -1
  202. package/dist/utils/tweakcnConverter.d.ts +1 -3
  203. package/dist/utils/tweakcnConverter.d.ts.map +1 -1
  204. package/dist/utils/tweakcnConverter.js +1 -3
  205. package/dist/utils/tweakcnConverter.js.map +1 -1
  206. package/package.json +11 -5
  207. package/dist/components/ThemePreview.d.ts +0 -3
  208. package/dist/components/ThemePreview.d.ts.map +0 -1
  209. package/dist/components/ThemePreview.js +0 -321
  210. package/dist/components/ThemePreview.js.map +0 -1
  211. package/dist/plugin.d.ts +0 -2
  212. package/dist/plugin.d.ts.map +0 -1
  213. package/dist/plugin.js +0 -3
  214. package/dist/plugin.js.map +0 -1
package/README.md CHANGED
@@ -1,117 +1,98 @@
1
- # Theme Management Plugin for Payload CMS v3
1
+ # @kilivi/payloadcms-theme-management
2
2
 
3
- A comprehensive theme management plugin for Payload CMS v3 that provides powerful theming capabilities with SSR support, preventing FOUC (Flash of Unstyled Content).
3
+ Theme management for Payload CMS v3: 60+ presets, a live preview inside the admin, an accessibility audit, and theme CSS variables that are rendered server-side (SSR-ready) for your Next.js front-end. It is built so that a non-technical site owner can restyle a site from the Payload admin without breaking it.
4
4
 
5
- ## 🎉 Version 1.0.2 - Production Ready!
5
+ **Repository:** [github.com/vitakili/payload-plugins → packages/theme-management](https://github.com/vitakili/payload-plugins/tree/main/packages/theme-management) · [Changelog](./CHANGELOG.md) · [Issues](https://github.com/vitakili/payload-plugins/issues)
6
6
 
7
- - **✅ Standalone Global Support** - Create separate appearance settings as a global
8
- - **✅ Auto-Populate Light/Dark Colors** - Theme selection hook automatically fills color fields
9
- - **✅ Multi-Tenant Ready** - Full support for multi-tenant applications with isolated themes
10
- - **✅ Cache Invalidation** - Automatic cache invalidation after saving appearance settings
11
- - **✅ Live Theme Preview** - Real-time theme preview in admin panel at `/admin/theme-preview`
12
- - **✅ Professional Color Picker** - Enhanced color picker with react-colorful library
13
- - **✅ shadcn/ui Compatible** - Works with https://ui.shadcn.com/themes
14
- - **✅ TweakCN Compatible** - Works with https://tweakcn.com/editor/theme
7
+ > **Package name:** the plugin is published as **`@kilivi/payloadcms-theme-management`**. Versions 1.x–2.1.x were also published under `@kilivi-dev/payloadcms-theme-management`, which is no longer updated. See [Migrating from `@kilivi-dev`](#migrating-from-kilivi-dev).
15
8
 
16
9
  ## Features
17
10
 
18
- - 🎨 **Multiple Theme Presets** - Cool, Brutal, Neon, Solar, and more
19
- - 🎯 **Auto-Populate Colors** - Theme selection automatically sets Light/Dark mode colors
20
- - 🌍 **Standalone Global** - Create separate appearance settings global (v0.6.0+)
21
- - 🖼️ **Live Theme Preview** - Real-time preview in admin panel (no configuration needed!)
22
- - 🎨 **Extended Themes** - OKLCH-based themes with full shadcn/ui token support
23
- - 🎨 **Custom Color Palette** - Full HSL color customization (19 semantic tokens)
24
- - 🔤 **Typography Control** - Google Fonts integration for headings, body, and code
25
- - 📐 **Border Radius Presets** - Sharp, Rounded, or Pill styles
26
- - 🌓 **Dark Mode Support** - Built-in light/dark/system mode toggle
27
- - 📊 **Chart Colors** - Data visualization color palette
28
- - 🚀 **SSR Theme Injection** - Zero FOUC with server-side rendering
29
- - ⚡ **Performance Optimized** - Critical CSS inlining, preload links
30
- - 🎯 **Type Safe** - Full TypeScript support
31
- - 🔧 **Highly Configurable** - Flexible plugin options
32
- - 👥 **Multi-Tenant Support** - Built-in multi-tenant capabilities
11
+ - **Theme presets:** 60+ colour themes (including tweakcn and OKLCH styles) plus visual style presets that set effects, component styles, radius and typography.
12
+ - **"Keep my" locks:** lock *Colours*, *Fonts* or *Style* so that picking a theme, applying a style or generating a palette leaves those parts untouched.
13
+ - **One-step undo:** every bulk change (theme, style, generated palette) can be undone from an *Applied: … · Undo* bar.
14
+ - **One live preview:** colours, fonts and component styles in a single site mock-up with device widths and light/dark mode. On wide admin screens it stays in view in a column on the right while you edit.
15
+ - **Palette generator:** a full light and dark palette from one brand colour or an uploaded logo.
16
+ - **WCAG audit:** a live contrast check of the palette in both modes, with a one-click *Fix* for each failing pair. Paired colour fields (for example `primary` and `primaryForeground`) also show their contrast right in the field.
17
+ - **Export and import:** W3C design tokens (JSON), Tailwind v4 `@theme` and Tailwind v3 config. Custom presets can be imported as JSON and appear in the theme picker straight away.
18
+ - **Front-end rendering:** SSR theme injection (`ServerThemeInjector`), `<meta name="theme-color">`, cache revalidation and Payload Live Preview.
19
+ - **Visitor components:** `ColorModeToggle` (light/dark/auto) and `ThemeSwitcher`.
20
+ - **Native to Payload:** uses Payload's theme tokens, so it works in the dark admin, has keyboard-accessible pickers and respects `prefers-reduced-motion`. It ships with English and Czech admin translations, and you can add more.
33
21
 
34
22
  ## Installation
35
23
 
36
24
  ```bash
37
25
  pnpm add @kilivi/payloadcms-theme-management
38
26
  # or
39
- npm install @kilivi/payloadcms-theme-management
27
+ npm i @kilivi/payloadcms-theme-management
40
28
  # or
41
29
  yarn add @kilivi/payloadcms-theme-management
42
30
  ```
43
31
 
44
- ## Quick Start
32
+ **Peer dependencies:**
45
33
 
46
- ### 1. Add Plugin to Payload Config
34
+ | Package | Versions |
35
+ | --- | --- |
36
+ | `payload`, `@payloadcms/ui` | `^3` |
37
+ | `next` | `14` / `15` / `16` |
38
+ | `react` | `18.3+` or `19` |
39
+ | `lucide-react` | `>=0.400` |
47
40
 
48
- #### Option A: Add as Tab to Existing Collection (Default)
41
+ ## Quick start
49
42
 
50
- ```typescript
43
+ ### 1) Register the plugin
44
+
45
+ #### A) As a tab in an existing collection (default)
46
+
47
+ ```ts
51
48
  import { themeManagementPlugin } from '@kilivi/payloadcms-theme-management'
52
49
  import { buildConfig } from 'payload'
53
50
 
54
51
  export default buildConfig({
55
- // ... other config
56
-
57
52
  collections: [
58
53
  {
59
54
  slug: 'site-settings',
60
- fields: [
61
- {
62
- name: 'siteName',
63
- type: 'text',
64
- },
65
- // Theme configuration will be injected here as a tab inside this collection
66
- ],
55
+ fields: [{ name: 'siteName', type: 'text' }],
67
56
  },
68
57
  ],
69
-
70
58
  plugins: [
71
59
  themeManagementPlugin({
72
- enabled: true,
73
60
  targetCollection: 'site-settings',
74
61
  defaultTheme: 'cool',
75
- includeColorModeToggle: true,
76
- enableLogging: true,
62
+ livePreview: true,
77
63
  }),
78
64
  ],
79
65
  })
80
66
  ```
81
67
 
82
- > **Note:** Option A injects an **Appearance Settings** tab into the specified collection (`targetCollection`). If you prefer a separate global (standalone settings), use **Option B** below and set `useStandaloneCollection: true` in the plugin options.
83
-
84
- #### Option B: Create Standalone Global (Separate Settings)
68
+ #### B) As a standalone appearance global
85
69
 
86
- ```typescript
70
+ ```ts
87
71
  import { themeManagementPlugin } from '@kilivi/payloadcms-theme-management'
88
72
  import { buildConfig } from 'payload'
89
73
 
90
74
  export default buildConfig({
91
- // ... other config
92
-
93
75
  plugins: [
94
76
  themeManagementPlugin({
95
- enabled: true,
96
- useStandaloneCollection: true, // Creates a separate global
97
- standaloneCollectionSlug: 'appearance-settings', // Optional: custom slug
98
- standaloneCollectionLabel: 'Appearance Settings', // Optional: custom label
77
+ useStandaloneCollection: true,
78
+ standaloneCollectionSlug: 'appearance-settings',
79
+ standaloneCollectionLabel: 'Appearance Settings',
99
80
  defaultTheme: 'cool',
100
- includeColorModeToggle: true,
101
- enableLogging: true,
81
+ livePreview: true,
102
82
  }),
103
83
  ],
104
84
  })
105
85
  ```
106
86
 
107
- ### 2. Add Server Theme Injection (Next.js)
87
+ After adding the plugin, regenerate the admin import map:
108
88
 
109
- **⚠️ IMPORTANT:** Import from `/server` entry point!
89
+ ```bash
90
+ pnpm payload generate:importmap
91
+ ```
110
92
 
111
- #### If Using Default (Tab in existing collection):
93
+ ### 2) Inject the theme in your Next.js layout
112
94
 
113
95
  ```tsx
114
- // app/layout.tsx
115
96
  import { ServerThemeInjector } from '@kilivi/payloadcms-theme-management/server'
116
97
  import configPromise from '@payload-config'
117
98
  import { getPayload } from 'payload'
@@ -119,15 +100,14 @@ import { getPayload } from 'payload'
119
100
  export default async function RootLayout({ children }: { children: React.ReactNode }) {
120
101
  const payload = await getPayload({ config: configPromise })
121
102
 
122
- const { docs } = await payload.find({
123
- collection: 'site-settings', // Your target collection
124
- limit: 1,
103
+ const appearance = await payload.findGlobal({
104
+ slug: 'appearance-settings',
125
105
  })
126
106
 
127
107
  return (
128
108
  <html lang="en">
129
109
  <head>
130
- <ServerThemeInjector themeConfiguration={docs[0]?.themeConfiguration} />
110
+ <ServerThemeInjector themeConfiguration={appearance?.themeConfiguration} />
131
111
  </head>
132
112
  <body>{children}</body>
133
113
  </html>
@@ -135,889 +115,380 @@ export default async function RootLayout({ children }: { children: React.ReactNo
135
115
  }
136
116
  ```
137
117
 
138
- #### If Using Standalone Global:
118
+ ## The Appearance Settings tab
139
119
 
140
- ```tsx
141
- // app/layout.tsx
142
- import { ServerThemeInjector } from '@kilivi/payloadcms-theme-management/server'
143
- import configPromise from '@payload-config'
144
- import { getPayload } from 'payload'
120
+ The tab is ordered as a **presets first, fine-tuning second** workflow:
145
121
 
146
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
147
- const payload = await getPayload({ config: configPromise })
122
+ 1. **Theme selection:**
123
+ - *Keep my: Colours · Fonts · Style* locks
124
+ - the palette generator
125
+ - the collapsible theme preset list
126
+ 2. **Live preview:** one preview for colours, fonts and component styles. Once the settings group is at least 960 px wide (a container query, not the viewport width), it moves into a sticky right-hand column that stays visible while you edit anything below it.
127
+ 3. **Style preset:** visual cards that set effects, component styles, radius and typography, but never colours.
128
+ 4. **Border radius and spacing scale.**
129
+ 5. **Fine-tuning sections:** these are collapsed by default.
130
+ - Colour mode (all light and dark tokens, the WCAG audit and export)
131
+ - Typography
132
+ - Custom theme presets (JSON import)
133
+ - Visual effects
134
+ - Component styles
135
+ - Advanced (animation level, custom CSS)
148
136
 
149
- // Fetch from standalone global
150
- const appearanceSettings = await payload.findGlobal({
151
- slug: 'appearance-settings', // Your standalone global slug
152
- })
137
+ ### Locks and undo
153
138
 
154
- return (
155
- <html lang="en">
156
- <head>
157
- <ServerThemeInjector themeConfiguration={appearanceSettings?.themeConfiguration} />
158
- </head>
159
- <body>{children}</body>
160
- </html>
161
- )
162
- }
163
- ```
139
+ Each bulk action honours the locks:
164
140
 
165
- #### Cache Optimization with Next.js (Out of the Box)
141
+ | Action | Colours | Fonts | Style (effects, components, radius) |
142
+ | --- | --- | --- | --- |
143
+ | Pick a theme preset | writes colours | pins the current preset font instead of switching it | writes the theme's visual effects |
144
+ | Pick a style preset | never | writes the style's typography | writes effects, components, radius, motion and spacing |
145
+ | Generate a palette | writes colours (disabled while locked) | never | never |
166
146
 
167
- When using standalone global, the plugin automatically invalidates cache after saving appearance settings using the tag `global_appearance-settings` (or `global_{your-slug}` if you use a custom slug).
147
+ - Locks last for the current editing session. They reset on reload and nothing extra is stored in your documents.
148
+ - After each bulk action an *Applied: … · Undo* bar appears for 10 seconds. It pauses while you hover it or it has focus.
149
+ - Undo restores exactly the fields that the action changed.
168
150
 
169
- You can optimize your theme fetching by using the built-in server helper:
151
+ ## Live Preview
170
152
 
171
- ```tsx
172
- // app/layout.tsx
173
- import {
174
- createCachedThemeFetcher,
175
- ServerThemeInjector,
176
- } from '@kilivi/payloadcms-theme-management/server'
177
- import configPromise from '@payload-config'
178
- import { getPayload } from 'payload'
153
+ Payload's Live Preview points at your client pages by slug:
179
154
 
180
- const getCachedTheme = createCachedThemeFetcher({
181
- globalSlug: 'appearance-settings',
182
- revalidate: 3600,
183
- loadAppearanceSettings: async () => {
184
- const payload = await getPayload({ config: configPromise })
185
- return payload.findGlobal({
186
- slug: 'appearance-settings',
187
- })
188
- },
155
+ - the `home` slug resolves to `/`
156
+ - any other slug resolves to `/{slug}`
157
+ - a tenant query is appended when one is available
158
+
159
+ ```ts
160
+ themeManagementPlugin({
161
+ livePreview: true,
189
162
  })
163
+ ```
190
164
 
191
- export default async function RootLayout({ children }: { children: React.ReactNode }) {
192
- const themeConfiguration = await getCachedTheme()
165
+ ### Advanced setup (with the injected endpoint)
193
166
 
194
- return (
195
- <html lang="en">
196
- <head>
197
- <ServerThemeInjector themeConfiguration={themeConfiguration} />
198
- </head>
199
- <body>{children}</body>
200
- </html>
201
- )
202
- }
167
+ ```ts
168
+ themeManagementPlugin({
169
+ livePreview: {
170
+ enabled: true,
171
+ injectRoute: true,
172
+ routePath: '/theme/preview',
173
+ pageCollection: 'pages',
174
+ pageSlug: 'home',
175
+ fallbackToFirstPage: true,
176
+ tenantField: 'tenant',
177
+ tenantQueryParam: 'tenant',
178
+ breakpoints: [
179
+ { name: 'tablet', label: 'Tablet', width: 1024, height: 768 },
180
+ { name: 'desktop', label: 'Desktop', width: 1440, height: 900 },
181
+ ],
182
+ },
183
+ })
203
184
  ```
204
185
 
205
- **Benefits:**
206
-
207
- - ⚡ Theme settings are cached and reused across requests
208
- - 🔄 Cache automatically invalidates when you save appearance settings in admin
209
- - 🚀 Reduces database queries and improves performance
186
+ With `livePreview.injectRoute` enabled, the plugin adds `GET /api/theme/preview` (or your custom `routePath`).
210
187
 
211
- **Cache Tag Format:**
188
+ - **Query parameters:**
189
+ - `pageSlug`
190
+ - `previewSecret` (or `preview`)
191
+ - `tenant`
192
+ - **Secret:** read from `PREVIEW_SECRET` or `PAYLOAD_PREVIEW_SECRET`.
193
+ - **Responses:**
194
+ - `307` redirects to the resolved client page
195
+ - `401` means a secret is configured but is missing or wrong
212
196
 
213
- - Default: `global_appearance-settings`
214
- - Custom slug: `global_{standaloneCollectionSlug}`
197
+ ```txt
198
+ /api/theme/preview?pageSlug=home&previewSecret=your-secret → /
199
+ /api/theme/preview?pageSlug=home&tenant=acme&previewSecret=your-secret → /?tenant=acme
200
+ ```
215
201
 
216
- #### Injected Revalidation Route
202
+ **Testing tip:** preview is most useful when the previewed page uses many theme tokens: buttons, cards, banners, links and several heading levels. The repository's `dev-local` app includes `pnpm seed:theme`, which fills the `home` page with blocks like these.
217
203
 
218
- The plugin now injects a Payload endpoint (default: `POST /api/theme/revalidate`) that triggers the same tag/path invalidation logic used by live preview saves.
204
+ ### Multi-tenant example
219
205
 
220
206
  ```ts
221
207
  themeManagementPlugin({
222
208
  useStandaloneCollection: true,
209
+ standaloneCollectionSlug: 'appearance-settings',
210
+ livePreview: {
211
+ enabled: true,
212
+ injectRoute: true,
213
+ routePath: '/theme/preview',
214
+ pageCollection: 'pages',
215
+ pageSlug: 'home',
216
+ tenantField: 'tenant',
217
+ tenantQueryParam: 'tenant',
218
+ },
223
219
  cacheRevalidation: {
220
+ enabled: true,
221
+ injectRoute: true,
224
222
  routePath: '/theme/revalidate',
225
223
  secret: process.env.THEME_REVALIDATE_SECRET,
226
- tags: ['tenant:default'],
224
+ tags: ['tenant:acme'],
227
225
  paths: ['/'],
228
226
  },
229
227
  })
230
228
  ```
231
229
 
232
- Set `cacheRevalidation.injectRoute` to `false` if you want to keep only automatic hook-based invalidation.
230
+ See [MULTI_TENANT_GUIDE.md](./MULTI_TENANT_GUIDE.md) for shared vs per-tenant setups.
233
231
 
234
- > **Note:** The cache invalidation hook is only available for standalone global mode. When using the tab mode (default), implement your own caching strategy based on your collection structure.
232
+ ## Cache revalidation
235
233
 
236
- ### 3. Use Theme Variables in Your CSS
234
+ When `useStandaloneCollection: true`, the default cache tag is `global_{standaloneCollectionSlug}`. The plugin can inject `POST /api/theme/revalidate`:
237
235
 
238
- The plugin injects CSS custom properties you can use:
239
-
240
- ```css
241
- .my-component {
242
- background-color: hsl(var(--background));
243
- color: hsl(var(--foreground));
244
- border-radius: var(--radius);
245
- font-family: var(--font-body);
246
- }
236
+ ```ts
237
+ themeManagementPlugin({
238
+ useStandaloneCollection: true,
239
+ cacheRevalidation: {
240
+ enabled: true,
241
+ injectRoute: true,
242
+ routePath: '/theme/revalidate',
243
+ secret: process.env.THEME_REVALIDATE_SECRET,
244
+ tags: ['tenant:default'],
245
+ paths: ['/'],
246
+ },
247
+ })
247
248
  ```
248
249
 
249
- Or with Tailwind CSS:
250
+ ### Next.js server caching helper
250
251
 
251
252
  ```tsx
252
- <div className="bg-primary text-primary-foreground rounded-lg">Themed Content</div>
253
- ```
254
-
255
- ## Plugin Configuration Options
256
-
257
- | Option | Type | Default | Description |
258
- | --------------------------- | ------------------------------------- | ----------------------- | -------------------------------------------------------------------- |
259
- | `enabled` | `boolean` | `true` | Enable/disable the plugin |
260
- | `targetCollection` | `string` | `'site-settings'` | Collection slug to add theme field to (when not using standalone) |
261
- | `useStandaloneCollection` | `boolean` | `false` | Create a separate global instead of adding as a tab |
262
- | `standaloneCollectionSlug` | `string` | `'appearance-settings'` | Slug for the standalone global |
263
- | `standaloneCollectionLabel` | `string \| Record<string, string>` | `'Appearance Settings'` | Label for the standalone global (supports i18n) |
264
- | `themePresets` | `ThemePreset[]` | Built-in presets | Custom theme presets |
265
- | `defaultTheme` | `string` | `'cool'` | Default theme preset name |
266
- | `includeColorModeToggle` | `boolean` | `true` | Show light/dark mode toggle |
267
- | `includeCustomCSS` | `boolean` | `true` | Allow custom CSS injection |
268
- | `includeBrandIdentity` | `boolean` | `false` | Show brand identity fields |
269
- | `enableAdvancedFeatures` | `boolean` | `true` | Enable advanced customization |
270
- | `enableLogging` | `boolean` | `false` | Log plugin actions to console (includes cache invalidation logs) |
271
- | `livePreview` | `boolean \| LivePreviewOptions` | `true` | Enable/administer live preview URL resolution (`home` → fallback) |
272
- | `cacheRevalidation` | `boolean \| CacheRevalidationOptions` | `standalone: true` | Configure injected route + tags/paths for Next.js cache invalidation |
273
-
274
- **Cache Invalidation Note:** In standalone mode, the plugin revalidates `global_{standaloneCollectionSlug}` automatically and injects `POST /api/theme/revalidate` by default. Pair it with `createCachedThemeFetcher` from `@kilivi/payloadcms-theme-management/server` for a zero-boilerplate setup.
275
-
276
- ## Available Theme Presets
277
-
278
- - **Cool** - Professional blue theme
279
- - **Brutal** - High contrast, bold design
280
- - **Neon** - Vibrant, energetic colors
281
- - **Solar** - Warm, golden tones
282
- - **Dealership** - Automotive-inspired
283
- - **Real Estate** - Professional property theme (+ Gold, Neutral variants)
284
-
285
- ## Live Theme Preview
286
-
287
- The plugin automatically adds a **Live Preview** page to your admin panel at `/admin/theme-preview`.
288
-
289
- ### Features:
290
-
291
- - ✅ **Zero Configuration** - Works automatically when plugin is installed
292
- - ✅ **Real-time Updates** - See changes instantly as you edit theme settings
293
- - ✅ **Light/Dark Toggle** - Preview both modes side-by-side
294
- - ✅ **Component Showcase** - View cards, buttons, inputs, badges, and more
295
- - ✅ **Professional UI** - Clean, modern preview interface
296
-
297
- ### How to Access:
298
-
299
- 1. Install the plugin (see Quick Start above)
300
- 2. Navigate to `/admin/theme-preview` in your Payload admin panel
301
- 3. Open theme settings in another tab and see changes update live!
302
-
303
- The preview automatically watches your `themeConfiguration` field and displays real-time updates without any additional setup.
304
-
305
- ## Translations / i18n
306
-
307
- Extend or override translations at runtime with `registerTranslations`. The plugin merges your translations with English defaults, so missing keys fall back to English. See `docs/TRANSLATIONS.md` for details and examples.
308
-
309
- ## API Reference
310
-
311
- ### Main Entry Point
312
-
313
- Import from `@kilivi/payloadcms-theme-management` for client-safe code:
314
-
315
- ```typescript
316
- // Plugin
317
- // Client Components
318
-
319
- // Utilities
320
- import {
321
- fetchThemeConfiguration,
322
- generateThemeColorsCss,
323
- generateThemeCSS,
324
- getThemeStyles,
325
- resolveThemeConfiguration,
326
- themeManagementPlugin,
327
- ThemeProvider,
328
- } from '@kilivi/payloadcms-theme-management'
329
- // Types
330
- import type {
331
- ThemeDefaults,
332
- ThemeManagementPluginOptions,
333
- ThemePreset,
334
- } from '@kilivi/payloadcms-theme-management'
335
- ```
336
-
337
- ### Server Entry Point
338
-
339
- ⚠️ **Import from `/server` for server components only:**
340
-
341
- ```typescript
342
253
  import {
343
- getThemeCriticalCSS,
344
- getThemeCSS,
254
+ createCachedThemeFetcher,
345
255
  ServerThemeInjector,
346
256
  } from '@kilivi/payloadcms-theme-management/server'
347
- ```
348
-
349
- > **Heads up:** Legacy helpers `getThemeCSSPath` and `generateThemePreloadLinks` are still exported for backwards compatibility but now return empty strings and emit console warnings. Prefer the inline CSS utilities above.
350
-
351
- ### Subpath Exports
352
-
353
- ```typescript
354
- // Direct field imports
355
-
356
- // Direct component imports
357
- import { ThemePreview } from '@kilivi/payloadcms-theme-management/components/ThemePreview'
358
- import { ThemeColorPickerField } from '@kilivi/payloadcms-theme-management/fields/ThemeColorPickerField'
359
- import { ThemeTokenSelectField } from '@kilivi/payloadcms-theme-management/fields/ThemeTokenSelectField'
360
- ```
361
-
362
- ## Integration Examples
363
-
364
- ### With Tailwind CSS
365
-
366
- #### tailwind.config.mjs
367
-
368
- ```typescript
369
- /* eslint-disable @typescript-eslint/no-unused-vars */
370
- /* eslint-disable @typescript-eslint/no-require-imports */
371
- /* eslint-disable import/no-anonymous-default-export */
372
- import defaultTheme from 'tailwindcss/defaultTheme'
373
-
374
- /** @type {import('tailwindcss').Config} */
375
- export default {
376
- darkMode: ['class'],
377
- content: [
378
- './src/app/**/*.{ts,tsx}',
379
- './src/components/**/*.{ts,tsx}',
380
- './src/collections/**/*.{ts,tsx}',
381
- './src/providers/**/*.{ts,tsx}',
382
- './src/heros/**/*.{ts,tsx}',
383
- ],
384
- safelist: [
385
- // Grid columns
386
- 'col-span-4',
387
- 'md:col-span-2',
388
- 'lg:col-span-4',
389
- 'lg:col-span-6',
390
- 'lg:col-span-8',
391
- 'lg:col-span-12',
392
- // Border colors
393
- 'border-border',
394
- 'border-error',
395
- 'border-success',
396
- 'border-warning',
397
- // Background colors
398
- 'bg-card',
399
- 'bg-error/30',
400
- 'bg-success/30',
401
- 'bg-warning/30',
402
- ],
403
- theme: {
404
- container: {
405
- center: true,
406
- padding: {
407
- DEFAULT: 'var(--layout-containerPadding)',
408
- md: 'var(--layout-containerPaddingTablet)',
409
- lg: 'var(--layout-containerPaddingDesktop)',
410
- },
411
- screens: {
412
- sm: '640px',
413
- md: '768px',
414
- lg: '1024px',
415
- xl: '1280px',
416
- },
417
- },
418
- extend: {
419
- colors: {
420
- background: 'var(--background)',
421
- foreground: 'var(--foreground)',
422
- card: 'var(--card)',
423
- 'card-foreground': 'var(--card-foreground)',
424
- popover: 'var(--popover)',
425
- 'popover-foreground': 'var(--popover-foreground)',
426
- primary: 'var(--primary)',
427
- 'primary-foreground': 'var(--primary-foreground)',
428
- secondary: 'var(--secondary)',
429
- 'secondary-foreground': 'var(--secondary-foreground)',
430
- muted: 'var(--muted)',
431
- 'muted-foreground': 'var(--muted-foreground)',
432
- accent: 'var(--accent)',
433
- 'accent-foreground': 'var(--accent-foreground)',
434
- destructive: 'var(--destructive)',
435
- 'destructive-foreground': 'var(--destructive-foreground)',
436
- border: 'var(--border)',
437
- input: 'var(--input)',
438
- ring: 'var(--ring)',
439
- success: 'var(--success)',
440
- 'success-foreground': 'var(--success-foreground)',
441
- warning: 'var(--warning)',
442
- 'warning-foreground': 'var(--warning-foreground)',
443
- error: 'var(--error)',
444
- 'error-foreground': 'var(--error-foreground)',
445
- info: 'var(--info)',
446
- 'info-foreground': 'var(--info-foreground)',
447
- },
448
- borderRadius: {
449
- sm: 'var(--radius-small)',
450
- DEFAULT: 'var(--radius-default)',
451
- md: 'var(--radius-medium)',
452
- lg: 'var(--radius-large)',
453
- },
454
- fontFamily: {
455
- sans: ['var(--typography-fontFamily)', ...defaultTheme.fontFamily.sans],
456
- heading: ['var(--typography-headingFamily)', ...defaultTheme.fontFamily.sans],
457
- mono: ['var(--font-geist-mono)', ...defaultTheme.fontFamily.mono],
458
- },
459
- fontSize: {
460
- base: ['var(--typography-baseFontSize)', { lineHeight: 'var(--typography-lineHeight)' }],
461
- },
462
- letterSpacing: {
463
- tight: 'var(--typography-letterSpacing-tight)',
464
- normal: 'var(--typography-letterSpacing-normal)',
465
- wide: 'var(--typography-letterSpacing-wide)',
466
- },
467
- fontWeight: {
468
- normal: 'var(--typography-fontWeights-normal)',
469
- medium: 'var(--typography-fontWeights-medium)',
470
- semibold: 'var(--typography-fontWeights-semibold)',
471
- bold: 'var(--typography-fontWeights-bold)',
472
- },
473
- spacing: {
474
- section: {
475
- DEFAULT: 'var(--layout-sectionSpacing)',
476
- md: 'var(--layout-sectionSpacingTablet)',
477
- lg: 'var(--layout-sectionSpacingDesktop)',
478
- },
479
- },
480
- transitionProperty: {
481
- button: 'var(--components-button-transition)',
482
- },
483
- scale: {
484
- 'button-hover': 'var(--components-button-hover-scale)',
485
- },
486
- opacity: {
487
- 'button-hover': 'var(--components-button-hover-opacity)',
488
- },
489
- boxShadow: {
490
- card: 'var(--components-card-shadow)',
491
- },
492
- padding: {
493
- card: 'var(--components-card-padding)',
494
- button: 'var(--components-button-padding)',
495
- input: 'var(--components-input-padding)',
496
- },
497
- height: {
498
- input: 'var(--components-input-height)',
499
- },
500
- typography: ({ theme }) => ({
501
- DEFAULT: {
502
- css: {
503
- '--tw-prose-body': theme('colors.foreground'),
504
- '--tw-prose-headings': theme('colors.foreground'),
505
- '--tw-prose-lead': theme('colors.muted.foreground'),
506
- '--tw-prose-links': theme('colors.primary'),
507
- '--tw-prose-bold': theme('colors.foreground'),
508
- '--tw-prose-counters': theme('colors.foreground'),
509
- '--tw-prose-bullets': theme('colors.foreground'),
510
- '--tw-prose-hr': theme('colors.border'),
511
- '--tw-prose-quotes': theme('colors.foreground'),
512
- '--tw-prose-quote-borders': theme('colors.border'),
513
- '--tw-prose-captions': theme('colors.muted.foreground'),
514
- '--tw-prose-code': theme('colors.foreground'),
515
- '--tw-prose-pre-code': theme('colors.foreground'),
516
- '--tw-prose-pre-bg': theme('colors.muted'),
517
- '--tw-prose-th-borders': theme('colors.border'),
518
- '--tw-prose-td-borders': theme('colors.border'),
519
- h1: {
520
- fontWeight: 'var(--typography-fontWeights-bold)',
521
- marginBottom: '0.25em',
522
- fontFamily: theme('fontFamily.heading'),
523
- letterSpacing: 'var(--typography-letterSpacing-tight)',
524
- },
525
- h2: {
526
- fontFamily: theme('fontFamily.heading'),
527
- fontWeight: 'var(--typography-fontWeights-semibold)',
528
- letterSpacing: 'var(--typography-letterSpacing-tight)',
529
- },
530
- h3: {
531
- fontFamily: theme('fontFamily.heading'),
532
- fontWeight: 'var(--typography-fontWeights-semibold)',
533
- letterSpacing: 'var(--typography-letterSpacing-tight)',
534
- },
535
- h4: {
536
- fontFamily: theme('fontFamily.heading'),
537
- fontWeight: 'var(--typography-fontWeights-medium)',
538
- letterSpacing: 'var(--typography-letterSpacing-normal)',
539
- },
540
- },
541
- },
542
- base: {
543
- css: [
544
- {
545
- h1: { fontSize: '2.5rem' },
546
- h2: { fontSize: '1.25rem' },
547
- },
548
- ],
549
- },
550
- md: {
551
- css: [
552
- {
553
- h1: { fontSize: '3.5rem' },
554
- h2: { fontSize: '1.5rem' },
555
- },
556
- ],
557
- },
558
- invert: {
559
- css: {
560
- '--tw-prose-body': 'var(--foreground)',
561
- '--tw-prose-headings': 'var(--foreground)',
562
- '--tw-prose-lead': 'var(--muted-foreground)',
563
- '--tw-prose-links': 'var(--primary)',
564
- '--tw-prose-bold': 'var(--foreground)',
565
- '--tw-prose-counters': 'var(--foreground)',
566
- '--tw-prose-bullets': 'var(--foreground)',
567
- '--tw-prose-hr': 'var(--border)',
568
- '--tw-prose-quotes': 'var(--foreground)',
569
- '--tw-prose-quote-borders': 'var(--border)',
570
- '--tw-prose-captions': 'var(--muted-foreground)',
571
- '--tw-prose-code': 'var(--foreground)',
572
- '--tw-prose-pre-code': 'var(--foreground)',
573
- '--tw-prose-pre-bg': 'var(--muted)',
574
- '--tw-prose-th-borders': 'var(--border)',
575
- '--tw-prose-td-borders': 'var(--border)',
576
- },
577
- },
578
- }),
579
- animation: {
580
- 'accordion-down': 'accordion-down 0.2s ease-out',
581
- 'accordion-up': 'accordion-up 0.2s ease-out',
582
- },
583
- keyframes: {
584
- 'accordion-down': {
585
- from: { height: 0 },
586
- to: { height: 'var(--radix-accordion-content-height)' },
587
- },
588
- 'accordion-up': {
589
- from: { height: 'var(--radix-accordion-content-height)' },
590
- to: { height: 0 },
591
- },
592
- },
593
- },
594
- },
595
- plugins: [require('tailwindcss-animate'), require('@tailwindcss/typography')],
596
- }
597
- ```
598
-
599
- #### globals.css
600
-
601
- ```css
602
- @import 'tailwindcss';
603
-
604
- @config '../../../../../tailwind.config.mjs';
605
-
606
- @layer base {
607
- /* Theme Configuration CSS Variables */
608
- :root {
609
- /* Font Scale Presets */
610
- --font-scale-small: 0.875;
611
- --font-scale-medium: 1;
612
- --font-scale-large: 1.125;
613
- --font-scale-xl: 1.25;
614
-
615
- /* Spacing Presets */
616
- --spacing-compact: 0.75;
617
- --spacing-comfortable: 1;
618
- --spacing-spacious: 1.25;
619
- --spacing-xl: 1.5;
620
-
621
- /* Animation Levels */
622
- --animation-none-duration: 0ms;
623
- --animation-reduced-duration: 100ms;
624
- --animation-normal-duration: 200ms;
625
- --animation-enhanced-duration: 300ms;
626
-
627
- /* Dynamic theme configuration (set by JS) */
628
- --theme-font-scale: var(--font-scale-medium);
629
- --theme-spacing: var(--spacing-comfortable);
630
- --theme-animation-level: normal;
631
-
632
- /* Apply spacing multiplier to layout */
633
- --layout-containerPadding-scaled: calc(
634
- var(--layout-containerPadding) * var(--theme-spacing, 1)
635
- );
636
- --layout-containerPaddingTablet-scaled: calc(
637
- var(--layout-containerPaddingTablet) * var(--theme-spacing, 1)
638
- );
639
- --layout-containerPaddingDesktop-scaled: calc(
640
- var(--layout-containerPaddingDesktop) * var(--theme-spacing, 1)
641
- );
642
- --layout-sectionSpacing-scaled: calc(var(--layout-sectionSpacing) * var(--theme-spacing, 1));
643
- --layout-sectionSpacingTablet-scaled: calc(
644
- var(--layout-sectionSpacingTablet) * var(--theme-spacing, 1)
645
- );
646
- --layout-sectionSpacingDesktop-scaled: calc(
647
- var(--layout-sectionSpacingDesktop) * var(--theme-spacing, 1)
648
- );
649
- --components-card-padding-scaled: calc(
650
- var(--components-card-padding) * var(--theme-spacing, 1)
651
- );
652
- --components-button-padding-scaled: var(--components-button-padding);
653
- --components-input-padding-scaled: var(--components-input-padding);
654
- }
655
-
656
- *,
657
- *::before,
658
- *::after {
659
- box-sizing: border-box;
660
- margin: 0;
661
- padding: 0;
662
- @apply border-border;
663
- }
664
-
665
- html {
666
- -webkit-text-size-adjust: 100%;
667
- font-size: calc(var(--typography-baseFontSize, 16px) * var(--theme-font-scale, 1));
668
- line-height: var(--typography-lineHeight, 1.5);
669
- font-family: var(--typography-fontFamily, system-ui, sans-serif);
670
- visibility: hidden;
671
- }
672
-
673
- /* Animation level controls */
674
- [data-animation-level='none'] * {
675
- animation-duration: var(--animation-none-duration) !important;
676
- transition-duration: var(--animation-none-duration) !important;
677
- }
678
-
679
- [data-animation-level='reduced'] * {
680
- animation-duration: var(--animation-reduced-duration) !important;
681
- transition-duration: var(--animation-reduced-duration) !important;
682
- }
683
-
684
- [data-animation-level='normal'] * {
685
- animation-duration: var(--animation-normal-duration);
686
- transition-duration: var(--animation-normal-duration);
687
- }
688
-
689
- [data-animation-level='enhanced'] * {
690
- animation-duration: var(--animation-enhanced-duration);
691
- transition-duration: var(--animation-enhanced-duration);
692
- }
693
-
694
- h1,
695
- h2,
696
- h3,
697
- h4,
698
- h5,
699
- h6 {
700
- font-family: var(
701
- --typography-headingFamily,
702
- var(--typography-fontFamily, system-ui, sans-serif)
703
- );
704
- font-weight: var(--typography-fontWeights-bold, 700);
705
- line-height: 1.2;
706
- }
707
-
708
- /* Prevent flash of unstyled content except for Storybook */
709
- :root:not([data-theme]):not([data-theme-mode]):not(.sb-show-main) {
710
- visibility: hidden;
711
- }
712
-
713
- :root[data-theme][data-theme-mode],
714
- .sb-show-main {
715
- visibility: visible;
716
- }
717
-
718
- html:not(.sb-show-main) {
719
- opacity: 0;
720
- }
721
-
722
- html[data-theme-mode='dark'],
723
- html[data-theme-mode='light'],
724
- html.sb-show-main {
725
- opacity: 1;
726
- transition: opacity 0ms;
727
- }
728
-
729
- /* Force immediate opacity when JS is disabled */
730
- html:not([data-theme-mode]) {
731
- opacity: 1;
732
- }
733
-
734
- /* Media queries for responsive layout */
735
- @media (min-width: 640px) {
736
- :root {
737
- --layout-containerPadding: var(--layout-containerPaddingTablet, 2rem);
738
- --layout-sectionSpacing: var(--layout-sectionSpacingTablet, 3rem);
739
- }
740
- }
741
-
742
- @media (min-width: 1024px) {
743
- :root {
744
- --layout-containerPadding: var(--layout-containerPaddingDesktop, 2rem);
745
- --layout-sectionSpacing: var(--layout-sectionSpacingDesktop, 4rem);
746
- }
747
- }
748
-
749
- /* Ensure dark mode styles take precedence */
750
- html[data-theme-mode='dark'] {
751
- color-scheme: dark;
752
- }
753
-
754
- /* Native View Transitions API - React 19 */
755
- @view-transition {
756
- navigation: auto;
757
- }
758
-
759
- /* Smooth fade transition for page navigation */
760
- ::view-transition-old(root),
761
- ::view-transition-new(root) {
762
- animation-duration: 0.3s;
763
- animation-timing-function: ease-in-out;
764
- }
765
-
766
- ::view-transition-old(root) {
767
- animation-name: fade-out;
768
- }
769
-
770
- ::view-transition-new(root) {
771
- animation-name: fade-in;
772
- }
773
-
774
- @keyframes fade-out {
775
- to {
776
- opacity: 0;
777
- }
778
- }
779
-
780
- @keyframes fade-in {
781
- from {
782
- opacity: 0;
783
- }
784
- }
785
-
786
- /* Respect user's reduced motion preference */
787
- @media (prefers-reduced-motion: reduce) {
788
- ::view-transition-old(root),
789
- ::view-transition-new(root) {
790
- animation-duration: 0.01ms !important;
791
- }
792
- }
793
- }
794
- ```
795
-
796
- ### Fetching Theme Configuration
797
-
798
- The recommended way to fetch theme configuration is using Payload's native API with Next.js caching:
799
-
800
- ```typescript
801
257
  import configPromise from '@payload-config'
802
258
  import { getPayload } from 'payload'
803
- import { unstable_cache } from 'next/cache'
804
259
 
805
- // Cached theme fetcher with automatic invalidation
806
- const getCachedTheme = unstable_cache(
807
- async () => {
260
+ const getCachedTheme = createCachedThemeFetcher({
261
+ globalSlug: 'appearance-settings',
262
+ revalidate: 3600,
263
+ loadAppearanceSettings: async () => {
808
264
  const payload = await getPayload({ config: configPromise })
809
- const global = await payload.findGlobal({
810
- slug: 'appearance-settings', // or 'your-global-slug'
811
- })
812
- return global?.themeConfiguration
265
+ return payload.findGlobal({ slug: 'appearance-settings' })
813
266
  },
814
- ['appearance-settings'],
815
- {
816
- tags: ['global_appearance-settings'],
817
- revalidate: 3600, // Revalidate every hour
818
- },
819
- )
267
+ })
820
268
 
821
- // Use in your layout
822
269
  export default async function RootLayout({ children }: { children: React.ReactNode }) {
823
270
  const themeConfiguration = await getCachedTheme()
824
- // Use themeConfiguration...
271
+
272
+ return (
273
+ <html lang="en">
274
+ <head>
275
+ <ServerThemeInjector themeConfiguration={themeConfiguration} />
276
+ </head>
277
+ <body>{children}</body>
278
+ </html>
279
+ )
825
280
  }
826
281
  ```
827
282
 
828
- **Legacy Helper (still available):**
283
+ ### Fetching the configuration
829
284
 
830
- If you need the helper function, `fetchThemeConfiguration` is still exported:
831
-
832
- ```typescript
285
+ ```ts
833
286
  import { fetchThemeConfiguration } from '@kilivi/payloadcms-theme-management'
834
287
 
835
- // Simple fetch without caching
836
- const theme = await fetchThemeConfiguration({
288
+ // Single tenant
289
+ const themeConfiguration = await fetchThemeConfiguration({
290
+ useGlobal: true,
837
291
  collectionSlug: 'appearance-settings',
292
+ })
293
+
294
+ // Multi-tenant
295
+ const tenantThemeConfiguration = await fetchThemeConfiguration({
838
296
  useGlobal: true,
297
+ collectionSlug: 'appearance-settings',
298
+ tenantSlug: 'acme',
839
299
  })
840
300
  ```
841
301
 
842
- However, prefer the native Payload API + `unstable_cache` approach for better performance and automatic invalidation support.
843
-
844
- ### Custom Theme Preset
845
-
846
- ```typescript
847
- import type { ThemePreset } from '@kilivi/payloadcms-theme-management'
302
+ ## Plugin options
303
+
304
+ | Option | Type | Default | Description |
305
+ | --- | --- | --- | --- |
306
+ | `enabled` | `boolean` | `true` | Enables or disables the plugin |
307
+ | `targetCollection` | `string` | `'site-settings'` | Collection slug for tab mode |
308
+ | `useStandaloneCollection` | `boolean` | `false` | Creates a standalone global instead of injecting a tab |
309
+ | `standaloneCollectionSlug` | `string` | `'appearance-settings'` | Global slug in standalone mode |
310
+ | `standaloneCollectionLabel` | `string \| Record<string, string>` | translated label | Label of the standalone global |
311
+ | `themePresets` | `ThemePreset[]` | built-in presets | Custom preset list |
312
+ | `defaultTheme` | `string` | `'cool'` | Default preset name |
313
+ | `includeColorModeToggle` | `boolean` | `true` | Colour mode section (light/dark tokens, audit, export) |
314
+ | `includeCustomCSS` | `boolean` | `true` | Enables the custom CSS field |
315
+ | `enableAdvancedFeatures` | `boolean` | `true` | Advanced controls such as animation level |
316
+ | `customThemeConfigurationFields` | `Field[]` | `undefined` | Extra fields appended inside the `themeConfiguration` group |
317
+ | `customThemeConfigurationSections` | `Array<{ label; fields; initCollapsed?; description? }>` | `undefined` | Extra collapsible sections inside the group |
318
+ | `enableLogging` | `boolean` | `false` | Logs plugin actions |
319
+ | `livePreview` | `boolean \| ThemeManagementLivePreviewOptions` | `true` | Live preview URL behaviour |
320
+ | `cacheRevalidation` | `boolean \| ThemeManagementCacheRevalidationOptions` | on in standalone mode | Cache invalidation endpoint, tags and paths |
321
+ | `i18n` | `ThemeManagementI18nOptions` | `undefined` | Extend or override admin translations |
322
+ | `includeBrandIdentity` | `boolean` | `false` | Reserved for future use |
323
+
324
+ ### `livePreview` options
325
+
326
+ | Option | Type | Default |
327
+ | --- | --- | --- |
328
+ | `enabled` | `boolean` | `true` |
329
+ | `injectRoute` | `boolean` | `false` |
330
+ | `routePath` | `string` | `'/theme/preview'` |
331
+ | `pageCollection` | `string` | `'pages'` |
332
+ | `pageSlug` | `string` | `'home'` |
333
+ | `fallbackToFirstPage` | `boolean` | `true` |
334
+ | `tenantField` | `string` | `'tenant'` |
335
+ | `tenantQueryParam` | `string` | `'tenant'` |
336
+ | `breakpoints` | `Array<{ name; label; width; height }>` | `undefined` |
337
+ | `url` | `(args) => string \| Promise<string>` | slug-based URL |
338
+
339
+ ### `cacheRevalidation` options
340
+
341
+ | Option | Type | Default |
342
+ | --- | --- | --- |
343
+ | `enabled` | `boolean` | `true` in standalone mode, otherwise `false` |
344
+ | `injectRoute` | `boolean` | `true` |
345
+ | `routePath` | `string` | `'/theme/revalidate'` |
346
+ | `secret` | `string` | `undefined` |
347
+ | `tags` | `string[]` | `[global_{slug}]` |
348
+ | `paths` | `string[]` | `[]` |
349
+
350
+ ## Internationalization
351
+
352
+ The plugin ships with **English (`en`)** and **Czech (`cs`)** admin translations. It registers them in Payload's native `config.i18n` under the `theme-management` namespace.
353
+
354
+ - Every admin string follows the active admin language, including field labels, the pickers, the audit, the import drawer and the font guides.
355
+ - Localised label objects (`{ en, cs, … }`) resolve to the admin language first, then to English.
356
+
357
+ To add languages or override strings, use the `i18n` option. It is deep-merged over the built-in strings, and missing keys fall back to English.
848
358
 
849
- const myTheme: ThemePreset = {
850
- name: 'my-custom-theme',
851
- label: 'My Custom Theme',
852
- colors: {
853
- primary: { h: 220, s: 70, l: 50 },
854
- secondary: { h: 180, s: 60, l: 45 },
855
- // ... other colors
856
- },
857
- borderRadius: 'rounded',
858
- typography: {
859
- heading: 'Poppins',
860
- body: 'Inter',
861
- code: 'Fira Code',
862
- },
863
- }
359
+ ```ts
360
+ import { de } from '@payloadcms/translations/languages/de'
864
361
 
865
- // Use in plugin config
866
362
  themeManagementPlugin({
867
- themePresets: [myTheme],
868
- defaultTheme: 'my-custom-theme',
363
+ i18n: {
364
+ translations: {
365
+ de: { tabLabel: 'Darstellung', ui: { lightMode: 'Heller Modus' } },
366
+ en: { tabLabel: 'Theme' }, // override a built-in string
367
+ },
368
+ supportedLanguages: { de }, // optional: register a new admin language
369
+ },
869
370
  })
870
371
  ```
871
372
 
872
- ## Migrating from Older Versions
873
-
874
- See [MIGRATION_GUIDE.md](./docs/MIGRATION_GUIDE.md) for detailed migration instructions.
875
-
876
- ### Key Changes in v0.1.9+
877
-
878
- - **Server components must import from `/server`**
879
- - Added `server-only` package to prevent client bundling errors
880
- - Removed `ServerThemeInjector` from main entry point
881
-
882
- ```diff
883
- - import { ServerThemeInjector } from '@kilivi/payloadcms-theme-management'
884
- + import { ServerThemeInjector } from '@kilivi/payloadcms-theme-management/server'
885
- ```
373
+ Full guide: [docs/TRANSLATIONS.md](./docs/TRANSLATIONS.md)
886
374
 
887
- ## Testing
375
+ ## Appearance controls
888
376
 
889
- See [TEST_APP_GUIDE.md](./docs/TEST_APP_GUIDE.md) for instructions on creating a test application.
377
+ | Section | Admin group | Fields |
378
+ | --- | --- | --- |
379
+ | Visual effects | `themeConfiguration.visualEffects` | effectStyle, shadowIntensity, backdropBlur, borderStyle, borderWidth, glassOpacity |
380
+ | Component styles | `themeConfiguration.componentStyles` | buttonVariant, buttonSize, cardStyle, cardHoverEffect, navbarStyle, footerStyle, imageStyle, linkStyle, enableScrollReveal, enableHoverAnimations |
381
+ | Typography | `themeConfiguration.typography` | bodyFont, headingFont, bodyFontCustom, headingFontCustom, baseFontSize, lineHeight |
890
382
 
891
- ## Troubleshooting
383
+ `heroBackground` (hero style, patterns, dividers) is part of the `ThemeHeroBackground` type and the CSS generator, but the tab has no admin fields for it. Add them through `customThemeConfigurationSections` if you need them.
892
384
 
893
- ### Type Conflicts: `Type 'SiteSetting' is not assignable to type 'SiteSetting'`
385
+ Full reference: [docs/APPEARANCE_CONTROLS.md](./docs/APPEARANCE_CONTROLS.md)
894
386
 
895
- **Problem:** You see errors about incompatible types even though you're passing the correct data structure.
387
+ ```tsx
388
+ import { fetchThemeConfiguration } from '@kilivi/payloadcms-theme-management'
896
389
 
897
- **Why This Happens:** Your app's generated `payload-types.ts` might have slightly different type definitions than the plugin's (e.g., different font options, field variations).
390
+ const theme = await fetchThemeConfiguration({ collectionSlug: 'site-settings' })
898
391
 
899
- **Solution:** Update to v0.1.11+ which uses generic types instead of strict payload-types:
392
+ const effect = theme?.visualEffects?.effectStyle ?? 'flat'
393
+ const button = theme?.componentStyles?.buttonVariant ?? 'filled'
900
394
 
901
- ```bash
902
- pnpm update @kilivi/payloadcms-theme-management@latest
395
+ return (
396
+ <html data-effect={effect} data-button={button}>
397
+ ...
398
+ </html>
399
+ )
903
400
  ```
904
401
 
905
- The plugin now accepts any compatible theme configuration structure, regardless of your Payload version or type variations. See [TYPE_INDEPENDENCE_GUIDE.md](./docs/TYPE_INDEPENDENCE_GUIDE.md) for technical details.
906
-
907
- ### `Module not found: Can't resolve 'fs/promises'`
402
+ ## Theme presets
908
403
 
909
- **Solution:** Make sure you're using v0.1.9+ and importing server components from `/server`:
404
+ Built-in presets are exported as `allThemePresets`, and style presets as `allStylePresets`:
910
405
 
911
- ```typescript
912
- import { ServerThemeInjector } from '@kilivi/payloadcms-theme-management/server'
913
- ```
914
-
915
- Then clear your cache:
916
-
917
- ```bash
918
- rm -rf .next node_modules/.cache
919
- pnpm install
406
+ ```ts
407
+ import { allStylePresets, allThemePresets } from '@kilivi/payloadcms-theme-management'
920
408
  ```
921
409
 
922
- ### Theme Not Applying
923
-
924
- 1. Verify `ServerThemeInjector` is in your `<head>` tag
925
- 2. Check that site settings exist with theme configuration
926
- 3. Inspect page source - should see `<style>` tag with CSS variables
927
- 4. Ensure Tailwind/CSS is configured to use the CSS variables
410
+ Editors can also import extra presets as JSON (*Custom theme presets* section). They are merged into the theme picker, and they override built-in presets that have the same `name`.
928
411
 
929
- ### TypeScript Errors
412
+ Full preset reference: [docs/THEME_PRESETS_EXTENDED.md](./docs/THEME_PRESETS_EXTENDED.md)
930
413
 
931
- ```bash
932
- # Regenerate Payload types
933
- pnpm payload generate:types
414
+ ## Visitor components
934
415
 
935
- # Restart TypeScript server
936
- # VS Code: Ctrl+Shift+P → "TypeScript: Restart TS Server"
416
+ ```tsx
417
+ import ColorModeToggle from '@kilivi/payloadcms-theme-management/components/ColorModeToggle'
418
+ import ThemeSwitcher from '@kilivi/payloadcms-theme-management/components/ThemeSwitcher'
937
419
  ```
938
420
 
939
- ## Documentation
940
-
941
- - [Type Independence Guide](./docs/TYPE_INDEPENDENCE_GUIDE.md) - Understanding type safety without coupling
942
- - [Migration Guide](./docs/MIGRATION_GUIDE.md) - Upgrading from older versions
943
- - [Test App Guide](./docs/TEST_APP_GUIDE.md) - Create a test application
944
- - [Server/Client Separation](./docs/SERVER_CLIENT_SEPARATION.md) - Technical details
945
- - [Build Setup](./docs/BUILD_SETUP.md) - Build configuration details
946
-
947
- ## Development
421
+ ## Public API
948
422
 
949
- ```bash
950
- # Install dependencies
951
- pnpm install
952
-
953
- # Build the plugin
954
- pnpm build
423
+ ### Main package
955
424
 
956
- # Watch mode for development
957
- pnpm dev
425
+ ```ts
426
+ import {
427
+ allStylePresets,
428
+ allThemePresets,
429
+ fetchThemeConfiguration,
430
+ generateThemeColorsCss,
431
+ generateThemeCSS,
432
+ getAvailableThemePresets,
433
+ getThemeHtmlAttributes,
434
+ getThemePreset,
435
+ getThemeStyles,
436
+ resolveThemeConfiguration,
437
+ themeManagementPlugin,
438
+ ThemeProvider,
439
+ } from '@kilivi/payloadcms-theme-management'
958
440
 
959
- # Clean build artifacts
960
- pnpm clean
441
+ import type {
442
+ SiteThemeConfiguration,
443
+ ThemeComponentStyles,
444
+ ThemeHeroBackground,
445
+ ThemeVisualEffects,
446
+ } from '@kilivi/payloadcms-theme-management'
961
447
  ```
962
448
 
963
- ## Contributing
964
-
965
- Contributions are welcome! Please:
966
-
967
- 1. Fork the repository
968
- 2. Create a feature branch
969
- 3. Make your changes
970
- 4. Add tests if applicable
971
- 5. Submit a pull request
972
-
973
- ## Troubleshooting
449
+ ### Server package
974
450
 
975
- ### Common Issues
976
-
977
- **Error: "right-hand side of 'in' should be an object, got undefined"**
978
-
979
- This is a known Payload CMS issue related to user preferences. See [TROUBLESHOOTING.md](./docs/TROUBLESHOOTING.md) for solutions.
980
-
981
- **Quick Fix:** Go to `/admin/account` and click "Reset Preferences" at the bottom of the page.
451
+ ```ts
452
+ import {
453
+ createCachedThemeFetcher,
454
+ getThemeCacheTag,
455
+ getThemeCriticalCSS,
456
+ getThemeCSS,
457
+ revalidateThemeCache,
458
+ ServerThemeInjector,
459
+ } from '@kilivi/payloadcms-theme-management/server'
460
+ ```
982
461
 
983
- ## License
462
+ Always import server-only helpers from `/server`.
984
463
 
985
- Apache-2.0
464
+ ## Migrating from `@kilivi-dev`
986
465
 
987
- ## Author
466
+ The API is unchanged. Only the package name differs.
988
467
 
989
- Created for Payload CMS v3 applications.
468
+ 1. Swap the dependency:
990
469
 
991
- ## Links
470
+ ```bash
471
+ pnpm remove @kilivi-dev/payloadcms-theme-management
472
+ pnpm add @kilivi/payloadcms-theme-management
473
+ ```
992
474
 
993
- - [NPM Package](https://www.npmjs.com/package/@kilivi/payloadcms-theme-management)
994
- - [GitHub Repository](https://github.com/vitakili/payload-plugins)
995
- - [Payload CMS](https://payloadcms.com)
475
+ 2. Replace the import prefix in your code: `@kilivi-dev/payloadcms-theme-management` → `@kilivi/payloadcms-theme-management`. This covers `/server`, `/components/*` and `/fields/*`.
476
+ 3. Regenerate the admin import map so Payload resolves the admin components under the new name:
996
477
 
997
- ## Changelog
478
+ ```bash
479
+ pnpm payload generate:importmap
480
+ ```
998
481
 
999
- ### v1.0.2 (Latest)
482
+ 4. If you list the package in `next.config` under `transpilePackages`, rename it there too.
1000
483
 
1001
- - ✅ **Added:** Automatic cache invalidation for standalone globals
1002
- - ✅ **Fixed:** Correct Payload CMS v3 API usage (findGlobal instead of non-existent findBySlug)
1003
- - ✅ **Improved:** Multi-tenant documentation with proper examples
1004
- - ✅ **Improved:** Cache optimization examples with unstable_cache
1005
- - ✅ **Updated:** README and Multi-Tenant guide with accurate API methods
484
+ Stored data does not need a migration: field names and database columns stay the same.
1006
485
 
1007
- ### v1.0.0
486
+ ## Notes
1008
487
 
1009
- - ✅ **Added:** Standalone Global Support — create separate appearance settings as a global
1010
- - ✅ **Added:** Auto-populate Light/Dark colors when selecting a theme
1011
- - ✅ **Added:** Multi-tenant support and tenant-aware theme fetching
1012
- - ✅ **Fixed:** Cleaner data structure for improved API integration
1013
- - ✅ **Added:** Live Theme Preview at `/admin/theme-preview` (real-time updates)
1014
- - ✅ **Added:** Professional color picker (react-colorful)
1015
- - ✅ **Improved:** shadcn/ui and TweakCN compatibility
1016
- - ✅ **Improved:** SSR theme injection, zero FOUC, and performance optimizations
1017
- - ✅ **Added:** Server/client component separation
1018
- - ✅ **Added:** `server-only` package to prevent bundling errors
1019
- - ✅ **Changed:** `ServerThemeInjector` now exported from `/server` entry
488
+ - For strict preview security, set `PREVIEW_SECRET` and enable `livePreview.injectRoute`.
489
+ - For multi-tenant apps, use `tenantField` and `tenantQueryParam` to keep preview URLs tenant-aware.
490
+ - Task-focused guides for AI coding assistants live in `.claude/skills/`.
1020
491
 
1021
- ### v0.6.0
492
+ ## License
1022
493
 
1023
- - Initial release with core theming functionality
494
+ MIT