@kilivi/payloadcms-theme-management 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/README.md +346 -845
  2. package/dist/components/AppearanceSessionControls.d.ts +15 -0
  3. package/dist/components/AppearanceSessionControls.d.ts.map +1 -0
  4. package/dist/components/AppearanceSessionControls.js +152 -0
  5. package/dist/components/AppearanceSessionControls.js.map +1 -0
  6. package/dist/components/ColorModeToggle.css +42 -0
  7. package/dist/components/ColorModeToggle.d.ts +40 -0
  8. package/dist/components/ColorModeToggle.d.ts.map +1 -0
  9. package/dist/components/ColorModeToggle.js +111 -0
  10. package/dist/components/ColorModeToggle.js.map +1 -0
  11. package/dist/components/ServerThemeInjector.d.ts +13 -1
  12. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  13. package/dist/components/ServerThemeInjector.js +65 -15
  14. package/dist/components/ServerThemeInjector.js.map +1 -1
  15. package/dist/components/ThemeLivePreviewSync.d.ts +4 -0
  16. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  17. package/dist/components/ThemeLivePreviewSync.js +28 -4
  18. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  19. package/dist/components/ThemeSwitcher.d.ts +36 -0
  20. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  21. package/dist/components/ThemeSwitcher.js +89 -0
  22. package/dist/components/ThemeSwitcher.js.map +1 -0
  23. package/dist/components/typographyPreviewUtils.d.ts +5 -0
  24. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  25. package/dist/components/typographyPreviewUtils.js +24 -11
  26. package/dist/components/typographyPreviewUtils.js.map +1 -1
  27. package/dist/constants/devicePreviews.d.ts +35 -0
  28. package/dist/constants/devicePreviews.d.ts.map +1 -0
  29. package/dist/constants/devicePreviews.js +43 -0
  30. package/dist/constants/devicePreviews.js.map +1 -0
  31. package/dist/constants/themeFonts.d.ts +94 -168
  32. package/dist/constants/themeFonts.d.ts.map +1 -1
  33. package/dist/constants/themeFonts.js +260 -140
  34. package/dist/constants/themeFonts.js.map +1 -1
  35. package/dist/endpoints/themeFonts.d.ts +5 -0
  36. package/dist/endpoints/themeFonts.d.ts.map +1 -0
  37. package/dist/endpoints/themeFonts.js +138 -0
  38. package/dist/endpoints/themeFonts.js.map +1 -0
  39. package/dist/extended-presets.d.ts +0 -4
  40. package/dist/extended-presets.d.ts.map +1 -1
  41. package/dist/extended-presets.js +1 -18
  42. package/dist/extended-presets.js.map +1 -1
  43. package/dist/extended-themes.d.ts.map +1 -1
  44. package/dist/extended-themes.js +428 -0
  45. package/dist/extended-themes.js.map +1 -1
  46. package/dist/fields/AccessibilityAuditField.d.ts +2 -0
  47. package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
  48. package/dist/fields/AccessibilityAuditField.js +303 -0
  49. package/dist/fields/AccessibilityAuditField.js.map +1 -0
  50. package/dist/fields/AppearancePreviewField.d.ts +2 -0
  51. package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
  52. package/dist/fields/AppearancePreviewField.js +826 -0
  53. package/dist/fields/AppearancePreviewField.js.map +1 -0
  54. package/dist/fields/ColorPickerField.d.ts.map +1 -1
  55. package/dist/fields/ColorPickerField.js +89 -85
  56. package/dist/fields/ColorPickerField.js.map +1 -1
  57. package/dist/fields/FontSelectField.css +155 -0
  58. package/dist/fields/FontSelectField.d.ts +8 -0
  59. package/dist/fields/FontSelectField.d.ts.map +1 -1
  60. package/dist/fields/FontSelectField.js +223 -188
  61. package/dist/fields/FontSelectField.js.map +1 -1
  62. package/dist/fields/PaletteGeneratorField.d.ts +7 -0
  63. package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
  64. package/dist/fields/PaletteGeneratorField.js +327 -0
  65. package/dist/fields/PaletteGeneratorField.js.map +1 -0
  66. package/dist/fields/StylePresetField.d.ts +3 -0
  67. package/dist/fields/StylePresetField.d.ts.map +1 -0
  68. package/dist/fields/StylePresetField.js +463 -0
  69. package/dist/fields/StylePresetField.js.map +1 -0
  70. package/dist/fields/ThemeColorPickerField.css +115 -35
  71. package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
  72. package/dist/fields/ThemeColorPickerField.js +204 -70
  73. package/dist/fields/ThemeColorPickerField.js.map +1 -1
  74. package/dist/fields/ThemeExportField.d.ts +6 -0
  75. package/dist/fields/ThemeExportField.d.ts.map +1 -0
  76. package/dist/fields/ThemeExportField.js +202 -0
  77. package/dist/fields/ThemeExportField.js.map +1 -0
  78. package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
  79. package/dist/fields/ThemePresetImportField.js +56 -25
  80. package/dist/fields/ThemePresetImportField.js.map +1 -1
  81. package/dist/fields/ThemePreviewField.css +444 -5
  82. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  83. package/dist/fields/ThemePreviewField.js +265 -170
  84. package/dist/fields/ThemePreviewField.js.map +1 -1
  85. package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
  86. package/dist/fields/ThemeTokenSelectField.js +278 -80
  87. package/dist/fields/ThemeTokenSelectField.js.map +1 -1
  88. package/dist/fields/colorFieldHelpers.d.ts +90 -0
  89. package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
  90. package/dist/fields/colorFieldHelpers.js +133 -0
  91. package/dist/fields/colorFieldHelpers.js.map +1 -0
  92. package/dist/fields/colorModeFields.d.ts +1 -1
  93. package/dist/fields/colorModeFields.d.ts.map +1 -1
  94. package/dist/fields/colorModeFields.js +49 -7
  95. package/dist/fields/colorModeFields.js.map +1 -1
  96. package/dist/fields/themeConfigurationField.d.ts +8 -1
  97. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  98. package/dist/fields/themeConfigurationField.js +1209 -228
  99. package/dist/fields/themeConfigurationField.js.map +1 -1
  100. package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
  101. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
  102. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
  103. package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
  104. package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
  105. package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
  106. package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
  107. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
  108. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
  109. package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
  110. package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
  111. package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
  112. package/dist/globals/ThemeSettings/config.js +76 -79
  113. package/dist/globals/ThemeSettings/config.js.map +1 -1
  114. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
  115. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
  116. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
  117. package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
  118. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
  119. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
  120. package/dist/hooks/useAppearanceSession.d.ts +50 -0
  121. package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
  122. package/dist/hooks/useAppearanceSession.js +91 -0
  123. package/dist/hooks/useAppearanceSession.js.map +1 -0
  124. package/dist/hooks/useNativeLivePreview.d.ts +15 -0
  125. package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
  126. package/dist/hooks/useNativeLivePreview.js +18 -0
  127. package/dist/hooks/useNativeLivePreview.js.map +1 -0
  128. package/dist/hooks/useThemeFonts.d.ts +10 -0
  129. package/dist/hooks/useThemeFonts.d.ts.map +1 -0
  130. package/dist/hooks/useThemeFonts.js +36 -0
  131. package/dist/hooks/useThemeFonts.js.map +1 -0
  132. package/dist/hooks/useThemeTranslations.d.ts +13 -0
  133. package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
  134. package/dist/hooks/useThemeTranslations.js +22 -0
  135. package/dist/hooks/useThemeTranslations.js.map +1 -0
  136. package/dist/i18n.d.ts +53 -0
  137. package/dist/i18n.d.ts.map +1 -0
  138. package/dist/i18n.js +53 -0
  139. package/dist/i18n.js.map +1 -0
  140. package/dist/index.d.ts +13 -4
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +59 -29
  143. package/dist/index.js.map +1 -1
  144. package/dist/next-fonts.d.ts +3 -0
  145. package/dist/next-fonts.d.ts.map +1 -0
  146. package/dist/next-fonts.js +360 -0
  147. package/dist/next-fonts.js.map +1 -0
  148. package/dist/payload-types.d.ts +34 -6
  149. package/dist/payload-types.d.ts.map +1 -1
  150. package/dist/payload-types.js.map +1 -1
  151. package/dist/presets.d.ts +14 -0
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js.map +1 -1
  154. package/dist/providers/Theme/index.d.ts.map +1 -1
  155. package/dist/providers/Theme/index.js +75 -11
  156. package/dist/providers/Theme/index.js.map +1 -1
  157. package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
  158. package/dist/providers/Theme/themeConfig.js +104 -0
  159. package/dist/providers/Theme/themeConfig.js.map +1 -1
  160. package/dist/providers/Theme/types.d.ts +1 -1
  161. package/dist/providers/Theme/types.d.ts.map +1 -1
  162. package/dist/providers/Theme/types.js +10 -1
  163. package/dist/providers/Theme/types.js.map +1 -1
  164. package/dist/style-presets.d.ts +75 -0
  165. package/dist/style-presets.d.ts.map +1 -0
  166. package/dist/style-presets.js +738 -0
  167. package/dist/style-presets.js.map +1 -0
  168. package/dist/translations.d.ts +164 -1
  169. package/dist/translations.d.ts.map +1 -1
  170. package/dist/translations.js +320 -9
  171. package/dist/translations.js.map +1 -1
  172. package/dist/tweakcn-special-themes.d.ts.map +1 -1
  173. package/dist/tweakcn-special-themes.js +585 -9
  174. package/dist/tweakcn-special-themes.js.map +1 -1
  175. package/dist/types/css.d.js +3 -0
  176. package/dist/types/css.d.js.map +1 -0
  177. package/dist/types.d.ts +26 -0
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/utilities/getThemeSettings.d.ts.map +1 -1
  182. package/dist/utilities/getThemeSettings.js +2 -14
  183. package/dist/utilities/getThemeSettings.js.map +1 -1
  184. package/dist/utils/contrast.d.ts +63 -0
  185. package/dist/utils/contrast.d.ts.map +1 -0
  186. package/dist/utils/contrast.js +158 -0
  187. package/dist/utils/contrast.js.map +1 -0
  188. package/dist/utils/exportTokens.d.ts +30 -0
  189. package/dist/utils/exportTokens.d.ts.map +1 -0
  190. package/dist/utils/exportTokens.js +183 -0
  191. package/dist/utils/exportTokens.js.map +1 -0
  192. package/dist/utils/generatePalette.d.ts +46 -0
  193. package/dist/utils/generatePalette.d.ts.map +1 -0
  194. package/dist/utils/generatePalette.js +127 -0
  195. package/dist/utils/generatePalette.js.map +1 -0
  196. package/dist/utils/generateThemeCSS.d.ts.map +1 -1
  197. package/dist/utils/generateThemeCSS.js +2 -40
  198. package/dist/utils/generateThemeCSS.js.map +1 -1
  199. package/dist/utils/livePreviewThemeRuntime.d.ts +22 -3
  200. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  201. package/dist/utils/livePreviewThemeRuntime.js +48 -15
  202. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  203. package/dist/utils/localizedText.d.ts +8 -0
  204. package/dist/utils/localizedText.d.ts.map +1 -0
  205. package/dist/utils/localizedText.js +21 -0
  206. package/dist/utils/localizedText.js.map +1 -0
  207. package/dist/utils/resolveThemeConfiguration.d.ts +60 -1
  208. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  209. package/dist/utils/resolveThemeConfiguration.js +53 -13
  210. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  211. package/dist/utils/themeColors.d.ts +3 -0
  212. package/dist/utils/themeColors.d.ts.map +1 -1
  213. package/dist/utils/themeColors.js +30 -1
  214. package/dist/utils/themeColors.js.map +1 -1
  215. package/dist/utils/themeHtmlAttributes.d.ts +32 -12
  216. package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
  217. package/dist/utils/themeHtmlAttributes.js +38 -5
  218. package/dist/utils/themeHtmlAttributes.js.map +1 -1
  219. package/dist/utils/themeUtils.d.ts +3 -5
  220. package/dist/utils/themeUtils.d.ts.map +1 -1
  221. package/dist/utils/themeUtils.js +474 -6
  222. package/dist/utils/themeUtils.js.map +1 -1
  223. package/dist/utils/tweakcnConverter.d.ts +1 -3
  224. package/dist/utils/tweakcnConverter.d.ts.map +1 -1
  225. package/dist/utils/tweakcnConverter.js +1 -3
  226. package/dist/utils/tweakcnConverter.js.map +1 -1
  227. package/package.json +15 -5
  228. package/dist/components/ThemePreview.d.ts +0 -3
  229. package/dist/components/ThemePreview.d.ts.map +0 -1
  230. package/dist/components/ThemePreview.js +0 -321
  231. package/dist/components/ThemePreview.js.map +0 -1
  232. package/dist/plugin.d.ts +0 -2
  233. package/dist/plugin.d.ts.map +0 -1
  234. package/dist/plugin.js +0 -3
  235. package/dist/plugin.js.map +0 -1
