@kilivi/payloadcms-theme-management 2.1.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 (81) hide show
  1. package/README.md +37 -7
  2. package/dist/components/ServerThemeInjector.d.ts +13 -1
  3. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  4. package/dist/components/ServerThemeInjector.js +24 -1
  5. package/dist/components/ServerThemeInjector.js.map +1 -1
  6. package/dist/components/ThemeLivePreviewSync.d.ts +3 -0
  7. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  8. package/dist/components/ThemeLivePreviewSync.js +28 -4
  9. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  10. package/dist/components/typographyPreviewUtils.d.ts +5 -0
  11. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  12. package/dist/components/typographyPreviewUtils.js +16 -10
  13. package/dist/components/typographyPreviewUtils.js.map +1 -1
  14. package/dist/constants/themeFonts.d.ts +94 -168
  15. package/dist/constants/themeFonts.d.ts.map +1 -1
  16. package/dist/constants/themeFonts.js +260 -140
  17. package/dist/constants/themeFonts.js.map +1 -1
  18. package/dist/endpoints/themeFonts.d.ts +5 -0
  19. package/dist/endpoints/themeFonts.d.ts.map +1 -0
  20. package/dist/endpoints/themeFonts.js +138 -0
  21. package/dist/endpoints/themeFonts.js.map +1 -0
  22. package/dist/fields/AppearancePreviewField.d.ts.map +1 -1
  23. package/dist/fields/AppearancePreviewField.js +196 -133
  24. package/dist/fields/AppearancePreviewField.js.map +1 -1
  25. package/dist/fields/FontSelectField.css +155 -0
  26. package/dist/fields/FontSelectField.d.ts +4 -2
  27. package/dist/fields/FontSelectField.d.ts.map +1 -1
  28. package/dist/fields/FontSelectField.js +80 -118
  29. package/dist/fields/FontSelectField.js.map +1 -1
  30. package/dist/fields/StylePresetField.d.ts.map +1 -1
  31. package/dist/fields/StylePresetField.js +31 -30
  32. package/dist/fields/StylePresetField.js.map +1 -1
  33. package/dist/fields/ThemePreviewField.css +63 -1
  34. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  35. package/dist/fields/ThemePreviewField.js +2 -5
  36. package/dist/fields/ThemePreviewField.js.map +1 -1
  37. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  38. package/dist/fields/themeConfigurationField.js +320 -271
  39. package/dist/fields/themeConfigurationField.js.map +1 -1
  40. package/dist/hooks/useNativeLivePreview.d.ts +15 -0
  41. package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
  42. package/dist/hooks/useNativeLivePreview.js +18 -0
  43. package/dist/hooks/useNativeLivePreview.js.map +1 -0
  44. package/dist/hooks/useThemeFonts.d.ts +10 -0
  45. package/dist/hooks/useThemeFonts.d.ts.map +1 -0
  46. package/dist/hooks/useThemeFonts.js +36 -0
  47. package/dist/hooks/useThemeFonts.js.map +1 -0
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +9 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/next-fonts.d.ts +3 -0
  53. package/dist/next-fonts.d.ts.map +1 -0
  54. package/dist/next-fonts.js +360 -0
  55. package/dist/next-fonts.js.map +1 -0
  56. package/dist/payload-types.d.ts +10 -6
  57. package/dist/payload-types.d.ts.map +1 -1
  58. package/dist/payload-types.js.map +1 -1
  59. package/dist/presets.d.ts +6 -0
  60. package/dist/presets.d.ts.map +1 -1
  61. package/dist/presets.js.map +1 -1
  62. package/dist/style-presets.d.ts +27 -18
  63. package/dist/style-presets.d.ts.map +1 -1
  64. package/dist/style-presets.js +399 -217
  65. package/dist/style-presets.js.map +1 -1
  66. package/dist/translations.d.ts +5 -0
  67. package/dist/translations.d.ts.map +1 -1
  68. package/dist/translations.js +10 -0
  69. package/dist/translations.js.map +1 -1
  70. package/dist/utils/livePreviewThemeRuntime.d.ts +6 -1
  71. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  72. package/dist/utils/livePreviewThemeRuntime.js +11 -1
  73. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  74. package/dist/utils/resolveThemeConfiguration.d.ts +15 -0
  75. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  76. package/dist/utils/resolveThemeConfiguration.js +41 -8
  77. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  78. package/dist/utils/themeUtils.d.ts.map +1 -1
  79. package/dist/utils/themeUtils.js +60 -2
  80. package/dist/utils/themeUtils.js.map +1 -1
  81. package/package.json +5 -1
