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