@kilivi/payloadcms-theme-management 2.0.0 → 2.2.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 (235) hide show
  1. package/README.md +346 -845
  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 +13 -1
  12. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  13. package/dist/components/ServerThemeInjector.js +65 -15
  14. package/dist/components/ServerThemeInjector.js.map +1 -1
  15. package/dist/components/ThemeLivePreviewSync.d.ts +4 -0
  16. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  17. package/dist/components/ThemeLivePreviewSync.js +28 -4
  18. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  19. package/dist/components/ThemeSwitcher.d.ts +36 -0
  20. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  21. package/dist/components/ThemeSwitcher.js +89 -0
  22. package/dist/components/ThemeSwitcher.js.map +1 -0
  23. package/dist/components/typographyPreviewUtils.d.ts +5 -0
  24. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  25. package/dist/components/typographyPreviewUtils.js +24 -11
  26. package/dist/components/typographyPreviewUtils.js.map +1 -1
  27. package/dist/constants/devicePreviews.d.ts +35 -0
  28. package/dist/constants/devicePreviews.d.ts.map +1 -0
  29. package/dist/constants/devicePreviews.js +43 -0
  30. package/dist/constants/devicePreviews.js.map +1 -0
  31. package/dist/constants/themeFonts.d.ts +94 -168
  32. package/dist/constants/themeFonts.d.ts.map +1 -1
  33. package/dist/constants/themeFonts.js +260 -140
  34. package/dist/constants/themeFonts.js.map +1 -1
  35. package/dist/endpoints/themeFonts.d.ts +5 -0
  36. package/dist/endpoints/themeFonts.d.ts.map +1 -0
  37. package/dist/endpoints/themeFonts.js +138 -0
  38. package/dist/endpoints/themeFonts.js.map +1 -0
  39. package/dist/extended-presets.d.ts +0 -4
  40. package/dist/extended-presets.d.ts.map +1 -1
  41. package/dist/extended-presets.js +1 -18
  42. package/dist/extended-presets.js.map +1 -1
  43. package/dist/extended-themes.d.ts.map +1 -1
  44. package/dist/extended-themes.js +428 -0
  45. package/dist/extended-themes.js.map +1 -1
  46. package/dist/fields/AccessibilityAuditField.d.ts +2 -0
  47. package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
  48. package/dist/fields/AccessibilityAuditField.js +303 -0
  49. package/dist/fields/AccessibilityAuditField.js.map +1 -0
  50. package/dist/fields/AppearancePreviewField.d.ts +2 -0
  51. package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
  52. package/dist/fields/AppearancePreviewField.js +826 -0
  53. package/dist/fields/AppearancePreviewField.js.map +1 -0
  54. package/dist/fields/ColorPickerField.d.ts.map +1 -1
  55. package/dist/fields/ColorPickerField.js +89 -85
  56. package/dist/fields/ColorPickerField.js.map +1 -1
  57. package/dist/fields/FontSelectField.css +155 -0
  58. package/dist/fields/FontSelectField.d.ts +8 -0
  59. package/dist/fields/FontSelectField.d.ts.map +1 -1
  60. package/dist/fields/FontSelectField.js +223 -188
  61. package/dist/fields/FontSelectField.js.map +1 -1
  62. package/dist/fields/PaletteGeneratorField.d.ts +7 -0
  63. package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
  64. package/dist/fields/PaletteGeneratorField.js +327 -0
  65. package/dist/fields/PaletteGeneratorField.js.map +1 -0
  66. package/dist/fields/StylePresetField.d.ts +3 -0
  67. package/dist/fields/StylePresetField.d.ts.map +1 -0
  68. package/dist/fields/StylePresetField.js +463 -0
  69. package/dist/fields/StylePresetField.js.map +1 -0
  70. package/dist/fields/ThemeColorPickerField.css +115 -35
  71. package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
  72. package/dist/fields/ThemeColorPickerField.js +204 -70
  73. package/dist/fields/ThemeColorPickerField.js.map +1 -1
  74. package/dist/fields/ThemeExportField.d.ts +6 -0
  75. package/dist/fields/ThemeExportField.d.ts.map +1 -0
  76. package/dist/fields/ThemeExportField.js +202 -0
  77. package/dist/fields/ThemeExportField.js.map +1 -0
  78. package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
  79. package/dist/fields/ThemePresetImportField.js +56 -25
  80. package/dist/fields/ThemePresetImportField.js.map +1 -1
  81. package/dist/fields/ThemePreviewField.css +444 -5
  82. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  83. package/dist/fields/ThemePreviewField.js +265 -170
  84. package/dist/fields/ThemePreviewField.js.map +1 -1
  85. package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
  86. package/dist/fields/ThemeTokenSelectField.js +278 -80
  87. package/dist/fields/ThemeTokenSelectField.js.map +1 -1
  88. package/dist/fields/colorFieldHelpers.d.ts +90 -0
  89. package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
  90. package/dist/fields/colorFieldHelpers.js +133 -0
  91. package/dist/fields/colorFieldHelpers.js.map +1 -0
  92. package/dist/fields/colorModeFields.d.ts +1 -1
  93. package/dist/fields/colorModeFields.d.ts.map +1 -1
  94. package/dist/fields/colorModeFields.js +49 -7
  95. package/dist/fields/colorModeFields.js.map +1 -1
  96. package/dist/fields/themeConfigurationField.d.ts +8 -1
  97. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  98. package/dist/fields/themeConfigurationField.js +1209 -228
  99. package/dist/fields/themeConfigurationField.js.map +1 -1
  100. package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
  101. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
  102. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
  103. package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
  104. package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
  105. package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
  106. package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
  107. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
  108. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
  109. package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
  110. package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
  111. package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
  112. package/dist/globals/ThemeSettings/config.js +76 -79
  113. package/dist/globals/ThemeSettings/config.js.map +1 -1
  114. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
  115. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
  116. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
  117. package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
  118. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
  119. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
  120. package/dist/hooks/useAppearanceSession.d.ts +50 -0
  121. package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
  122. package/dist/hooks/useAppearanceSession.js +91 -0
  123. package/dist/hooks/useAppearanceSession.js.map +1 -0
  124. package/dist/hooks/useNativeLivePreview.d.ts +15 -0
  125. package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
  126. package/dist/hooks/useNativeLivePreview.js +18 -0
  127. package/dist/hooks/useNativeLivePreview.js.map +1 -0
  128. package/dist/hooks/useThemeFonts.d.ts +10 -0
  129. package/dist/hooks/useThemeFonts.d.ts.map +1 -0
  130. package/dist/hooks/useThemeFonts.js +36 -0
  131. package/dist/hooks/useThemeFonts.js.map +1 -0
  132. package/dist/hooks/useThemeTranslations.d.ts +13 -0
  133. package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
  134. package/dist/hooks/useThemeTranslations.js +22 -0
  135. package/dist/hooks/useThemeTranslations.js.map +1 -0
  136. package/dist/i18n.d.ts +53 -0
  137. package/dist/i18n.d.ts.map +1 -0
  138. package/dist/i18n.js +53 -0
  139. package/dist/i18n.js.map +1 -0
  140. package/dist/index.d.ts +13 -4
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +59 -29
  143. package/dist/index.js.map +1 -1
  144. package/dist/next-fonts.d.ts +3 -0
  145. package/dist/next-fonts.d.ts.map +1 -0
  146. package/dist/next-fonts.js +360 -0
  147. package/dist/next-fonts.js.map +1 -0
  148. package/dist/payload-types.d.ts +34 -6
  149. package/dist/payload-types.d.ts.map +1 -1
  150. package/dist/payload-types.js.map +1 -1
  151. package/dist/presets.d.ts +14 -0
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js.map +1 -1
  154. package/dist/providers/Theme/index.d.ts.map +1 -1
  155. package/dist/providers/Theme/index.js +75 -11
  156. package/dist/providers/Theme/index.js.map +1 -1
  157. package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
  158. package/dist/providers/Theme/themeConfig.js +104 -0
  159. package/dist/providers/Theme/themeConfig.js.map +1 -1
  160. package/dist/providers/Theme/types.d.ts +1 -1
  161. package/dist/providers/Theme/types.d.ts.map +1 -1
  162. package/dist/providers/Theme/types.js +10 -1
  163. package/dist/providers/Theme/types.js.map +1 -1
  164. package/dist/style-presets.d.ts +75 -0
  165. package/dist/style-presets.d.ts.map +1 -0
  166. package/dist/style-presets.js +738 -0
  167. package/dist/style-presets.js.map +1 -0
  168. package/dist/translations.d.ts +164 -1
  169. package/dist/translations.d.ts.map +1 -1
  170. package/dist/translations.js +320 -9
  171. package/dist/translations.js.map +1 -1
  172. package/dist/tweakcn-special-themes.d.ts.map +1 -1
  173. package/dist/tweakcn-special-themes.js +585 -9
  174. package/dist/tweakcn-special-themes.js.map +1 -1
  175. package/dist/types/css.d.js +3 -0
  176. package/dist/types/css.d.js.map +1 -0
  177. package/dist/types.d.ts +26 -0
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/utilities/getThemeSettings.d.ts.map +1 -1
  182. package/dist/utilities/getThemeSettings.js +2 -14
  183. package/dist/utilities/getThemeSettings.js.map +1 -1
  184. package/dist/utils/contrast.d.ts +63 -0
  185. package/dist/utils/contrast.d.ts.map +1 -0
  186. package/dist/utils/contrast.js +158 -0
  187. package/dist/utils/contrast.js.map +1 -0
  188. package/dist/utils/exportTokens.d.ts +30 -0
  189. package/dist/utils/exportTokens.d.ts.map +1 -0
  190. package/dist/utils/exportTokens.js +183 -0
  191. package/dist/utils/exportTokens.js.map +1 -0
  192. package/dist/utils/generatePalette.d.ts +46 -0
  193. package/dist/utils/generatePalette.d.ts.map +1 -0
  194. package/dist/utils/generatePalette.js +127 -0
  195. package/dist/utils/generatePalette.js.map +1 -0
  196. package/dist/utils/generateThemeCSS.d.ts.map +1 -1
  197. package/dist/utils/generateThemeCSS.js +2 -40
  198. package/dist/utils/generateThemeCSS.js.map +1 -1
  199. package/dist/utils/livePreviewThemeRuntime.d.ts +22 -3
  200. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  201. package/dist/utils/livePreviewThemeRuntime.js +48 -15
  202. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  203. package/dist/utils/localizedText.d.ts +8 -0
  204. package/dist/utils/localizedText.d.ts.map +1 -0
  205. package/dist/utils/localizedText.js +21 -0
  206. package/dist/utils/localizedText.js.map +1 -0
  207. package/dist/utils/resolveThemeConfiguration.d.ts +60 -1
  208. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  209. package/dist/utils/resolveThemeConfiguration.js +53 -13
  210. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  211. package/dist/utils/themeColors.d.ts +3 -0
  212. package/dist/utils/themeColors.d.ts.map +1 -1
  213. package/dist/utils/themeColors.js +30 -1
  214. package/dist/utils/themeColors.js.map +1 -1
  215. package/dist/utils/themeHtmlAttributes.d.ts +32 -12
  216. package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
  217. package/dist/utils/themeHtmlAttributes.js +38 -5
  218. package/dist/utils/themeHtmlAttributes.js.map +1 -1
  219. package/dist/utils/themeUtils.d.ts +3 -5
  220. package/dist/utils/themeUtils.d.ts.map +1 -1
  221. package/dist/utils/themeUtils.js +474 -6
  222. package/dist/utils/themeUtils.js.map +1 -1
  223. package/dist/utils/tweakcnConverter.d.ts +1 -3
  224. package/dist/utils/tweakcnConverter.d.ts.map +1 -1
  225. package/dist/utils/tweakcnConverter.js +1 -3
  226. package/dist/utils/tweakcnConverter.js.map +1 -1
  227. package/package.json +15 -5
  228. package/dist/components/ThemePreview.d.ts +0 -3
  229. package/dist/components/ThemePreview.d.ts.map +0 -1
  230. package/dist/components/ThemePreview.js +0 -321
  231. package/dist/components/ThemePreview.js.map +0 -1
  232. package/dist/plugin.d.ts +0 -2
  233. package/dist/plugin.d.ts.map +0 -1
  234. package/dist/plugin.js +0 -3
  235. package/dist/plugin.js.map +0 -1
