@kilivi/payloadcms-theme-management 0.5.7 → 0.5.9
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/dist/components/InitTheme.d.ts +3 -0
- package/dist/components/InitTheme.d.ts.map +1 -1
- package/dist/components/InitTheme.js +9 -7
- package/dist/components/InitTheme.js.map +1 -1
- package/dist/fields/FontSelectField.js +1 -1
- package/dist/fields/FontSelectField.js.map +1 -1
- package/dist/fields/ThemePresetImportField.d.ts +4 -0
- package/dist/fields/ThemePresetImportField.d.ts.map +1 -0
- package/dist/fields/ThemePresetImportField.js +219 -0
- package/dist/fields/ThemePresetImportField.js.map +1 -0
- package/dist/fields/ThemePreviewField.d.ts.map +1 -1
- package/dist/fields/ThemePreviewField.js +32 -25
- package/dist/fields/ThemePreviewField.js.map +1 -1
- package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
- package/dist/fields/ThemeTokenSelectField.js +12 -5
- package/dist/fields/ThemeTokenSelectField.js.map +1 -1
- package/dist/fields/themeConfigurationField.d.ts.map +1 -1
- package/dist/fields/themeConfigurationField.js +50 -14
- package/dist/fields/themeConfigurationField.js.map +1 -1
- package/dist/utils/customThemePresets.d.ts +5 -0
- package/dist/utils/customThemePresets.d.ts.map +1 -0
- package/dist/utils/customThemePresets.js +82 -0
- package/dist/utils/customThemePresets.js.map +1 -0
- package/dist/utils/extendedThemeHelpers.d.ts +1 -1
- package/dist/utils/extendedThemeHelpers.d.ts.map +1 -1
- package/dist/utils/extendedThemeHelpers.js +16 -16
- package/dist/utils/extendedThemeHelpers.js.map +1 -1
- package/package.json +3 -1
|
@@ -7,6 +7,9 @@ import React from 'react';
|
|
|
7
7
|
* - User preference from localStorage
|
|
8
8
|
* - Admin-controlled color mode toggle
|
|
9
9
|
* - Auto mode with system preference fallback
|
|
10
|
+
*
|
|
11
|
+
* Note: Uses Next.js Script component with beforeInteractive strategy
|
|
12
|
+
* to ensure theme is applied before React hydration, preventing flash of unstyled content.
|
|
10
13
|
*/
|
|
11
14
|
export declare const InitTheme: React.FC;
|
|
12
15
|
//# sourceMappingURL=InitTheme.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InitTheme.d.ts","sourceRoot":"","sources":["../../src/components/InitTheme.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"InitTheme.d.ts","sourceRoot":"","sources":["../../src/components/InitTheme.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAyD7B,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import Script from 'next/script';
|
|
3
2
|
import React from 'react';
|
|
3
|
+
import Script from 'next/script';
|
|
4
4
|
import { modeLocalStorageKey } from '../providers/Theme/shared.js';
|
|
5
5
|
/**
|
|
6
6
|
* Client-side theme initialization script
|
|
@@ -10,10 +10,11 @@ import { modeLocalStorageKey } from '../providers/Theme/shared.js';
|
|
|
10
10
|
* - User preference from localStorage
|
|
11
11
|
* - Admin-controlled color mode toggle
|
|
12
12
|
* - Auto mode with system preference fallback
|
|
13
|
+
*
|
|
14
|
+
* Note: Uses Next.js Script component with beforeInteractive strategy
|
|
15
|
+
* to ensure theme is applied before React hydration, preventing flash of unstyled content.
|
|
13
16
|
*/ export const InitTheme = ()=>{
|
|
14
|
-
|
|
15
|
-
dangerouslySetInnerHTML: {
|
|
16
|
-
__html: `
|
|
17
|
+
const scriptContent = `
|
|
17
18
|
(function () {
|
|
18
19
|
function getImplicitModePreference() {
|
|
19
20
|
var mediaQuery = '(prefers-color-scheme: dark)'
|
|
@@ -62,10 +63,11 @@ import { modeLocalStorageKey } from '../providers/Theme/shared.js';
|
|
|
62
63
|
})
|
|
63
64
|
}
|
|
64
65
|
})();
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
`;
|
|
67
|
+
return /*#__PURE__*/ _jsx(Script, {
|
|
67
68
|
id: "theme-script",
|
|
68
|
-
strategy: "beforeInteractive"
|
|
69
|
+
strategy: "beforeInteractive",
|
|
70
|
+
children: scriptContent
|
|
69
71
|
});
|
|
70
72
|
};
|
|
71
73
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/InitTheme.tsx"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"sources":["../../src/components/InitTheme.tsx"],"sourcesContent":["import React from 'react'\nimport Script from 'next/script'\nimport { modeLocalStorageKey } from '../providers/Theme/shared.js'\n\n/**\n * Client-side theme initialization script\n * Prevents FOUC by applying theme mode before React hydration\n * Handles:\n * - System preference detection\n * - User preference from localStorage\n * - Admin-controlled color mode toggle\n * - Auto mode with system preference fallback\n *\n * Note: Uses Next.js Script component with beforeInteractive strategy\n * to ensure theme is applied before React hydration, preventing flash of unstyled content.\n */\nexport const InitTheme: React.FC = () => {\n const scriptContent = `\n (function () {\n function getImplicitModePreference() {\n var mediaQuery = '(prefers-color-scheme: dark)'\n var mql = window.matchMedia(mediaQuery)\n return mql.matches ? 'dark' : 'light'\n }\n\n function modeIsValid(mode) {\n return mode === 'light' || mode === 'dark'\n }\n\n // Get current mode from server-set attribute\n var currentMode = document.documentElement.getAttribute('data-theme-mode')\n var allowToggle = document.documentElement.getAttribute('data-allow-color-mode-toggle') === 'true'\n \n // Only override mode if admin allows toggle and user has preference\n if (allowToggle) {\n var modePreference = window.localStorage.getItem('${modeLocalStorageKey}')\n \n if (modeIsValid(modePreference)) {\n currentMode = modePreference\n } else if (currentMode === 'auto') {\n currentMode = getImplicitModePreference()\n }\n } else if (currentMode === 'auto') {\n // Admin set auto mode - use system preference\n currentMode = getImplicitModePreference()\n }\n\n // Apply the determined mode\n if (currentMode && modeIsValid(currentMode)) {\n document.documentElement.setAttribute('data-theme-mode', currentMode)\n }\n\n // Listen for system theme changes only when needed\n if (allowToggle || currentMode === 'auto') {\n var mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')\n mediaQuery.addEventListener('change', function(e) {\n var shouldUpdate = allowToggle \n ? !window.localStorage.getItem('${modeLocalStorageKey}')\n : document.documentElement.getAttribute('data-theme-mode') === 'auto'\n \n if (shouldUpdate) {\n document.documentElement.setAttribute('data-theme-mode', e.matches ? 'dark' : 'light')\n }\n })\n }\n })();\n `\n\n return (\n <Script id=\"theme-script\" strategy=\"beforeInteractive\">\n {scriptContent}\n </Script>\n )\n}\n"],"names":["React","Script","modeLocalStorageKey","InitTheme","scriptContent","id","strategy"],"mappings":";AAAA,OAAOA,WAAW,QAAO;AACzB,OAAOC,YAAY,cAAa;AAChC,SAASC,mBAAmB,QAAQ,+BAA8B;AAElE;;;;;;;;;;;CAWC,GACD,OAAO,MAAMC,YAAsB;IACjC,MAAMC,gBAAgB,CAAC;;;;;;;;;;;;;;;;;;wDAkB+B,EAAEF,oBAAoB;;;;;;;;;;;;;;;;;;;;;;0CAsBpC,EAAEA,oBAAoB;;;;;;;;;EAS9D,CAAC;IAED,qBACE,KAACD;QAAOI,IAAG;QAAeC,UAAS;kBAChCF;;AAGP,EAAC"}
|
|
@@ -124,7 +124,7 @@ const FontSelectField = ({ field, path })=>{
|
|
|
124
124
|
type: "button",
|
|
125
125
|
onClick: ()=>handleSelect(option.value),
|
|
126
126
|
style: {
|
|
127
|
-
width: '
|
|
127
|
+
width: '250px',
|
|
128
128
|
padding: '12px 14px',
|
|
129
129
|
border: 'none',
|
|
130
130
|
backgroundColor: isSelected ? 'var(--theme-elevation-100)' : 'transparent',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fields/FontSelectField.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport type { SelectFieldClientComponent } from 'payload'\nimport { useEffect, useState } from 'react'\n\ninterface FontOption {\n label: string\n value: string\n fontFamily?: string\n category?: 'sans-serif' | 'serif' | 'monospace' | 'display'\n}\n\nconst FontSelectField: SelectFieldClientComponent = ({ field, path }) => {\n const { value, setValue } = useField<string>({ path })\n const [isOpen, setIsOpen] = useState(false)\n\n const options = (field.options as FontOption[]) || []\n const selectedOption = options.find((opt) => opt.value === value)\n\n const handleSelect = (optionValue: string) => {\n setValue(optionValue)\n setIsOpen(false)\n }\n\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n const target = e.target as HTMLElement\n if (!target.closest('.font-select-container')) {\n setIsOpen(false)\n }\n }\n\n if (isOpen) {\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }\n }, [isOpen])\n\n const label =\n typeof field.label === 'string' ? field.label : field.label?.en || field.label?.cs || 'Font'\n\n return (\n <div className=\"font-select-container\" style={{ position: 'relative', marginBottom: '12px' }}>\n <label\n style={{\n display: 'block',\n marginBottom: '6px',\n fontSize: '13px',\n fontWeight: 600,\n color: 'var(--theme-elevation-800)',\n }}\n >\n {label}\n </label>\n\n <button\n type=\"button\"\n onClick={() => setIsOpen(!isOpen)}\n style={{\n width: '100%',\n padding: '10px 14px',\n borderRadius: '6px',\n border: '1px solid var(--theme-elevation-200)',\n backgroundColor: 'var(--theme-input-bg)',\n color: 'var(--theme-elevation-800)',\n textAlign: 'left',\n cursor: 'pointer',\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontSize: '14px',\n fontFamily: selectedOption?.fontFamily || 'inherit',\n transition: 'border-color 0.2s ease',\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.borderColor = 'var(--theme-elevation-400)'\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.borderColor = 'var(--theme-elevation-200)'\n }}\n >\n <span style={{ flex: 1 }}>{selectedOption?.label || 'Select font...'}</span>\n <svg\n width=\"12\"\n height=\"12\"\n viewBox=\"0 0 12 12\"\n fill=\"none\"\n style={{\n transition: 'transform 0.2s ease',\n transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',\n }}\n >\n <path\n d=\"M3 4.5L6 7.5L9 4.5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n {isOpen && (\n <div\n style={{\n position: 'absolute',\n top: '100%',\n left: 0,\n right: 0,\n marginTop: '4px',\n maxHeight: '280px',\n overflowY: 'auto',\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '8px',\n boxShadow: '0 8px 24px rgba(15, 23, 42, 0.15)',\n zIndex: 1000,\n }}\n >\n {options.map((option) => {\n const isSelected = option.value === value\n\n let fontFamily = option.fontFamily\n if (!fontFamily) {\n if (option.category === 'sans-serif') fontFamily = 'system-ui, sans-serif'\n else if (option.category === 'serif') fontFamily = 'Georgia, serif'\n else if (option.category === 'monospace') fontFamily = 'monospace'\n else fontFamily = 'inherit'\n }\n\n return (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => handleSelect(option.value)}\n style={{\n width: '100%',\n padding: '12px 14px',\n border: 'none',\n backgroundColor: isSelected ? 'var(--theme-elevation-100)' : 'transparent',\n color: 'var(--theme-elevation-800)',\n textAlign: 'left',\n cursor: 'pointer',\n fontSize: '14px',\n fontFamily,\n display: 'flex',\n flexDirection: 'column',\n gap: '4px',\n transition: 'background-color 0.15s ease',\n }}\n onMouseEnter={(e) => {\n if (!isSelected) {\n e.currentTarget.style.backgroundColor = 'var(--theme-elevation-50)'\n }\n }}\n onMouseLeave={(e) => {\n if (!isSelected) {\n e.currentTarget.style.backgroundColor = 'transparent'\n }\n }}\n >\n <div\n style={{\n fontWeight: isSelected ? 600 : 500,\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n }}\n >\n {isSelected && (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\">\n <path\n d=\"M11.6667 3.5L5.25 9.91667L2.33334 7\"\n stroke=\"var(--theme-elevation-500)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n {option.label}\n </div>\n <div\n style={{\n fontSize: '16px',\n color: 'var(--theme-elevation-600)',\n fontStyle:\n option.value === 'preset' || option.value === 'custom' ? 'italic' : 'normal',\n }}\n >\n {(() => {\n if (option.value === 'preset') return 'Use theme preset font'\n if (option.value === 'custom') return 'Specify custom font below'\n return 'The quick brown fox jumps over the lazy dog'\n })()}\n </div>\n </button>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n\nexport default FontSelectField\n"],"names":["useField","useEffect","useState","FontSelectField","field","path","value","setValue","isOpen","setIsOpen","options","selectedOption","find","opt","handleSelect","optionValue","handleClickOutside","e","target","closest","document","addEventListener","removeEventListener","label","en","cs","div","className","style","position","marginBottom","display","fontSize","fontWeight","color","button","type","onClick","width","padding","borderRadius","border","backgroundColor","textAlign","cursor","justifyContent","alignItems","fontFamily","transition","onMouseEnter","currentTarget","borderColor","onMouseLeave","span","flex","svg","height","viewBox","fill","transform","d","stroke","strokeWidth","strokeLinecap","strokeLinejoin","top","left","right","marginTop","maxHeight","overflowY","boxShadow","zIndex","map","option","isSelected","category","flexDirection","gap","fontStyle"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/fields/FontSelectField.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport type { SelectFieldClientComponent } from 'payload'\nimport { useEffect, useState } from 'react'\n\ninterface FontOption {\n label: string\n value: string\n fontFamily?: string\n category?: 'sans-serif' | 'serif' | 'monospace' | 'display'\n}\n\nconst FontSelectField: SelectFieldClientComponent = ({ field, path }) => {\n const { value, setValue } = useField<string>({ path })\n const [isOpen, setIsOpen] = useState(false)\n\n const options = (field.options as FontOption[]) || []\n const selectedOption = options.find((opt) => opt.value === value)\n\n const handleSelect = (optionValue: string) => {\n setValue(optionValue)\n setIsOpen(false)\n }\n\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n const target = e.target as HTMLElement\n if (!target.closest('.font-select-container')) {\n setIsOpen(false)\n }\n }\n\n if (isOpen) {\n document.addEventListener('mousedown', handleClickOutside)\n return () => document.removeEventListener('mousedown', handleClickOutside)\n }\n }, [isOpen])\n\n const label =\n typeof field.label === 'string' ? field.label : field.label?.en || field.label?.cs || 'Font'\n\n return (\n <div className=\"font-select-container\" style={{ position: 'relative', marginBottom: '12px' }}>\n <label\n style={{\n display: 'block',\n marginBottom: '6px',\n fontSize: '13px',\n fontWeight: 600,\n color: 'var(--theme-elevation-800)',\n }}\n >\n {label}\n </label>\n\n <button\n type=\"button\"\n onClick={() => setIsOpen(!isOpen)}\n style={{\n width: '100%',\n padding: '10px 14px',\n borderRadius: '6px',\n border: '1px solid var(--theme-elevation-200)',\n backgroundColor: 'var(--theme-input-bg)',\n color: 'var(--theme-elevation-800)',\n textAlign: 'left',\n cursor: 'pointer',\n display: 'flex',\n justifyContent: 'space-between',\n alignItems: 'center',\n fontSize: '14px',\n fontFamily: selectedOption?.fontFamily || 'inherit',\n transition: 'border-color 0.2s ease',\n }}\n onMouseEnter={(e) => {\n e.currentTarget.style.borderColor = 'var(--theme-elevation-400)'\n }}\n onMouseLeave={(e) => {\n e.currentTarget.style.borderColor = 'var(--theme-elevation-200)'\n }}\n >\n <span style={{ flex: 1 }}>{selectedOption?.label || 'Select font...'}</span>\n <svg\n width=\"12\"\n height=\"12\"\n viewBox=\"0 0 12 12\"\n fill=\"none\"\n style={{\n transition: 'transform 0.2s ease',\n transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)',\n }}\n >\n <path\n d=\"M3 4.5L6 7.5L9 4.5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n </button>\n\n {isOpen && (\n <div\n style={{\n position: 'absolute',\n top: '100%',\n left: 0,\n right: 0,\n marginTop: '4px',\n maxHeight: '280px',\n overflowY: 'auto',\n backgroundColor: 'var(--theme-elevation-0)',\n border: '1px solid var(--theme-elevation-200)',\n borderRadius: '8px',\n boxShadow: '0 8px 24px rgba(15, 23, 42, 0.15)',\n zIndex: 1000,\n }}\n >\n {options.map((option) => {\n const isSelected = option.value === value\n\n let fontFamily = option.fontFamily\n if (!fontFamily) {\n if (option.category === 'sans-serif') fontFamily = 'system-ui, sans-serif'\n else if (option.category === 'serif') fontFamily = 'Georgia, serif'\n else if (option.category === 'monospace') fontFamily = 'monospace'\n else fontFamily = 'inherit'\n }\n\n return (\n <button\n key={option.value}\n type=\"button\"\n onClick={() => handleSelect(option.value)}\n style={{\n width: '250px',\n padding: '12px 14px',\n border: 'none',\n backgroundColor: isSelected ? 'var(--theme-elevation-100)' : 'transparent',\n color: 'var(--theme-elevation-800)',\n textAlign: 'left',\n cursor: 'pointer',\n fontSize: '14px',\n fontFamily,\n display: 'flex',\n flexDirection: 'column',\n gap: '4px',\n transition: 'background-color 0.15s ease',\n }}\n onMouseEnter={(e) => {\n if (!isSelected) {\n e.currentTarget.style.backgroundColor = 'var(--theme-elevation-50)'\n }\n }}\n onMouseLeave={(e) => {\n if (!isSelected) {\n e.currentTarget.style.backgroundColor = 'transparent'\n }\n }}\n >\n <div\n style={{\n fontWeight: isSelected ? 600 : 500,\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n }}\n >\n {isSelected && (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 14 14\" fill=\"none\">\n <path\n d=\"M11.6667 3.5L5.25 9.91667L2.33334 7\"\n stroke=\"var(--theme-elevation-500)\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n )}\n {option.label}\n </div>\n <div\n style={{\n fontSize: '16px',\n color: 'var(--theme-elevation-600)',\n fontStyle:\n option.value === 'preset' || option.value === 'custom' ? 'italic' : 'normal',\n }}\n >\n {(() => {\n if (option.value === 'preset') return 'Use theme preset font'\n if (option.value === 'custom') return 'Specify custom font below'\n return 'The quick brown fox jumps over the lazy dog'\n })()}\n </div>\n </button>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n\nexport default FontSelectField\n"],"names":["useField","useEffect","useState","FontSelectField","field","path","value","setValue","isOpen","setIsOpen","options","selectedOption","find","opt","handleSelect","optionValue","handleClickOutside","e","target","closest","document","addEventListener","removeEventListener","label","en","cs","div","className","style","position","marginBottom","display","fontSize","fontWeight","color","button","type","onClick","width","padding","borderRadius","border","backgroundColor","textAlign","cursor","justifyContent","alignItems","fontFamily","transition","onMouseEnter","currentTarget","borderColor","onMouseLeave","span","flex","svg","height","viewBox","fill","transform","d","stroke","strokeWidth","strokeLinecap","strokeLinejoin","top","left","right","marginTop","maxHeight","overflowY","boxShadow","zIndex","map","option","isSelected","category","flexDirection","gap","fontStyle"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemePresetImportField.d.ts","sourceRoot":"","sources":["../../src/fields/ThemePresetImportField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AASnD,QAAA,MAAM,sBAAsB,GAAI,iBAAiB,oBAAoB,gCAyJpE,CAAA;AAED,eAAe,sBAAsB,CAAA"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Drawer, useField, useModal } from '@payloadcms/ui';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { parseThemePresetInput } from '../utils/customThemePresets.js';
|
|
7
|
+
const baseClass = 'theme-preset-import-field';
|
|
8
|
+
const drawerSlug = 'import-theme-presets';
|
|
9
|
+
const ThemePresetImportField = ({ field, path })=>{
|
|
10
|
+
const { value, setValue } = useField({
|
|
11
|
+
path
|
|
12
|
+
});
|
|
13
|
+
const [textValue, setTextValue] = useState('');
|
|
14
|
+
const { closeModal, openModal } = useModal();
|
|
15
|
+
const fileInputRef = useRef(null);
|
|
16
|
+
const presetSummary = useMemo(()=>{
|
|
17
|
+
if (!value || value.length === 0) {
|
|
18
|
+
return 'No custom presets imported';
|
|
19
|
+
}
|
|
20
|
+
const names = value.map((preset)=>preset.name).join(', ');
|
|
21
|
+
return `${value.length} preset${value.length === 1 ? '' : 's'}: ${names}`;
|
|
22
|
+
}, [
|
|
23
|
+
value
|
|
24
|
+
]);
|
|
25
|
+
const applyFromText = useCallback((raw)=>{
|
|
26
|
+
if (!raw.trim()) {
|
|
27
|
+
setValue(null);
|
|
28
|
+
closeModal(drawerSlug);
|
|
29
|
+
toast.success('Cleared custom presets');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const presets = parseThemePresetInput(raw);
|
|
33
|
+
if (presets.length === 0) {
|
|
34
|
+
toast.error('No valid presets found. Ensure each entry has a name and label.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
setValue(presets);
|
|
38
|
+
closeModal(drawerSlug);
|
|
39
|
+
toast.success(`Imported ${presets.length} preset${presets.length === 1 ? '' : 's'}`);
|
|
40
|
+
setTextValue('');
|
|
41
|
+
}, [
|
|
42
|
+
setValue,
|
|
43
|
+
closeModal
|
|
44
|
+
]);
|
|
45
|
+
const handleApplyClick = useCallback(()=>{
|
|
46
|
+
applyFromText(textValue);
|
|
47
|
+
}, [
|
|
48
|
+
applyFromText,
|
|
49
|
+
textValue
|
|
50
|
+
]);
|
|
51
|
+
const handleFileChange = useCallback(async (event)=>{
|
|
52
|
+
const file = event.target?.files?.[0];
|
|
53
|
+
if (!file) return;
|
|
54
|
+
try {
|
|
55
|
+
const content = await file.text();
|
|
56
|
+
setTextValue(content);
|
|
57
|
+
applyFromText(content);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
toast.error(error.message);
|
|
60
|
+
} finally{
|
|
61
|
+
if (event.target) {
|
|
62
|
+
event.target.value = '';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, [
|
|
66
|
+
applyFromText
|
|
67
|
+
]);
|
|
68
|
+
const handleImportClick = useCallback(()=>{
|
|
69
|
+
fileInputRef.current?.click();
|
|
70
|
+
}, []);
|
|
71
|
+
const handleClear = useCallback(()=>{
|
|
72
|
+
setValue(null);
|
|
73
|
+
toast.success('Cleared custom presets');
|
|
74
|
+
}, [
|
|
75
|
+
setValue
|
|
76
|
+
]);
|
|
77
|
+
const label = typeof field.label === 'string' ? field.label : field.label?.en ?? 'Custom Presets';
|
|
78
|
+
const description = typeof field.admin?.description === 'string' ? field.admin.description : field.admin?.description?.en;
|
|
79
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
80
|
+
children: [
|
|
81
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
82
|
+
className: baseClass,
|
|
83
|
+
style: {
|
|
84
|
+
display: 'grid',
|
|
85
|
+
gap: '0.75rem'
|
|
86
|
+
},
|
|
87
|
+
children: [
|
|
88
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
89
|
+
style: {
|
|
90
|
+
display: 'flex',
|
|
91
|
+
flexDirection: 'column',
|
|
92
|
+
gap: '0.25rem'
|
|
93
|
+
},
|
|
94
|
+
children: [
|
|
95
|
+
/*#__PURE__*/ _jsx("label", {
|
|
96
|
+
style: {
|
|
97
|
+
fontWeight: 600
|
|
98
|
+
},
|
|
99
|
+
children: label
|
|
100
|
+
}),
|
|
101
|
+
description && /*#__PURE__*/ _jsx("p", {
|
|
102
|
+
style: {
|
|
103
|
+
margin: 0,
|
|
104
|
+
color: 'var(--theme-elevation-500)',
|
|
105
|
+
fontSize: '0.9rem'
|
|
106
|
+
},
|
|
107
|
+
children: description
|
|
108
|
+
})
|
|
109
|
+
]
|
|
110
|
+
}),
|
|
111
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
112
|
+
style: {
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: '0.5rem',
|
|
116
|
+
flexWrap: 'wrap'
|
|
117
|
+
},
|
|
118
|
+
children: [
|
|
119
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
120
|
+
buttonStyle: "secondary",
|
|
121
|
+
onClick: ()=>openModal(drawerSlug),
|
|
122
|
+
size: "small",
|
|
123
|
+
children: "Import Presets"
|
|
124
|
+
}),
|
|
125
|
+
value && value.length > 0 && /*#__PURE__*/ _jsx(Button, {
|
|
126
|
+
buttonStyle: "secondary",
|
|
127
|
+
onClick: handleClear,
|
|
128
|
+
size: "small",
|
|
129
|
+
children: "Clear"
|
|
130
|
+
}),
|
|
131
|
+
/*#__PURE__*/ _jsx("span", {
|
|
132
|
+
style: {
|
|
133
|
+
fontSize: '0.875rem',
|
|
134
|
+
color: 'var(--theme-elevation-600)'
|
|
135
|
+
},
|
|
136
|
+
children: presetSummary
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
]
|
|
141
|
+
}),
|
|
142
|
+
/*#__PURE__*/ _jsx(Drawer, {
|
|
143
|
+
slug: drawerSlug,
|
|
144
|
+
title: "Import Theme Presets",
|
|
145
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
146
|
+
style: {
|
|
147
|
+
padding: '1.5rem',
|
|
148
|
+
display: 'grid',
|
|
149
|
+
gap: '1rem'
|
|
150
|
+
},
|
|
151
|
+
children: [
|
|
152
|
+
/*#__PURE__*/ _jsx("p", {
|
|
153
|
+
style: {
|
|
154
|
+
margin: 0,
|
|
155
|
+
color: 'var(--theme-elevation-600)'
|
|
156
|
+
},
|
|
157
|
+
children: "Paste JSON or upload a .json file containing theme preset definitions."
|
|
158
|
+
}),
|
|
159
|
+
/*#__PURE__*/ _jsx("textarea", {
|
|
160
|
+
value: textValue,
|
|
161
|
+
onChange: (event)=>setTextValue(event.target.value),
|
|
162
|
+
rows: 15,
|
|
163
|
+
spellCheck: false,
|
|
164
|
+
style: {
|
|
165
|
+
width: '100%',
|
|
166
|
+
fontFamily: 'monospace',
|
|
167
|
+
borderRadius: '8px',
|
|
168
|
+
border: '1px solid var(--theme-elevation-200)',
|
|
169
|
+
padding: '12px',
|
|
170
|
+
fontSize: '0.875rem'
|
|
171
|
+
},
|
|
172
|
+
placeholder: `[
|
|
173
|
+
{
|
|
174
|
+
"name": "brand-dark",
|
|
175
|
+
"label": "Brand Dark",
|
|
176
|
+
"borderRadius": "medium",
|
|
177
|
+
"lightMode": { "background": "#ffffff", "foreground": "#111" },
|
|
178
|
+
"darkMode": { "background": "#0a0a0a", "foreground": "#fff" }
|
|
179
|
+
}
|
|
180
|
+
]`
|
|
181
|
+
}),
|
|
182
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
183
|
+
style: {
|
|
184
|
+
display: 'flex',
|
|
185
|
+
gap: '0.5rem',
|
|
186
|
+
justifyContent: 'flex-end'
|
|
187
|
+
},
|
|
188
|
+
children: [
|
|
189
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
190
|
+
buttonStyle: "secondary",
|
|
191
|
+
onClick: handleImportClick,
|
|
192
|
+
children: "Upload JSON File"
|
|
193
|
+
}),
|
|
194
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
195
|
+
buttonStyle: "primary",
|
|
196
|
+
onClick: handleApplyClick,
|
|
197
|
+
disabled: !textValue.trim(),
|
|
198
|
+
children: "Apply"
|
|
199
|
+
})
|
|
200
|
+
]
|
|
201
|
+
}),
|
|
202
|
+
/*#__PURE__*/ _jsx("input", {
|
|
203
|
+
type: "file",
|
|
204
|
+
accept: "application/json",
|
|
205
|
+
ref: fileInputRef,
|
|
206
|
+
onChange: handleFileChange,
|
|
207
|
+
style: {
|
|
208
|
+
display: 'none'
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
]
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
]
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
export default ThemePresetImportField;
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=ThemePresetImportField.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/fields/ThemePresetImportField.tsx"],"sourcesContent":["'use client'\n\nimport { Button, Drawer, useField, useModal } from '@payloadcms/ui'\nimport type { JSONFieldClientProps } from 'payload'\nimport { toast } from 'sonner'\nimport { useCallback, useMemo, useRef, useState, type ChangeEvent } from 'react'\nimport type { ThemePreset } from '../presets.js'\nimport { parseThemePresetInput } from '../utils/customThemePresets.js'\n\nconst baseClass = 'theme-preset-import-field'\nconst drawerSlug = 'import-theme-presets'\n\nconst ThemePresetImportField = ({ field, path }: JSONFieldClientProps) => {\n const { value, setValue } = useField<ThemePreset[] | null>({ path })\n const [textValue, setTextValue] = useState('')\n const { closeModal, openModal } = useModal()\n const fileInputRef = useRef<HTMLInputElement | null>(null)\n\n const presetSummary = useMemo(() => {\n if (!value || value.length === 0) {\n return 'No custom presets imported'\n }\n const names = value.map((preset) => preset.name).join(', ')\n return `${value.length} preset${value.length === 1 ? '' : 's'}: ${names}`\n }, [value])\n\n const applyFromText = useCallback(\n (raw: string) => {\n if (!raw.trim()) {\n setValue(null)\n closeModal(drawerSlug)\n toast.success('Cleared custom presets')\n return\n }\n\n const presets = parseThemePresetInput(raw)\n if (presets.length === 0) {\n toast.error('No valid presets found. Ensure each entry has a name and label.')\n return\n }\n\n setValue(presets)\n closeModal(drawerSlug)\n toast.success(`Imported ${presets.length} preset${presets.length === 1 ? '' : 's'}`)\n setTextValue('')\n },\n [setValue, closeModal],\n )\n\n const handleApplyClick = useCallback(() => {\n applyFromText(textValue)\n }, [applyFromText, textValue])\n\n const handleFileChange = useCallback(\n async (event: ChangeEvent<HTMLInputElement>) => {\n const file = event.target?.files?.[0]\n if (!file) return\n try {\n const content = await file.text()\n setTextValue(content)\n applyFromText(content)\n } catch (error) {\n toast.error((error as Error).message)\n } finally {\n if (event.target) {\n event.target.value = ''\n }\n }\n },\n [applyFromText],\n )\n\n const handleImportClick = useCallback(() => {\n fileInputRef.current?.click()\n }, [])\n\n const handleClear = useCallback(() => {\n setValue(null)\n toast.success('Cleared custom presets')\n }, [setValue])\n\n const label =\n typeof field.label === 'string' ? field.label : (field.label?.en ?? 'Custom Presets')\n const description =\n typeof field.admin?.description === 'string'\n ? field.admin.description\n : (field.admin?.description as Record<string, string>)?.en\n\n return (\n <>\n <div className={baseClass} style={{ display: 'grid', gap: '0.75rem' }}>\n <div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>\n <label style={{ fontWeight: 600 }}>{label}</label>\n {description && (\n <p style={{ margin: 0, color: 'var(--theme-elevation-500)', fontSize: '0.9rem' }}>\n {description}\n </p>\n )}\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>\n <Button buttonStyle=\"secondary\" onClick={() => openModal(drawerSlug)} size=\"small\">\n Import Presets\n </Button>\n {value && value.length > 0 && (\n <Button buttonStyle=\"secondary\" onClick={handleClear} size=\"small\">\n Clear\n </Button>\n )}\n <span style={{ fontSize: '0.875rem', color: 'var(--theme-elevation-600)' }}>\n {presetSummary}\n </span>\n </div>\n </div>\n\n <Drawer slug={drawerSlug} title=\"Import Theme Presets\">\n <div style={{ padding: '1.5rem', display: 'grid', gap: '1rem' }}>\n <p style={{ margin: 0, color: 'var(--theme-elevation-600)' }}>\n Paste JSON or upload a .json file containing theme preset definitions.\n </p>\n\n <textarea\n value={textValue}\n onChange={(event) => setTextValue(event.target.value)}\n rows={15}\n spellCheck={false}\n style={{\n width: '100%',\n fontFamily: 'monospace',\n borderRadius: '8px',\n border: '1px solid var(--theme-elevation-200)',\n padding: '12px',\n fontSize: '0.875rem',\n }}\n placeholder={`[\n {\n \"name\": \"brand-dark\",\n \"label\": \"Brand Dark\",\n \"borderRadius\": \"medium\",\n \"lightMode\": { \"background\": \"#ffffff\", \"foreground\": \"#111\" },\n \"darkMode\": { \"background\": \"#0a0a0a\", \"foreground\": \"#fff\" }\n }\n]`}\n />\n\n <div style={{ display: 'flex', gap: '0.5rem', justifyContent: 'flex-end' }}>\n <Button buttonStyle=\"secondary\" onClick={handleImportClick}>\n Upload JSON File\n </Button>\n <Button buttonStyle=\"primary\" onClick={handleApplyClick} disabled={!textValue.trim()}>\n Apply\n </Button>\n </div>\n\n <input\n type=\"file\"\n accept=\"application/json\"\n ref={fileInputRef}\n onChange={handleFileChange}\n style={{ display: 'none' }}\n />\n </div>\n </Drawer>\n </>\n )\n}\n\nexport default ThemePresetImportField\n"],"names":["Button","Drawer","useField","useModal","toast","useCallback","useMemo","useRef","useState","parseThemePresetInput","baseClass","drawerSlug","ThemePresetImportField","field","path","value","setValue","textValue","setTextValue","closeModal","openModal","fileInputRef","presetSummary","length","names","map","preset","name","join","applyFromText","raw","trim","success","presets","error","handleApplyClick","handleFileChange","event","file","target","files","content","text","message","handleImportClick","current","click","handleClear","label","en","description","admin","div","className","style","display","gap","flexDirection","fontWeight","p","margin","color","fontSize","alignItems","flexWrap","buttonStyle","onClick","size","span","slug","title","padding","textarea","onChange","rows","spellCheck","width","fontFamily","borderRadius","border","placeholder","justifyContent","disabled","input","type","accept","ref"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemePreviewField.d.ts","sourceRoot":"","sources":["../../src/fields/ThemePreviewField.tsx"],"names":[],"mappings":"AAEA,OAAO,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"ThemePreviewField.d.ts","sourceRoot":"","sources":["../../src/fields/ThemePreviewField.tsx"],"names":[],"mappings":"AAEA,OAAO,yBAAyB,CAAA;AAEhC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAA;AA2PrD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,KAAK,EAAE,sBAAsB,+BAmZtE"}
|
|
@@ -7,23 +7,7 @@ import { resolveTypographyPreview } from '../components/typographyPreviewUtils.j
|
|
|
7
7
|
import { allThemePresets } from '../index.js';
|
|
8
8
|
import { borderRadiusPresets } from '../providers/Theme/themeConfig.js';
|
|
9
9
|
import { darkModeDefaults, lightModeDefaults } from './colorModeFields.js';
|
|
10
|
-
|
|
11
|
-
const themePresets = allThemePresets.reduce((acc, preset)=>{
|
|
12
|
-
acc[preset.name] = {
|
|
13
|
-
label: preset.label,
|
|
14
|
-
lightMode: {
|
|
15
|
-
...lightModeDefaults,
|
|
16
|
-
...preset.lightMode ?? {}
|
|
17
|
-
},
|
|
18
|
-
darkMode: {
|
|
19
|
-
...darkModeDefaults,
|
|
20
|
-
...preset.darkMode ?? {}
|
|
21
|
-
},
|
|
22
|
-
typography: preset.typography,
|
|
23
|
-
borderRadius: preset.borderRadius
|
|
24
|
-
};
|
|
25
|
-
return acc;
|
|
26
|
-
}, {});
|
|
10
|
+
// FALLBACK_THEME and runtime theme presets are computed inside the component
|
|
27
11
|
const colorKeys = Object.keys(lightModeDefaults);
|
|
28
12
|
const highlightSwatches = [
|
|
29
13
|
{
|
|
@@ -255,6 +239,29 @@ function ModePreview({ icon, title, colors, typography, radius }) {
|
|
|
255
239
|
}
|
|
256
240
|
export default function ThemePreviewField(props) {
|
|
257
241
|
const { field, path } = props;
|
|
242
|
+
// If the plugin provided theme presets via admin config, prefer those; otherwise fallback to allThemePresets
|
|
243
|
+
const baseThemePresets = field?.admin?.themePresets ?? allThemePresets;
|
|
244
|
+
const runtimeThemePresets = useMemo(()=>{
|
|
245
|
+
return baseThemePresets.reduce((acc, preset)=>{
|
|
246
|
+
acc[preset.name] = {
|
|
247
|
+
label: preset.label,
|
|
248
|
+
lightMode: {
|
|
249
|
+
...lightModeDefaults,
|
|
250
|
+
...preset.lightMode ?? {}
|
|
251
|
+
},
|
|
252
|
+
darkMode: {
|
|
253
|
+
...darkModeDefaults,
|
|
254
|
+
...preset.darkMode ?? {}
|
|
255
|
+
},
|
|
256
|
+
typography: preset.typography,
|
|
257
|
+
borderRadius: preset.borderRadius
|
|
258
|
+
};
|
|
259
|
+
return acc;
|
|
260
|
+
}, {});
|
|
261
|
+
}, [
|
|
262
|
+
baseThemePresets
|
|
263
|
+
]);
|
|
264
|
+
const fallbackTheme = baseThemePresets[0]?.name ?? 'cool';
|
|
258
265
|
const { value: selectedTheme, setValue } = useField({
|
|
259
266
|
path
|
|
260
267
|
});
|
|
@@ -262,7 +269,7 @@ export default function ThemePreviewField(props) {
|
|
|
262
269
|
const formFields = useFormFields(([formState])=>formState);
|
|
263
270
|
const hasAppliedInitialPresetRef = useRef(false);
|
|
264
271
|
const applyPreset = useCallback((presetName)=>{
|
|
265
|
-
const preset =
|
|
272
|
+
const preset = runtimeThemePresets[presetName];
|
|
266
273
|
if (!preset) {
|
|
267
274
|
return;
|
|
268
275
|
}
|
|
@@ -302,11 +309,11 @@ export default function ThemePreviewField(props) {
|
|
|
302
309
|
if (!selectedTheme) {
|
|
303
310
|
return;
|
|
304
311
|
}
|
|
305
|
-
if (
|
|
312
|
+
if (runtimeThemePresets[selectedTheme]) {
|
|
306
313
|
return;
|
|
307
314
|
}
|
|
308
315
|
hasAppliedInitialPresetRef.current = false;
|
|
309
|
-
setValue(
|
|
316
|
+
setValue(fallbackTheme);
|
|
310
317
|
}, [
|
|
311
318
|
selectedTheme,
|
|
312
319
|
setValue
|
|
@@ -321,7 +328,7 @@ export default function ThemePreviewField(props) {
|
|
|
321
328
|
if (!formFields) {
|
|
322
329
|
return;
|
|
323
330
|
}
|
|
324
|
-
const preset =
|
|
331
|
+
const preset = runtimeThemePresets[selectedTheme];
|
|
325
332
|
if (!preset) {
|
|
326
333
|
hasAppliedInitialPresetRef.current = true;
|
|
327
334
|
return;
|
|
@@ -345,7 +352,7 @@ export default function ThemePreviewField(props) {
|
|
|
345
352
|
applyPreset
|
|
346
353
|
]);
|
|
347
354
|
const lightModeColors = useMemo(()=>{
|
|
348
|
-
const presetLightMode = selectedTheme ?
|
|
355
|
+
const presetLightMode = selectedTheme ? runtimeThemePresets[selectedTheme]?.lightMode : undefined;
|
|
349
356
|
return colorKeys.reduce((acc, key)=>{
|
|
350
357
|
const fieldState = formFields?.[`themeConfiguration.lightMode.${key}`];
|
|
351
358
|
const rawValue = typeof fieldState?.value === 'string' && fieldState.value.trim().length > 0 ? fieldState.value : undefined;
|
|
@@ -357,7 +364,7 @@ export default function ThemePreviewField(props) {
|
|
|
357
364
|
selectedTheme
|
|
358
365
|
]);
|
|
359
366
|
const darkModeColors = useMemo(()=>{
|
|
360
|
-
const presetDarkMode = selectedTheme ?
|
|
367
|
+
const presetDarkMode = selectedTheme ? runtimeThemePresets[selectedTheme]?.darkMode : undefined;
|
|
361
368
|
return colorKeys.reduce((acc, key)=>{
|
|
362
369
|
const fieldState = formFields?.[`themeConfiguration.darkMode.${key}`];
|
|
363
370
|
const rawValue = typeof fieldState?.value === 'string' && fieldState.value.trim().length > 0 ? fieldState.value : undefined;
|
|
@@ -368,7 +375,7 @@ export default function ThemePreviewField(props) {
|
|
|
368
375
|
formFields,
|
|
369
376
|
selectedTheme
|
|
370
377
|
]);
|
|
371
|
-
const activePreset = selectedTheme ?
|
|
378
|
+
const activePreset = selectedTheme ? runtimeThemePresets[selectedTheme] : undefined;
|
|
372
379
|
const formTypographySelection = useMemo(()=>{
|
|
373
380
|
if (!formFields) {
|
|
374
381
|
return null;
|
|
@@ -438,7 +445,7 @@ export default function ThemePreviewField(props) {
|
|
|
438
445
|
children: [
|
|
439
446
|
/*#__PURE__*/ _jsx("div", {
|
|
440
447
|
className: "theme-preset-list",
|
|
441
|
-
children: Object.entries(
|
|
448
|
+
children: Object.entries(runtimeThemePresets).map(([key, preset])=>{
|
|
442
449
|
const isSelected = key === selectedTheme;
|
|
443
450
|
const swatches = highlightSwatches.map(({ key: colorKey })=>preset.lightMode[colorKey]).filter((color)=>Boolean(color));
|
|
444
451
|
return /*#__PURE__*/ _jsxs("button", {
|