@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
package/README.md CHANGED
@@ -115,6 +115,32 @@ export default async function RootLayout({ children }: { children: React.ReactNo
115
115
  }
116
116
  ```
117
117
 
118
+ That is all an app needs. The fonts the editor picks are served from **your own
119
+ domain** by the plugin's `/api/theme-fonts/*` endpoints (fetched and cached on the
120
+ server), so visitors' browsers never contact Google (GDPR-friendly), with no setup.
121
+
122
+ ### 3) Optional: fonts via `next/font` (Next.js best practice)
123
+
124
+ For build-time self-hosting with metric-matched fallbacks (no layout shift), add
125
+ one import. Every catalog font is declared with `preload: false`, so a page only
126
+ downloads the face it actually uses:
127
+
128
+ ```tsx
129
+ import { themeFontVariables } from '@kilivi/payloadcms-theme-management/next-fonts'
130
+
131
+ <html lang="en" className={themeFontVariables}>
132
+ <head>
133
+ <ServerThemeInjector themeConfiguration={appearance?.themeConfiguration} fontLoading="none" />
134
+ </head>
135
+ ```
136
+
137
+ If your Next.js setup does not compile packages from `node_modules`, add
138
+ `'@kilivi/payloadcms-theme-management'` to `transpilePackages` in `next.config`.
139
+ Pass `fontLoading="none"` to `ThemeLivePreviewSync` as well if you use it.
140
+
141
+ `fontLoading` options: `'self-hosted'` (default), `'google'` (Google Fonts CDN),
142
+ `'none'` (you load the faces, e.g. with `next/font`).
143
+
118
144
  ## The Appearance Settings tab
119
145
 
120
146
  The tab is ordered as a **presets first, fine-tuning second** workflow:
@@ -123,17 +149,21 @@ The tab is ordered as a **presets first, fine-tuning second** workflow:
123
149
  - *Keep my: Colours · Fonts · Style* locks
124
150
  - the palette generator
125
151
  - the collapsible theme preset list
126
- 2. **Live preview:** one preview for colours, fonts and component styles. Once the settings group is at least 960 px wide (a container query, not the viewport width), it moves into a sticky right-hand column that stays visible while you edit anything below it.
127
- 3. **Style preset:** visual cards that set effects, component styles, radius and typography, but never colours.
128
- 4. **Border radius and spacing scale.**
129
- 5. **Fine-tuning sections:** these are collapsed by default.
130
- - Colour mode (all light and dark tokens, the WCAG audit and export)
131
- - Typography
132
- - Custom theme presets (JSON import)
152
+ 2. **Live preview:** with Payload's native Live Preview configured (`livePreview` option), the real site is the preview: the tab shows an *Open Live Preview* button instead of a mock, and nothing while the panel is open, so the fields get the full width. Without it, one mock preview covers colours, fonts and component styles. Once the settings group is at least 960 px wide (a container query, not the viewport width), it moves into a sticky right-hand column that stays visible while you edit anything below it.
153
+ 3. **Style preset:** 17 complete looks (Swiss, Editorial, Luxury, Art Deco, Tech, Poster, Brutalism, Retro, Glass …). Each sets fonts, text size, heading weight and tracking, radius, spacing, effects and every component style — but never colours. Combine any style with any colour theme.
154
+ 4. **Typography:** body and heading font (30 curated Google fonts with Czech diacritics, grouped by category), base size, line height, heading weight and heading letter spacing.
155
+ 5. **Border radius and spacing scale.**
156
+ 6. **Fine-tuning sections:** these are collapsed by default.
133
157
  - Visual effects
134
158
  - Component styles
159
+ - Colour mode (all light and dark tokens, the WCAG audit and export)
160
+ - Custom theme presets (JSON import, for developers)
135
161
  - Advanced (animation level, custom CSS)
136
162
 
163
+ The generated CSS applies the chosen fonts to `body` and headings with
164
+ zero-specificity `:where()` rules — only once the editor has picked typography,
165
+ so untouched sites render as before and your own CSS always wins.
166
+
137
167
  ### Locks and undo
138
168
 
139
169
  Each bulk action honours the locks:
@@ -1,3 +1,4 @@
1
+ import { type ThemeFontLoading } from '../constants/themeFonts.js';
1
2
  interface ServerThemeInjectorProps {
2
3
  /**
3
4
  * Theme configuration object from your site settings.
@@ -10,11 +11,22 @@ interface ServerThemeInjectorProps {
10
11
  * ```
11
12
  */
12
13
  themeConfiguration?: unknown;
14
+ /**
15
+ * How the body / heading fonts picked in the admin are loaded:
16
+ * - `'self-hosted'` (default) — served from your own origin by the plugin's
17
+ * `/api/theme-fonts/*` endpoints; visitors never contact Google (GDPR-friendly).
18
+ * - `'google'` — straight from the Google Fonts CDN.
19
+ * - `'none'` — you load the faces yourself, e.g. with `next/font` exposing the
20
+ * `--font-*` variables listed in `FONT_CATALOG` (they take precedence).
21
+ */
22
+ fontLoading?: ThemeFontLoading;
23
+ /** Payload `routes.api` when it is not '/api'. */
24
+ apiRoute?: string;
13
25
  }
14
26
  /**
15
27
  * Server-side theme CSS injector to prevent FOUC
16
28
  * This component injects critical theme CSS directly into the HTML head during SSR
17
29
  */
18
- export declare function ServerThemeInjector({ themeConfiguration, }: Readonly<ServerThemeInjectorProps>): Promise<import("react").JSX.Element>;
30
+ export declare function ServerThemeInjector({ themeConfiguration, fontLoading, apiRoute, }: Readonly<ServerThemeInjectorProps>): Promise<import("react").JSX.Element>;
19
31
  export {};
20
32
  //# sourceMappingURL=ServerThemeInjector.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ServerThemeInjector.d.ts","sourceRoot":"","sources":["../../src/components/ServerThemeInjector.tsx"],"names":[],"mappings":"AAUA,UAAU,wBAAwB;IAChC;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAKD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,kBAAkB,GACnB,EAAE,QAAQ,CAAC,wBAAwB,CAAC,wCAqHpC"}
