@kilivi/payloadcms-theme-management 2.0.0 → 2.1.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 +318 -847
- 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.map +1 -1
- package/dist/components/ServerThemeInjector.js +41 -14
- package/dist/components/ServerThemeInjector.js.map +1 -1
- package/dist/components/ThemeLivePreviewSync.d.ts +1 -0
- package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
- package/dist/components/ThemeLivePreviewSync.js +1 -1
- 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.map +1 -1
- package/dist/components/typographyPreviewUtils.js +8 -1
- 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.js +9 -9
- package/dist/constants/themeFonts.js.map +1 -1
- 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 +763 -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.d.ts +6 -0
- package/dist/fields/FontSelectField.d.ts.map +1 -1
- package/dist/fields/FontSelectField.js +195 -122
- 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 +462 -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 +382 -5
- package/dist/fields/ThemePreviewField.d.ts.map +1 -1
- package/dist/fields/ThemePreviewField.js +268 -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 +961 -29
- 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/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 +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -29
- package/dist/index.js.map +1 -1
- package/dist/payload-types.d.ts +24 -0
- package/dist/payload-types.d.ts.map +1 -1
- package/dist/payload-types.js.map +1 -1
- package/dist/presets.d.ts +8 -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 +66 -0
- package/dist/style-presets.d.ts.map +1 -0
- package/dist/style-presets.js +556 -0
- package/dist/style-presets.js.map +1 -0
- package/dist/translations.d.ts +159 -1
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +310 -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 +16 -2
- package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.js +37 -14
- 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 +45 -1
- package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
- package/dist/utils/resolveThemeConfiguration.js +12 -5
- 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 +416 -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 +11 -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 +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;
|
|
1
|
+
{"version":3,"file":"ServerThemeInjector.d.ts","sourceRoot":"","sources":["../../src/components/ServerThemeInjector.tsx"],"names":[],"mappings":"AAUA,UAAU,wBAAwB;IAChC;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAKD;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,EACxC,kBAAkB,GACnB,EAAE,QAAQ,CAAC,wBAAwB,CAAC,wCAqHpC"}
|
|
@@ -9,10 +9,9 @@ import { InitTheme } from './InitTheme.js';
|
|
|
9
9
|
* Server-side theme CSS injector to prevent FOUC
|
|
10
10
|
* This component injects critical theme CSS directly into the HTML head during SSR
|
|
11
11
|
*/ export async function ServerThemeInjector({ themeConfiguration }) {
|
|
12
|
-
//
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
// resolveThemeConfiguration accepts `unknown` — no cast needed
|
|
14
13
|
const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration);
|
|
15
|
-
const { theme, borderRadius, customCSS, lightMode, darkMode, fontScale, spacing, animationLevel, allowColorModeToggle, colorMode, typography } = resolvedConfiguration;
|
|
14
|
+
const { theme, borderRadius, customCSS, lightMode, darkMode, fontScale, spacing, animationLevel, allowColorModeToggle, colorMode, typography, visualEffects, componentStyles } = resolvedConfiguration;
|
|
16
15
|
// Use resolved configuration directly
|
|
17
16
|
const normalizedThemeConfiguration = {
|
|
18
17
|
theme,
|
|
@@ -25,20 +24,13 @@ import { InitTheme } from './InitTheme.js';
|
|
|
25
24
|
customCSS: customCSS ?? null,
|
|
26
25
|
lightMode,
|
|
27
26
|
darkMode,
|
|
28
|
-
typography: typography ?? undefined
|
|
27
|
+
typography: typography ?? undefined,
|
|
28
|
+
visualEffects: visualEffects ?? null,
|
|
29
|
+
componentStyles: componentStyles ?? null
|
|
29
30
|
};
|
|
30
31
|
let criticalCSS = await getThemeCriticalCSS(theme);
|
|
31
32
|
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);
|
|
33
|
+
const borderRadiusConfig = getBorderRadiusConfig(borderRadius ?? 'medium');
|
|
42
34
|
const cssRecord = typeof borderRadiusConfig?.css === 'string' ? {
|
|
43
35
|
'--radius-default': borderRadiusConfig.css
|
|
44
36
|
} : borderRadiusConfig?.css ?? {};
|
|
@@ -53,16 +45,51 @@ ${borderRadiusCSS}
|
|
|
53
45
|
darkMode
|
|
54
46
|
});
|
|
55
47
|
const customCSSBlock = typeof customCSS === 'string' ? customCSS.trim() : '';
|
|
48
|
+
// `color-scheme` makes native UI (form controls, scrollbars) match the theme and
|
|
49
|
+
// avoids a white flash on dark sites. Fixed mode → single scheme; toggle/auto → both.
|
|
50
|
+
const colorScheme = allowColorModeToggle === false ? colorMode === 'dark' ? 'dark' : 'light' : 'light dark';
|
|
51
|
+
const colorSchemeBlock = `:root {\n color-scheme: ${colorScheme};\n}`;
|
|
56
52
|
const combinedCSS = [
|
|
57
53
|
criticalCSS,
|
|
54
|
+
colorSchemeBlock,
|
|
58
55
|
borderRadiusBlock,
|
|
59
56
|
themeConfigurationCSS,
|
|
60
57
|
colorModesCSS,
|
|
61
58
|
customCSSBlock
|
|
62
59
|
].filter((block)=>Boolean(block?.trim?.().length)).join('\n\n');
|
|
60
|
+
// <meta name="theme-color"> colours the mobile browser chrome to match the page
|
|
61
|
+
// background. Emit per-scheme variants when the mode can change, otherwise a
|
|
62
|
+
// single tag matching the fixed mode.
|
|
63
|
+
const pick = (mode, key)=>{
|
|
64
|
+
const value = mode?.[key];
|
|
65
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
66
|
+
};
|
|
67
|
+
const lightBg = pick(lightMode, 'background');
|
|
68
|
+
const darkBg = pick(darkMode, 'background');
|
|
69
|
+
const emitBothSchemes = allowColorModeToggle !== false || colorMode === 'auto';
|
|
63
70
|
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
64
71
|
children: [
|
|
65
72
|
/*#__PURE__*/ _jsx(InitTheme, {}),
|
|
73
|
+
emitBothSchemes ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
74
|
+
children: [
|
|
75
|
+
lightBg ? /*#__PURE__*/ _jsx("meta", {
|
|
76
|
+
name: "theme-color",
|
|
77
|
+
media: "(prefers-color-scheme: light)",
|
|
78
|
+
content: lightBg
|
|
79
|
+
}) : null,
|
|
80
|
+
darkBg ? /*#__PURE__*/ _jsx("meta", {
|
|
81
|
+
name: "theme-color",
|
|
82
|
+
media: "(prefers-color-scheme: dark)",
|
|
83
|
+
content: darkBg
|
|
84
|
+
}) : null
|
|
85
|
+
]
|
|
86
|
+
}) : (()=>{
|
|
87
|
+
const fixed = colorMode === 'dark' ? darkBg : lightBg;
|
|
88
|
+
return fixed ? /*#__PURE__*/ _jsx("meta", {
|
|
89
|
+
name: "theme-color",
|
|
90
|
+
content: fixed
|
|
91
|
+
}) : null;
|
|
92
|
+
})(),
|
|
66
93
|
/*#__PURE__*/ _jsx("style", {
|
|
67
94
|
id: "theme-critical-css",
|
|
68
95
|
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 {
|
|
1
|
+
{"version":3,"sources":["../../src/components/ServerThemeInjector.tsx"],"sourcesContent":["import type { ThemeTypographyPreset } from '../presets.js'\nimport { getBorderRadiusConfig } from '../providers/Theme/themeConfig.js'\nimport type { ThemeDefaults } from '../providers/Theme/types.js'\nimport { resolveThemeConfiguration } from '../utils/resolveThemeConfiguration.js'\nimport type { ResolvedThemeConfiguration } from '../utils/resolveThemeConfiguration.js'\nimport { createFallbackCriticalCSS, getThemeCriticalCSS } from '../utils/themeAssets.js'\nimport { generateThemeColorsCss } from '../utils/themeColors.js'\nimport { generateThemeCSS } from '../utils/themeUtils.js'\nimport { InitTheme } from './InitTheme.js'\n\ninterface ServerThemeInjectorProps {\n /**\n * Theme configuration object from your site settings.\n * Pass the themeConfiguration field directly, not the entire site settings object.\n *\n * @example\n * ```tsx\n * const siteSettings = await payload.findGlobal({ slug: 'site-settings' })\n * <ServerThemeInjector themeConfiguration={siteSettings.themeConfiguration} />\n * ```\n */\n themeConfiguration?: unknown\n}\n\ntype RuntimeThemeConfiguration = Omit<ResolvedThemeConfiguration, 'typography'> & {\n typography?: ThemeTypographyPreset | null\n}\n/**\n * Server-side theme CSS injector to prevent FOUC\n * This component injects critical theme CSS directly into the HTML head during SSR\n */\nexport async function ServerThemeInjector({\n themeConfiguration,\n}: Readonly<ServerThemeInjectorProps>) {\n // resolveThemeConfiguration accepts `unknown` — no cast needed\n const resolvedConfiguration = resolveThemeConfiguration(themeConfiguration)\n const {\n theme,\n borderRadius,\n customCSS,\n lightMode,\n darkMode,\n fontScale,\n spacing,\n animationLevel,\n allowColorModeToggle,\n colorMode,\n typography,\n visualEffects,\n componentStyles,\n } = resolvedConfiguration\n\n // Use resolved configuration directly\n const normalizedThemeConfiguration: RuntimeThemeConfiguration = {\n theme,\n colorMode,\n allowColorModeToggle,\n borderRadius,\n fontScale,\n spacing,\n animationLevel,\n customCSS: customCSS ?? null,\n lightMode,\n darkMode,\n typography: typography ?? undefined,\n visualEffects: visualEffects ?? null,\n componentStyles: componentStyles ?? null,\n }\n\n let criticalCSS = await getThemeCriticalCSS(theme as ThemeDefaults)\n criticalCSS ??= createFallbackCriticalCSS(theme as ThemeDefaults)\n\n const borderRadiusConfig = getBorderRadiusConfig(borderRadius ?? 'medium')\n const cssRecord =\n typeof borderRadiusConfig?.css === 'string'\n ? { '--radius-default': borderRadiusConfig.css }\n : (borderRadiusConfig?.css ?? {})\n\n const borderRadiusCSS = Object.entries(cssRecord)\n .map(([property, value]) => ` ${property}: ${value};`)\n .join('\\n')\n\n const borderRadiusBlock = borderRadiusCSS\n ? `:root {\n${borderRadiusCSS}\n}`\n : ''\n\n const themeConfigurationCSS = generateThemeCSS(normalizedThemeConfiguration)\n const colorModesCSS = generateThemeColorsCss({ themeName: theme, lightMode, darkMode })\n const customCSSBlock = typeof customCSS === 'string' ? customCSS.trim() : ''\n\n // `color-scheme` makes native UI (form controls, scrollbars) match the theme and\n // avoids a white flash on dark sites. Fixed mode → single scheme; toggle/auto → both.\n const colorScheme =\n allowColorModeToggle === false\n ? colorMode === 'dark'\n ? 'dark'\n : 'light'\n : 'light dark'\n const colorSchemeBlock = `:root {\\n color-scheme: ${colorScheme};\\n}`\n\n const combinedCSS = [\n criticalCSS,\n colorSchemeBlock,\n borderRadiusBlock,\n themeConfigurationCSS,\n colorModesCSS,\n customCSSBlock,\n ]\n .filter((block) => Boolean(block?.trim?.().length))\n .join('\\n\\n')\n\n // <meta name=\"theme-color\"> colours the mobile browser chrome to match the page\n // background. Emit per-scheme variants when the mode can change, otherwise a\n // single tag matching the fixed mode.\n const pick = (mode: typeof lightMode, key: 'background'): string | undefined => {\n const value = (mode as Record<string, unknown> | null | undefined)?.[key]\n return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined\n }\n const lightBg = pick(lightMode, 'background')\n const darkBg = pick(darkMode, 'background')\n const emitBothSchemes = allowColorModeToggle !== false || colorMode === 'auto'\n\n return (\n <>\n <InitTheme />\n {emitBothSchemes ? (\n <>\n {lightBg ? (\n <meta name=\"theme-color\" media=\"(prefers-color-scheme: light)\" content={lightBg} />\n ) : null}\n {darkBg ? (\n <meta name=\"theme-color\" media=\"(prefers-color-scheme: dark)\" content={darkBg} />\n ) : null}\n </>\n ) : (\n (() => {\n const fixed = colorMode === 'dark' ? darkBg : lightBg\n return fixed ? <meta name=\"theme-color\" content={fixed} /> : null\n })()\n )}\n <style\n id=\"theme-critical-css\"\n dangerouslySetInnerHTML={{\n __html: combinedCSS,\n }}\n />\n </>\n )\n}\n"],"names":["getBorderRadiusConfig","resolveThemeConfiguration","createFallbackCriticalCSS","getThemeCriticalCSS","generateThemeColorsCss","generateThemeCSS","InitTheme","ServerThemeInjector","themeConfiguration","resolvedConfiguration","theme","borderRadius","customCSS","lightMode","darkMode","fontScale","spacing","animationLevel","allowColorModeToggle","colorMode","typography","visualEffects","componentStyles","normalizedThemeConfiguration","undefined","criticalCSS","borderRadiusConfig","cssRecord","css","borderRadiusCSS","Object","entries","map","property","value","join","borderRadiusBlock","themeConfigurationCSS","colorModesCSS","themeName","customCSSBlock","trim","colorScheme","colorSchemeBlock","combinedCSS","filter","block","Boolean","length","pick","mode","key","lightBg","darkBg","emitBothSchemes","meta","name","media","content","fixed","style","id","dangerouslySetInnerHTML","__html"],"mappings":"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"}
|
|
@@ -1 +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,KAA6B,MAAM,OAAO,CAAA;AAMjD,UAAU,yBAAyB;IACjC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpC,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,EAAE,CAAC,yBAAyB,CA4CpE,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useLivePreview } from '@payloadcms/live-preview-react';
|
|
3
|
-
import { useEffect, useMemo } from 'react';
|
|
3
|
+
import React, { useEffect, useMemo } from 'react';
|
|
4
4
|
import { buildLivePreviewThemeRuntime, extractThemeConfigurationFromLiveData } from '../utils/livePreviewThemeRuntime.js';
|
|
5
5
|
export const ThemeLivePreviewSync = ({ initialData, serverURL, apiRoute, depth, themeConfigurationKey = 'themeConfiguration', styleElementId = 'theme-live-preview-css' })=>{
|
|
6
6
|
const { data } = useLivePreview({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/ThemeLivePreviewSync.tsx"],"sourcesContent":["'use client'\n\nimport { useLivePreview } from '@payloadcms/live-preview-react'\nimport { useEffect, useMemo } from 'react'\nimport {\n buildLivePreviewThemeRuntime,\n extractThemeConfigurationFromLiveData,\n} from '../utils/livePreviewThemeRuntime.js'\n\ninterface ThemeLivePreviewSyncProps {\n initialData: Record<string, unknown>\n serverURL: string\n apiRoute?: string\n depth?: number\n themeConfigurationKey?: string\n styleElementId?: string\n}\n\nexport const ThemeLivePreviewSync: React.FC<ThemeLivePreviewSyncProps> = ({\n initialData,\n serverURL,\n apiRoute,\n depth,\n themeConfigurationKey = 'themeConfiguration',\n styleElementId = 'theme-live-preview-css',\n}) => {\n const { data } = useLivePreview<Record<string, unknown>>({\n apiRoute,\n depth,\n initialData,\n serverURL,\n })\n\n const themeConfiguration = useMemo(\n () => extractThemeConfigurationFromLiveData(data, themeConfigurationKey),\n [data, themeConfigurationKey],\n )\n\n useEffect(() => {\n if (typeof document === 'undefined' || !themeConfiguration) {\n return\n }\n\n const runtime = buildLivePreviewThemeRuntime(themeConfiguration)\n\n let styleElement = document.getElementById(styleElementId) as HTMLStyleElement | null\n if (!styleElement) {\n styleElement = document.createElement('style')\n styleElement.id = styleElementId\n document.head.appendChild(styleElement)\n }\n\n styleElement.innerHTML = runtime.css\n\n document.documentElement.dataset.theme = runtime.theme\n document.documentElement.dataset.colorMode = runtime.mode\n document.documentElement.dataset.themeMode = runtime.mode\n document.documentElement.classList.remove('light', 'dark', 'auto')\n document.documentElement.classList.add(runtime.mode)\n }, [themeConfiguration, styleElementId])\n\n return null\n}\n"],"names":["useLivePreview","useEffect","useMemo","buildLivePreviewThemeRuntime","extractThemeConfigurationFromLiveData","ThemeLivePreviewSync","initialData","serverURL","apiRoute","depth","themeConfigurationKey","styleElementId","data","themeConfiguration","document","runtime","styleElement","getElementById","createElement","id","head","appendChild","innerHTML","css","documentElement","dataset","theme","colorMode","mode","themeMode","classList","remove","add"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,iCAAgC;AAC/D,SAASC,SAAS,EAAEC,OAAO,QAAQ,QAAO;
|
|
1
|
+
{"version":3,"sources":["../../src/components/ThemeLivePreviewSync.tsx"],"sourcesContent":["'use client'\n\nimport { useLivePreview } from '@payloadcms/live-preview-react'\nimport React, { useEffect, useMemo } from 'react'\nimport {\n buildLivePreviewThemeRuntime,\n extractThemeConfigurationFromLiveData,\n} from '../utils/livePreviewThemeRuntime.js'\n\ninterface ThemeLivePreviewSyncProps {\n initialData: Record<string, unknown>\n serverURL: string\n apiRoute?: string\n depth?: number\n themeConfigurationKey?: string\n styleElementId?: string\n}\n\nexport const ThemeLivePreviewSync: React.FC<ThemeLivePreviewSyncProps> = ({\n initialData,\n serverURL,\n apiRoute,\n depth,\n themeConfigurationKey = 'themeConfiguration',\n styleElementId = 'theme-live-preview-css',\n}) => {\n const { data } = useLivePreview<Record<string, unknown>>({\n apiRoute,\n depth,\n initialData,\n serverURL,\n })\n\n const themeConfiguration = useMemo(\n () => extractThemeConfigurationFromLiveData(data, themeConfigurationKey),\n [data, themeConfigurationKey],\n )\n\n useEffect(() => {\n if (typeof document === 'undefined' || !themeConfiguration) {\n return\n }\n\n const runtime = buildLivePreviewThemeRuntime(themeConfiguration)\n\n let styleElement = document.getElementById(styleElementId) as HTMLStyleElement | null\n if (!styleElement) {\n styleElement = document.createElement('style')\n styleElement.id = styleElementId\n document.head.appendChild(styleElement)\n }\n\n styleElement.innerHTML = runtime.css\n\n document.documentElement.dataset.theme = runtime.theme\n document.documentElement.dataset.colorMode = runtime.mode\n document.documentElement.dataset.themeMode = runtime.mode\n document.documentElement.classList.remove('light', 'dark', 'auto')\n document.documentElement.classList.add(runtime.mode)\n }, [themeConfiguration, styleElementId])\n\n return null\n}\n"],"names":["useLivePreview","React","useEffect","useMemo","buildLivePreviewThemeRuntime","extractThemeConfigurationFromLiveData","ThemeLivePreviewSync","initialData","serverURL","apiRoute","depth","themeConfigurationKey","styleElementId","data","themeConfiguration","document","runtime","styleElement","getElementById","createElement","id","head","appendChild","innerHTML","css","documentElement","dataset","theme","colorMode","mode","themeMode","classList","remove","add"],"mappings":"AAAA;AAEA,SAASA,cAAc,QAAQ,iCAAgC;AAC/D,OAAOC,SAASC,SAAS,EAAEC,OAAO,QAAQ,QAAO;AACjD,SACEC,4BAA4B,EAC5BC,qCAAqC,QAChC,sCAAqC;AAW5C,OAAO,MAAMC,uBAA4D,CAAC,EACxEC,WAAW,EACXC,SAAS,EACTC,QAAQ,EACRC,KAAK,EACLC,wBAAwB,oBAAoB,EAC5CC,iBAAiB,wBAAwB,EAC1C;IACC,MAAM,EAAEC,IAAI,EAAE,GAAGb,eAAwC;QACvDS;QACAC;QACAH;QACAC;IACF;IAEA,MAAMM,qBAAqBX,QACzB,IAAME,sCAAsCQ,MAAMF,wBAClD;QAACE;QAAMF;KAAsB;IAG/BT,UAAU;QACR,IAAI,OAAOa,aAAa,eAAe,CAACD,oBAAoB;YAC1D;QACF;QAEA,MAAME,UAAUZ,6BAA6BU;QAE7C,IAAIG,eAAeF,SAASG,cAAc,CAACN;QAC3C,IAAI,CAACK,cAAc;YACjBA,eAAeF,SAASI,aAAa,CAAC;YACtCF,aAAaG,EAAE,GAAGR;YAClBG,SAASM,IAAI,CAACC,WAAW,CAACL;QAC5B;QAEAA,aAAaM,SAAS,GAAGP,QAAQQ,GAAG;QAEpCT,SAASU,eAAe,CAACC,OAAO,CAACC,KAAK,GAAGX,QAAQW,KAAK;QACtDZ,SAASU,eAAe,CAACC,OAAO,CAACE,SAAS,GAAGZ,QAAQa,IAAI;QACzDd,SAASU,eAAe,CAACC,OAAO,CAACI,SAAS,GAAGd,QAAQa,IAAI;QACzDd,SAASU,eAAe,CAACM,SAAS,CAACC,MAAM,CAAC,SAAS,QAAQ;QAC3DjB,SAASU,eAAe,CAACM,SAAS,CAACE,GAAG,CAACjB,QAAQa,IAAI;IACrD,GAAG;QAACf;QAAoBF;KAAe;IAEvC,OAAO;AACT,EAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ThemePreset } from '../presets.js';
|
|
3
|
+
export type SwitchablePreset = Pick<ThemePreset, 'name' | 'label' | 'lightMode' | 'darkMode'>;
|
|
4
|
+
export interface ThemeSwitcherProps {
|
|
5
|
+
/**
|
|
6
|
+
* Presets the visitor can switch between. Defaults to all bundled presets.
|
|
7
|
+
* Pass a curated subset for a focused switcher.
|
|
8
|
+
*/
|
|
9
|
+
presets?: SwitchablePreset[];
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: React.CSSProperties;
|
|
12
|
+
'aria-label'?: string;
|
|
13
|
+
/** Persist the choice to localStorage and restore it on load. Default true. */
|
|
14
|
+
persist?: boolean;
|
|
15
|
+
/** Called after a preset is applied. */
|
|
16
|
+
onChange?: (presetName: string) => void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Front-end theme-preset switcher. Lets visitors swap between bundled (or a
|
|
20
|
+
* curated subset of) theme presets at runtime by injecting the preset's colour
|
|
21
|
+
* CSS and updating `data-theme`. The choice is persisted to localStorage and
|
|
22
|
+
* restored on the next visit.
|
|
23
|
+
*
|
|
24
|
+
* Note: colour-mode (light/dark) is handled by {@link ColorModeToggle} and the
|
|
25
|
+
* SSR injector with no flash; preset switching re-applies on mount, so a brief
|
|
26
|
+
* flash to the SSR default can occur before the stored preset is restored.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import { ThemeSwitcher } from '@kilivi/payloadcms-theme-management/components/ThemeSwitcher'
|
|
31
|
+
* <ThemeSwitcher presets={[oceanPreset, forestPreset]} />
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare const ThemeSwitcher: React.FC<ThemeSwitcherProps>;
|
|
35
|
+
export default ThemeSwitcher;
|
|
36
|
+
//# sourceMappingURL=ThemeSwitcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeSwitcher.d.ts","sourceRoot":"","sources":["../../src/components/ThemeSwitcher.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA2C,MAAM,OAAO,CAAA;AAC/D,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAMjE,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC,CAAA;AAE7F,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAA;CACxC;AAuBD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CA6DtD,CAAA;AAED,eAAe,aAAa,CAAA"}
|