@@ -0,0 +1,15 @@
1
+ import { type AppearanceSource } from '../hooks/useAppearanceSession.js';
2
+ export declare const formatTemplate: (template: string, values: Record<string, string | number>) => string;
3
+ /**
4
+ * "Keep my: Colours · Fonts · Style" toggles. Locks apply to every bulk action
5
+ * (theme preset, style preset, palette generator) for the current editing session.
6
+ */
7
+ export declare function AppearanceLocks(): import("react").JSX.Element;
8
+ /**
9
+ * One-step undo for the last bulk change made from `source`. Stays for 10 s,
10
+ * paused while the pointer or keyboard focus is inside it.
11
+ */
12
+ export declare function AppearanceUndoBar({ sources }: {
13
+ sources: AppearanceSource[];
14
+ }): import("react").JSX.Element;
15
+ //# sourceMappingURL=AppearanceSessionControls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppearanceSessionControls.d.ts","sourceRoot":"","sources":["../../src/components/AppearanceSessionControls.tsx"],"names":[],"mappings":"AAKA,OAAO,EAKL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAA;AAMzC,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,EAAE,QAAQ,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,WACF,CAAA;AAEtF;;;GAGG;AACH,wBAAgB,eAAe,gCA8B9B;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,gBAAgB,EAAE,CAAA;CAAE,+BA+D7E"}
@@ -0,0 +1,152 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useForm } from '@payloadcms/ui';
4
+ import { Lock, LockOpen, Undo2, X } from 'lucide-react';
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { clearAppearanceUndo, toggleAppearanceLock, useAppearanceSession } from '../hooks/useAppearanceSession.js';
7
+ import { useThemeTranslations } from '../hooks/useThemeTranslations.js';
8
+ const LOCKS = [
9
+ 'colors',
10
+ 'fonts',
11
+ 'style'
12
+ ];
13
+ const UNDO_TIMEOUT_MS = 10_000;
14
+ export const formatTemplate = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>String(values[key] ?? match));
15
+ /**
16
+ * "Keep my: Colours · Fonts · Style" toggles. Locks apply to every bulk action
17
+ * (theme preset, style preset, palette generator) for the current editing session.
18
+ */ export function AppearanceLocks() {
19
+ const { locks } = useAppearanceSession();
20
+ const t = useThemeTranslations().appearance;
21
+ return /*#__PURE__*/ _jsxs("div", {
22
+ className: "tm-appearance-locks",
23
+ children: [
24
+ /*#__PURE__*/ _jsxs("div", {
25
+ className: "tm-appearance-locks__row",
26
+ role: "group",
27
+ "aria-label": t.locksLabel,
28
+ children: [
29
+ /*#__PURE__*/ _jsx("span", {
30
+ className: "tm-appearance-locks__label",
31
+ "aria-hidden": "true",
32
+ children: t.locksLabel
33
+ }),
34
+ LOCKS.map((lock)=>{
35
+ const locked = locks[lock];
36
+ const Icon = locked ? Lock : LockOpen;
37
+ return /*#__PURE__*/ _jsxs("button", {
38
+ type: "button",
39
+ className: `tm-appearance-lock${locked ? ' is-locked' : ''}`,
40
+ "aria-pressed": locked,
41
+ onClick: ()=>toggleAppearanceLock(lock),
42
+ children: [
43
+ /*#__PURE__*/ _jsx(Icon, {
44
+ size: 14,
45
+ "aria-hidden": true
46
+ }),
47
+ t[lock]
48
+ ]
49
+ }, lock);
50
+ })
51
+ ]
52
+ }),
53
+ /*#__PURE__*/ _jsx("p", {
54
+ className: "tm-appearance-locks__hint",
55
+ children: t.locksHint
56
+ })
57
+ ]
58
+ });
59
+ }
60
+ /**
61
+ * One-step undo for the last bulk change made from `source`. Stays for 10 s,
62
+ * paused while the pointer or keyboard focus is inside it.
63
+ */ export function AppearanceUndoBar({ sources }) {
64
+ const { undo } = useAppearanceSession();
65
+ const { dispatchFields, setModified } = useForm();
66
+ const t = useThemeTranslations().appearance;
67
+ const [paused, setPaused] = useState(false);
68
+ const [announcement, setAnnouncement] = useState('');
69
+ const barRef = useRef(null);
70
+ const visible = undo !== null && sources.includes(undo.source);
71
+ useEffect(()=>{
72
+ if (!visible || paused || !undo) return;
73
+ const timer = window.setTimeout(()=>clearAppearanceUndo(undo.id), UNDO_TIMEOUT_MS);
74
+ return ()=>window.clearTimeout(timer);
75
+ }, [
76
+ visible,
77
+ paused,
78
+ undo
79
+ ]);
80
+ useEffect(()=>{
81
+ if (visible && undo) setAnnouncement(formatTemplate(t.applied, {
82
+ name: undo.label
83
+ }));
84
+ }, [
85
+ visible,
86
+ undo,
87
+ t.applied
88
+ ]);
89
+ const handleUndo = ()=>{
90
+ if (!undo) return;
91
+ undo.entries.forEach(({ path, value })=>dispatchFields({
92
+ type: 'UPDATE',
93
+ path,
94
+ value
95
+ }));
96
+ setModified(true);
97
+ clearAppearanceUndo(undo.id);
98
+ setAnnouncement(t.undone);
99
+ };
100
+ return /*#__PURE__*/ _jsxs(_Fragment, {
101
+ children: [
102
+ /*#__PURE__*/ _jsx("span", {
103
+ className: "tm-sr-only",
104
+ role: "status",
105
+ "aria-live": "polite",
106
+ children: announcement
107
+ }),
108
+ visible && undo ? /*#__PURE__*/ _jsxs("div", {
109
+ ref: barRef,
110
+ className: "tm-appearance-undo",
111
+ onPointerEnter: ()=>setPaused(true),
112
+ onPointerLeave: ()=>setPaused(false),
113
+ onFocus: ()=>setPaused(true),
114
+ onBlur: (event)=>{
115
+ if (!barRef.current?.contains(event.relatedTarget)) setPaused(false);
116
+ },
117
+ children: [
118
+ /*#__PURE__*/ _jsx("span", {
119
+ className: "tm-appearance-undo__text",
120
+ children: formatTemplate(t.applied, {
121
+ name: undo.label
122
+ })
123
+ }),
124
+ /*#__PURE__*/ _jsxs("button", {
125
+ type: "button",
126
+ className: "tm-appearance-undo__action",
127
+ onClick: handleUndo,
128
+ children: [
129
+ /*#__PURE__*/ _jsx(Undo2, {
130
+ size: 14,
131
+ "aria-hidden": true
132
+ }),
133
+ t.undo
134
+ ]
135
+ }),
136
+ /*#__PURE__*/ _jsx("button", {
137
+ type: "button",
138
+ className: "tm-appearance-undo__dismiss",
139
+ "aria-label": t.dismiss,
140
+ onClick: ()=>clearAppearanceUndo(undo.id),
141
+ children: /*#__PURE__*/ _jsx(X, {
142
+ size: 14,
143
+ "aria-hidden": true
144
+ })
145
+ })
146
+ ]
147
+ }) : null
148
+ ]
149
+ });
150
+ }
151
+
152
+ //# sourceMappingURL=AppearanceSessionControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/AppearanceSessionControls.tsx"],"sourcesContent":["'use client'\n\nimport { useForm } from '@payloadcms/ui'\nimport { Lock, LockOpen, Undo2, X } from 'lucide-react'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n clearAppearanceUndo,\n toggleAppearanceLock,\n useAppearanceSession,\n type AppearanceLock,\n type AppearanceSource,\n} from '../hooks/useAppearanceSession.js'\nimport { useThemeTranslations } from '../hooks/useThemeTranslations.js'\n\nconst LOCKS: AppearanceLock[] = ['colors', 'fonts', 'style']\nconst UNDO_TIMEOUT_MS = 10_000\n\nexport const formatTemplate = (template: string, values: Record<string, string | number>) =>\n template.replace(/\\{(\\w+)\\}/g, (match, key: string) => String(values[key] ?? match))\n\n/**\n * \"Keep my: Colours · Fonts · Style\" toggles. Locks apply to every bulk action\n * (theme preset, style preset, palette generator) for the current editing session.\n */\nexport function AppearanceLocks() {\n const { locks } = useAppearanceSession()\n const t = useThemeTranslations().appearance\n\n return (\n <div className=\"tm-appearance-locks\">\n <div className=\"tm-appearance-locks__row\" role=\"group\" aria-label={t.locksLabel}>\n <span className=\"tm-appearance-locks__label\" aria-hidden=\"true\">\n {t.locksLabel}\n </span>\n {LOCKS.map((lock) => {\n const locked = locks[lock]\n const Icon = locked ? Lock : LockOpen\n return (\n <button\n key={lock}\n type=\"button\"\n className={`tm-appearance-lock${locked ? ' is-locked' : ''}`}\n aria-pressed={locked}\n onClick={() => toggleAppearanceLock(lock)}\n >\n <Icon size={14} aria-hidden />\n {t[lock]}\n </button>\n )\n })}\n </div>\n <p className=\"tm-appearance-locks__hint\">{t.locksHint}</p>\n </div>\n )\n}\n\n/**\n * One-step undo for the last bulk change made from `source`. Stays for 10 s,\n * paused while the pointer or keyboard focus is inside it.\n */\nexport function AppearanceUndoBar({ sources }: { sources: AppearanceSource[] }) {\n const { undo } = useAppearanceSession()\n const { dispatchFields, setModified } = useForm()\n const t = useThemeTranslations().appearance\n const [paused, setPaused] = useState(false)\n const [announcement, setAnnouncement] = useState('')\n const barRef = useRef<HTMLDivElement>(null)\n const visible = undo !== null && sources.includes(undo.source)\n\n useEffect(() => {\n if (!visible || paused || !undo) return\n const timer = window.setTimeout(() => clearAppearanceUndo(undo.id), UNDO_TIMEOUT_MS)\n return () => window.clearTimeout(timer)\n }, [visible, paused, undo])\n\n useEffect(() => {\n if (visible && undo) setAnnouncement(formatTemplate(t.applied, { name: undo.label }))\n }, [visible, undo, t.applied])\n\n const handleUndo = () => {\n if (!undo) return\n undo.entries.forEach(({ path, value }) => dispatchFields({ type: 'UPDATE', path, value }))\n setModified(true)\n clearAppearanceUndo(undo.id)\n setAnnouncement(t.undone)\n }\n\n return (\n <>\n {/* Always-mounted live region so both \"applied\" and \"reverted\" are announced. */}\n <span className=\"tm-sr-only\" role=\"status\" aria-live=\"polite\">\n {announcement}\n </span>\n {visible && undo ? (\n <div\n ref={barRef}\n className=\"tm-appearance-undo\"\n onPointerEnter={() => setPaused(true)}\n onPointerLeave={() => setPaused(false)}\n onFocus={() => setPaused(true)}\n onBlur={(event) => {\n if (!barRef.current?.contains(event.relatedTarget as Node)) setPaused(false)\n }}\n >\n <span className=\"tm-appearance-undo__text\">\n {formatTemplate(t.applied, { name: undo.label })}\n </span>\n <button type=\"button\" className=\"tm-appearance-undo__action\" onClick={handleUndo}>\n <Undo2 size={14} aria-hidden />\n {t.undo}\n </button>\n <button\n type=\"button\"\n className=\"tm-appearance-undo__dismiss\"\n aria-label={t.dismiss}\n onClick={() => clearAppearanceUndo(undo.id)}\n >\n <X size={14} aria-hidden />\n </button>\n </div>\n ) : null}\n </>\n )\n}\n"],"names":["useForm","Lock","LockOpen","Undo2","X","useEffect","useRef","useState","clearAppearanceUndo","toggleAppearanceLock","useAppearanceSession","useThemeTranslations","LOCKS","UNDO_TIMEOUT_MS","formatTemplate","template","values","replace","match","key","String","AppearanceLocks","locks","t","appearance","div","className","role","aria-label","locksLabel","span","aria-hidden","map","lock","locked","Icon","button","type","aria-pressed","onClick","size","p","locksHint","AppearanceUndoBar","sources","undo","dispatchFields","setModified","paused","setPaused","announcement","setAnnouncement","barRef","visible","includes","source","timer","window","setTimeout","id","clearTimeout","applied","name","label","handleUndo","entries","forEach","path","value","undone","aria-live","ref","onPointerEnter","onPointerLeave","onFocus","onBlur","event","current","contains","relatedTarget","dismiss"],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /* View Transitions ripple for ColorModeToggle.
2
+ Disable the default cross-fade so the clip-path circle reveal is clean. */
3
+ ::view-transition-old(root),
4
+ ::view-transition-new(root) {
5
+ animation: none;
6
+ mix-blend-mode: normal;
7
+ }
8
+
9
+ /* New (incoming) theme sits on top and is revealed by the expanding circle. */
10
+ ::view-transition-new(root) {
11
+ z-index: 1;
12
+ }
13
+ ::view-transition-old(root) {
14
+ z-index: 0;
15
+ }
16
+
17
+ .tm-color-mode-toggle {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ padding: 0;
22
+ margin: 0;
23
+ border: none;
24
+ background: transparent;
25
+ color: inherit;
26
+ cursor: pointer;
27
+ line-height: 0;
28
+ -webkit-tap-highlight-color: transparent;
29
+ }
30
+
31
+ .tm-color-mode-toggle:focus-visible {
32
+ outline: 2px solid currentColor;
33
+ outline-offset: 3px;
34
+ border-radius: 6px;
35
+ }
36
+
37
+ @media (prefers-reduced-motion: reduce) {
38
+ ::view-transition-old(root),
39
+ ::view-transition-new(root) {
40
+ animation: none !important;
41
+ }
42
+ }
@@ -0,0 +1,40 @@
1
+ import { type LucideIcon } from 'lucide-react';
2
+ import React from 'react';
3
+ import type { Mode } from '../providers/Theme/types.js';
4
+ import './ColorModeToggle.css';
5
+ export interface ColorModeToggleProps {
6
+ /** Extra class applied to the button (alongside `tm-color-mode-toggle`). */
7
+ className?: string;
8
+ style?: React.CSSProperties;
9
+ /** Icon size in px. Default 20. */
10
+ size?: number;
11
+ /**
12
+ * Cycle light → dark → auto on each click instead of a simple light/dark
13
+ * toggle. Default false.
14
+ */
15
+ includeAuto?: boolean;
16
+ /** Disable the View Transitions ripple (falls back to an instant switch). */
17
+ disableAnimation?: boolean;
18
+ /** Override the icons used for each mode. */
19
+ icons?: Partial<Record<Mode, LucideIcon>>;
20
+ 'aria-label'?: string;
21
+ }
22
+ /**
23
+ * Drop-in, accessible light/dark (optionally auto) toggle for the front-end.
24
+ *
25
+ * Renders a single icon button wired to the plugin's `ThemeProvider`. When the
26
+ * browser supports the View Transitions API (and the user hasn't requested
27
+ * reduced motion) the theme change animates as a circular "ripple" expanding
28
+ * from the click position.
29
+ *
30
+ * Returns `null` when the admin has disabled the color-mode toggle.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * import { ColorModeToggle } from '@kilivi/payloadcms-theme-management/components/ColorModeToggle'
35
+ * <ColorModeToggle />
36
+ * ```
37
+ */
38
+ export declare const ColorModeToggle: React.FC<ColorModeToggleProps>;
39
+ export default ColorModeToggle;
40
+ //# sourceMappingURL=ColorModeToggle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorModeToggle.d.ts","sourceRoot":"","sources":["../../src/components/ColorModeToggle.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAClE,OAAO,KAAsB,MAAM,OAAO,CAAA;AAE1C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,6BAA6B,CAAA;AACvD,OAAO,uBAAuB,CAAA;AAE9B,MAAM,WAAW,oBAAoB;IACnC,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,mCAAmC;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAA;IACzC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAUD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,EAAE,CAAC,oBAAoB,CAqG1D,CAAA;AAED,eAAe,eAAe,CAAA"}
@@ -0,0 +1,111 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Monitor, Moon, Sun } from 'lucide-react';
4
+ import React, { useCallback } from 'react';
5
+ import { useTheme } from '../providers/Theme/index.js';
6
+ import './ColorModeToggle.css';
7
+ const DEFAULT_ICONS = {
8
+ light: Sun,
9
+ dark: Moon,
10
+ auto: Monitor
11
+ };
12
+ /**
13
+ * Drop-in, accessible light/dark (optionally auto) toggle for the front-end.
14
+ *
15
+ * Renders a single icon button wired to the plugin's `ThemeProvider`. When the
16
+ * browser supports the View Transitions API (and the user hasn't requested
17
+ * reduced motion) the theme change animates as a circular "ripple" expanding
18
+ * from the click position.
19
+ *
20
+ * Returns `null` when the admin has disabled the color-mode toggle.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * import { ColorModeToggle } from '@kilivi/payloadcms-theme-management/components/ColorModeToggle'
25
+ * <ColorModeToggle />
26
+ * ```
27
+ */ export const ColorModeToggle = ({ className, style, size = 20, includeAuto = false, disableAnimation = false, icons, 'aria-label': ariaLabel })=>{
28
+ const { mode, setMode, isColorModeToggleAllowed } = useTheme();
29
+ const resolvedCurrent = useCallback(()=>{
30
+ if (typeof document !== 'undefined') {
31
+ const attr = document.documentElement.getAttribute('data-theme-mode');
32
+ if (attr === 'dark' || attr === 'light') return attr;
33
+ }
34
+ return mode === 'dark' ? 'dark' : 'light';
35
+ }, [
36
+ mode
37
+ ]);
38
+ const getNextMode = useCallback(()=>{
39
+ if (includeAuto) {
40
+ if (mode === 'light') return 'dark';
41
+ if (mode === 'dark') return 'auto';
42
+ return 'light';
43
+ }
44
+ return resolvedCurrent() === 'dark' ? 'light' : 'dark';
45
+ }, [
46
+ includeAuto,
47
+ mode,
48
+ resolvedCurrent
49
+ ]);
50
+ const handleClick = useCallback((event)=>{
51
+ const next = getNextMode();
52
+ const prefersReduced = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
53
+ const canAnimate = !disableAnimation && !prefersReduced && typeof document !== 'undefined' && 'startViewTransition' in document;
54
+ if (!canAnimate) {
55
+ setMode(next);
56
+ return;
57
+ }
58
+ const x = event.clientX;
59
+ const y = event.clientY;
60
+ const endRadius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));
61
+ const startViewTransition = document.startViewTransition;
62
+ const transition = startViewTransition(()=>{
63
+ setMode(next);
64
+ });
65
+ transition.ready.then(()=>{
66
+ document.documentElement.animate({
67
+ clipPath: [
68
+ `circle(0px at ${x}px ${y}px)`,
69
+ `circle(${endRadius}px at ${x}px ${y}px)`
70
+ ]
71
+ }, {
72
+ duration: 450,
73
+ easing: 'ease-in-out',
74
+ pseudoElement: '::view-transition-new(root)'
75
+ });
76
+ }).catch(()=>{
77
+ /* animation is best-effort; the mode change already applied */ });
78
+ }, [
79
+ disableAnimation,
80
+ getNextMode,
81
+ setMode
82
+ ]);
83
+ // Honour the admin "allow color mode toggle" switch.
84
+ if (isColorModeToggleAllowed === false) {
85
+ return null;
86
+ }
87
+ const iconSet = {
88
+ ...DEFAULT_ICONS,
89
+ ...icons
90
+ };
91
+ const activeMode = mode ?? 'light';
92
+ const Icon = iconSet[activeMode] ?? DEFAULT_ICONS.light;
93
+ return /*#__PURE__*/ _jsx("button", {
94
+ type: "button",
95
+ onClick: handleClick,
96
+ className: [
97
+ 'tm-color-mode-toggle',
98
+ className
99
+ ].filter(Boolean).join(' '),
100
+ style: style,
101
+ "aria-label": ariaLabel ?? `Switch color mode (current: ${activeMode})`,
102
+ title: ariaLabel ?? `Color mode: ${activeMode}`,
103
+ children: /*#__PURE__*/ _jsx(Icon, {
104
+ size: size,
105
+ "aria-hidden": true
106
+ })
107
+ });
108
+ };
109
+ export default ColorModeToggle;
110
+
111
+ //# sourceMappingURL=ColorModeToggle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/ColorModeToggle.tsx"],"sourcesContent":["'use client'\n\nimport { Monitor, Moon, Sun, type LucideIcon } from 'lucide-react'\nimport React, { useCallback } from 'react'\nimport { useTheme } from '../providers/Theme/index.js'\nimport type { Mode } from '../providers/Theme/types.js'\nimport './ColorModeToggle.css'\n\nexport interface ColorModeToggleProps {\n /** Extra class applied to the button (alongside `tm-color-mode-toggle`). */\n className?: string\n style?: React.CSSProperties\n /** Icon size in px. Default 20. */\n size?: number\n /**\n * Cycle light → dark → auto on each click instead of a simple light/dark\n * toggle. Default false.\n */\n includeAuto?: boolean\n /** Disable the View Transitions ripple (falls back to an instant switch). */\n disableAnimation?: boolean\n /** Override the icons used for each mode. */\n icons?: Partial<Record<Mode, LucideIcon>>\n 'aria-label'?: string\n}\n\nconst DEFAULT_ICONS: Record<Mode, LucideIcon> = {\n light: Sun,\n dark: Moon,\n auto: Monitor,\n}\n\ntype StartViewTransition = (callback: () => void) => { ready: Promise<void> }\n\n/**\n * Drop-in, accessible light/dark (optionally auto) toggle for the front-end.\n *\n * Renders a single icon button wired to the plugin's `ThemeProvider`. When the\n * browser supports the View Transitions API (and the user hasn't requested\n * reduced motion) the theme change animates as a circular \"ripple\" expanding\n * from the click position.\n *\n * Returns `null` when the admin has disabled the color-mode toggle.\n *\n * @example\n * ```tsx\n * import { ColorModeToggle } from '@kilivi/payloadcms-theme-management/components/ColorModeToggle'\n * <ColorModeToggle />\n * ```\n */\nexport const ColorModeToggle: React.FC<ColorModeToggleProps> = ({\n className,\n style,\n size = 20,\n includeAuto = false,\n disableAnimation = false,\n icons,\n 'aria-label': ariaLabel,\n}) => {\n const { mode, setMode, isColorModeToggleAllowed } = useTheme()\n\n const resolvedCurrent = useCallback((): 'light' | 'dark' => {\n if (typeof document !== 'undefined') {\n const attr = document.documentElement.getAttribute('data-theme-mode')\n if (attr === 'dark' || attr === 'light') return attr\n }\n return mode === 'dark' ? 'dark' : 'light'\n }, [mode])\n\n const getNextMode = useCallback((): Mode => {\n if (includeAuto) {\n if (mode === 'light') return 'dark'\n if (mode === 'dark') return 'auto'\n return 'light'\n }\n return resolvedCurrent() === 'dark' ? 'light' : 'dark'\n }, [includeAuto, mode, resolvedCurrent])\n\n const handleClick = useCallback(\n (event: React.MouseEvent<HTMLButtonElement>) => {\n const next = getNextMode()\n const prefersReduced =\n typeof window !== 'undefined' &&\n window.matchMedia('(prefers-reduced-motion: reduce)').matches\n const canAnimate =\n !disableAnimation &&\n !prefersReduced &&\n typeof document !== 'undefined' &&\n 'startViewTransition' in document\n\n if (!canAnimate) {\n setMode(next)\n return\n }\n\n const x = event.clientX\n const y = event.clientY\n const endRadius = Math.hypot(\n Math.max(x, window.innerWidth - x),\n Math.max(y, window.innerHeight - y),\n )\n\n const startViewTransition = (\n document as unknown as { startViewTransition: StartViewTransition }\n ).startViewTransition\n\n const transition = startViewTransition(() => {\n setMode(next)\n })\n\n transition.ready\n .then(() => {\n document.documentElement.animate(\n {\n clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`],\n },\n {\n duration: 450,\n easing: 'ease-in-out',\n pseudoElement: '::view-transition-new(root)',\n },\n )\n })\n .catch(() => {\n /* animation is best-effort; the mode change already applied */\n })\n },\n [disableAnimation, getNextMode, setMode],\n )\n\n // Honour the admin \"allow color mode toggle\" switch.\n if (isColorModeToggleAllowed === false) {\n return null\n }\n\n const iconSet = { ...DEFAULT_ICONS, ...icons }\n const activeMode: Mode = mode ?? 'light'\n const Icon = iconSet[activeMode] ?? DEFAULT_ICONS.light\n\n return (\n <button\n type=\"button\"\n onClick={handleClick}\n className={['tm-color-mode-toggle', className].filter(Boolean).join(' ')}\n style={style}\n aria-label={ariaLabel ?? `Switch color mode (current: ${activeMode})`}\n title={ariaLabel ?? `Color mode: ${activeMode}`}\n >\n <Icon size={size} aria-hidden />\n </button>\n )\n}\n\nexport default ColorModeToggle\n"],"names":["Monitor","Moon","Sun","React","useCallback","useTheme","DEFAULT_ICONS","light","dark","auto","ColorModeToggle","className","style","size","includeAuto","disableAnimation","icons","ariaLabel","mode","setMode","isColorModeToggleAllowed","resolvedCurrent","document","attr","documentElement","getAttribute","getNextMode","handleClick","event","next","prefersReduced","window","matchMedia","matches","canAnimate","x","clientX","y","clientY","endRadius","Math","hypot","max","innerWidth","innerHeight","startViewTransition","transition","ready","then","animate","clipPath","duration","easing","pseudoElement","catch","iconSet","activeMode","Icon","button","type","onClick","filter","Boolean","join","aria-label","title","aria-hidden"],"mappings":"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"}
@@ -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;AAMD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,kBAAkB,GACnB,EAAE,QAAQ,CAAC,wBAAwB,CAAC,wCAsFpC"}
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,11 +9,14 @@ 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
- // Accept unknown type to avoid type conflicts between app's payload-types and plugin's payload-types
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
+ */ export async function ServerThemeInjector({ themeConfiguration, fontLoading = 'self-hosted', apiRoute = '/api' }) {
13
+ // resolveThemeConfiguration accepts `unknown` — no cast needed
14
14
  const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration);