1
+ {"version":3,"file":"ServerThemeInjector.d.ts","sourceRoot":"","sources":["../../src/components/ServerThemeInjector.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAWvF,UAAU,wBAAwB;IAChC;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B;;;;;;;OAOG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAMD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,kBAAkB,EAClB,WAA2B,EAC3B,QAAiB,GAClB,EAAE,QAAQ,CAAC,wBAAwB,CAAC,wCAiIpC"}
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { buildThemeFontsHref } from '../constants/themeFonts.js';
2
3
  import { getBorderRadiusConfig } from '../providers/Theme/themeConfig.js';
3
4
  import { resolveThemeConfiguration } from '../utils/resolveThemeConfiguration.js';
4
5
  import { createFallbackCriticalCSS, getThemeCriticalCSS } from '../utils/themeAssets.js';
@@ -8,9 +9,13 @@ import { InitTheme } from './InitTheme.js';
8
9
  /**
9
10
  * Server-side theme CSS injector to prevent FOUC
10
11
  * This component injects critical theme CSS directly into the HTML head during SSR
11
- */ export async function ServerThemeInjector({ themeConfiguration }) {
12
+ */ export async function ServerThemeInjector({ themeConfiguration, fontLoading = 'self-hosted', apiRoute = '/api' }) {
12
13
  // resolveThemeConfiguration accepts `unknown` — no cast needed
13
14
  const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration);
15
+ const fontsHref = buildThemeFontsHref(resolvedConfiguration.fontSelections ?? [], {
16
+ mode: fontLoading,
17
+ apiRoute
18
+ });
14
19
  const { theme, borderRadius, customCSS, lightMode, darkMode, fontScale, spacing, animationLevel, allowColorModeToggle, colorMode, typography, visualEffects, componentStyles } = resolvedConfiguration;
15
20
  // Use resolved configuration directly
16
21
  const normalizedThemeConfiguration = {
@@ -70,6 +75,24 @@ ${borderRadiusCSS}
70
75
  return /*#__PURE__*/ _jsxs(_Fragment, {
71
76
  children: [
72
77
  /*#__PURE__*/ _jsx(InitTheme, {}),
78
+ fontsHref && fontLoading === 'google' ? /*#__PURE__*/ _jsxs(_Fragment, {
79
+ children: [
80
+ /*#__PURE__*/ _jsx("link", {
81
+ rel: "preconnect",
82
+ href: "https://fonts.googleapis.com"
83
+ }),
84
+ /*#__PURE__*/ _jsx("link", {
85
+ rel: "preconnect",
86
+ href: "https://fonts.gstatic.com",
87
+ crossOrigin: ""
88
+ })
89
+ ]
90
+ }) : null,
91
+ fontsHref ? /*#__PURE__*/ _jsx("link", {
92
+ rel: "stylesheet",
93
+ href: fontsHref,
94
+ precedence: "theme-fonts"
95
+ }) : null,
73
96
  emitBothSchemes ? /*#__PURE__*/ _jsxs(_Fragment, {
74
97
  children: [
75
98
  lightBg ? /*#__PURE__*/ _jsx("meta", {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ServerThemeInjector.tsx"],"sourcesContent":["import type { ThemeTypographyPreset } from '../presets.js'\nimport { getBorderRadiusConfig } from '../providers/Theme/themeConfig.js'\nimport type { ThemeDefaults } from '../providers/Theme/types.js'\nimport { resolveThemeConfiguration } from '../utils/resolveThemeConfiguration.js'\nimport type { ResolvedThemeConfiguration } from '../utils/resolveThemeConfiguration.js'\nimport { createFallbackCriticalCSS, getThemeCriticalCSS } from '../utils/themeAssets.js'\nimport { generateThemeColorsCss } from '../utils/themeColors.js'\nimport { generateThemeCSS } from '../utils/themeUtils.js'\nimport { InitTheme } from './InitTheme.js'\n\ninterface ServerThemeInjectorProps {\n /**\n * Theme configuration object from your site settings.\n * Pass the themeConfiguration field directly, not the entire site settings object.\n *\n * @example\n * ```tsx\n * const siteSettings = await payload.findGlobal({ slug: 'site-settings' })\n * <ServerThemeInjector themeConfiguration={siteSettings.themeConfiguration} />\n * ```\n */\n themeConfiguration?: unknown\n}\n\ntype RuntimeThemeConfiguration = Omit<ResolvedThemeConfiguration, 'typography'> & {\n typography?: ThemeTypographyPreset | null\n}\n/**\n * Server-side theme CSS injector to prevent FOUC\n * This component injects critical theme CSS directly into the HTML head during SSR\n */\nexport async function ServerThemeInjector({\n themeConfiguration,\n}: Readonly<ServerThemeInjectorProps>) {\n // resolveThemeConfiguration accepts `unknown` — no cast needed\n const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration)\n const {\n theme,\n borderRadius,\n customCSS,\n lightMode,\n darkMode,\n fontScale,\n spacing,\n animationLevel,\n allowColorModeToggle,\n colorMode,\n typography,\n visualEffects,\n componentStyles,\n } = resolvedConfiguration\n\n // Use resolved configuration directly\n const normalizedThemeConfiguration: RuntimeThemeConfiguration = {\n theme,\n colorMode,\n allowColorModeToggle,\n borderRadius,\n fontScale,\n spacing,\n animationLevel,\n customCSS: customCSS ?? null,\n lightMode,\n darkMode,\n typography: typography ?? undefined,\n visualEffects: visualEffects ?? null,\n componentStyles: componentStyles ?? null,\n }\n\n let criticalCSS = await getThemeCriticalCSS(theme as ThemeDefaults)\n criticalCSS ??= createFallbackCriticalCSS(theme as ThemeDefaults)\n\n const borderRadiusConfig = getBorderRadiusConfig(borderRadius ?? 'medium')\n const cssRecord =\n typeof borderRadiusConfig?.css === 'string'\n ? { '--radius-default': borderRadiusConfig.css }\n : (borderRadiusConfig?.css ?? {})\n\n const borderRadiusCSS = Object.entries(cssRecord)\n .map(([property, value]) => ` ${property}: ${value};`)\n .join('\\n')\n\n const borderRadiusBlock = borderRadiusCSS\n ? `:root {\n${borderRadiusCSS}\n}`\n : ''\n\n const themeConfigurationCSS = generateThemeCSS(normalizedThemeConfiguration)\n const colorModesCSS = generateThemeColorsCss({ themeName: theme, lightMode, darkMode })\n const customCSSBlock = typeof customCSS === 'string' ? customCSS.trim() : ''\n\n // `color-scheme` makes native UI (form controls, scrollbars) match the theme and\n // avoids a white flash on dark sites. Fixed mode → single scheme; toggle/auto → both.\n const colorScheme =\n allowColorModeToggle === false\n ? colorMode === 'dark'\n ? 'dark'\n : 'light'\n : 'light dark'\n const colorSchemeBlock = `:root {\\n color-scheme: ${colorScheme};\\n}`\n\n const combinedCSS = [\n criticalCSS,\n colorSchemeBlock,\n borderRadiusBlock,\n themeConfigurationCSS,\n colorModesCSS,\n customCSSBlock,\n ]\n .filter((block) => Boolean(block?.trim?.().length))\n .join('\\n\\n')\n\n // <meta name=\"theme-color\"> colours the mobile browser chrome to match the page\n // background. Emit per-scheme variants when the mode can change, otherwise a\n // single tag matching the fixed mode.\n const pick = (mode: typeof lightMode, key: 'background'): string | undefined => {\n const value = (mode as Record<string, unknown> | null | undefined)?.[key]\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined\n }\n const lightBg = pick(lightMode, 'background')\n const darkBg = pick(darkMode, 'background')\n const emitBothSchemes = allowColorModeToggle !== false || colorMode === 'auto'\n\n return (\n <>\n <InitTheme />\n {emitBothSchemes ? (\n <>\n {lightBg ? (\n <meta name=\"theme-color\" media=\"(prefers-color-scheme: light)\" content={lightBg} />\n ) : null}\n {darkBg ? (\n <meta name=\"theme-color\" media=\"(prefers-color-scheme: dark)\" content={darkBg} />\n ) : null}\n </>\n ) : (\n (() => {\n const fixed = colorMode === 'dark' ? darkBg : lightBg\n return fixed ? <meta name=\"theme-color\" content={fixed} /> : null\n })()\n )}\n <style\n id=\"theme-critical-css\"\n dangerouslySetInnerHTML={{\n __html: combinedCSS,\n }}\n />\n </>\n )\n}\n"],"names":["getBorderRadiusConfig","resolveThemeConfiguration","createFallbackCriticalCSS","getThemeCriticalCSS","generateThemeColorsCss","generateThemeCSS","InitTheme","ServerThemeInjector","themeConfiguration","resolvedConfiguration","theme","borderRadius","customCSS","lightMode","darkMode","fontScale","spacing","animationLevel","allowColorModeToggle","colorMode","typography","visualEffects","componentStyles","normalizedThemeConfiguration","undefined","criticalCSS","borderRadiusConfig","cssRecord","css","borderRadiusCSS","Object","entries","map","property","value","join","borderRadiusBlock","themeConfigurationCSS","colorModesCSS","themeName","customCSSBlock","trim","colorScheme","colorSchemeBlock","combinedCSS","filter","block","Boolean","length","pick","mode","key","lightBg","darkBg","emitBothSchemes","meta","name","media","content","fixed","style","id","dangerouslySetInnerHTML","__html"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/ServerThemeInjector.tsx"],"sourcesContent":["import { buildThemeFontsHref, type ThemeFontLoading } from '../constants/themeFonts.js'\nimport type { ThemeTypographyPreset } from '../presets.js'\nimport { getBorderRadiusConfig } from '../providers/Theme/themeConfig.js'\nimport type { ThemeDefaults } from '../providers/Theme/types.js'\nimport { resolveThemeConfiguration } from '../utils/resolveThemeConfiguration.js'\nimport type { ResolvedThemeConfiguration } from '../utils/resolveThemeConfiguration.js'\nimport { createFallbackCriticalCSS, getThemeCriticalCSS } from '../utils/themeAssets.js'\nimport { generateThemeColorsCss } from '../utils/themeColors.js'\nimport { generateThemeCSS } from '../utils/themeUtils.js'\nimport { InitTheme } from './InitTheme.js'\n\ninterface ServerThemeInjectorProps {\n /**\n * Theme configuration object from your site settings.\n * Pass the themeConfiguration field directly, not the entire site settings object.\n *\n * @example\n * ```tsx\n * const siteSettings = await payload.findGlobal({ slug: 'site-settings' })\n * <ServerThemeInjector themeConfiguration={siteSettings.themeConfiguration} />\n * ```\n */\n themeConfiguration?: unknown\n /**\n * How the body / heading fonts picked in the admin are loaded:\n * - `'self-hosted'` (default) — served from your own origin by the plugin's\n * `/api/theme-fonts/*` endpoints; visitors never contact Google (GDPR-friendly).\n * - `'google'` — straight from the Google Fonts CDN.\n * - `'none'` — you load the faces yourself, e.g. with `next/font` exposing the\n * `--font-*` variables listed in `FONT_CATALOG` (they take precedence).\n */\n fontLoading?: ThemeFontLoading\n /** Payload `routes.api` when it is not '/api'. */\n apiRoute?: string\n}\n\ntype RuntimeThemeConfiguration = Omit<ResolvedThemeConfiguration, 'typography'> & {\n typography?: ThemeTypographyPreset | null\n}\n\n/**\n * Server-side theme CSS injector to prevent FOUC\n * This component injects critical theme CSS directly into the HTML head during SSR\n */\nexport async function ServerThemeInjector({\n themeConfiguration,\n fontLoading = 'self-hosted',\n apiRoute = '/api',\n}: Readonly<ServerThemeInjectorProps>) {\n // resolveThemeConfiguration accepts `unknown` — no cast needed\n const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration)\n const fontsHref = buildThemeFontsHref(resolvedConfiguration.fontSelections ?? [], {\n mode: fontLoading,\n apiRoute,\n })\n const {\n theme,\n borderRadius,\n customCSS,\n lightMode,\n darkMode,\n fontScale,\n spacing,\n animationLevel,\n allowColorModeToggle,\n colorMode,\n typography,\n visualEffects,\n componentStyles,\n } = resolvedConfiguration\n\n // Use resolved configuration directly\n const normalizedThemeConfiguration: RuntimeThemeConfiguration = {\n theme,\n colorMode,\n allowColorModeToggle,\n borderRadius,\n fontScale,\n spacing,\n animationLevel,\n customCSS: customCSS ?? null,\n lightMode,\n darkMode,\n typography: typography ?? undefined,\n visualEffects: visualEffects ?? null,\n componentStyles: componentStyles ?? null,\n }\n\n let criticalCSS = await getThemeCriticalCSS(theme as ThemeDefaults)\n criticalCSS ??= createFallbackCriticalCSS(theme as ThemeDefaults)\n\n const borderRadiusConfig = getBorderRadiusConfig(borderRadius ?? 'medium')\n const cssRecord =\n typeof borderRadiusConfig?.css === 'string'\n ? { '--radius-default': borderRadiusConfig.css }\n : (borderRadiusConfig?.css ?? {})\n\n const borderRadiusCSS = Object.entries(cssRecord)\n .map(([property, value]) => ` ${property}: ${value};`)\n .join('\\n')\n\n const borderRadiusBlock = borderRadiusCSS\n ? `:root {\n${borderRadiusCSS}\n}`\n : ''\n\n const themeConfigurationCSS = generateThemeCSS(normalizedThemeConfiguration)\n const colorModesCSS = generateThemeColorsCss({ themeName: theme, lightMode, darkMode })\n const customCSSBlock = typeof customCSS === 'string' ? customCSS.trim() : ''\n\n // `color-scheme` makes native UI (form controls, scrollbars) match the theme and\n // avoids a white flash on dark sites. Fixed mode → single scheme; toggle/auto → both.\n const colorScheme =\n allowColorModeToggle === false\n ? colorMode === 'dark'\n ? 'dark'\n : 'light'\n : 'light dark'\n const colorSchemeBlock = `:root {\\n color-scheme: ${colorScheme};\\n}`\n\n const combinedCSS = [\n criticalCSS,\n colorSchemeBlock,\n borderRadiusBlock,\n themeConfigurationCSS,\n colorModesCSS,\n customCSSBlock,\n ]\n .filter((block) => Boolean(block?.trim?.().length))\n .join('\\n\\n')\n\n // <meta name=\"theme-color\"> colours the mobile browser chrome to match the page\n // background. Emit per-scheme variants when the mode can change, otherwise a\n // single tag matching the fixed mode.\n const pick = (mode: typeof lightMode, key: 'background'): string | undefined => {\n const value = (mode as Record<string, unknown> | null | undefined)?.[key]\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined\n }\n const lightBg = pick(lightMode, 'background')\n const darkBg = pick(darkMode, 'background')\n const emitBothSchemes = allowColorModeToggle !== false || colorMode === 'auto'\n\n return (\n <>\n <InitTheme />\n {fontsHref && fontLoading === 'google' ? (\n <>\n <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" />\n <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossOrigin=\"\" />\n </>\n ) : null}\n {/* `precedence` lets React 19 hoist the stylesheet into <head> and de-duplicate it. */}\n {fontsHref ? <link rel=\"stylesheet\" href={fontsHref} precedence=\"theme-fonts\" /> : null}\n {emitBothSchemes ? (\n <>\n {lightBg ? (\n <meta name=\"theme-color\" media=\"(prefers-color-scheme: light)\" content={lightBg} />\n ) : null}\n {darkBg ? (\n <meta name=\"theme-color\" media=\"(prefers-color-scheme: dark)\" content={darkBg} />\n ) : null}\n </>\n ) : (\n (() => {\n const fixed = colorMode === 'dark' ? darkBg : lightBg\n return fixed ? <meta name=\"theme-color\" content={fixed} /> : null\n })()\n )}\n <style\n id=\"theme-critical-css\"\n dangerouslySetInnerHTML={{\n __html: combinedCSS,\n }}\n />\n </>\n )\n}\n"],"names":["buildThemeFontsHref","getBorderRadiusConfig","resolveThemeConfiguration","createFallbackCriticalCSS","getThemeCriticalCSS","generateThemeColorsCss","generateThemeCSS","InitTheme","ServerThemeInjector","themeConfiguration","fontLoading","apiRoute","resolvedConfiguration","fontsHref","fontSelections","mode","theme","borderRadius","customCSS","lightMode","darkMode","fontScale","spacing","animationLevel","allowColorModeToggle","colorMode","typography","visualEffects","componentStyles","normalizedThemeConfiguration","undefined","criticalCSS","borderRadiusConfig","cssRecord","css","borderRadiusCSS","Object","entries","map","property","value","join","borderRadiusBlock","themeConfigurationCSS","colorModesCSS","themeName","customCSSBlock","trim","colorScheme","colorSchemeBlock","combinedCSS","filter","block","Boolean","length","pick","key","lightBg","darkBg","emitBothSchemes","link","rel","href","crossOrigin","precedence","meta","name","media","content","fixed","style","id","dangerouslySetInnerHTML","__html"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import type { ThemeFontLoading } from '../constants/themeFonts.js';
2
3
  interface ThemeLivePreviewSyncProps {
3
4
  initialData: Record<string, unknown>;
4
5
  serverURL: string;
@@ -6,6 +7,8 @@ interface ThemeLivePreviewSyncProps {
6
7
  depth?: number;
7
8
  themeConfigurationKey?: string;
8
9
  styleElementId?: string;
10
+ /** Same as ServerThemeInjector's `fontLoading`; use 'none' with next/font. */
11
+ fontLoading?: ThemeFontLoading;
9
12
  }
10
13
  export declare const ThemeLivePreviewSync: React.FC<ThemeLivePreviewSyncProps>;
11
14
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeLivePreviewSync.d.ts","sourceRoot":"","sources":["../../src/components/ThemeLivePreviewSync.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA6B,MAAM,OAAO,CAAA;AAMjD,UAAU,yBAAyB;IACjC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA4CpE,CAAA"}
1
+ {"version":3,"file":"ThemeLivePreviewSync.d.ts","sourceRoot":"","sources":["../../src/components/ThemeLivePreviewSync.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAqC,MAAM,OAAO,CAAA;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAMlE,UAAU,yBAAyB;IACjC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,gBAAgB,CAAA;CAC/B;AAED,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA0EpE,CAAA"}
@@ -1,14 +1,15 @@
1
1
  'use client';
2
2
  import { useLivePreview } from '@payloadcms/live-preview-react';
3
- import React, { useEffect, useMemo } from 'react';
3
+ import React, { useEffect, useMemo, useRef } from 'react';
4
4
  import { buildLivePreviewThemeRuntime, extractThemeConfigurationFromLiveData } from '../utils/livePreviewThemeRuntime.js';
5
- export const ThemeLivePreviewSync = ({ initialData, serverURL, apiRoute, depth, themeConfigurationKey = 'themeConfiguration', styleElementId = 'theme-live-preview-css' })=>{
5
+ export const ThemeLivePreviewSync = ({ initialData, serverURL, apiRoute, depth, themeConfigurationKey = 'themeConfiguration', styleElementId = 'theme-live-preview-css', fontLoading = 'self-hosted' })=>{
6
6
  const { data } = useLivePreview({
7
7
  apiRoute,
8
8
  depth,
9
9
  initialData,
10
10
  serverURL
11
11
  });
12
+ const appliedAttributes = useRef([]);
12
13
  const themeConfiguration = useMemo(()=>extractThemeConfigurationFromLiveData(data, themeConfigurationKey), [
13
14
  data,
14
15
  themeConfigurationKey
@@ -17,7 +18,7 @@ export const ThemeLivePreviewSync = ({ initialData, serverURL, apiRoute, depth,
17
18
  if (typeof document === 'undefined' || !themeConfiguration) {
18
19
  return;
19
20
  }
20
- const runtime = buildLivePreviewThemeRuntime(themeConfiguration);
21
+ const runtime = buildLivePreviewThemeRuntime(themeConfiguration, apiRoute, fontLoading);
21
22
  let styleElement = document.getElementById(styleElementId);
22
23
  if (!styleElement) {
23
24
  styleElement = document.createElement('style');
@@ -25,6 +26,27 @@ export const ThemeLivePreviewSync = ({ initialData, serverURL, apiRoute, depth,
25
26
  document.head.appendChild(styleElement);
26
27
  }
27
28
  styleElement.innerHTML = runtime.css;
29
+ const fontsLinkId = `${styleElementId}-fonts`;
30
+ let fontsLink = document.getElementById(fontsLinkId);
31
+ const fontsAlreadyLoaded = runtime.fontsHref !== null && [
32
+ ...document.querySelectorAll('link[rel="stylesheet"]')
33
+ ].some((link)=>link !== fontsLink && link.getAttribute('href') === runtime.fontsHref);
34
+ if (runtime.fontsHref && !fontsAlreadyLoaded) {
35
+ if (!fontsLink) {
36
+ fontsLink = document.createElement('link');
37
+ fontsLink.id = fontsLinkId;
38
+ fontsLink.rel = 'stylesheet';
39
+ document.head.appendChild(fontsLink);
40
+ }
41
+ if (fontsLink.getAttribute('href') !== runtime.fontsHref) fontsLink.href = runtime.fontsHref;
42
+ } else {
43
+ fontsLink?.remove();
44
+ }
45
+ const root = document.documentElement;
46
+ // Drop style attributes that are no longer configured, then apply the current set.
47
+ appliedAttributes.current.filter((name)=>!(name in runtime.attributes)).forEach((name)=>root.removeAttribute(name));
48
+ Object.entries(runtime.attributes).forEach(([name, value])=>root.setAttribute(name, value));
49
+ appliedAttributes.current = Object.keys(runtime.attributes);
28
50
  document.documentElement.dataset.theme = runtime.theme;
29
51
  document.documentElement.dataset.colorMode = runtime.mode;
30
52
  document.documentElement.dataset.themeMode = runtime.mode;
@@ -32,7 +54,9 @@ export const ThemeLivePreviewSync = ({ initialData, serverURL, apiRoute, depth,
32
54
  document.documentElement.classList.add(runtime.mode);
33
55
  }, [
34
56
  themeConfiguration,
35
- styleElementId
57
+ styleElementId,
58
+ apiRoute,
59
+ fontLoading
36
60
  ]);
37
61
  return null;
38
62
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/ThemeLivePreviewSync.tsx"],"sourcesContent":["'use client'\n\nimport { useLivePreview } from '@payloadcms/live-preview-react'\nimport React, { useEffect, useMemo } from 'react'\nimport {\n buildLivePreviewThemeRuntime,\n extractThemeConfigurationFromLiveData,\n} from '../utils/livePreviewThemeRuntime.js'\n\ninterface ThemeLivePreviewSyncProps {\n initialData: Record<string, unknown>\n serverURL: string\n apiRoute?: string\n depth?: number\n themeConfigurationKey?: string\n styleElementId?: string\n}\n\nexport const ThemeLivePreviewSync: React.FC<ThemeLivePreviewSyncProps> = ({\n initialData,\n serverURL,\n apiRoute,\n depth,\n themeConfigurationKey = 'themeConfiguration',\n styleElementId = 'theme-live-preview-css',\n}) => {\n const { data } = useLivePreview<Record<string, unknown>>({\n apiRoute,\n depth,\n initialData,\n serverURL,\n })\n\n const themeConfiguration = useMemo(\n () => extractThemeConfigurationFromLiveData(data, themeConfigurationKey),\n [data, themeConfigurationKey],\n )\n\n useEffect(() => {\n if (typeof document === 'undefined' || !themeConfiguration) {\n return\n }\n\n const runtime = buildLivePreviewThemeRuntime(themeConfiguration)\n\n let styleElement = document.getElementById(styleElementId) as HTMLStyleElement | null\n if (!styleElement) {\n styleElement = document.createElement('style')\n styleElement.id = styleElementId\n document.head.appendChild(styleElement)\n }\n\n styleElement.innerHTML = runtime.css\n\n document.documentElement.dataset.theme = runtime.theme\n document.documentElement.dataset.colorMode = runtime.mode\n document.documentElement.dataset.themeMode = runtime.mode\n document.documentElement.classList.remove('light', 'dark', 'auto')\n document.documentElement.classList.add(runtime.mode)\n }, [themeConfiguration, styleElementId])\n\n return null\n}\n"],"names":["useLivePreview","React","useEffect","useMemo","buildLivePreviewThemeRuntime","extractThemeConfigurationFromLiveData","ThemeLivePreviewSync","initialData","serverURL","apiRoute","depth","themeConfigurationKey","styleElementId","data","themeConfiguration","document","runtime","styleElement","getElementById","createElement","id","head","appendChild","innerHTML","css","documentElement","dataset","theme","colorMode","mode","themeMode","classList","remove","add"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,iCAAgC;AAC/D,OAAOC,SAASC,SAAS,EAAEC,OAAO,QAAQ,QAAO;AACjD,SACEC,4BAA4B,EAC5BC,qCAAqC,QAChC,sCAAqC;AAW5C,OAAO,MAAMC,uBAA4D,CAAC,EACxEC,WAAW,EACXC,SAAS,EACTC,QAAQ,EACRC,KAAK,EACLC,wBAAwB,oBAAoB,EAC5CC,iBAAiB,wBAAwB,EAC1C;IACC,MAAM,EAAEC,IAAI,EAAE,GAAGb,eAAwC;QACvDS;QACAC;QACAH;QACAC;IACF;IAEA,MAAMM,qBAAqBX,QACzB,IAAME,sCAAsCQ,MAAMF,wBAClD;QAACE;QAAMF;KAAsB;IAG/BT,UAAU;QACR,IAAI,OAAOa,aAAa,eAAe,CAACD,oBAAoB;YAC1D;QACF;QAEA,MAAME,UAAUZ,6BAA6BU;QAE7C,IAAIG,eAAeF,SAASG,cAAc,CAACN;QAC3C,IAAI,CAACK,cAAc;YACjBA,eAAeF,SAASI,aAAa,CAAC;YACtCF,aAAaG,EAAE,GAAGR;YAClBG,SAASM,IAAI,CAACC,WAAW,CAACL;QAC5B;QAEAA,aAAaM,SAAS,GAAGP,QAAQQ,GAAG;QAEpCT,SAASU,eAAe,CAACC,OAAO,CAACC,KAAK,GAAGX,QAAQW,KAAK;QACtDZ,SAASU,eAAe,CAACC,OAAO,CAACE,SAAS,GAAGZ,QAAQa,IAAI;QACzDd,SAASU,eAAe,CAACC,OAAO,CAACI,SAAS,GAAGd,QAAQa,IAAI;QACzDd,SAASU,eAAe,CAACM,SAAS,CAACC,MAAM,CAAC,SAAS,QAAQ;QAC3DjB,SAASU,eAAe,CAACM,SAAS,CAACE,GAAG,CAACjB,QAAQa,IAAI;IACrD,GAAG;QAACf;QAAoBF;KAAe;IAEvC,OAAO;AACT,EAAC"}
1
+ {"version":3,"sources":["../../src/components/ThemeLivePreviewSync.tsx"],"sourcesContent":["'use client'\n\nimport { useLivePreview } from '@payloadcms/live-preview-react'\nimport React, { useEffect, useMemo, useRef } from 'react'\nimport type { ThemeFontLoading } from '../constants/themeFonts.js'\nimport {\n buildLivePreviewThemeRuntime,\n extractThemeConfigurationFromLiveData,\n} from '../utils/livePreviewThemeRuntime.js'\n\ninterface ThemeLivePreviewSyncProps {\n initialData: Record<string, unknown>\n serverURL: string\n apiRoute?: string\n depth?: number\n themeConfigurationKey?: string\n styleElementId?: string\n /** Same as ServerThemeInjector's `fontLoading`; use 'none' with next/font. */\n fontLoading?: ThemeFontLoading\n}\n\nexport const ThemeLivePreviewSync: React.FC<ThemeLivePreviewSyncProps> = ({\n initialData,\n serverURL,\n apiRoute,\n depth,\n themeConfigurationKey = 'themeConfiguration',\n styleElementId = 'theme-live-preview-css',\n fontLoading = 'self-hosted',\n}) => {\n const { data } = useLivePreview<Record<string, unknown>>({\n apiRoute,\n depth,\n initialData,\n serverURL,\n })\n\n const appliedAttributes = useRef<string[]>([])\n\n const themeConfiguration = useMemo(\n () => extractThemeConfigurationFromLiveData(data, themeConfigurationKey),\n [data, themeConfigurationKey],\n )\n\n useEffect(() => {\n if (typeof document === 'undefined' || !themeConfiguration) {\n return\n }\n\n const runtime = buildLivePreviewThemeRuntime(themeConfiguration, apiRoute, fontLoading)\n\n let styleElement = document.getElementById(styleElementId) as HTMLStyleElement | null\n if (!styleElement) {\n styleElement = document.createElement('style')\n styleElement.id = styleElementId\n document.head.appendChild(styleElement)\n }\n\n styleElement.innerHTML = runtime.css\n\n const fontsLinkId = `${styleElementId}-fonts`\n let fontsLink = document.getElementById(fontsLinkId) as HTMLLinkElement | null\n const fontsAlreadyLoaded =\n runtime.fontsHref !== null &&\n [...document.querySelectorAll<HTMLLinkElement>('link[rel=\"stylesheet\"]')].some(\n (link) => link !== fontsLink && link.getAttribute('href') === runtime.fontsHref,\n )\n if (runtime.fontsHref && !fontsAlreadyLoaded) {\n if (!fontsLink) {\n fontsLink = document.createElement('link')\n fontsLink.id = fontsLinkId\n fontsLink.rel = 'stylesheet'\n document.head.appendChild(fontsLink)\n }\n if (fontsLink.getAttribute('href') !== runtime.fontsHref) fontsLink.href = runtime.fontsHref\n } else {\n fontsLink?.remove()\n }\n\n const root = document.documentElement\n // Drop style attributes that are no longer configured, then apply the current set.\n appliedAttributes.current\n .filter((name) => !(name in runtime.attributes))\n .forEach((name) => root.removeAttribute(name))\n Object.entries(runtime.attributes).forEach(([name, value]) => root.setAttribute(name, value))\n appliedAttributes.current = Object.keys(runtime.attributes)\n\n document.documentElement.dataset.theme = runtime.theme\n document.documentElement.dataset.colorMode = runtime.mode\n document.documentElement.dataset.themeMode = runtime.mode\n document.documentElement.classList.remove('light', 'dark', 'auto')\n document.documentElement.classList.add(runtime.mode)\n }, [themeConfiguration, styleElementId, apiRoute, fontLoading])\n\n return null\n}\n"],"names":["useLivePreview","React","useEffect","useMemo","useRef","buildLivePreviewThemeRuntime","extractThemeConfigurationFromLiveData","ThemeLivePreviewSync","initialData","serverURL","apiRoute","depth","themeConfigurationKey","styleElementId","fontLoading","data","appliedAttributes","themeConfiguration","document","runtime","styleElement","getElementById","createElement","id","head","appendChild","innerHTML","css","fontsLinkId","fontsLink","fontsAlreadyLoaded","fontsHref","querySelectorAll","some","link","getAttribute","rel","href","remove","root","documentElement","current","filter","name","attributes","forEach","removeAttribute","Object","entries","value","setAttribute","keys","dataset","theme","colorMode","mode","themeMode","classList","add"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,iCAAgC;AAC/D,OAAOC,SAASC,SAAS,EAAEC,OAAO,EAAEC,MAAM,QAAQ,QAAO;AAEzD,SACEC,4BAA4B,EAC5BC,qCAAqC,QAChC,sCAAqC;AAa5C,OAAO,MAAMC,uBAA4D,CAAC,EACxEC,WAAW,EACXC,SAAS,EACTC,QAAQ,EACRC,KAAK,EACLC,wBAAwB,oBAAoB,EAC5CC,iBAAiB,wBAAwB,EACzCC,cAAc,aAAa,EAC5B;IACC,MAAM,EAAEC,IAAI,EAAE,GAAGf,eAAwC;QACvDU;QACAC;QACAH;QACAC;IACF;IAEA,MAAMO,oBAAoBZ,OAAiB,EAAE;IAE7C,MAAMa,qBAAqBd,QACzB,IAAMG,sCAAsCS,MAAMH,wBAClD;QAACG;QAAMH;KAAsB;IAG/BV,UAAU;QACR,IAAI,OAAOgB,aAAa,eAAe,CAACD,oBAAoB;YAC1D;QACF;QAEA,MAAME,UAAUd,6BAA6BY,oBAAoBP,UAAUI;QAE3E,IAAIM,eAAeF,SAASG,cAAc,CAACR;QAC3C,IAAI,CAACO,cAAc;YACjBA,eAAeF,SAASI,aAAa,CAAC;YACtCF,aAAaG,EAAE,GAAGV;YAClBK,SAASM,IAAI,CAACC,WAAW,CAACL;QAC5B;QAEAA,aAAaM,SAAS,GAAGP,QAAQQ,GAAG;QAEpC,MAAMC,cAAc,GAAGf,eAAe,MAAM,CAAC;QAC7C,IAAIgB,YAAYX,SAASG,cAAc,CAACO;QACxC,MAAME,qBACJX,QAAQY,SAAS,KAAK,QACtB;eAAIb,SAASc,gBAAgB,CAAkB;SAA0B,CAACC,IAAI,CAC5E,CAACC,OAASA,SAASL,aAAaK,KAAKC,YAAY,CAAC,YAAYhB,QAAQY,SAAS;QAEnF,IAAIZ,QAAQY,SAAS,IAAI,CAACD,oBAAoB;YAC5C,IAAI,CAACD,WAAW;gBACdA,YAAYX,SAASI,aAAa,CAAC;gBACnCO,UAAUN,EAAE,GAAGK;gBACfC,UAAUO,GAAG,GAAG;gBAChBlB,SAASM,IAAI,CAACC,WAAW,CAACI;YAC5B;YACA,IAAIA,UAAUM,YAAY,CAAC,YAAYhB,QAAQY,SAAS,EAAEF,UAAUQ,IAAI,GAAGlB,QAAQY,SAAS;QAC9F,OAAO;YACLF,WAAWS;QACb;QAEA,MAAMC,OAAOrB,SAASsB,eAAe;QACrC,mFAAmF;QACnFxB,kBAAkByB,OAAO,CACtBC,MAAM,CAAC,CAACC,OAAS,CAAEA,CAAAA,QAAQxB,QAAQyB,UAAU,AAAD,GAC5CC,OAAO,CAAC,CAACF,OAASJ,KAAKO,eAAe,CAACH;QAC1CI,OAAOC,OAAO,CAAC7B,QAAQyB,UAAU,EAAEC,OAAO,CAAC,CAAC,CAACF,MAAMM,MAAM,GAAKV,KAAKW,YAAY,CAACP,MAAMM;QACtFjC,kBAAkByB,OAAO,GAAGM,OAAOI,IAAI,CAAChC,QAAQyB,UAAU;QAE1D1B,SAASsB,eAAe,CAACY,OAAO,CAACC,KAAK,GAAGlC,QAAQkC,KAAK;QACtDnC,SAASsB,eAAe,CAACY,OAAO,CAACE,SAAS,GAAGnC,QAAQoC,IAAI;QACzDrC,SAASsB,eAAe,CAACY,OAAO,CAACI,SAAS,GAAGrC,QAAQoC,IAAI;QACzDrC,SAASsB,eAAe,CAACiB,SAAS,CAACnB,MAAM,CAAC,SAAS,QAAQ;QAC3DpB,SAASsB,eAAe,CAACiB,SAAS,CAACC,GAAG,CAACvC,QAAQoC,IAAI;IACrD,GAAG;QAACtC;QAAoBJ;QAAgBH;QAAUI;KAAY;IAE9D,OAAO;AACT,EAAC"}
@@ -5,12 +5,17 @@ export interface TypographySelection {
5
5
  headingFontCustom?: string | null;
6
6
  baseFontSize?: string | null;
7
7
  lineHeight?: string | null;
8
+ headingWeight?: string | null;
9
+ letterSpacing?: string | null;
8
10
  }
9
11
  export interface ResolvedTypographyPreview {
10
12
  bodyFont: string;
11
13
  headingFont: string;
12
14
  baseFontSize: string;
13
15
  lineHeight: string;
16
+ headingWeight: string;
17
+ headingLetterSpacing: string;
18
+ headingTransform: 'none' | 'uppercase';
14
19
  bodyLabel: string;
15
20
  headingLabel: string;
16
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"typographyPreviewUtils.d.ts","sourceRoot":"","sources":["../../src/components/typographyPreviewUtils.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;CACrB;AAgFD,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,EACjD,SAAS,CAAC,EAAE,MAAM,GACjB,yBAAyB,CAmC3B"}
1
+ {"version":3,"file":"typographyPreviewUtils.d.ts","sourceRoot":"","sources":["../../src/components/typographyPreviewUtils.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC1B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,gBAAgB,EAAE,MAAM,GAAG,WAAW,CAAA;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,YAAY,EAAE,MAAM,CAAA;CACrB;AA+ED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,EACjD,SAAS,CAAC,EAAE,MAAM,GACjB,yBAAyB,CAyC3B"}
@@ -1,4 +1,4 @@
1
- import { BODY_FONT_OPTIONS, FONT_VALUE_TO_CSS, HEADING_FONT_OPTIONS } from '../constants/themeFonts.js';
1
+ import { BODY_FONT_OPTIONS, getFontCatalogEntry, HEADING_FONT_OPTIONS, LETTER_SPACING_TO_CSS, toFontSizeCss } from '../constants/themeFonts.js';
2
2
  import { defaultThemePresets } from '../presets.js';
3
3
  const FALLBACK_THEME = defaultThemePresets[0]?.name ?? 'cool';
4
4
  const fallbackPreset = defaultThemePresets.find((preset)=>preset.name === FALLBACK_THEME) ?? defaultThemePresets[0];
@@ -20,12 +20,14 @@ const FONT_VAR_FALLBACK = {
20
20
  'var(--font-urbanist)': '"Urbanist", system-ui, sans-serif'
21
21
  };
22
22
  function withFallback(fontFamily, fallback) {
23
- if (!fontFamily) return fallback;
24
- if (fontFamily.startsWith('var(')) {
25
- const mappedFallback = FONT_VAR_FALLBACK[fontFamily] ?? fallback;
26
- return `${fontFamily}, ${mappedFallback}`;
27
- }
28
- return fontFamily;
23
+ const value = fontFamily?.trim() || fallback;
24
+ // A `var(--font-x)` without a fallback whose variable is not defined (always the
25
+ // case in the admin) makes the whole font-family declaration invalid. Give every
26
+ // such reference a fallback stack so the preview always renders a real face.
27
+ return value.replace(/var\((--[\w-]+)\)/g, (match, name)=>{
28
+ const mapped = FONT_VAR_FALLBACK[match] ?? 'system-ui, sans-serif';
29
+ return `var(${name}, ${mapped})`;
30
+ });
29
31
  }
30
32
  function getPresetTypography(themeName) {
31
33
  const preset = defaultThemePresets.find((item)=>item.name === themeName) ?? fallbackPreset;
@@ -41,9 +43,10 @@ function resolveFontFamily(value, customValue, fallback) {
41
43
  }
42
44
  return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif');
43
45
  }
44
- const preconfigured = FONT_VALUE_TO_CSS[value];
46
+ // Catalog stacks are complete (variable with fallback + generic family).
47
+ const preconfigured = getFontCatalogEntry(value)?.fontFamily;
45
48
  if (preconfigured) {
46
- return withFallback(preconfigured, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif');
49
+ return preconfigured;
47
50
  }
48
51
  return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif');
49
52
  }
@@ -67,13 +70,16 @@ export function resolveTypographyPreview(selection, themeName) {
67
70
  const headingFontCustom = selection?.headingFontCustom ?? undefined;
68
71
  const resolvedBodyFont = resolveFontFamily(bodyFontValue, bodyFontCustom, fallbackBody);
69
72
  const resolvedHeadingFont = resolveFontFamily(headingFontValue, headingFontCustom, fallbackHeading);
70
- const baseFontSize = selection?.baseFontSize && selection.baseFontSize !== 'preset' ? selection.baseFontSize : fallbackSize;
73
+ const baseFontSize = toFontSizeCss(selection?.baseFontSize) ?? fallbackSize;
71
74
  const lineHeight = selection?.lineHeight && selection.lineHeight !== 'preset' ? selection.lineHeight : fallbackLineHeight;
72
75
  return {
73
76
  bodyFont: resolvedBodyFont,
74
77
  headingFont: resolvedHeadingFont,
75
78
  baseFontSize,
76
79
  lineHeight,
80
+ headingWeight: selection?.headingWeight && selection.headingWeight !== 'preset' ? selection.headingWeight : presetTypography?.headingWeight ?? '700',
81
+ headingLetterSpacing: (selection?.letterSpacing && LETTER_SPACING_TO_CSS[selection.letterSpacing]) ?? presetTypography?.headingLetterSpacing ?? 'normal',
82
+ headingTransform: selection?.letterSpacing === 'wider' ? 'uppercase' : 'none',
77
83
  bodyLabel: resolveOptionLabel(BODY_FONT_OPTIONS, bodyFontValue),
78
84
  headingLabel: resolveOptionLabel(HEADING_FONT_OPTIONS, headingFontValue)
79
85
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/typographyPreviewUtils.ts"],"sourcesContent":["import {\n BODY_FONT_OPTIONS,\n FONT_VALUE_TO_CSS,\n HEADING_FONT_OPTIONS,\n} from '../constants/themeFonts.js'\nimport { defaultThemePresets } from '../presets.js'\n\nexport interface TypographySelection {\n bodyFont?: string | null\n headingFont?: string | null\n bodyFontCustom?: string | null\n headingFontCustom?: string | null\n baseFontSize?: string | null\n lineHeight?: string | null\n}\n\nexport interface ResolvedTypographyPreview {\n bodyFont: string\n headingFont: string\n baseFontSize: string\n lineHeight: string\n bodyLabel: string\n headingLabel: string\n}\n\nconst FALLBACK_THEME = defaultThemePresets[0]?.name ?? 'cool'\nconst fallbackPreset =\n defaultThemePresets.find((preset) => preset.name === FALLBACK_THEME) ?? defaultThemePresets[0]\n\nconst FONT_VAR_FALLBACK: Record<string, string> = {\n 'var(--font-inter)': '\"Inter\", system-ui, sans-serif',\n 'var(--font-geist-sans)': '\"Geist Sans\", system-ui, sans-serif',\n 'var(--font-geist-mono)': '\"Geist Mono\", SFMono-Regular, ui-monospace, monospace',\n 'var(--font-roboto)': '\"Roboto\", system-ui, sans-serif',\n 'var(--font-open-sans)': '\"Open Sans\", system-ui, sans-serif',\n 'var(--font-lato)': '\"Lato\", system-ui, sans-serif',\n 'var(--font-montserrat)': '\"Montserrat\", system-ui, sans-serif',\n 'var(--font-poppins)': '\"Poppins\", system-ui, sans-serif',\n 'var(--font-outfit)': '\"Outfit\", system-ui, sans-serif',\n 'var(--font-playfair)': '\"Playfair Display\", \"Times New Roman\", serif',\n 'var(--font-merriweather)': '\"Merriweather\", \"Times New Roman\", serif',\n 'var(--font-raleway)': '\"Raleway\", system-ui, sans-serif',\n 'var(--font-lora)': '\"Lora\", \"Times New Roman\", serif',\n 'var(--font-manrope)': '\"Manrope\", system-ui, sans-serif',\n 'var(--font-urbanist)': '\"Urbanist\", system-ui, sans-serif',\n}\n\nfunction withFallback(fontFamily: string, fallback: string): string {\n if (!fontFamily) return fallback\n\n if (fontFamily.startsWith('var(')) {\n const mappedFallback = FONT_VAR_FALLBACK[fontFamily] ?? fallback\n return `${fontFamily}, ${mappedFallback}`\n }\n\n return fontFamily\n}\n\nfunction getPresetTypography(themeName?: string) {\n const preset = defaultThemePresets.find((item) => item.name === themeName) ?? fallbackPreset\n return preset?.typography\n}\n\nfunction resolveFontFamily(\n value: string | null | undefined,\n customValue: string | null | undefined,\n fallback: string,\n): string {\n if (!value || value === 'preset') {\n return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif')\n }\n\n if (value === 'custom') {\n if (customValue && customValue.trim().length > 0) {\n return customValue.trim()\n }\n return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif')\n }\n\n const preconfigured = FONT_VALUE_TO_CSS[value]\n if (preconfigured) {\n return withFallback(\n preconfigured,\n fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif',\n )\n }\n\n return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif')\n}\n\nfunction resolveOptionLabel(\n options: ReadonlyArray<{ label: string | Record<string, string>; value: string }>,\n value?: string | null,\n locale: 'en' | 'cs' = 'en',\n): string {\n const option = options.find((item) => item.value === value)\n if (!option) return value ?? ''\n if (typeof option.label === 'string') {\n return option.label\n }\n return option.label?.[locale] ?? Object.values(option.label)[0] ?? option.value\n}\n\nexport function resolveTypographyPreview(\n selection: TypographySelection | null | undefined,\n themeName?: string,\n): ResolvedTypographyPreview {\n const presetTypography = getPresetTypography(themeName)\n const fallbackBody = presetTypography?.fontFamily ?? 'system-ui, sans-serif'\n const fallbackHeading = presetTypography?.headingFamily ?? fallbackBody\n const fallbackSize = presetTypography?.baseFontSize ?? '16px'\n const fallbackLineHeight = presetTypography?.lineHeight ?? '1.6'\n\n const bodyFontValue = selection?.bodyFont ?? 'preset'\n const headingFontValue = selection?.headingFont ?? 'preset'\n const bodyFontCustom = selection?.bodyFontCustom ?? undefined\n const headingFontCustom = selection?.headingFontCustom ?? undefined\n const resolvedBodyFont = resolveFontFamily(bodyFontValue, bodyFontCustom, fallbackBody)\n const resolvedHeadingFont = resolveFontFamily(\n headingFontValue,\n headingFontCustom,\n fallbackHeading,\n )\n\n const baseFontSize =\n selection?.baseFontSize && selection.baseFontSize !== 'preset'\n ? selection.baseFontSize\n : fallbackSize\n const lineHeight =\n selection?.lineHeight && selection.lineHeight !== 'preset'\n ? selection.lineHeight\n : fallbackLineHeight\n\n return {\n bodyFont: resolvedBodyFont,\n headingFont: resolvedHeadingFont,\n baseFontSize,\n lineHeight,\n bodyLabel: resolveOptionLabel(BODY_FONT_OPTIONS, bodyFontValue),\n headingLabel: resolveOptionLabel(HEADING_FONT_OPTIONS, headingFontValue),\n }\n}\n"],"names":["BODY_FONT_OPTIONS","FONT_VALUE_TO_CSS","HEADING_FONT_OPTIONS","defaultThemePresets","FALLBACK_THEME","name","fallbackPreset","find","preset","FONT_VAR_FALLBACK","withFallback","fontFamily","fallback","startsWith","mappedFallback","getPresetTypography","themeName","item","typography","resolveFontFamily","value","customValue","trim","length","preconfigured","resolveOptionLabel","options","locale","option","label","Object","values","resolveTypographyPreview","selection","presetTypography","fallbackBody","fallbackHeading","headingFamily","fallbackSize","baseFontSize","fallbackLineHeight","lineHeight","bodyFontValue","bodyFont","headingFontValue","headingFont","bodyFontCustom","undefined","headingFontCustom","resolvedBodyFont","resolvedHeadingFont","bodyLabel","headingLabel"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/components/typographyPreviewUtils.ts"],"sourcesContent":["import {\n BODY_FONT_OPTIONS,\n getFontCatalogEntry,\n HEADING_FONT_OPTIONS,\n LETTER_SPACING_TO_CSS,\n toFontSizeCss,\n} from '../constants/themeFonts.js'\nimport { defaultThemePresets } from '../presets.js'\n\nexport interface TypographySelection {\n bodyFont?: string | null\n headingFont?: string | null\n bodyFontCustom?: string | null\n headingFontCustom?: string | null\n baseFontSize?: string | null\n lineHeight?: string | null\n headingWeight?: string | null\n letterSpacing?: string | null\n}\n\nexport interface ResolvedTypographyPreview {\n bodyFont: string\n headingFont: string\n baseFontSize: string\n lineHeight: string\n headingWeight: string\n headingLetterSpacing: string\n headingTransform: 'none' | 'uppercase'\n bodyLabel: string\n headingLabel: string\n}\n\nconst FALLBACK_THEME = defaultThemePresets[0]?.name ?? 'cool'\nconst fallbackPreset =\n defaultThemePresets.find((preset) => preset.name === FALLBACK_THEME) ?? defaultThemePresets[0]\n\nconst FONT_VAR_FALLBACK: Record<string, string> = {\n 'var(--font-inter)': '\"Inter\", system-ui, sans-serif',\n 'var(--font-geist-sans)': '\"Geist Sans\", system-ui, sans-serif',\n 'var(--font-geist-mono)': '\"Geist Mono\", SFMono-Regular, ui-monospace, monospace',\n 'var(--font-roboto)': '\"Roboto\", system-ui, sans-serif',\n 'var(--font-open-sans)': '\"Open Sans\", system-ui, sans-serif',\n 'var(--font-lato)': '\"Lato\", system-ui, sans-serif',\n 'var(--font-montserrat)': '\"Montserrat\", system-ui, sans-serif',\n 'var(--font-poppins)': '\"Poppins\", system-ui, sans-serif',\n 'var(--font-outfit)': '\"Outfit\", system-ui, sans-serif',\n 'var(--font-playfair)': '\"Playfair Display\", \"Times New Roman\", serif',\n 'var(--font-merriweather)': '\"Merriweather\", \"Times New Roman\", serif',\n 'var(--font-raleway)': '\"Raleway\", system-ui, sans-serif',\n 'var(--font-lora)': '\"Lora\", \"Times New Roman\", serif',\n 'var(--font-manrope)': '\"Manrope\", system-ui, sans-serif',\n 'var(--font-urbanist)': '\"Urbanist\", system-ui, sans-serif',\n}\n\nfunction withFallback(fontFamily: string, fallback: string): string {\n const value = fontFamily?.trim() || fallback\n\n // A `var(--font-x)` without a fallback whose variable is not defined (always the\n // case in the admin) makes the whole font-family declaration invalid. Give every\n // such reference a fallback stack so the preview always renders a real face.\n return value.replace(/var\\((--[\\w-]+)\\)/g, (match, name: string) => {\n const mapped = FONT_VAR_FALLBACK[match] ?? 'system-ui, sans-serif'\n return `var(${name}, ${mapped})`\n })\n}\n\nfunction getPresetTypography(themeName?: string) {\n const preset = defaultThemePresets.find((item) => item.name === themeName) ?? fallbackPreset\n return preset?.typography\n}\n\nfunction resolveFontFamily(\n value: string | null | undefined,\n customValue: string | null | undefined,\n fallback: string,\n): string {\n if (!value || value === 'preset') {\n return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif')\n }\n\n if (value === 'custom') {\n if (customValue && customValue.trim().length > 0) {\n return customValue.trim()\n }\n return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif')\n }\n\n // Catalog stacks are complete (variable with fallback + generic family).\n const preconfigured = getFontCatalogEntry(value)?.fontFamily\n if (preconfigured) {\n return preconfigured\n }\n\n return withFallback(fallback, fallbackPreset?.typography?.fontFamily ?? 'system-ui, sans-serif')\n}\n\nfunction resolveOptionLabel(\n options: ReadonlyArray<{ label: string | Record<string, string>; value: string }>,\n value?: string | null,\n locale: 'en' | 'cs' = 'en',\n): string {\n const option = options.find((item) => item.value === value)\n if (!option) return value ?? ''\n if (typeof option.label === 'string') {\n return option.label\n }\n return option.label?.[locale] ?? Object.values(option.label)[0] ?? option.value\n}\n\nexport function resolveTypographyPreview(\n selection: TypographySelection | null | undefined,\n themeName?: string,\n): ResolvedTypographyPreview {\n const presetTypography = getPresetTypography(themeName)\n const fallbackBody = presetTypography?.fontFamily ?? 'system-ui, sans-serif'\n const fallbackHeading = presetTypography?.headingFamily ?? fallbackBody\n const fallbackSize = presetTypography?.baseFontSize ?? '16px'\n const fallbackLineHeight = presetTypography?.lineHeight ?? '1.6'\n\n const bodyFontValue = selection?.bodyFont ?? 'preset'\n const headingFontValue = selection?.headingFont ?? 'preset'\n const bodyFontCustom = selection?.bodyFontCustom ?? undefined\n const headingFontCustom = selection?.headingFontCustom ?? undefined\n const resolvedBodyFont = resolveFontFamily(bodyFontValue, bodyFontCustom, fallbackBody)\n const resolvedHeadingFont = resolveFontFamily(\n headingFontValue,\n headingFontCustom,\n fallbackHeading,\n )\n\n const baseFontSize = toFontSizeCss(selection?.baseFontSize) ?? fallbackSize\n const lineHeight =\n selection?.lineHeight && selection.lineHeight !== 'preset'\n ? selection.lineHeight\n : fallbackLineHeight\n\n return {\n bodyFont: resolvedBodyFont,\n headingFont: resolvedHeadingFont,\n baseFontSize,\n lineHeight,\n headingWeight:\n selection?.headingWeight && selection.headingWeight !== 'preset'\n ? selection.headingWeight\n : (presetTypography?.headingWeight ?? '700'),\n headingLetterSpacing:\n (selection?.letterSpacing && LETTER_SPACING_TO_CSS[selection.letterSpacing]) ??\n presetTypography?.headingLetterSpacing ??\n 'normal',\n headingTransform: selection?.letterSpacing === 'wider' ? 'uppercase' : 'none',\n bodyLabel: resolveOptionLabel(BODY_FONT_OPTIONS, bodyFontValue),\n headingLabel: resolveOptionLabel(HEADING_FONT_OPTIONS, headingFontValue),\n }\n}\n"],"names":["BODY_FONT_OPTIONS","getFontCatalogEntry","HEADING_FONT_OPTIONS","LETTER_SPACING_TO_CSS","toFontSizeCss","defaultThemePresets","FALLBACK_THEME","name","fallbackPreset","find","preset","FONT_VAR_FALLBACK","withFallback","fontFamily","fallback","value","trim","replace","match","mapped","getPresetTypography","themeName","item","typography","resolveFontFamily","customValue","length","preconfigured","resolveOptionLabel","options","locale","option","label","Object","values","resolveTypographyPreview","selection","presetTypography","fallbackBody","fallbackHeading","headingFamily","fallbackSize","baseFontSize","fallbackLineHeight","lineHeight","bodyFontValue","bodyFont","headingFontValue","headingFont","bodyFontCustom","undefined","headingFontCustom","resolvedBodyFont","resolvedHeadingFont","headingWeight","headingLetterSpacing","letterSpacing","headingTransform","bodyLabel","headingLabel"],"mappings":"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"}