@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.
- package/README.md +346 -845
- package/dist/components/AppearanceSessionControls.d.ts +15 -0
- package/dist/components/AppearanceSessionControls.d.ts.map +1 -0
- package/dist/components/AppearanceSessionControls.js +152 -0
- package/dist/components/AppearanceSessionControls.js.map +1 -0
- package/dist/components/ColorModeToggle.css +42 -0
- package/dist/components/ColorModeToggle.d.ts +40 -0
- package/dist/components/ColorModeToggle.d.ts.map +1 -0
- package/dist/components/ColorModeToggle.js +111 -0
- package/dist/components/ColorModeToggle.js.map +1 -0
- package/dist/components/ServerThemeInjector.d.ts +13 -1
- package/dist/components/ServerThemeInjector.d.ts.map +1 -1
- package/dist/components/ServerThemeInjector.js +65 -15
- package/dist/components/ServerThemeInjector.js.map +1 -1
- package/dist/components/ThemeLivePreviewSync.d.ts +4 -0
- package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
- package/dist/components/ThemeLivePreviewSync.js +28 -4
- package/dist/components/ThemeLivePreviewSync.js.map +1 -1
- package/dist/components/ThemeSwitcher.d.ts +36 -0
- package/dist/components/ThemeSwitcher.d.ts.map +1 -0
- package/dist/components/ThemeSwitcher.js +89 -0
- package/dist/components/ThemeSwitcher.js.map +1 -0
- package/dist/components/typographyPreviewUtils.d.ts +5 -0
- package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
- package/dist/components/typographyPreviewUtils.js +24 -11
- package/dist/components/typographyPreviewUtils.js.map +1 -1
- package/dist/constants/devicePreviews.d.ts +35 -0
- package/dist/constants/devicePreviews.d.ts.map +1 -0
- package/dist/constants/devicePreviews.js +43 -0
- package/dist/constants/devicePreviews.js.map +1 -0
- package/dist/constants/themeFonts.d.ts +94 -168
- package/dist/constants/themeFonts.d.ts.map +1 -1
- package/dist/constants/themeFonts.js +260 -140
- package/dist/constants/themeFonts.js.map +1 -1
- package/dist/endpoints/themeFonts.d.ts +5 -0
- package/dist/endpoints/themeFonts.d.ts.map +1 -0
- package/dist/endpoints/themeFonts.js +138 -0
- package/dist/endpoints/themeFonts.js.map +1 -0
- package/dist/extended-presets.d.ts +0 -4
- package/dist/extended-presets.d.ts.map +1 -1
- package/dist/extended-presets.js +1 -18
- package/dist/extended-presets.js.map +1 -1
- package/dist/extended-themes.d.ts.map +1 -1
- package/dist/extended-themes.js +428 -0
- package/dist/extended-themes.js.map +1 -1
- package/dist/fields/AccessibilityAuditField.d.ts +2 -0
- package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
- package/dist/fields/AccessibilityAuditField.js +303 -0
- package/dist/fields/AccessibilityAuditField.js.map +1 -0
- package/dist/fields/AppearancePreviewField.d.ts +2 -0
- package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
- package/dist/fields/AppearancePreviewField.js +826 -0
- package/dist/fields/AppearancePreviewField.js.map +1 -0
- package/dist/fields/ColorPickerField.d.ts.map +1 -1
- package/dist/fields/ColorPickerField.js +89 -85
- package/dist/fields/ColorPickerField.js.map +1 -1
- package/dist/fields/FontSelectField.css +155 -0
- package/dist/fields/FontSelectField.d.ts +8 -0
- package/dist/fields/FontSelectField.d.ts.map +1 -1
- package/dist/fields/FontSelectField.js +223 -188
- package/dist/fields/FontSelectField.js.map +1 -1
- package/dist/fields/PaletteGeneratorField.d.ts +7 -0
- package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
- package/dist/fields/PaletteGeneratorField.js +327 -0
- package/dist/fields/PaletteGeneratorField.js.map +1 -0
- package/dist/fields/StylePresetField.d.ts +3 -0
- package/dist/fields/StylePresetField.d.ts.map +1 -0
- package/dist/fields/StylePresetField.js +463 -0
- package/dist/fields/StylePresetField.js.map +1 -0
- package/dist/fields/ThemeColorPickerField.css +115 -35
- package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
- package/dist/fields/ThemeColorPickerField.js +204 -70
- package/dist/fields/ThemeColorPickerField.js.map +1 -1
- package/dist/fields/ThemeExportField.d.ts +6 -0
- package/dist/fields/ThemeExportField.d.ts.map +1 -0
- package/dist/fields/ThemeExportField.js +202 -0
- package/dist/fields/ThemeExportField.js.map +1 -0
- package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
- package/dist/fields/ThemePresetImportField.js +56 -25
- package/dist/fields/ThemePresetImportField.js.map +1 -1
- package/dist/fields/ThemePreviewField.css +444 -5
- package/dist/fields/ThemePreviewField.d.ts.map +1 -1
- package/dist/fields/ThemePreviewField.js +265 -170
- package/dist/fields/ThemePreviewField.js.map +1 -1
- package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
- package/dist/fields/ThemeTokenSelectField.js +278 -80
- package/dist/fields/ThemeTokenSelectField.js.map +1 -1
- package/dist/fields/colorFieldHelpers.d.ts +90 -0
- package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
- package/dist/fields/colorFieldHelpers.js +133 -0
- package/dist/fields/colorFieldHelpers.js.map +1 -0
- package/dist/fields/colorModeFields.d.ts +1 -1
- package/dist/fields/colorModeFields.d.ts.map +1 -1
- package/dist/fields/colorModeFields.js +49 -7
- package/dist/fields/colorModeFields.js.map +1 -1
- package/dist/fields/themeConfigurationField.d.ts +8 -1
- package/dist/fields/themeConfigurationField.d.ts.map +1 -1
- package/dist/fields/themeConfigurationField.js +1209 -228
- package/dist/fields/themeConfigurationField.js.map +1 -1
- package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
- package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
- package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
- package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
- package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
- package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
- package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
- package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
- package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
- package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
- package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
- package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
- package/dist/globals/ThemeSettings/config.js +76 -79
- package/dist/globals/ThemeSettings/config.js.map +1 -1
- package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
- package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
- package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
- package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
- package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
- package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
- package/dist/hooks/useAppearanceSession.d.ts +50 -0
- package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
- package/dist/hooks/useAppearanceSession.js +91 -0
- package/dist/hooks/useAppearanceSession.js.map +1 -0
- package/dist/hooks/useNativeLivePreview.d.ts +15 -0
- package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
- package/dist/hooks/useNativeLivePreview.js +18 -0
- package/dist/hooks/useNativeLivePreview.js.map +1 -0
- package/dist/hooks/useThemeFonts.d.ts +10 -0
- package/dist/hooks/useThemeFonts.d.ts.map +1 -0
- package/dist/hooks/useThemeFonts.js +36 -0
- package/dist/hooks/useThemeFonts.js.map +1 -0
- package/dist/hooks/useThemeTranslations.d.ts +13 -0
- package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
- package/dist/hooks/useThemeTranslations.js +22 -0
- package/dist/hooks/useThemeTranslations.js.map +1 -0
- package/dist/i18n.d.ts +53 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +53 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -29
- package/dist/index.js.map +1 -1
- package/dist/next-fonts.d.ts +3 -0
- package/dist/next-fonts.d.ts.map +1 -0
- package/dist/next-fonts.js +360 -0
- package/dist/next-fonts.js.map +1 -0
- package/dist/payload-types.d.ts +34 -6
- package/dist/payload-types.d.ts.map +1 -1
- package/dist/payload-types.js.map +1 -1
- package/dist/presets.d.ts +14 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/providers/Theme/index.d.ts.map +1 -1
- package/dist/providers/Theme/index.js +75 -11
- package/dist/providers/Theme/index.js.map +1 -1
- package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
- package/dist/providers/Theme/themeConfig.js +104 -0
- package/dist/providers/Theme/themeConfig.js.map +1 -1
- package/dist/providers/Theme/types.d.ts +1 -1
- package/dist/providers/Theme/types.d.ts.map +1 -1
- package/dist/providers/Theme/types.js +10 -1
- package/dist/providers/Theme/types.js.map +1 -1
- package/dist/style-presets.d.ts +75 -0
- package/dist/style-presets.d.ts.map +1 -0
- package/dist/style-presets.js +738 -0
- package/dist/style-presets.js.map +1 -0
- package/dist/translations.d.ts +164 -1
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +320 -9
- package/dist/translations.js.map +1 -1
- package/dist/tweakcn-special-themes.d.ts.map +1 -1
- package/dist/tweakcn-special-themes.js +585 -9
- package/dist/tweakcn-special-themes.js.map +1 -1
- package/dist/types/css.d.js +3 -0
- package/dist/types/css.d.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/getThemeSettings.d.ts.map +1 -1
- package/dist/utilities/getThemeSettings.js +2 -14
- package/dist/utilities/getThemeSettings.js.map +1 -1
- package/dist/utils/contrast.d.ts +63 -0
- package/dist/utils/contrast.d.ts.map +1 -0
- package/dist/utils/contrast.js +158 -0
- package/dist/utils/contrast.js.map +1 -0
- package/dist/utils/exportTokens.d.ts +30 -0
- package/dist/utils/exportTokens.d.ts.map +1 -0
- package/dist/utils/exportTokens.js +183 -0
- package/dist/utils/exportTokens.js.map +1 -0
- package/dist/utils/generatePalette.d.ts +46 -0
- package/dist/utils/generatePalette.d.ts.map +1 -0
- package/dist/utils/generatePalette.js +127 -0
- package/dist/utils/generatePalette.js.map +1 -0
- package/dist/utils/generateThemeCSS.d.ts.map +1 -1
- package/dist/utils/generateThemeCSS.js +2 -40
- package/dist/utils/generateThemeCSS.js.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.d.ts +22 -3
- package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.js +48 -15
- package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
- package/dist/utils/localizedText.d.ts +8 -0
- package/dist/utils/localizedText.d.ts.map +1 -0
- package/dist/utils/localizedText.js +21 -0
- package/dist/utils/localizedText.js.map +1 -0
- package/dist/utils/resolveThemeConfiguration.d.ts +60 -1
- package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
- package/dist/utils/resolveThemeConfiguration.js +53 -13
- package/dist/utils/resolveThemeConfiguration.js.map +1 -1
- package/dist/utils/themeColors.d.ts +3 -0
- package/dist/utils/themeColors.d.ts.map +1 -1
- package/dist/utils/themeColors.js +30 -1
- package/dist/utils/themeColors.js.map +1 -1
- package/dist/utils/themeHtmlAttributes.d.ts +32 -12
- package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
- package/dist/utils/themeHtmlAttributes.js +38 -5
- package/dist/utils/themeHtmlAttributes.js.map +1 -1
- package/dist/utils/themeUtils.d.ts +3 -5
- package/dist/utils/themeUtils.d.ts.map +1 -1
- package/dist/utils/themeUtils.js +474 -6
- package/dist/utils/themeUtils.js.map +1 -1
- package/dist/utils/tweakcnConverter.d.ts +1 -3
- package/dist/utils/tweakcnConverter.d.ts.map +1 -1
- package/dist/utils/tweakcnConverter.js +1 -3
- package/dist/utils/tweakcnConverter.js.map +1 -1
- package/package.json +15 -5
- package/dist/components/ThemePreview.d.ts +0 -3
- package/dist/components/ThemePreview.d.ts.map +0 -1
- package/dist/components/ThemePreview.js +0 -321
- package/dist/components/ThemePreview.js.map +0 -1
- package/dist/plugin.d.ts +0 -2
- package/dist/plugin.d.ts.map +0 -1
- package/dist/plugin.js +0 -3
- 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":"
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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":"
|
|
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"}
|