15
- const { theme, borderRadius, customCSS, lightMode, darkMode, fontScale, spacing, animationLevel, allowColorModeToggle, colorMode, typography } = resolvedConfiguration;
15
+ const fontsHref = buildThemeFontsHref(resolvedConfiguration.fontSelections ?? [], {
16
+ mode: fontLoading,
17
+ apiRoute
18
+ });
19
+ const { theme, borderRadius, customCSS, lightMode, darkMode, fontScale, spacing, animationLevel, allowColorModeToggle, colorMode, typography, visualEffects, componentStyles } = resolvedConfiguration;
16
20
  // Use resolved configuration directly
17
21
  const normalizedThemeConfiguration = {
18
22
  theme,
@@ -25,20 +29,13 @@ import { InitTheme } from './InitTheme.js';
25
29
  customCSS: customCSS ?? null,
26
30
  lightMode,
27
31
  darkMode,
28
- typography: typography ?? undefined
32
+ typography: typography ?? undefined,
33
+ visualEffects: visualEffects ?? null,
34
+ componentStyles: componentStyles ?? null
29
35
  };
30
36
  let criticalCSS = await getThemeCriticalCSS(theme);
31
37
  criticalCSS ??= createFallbackCriticalCSS(theme);
32
- const borderRadiusMap = {
33
- none: 'none',
34
- small: 'small',
35
- medium: 'medium',
36
- large: 'large',
37
- xl: 'xl',
38
- full: 'xl'
39
- };
40
- const mappedBorderRadius = borderRadiusMap[borderRadius ?? 'medium'] ?? 'medium';
41
- const borderRadiusConfig = getBorderRadiusConfig(mappedBorderRadius);
38
+ const borderRadiusConfig = getBorderRadiusConfig(borderRadius ?? 'medium');
42
39
  const cssRecord = typeof borderRadiusConfig?.css === 'string' ? {
43
40
  '--radius-default': borderRadiusConfig.css
44
41
  } : borderRadiusConfig?.css ?? {};