@@ -0,0 +1,138 @@
1
+ import { buildGoogleFontsHref, parseFontFamilies, THEME_FONTS_CSS_PATH, THEME_FONTS_FILE_PATH } from '../constants/themeFonts.js';
2
+ /**
3
+ * Self-hosted theme fonts.
4
+ *
5
+ * The visitor's browser never talks to Google: these endpoints fetch the
6
+ * Google Fonts stylesheet and font files on the server, cache them in memory and
7
+ * serve them from the site's own origin (GDPR-friendly). Only fonts from
8
+ * FONT_CATALOG are served, and only files from fonts.gstatic.com are proxied, so
9
+ * the endpoints cannot be used as an open proxy.
10
+ *
11
+ * GET {api}/theme-fonts/css?f=Inter,Playfair%20Display → @font-face CSS
12
+ * GET {api}/theme-fonts/file?p=s/inter/v19/….woff2 → font file
13
+ */ // A current desktop browser UA makes Google return woff2 with unicode-range subsets.
14
+ const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
15
+ const CSS_TTL_MS = 24 * 60 * 60 * 1000;
16
+ const MAX_CACHED_FILES = 300;
17
+ const FONT_FILE_PATTERN = /^s\/[a-z0-9]+\/v\d+\/[A-Za-z0-9_\-.]+\.woff2$/;
18
+ const cssCache = new Map();
19
+ const fileCache = new Map();
20
+ function rememberFile(key, data) {
21
+ if (fileCache.size >= MAX_CACHED_FILES) {
22
+ const oldest = fileCache.keys().next().value;
23
+ if (oldest !== undefined) fileCache.delete(oldest);
24
+ }
25
+ fileCache.set(key, data);
26
+ }
27
+ const apiRouteOf = (req)=>req.payload?.config?.routes?.api ?? '/api';
28
+ const queryValue = (req, key)=>{
29
+ const fromSearchParams = req.searchParams?.get?.(key);
30
+ if (fromSearchParams) return fromSearchParams;
31
+ const fromQuery = req.query?.[key];
32
+ return typeof fromQuery === 'string' ? fromQuery : null;
33
+ };
34
+ /** Rewrites fonts.gstatic.com URLs in Google's CSS to the self-hosted file endpoint. */ export function rewriteFontUrls(css, fileEndpoint) {
35
+ return css.replace(/https:\/\/fonts\.gstatic\.com\/([^)\s'"]+)/g, (match, path)=>FONT_FILE_PATTERN.test(path) ? `${fileEndpoint}?p=${encodeURIComponent(path)}` : match);
36
+ }
37
+ const cssEndpoint = {
38
+ path: THEME_FONTS_CSS_PATH,
39
+ method: 'get',
40
+ handler: async (req)=>{
41
+ const families = parseFontFamilies(queryValue(req, 'f'));
42
+ const href = buildGoogleFontsHref(families);
43
+ if (!href) {
44
+ return new Response('/* no fonts requested */', {
45
+ status: 200,
46
+ headers: {
47
+ 'Content-Type': 'text/css; charset=utf-8'
48
+ }
49
+ });
50
+ }
51
+ const cacheKey = families.join(',');
52
+ const cached = cssCache.get(cacheKey);
53
+ if (cached && cached.expires > Date.now()) {
54
+ return new Response(cached.css, {
55
+ headers: {
56
+ 'Content-Type': 'text/css; charset=utf-8',
57
+ 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800'
58
+ }
59
+ });
60
+ }
61
+ try {
62
+ const upstream = await fetch(href, {
63
+ headers: {
64
+ 'User-Agent': BROWSER_UA
65
+ }
66
+ });
67
+ if (!upstream.ok) throw new Error(`Google Fonts responded ${upstream.status}`);
68
+ const css = rewriteFontUrls(await upstream.text(), `${apiRouteOf(req)}${THEME_FONTS_FILE_PATH}`);
69
+ cssCache.set(cacheKey, {
70
+ css,
71
+ expires: Date.now() + CSS_TTL_MS
72
+ });
73
+ return new Response(css, {
74
+ headers: {
75
+ 'Content-Type': 'text/css; charset=utf-8',
76
+ 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800'
77
+ }
78
+ });
79
+ } catch (error) {
80
+ req.payload?.logger?.warn?.(`Theme Management Plugin: could not load fonts (${cacheKey}): ${String(error)}`);
81
+ // Fall back to the CSS font stacks; never break the page over a font.
82
+ return new Response('/* fonts unavailable — falling back to system fonts */', {
83
+ status: 200,
84
+ headers: {
85
+ 'Content-Type': 'text/css; charset=utf-8',
86
+ 'Cache-Control': 'no-store'
87
+ }
88
+ });
89
+ }
90
+ }
91
+ };
92
+ const fileEndpoint = {
93
+ path: THEME_FONTS_FILE_PATH,
94
+ method: 'get',
95
+ handler: async (req)=>{
96
+ const path = queryValue(req, 'p');
97
+ if (!path || !FONT_FILE_PATTERN.test(path)) {
98
+ return new Response('Not found', {
99
+ status: 404
100
+ });
101
+ }
102
+ const headers = {
103
+ 'Content-Type': 'font/woff2',
104
+ // gstatic paths are versioned, so a file never changes.
105
+ 'Cache-Control': 'public, max-age=31536000, immutable',
106
+ 'Access-Control-Allow-Origin': '*'
107
+ };
108
+ const cached = fileCache.get(path);
109
+ if (cached) return new Response(cached, {
110
+ headers
111
+ });
112
+ try {
113
+ const upstream = await fetch(`https://fonts.gstatic.com/${path}`, {
114
+ headers: {
115
+ 'User-Agent': BROWSER_UA
116
+ }
117
+ });
118
+ if (!upstream.ok) return new Response('Not found', {
119
+ status: 404
120
+ });
121
+ const data = await upstream.arrayBuffer();
122
+ rememberFile(path, data);
123
+ return new Response(data, {
124
+ headers
125
+ });
126
+ } catch {
127
+ return new Response('Font unavailable', {
128
+ status: 502
129
+ });
130
+ }
131
+ }
132
+ };
133
+ export const themeFontEndpoints = [
134
+ cssEndpoint,
135
+ fileEndpoint
136
+ ];
137
+
138
+ //# sourceMappingURL=themeFonts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/endpoints/themeFonts.ts"],"sourcesContent":["import type { Endpoint, PayloadRequest } from 'payload'\nimport {\n buildGoogleFontsHref,\n parseFontFamilies,\n THEME_FONTS_CSS_PATH,\n THEME_FONTS_FILE_PATH,\n} from '../constants/themeFonts.js'\n\n/**\n * Self-hosted theme fonts.\n *\n * The visitor's browser never talks to Google: these endpoints fetch the\n * Google Fonts stylesheet and font files on the server, cache them in memory and\n * serve them from the site's own origin (GDPR-friendly). Only fonts from\n * FONT_CATALOG are served, and only files from fonts.gstatic.com are proxied, so\n * the endpoints cannot be used as an open proxy.\n *\n * GET {api}/theme-fonts/css?f=Inter,Playfair%20Display → @font-face CSS\n * GET {api}/theme-fonts/file?p=s/inter/v19/….woff2 → font file\n */\n\n// A current desktop browser UA makes Google return woff2 with unicode-range subsets.\nconst BROWSER_UA =\n 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'\nconst CSS_TTL_MS = 24 * 60 * 60 * 1000\nconst MAX_CACHED_FILES = 300\nconst FONT_FILE_PATTERN = /^s\\/[a-z0-9]+\\/v\\d+\\/[A-Za-z0-9_\\-.]+\\.woff2$/\n\nconst cssCache = new Map<string, { css: string; expires: number }>()\nconst fileCache = new Map<string, ArrayBuffer>()\n\nfunction rememberFile(key: string, data: ArrayBuffer) {\n if (fileCache.size >= MAX_CACHED_FILES) {\n const oldest = fileCache.keys().next().value\n if (oldest !== undefined) fileCache.delete(oldest)\n }\n fileCache.set(key, data)\n}\n\nconst apiRouteOf = (req: PayloadRequest) => req.payload?.config?.routes?.api ?? '/api'\n\nconst queryValue = (req: PayloadRequest, key: string): string | null => {\n const fromSearchParams = req.searchParams?.get?.(key)\n if (fromSearchParams) return fromSearchParams\n const fromQuery = (req.query as Record<string, unknown> | undefined)?.[key]\n return typeof fromQuery === 'string' ? fromQuery : null\n}\n\n/** Rewrites fonts.gstatic.com URLs in Google's CSS to the self-hosted file endpoint. */\nexport function rewriteFontUrls(css: string, fileEndpoint: string): string {\n return css.replace(/https:\\/\\/fonts\\.gstatic\\.com\\/([^)\\s'\"]+)/g, (match, path: string) =>\n FONT_FILE_PATTERN.test(path) ? `${fileEndpoint}?p=${encodeURIComponent(path)}` : match,\n )\n}\n\nconst cssEndpoint: Endpoint = {\n path: THEME_FONTS_CSS_PATH,\n method: 'get',\n handler: async (req) => {\n const families = parseFontFamilies(queryValue(req, 'f'))\n const href = buildGoogleFontsHref(families)\n if (!href) {\n return new Response('/* no fonts requested */', {\n status: 200,\n headers: { 'Content-Type': 'text/css; charset=utf-8' },\n })\n }\n\n const cacheKey = families.join(',')\n const cached = cssCache.get(cacheKey)\n if (cached && cached.expires > Date.now()) {\n return new Response(cached.css, {\n headers: {\n 'Content-Type': 'text/css; charset=utf-8',\n 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800',\n },\n })\n }\n\n try {\n const upstream = await fetch(href, { headers: { 'User-Agent': BROWSER_UA } })\n if (!upstream.ok) throw new Error(`Google Fonts responded ${upstream.status}`)\n const css = rewriteFontUrls(await upstream.text(), `${apiRouteOf(req)}${THEME_FONTS_FILE_PATH}`)\n cssCache.set(cacheKey, { css, expires: Date.now() + CSS_TTL_MS })\n return new Response(css, {\n headers: {\n 'Content-Type': 'text/css; charset=utf-8',\n 'Cache-Control': 'public, max-age=86400, stale-while-revalidate=604800',\n },\n })\n } catch (error) {\n req.payload?.logger?.warn?.(\n `Theme Management Plugin: could not load fonts (${cacheKey}): ${String(error)}`,\n )\n // Fall back to the CSS font stacks; never break the page over a font.\n return new Response('/* fonts unavailable — falling back to system fonts */', {\n status: 200,\n headers: { 'Content-Type': 'text/css; charset=utf-8', 'Cache-Control': 'no-store' },\n })\n }\n },\n}\n\nconst fileEndpoint: Endpoint = {\n path: THEME_FONTS_FILE_PATH,\n method: 'get',\n handler: async (req) => {\n const path = queryValue(req, 'p')\n if (!path || !FONT_FILE_PATTERN.test(path)) {\n return new Response('Not found', { status: 404 })\n }\n\n const headers = {\n 'Content-Type': 'font/woff2',\n // gstatic paths are versioned, so a file never changes.\n 'Cache-Control': 'public, max-age=31536000, immutable',\n 'Access-Control-Allow-Origin': '*',\n }\n\n const cached = fileCache.get(path)\n if (cached) return new Response(cached, { headers })\n\n try {\n const upstream = await fetch(`https://fonts.gstatic.com/${path}`, {\n headers: { 'User-Agent': BROWSER_UA },\n })\n if (!upstream.ok) return new Response('Not found', { status: 404 })\n const data = await upstream.arrayBuffer()\n rememberFile(path, data)\n return new Response(data, { headers })\n } catch {\n return new Response('Font unavailable', { status: 502 })\n }\n },\n}\n\nexport const themeFontEndpoints: Endpoint[] = [cssEndpoint, fileEndpoint]\n"],"names":["buildGoogleFontsHref","parseFontFamilies","THEME_FONTS_CSS_PATH","THEME_FONTS_FILE_PATH","BROWSER_UA","CSS_TTL_MS","MAX_CACHED_FILES","FONT_FILE_PATTERN","cssCache","Map","fileCache","rememberFile","key","data","size","oldest","keys","next","value","undefined","delete","set","apiRouteOf","req","payload","config","routes","api","queryValue","fromSearchParams","searchParams","get","fromQuery","query","rewriteFontUrls","css","fileEndpoint","replace","match","path","test","encodeURIComponent","cssEndpoint","method","handler","families","href","Response","status","headers","cacheKey","join","cached","expires","Date","now","upstream","fetch","ok","Error","text","error","logger","warn","String","arrayBuffer","themeFontEndpoints"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AppearancePreviewField.d.ts","sourceRoot":"","sources":["../../src/fields/AppearancePreviewField.tsx"],"names":[],"mappings":"AA6PA,MAAM,CAAC,OAAO,UAAU,sBAAsB,gCAqY7C"}
1
+ {"version":3,"file":"AppearancePreviewField.d.ts","sourceRoot":"","sources":["../../src/fields/AppearancePreviewField.tsx"],"names":[],"mappings":"AAgRA,MAAM,CAAC,OAAO,UAAU,sBAAsB,gCAqb7C"}
@@ -1,11 +1,13 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useFormFields } from '@payloadcms/ui';
4
- import { Monitor, Moon, Smartphone, Sparkles, Sun, Tablet } from 'lucide-react';
4
+ import { ExternalLink, Monitor, Moon, Smartphone, Sparkles, Sun, Tablet } from 'lucide-react';
5
5
  import { useId, useMemo, useState } from 'react';
6
6
  import { DEFAULT_DEVICE_PREVIEW_ID, DEVICE_PREVIEWS, getDevicePreview } from '../constants/devicePreviews.js';
7
7
  import { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js';
8
8
  import { resolveTypographyPreview } from '../components/typographyPreviewUtils.js';
9
+ import { useNativeLivePreview } from '../hooks/useNativeLivePreview.js';
10
+ import { useThemeFonts } from '../hooks/useThemeFonts.js';
9
11
  import { borderRadiusPresets } from '../providers/Theme/themeConfig.js';
10
12
  const DEVICE_ICONS = {
11
13
  mobile: Smartphone,
@@ -49,13 +51,20 @@ function radiusToPx(token) {
49
51
  const css = preset?.css;
50
52
  return css?.['--radius-large'] ?? css?.['--radius-default'] ?? '0.75rem';
51
53
  }
54
+ const SPACING_SCALE = {
55
+ compact: 0.75,
56
+ medium: 1,
57
+ comfortable: 1.5,
58
+ spacious: 1.5
59
+ };
60
+ const px = (value, c)=>`${Math.round(value * c.space)}px`;
52
61
  function cardSurface(c, p, radius) {
53
62
  const base = {
54
63
  background: p.card,
55
64
  color: p.cardForeground,
56
65
  borderRadius: radius,
57
- border: `${c.borderWidth} ${c.borderStyle === 'none' ? 'solid' : c.borderStyle} ${p.border}`,
58
- padding: '16px',
66
+ border: c.borderStyle === 'none' || c.borderWidth === '0px' ? 'none' : `${c.borderWidth} ${c.borderStyle} ${p.border}`,
67
+ padding: px(16, c),
59
68
  transition: 'transform 200ms ease, box-shadow 200ms ease'
60
69
  };
61
70
  // Card style takes precedence for the structural look, effect style for depth.
@@ -172,7 +181,7 @@ function navbarStyle(c, p) {
172
181
  display: 'flex',
173
182
  alignItems: 'center',
174
183
  justifyContent: 'space-between',
175
- padding: '10px 14px',
184
+ padding: `${px(10, c)} ${px(14, c)}`,
176
185
  background: p.background,
177
186
  color: p.foreground,
178
187
  borderBottom: `1px solid ${p.border}`
@@ -252,6 +261,7 @@ export default function AppearancePreviewField() {
252
261
  const [mode, setMode] = useState('light');
253
262
  const [device, setDevice] = useState(DEFAULT_DEVICE_PREVIEW_ID);
254
263
  const uid = useId().replace(/[:]/g, '');
264
+ const nativePreview = useNativeLivePreview();
255
265
  const activeDevice = getDevicePreview(device);
256
266
  // Fonts follow the Typography section (or the theme preset when set to "Use theme preset"),
257
267
  // so this single preview shows colours, type and component styles together.
@@ -262,8 +272,14 @@ export default function AppearancePreviewField() {
262
272
  bodyFontCustom: typographyValue('bodyFontCustom'),
263
273
  headingFontCustom: typographyValue('headingFontCustom'),
264
274
  baseFontSize: typographyValue('baseFontSize'),
265
- lineHeight: typographyValue('lineHeight')
275
+ lineHeight: typographyValue('lineHeight'),
276
+ headingWeight: typographyValue('headingWeight'),
277
+ letterSpacing: typographyValue('letterSpacing')
266
278
  }, formFields?.['themeConfiguration.theme']?.value);
279
+ useThemeFonts([
280
+ typographyValue('bodyFont'),
281
+ typographyValue('headingFont')
282
+ ]);
267
283
  const str = (key, fallback)=>{
268
284
  const v = formFields?.[`themeConfiguration.${key}`]?.value;
269
285
  return typeof v === 'string' && v.trim().length > 0 ? v : fallback;
@@ -287,7 +303,8 @@ export default function AppearancePreviewField() {
287
303
  navbarStyle: str('componentStyles.navbarStyle', 'solid'),
288
304
  footerStyle: str('componentStyles.footerStyle', 'standard'),
289
305
  linkStyle: str('componentStyles.linkStyle', 'underline-hover'),
290
- borderRadius: str('borderRadius', 'medium')
306
+ borderRadius: str('borderRadius', 'medium'),
307
+ space: SPACING_SCALE[str('spacing', 'medium')] ?? 1
291
308
  };
292
309
  const modeKey = mode === 'light' ? 'lightMode' : 'darkMode';
293
310
  const fallback = mode === 'light' ? {
@@ -332,6 +349,10 @@ export default function AppearancePreviewField() {
332
349
  Object.keys(fallback).forEach((key)=>{
333
350
  p[key] = str(`${modeKey}.${key}`, fallback[key]);
334
351
  });
352
+ // Same fallback as the site's --gradient-brand: primary → accent when no stops are set.
353
+ p.gradientFrom = str(`${modeKey}.gradientFrom`, p.primary);
354
+ p.gradientTo = str(`${modeKey}.gradientTo`, p.accent);
355
+ p.gradientVia = str(`${modeKey}.gradientVia`, p.gradientFrom);
335
356
  return p;
336
357
  }, [
337
358
  formFields,
@@ -362,45 +383,61 @@ export default function AppearancePreviewField() {
362
383
  const btnHoverCSS = `.${uid} .tm-prev-btn:hover{filter:brightness(1.05)}${choices.buttonVariant === 'brutal' ? `.${uid} .tm-prev-btn:hover{transform:translate(-2px,-2px);box-shadow:5px 5px 0 ${palette.foreground}}` : ''}`;
363
384
  // Movement-based hover effects only run for users who haven't asked for reduced motion.
364
385
  const previewCSS = `@media (prefers-reduced-motion: no-preference){${hoverCSS}${btnHoverCSS}}`;
365
- const chipLabels = t.chips;
366
- const chips = [
367
- [
368
- chipLabels.effect,
369
- choices.effectStyle
370
- ],
371
- [
372
- chipLabels.shadow,
373
- choices.shadowIntensity
374
- ],
375
- [
376
- chipLabels.card,
377
- choices.cardStyle
378
- ],
379
- [
380
- chipLabels.hover,
381
- choices.cardHoverEffect
382
- ],
383
- [
384
- chipLabels.button,
385
- `${choices.buttonVariant} / ${choices.buttonSize}`
386
- ],
387
- [
388
- chipLabels.navbar,
389
- choices.navbarStyle
390
- ],
391
- [
392
- chipLabels.footer,
393
- choices.footerStyle
394
- ],
395
- [
396
- chipLabels.link,
397
- choices.linkStyle
398
- ],
399
- [
400
- chipLabels.radius,
401
- choices.borderRadius
402
- ]
403
- ];
386
+ // Content image treatment, mirroring the site's [data-img] rules.
387
+ const imageCss = (()=>{
388
+ const base = {
389
+ aspectRatio: '4 / 3',
390
+ borderRadius: 0,
391
+ backgroundImage: `radial-gradient(circle at 70% 30%, ${palette.accent} 0 18%, transparent 19%), linear-gradient(160deg, ${palette.gradientFrom}, ${palette.gradientTo})`
392
+ };
393
+ switch(choices.imageStyle){
394
+ case 'rounded':
395
+ return {
396
+ ...base,
397
+ borderRadius: radius
398
+ };
399
+ case 'circle':
400
+ return {
401
+ ...base,
402
+ aspectRatio: '1 / 1',
403
+ borderRadius: '9999px'
404
+ };
405
+ case 'grayscale':
406
+ return {
407
+ ...base,
408
+ filter: 'grayscale(1)'
409
+ };
410
+ case 'duotone':
411
+ return {
412
+ ...base,
413
+ filter: 'grayscale(1) contrast(1.05)',
414
+ mixBlendMode: 'luminosity'
415
+ };
416
+ case 'vignette':
417
+ return {
418
+ ...base,
419
+ boxShadow: 'inset 0 0 40px rgba(0,0,0,0.45)'
420
+ };
421
+ case 'polaroid':
422
+ return {
423
+ ...base,
424
+ border: '8px solid #fff',
425
+ borderBottomWidth: '24px',
426
+ boxShadow: '0 6px 16px rgba(0,0,0,0.18)',
427
+ borderRadius: '2px'
428
+ };
429
+ default:
430
+ return base;
431
+ }
432
+ })();
433
+ const headingStyle = {
434
+ fontFamily: typography.headingFont,
435
+ fontWeight: Number(typography.headingWeight) || 700,
436
+ letterSpacing: typography.headingLetterSpacing,
437
+ textTransform: typography.headingTransform,
438
+ lineHeight: 1.15,
439
+ margin: 0
440
+ };
404
441
  const footerStyleCss = choices.footerStyle === 'dark' ? {
405
442
  background: palette.foreground,
406
443
  color: palette.background
@@ -421,6 +458,46 @@ export default function AppearancePreviewField() {
421
458
  color: palette.mutedForeground,
422
459
  borderTop: `1px solid ${palette.border}`
423
460
  };
461
+ // With Payload's native Live Preview the real site is the preview: no mock.
462
+ if (nativePreview.isEnabled) {
463
+ if (nativePreview.isOpen) return null;
464
+ return /*#__PURE__*/ _jsxs("div", {
465
+ className: "tm-native-preview-hint",
466
+ children: [
467
+ /*#__PURE__*/ _jsxs("div", {
468
+ className: "tm-native-preview-hint__text",
469
+ children: [
470
+ /*#__PURE__*/ _jsxs("span", {
471
+ className: "tm-native-preview-hint__title",
472
+ children: [
473
+ /*#__PURE__*/ _jsx(Sparkles, {
474
+ size: 14,
475
+ "aria-hidden": true
476
+ }),
477
+ t.nativeTitle
478
+ ]
479
+ }),
480
+ /*#__PURE__*/ _jsx("span", {
481
+ className: "tm-native-preview-hint__body",
482
+ children: t.nativeBody
483
+ })
484
+ ]
485
+ }),
486
+ /*#__PURE__*/ _jsxs("button", {
487
+ type: "button",
488
+ className: "tm-native-preview-hint__action",
489
+ onClick: nativePreview.open,
490
+ children: [
491
+ /*#__PURE__*/ _jsx(ExternalLink, {
492
+ size: 14,
493
+ "aria-hidden": true
494
+ }),
495
+ t.nativeOpen
496
+ ]
497
+ })
498
+ ]
499
+ });
500
+ }
424
501
  return(// The slot spans the whole right column of the Appearance tab on wide admins;
425
502
  // its inner panel is sticky so the preview stays in view while editing.
426
503
  /*#__PURE__*/ _jsx("div", {
@@ -621,28 +698,27 @@ export default function AppearancePreviewField() {
621
698
  }),
622
699
  /*#__PURE__*/ _jsxs("div", {
623
700
  style: {
624
- padding: '20px',
701
+ padding: px(20, choices),
625
702
  display: 'grid',
626
- gap: '14px'
703
+ gap: px(14, choices)
627
704
  },
628
705
  children: [
629
706
  /*#__PURE__*/ _jsxs("div", {
630
707
  children: [
631
708
  /*#__PURE__*/ _jsx("div", {
632
709
  style: {
633
- fontFamily: typography.headingFont,
634
- fontSize: '22px',
635
- fontWeight: 700,
636
- lineHeight: 1.2
710
+ ...headingStyle,
711
+ fontSize: '24px'
637
712
  },
638
713
  children: t.heading
639
714
  }),
640
715
  /*#__PURE__*/ _jsxs("p", {
641
716
  style: {
642
- margin: '6px 0 0',
717
+ margin: `${px(6, choices)} 0 0`,
643
718
  fontSize: `calc(${typography.baseFontSize} * 0.8125)`,
644
719
  lineHeight: typography.lineHeight,
645
- color: palette.mutedForeground
720
+ color: palette.mutedForeground,
721
+ maxWidth: '60ch'
646
722
  },
647
723
  children: [
648
724
  t.body,
@@ -655,55 +731,78 @@ export default function AppearancePreviewField() {
655
731
  })
656
732
  ]
657
733
  }),
658
- /*#__PURE__*/ _jsx("div", {
659
- className: "tm-prev-card",
660
- style: card,
661
- children: /*#__PURE__*/ _jsxs("div", {
662
- style: {
663
- display: 'flex',
664
- gap: '10px',
665
- alignItems: 'center',
666
- flexWrap: 'wrap'
667
- },
668
- children: [
669
- /*#__PURE__*/ _jsx("button", {
670
- type: "button",
671
- className: "tm-prev-btn",
672
- style: btn,
673
- children: t.primary
674
- }),
675
- /*#__PURE__*/ _jsx("button", {
676
- type: "button",
677
- style: {
678
- ...BUTTON_SIZE_MAP[choices.buttonSize] ?? BUTTON_SIZE_MAP.medium,
679
- fontWeight: 600,
680
- cursor: 'pointer',
681
- borderRadius: radius,
682
- background: palette.secondary,
683
- color: palette.secondaryForeground,
684
- border: 'none'
685
- },
686
- children: t.secondary
687
- }),
688
- /*#__PURE__*/ _jsx("span", {
689
- "aria-hidden": true,
690
- style: {
691
- marginLeft: 'auto',
692
- width: '54px',
693
- height: '22px',
694
- borderRadius: '6px',
695
- backgroundImage: `linear-gradient(135deg, ${palette.gradientFrom}, ${palette.gradientVia}, ${palette.gradientTo})`,
696
- boxShadow: 'inset 0 0 0 1px rgba(0,0,0,0.08)'
697
- }
698
- })
699
- ]
700
- })
734
+ /*#__PURE__*/ _jsxs("div", {
735
+ style: {
736
+ display: 'grid',
737
+ gridTemplateColumns: device === 'mobile' ? '1fr' : 'minmax(0, 3fr) minmax(0, 2fr)',
738
+ gap: px(12, choices),
739
+ alignItems: 'start'
740
+ },
741
+ children: [
742
+ /*#__PURE__*/ _jsxs("div", {
743
+ className: "tm-prev-card",
744
+ style: card,
745
+ children: [
746
+ /*#__PURE__*/ _jsx("div", {
747
+ style: {
748
+ ...headingStyle,
749
+ fontSize: '15px',
750
+ marginBottom: px(6, choices)
751
+ },
752
+ children: t.cardTitle
753
+ }),
754
+ /*#__PURE__*/ _jsx("p", {
755
+ style: {
756
+ margin: `0 0 ${px(12, choices)}`,
757
+ fontSize: `calc(${typography.baseFontSize} * 0.75)`,
758
+ lineHeight: typography.lineHeight,
759
+ color: palette.cardForeground,
760
+ opacity: 0.8
761
+ },
762
+ children: t.cardBody
763
+ }),
764
+ /*#__PURE__*/ _jsxs("div", {
765
+ style: {
766
+ display: 'flex',
767
+ gap: '8px',
768
+ alignItems: 'center',
769
+ flexWrap: 'wrap'
770
+ },
771
+ children: [
772
+ /*#__PURE__*/ _jsx("button", {
773
+ type: "button",
774
+ className: "tm-prev-btn",
775
+ style: btn,
776
+ children: t.primary
777
+ }),
778
+ /*#__PURE__*/ _jsx("button", {
779
+ type: "button",
780
+ style: {
781
+ ...BUTTON_SIZE_MAP[choices.buttonSize] ?? BUTTON_SIZE_MAP.medium,
782
+ fontWeight: 600,
783
+ cursor: 'pointer',
784
+ borderRadius: choices.buttonVariant === 'pill' ? '9999px' : radius,
785
+ background: palette.secondary,
786
+ color: palette.secondaryForeground,
787
+ border: 'none'
788
+ },
789
+ children: t.secondary
790
+ })
791
+ ]
792
+ })
793
+ ]
794
+ }),
795
+ /*#__PURE__*/ _jsx("div", {
796
+ "aria-hidden": true,
797
+ style: imageCss
798
+ })
799
+ ]
701
800
  })
702
801
  ]
703
802
  }),
704
803
  /*#__PURE__*/ _jsxs("div", {
705
804
  style: {
706
- padding: '12px 16px',
805
+ padding: `${px(12, choices)} ${px(16, choices)}`,
707
806
  fontSize: '11px',
708
807
  ...footerStyleCss
709
808
  },
@@ -718,42 +817,6 @@ export default function AppearancePreviewField() {
718
817
  })
719
818
  ]
720
819
  })
721
- }),
722
- /*#__PURE__*/ _jsx("div", {
723
- style: {
724
- display: 'flex',
725
- flexWrap: 'wrap',
726
- gap: '5px',
727
- marginTop: '10px'
728
- },
729
- children: chips.map(([k, v])=>/*#__PURE__*/ _jsxs("span", {
730
- style: {
731
- fontSize: '11px',
732
- padding: '2px 7px',
733
- borderRadius: '999px',
734
- background: 'var(--theme-elevation-50)',
735
- border: '1px solid var(--theme-elevation-150)',
736
- color: 'var(--theme-elevation-700)'
737
- },
738
- children: [
739
- /*#__PURE__*/ _jsxs("span", {
740
- style: {
741
- color: 'var(--theme-elevation-600)'
742
- },
743
- children: [
744
- k,
745
- ":"
746
- ]
747
- }),
748
- " ",
749
- /*#__PURE__*/ _jsx("strong", {
750
- style: {
751
- fontWeight: 600
752
- },
753
- children: v
754
- })
755
- ]
756
- }, k))
757
820
  })
758
821
  ]
759
822
  })