@@ -2,17 +2,23 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import './ThemePreviewField.css';
4
4
  import { useField, useForm, useFormFields } from '@payloadcms/ui';
5
- import { useCallback, useEffect, useMemo, useRef } from 'react';
5
+ import { ChevronDown, ChevronRight, Moon, Palette, Sun } from 'lucide-react';
6
+ import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
6
7
  import { resolveTypographyPreview } from '../components/typographyPreviewUtils.js';
7
8
  import { allThemePresets } from '../index.js';
9
+ import { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js';
10
+ import PaletteGeneratorField from './PaletteGeneratorField.js';
8
11
  import { borderRadiusPresets } from '../providers/Theme/themeConfig.js';
9
- import { getTranslations } from '../translations.js';
10
- import { getAdminLanguage } from '../utils/getAdminLanguage.js';
11
12
  import { darkModeDefaults, lightModeDefaults } from './colorModeFields.js';
13
+ import { parseThemePresetInput } from '../utils/customThemePresets.js';
14
+ import { resolveLocalizedText } from '../utils/localizedText.js';
15
+ import { AppearanceLocks, AppearanceUndoBar } from '../components/AppearanceSessionControls.js';
16
+ import { createBulkWriter, getAppearanceSession } from '../hooks/useAppearanceSession.js';
17
+ import { useNativeLivePreview } from '../hooks/useNativeLivePreview.js';
12
18
  // FALLBACK_THEME and runtime theme presets are computed inside the component
13
19
  const colorKeys = Object.keys(lightModeDefaults);
14
- function ModePreview({ icon, title, colors, typography, radius }) {
15
- const t = getTranslations(getAdminLanguage());
20
+ function ModePreview({ icon: Icon, title, colors, typography, radius }) {
21
+ const t = useThemeTranslations();
16
22
  const background = colors.background ?? '#ffffff';
17
23
  const foreground = colors.foreground ?? '#0a0a0a';
18
24
  const borderColor = colors.border ?? 'rgba(148, 163, 184, 0.35)';
@@ -31,6 +37,8 @@ function ModePreview({ icon, title, colors, typography, radius }) {
31
37
  const baseFontSize = typography.baseFontSize;
32
38
  const lineHeight = typography.lineHeight;
33
39
  return /*#__PURE__*/ _jsxs("div", {
40
+ // A picture of the site: its sample buttons must not become dead tab stops.
41
+ ref: (el)=>el?.setAttribute('inert', ''),
34
42
  style: {
35
43
  display: 'grid',
36
44
  gap: '16px',
@@ -56,9 +64,16 @@ function ModePreview({ icon, title, colors, typography, radius }) {
56
64
  },
57
65
  children: [
58
66
  /*#__PURE__*/ _jsxs("span", {
67
+ style: {
68
+ display: 'inline-flex',
69
+ alignItems: 'center',
70
+ gap: '6px'
71
+ },
59
72
  children: [
60
- icon,
61
- " ",
73
+ /*#__PURE__*/ _jsx(Icon, {
74
+ size: 13,
75
+ "aria-hidden": true
76
+ }),
62
77
  title
63
78
  ]
64
79
  }),
@@ -133,7 +148,7 @@ function ModePreview({ icon, title, colors, typography, radius }) {
133
148
  backgroundColor: muted,
134
149
  color: mutedForeground
135
150
  },
136
- children: "Muted Card"
151
+ children: t.ui.mutedSample
137
152
  }),
138
153
  /*#__PURE__*/ _jsx("div", {
139
154
  style: {
@@ -144,7 +159,7 @@ function ModePreview({ icon, title, colors, typography, radius }) {
144
159
  backgroundColor: accent,
145
160
  color: accentForeground
146
161
  },
147
- children: "Accent Tag"
162
+ children: t.ui.accentSample
148
163
  })
149
164
  ]
150
165
  }),
@@ -165,7 +180,7 @@ function ModePreview({ icon, title, colors, typography, radius }) {
165
180
  fontWeight: 600,
166
181
  lineHeight: 1.2
167
182
  },
168
- children: "Nadpis / Heading preview"
183
+ children: t.ui.headingSample
169
184
  }),
170
185
  /*#__PURE__*/ _jsx("p", {
171
186
  style: {
@@ -176,38 +191,43 @@ function ModePreview({ icon, title, colors, typography, radius }) {
176
191
  color: foreground,
177
192
  opacity: 0.85
178
193
  },
179
- children: "Sphinx of black quartz, judge my vow. Příliš žluťoučký kůň úpěl ďábelské ódy."
194
+ children: t.ui.bodySample
180
195
  }),
181
196
  /*#__PURE__*/ _jsxs("div", {
182
197
  style: {
183
198
  display: 'flex',
184
199
  flexWrap: 'wrap',
185
200
  gap: '10px',
186
- fontSize: '11px',
187
- color: 'rgba(15, 23, 42, 0.7)'
201
+ fontSize: '12px',
202
+ // Follow the previewed palette so the meta line stays readable in dark mode.
203
+ color: mutedForeground
188
204
  },
189
205
  children: [
190
206
  /*#__PURE__*/ _jsxs("span", {
191
207
  children: [
192
- "Body: ",
208
+ t.ui.bodyFontLabel,
209
+ ": ",
193
210
  typography.bodyLabel
194
211
  ]
195
212
  }),
196
213
  /*#__PURE__*/ _jsxs("span", {
197
214
  children: [
198
- "Heading: ",
215
+ t.ui.headingFontLabel,
216
+ ": ",
199
217
  typography.headingLabel
200
218
  ]
201
219
  }),
202
220
  /*#__PURE__*/ _jsxs("span", {
203
221
  children: [
204
- "Base size: ",
222
+ t.ui.baseSizeLabel,
223
+ ": ",
205
224
  baseFontSize
206
225
  ]
207
226
  }),
208
227
  /*#__PURE__*/ _jsxs("span", {
209
228
  children: [
210
- "Line height: ",
229
+ t.ui.lineHeightLabel,
230
+ ": ",
211
231
  lineHeight
212
232
  ]
213
233
  })
@@ -220,13 +240,43 @@ function ModePreview({ icon, title, colors, typography, radius }) {
220
240
  }
221
241
  export default function ThemePreviewField(props) {
222
242
  const { field, path } = props;
223
- const showPreviewPanel = field?.admin?.custom?.useThemePreviewField ?? true;
243
+ // useThemePreviewField: explicit opt-out via admin.custom.useThemePreviewField = false
244
+ // Plugin always passes true; advanced users can override to permanently hide the panel.
245
+ const configuredShowPreview = field?.admin?.custom?.useThemePreviewField ?? true;
246
+ // isLivePreviewing: true only when the user actively clicked Preview and the iframe/popup is open.
247
+ // While editing (before clicking Preview), the panel is always shown regardless of livePreview config.
248
+ // Default context value is false, so the panel shows by default even without LivePreview provider.
249
+ const { isOpen: isLivePreviewing } = useNativeLivePreview();
250
+ const showPreviewPanel = configuredShowPreview && !isLivePreviewing;
224
251
  // If the plugin provided theme presets via admin config, prefer those;
225
252
  // support both `field.admin.themePresets` (legacy) and `field.admin.custom.themePresets`
226
253
  // otherwise fallback to `allThemePresets`.
227
254
  // Prefer presets passed via `admin.custom.themePresets` (set by the field factory / plugin).
228
255
  // Fall back to legacy `admin.themePresets` for backwards compatibility, then to `allThemePresets`.
229
- const baseThemePresets = field?.admin?.custom?.themePresets ?? field?.admin?.themePresets ?? allThemePresets;
256
+ const configuredThemePresets = field?.admin?.custom?.themePresets ?? field?.admin?.themePresets ?? allThemePresets;
257
+ // Presets imported through the "Custom Theme Presets" JSON field live next to
258
+ // this field. Merge them in (overriding built-ins by name) so they show up in
259
+ // the picker as soon as they are imported.
260
+ const customPresetsPath = [
261
+ ...path.split('.').slice(0, -1),
262
+ 'customThemePresets'
263
+ ].join('.');
264
+ const customPresetsValue = useFormFields(([formState])=>formState?.[customPresetsPath]?.value);
265
+ const baseThemePresets = useMemo(()=>{
266
+ const custom = parseThemePresetInput(customPresetsValue);
267
+ if (custom.length === 0) return configuredThemePresets;
268
+ const byName = new Map(configuredThemePresets.map((preset)=>[
269
+ preset.name,
270
+ preset
271
+ ]));
272
+ custom.forEach((preset)=>byName.set(preset.name, preset));
273
+ return [
274
+ ...byName.values()
275
+ ];
276
+ }, [
277
+ configuredThemePresets,
278
+ customPresetsValue
279
+ ]);
230
280
  const runtimeThemePresets = useMemo(()=>{
231
281
  return baseThemePresets.reduce((acc, preset)=>{
232
282
  acc[preset.name] = {
@@ -240,7 +290,8 @@ export default function ThemePreviewField(props) {
240
290
  ...preset.darkMode ?? {}
241
291
  },
242
292
  typography: preset.typography,
243
- borderRadius: preset.borderRadius
293
+ borderRadius: preset.borderRadius,
294
+ visualEffects: preset.visualEffects
244
295
  };
245
296
  return acc;
246
297
  }, {});
@@ -254,42 +305,84 @@ export default function ThemePreviewField(props) {
254
305
  const { dispatchFields } = useForm();
255
306
  const formFields = useFormFields(([formState])=>formState);
256
307
  const hasAppliedInitialPresetRef = useRef(false);
257
- const applyPreset = useCallback((presetName)=>{
308
+ const [previewOpen, setPreviewOpen] = useState(true);
309
+ // Preset list starts collapsed once a theme is already selected so the
310
+ // preview panel — not the long preset list — dominates the layout.
311
+ const [presetsOpen, setPresetsOpen] = useState(false);
312
+ const { setModified } = useForm();
313
+ /**
314
+ * Write a theme preset into the form, honouring the session locks:
315
+ * colours (Colours lock), visual effects (Style lock). Theme fonts apply
316
+ * implicitly through the "Use theme preset" font option, so with the Fonts lock
317
+ * on we pin the current preset font as a custom value before switching.
318
+ */ const applyPreset = useCallback((presetName, writer)=>{
258
319
  const preset = runtimeThemePresets[presetName];
259
320
  if (!preset) {
260
321
  return;
261
322
  }
262
- colorKeys.forEach((key)=>{
263
- const lightValue = preset.lightMode[key];
264
- const darkValue = preset.darkMode[key];
265
- if (lightValue) {
266
- dispatchFields({
267
- type: 'UPDATE',
268
- path: `themeConfiguration.lightMode.${key}`,
269
- value: lightValue
270
- });
271
- }
272
- if (darkValue) {
273
- dispatchFields({
274
- type: 'UPDATE',
275
- path: `themeConfiguration.darkMode.${key}`,
276
- value: darkValue
277
- });
278
- }
279
- });
323
+ const { locks } = getAppearanceSession();
324
+ if (!locks.colors) {
325
+ colorKeys.forEach((key)=>{
326
+ const lightValue = preset.lightMode[key];
327
+ const darkValue = preset.darkMode[key];
328
+ if (lightValue) writer.write(`themeConfiguration.lightMode.${key}`, lightValue);
329
+ if (darkValue) writer.write(`themeConfiguration.darkMode.${key}`, darkValue);
330
+ });
331
+ }
332
+ if (locks.fonts && selectedTheme) {
333
+ const typographyValue = (key)=>formFields?.[`themeConfiguration.typography.${key}`]?.value;
334
+ const current = resolveTypographyPreview({
335
+ bodyFont: typographyValue('bodyFont'),
336
+ headingFont: typographyValue('headingFont')
337
+ }, selectedTheme);
338
+ const pin = (key, family)=>{
339
+ const value = typographyValue(key);
340
+ if (!value || value === 'preset') {
341
+ writer.write(`themeConfiguration.typography.${key}`, 'custom');
342
+ writer.write(`themeConfiguration.typography.${key}Custom`, family);
343
+ }
344
+ };
345
+ pin('bodyFont', current.bodyFont);
346
+ pin('headingFont', current.headingFont);
347
+ }
348
+ if (!locks.style && preset.visualEffects) {
349
+ const vfKeys = [
350
+ 'effectStyle',
351
+ 'shadowIntensity',
352
+ 'backdropBlur',
353
+ 'borderStyle',
354
+ 'borderWidth',
355
+ 'glassOpacity'
356
+ ];
357
+ vfKeys.forEach((key)=>{
358
+ const value = preset.visualEffects?.[key];
359
+ if (value !== undefined) writer.write(`themeConfiguration.visualEffects.${key}`, value);
360
+ });
361
+ }
280
362
  }, [
281
- dispatchFields
363
+ runtimeThemePresets,
364
+ formFields,
365
+ selectedTheme
282
366
  ]);
283
367
  const handleThemeSelect = useCallback((value)=>{
284
- setValue(value);
285
368
  if (!value) {
369
+ setValue(value);
286
370
  return;
287
371
  }
288
372
  hasAppliedInitialPresetRef.current = true;
289
- applyPreset(value);
373
+ const writer = createBulkWriter(formFields, dispatchFields);
374
+ writer.write(path, value);
375
+ applyPreset(value, writer);
376
+ setModified(true);
377
+ writer.commit('theme', runtimeThemePresets[value]?.label ?? value);
290
378
  }, [
291
379
  applyPreset,
292
- setValue
380
+ setValue,
381
+ formFields,
382
+ dispatchFields,
383
+ path,
384
+ setModified,
385
+ runtimeThemePresets
293
386
  ]);
294
387
  useEffect(()=>{
295
388
  if (!selectedTheme) {
@@ -330,7 +423,8 @@ export default function ThemePreviewField(props) {
330
423
  hasAppliedInitialPresetRef.current = true;
331
424
  return;
332
425
  }
333
- applyPreset(selectedTheme);
426
+ // First-time fill of empty colour fields: not an editor action, so no undo entry.
427
+ applyPreset(selectedTheme, createBulkWriter(formFields, dispatchFields));
334
428
  hasAppliedInitialPresetRef.current = true;
335
429
  }, [
336
430
  selectedTheme,
@@ -396,8 +490,9 @@ export default function ThemePreviewField(props) {
396
490
  chip: borderRadiusCSS['--radius-default'] ?? '0.75rem',
397
491
  pill: borderRadiusCSS['--radius-xl'] ?? borderRadiusCSS['--radius-large'] ?? '999px'
398
492
  };
399
- // Translate labels at runtime based on admin language
400
- const t = getTranslations(getAdminLanguage());
493
+ // Translate labels at runtime based on the active Payload admin language
494
+ const t = useThemeTranslations();
495
+ const fieldId = `tm-theme-${useId().replace(/:/g, '')}`;
401
496
  // Explicitly type keys as `keyof ColorModeColors` to satisfy TypeScript when indexing `lightMode`/`darkMode`
402
497
  const highlightSwatches = [
403
498
  {
@@ -421,61 +516,130 @@ export default function ThemePreviewField(props) {
421
516
  label: t.colors.foreground
422
517
  }
423
518
  ];
424
- const fieldLabel = typeof field.label === 'string' ? field.label : field.label?.en || field.label?.cs || 'Theme';
519
+ const presetCount = Object.keys(runtimeThemePresets).length;
520
+ const summarySwatches = highlightSwatches.map(({ key })=>activePreset ? activePreset.lightMode[key] : lightModeColors[key]).filter((v)=>typeof v === 'string' && v.trim().length > 0);
521
+ // Field labels/descriptions arrive as `{ en, cs, … }` objects. Resolve them
522
+ // against the active admin language (Payload i18n) instead of hard-coding `en`,
523
+ // otherwise the theme selection stays English in every locale.
524
+ const adminLanguage = useThemeLanguage();
525
+ const resolveLocalized = (value, fallback = '')=>resolveLocalizedText(value, adminLanguage, fallback);
526
+ const fieldLabel = resolveLocalized(field.label, 'Theme');
527
+ const fieldDescription = resolveLocalized(field.admin?.description);
425
528
  return /*#__PURE__*/ _jsxs("div", {
426
529
  style: {
427
530
  marginBottom: '24px'
428
531
  },
429
532
  children: [
430
- /*#__PURE__*/ _jsx("label", {
431
- style: {
432
- display: 'block',
433
- marginBottom: '10px',
434
- fontSize: '13px',
435
- fontWeight: 600,
436
- color: 'var(--theme-elevation-800)'
437
- },
438
- children: fieldLabel
439
- }),
440
533
  /*#__PURE__*/ _jsxs("div", {
441
534
  style: {
442
535
  display: 'flex',
443
- flexWrap: 'wrap',
444
- gap: '24px',
445
- alignItems: 'flex-start'
536
+ alignItems: 'center',
537
+ justifyContent: 'space-between',
538
+ marginBottom: '10px'
446
539
  },
447
540
  children: [
448
- /*#__PURE__*/ _jsxs("div", {
541
+ /*#__PURE__*/ _jsx("div", {
542
+ id: `${fieldId}-label`,
449
543
  style: {
450
- flex: '0 0 320px',
451
- minWidth: '260px',
452
- maxWidth: '420px',
453
- display: 'grid',
454
- gap: '12px'
544
+ fontSize: '13px',
545
+ fontWeight: 600,
546
+ color: 'var(--theme-elevation-800)'
455
547
  },
548
+ children: fieldLabel
549
+ }),
550
+ showPreviewPanel && /*#__PURE__*/ _jsxs("button", {
551
+ type: "button",
552
+ onClick: ()=>setPreviewOpen((v)=>!v),
553
+ className: "theme-preview-toggle",
554
+ "aria-expanded": previewOpen,
555
+ "aria-controls": `${fieldId}-preview`,
556
+ children: [
557
+ previewOpen ? /*#__PURE__*/ _jsx(ChevronDown, {
558
+ size: 12,
559
+ "aria-hidden": true
560
+ }) : /*#__PURE__*/ _jsx(ChevronRight, {
561
+ size: 12,
562
+ "aria-hidden": true
563
+ }),
564
+ previewOpen ? t.ui.hidePreview : t.ui.showPreview
565
+ ]
566
+ })
567
+ ]
568
+ }),
569
+ /*#__PURE__*/ _jsxs("div", {
570
+ className: `theme-preview-layout${showPreviewPanel && previewOpen ? '' : ' theme-preview-layout--single'}`,
571
+ children: [
572
+ /*#__PURE__*/ _jsxs("div", {
573
+ className: "theme-preset-column",
456
574
  children: [
457
- /*#__PURE__*/ _jsx("div", {
575
+ /*#__PURE__*/ _jsx(AppearanceLocks, {}),
576
+ /*#__PURE__*/ _jsx(AppearanceUndoBar, {
577
+ sources: [
578
+ 'theme',
579
+ 'palette'
580
+ ]
581
+ }),
582
+ /*#__PURE__*/ _jsx(PaletteGeneratorField, {}),
583
+ /*#__PURE__*/ _jsxs("button", {
584
+ type: "button",
585
+ className: `theme-preset-summary${presetsOpen ? ' is-open' : ''}`,
586
+ onClick: ()=>setPresetsOpen((v)=>!v),
587
+ "aria-expanded": presetsOpen,
588
+ "aria-controls": `${fieldId}-presets`,
589
+ "aria-label": activePreset ? `${t.ui.choosePreset}: ${activePreset.label}` : t.ui.choosePreset,
590
+ children: [
591
+ presetsOpen ? /*#__PURE__*/ _jsx(ChevronDown, {
592
+ size: 14,
593
+ "aria-hidden": true
594
+ }) : /*#__PURE__*/ _jsx(ChevronRight, {
595
+ size: 14,
596
+ "aria-hidden": true
597
+ }),
598
+ /*#__PURE__*/ _jsxs("span", {
599
+ className: "theme-preset-summary__info",
600
+ children: [
601
+ /*#__PURE__*/ _jsx("span", {
602
+ className: "theme-label",
603
+ children: activePreset?.label ?? t.ui.choosePreset
604
+ }),
605
+ /*#__PURE__*/ _jsx("span", {
606
+ className: "theme-name",
607
+ children: `${presetCount} ${t.ui.presetCount}`
608
+ })
609
+ ]
610
+ }),
611
+ /*#__PURE__*/ _jsx("span", {
612
+ className: "theme-swatches",
613
+ "aria-hidden": "true",
614
+ children: summarySwatches.map((color, index)=>/*#__PURE__*/ _jsx("span", {
615
+ className: "color-swatch",
616
+ style: {
617
+ backgroundColor: color
618
+ }
619
+ }, `summary-swatch-${color}-${index}`))
620
+ })
621
+ ]
622
+ }),
623
+ presetsOpen && /*#__PURE__*/ _jsx("div", {
458
624
  className: "theme-preset-list",
625
+ id: `${fieldId}-presets`,
626
+ role: "group",
627
+ "aria-labelledby": `${fieldId}-label`,
459
628
  children: Object.entries(runtimeThemePresets).map(([key, preset])=>{
460
629
  const isSelected = key === selectedTheme;
461
630
  const swatches = highlightSwatches.map(({ key: colorKey })=>preset.lightMode[colorKey]).filter((v)=>typeof v === 'string' && v.trim().length > 0);
462
631
  return /*#__PURE__*/ _jsxs("button", {
463
632
  type: "button",
633
+ "aria-pressed": isSelected,
464
634
  onClick: ()=>handleThemeSelect(key),
465
635
  className: `theme-preset-button ${isSelected ? 'selected' : ''}`,
466
636
  children: [
467
- /*#__PURE__*/ _jsxs("div", {
637
+ /*#__PURE__*/ _jsx("div", {
468
638
  className: "theme-info",
469
- children: [
470
- /*#__PURE__*/ _jsx("span", {
471
- className: "theme-label",
472
- children: preset.label
473
- }),
474
- /*#__PURE__*/ _jsx("span", {
475
- className: "theme-name",
476
- children: key
477
- })
478
- ]
639
+ children: /*#__PURE__*/ _jsx("span", {
640
+ className: "theme-label",
641
+ children: preset.label
642
+ })
479
643
  }),
480
644
  /*#__PURE__*/ _jsx("div", {
481
645
  className: "theme-swatches",
@@ -491,99 +655,19 @@ export default function ThemePreviewField(props) {
491
655
  }, key);
492
656
  })
493
657
  }),
494
- activePreset && /*#__PURE__*/ _jsxs("div", {
495
- style: {
496
- padding: '12px',
497
- borderRadius: '10px',
498
- border: '1px solid var(--theme-elevation-200)',
499
- backgroundColor: 'var(--theme-elevation-50)',
500
- display: 'grid',
501
- gap: '8px',
502
- fontSize: '12px',
503
- color: 'var(--theme-elevation-600)'
504
- },
505
- children: [
506
- /*#__PURE__*/ _jsx("div", {
507
- style: {
508
- fontWeight: 600,
509
- color: 'var(--theme-elevation-800)'
510
- },
511
- children: activePreset.label
512
- }),
513
- /*#__PURE__*/ _jsxs("div", {
514
- children: [
515
- "Primary color: ",
516
- activePreset.lightMode.primary
517
- ]
518
- }),
519
- /*#__PURE__*/ _jsxs("div", {
520
- children: [
521
- "Accent color: ",
522
- activePreset.lightMode.accent
523
- ]
524
- }),
525
- /*#__PURE__*/ _jsx("div", {
526
- style: {
527
- display: 'flex',
528
- flexWrap: 'wrap',
529
- gap: '12px',
530
- marginTop: '4px'
531
- },
532
- children: highlightSwatches.map(({ key, label })=>{
533
- const swatchValue = activePreset.lightMode[key];
534
- if (!swatchValue) {
535
- return null;
536
- }
537
- return /*#__PURE__*/ _jsxs("div", {
538
- style: {
539
- display: 'flex',
540
- alignItems: 'center',
541
- gap: '6px',
542
- minWidth: 'fit-content'
543
- },
544
- children: [
545
- /*#__PURE__*/ _jsx("span", {
546
- style: {
547
- width: '22px',
548
- height: '22px',
549
- borderRadius: '999px',
550
- border: '1px solid rgba(15, 23, 42, 0.12)',
551
- backgroundColor: swatchValue,
552
- boxShadow: 'inset 0 1px 2px rgba(15, 23, 42, 0.08)'
553
- },
554
- "aria-hidden": "true"
555
- }),
556
- /*#__PURE__*/ _jsx("span", {
557
- style: {
558
- fontSize: '11px',
559
- color: 'var(--theme-elevation-700)'
560
- },
561
- children: label
562
- })
563
- ]
564
- }, key);
565
- })
566
- })
567
- ]
568
- }),
569
- field.admin?.description && /*#__PURE__*/ _jsx("div", {
658
+ fieldDescription && /*#__PURE__*/ _jsx("div", {
570
659
  style: {
571
660
  fontSize: '12px',
572
661
  color: 'var(--theme-elevation-600)',
573
662
  lineHeight: 1.5
574
663
  },
575
- children: typeof field.admin.description === 'string' ? field.admin.description : field.admin.description?.en || field.admin.description?.cs || ''
664
+ children: fieldDescription
576
665
  })
577
666
  ]
578
667
  }),
579
- showPreviewPanel && /*#__PURE__*/ _jsx("div", {
580
- style: {
581
- flex: '1 1 420px',
582
- minWidth: '320px',
583
- position: 'sticky',
584
- top: '96px',
585
- alignSelf: 'flex-start'
586
- },
668
+ showPreviewPanel && previewOpen && /*#__PURE__*/ _jsx("div", {
669
+ className: "theme-preview-panel",
670
+ id: `${fieldId}-preview`,
587
671
  children: /*#__PURE__*/ _jsxs("div", {
588
672
  style: {
589
673
  width: '100%',
@@ -607,15 +691,26 @@ export default function ThemePreviewField(props) {
607
691
  color: 'var(--theme-elevation-800)'
608
692
  },
609
693
  children: [
610
- /*#__PURE__*/ _jsx("span", {
611
- children: getTranslations('en').livePreview.smallTitle
694
+ /*#__PURE__*/ _jsxs("span", {
695
+ style: {
696
+ display: 'inline-flex',
697
+ alignItems: 'center',
698
+ gap: '6px'
699
+ },
700
+ children: [
701
+ /*#__PURE__*/ _jsx(Palette, {
702
+ size: 14,
703
+ "aria-hidden": true
704
+ }),
705
+ t.ui.colorsAndTypography
706
+ ]
612
707
  }),
613
708
  /*#__PURE__*/ _jsx("span", {
614
709
  style: {
615
710
  fontSize: '12px',
616
711
  color: 'var(--theme-elevation-600)'
617
712
  },
618
- children: activePreset ? activePreset.label : getTranslations('en').preview.customPalette
713
+ children: activePreset ? activePreset.label : t.preview.customPalette
619
714
  })
620
715
  ]
621
716
  }),
@@ -626,15 +721,15 @@ export default function ThemePreviewField(props) {
626
721
  },
627
722
  children: [
628
723
  /*#__PURE__*/ _jsx(ModePreview, {
629
- icon: "☀️",
630
- title: getTranslations('en').ui.lightMode,
724
+ icon: Sun,
725
+ title: t.ui.lightMode,
631
726
  colors: lightModeColors,
632
727
  typography: previewTypography,
633
728
  radius: previewRadius
634
729
  }),
635
730
  /*#__PURE__*/ _jsx(ModePreview, {
636
- icon: "🌙",
637
- title: getTranslations('en').ui.darkMode,
731
+ icon: Moon,
732
+ title: t.ui.darkMode,
638
733
  colors: darkModeColors,
639
734
  typography: previewTypography,
640
735
  radius: previewRadius