@@ -53,16 +50,69 @@ ${borderRadiusCSS}
53
50
  darkMode
54
51
  });
55
52
  const customCSSBlock = typeof customCSS === 'string' ? customCSS.trim() : '';
53
+ // `color-scheme` makes native UI (form controls, scrollbars) match the theme and
54
+ // avoids a white flash on dark sites. Fixed mode → single scheme; toggle/auto → both.
55
+ const colorScheme = allowColorModeToggle === false ? colorMode === 'dark' ? 'dark' : 'light' : 'light dark';
56
+ const colorSchemeBlock = `:root {\n color-scheme: ${colorScheme};\n}`;
56
57
  const combinedCSS = [
57
58
  criticalCSS,
59
+ colorSchemeBlock,
58
60
  borderRadiusBlock,
59
61
  themeConfigurationCSS,
60
62
  colorModesCSS,
61
63
  customCSSBlock
62
64
  ].filter((block)=>Boolean(block?.trim?.().length)).join('\n\n');
65
+ // <meta name="theme-color"> colours the mobile browser chrome to match the page
66
+ // background. Emit per-scheme variants when the mode can change, otherwise a
67
+ // single tag matching the fixed mode.
68
+ const pick = (mode, key)=>{
69
+ const value = mode?.[key];
70
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
71
+ };
72
+ const lightBg = pick(lightMode, 'background');
73
+ const darkBg = pick(darkMode, 'background');
74
+ const emitBothSchemes = allowColorModeToggle !== false || colorMode === 'auto';
63
75
  return /*#__PURE__*/ _jsxs(_Fragment, {
64
76
  children: [
65
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,
96
+ emitBothSchemes ? /*#__PURE__*/ _jsxs(_Fragment, {
97
+ children: [
98
+ lightBg ? /*#__PURE__*/ _jsx("meta", {
99
+ name: "theme-color",
100
+ media: "(prefers-color-scheme: light)",
101
+ content: lightBg
102
+ }) : null,
103
+ darkBg ? /*#__PURE__*/ _jsx("meta", {
104
+ name: "theme-color",
105
+ media: "(prefers-color-scheme: dark)",
106
+ content: darkBg
107
+ }) : null
108
+ ]
109
+ }) : (()=>{
110
+ const fixed = colorMode === 'dark' ? darkBg : lightBg;
111
+ return fixed ? /*#__PURE__*/ _jsx("meta", {
112
+ name: "theme-color",
113
+ content: fixed
114
+ }) : null;
115
+ })(),
66
116
  /*#__PURE__*/ _jsx("style", {
67
117
  id: "theme-critical-css",
68
118
  dangerouslySetInnerHTML: {
@@ -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 { BorderRadiusPreset, 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/**\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 // Accept unknown type to avoid type conflicts between app's payload-types and plugin's payload-types\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration as any)\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 } = 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 }\n\n let criticalCSS = await getThemeCriticalCSS(theme as ThemeDefaults)\n criticalCSS ??= createFallbackCriticalCSS(theme as ThemeDefaults)\n\n const borderRadiusMap: Record<string, BorderRadiusPreset> = {\n none: 'none',\n small: 'small',\n medium: 'medium',\n large: 'large',\n xl: 'xl',\n full: 'xl',\n }\n const mappedBorderRadius = borderRadiusMap[borderRadius ?? 'medium'] ?? 'medium'\n const borderRadiusConfig = getBorderRadiusConfig(mappedBorderRadius)\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 const combinedCSS = [\n criticalCSS,\n borderRadiusBlock,\n themeConfigurationCSS,\n colorModesCSS,\n customCSSBlock,\n ]\n .filter((block) => Boolean(block?.trim?.().length))\n .join('\\n\\n')\n\n return (\n <>\n <InitTheme />\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","normalizedThemeConfiguration","undefined","criticalCSS","borderRadiusMap","none","small","medium","large","xl","full","mappedBorderRadius","borderRadiusConfig","cssRecord","css","borderRadiusCSS","Object","entries","map","property","value","join","borderRadiusBlock","themeConfigurationCSS","colorModesCSS","themeName","customCSSBlock","trim","combinedCSS","filter","block","Boolean","length","style","id","dangerouslySetInnerHTML","__html"],"mappings":";AACA,SAASA,qBAAqB,QAAQ,oCAAmC;AAEzE,SAASC,yBAAyB,QAAQ,wCAAuC;AAEjF,SAASC,yBAAyB,EAAEC,mBAAmB,QAAQ,0BAAyB;AACxF,SAASC,sBAAsB,QAAQ,0BAAyB;AAChE,SAASC,gBAAgB,QAAQ,yBAAwB;AACzD,SAASC,SAAS,QAAQ,iBAAgB;AAoB1C;;;CAGC,GACD,OAAO,eAAeC,oBAAoB,EACxCC,kBAAkB,EACiB;IACnC,qGAAqG;IACrG,8DAA8D;IAC9D,MAAMC,wBAAwBR,0BAA0BO;IACxD,MAAM,EACJE,KAAK,EACLC,YAAY,EACZC,SAAS,EACTC,SAAS,EACTC,QAAQ,EACRC,SAAS,EACTC,OAAO,EACPC,cAAc,EACdC,oBAAoB,EACpBC,SAAS,EACTC,UAAU,EACX,GAAGX;IAEJ,sCAAsC;IACtC,MAAMY,+BAA0D;QAC9DX;QACAS;QACAD;QACAP;QACAI;QACAC;QACAC;QACAL,WAAWA,aAAa;QACxBC;QACAC;QACAM,YAAYA,cAAcE;IAC5B;IAEA,IAAIC,cAAc,MAAMpB,oBAAoBO;IAC5Ca,gBAAgBrB,0BAA0BQ;IAE1C,MAAMc,kBAAsD;QAC1DC,MAAM;QACNC,OAAO;QACPC,QAAQ;QACRC,OAAO;QACPC,IAAI;QACJC,MAAM;IACR;IACA,MAAMC,qBAAqBP,eAAe,CAACb,gBAAgB,SAAS,IAAI;IACxE,MAAMqB,qBAAqBhC,sBAAsB+B;IACjD,MAAME,YACJ,OAAOD,oBAAoBE,QAAQ,WAC/B;QAAE,oBAAoBF,mBAAmBE,GAAG;IAAC,IAC5CF,oBAAoBE,OAAO,CAAC;IAEnC,MAAMC,kBAAkBC,OAAOC,OAAO,CAACJ,WACpCK,GAAG,CAAC,CAAC,CAACC,UAAUC,MAAM,GAAK,CAAC,EAAE,EAAED,SAAS,EAAE,EAAEC,MAAM,CAAC,CAAC,EACrDC,IAAI,CAAC;IAER,MAAMC,oBAAoBP,kBACtB,CAAC;AACP,EAAEA,gBAAgB;CACjB,CAAC,GACI;IAEJ,MAAMQ,wBAAwBtC,iBAAiBgB;IAC/C,MAAMuB,gBAAgBxC,uBAAuB;QAAEyC,WAAWnC;QAAOG;QAAWC;IAAS;IACrF,MAAMgC,iBAAiB,OAAOlC,cAAc,WAAWA,UAAUmC,IAAI,KAAK;IAE1E,MAAMC,cAAc;QAClBzB;QACAmB;QACAC;QACAC;QACAE;KACD,CACEG,MAAM,CAAC,CAACC,QAAUC,QAAQD,OAAOH,SAASK,SAC1CX,IAAI,CAAC;IAER,qBACE;;0BACE,KAACnC;0BACD,KAAC+C;gBACCC,IAAG;gBACHC,yBAAyB;oBACvBC,QAAQR;gBACV;;;;AAIR"}
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,3 +1,5 @@
1
+ import React from 'react';
2
+ import type { ThemeFontLoading } from '../constants/themeFonts.js';
1
3
  interface ThemeLivePreviewSyncProps {
2
4
  initialData: Record<string, unknown>;
3
5
  serverURL: string;
@@ -5,6 +7,8 @@ interface ThemeLivePreviewSyncProps {
5
7
  depth?: number;
6
8
  themeConfigurationKey?: string;
7
9
  styleElementId?: string;
10
+ /** Same as ServerThemeInjector's `fontLoading`; use 'none' with next/font. */
11
+ fontLoading?: ThemeFontLoading;
8
12
  }
9
13
  export declare const ThemeLivePreviewSync: React.FC<ThemeLivePreviewSyncProps>;
10
14
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeLivePreviewSync.d.ts","sourceRoot":"","sources":["../../src/components/ThemeLivePreviewSync.tsx"],"names":[],"mappings":"AASA,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"}