@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.
- package/README.md +37 -7
- package/dist/components/ServerThemeInjector.d.ts +13 -1
- package/dist/components/ServerThemeInjector.d.ts.map +1 -1
- package/dist/components/ServerThemeInjector.js +24 -1
- package/dist/components/ServerThemeInjector.js.map +1 -1
- package/dist/components/ThemeLivePreviewSync.d.ts +3 -0
- package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
- package/dist/components/ThemeLivePreviewSync.js +28 -4
- package/dist/components/ThemeLivePreviewSync.js.map +1 -1
- package/dist/components/typographyPreviewUtils.d.ts +5 -0
- package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
- package/dist/components/typographyPreviewUtils.js +16 -10
- package/dist/components/typographyPreviewUtils.js.map +1 -1
- package/dist/constants/themeFonts.d.ts +94 -168
- package/dist/constants/themeFonts.d.ts.map +1 -1
- package/dist/constants/themeFonts.js +260 -140
- package/dist/constants/themeFonts.js.map +1 -1
- package/dist/endpoints/themeFonts.d.ts +5 -0
- package/dist/endpoints/themeFonts.d.ts.map +1 -0
- package/dist/endpoints/themeFonts.js +138 -0
- package/dist/endpoints/themeFonts.js.map +1 -0
- package/dist/fields/AppearancePreviewField.d.ts.map +1 -1
- package/dist/fields/AppearancePreviewField.js +196 -133
- package/dist/fields/AppearancePreviewField.js.map +1 -1
- package/dist/fields/FontSelectField.css +155 -0
- package/dist/fields/FontSelectField.d.ts +4 -2
- package/dist/fields/FontSelectField.d.ts.map +1 -1
- package/dist/fields/FontSelectField.js +80 -118
- package/dist/fields/FontSelectField.js.map +1 -1
- package/dist/fields/StylePresetField.d.ts.map +1 -1
- package/dist/fields/StylePresetField.js +31 -30
- package/dist/fields/StylePresetField.js.map +1 -1
- package/dist/fields/ThemePreviewField.css +63 -1
- package/dist/fields/ThemePreviewField.d.ts.map +1 -1
- package/dist/fields/ThemePreviewField.js +2 -5
- package/dist/fields/ThemePreviewField.js.map +1 -1
- package/dist/fields/themeConfigurationField.d.ts.map +1 -1
- package/dist/fields/themeConfigurationField.js +320 -271
- package/dist/fields/themeConfigurationField.js.map +1 -1
- package/dist/hooks/useNativeLivePreview.d.ts +15 -0
- package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
- package/dist/hooks/useNativeLivePreview.js +18 -0
- package/dist/hooks/useNativeLivePreview.js.map +1 -0
- package/dist/hooks/useThemeFonts.d.ts +10 -0
- package/dist/hooks/useThemeFonts.d.ts.map +1 -0
- package/dist/hooks/useThemeFonts.js +36 -0
- package/dist/hooks/useThemeFonts.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/next-fonts.d.ts +3 -0
- package/dist/next-fonts.d.ts.map +1 -0
- package/dist/next-fonts.js +360 -0
- package/dist/next-fonts.js.map +1 -0
- package/dist/payload-types.d.ts +10 -6
- package/dist/payload-types.d.ts.map +1 -1
- package/dist/payload-types.js.map +1 -1
- package/dist/presets.d.ts +6 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/style-presets.d.ts +27 -18
- package/dist/style-presets.d.ts.map +1 -1
- package/dist/style-presets.js +399 -217
- package/dist/style-presets.js.map +1 -1
- package/dist/translations.d.ts +5 -0
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +10 -0
- package/dist/translations.js.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.d.ts +6 -1
- package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.js +11 -1
- package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
- package/dist/utils/resolveThemeConfiguration.d.ts +15 -0
- package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
- package/dist/utils/resolveThemeConfiguration.js +41 -8
- package/dist/utils/resolveThemeConfiguration.js.map +1 -1
- package/dist/utils/themeUtils.d.ts.map +1 -1
- package/dist/utils/themeUtils.js +60 -2
- package/dist/utils/themeUtils.js.map +1 -1
- 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":"AACA,SACEA,oBAAoB,EACpBC,iBAAiB,EACjBC,oBAAoB,EACpBC,qBAAqB,QAChB,6BAA4B;AAEnC;;;;;;;;;;;CAWC,GAED,qFAAqF;AACrF,MAAMC,aACJ;AACF,MAAMC,aAAa,KAAK,KAAK,KAAK;AAClC,MAAMC,mBAAmB;AACzB,MAAMC,oBAAoB;AAE1B,MAAMC,WAAW,IAAIC;AACrB,MAAMC,YAAY,IAAID;AAEtB,SAASE,aAAaC,GAAW,EAAEC,IAAiB;IAClD,IAAIH,UAAUI,IAAI,IAAIR,kBAAkB;QACtC,MAAMS,SAASL,UAAUM,IAAI,GAAGC,IAAI,GAAGC,KAAK;QAC5C,IAAIH,WAAWI,WAAWT,UAAUU,MAAM,CAACL;IAC7C;IACAL,UAAUW,GAAG,CAACT,KAAKC;AACrB;AAEA,MAAMS,aAAa,CAACC,MAAwBA,IAAIC,OAAO,EAAEC,QAAQC,QAAQC,OAAO;AAEhF,MAAMC,aAAa,CAACL,KAAqBX;IACvC,MAAMiB,mBAAmBN,IAAIO,YAAY,EAAEC,MAAMnB;IACjD,IAAIiB,kBAAkB,OAAOA;IAC7B,MAAMG,YAAaT,IAAIU,KAAK,EAA0C,CAACrB,IAAI;IAC3E,OAAO,OAAOoB,cAAc,WAAWA,YAAY;AACrD;AAEA,sFAAsF,GACtF,OAAO,SAASE,gBAAgBC,GAAW,EAAEC,YAAoB;IAC/D,OAAOD,IAAIE,OAAO,CAAC,+CAA+C,CAACC,OAAOC,OACxEhC,kBAAkBiC,IAAI,CAACD,QAAQ,GAAGH,aAAa,GAAG,EAAEK,mBAAmBF,OAAO,GAAGD;AAErF;AAEA,MAAMI,cAAwB;IAC5BH,MAAMrC;IACNyC,QAAQ;IACRC,SAAS,OAAOrB;QACd,MAAMsB,WAAW5C,kBAAkB2B,WAAWL,KAAK;QACnD,MAAMuB,OAAO9C,qBAAqB6C;QAClC,IAAI,CAACC,MAAM;YACT,OAAO,IAAIC,SAAS,4BAA4B;gBAC9CC,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAA0B;YACvD;QACF;QAEA,MAAMC,WAAWL,SAASM,IAAI,CAAC;QAC/B,MAAMC,SAAS5C,SAASuB,GAAG,CAACmB;QAC5B,IAAIE,UAAUA,OAAOC,OAAO,GAAGC,KAAKC,GAAG,IAAI;YACzC,OAAO,IAAIR,SAASK,OAAOjB,GAAG,EAAE;gBAC9Bc,SAAS;oBACP,gBAAgB;oBAChB,iBAAiB;gBACnB;YACF;QACF;QAEA,IAAI;YACF,MAAMO,WAAW,MAAMC,MAAMX,MAAM;gBAAEG,SAAS;oBAAE,cAAc7C;gBAAW;YAAE;YAC3E,IAAI,CAACoD,SAASE,EAAE,EAAE,MAAM,IAAIC,MAAM,CAAC,uBAAuB,EAAEH,SAASR,MAAM,EAAE;YAC7E,MAAMb,MAAMD,gBAAgB,MAAMsB,SAASI,IAAI,IAAI,GAAGtC,WAAWC,OAAOpB,uBAAuB;YAC/FK,SAASa,GAAG,CAAC6B,UAAU;gBAAEf;gBAAKkB,SAASC,KAAKC,GAAG,KAAKlD;YAAW;YAC/D,OAAO,IAAI0C,SAASZ,KAAK;gBACvBc,SAAS;oBACP,gBAAgB;oBAChB,iBAAiB;gBACnB;YACF;QACF,EAAE,OAAOY,OAAO;YACdtC,IAAIC,OAAO,EAAEsC,QAAQC,OACnB,CAAC,+CAA+C,EAAEb,SAAS,GAAG,EAAEc,OAAOH,QAAQ;YAEjF,sEAAsE;YACtE,OAAO,IAAId,SAAS,0DAA0D;gBAC5EC,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;oBAA2B,iBAAiB;gBAAW;YACpF;QACF;IACF;AACF;AAEA,MAAMb,eAAyB;IAC7BG,MAAMpC;IACNwC,QAAQ;IACRC,SAAS,OAAOrB;QACd,MAAMgB,OAAOX,WAAWL,KAAK;QAC7B,IAAI,CAACgB,QAAQ,CAAChC,kBAAkBiC,IAAI,CAACD,OAAO;YAC1C,OAAO,IAAIQ,SAAS,aAAa;gBAAEC,QAAQ;YAAI;QACjD;QAEA,MAAMC,UAAU;YACd,gBAAgB;YAChB,wDAAwD;YACxD,iBAAiB;YACjB,+BAA+B;QACjC;QAEA,MAAMG,SAAS1C,UAAUqB,GAAG,CAACQ;QAC7B,IAAIa,QAAQ,OAAO,IAAIL,SAASK,QAAQ;YAAEH;QAAQ;QAElD,IAAI;YACF,MAAMO,WAAW,MAAMC,MAAM,CAAC,0BAA0B,EAAElB,MAAM,EAAE;gBAChEU,SAAS;oBAAE,cAAc7C;gBAAW;YACtC;YACA,IAAI,CAACoD,SAASE,EAAE,EAAE,OAAO,IAAIX,SAAS,aAAa;gBAAEC,QAAQ;YAAI;YACjE,MAAMnC,OAAO,MAAM2C,SAASS,WAAW;YACvCtD,aAAa4B,MAAM1B;YACnB,OAAO,IAAIkC,SAASlC,MAAM;gBAAEoC;YAAQ;QACtC,EAAE,OAAM;YACN,OAAO,IAAIF,SAAS,oBAAoB;gBAAEC,QAAQ;YAAI;QACxD;IACF;AACF;AAEA,OAAO,MAAMkB,qBAAiC;IAACxB;IAAaN;CAAa,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AppearancePreviewField.d.ts","sourceRoot":"","sources":["../../src/fields/AppearancePreviewField.tsx"],"names":[],"mappings":"
|
|
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:
|
|
58
|
-
padding:
|
|
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:
|
|
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
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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:
|
|
701
|
+
padding: px(20, choices),
|
|
625
702
|
display: 'grid',
|
|
626
|
-
gap:
|
|
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
|
-
|
|
634
|
-
fontSize: '
|
|
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:
|
|
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__*/
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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:
|
|
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
|
})
|