@kilivi/payloadcms-theme-management 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -7
- package/dist/components/ServerThemeInjector.d.ts +13 -1
- package/dist/components/ServerThemeInjector.d.ts.map +1 -1
- package/dist/components/ServerThemeInjector.js +24 -1
- package/dist/components/ServerThemeInjector.js.map +1 -1
- package/dist/components/ThemeLivePreviewSync.d.ts +3 -0
- package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
- package/dist/components/ThemeLivePreviewSync.js +28 -4
- package/dist/components/ThemeLivePreviewSync.js.map +1 -1
- package/dist/components/typographyPreviewUtils.d.ts +5 -0
- package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
- package/dist/components/typographyPreviewUtils.js +16 -10
- package/dist/components/typographyPreviewUtils.js.map +1 -1
- package/dist/constants/themeFonts.d.ts +94 -168
- package/dist/constants/themeFonts.d.ts.map +1 -1
- package/dist/constants/themeFonts.js +260 -140
- package/dist/constants/themeFonts.js.map +1 -1
- package/dist/endpoints/themeFonts.d.ts +5 -0
- package/dist/endpoints/themeFonts.d.ts.map +1 -0
- package/dist/endpoints/themeFonts.js +138 -0
- package/dist/endpoints/themeFonts.js.map +1 -0
- package/dist/fields/AppearancePreviewField.d.ts.map +1 -1
- package/dist/fields/AppearancePreviewField.js +196 -133
- package/dist/fields/AppearancePreviewField.js.map +1 -1
- package/dist/fields/FontSelectField.css +155 -0
- package/dist/fields/FontSelectField.d.ts +4 -2
- package/dist/fields/FontSelectField.d.ts.map +1 -1
- package/dist/fields/FontSelectField.js +80 -118
- package/dist/fields/FontSelectField.js.map +1 -1
- package/dist/fields/StylePresetField.d.ts.map +1 -1
- package/dist/fields/StylePresetField.js +31 -30
- package/dist/fields/StylePresetField.js.map +1 -1
- package/dist/fields/ThemePreviewField.css +63 -1
- package/dist/fields/ThemePreviewField.d.ts.map +1 -1
- package/dist/fields/ThemePreviewField.js +2 -5
- package/dist/fields/ThemePreviewField.js.map +1 -1
- package/dist/fields/themeConfigurationField.d.ts.map +1 -1
- package/dist/fields/themeConfigurationField.js +320 -271
- package/dist/fields/themeConfigurationField.js.map +1 -1
- package/dist/hooks/useNativeLivePreview.d.ts +15 -0
- package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
- package/dist/hooks/useNativeLivePreview.js +18 -0
- package/dist/hooks/useNativeLivePreview.js.map +1 -0
- package/dist/hooks/useThemeFonts.d.ts +10 -0
- package/dist/hooks/useThemeFonts.d.ts.map +1 -0
- package/dist/hooks/useThemeFonts.js +36 -0
- package/dist/hooks/useThemeFonts.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/next-fonts.d.ts +3 -0
- package/dist/next-fonts.d.ts.map +1 -0
- package/dist/next-fonts.js +360 -0
- package/dist/next-fonts.js.map +1 -0
- package/dist/payload-types.d.ts +10 -6
- package/dist/payload-types.d.ts.map +1 -1
- package/dist/payload-types.js.map +1 -1
- package/dist/presets.d.ts +6 -0
- package/dist/presets.d.ts.map +1 -1
- package/dist/presets.js.map +1 -1
- package/dist/style-presets.d.ts +27 -18
- package/dist/style-presets.d.ts.map +1 -1
- package/dist/style-presets.js +399 -217
- package/dist/style-presets.js.map +1 -1
- package/dist/translations.d.ts +5 -0
- package/dist/translations.d.ts.map +1 -1
- package/dist/translations.js +10 -0
- package/dist/translations.js.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.d.ts +6 -1
- package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
- package/dist/utils/livePreviewThemeRuntime.js +11 -1
- package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
- package/dist/utils/resolveThemeConfiguration.d.ts +15 -0
- package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
- package/dist/utils/resolveThemeConfiguration.js +41 -8
- package/dist/utils/resolveThemeConfiguration.js.map +1 -1
- package/dist/utils/themeUtils.d.ts.map +1 -1
- package/dist/utils/themeUtils.js +60 -2
- package/dist/utils/themeUtils.js.map +1 -1
- package/package.json +5 -1
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import './FontSelectField.css';
|
|
3
4
|
import { useField } from '@payloadcms/ui';
|
|
5
|
+
import { mergeFieldStyles } from '@payloadcms/ui/shared';
|
|
4
6
|
import { Check, ChevronDown } from 'lucide-react';
|
|
5
|
-
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
7
|
+
import React, { useCallback, useEffect, useId, useRef, useState } from 'react';
|
|
8
|
+
import { FONT_CATEGORY_LABELS, getFontCatalogEntry } from '../constants/themeFonts.js';
|
|
9
|
+
import { useThemeFonts } from '../hooks/useThemeFonts.js';
|
|
6
10
|
import { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js';
|
|
7
11
|
import { resolveLocalizedText } from '../utils/localizedText.js';
|
|
8
|
-
const NON_GOOGLE_VALUES = [
|
|
9
|
-
'preset',
|
|
10
|
-
'custom',
|
|
11
|
-
'system-ui'
|
|
12
|
-
];
|
|
13
12
|
function previewFamily(option) {
|
|
14
13
|
if (option.fontFamily) return option.fontFamily;
|
|
15
14
|
if (option.category === 'sans-serif') return 'system-ui, sans-serif';
|
|
@@ -20,8 +19,9 @@ function previewFamily(option) {
|
|
|
20
19
|
/**
|
|
21
20
|
* Font select with live font previews. Implements the WAI-ARIA "select-only
|
|
22
21
|
* combobox" pattern: a button trigger plus a listbox navigated with the arrow
|
|
23
|
-
* keys, Home/End, Enter/Space and Escape.
|
|
24
|
-
*
|
|
22
|
+
* keys, Home/End, Enter/Space and Escape. Options are grouped by font category.
|
|
23
|
+
* Preview fonts come from the plugin's self-hosted font endpoint: the selected
|
|
24
|
+
* face always, every face while the list is open.
|
|
25
25
|
*/ const FontSelectField = ({ field, path })=>{
|
|
26
26
|
const { value, setValue } = useField({
|
|
27
27
|
path
|
|
@@ -36,26 +36,30 @@ function previewFamily(option) {
|
|
|
36
36
|
const containerRef = useRef(null);
|
|
37
37
|
const triggerRef = useRef(null);
|
|
38
38
|
const listboxRef = useRef(null);
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Payload strips custom option props on the way to the client, so the face and
|
|
40
|
+
// category come from the font catalog by value.
|
|
41
|
+
const options = (field.options || []).map((option)=>{
|
|
42
|
+
const entry = getFontCatalogEntry(option.value);
|
|
43
|
+
return entry ? {
|
|
44
|
+
...option,
|
|
45
|
+
fontFamily: entry.fontFamily,
|
|
46
|
+
category: entry.category
|
|
47
|
+
} : option;
|
|
48
|
+
});
|
|
49
|
+
const [alignEnd, setAlignEnd] = useState(false);
|
|
41
50
|
const selectedIndex = options.findIndex((opt)=>opt.value === value);
|
|
42
51
|
const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined;
|
|
43
52
|
const getOptionLabel = (opt)=>resolveLocalizedText(opt.label, lang, 'Font');
|
|
44
53
|
const label = resolveLocalizedText(field.label, lang, 'Font');
|
|
45
54
|
const optionId = (index)=>`${baseId}-option-${index}`;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
loadedFonts.current.add(fontValue);
|
|
50
|
-
const linkId = `font-${fontValue.replace(/\s+/g, '-')}`;
|
|
51
|
-
if (document.getElementById(linkId)) return;
|
|
52
|
-
const link = document.createElement('link');
|
|
53
|
-
link.id = linkId;
|
|
54
|
-
link.rel = 'stylesheet';
|
|
55
|
-
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontValue)}:wght@400;700&display=swap`;
|
|
56
|
-
document.head.appendChild(link);
|
|
57
|
-
}, []);
|
|
55
|
+
useThemeFonts(isOpen ? options.map((option)=>option.value) : [
|
|
56
|
+
value
|
|
57
|
+
]);
|
|
58
58
|
const open = useCallback((index = selectedIndex >= 0 ? selectedIndex : 0)=>{
|
|
59
|
+
// Open towards the side with room, so the list never spills past the form column.
|
|
60
|
+
const trigger = triggerRef.current?.getBoundingClientRect();
|
|
61
|
+
const column = containerRef.current?.closest('.render-fields, .group-field__wrap')?.getBoundingClientRect();
|
|
62
|
+
setAlignEnd(Boolean(trigger && column && trigger.left + 300 > column.right));
|
|
59
63
|
setActiveIndex(index);
|
|
60
64
|
setIsOpen(true);
|
|
61
65
|
}, [
|
|
@@ -71,10 +75,9 @@ function previewFamily(option) {
|
|
|
71
75
|
setValue(option.value);
|
|
72
76
|
close(true);
|
|
73
77
|
};
|
|
74
|
-
//
|
|
78
|
+
// Keep the active option scrolled into view.
|
|
75
79
|
useEffect(()=>{
|
|
76
80
|
if (!isOpen || activeIndex < 0) return;
|
|
77
|
-
ensureFontLoaded(options[activeIndex]?.value);
|
|
78
81
|
listboxRef.current?.querySelector(`#${CSS.escape(optionId(activeIndex))}`)?.scrollIntoView({
|
|
79
82
|
block: 'nearest'
|
|
80
83
|
});
|
|
@@ -156,30 +159,22 @@ function previewFamily(option) {
|
|
|
156
159
|
}
|
|
157
160
|
}
|
|
158
161
|
};
|
|
162
|
+
const isSpecial = (option)=>option.value === 'preset' || option.value === 'custom';
|
|
159
163
|
return /*#__PURE__*/ _jsxs("div", {
|
|
160
164
|
ref: containerRef,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
marginBottom: '12px',
|
|
165
|
-
maxWidth: '360px'
|
|
166
|
-
},
|
|
165
|
+
// Payload rows size each field through --field-width on the field root.
|
|
166
|
+
className: "field-type tm-font-select",
|
|
167
|
+
style: mergeFieldStyles(field),
|
|
167
168
|
children: [
|
|
168
169
|
/*#__PURE__*/ _jsx("div", {
|
|
169
170
|
id: labelId,
|
|
170
|
-
|
|
171
|
-
display: 'block',
|
|
172
|
-
marginBottom: '6px',
|
|
173
|
-
fontSize: '13px',
|
|
174
|
-
fontWeight: 600,
|
|
175
|
-
color: 'var(--theme-elevation-800)'
|
|
176
|
-
},
|
|
171
|
+
className: "field-label tm-font-select__label",
|
|
177
172
|
children: label
|
|
178
173
|
}),
|
|
179
174
|
/*#__PURE__*/ _jsxs("button", {
|
|
180
175
|
ref: triggerRef,
|
|
181
176
|
type: "button",
|
|
182
|
-
className: "font-
|
|
177
|
+
className: "tm-font-select__trigger",
|
|
183
178
|
role: "combobox",
|
|
184
179
|
"aria-haspopup": "listbox",
|
|
185
180
|
"aria-expanded": isOpen,
|
|
@@ -187,38 +182,19 @@ function previewFamily(option) {
|
|
|
187
182
|
"aria-labelledby": `${labelId} ${baseId}-value`,
|
|
188
183
|
onClick: ()=>isOpen ? close(false) : open(),
|
|
189
184
|
onKeyDown: handleTriggerKeyDown,
|
|
190
|
-
style: {
|
|
191
|
-
width: '100%',
|
|
192
|
-
padding: '10px 14px',
|
|
193
|
-
borderRadius: '6px',
|
|
194
|
-
border: '1px solid var(--theme-elevation-200)',
|
|
195
|
-
backgroundColor: 'var(--theme-input-bg)',
|
|
196
|
-
color: 'var(--theme-elevation-800)',
|
|
197
|
-
textAlign: 'left',
|
|
198
|
-
cursor: 'pointer',
|
|
199
|
-
display: 'flex',
|
|
200
|
-
justifyContent: 'space-between',
|
|
201
|
-
alignItems: 'center',
|
|
202
|
-
gap: '8px',
|
|
203
|
-
fontSize: '14px',
|
|
204
|
-
fontFamily: selectedOption?.fontFamily || 'inherit'
|
|
205
|
-
},
|
|
206
185
|
children: [
|
|
207
186
|
/*#__PURE__*/ _jsx("span", {
|
|
208
187
|
id: `${baseId}-value`,
|
|
188
|
+
className: "tm-font-select__value",
|
|
209
189
|
style: {
|
|
210
|
-
|
|
211
|
-
minWidth: 0
|
|
190
|
+
fontFamily: selectedOption && !isSpecial(selectedOption) ? previewFamily(selectedOption) : undefined
|
|
212
191
|
},
|
|
213
192
|
children: selectedOption ? getOptionLabel(selectedOption) : t.ui.selectFontPlaceholder
|
|
214
193
|
}),
|
|
215
194
|
/*#__PURE__*/ _jsx(ChevronDown, {
|
|
216
|
-
size:
|
|
195
|
+
size: 16,
|
|
217
196
|
"aria-hidden": true,
|
|
218
|
-
|
|
219
|
-
flexShrink: 0,
|
|
220
|
-
transform: isOpen ? 'rotate(180deg)' : undefined
|
|
221
|
-
}
|
|
197
|
+
className: `tm-font-select__chevron${isOpen ? ' is-open' : ''}`
|
|
222
198
|
})
|
|
223
199
|
]
|
|
224
200
|
}),
|
|
@@ -230,70 +206,56 @@ function previewFamily(option) {
|
|
|
230
206
|
"aria-labelledby": labelId,
|
|
231
207
|
"aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : undefined,
|
|
232
208
|
onKeyDown: handleListboxKeyDown,
|
|
233
|
-
className:
|
|
234
|
-
style: {
|
|
235
|
-
position: 'absolute',
|
|
236
|
-
top: '100%',
|
|
237
|
-
left: 0,
|
|
238
|
-
right: 0,
|
|
239
|
-
marginTop: '4px',
|
|
240
|
-
maxHeight: '280px',
|
|
241
|
-
overflowY: 'auto',
|
|
242
|
-
backgroundColor: 'var(--theme-elevation-0)',
|
|
243
|
-
border: '1px solid var(--theme-elevation-200)',
|
|
244
|
-
borderRadius: '8px',
|
|
245
|
-
boxShadow: '0 8px 24px rgba(15, 23, 42, 0.15)',
|
|
246
|
-
zIndex: 1000
|
|
247
|
-
},
|
|
209
|
+
className: `tm-font-select__listbox${alignEnd ? ' is-align-end' : ''}`,
|
|
248
210
|
children: options.map((option, index)=>{
|
|
249
211
|
const isSelected = option.value === value;
|
|
250
212
|
const isActive = index === activeIndex;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
onClick: ()=>handleSelect(index),
|
|
256
|
-
onPointerMove: ()=>setActiveIndex(index),
|
|
257
|
-
style: {
|
|
258
|
-
padding: '12px 14px',
|
|
259
|
-
backgroundColor: isActive ? 'var(--theme-elevation-100)' : isSelected ? 'var(--theme-elevation-50)' : 'transparent',
|
|
260
|
-
// The active option gets an outline so keyboard focus never relies on a subtle fill alone.
|
|
261
|
-
outline: isActive ? '2px solid var(--theme-elevation-800)' : undefined,
|
|
262
|
-
outlineOffset: '-2px',
|
|
263
|
-
color: 'var(--theme-elevation-800)',
|
|
264
|
-
cursor: 'pointer',
|
|
265
|
-
fontSize: '14px',
|
|
266
|
-
fontFamily: previewFamily(option),
|
|
267
|
-
display: 'flex',
|
|
268
|
-
flexDirection: 'column',
|
|
269
|
-
gap: '4px'
|
|
270
|
-
},
|
|
213
|
+
const startsGroup = option.category !== undefined && option.category !== options[index - 1]?.category;
|
|
214
|
+
const special = isSpecial(option);
|
|
215
|
+
const family = special ? undefined : previewFamily(option);
|
|
216
|
+
return /*#__PURE__*/ _jsxs(React.Fragment, {
|
|
271
217
|
children: [
|
|
272
|
-
/*#__PURE__*/
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
218
|
+
startsGroup && option.category ? /*#__PURE__*/ _jsx("div", {
|
|
219
|
+
role: "presentation",
|
|
220
|
+
className: "tm-font-select__group",
|
|
221
|
+
children: resolveLocalizedText(FONT_CATEGORY_LABELS[option.category], lang, '')
|
|
222
|
+
}) : null,
|
|
223
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
224
|
+
id: optionId(index),
|
|
225
|
+
role: "option",
|
|
226
|
+
"aria-selected": isSelected,
|
|
227
|
+
onClick: ()=>handleSelect(index),
|
|
228
|
+
onPointerMove: ()=>setActiveIndex(index),
|
|
229
|
+
className: `tm-font-select__option${isActive ? ' is-active' : ''}${isSelected ? ' is-selected' : ''}`,
|
|
279
230
|
children: [
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
231
|
+
/*#__PURE__*/ _jsx("span", {
|
|
232
|
+
className: "tm-font-select__check",
|
|
233
|
+
"aria-hidden": true,
|
|
234
|
+
children: isSelected ? /*#__PURE__*/ _jsx(Check, {
|
|
235
|
+
size: 14
|
|
236
|
+
}) : null
|
|
284
237
|
}),
|
|
285
|
-
|
|
238
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
239
|
+
className: "tm-font-select__text",
|
|
240
|
+
children: [
|
|
241
|
+
/*#__PURE__*/ _jsx("span", {
|
|
242
|
+
className: "tm-font-select__name",
|
|
243
|
+
style: {
|
|
244
|
+
fontFamily: family
|
|
245
|
+
},
|
|
246
|
+
children: getOptionLabel(option)
|
|
247
|
+
}),
|
|
248
|
+
/*#__PURE__*/ _jsx("span", {
|
|
249
|
+
"aria-hidden": true,
|
|
250
|
+
className: `tm-font-select__sample${special ? ' is-hint' : ''}`,
|
|
251
|
+
style: {
|
|
252
|
+
fontFamily: family
|
|
253
|
+
},
|
|
254
|
+
children: option.value === 'preset' ? t.ui.usePreset : option.value === 'custom' ? t.ui.specifyCustom : t.ui.sampleSentence
|
|
255
|
+
})
|
|
256
|
+
]
|
|
257
|
+
})
|
|
286
258
|
]
|
|
287
|
-
}),
|
|
288
|
-
/*#__PURE__*/ _jsx("span", {
|
|
289
|
-
"aria-hidden": true,
|
|
290
|
-
style: {
|
|
291
|
-
fontSize: '16px',
|
|
292
|
-
color: 'var(--theme-elevation-600)',
|
|
293
|
-
fontStyle: option.value === 'preset' || option.value === 'custom' ? 'italic' : 'normal',
|
|
294
|
-
fontFamily: option.fontFamily || undefined
|
|
295
|
-
},
|
|
296
|
-
children: option.value === 'preset' ? t.ui.usePreset : option.value === 'custom' ? t.ui.specifyCustom : t.ui.sampleSentence
|
|
297
259
|
})
|
|
298
260
|
]
|
|
299
261
|
}, option.value);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/fields/FontSelectField.tsx"],"sourcesContent":["'use client'\n\nimport { useField } from '@payloadcms/ui'\nimport { Check, ChevronDown } from 'lucide-react'\nimport type { SelectFieldClientComponent } from 'payload'\nimport { useCallback, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react'\nimport { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js'\nimport { resolveLocalizedText } from '../utils/localizedText.js'\n\ntype LocalizedLabel = Record<string, string>\n\ninterface FontOption {\n label: string | LocalizedLabel\n value: string\n fontFamily?: string\n category?: 'sans-serif' | 'serif' | 'monospace' | 'display'\n}\n\nconst NON_GOOGLE_VALUES = ['preset', 'custom', 'system-ui']\n\nfunction previewFamily(option: FontOption): string {\n if (option.fontFamily) return option.fontFamily\n if (option.category === 'sans-serif') return 'system-ui, sans-serif'\n if (option.category === 'serif') return 'Georgia, serif'\n if (option.category === 'monospace') return 'monospace'\n return 'inherit'\n}\n\n/**\n * Font select with live font previews. Implements the WAI-ARIA \"select-only\n * combobox\" pattern: a button trigger plus a listbox navigated with the arrow\n * keys, Home/End, Enter/Space and Escape. Preview fonts load for whichever option\n * is active, so keyboard users get the same previews as mouse users.\n */\nconst FontSelectField: SelectFieldClientComponent = ({ field, path }) => {\n const { value, setValue } = useField<string>({ path })\n const [isOpen, setIsOpen] = useState(false)\n const [activeIndex, setActiveIndex] = useState(-1)\n const lang = useThemeLanguage()\n const t = useThemeTranslations()\n const baseId = useId()\n const labelId = `${baseId}-label`\n const listboxId = `${baseId}-listbox`\n const containerRef = useRef<HTMLDivElement>(null)\n const triggerRef = useRef<HTMLButtonElement>(null)\n const listboxRef = useRef<HTMLDivElement>(null)\n const loadedFonts = useRef<Set<string>>(new Set())\n\n const options = (field.options as FontOption[]) || []\n const selectedIndex = options.findIndex((opt) => opt.value === value)\n const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined\n\n const getOptionLabel = (opt: FontOption) => resolveLocalizedText(opt.label, lang, 'Font')\n const label = resolveLocalizedText(field.label, lang, 'Font')\n const optionId = (index: number) => `${baseId}-option-${index}`\n\n const ensureFontLoaded = useCallback((fontValue: string | undefined) => {\n if (!fontValue || NON_GOOGLE_VALUES.includes(fontValue)) return\n if (loadedFonts.current.has(fontValue) || typeof document === 'undefined') return\n loadedFonts.current.add(fontValue)\n\n const linkId = `font-${fontValue.replace(/\\s+/g, '-')}`\n if (document.getElementById(linkId)) return\n\n const link = document.createElement('link')\n link.id = linkId\n link.rel = 'stylesheet'\n link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontValue)}:wght@400;700&display=swap`\n document.head.appendChild(link)\n }, [])\n\n const open = useCallback(\n (index = selectedIndex >= 0 ? selectedIndex : 0) => {\n setActiveIndex(index)\n setIsOpen(true)\n },\n [selectedIndex],\n )\n\n const close = useCallback((restoreFocus: boolean) => {\n setIsOpen(false)\n if (restoreFocus) triggerRef.current?.focus()\n }, [])\n\n const handleSelect = (index: number) => {\n const option = options[index]\n if (!option) return\n setValue(option.value)\n close(true)\n }\n\n // Load the preview font for the active option and keep it scrolled into view.\n useEffect(() => {\n if (!isOpen || activeIndex < 0) return\n ensureFontLoaded(options[activeIndex]?.value)\n listboxRef.current\n ?.querySelector<HTMLElement>(`#${CSS.escape(optionId(activeIndex))}`)\n ?.scrollIntoView({ block: 'nearest' })\n }, [isOpen, activeIndex])\n\n // Focus the listbox when it opens so keyboard navigation works immediately.\n useEffect(() => {\n if (isOpen) listboxRef.current?.focus()\n }, [isOpen])\n\n useEffect(() => {\n if (!isOpen) return\n const handlePointerOutside = (e: PointerEvent) => {\n if (!containerRef.current?.contains(e.target as Node)) close(false)\n }\n document.addEventListener('pointerdown', handlePointerOutside)\n return () => document.removeEventListener('pointerdown', handlePointerOutside)\n }, [isOpen, close])\n\n const handleTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {\n event.preventDefault()\n open(event.key === 'ArrowUp' && selectedIndex < 0 ? options.length - 1 : undefined)\n }\n }\n\n const handleListboxKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const last = options.length - 1\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n setActiveIndex((i) => Math.min(last, i + 1))\n break\n case 'ArrowUp':\n event.preventDefault()\n setActiveIndex((i) => Math.max(0, i - 1))\n break\n case 'Home':\n event.preventDefault()\n setActiveIndex(0)\n break\n case 'End':\n event.preventDefault()\n setActiveIndex(last)\n break\n case 'Enter':\n case ' ':\n event.preventDefault()\n handleSelect(activeIndex)\n break\n case 'Escape':\n event.preventDefault()\n event.stopPropagation()\n close(true)\n break\n case 'Tab':\n close(false)\n break\n default:\n // Type-ahead: jump to the first option starting with the typed letter.\n if (event.key.length === 1 && /\\S/.test(event.key)) {\n const letter = event.key.toLowerCase()\n const start = activeIndex + 1\n const ordered = [...options.slice(start), ...options.slice(0, start)]\n const match = ordered.find((opt) => getOptionLabel(opt).toLowerCase().startsWith(letter))\n if (match) setActiveIndex(options.indexOf(match))\n }\n }\n }\n\n return (\n <div\n ref={containerRef}\n className=\"font-select-container\"\n style={{ position: 'relative', marginBottom: '12px', maxWidth: '360px' }}\n >\n <div\n id={labelId}\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 </div>\n\n <button\n ref={triggerRef}\n type=\"button\"\n className=\"font-select-trigger\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={isOpen}\n aria-controls={isOpen ? listboxId : undefined}\n aria-labelledby={`${labelId} ${baseId}-value`}\n onClick={() => (isOpen ? close(false) : open())}\n onKeyDown={handleTriggerKeyDown}\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 gap: '8px',\n fontSize: '14px',\n fontFamily: selectedOption?.fontFamily || 'inherit',\n }}\n >\n <span id={`${baseId}-value`} style={{ flex: 1, minWidth: 0 }}>\n {selectedOption ? getOptionLabel(selectedOption) : t.ui.selectFontPlaceholder}\n </span>\n <ChevronDown\n size={14}\n aria-hidden\n style={{ flexShrink: 0, transform: isOpen ? 'rotate(180deg)' : undefined }}\n />\n </button>\n\n {isOpen && (\n <div\n ref={listboxRef}\n id={listboxId}\n role=\"listbox\"\n tabIndex={-1}\n aria-labelledby={labelId}\n aria-activedescendant={activeIndex >= 0 ? optionId(activeIndex) : undefined}\n onKeyDown={handleListboxKeyDown}\n className=\"font-select-listbox\"\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, index) => {\n const isSelected = option.value === value\n const isActive = index === activeIndex\n\n return (\n <div\n key={option.value}\n id={optionId(index)}\n role=\"option\"\n aria-selected={isSelected}\n onClick={() => handleSelect(index)}\n onPointerMove={() => setActiveIndex(index)}\n style={{\n padding: '12px 14px',\n backgroundColor: isActive\n ? 'var(--theme-elevation-100)'\n : isSelected\n ? 'var(--theme-elevation-50)'\n : 'transparent',\n // The active option gets an outline so keyboard focus never relies on a subtle fill alone.\n outline: isActive ? '2px solid var(--theme-elevation-800)' : undefined,\n outlineOffset: '-2px',\n color: 'var(--theme-elevation-800)',\n cursor: 'pointer',\n fontSize: '14px',\n fontFamily: previewFamily(option),\n display: 'flex',\n flexDirection: 'column',\n gap: '4px',\n }}\n >\n <span\n style={{\n fontWeight: isSelected ? 600 : 500,\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n }}\n >\n {isSelected && <Check size={14} color=\"var(--theme-elevation-600)\" aria-hidden />}\n {getOptionLabel(option)}\n </span>\n <span\n aria-hidden\n style={{\n fontSize: '16px',\n color: 'var(--theme-elevation-600)',\n fontStyle:\n option.value === 'preset' || option.value === 'custom' ? 'italic' : 'normal',\n fontFamily: option.fontFamily || undefined,\n }}\n >\n {option.value === 'preset'\n ? t.ui.usePreset\n : option.value === 'custom'\n ? t.ui.specifyCustom\n : t.ui.sampleSentence}\n </span>\n </div>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n\nexport default FontSelectField\n"],"names":["useField","Check","ChevronDown","useCallback","useEffect","useId","useRef","useState","useThemeLanguage","useThemeTranslations","resolveLocalizedText","NON_GOOGLE_VALUES","previewFamily","option","fontFamily","category","FontSelectField","field","path","value","setValue","isOpen","setIsOpen","activeIndex","setActiveIndex","lang","t","baseId","labelId","listboxId","containerRef","triggerRef","listboxRef","loadedFonts","Set","options","selectedIndex","findIndex","opt","selectedOption","undefined","getOptionLabel","label","optionId","index","ensureFontLoaded","fontValue","includes","current","has","document","add","linkId","replace","getElementById","link","createElement","id","rel","href","encodeURIComponent","head","appendChild","open","close","restoreFocus","focus","handleSelect","querySelector","CSS","escape","scrollIntoView","block","handlePointerOutside","e","contains","target","addEventListener","removeEventListener","handleTriggerKeyDown","event","key","preventDefault","length","handleListboxKeyDown","last","i","Math","min","max","stopPropagation","test","letter","toLowerCase","start","ordered","slice","match","find","startsWith","indexOf","div","ref","className","style","position","marginBottom","maxWidth","display","fontSize","fontWeight","color","button","type","role","aria-haspopup","aria-expanded","aria-controls","aria-labelledby","onClick","onKeyDown","width","padding","borderRadius","border","backgroundColor","textAlign","cursor","justifyContent","alignItems","gap","span","flex","minWidth","ui","selectFontPlaceholder","size","aria-hidden","flexShrink","transform","tabIndex","aria-activedescendant","top","left","right","marginTop","maxHeight","overflowY","boxShadow","zIndex","map","isSelected","isActive","aria-selected","onPointerMove","outline","outlineOffset","flexDirection","fontStyle","usePreset","specifyCustom","sampleSentence"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/fields/FontSelectField.tsx"],"sourcesContent":["'use client'\n\nimport './FontSelectField.css'\nimport { useField } from '@payloadcms/ui'\nimport { mergeFieldStyles } from '@payloadcms/ui/shared'\nimport { Check, ChevronDown } from 'lucide-react'\nimport type { SelectFieldClientComponent } from 'payload'\nimport React, { useCallback, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react'\nimport {\n FONT_CATEGORY_LABELS,\n getFontCatalogEntry,\n type FontCategory,\n} from '../constants/themeFonts.js'\nimport { useThemeFonts } from '../hooks/useThemeFonts.js'\nimport { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js'\nimport { resolveLocalizedText } from '../utils/localizedText.js'\n\ntype LocalizedLabel = Record<string, string>\n\ninterface FontOption {\n label: string | LocalizedLabel\n value: string\n fontFamily?: string\n category?: FontCategory\n}\n\nfunction previewFamily(option: FontOption): string {\n if (option.fontFamily) return option.fontFamily\n if (option.category === 'sans-serif') return 'system-ui, sans-serif'\n if (option.category === 'serif') return 'Georgia, serif'\n if (option.category === 'monospace') return 'monospace'\n return 'inherit'\n}\n\n/**\n * Font select with live font previews. Implements the WAI-ARIA \"select-only\n * combobox\" pattern: a button trigger plus a listbox navigated with the arrow\n * keys, Home/End, Enter/Space and Escape. Options are grouped by font category.\n * Preview fonts come from the plugin's self-hosted font endpoint: the selected\n * face always, every face while the list is open.\n */\nconst FontSelectField: SelectFieldClientComponent = ({ field, path }) => {\n const { value, setValue } = useField<string>({ path })\n const [isOpen, setIsOpen] = useState(false)\n const [activeIndex, setActiveIndex] = useState(-1)\n const lang = useThemeLanguage()\n const t = useThemeTranslations()\n const baseId = useId()\n const labelId = `${baseId}-label`\n const listboxId = `${baseId}-listbox`\n const containerRef = useRef<HTMLDivElement>(null)\n const triggerRef = useRef<HTMLButtonElement>(null)\n const listboxRef = useRef<HTMLDivElement>(null)\n\n // Payload strips custom option props on the way to the client, so the face and\n // category come from the font catalog by value.\n const options = ((field.options as FontOption[]) || []).map((option) => {\n const entry = getFontCatalogEntry(option.value)\n return entry ? { ...option, fontFamily: entry.fontFamily, category: entry.category } : option\n })\n const [alignEnd, setAlignEnd] = useState(false)\n const selectedIndex = options.findIndex((opt) => opt.value === value)\n const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined\n\n const getOptionLabel = (opt: FontOption) => resolveLocalizedText(opt.label, lang, 'Font')\n const label = resolveLocalizedText(field.label, lang, 'Font')\n const optionId = (index: number) => `${baseId}-option-${index}`\n\n useThemeFonts(isOpen ? options.map((option) => option.value) : [value])\n\n const open = useCallback(\n (index = selectedIndex >= 0 ? selectedIndex : 0) => {\n // Open towards the side with room, so the list never spills past the form column.\n const trigger = triggerRef.current?.getBoundingClientRect()\n const column = containerRef.current\n ?.closest('.render-fields, .group-field__wrap')\n ?.getBoundingClientRect()\n setAlignEnd(Boolean(trigger && column && trigger.left + 300 > column.right))\n setActiveIndex(index)\n setIsOpen(true)\n },\n [selectedIndex],\n )\n\n const close = useCallback((restoreFocus: boolean) => {\n setIsOpen(false)\n if (restoreFocus) triggerRef.current?.focus()\n }, [])\n\n const handleSelect = (index: number) => {\n const option = options[index]\n if (!option) return\n setValue(option.value)\n close(true)\n }\n\n // Keep the active option scrolled into view.\n useEffect(() => {\n if (!isOpen || activeIndex < 0) return\n listboxRef.current\n ?.querySelector<HTMLElement>(`#${CSS.escape(optionId(activeIndex))}`)\n ?.scrollIntoView({ block: 'nearest' })\n }, [isOpen, activeIndex])\n\n // Focus the listbox when it opens so keyboard navigation works immediately.\n useEffect(() => {\n if (isOpen) listboxRef.current?.focus()\n }, [isOpen])\n\n useEffect(() => {\n if (!isOpen) return\n const handlePointerOutside = (e: PointerEvent) => {\n if (!containerRef.current?.contains(e.target as Node)) close(false)\n }\n document.addEventListener('pointerdown', handlePointerOutside)\n return () => document.removeEventListener('pointerdown', handlePointerOutside)\n }, [isOpen, close])\n\n const handleTriggerKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n if (['ArrowDown', 'ArrowUp', 'Enter', ' '].includes(event.key)) {\n event.preventDefault()\n open(event.key === 'ArrowUp' && selectedIndex < 0 ? options.length - 1 : undefined)\n }\n }\n\n const handleListboxKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n const last = options.length - 1\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n setActiveIndex((i) => Math.min(last, i + 1))\n break\n case 'ArrowUp':\n event.preventDefault()\n setActiveIndex((i) => Math.max(0, i - 1))\n break\n case 'Home':\n event.preventDefault()\n setActiveIndex(0)\n break\n case 'End':\n event.preventDefault()\n setActiveIndex(last)\n break\n case 'Enter':\n case ' ':\n event.preventDefault()\n handleSelect(activeIndex)\n break\n case 'Escape':\n event.preventDefault()\n event.stopPropagation()\n close(true)\n break\n case 'Tab':\n close(false)\n break\n default:\n // Type-ahead: jump to the first option starting with the typed letter.\n if (event.key.length === 1 && /\\S/.test(event.key)) {\n const letter = event.key.toLowerCase()\n const start = activeIndex + 1\n const ordered = [...options.slice(start), ...options.slice(0, start)]\n const match = ordered.find((opt) => getOptionLabel(opt).toLowerCase().startsWith(letter))\n if (match) setActiveIndex(options.indexOf(match))\n }\n }\n }\n\n const isSpecial = (option: FontOption) => option.value === 'preset' || option.value === 'custom'\n\n return (\n <div\n ref={containerRef}\n // Payload rows size each field through --field-width on the field root.\n className=\"field-type tm-font-select\"\n style={mergeFieldStyles(field) as React.CSSProperties}\n >\n <div id={labelId} className=\"field-label tm-font-select__label\">\n {label}\n </div>\n\n <button\n ref={triggerRef}\n type=\"button\"\n className=\"tm-font-select__trigger\"\n role=\"combobox\"\n aria-haspopup=\"listbox\"\n aria-expanded={isOpen}\n aria-controls={isOpen ? listboxId : undefined}\n aria-labelledby={`${labelId} ${baseId}-value`}\n onClick={() => (isOpen ? close(false) : open())}\n onKeyDown={handleTriggerKeyDown}\n >\n <span\n id={`${baseId}-value`}\n className=\"tm-font-select__value\"\n style={{\n fontFamily:\n selectedOption && !isSpecial(selectedOption) ? previewFamily(selectedOption) : undefined,\n }}\n >\n {selectedOption ? getOptionLabel(selectedOption) : t.ui.selectFontPlaceholder}\n </span>\n <ChevronDown\n size={16}\n aria-hidden\n className={`tm-font-select__chevron${isOpen ? ' is-open' : ''}`}\n />\n </button>\n\n {isOpen && (\n <div\n ref={listboxRef}\n id={listboxId}\n role=\"listbox\"\n tabIndex={-1}\n aria-labelledby={labelId}\n aria-activedescendant={activeIndex >= 0 ? optionId(activeIndex) : undefined}\n onKeyDown={handleListboxKeyDown}\n className={`tm-font-select__listbox${alignEnd ? ' is-align-end' : ''}`}\n >\n {options.map((option, index) => {\n const isSelected = option.value === value\n const isActive = index === activeIndex\n const startsGroup =\n option.category !== undefined && option.category !== options[index - 1]?.category\n const special = isSpecial(option)\n const family = special ? undefined : previewFamily(option)\n\n return (\n <React.Fragment key={option.value}>\n {startsGroup && option.category ? (\n <div role=\"presentation\" className=\"tm-font-select__group\">\n {resolveLocalizedText(FONT_CATEGORY_LABELS[option.category], lang, '')}\n </div>\n ) : null}\n <div\n id={optionId(index)}\n role=\"option\"\n aria-selected={isSelected}\n onClick={() => handleSelect(index)}\n onPointerMove={() => setActiveIndex(index)}\n className={`tm-font-select__option${isActive ? ' is-active' : ''}${\n isSelected ? ' is-selected' : ''\n }`}\n >\n <span className=\"tm-font-select__check\" aria-hidden>\n {isSelected ? <Check size={14} /> : null}\n </span>\n <span className=\"tm-font-select__text\">\n <span className=\"tm-font-select__name\" style={{ fontFamily: family }}>\n {getOptionLabel(option)}\n </span>\n <span\n aria-hidden\n className={`tm-font-select__sample${special ? ' is-hint' : ''}`}\n style={{ fontFamily: family }}\n >\n {option.value === 'preset'\n ? t.ui.usePreset\n : option.value === 'custom'\n ? t.ui.specifyCustom\n : t.ui.sampleSentence}\n </span>\n </span>\n </div>\n </React.Fragment>\n )\n })}\n </div>\n )}\n </div>\n )\n}\n\nexport default FontSelectField\n"],"names":["useField","mergeFieldStyles","Check","ChevronDown","React","useCallback","useEffect","useId","useRef","useState","FONT_CATEGORY_LABELS","getFontCatalogEntry","useThemeFonts","useThemeLanguage","useThemeTranslations","resolveLocalizedText","previewFamily","option","fontFamily","category","FontSelectField","field","path","value","setValue","isOpen","setIsOpen","activeIndex","setActiveIndex","lang","t","baseId","labelId","listboxId","containerRef","triggerRef","listboxRef","options","map","entry","alignEnd","setAlignEnd","selectedIndex","findIndex","opt","selectedOption","undefined","getOptionLabel","label","optionId","index","open","trigger","current","getBoundingClientRect","column","closest","Boolean","left","right","close","restoreFocus","focus","handleSelect","querySelector","CSS","escape","scrollIntoView","block","handlePointerOutside","e","contains","target","document","addEventListener","removeEventListener","handleTriggerKeyDown","event","includes","key","preventDefault","length","handleListboxKeyDown","last","i","Math","min","max","stopPropagation","test","letter","toLowerCase","start","ordered","slice","match","find","startsWith","indexOf","isSpecial","div","ref","className","style","id","button","type","role","aria-haspopup","aria-expanded","aria-controls","aria-labelledby","onClick","onKeyDown","span","ui","selectFontPlaceholder","size","aria-hidden","tabIndex","aria-activedescendant","isSelected","isActive","startsGroup","special","family","Fragment","aria-selected","onPointerMove","usePreset","specifyCustom","sampleSentence"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StylePresetField.d.ts","sourceRoot":"","sources":["../../src/fields/StylePresetField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"StylePresetField.d.ts","sourceRoot":"","sources":["../../src/fields/StylePresetField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAA;AAuJnD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,oBAAoB,+BAgYnE"}
|
|
@@ -5,8 +5,10 @@ import { useCallback, useMemo } from 'react';
|
|
|
5
5
|
import { X } from 'lucide-react';
|
|
6
6
|
import { AppearanceUndoBar } from '../components/AppearanceSessionControls.js';
|
|
7
7
|
import { createBulkWriter, getAppearanceSession, useAppearanceSession } from '../hooks/useAppearanceSession.js';
|
|
8
|
+
import { getFontCatalogEntry } from '../constants/themeFonts.js';
|
|
9
|
+
import { useThemeFonts } from '../hooks/useThemeFonts.js';
|
|
8
10
|
import { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js';
|
|
9
|
-
import { allStylePresets, stylePresetCategories } from '../style-presets.js';
|
|
11
|
+
import { allStylePresets, normalizeStylePresetValue, stylePresetCategories } from '../style-presets.js';
|
|
10
12
|
const VF_KEYS = [
|
|
11
13
|
'effectStyle',
|
|
12
14
|
'shadowIntensity',
|
|
@@ -17,9 +19,13 @@ const VF_KEYS = [
|
|
|
17
19
|
];
|
|
18
20
|
const CS_KEYS = [
|
|
19
21
|
'buttonVariant',
|
|
22
|
+
'buttonSize',
|
|
20
23
|
'cardStyle',
|
|
21
24
|
'cardHoverEffect',
|
|
22
|
-
'navbarStyle'
|
|
25
|
+
'navbarStyle',
|
|
26
|
+
'footerStyle',
|
|
27
|
+
'imageStyle',
|
|
28
|
+
'linkStyle'
|
|
23
29
|
];
|
|
24
30
|
const TYPO_KEYS = [
|
|
25
31
|
'bodyFont',
|
|
@@ -110,6 +116,12 @@ function getContainerBg(preset) {
|
|
|
110
116
|
const effect = preset.visualEffects?.effectStyle ?? 'elevated';
|
|
111
117
|
return EFFECT_PREVIEW[effect]?.containerBg ?? '#f1f5f9';
|
|
112
118
|
}
|
|
119
|
+
const ALL_PRESET_FONTS = allStylePresets.flatMap((preset)=>[
|
|
120
|
+
preset.typography?.bodyFont,
|
|
121
|
+
preset.typography?.headingFont
|
|
122
|
+
]);
|
|
123
|
+
const fontStack = (value)=>getFontCatalogEntry(value)?.fontFamily ?? 'inherit';
|
|
124
|
+
const fontName = (value)=>value === 'helvetica' ? 'Helvetica' : value === 'system-ui' ? 'System' : value ?? '';
|
|
113
125
|
// Selection uses Payload's own contrast token so it reads in both admin themes.
|
|
114
126
|
const SELECTED_ACCENT = 'var(--theme-elevation-800)';
|
|
115
127
|
// The thumbnail is always drawn on a light surface, so its sample button keeps a fixed dark fill.
|
|
@@ -126,6 +138,8 @@ export default function StylePresetField(props) {
|
|
|
126
138
|
const formFields = useFormFields(([state])=>state);
|
|
127
139
|
const { locks } = useAppearanceSession();
|
|
128
140
|
const fullyLocked = locks.style && locks.fonts;
|
|
141
|
+
// The cards show each style's real fonts.
|
|
142
|
+
useThemeFonts(ALL_PRESET_FONTS);
|
|
129
143
|
/**
|
|
130
144
|
* Write a style preset, honouring the session locks: typography is skipped with
|
|
131
145
|
* the Fonts lock, and effects / components / radius / motion / spacing with the
|
|
@@ -136,7 +150,7 @@ export default function StylePresetField(props) {
|
|
|
136
150
|
if (!current.style) {
|
|
137
151
|
if (preset.borderRadius !== undefined) writer.write(`${basePath}.borderRadius`, preset.borderRadius);
|
|
138
152
|
if (preset.animationLevel !== undefined) writer.write(`${basePath}.animationLevel`, preset.animationLevel);
|
|
139
|
-
if (preset.spacing !== undefined) writer.write(`${basePath}.spacing`, preset.spacing);
|
|
153
|
+
if (preset.spacing !== undefined) writer.write(`${basePath}.spacing`, normalizeStylePresetValue('spacing', preset.spacing));
|
|
140
154
|
VF_KEYS.forEach((key)=>{
|
|
141
155
|
const value = preset.visualEffects?.[key];
|
|
142
156
|
if (value !== undefined) writer.write(`${basePath}.visualEffects.${key}`, value);
|
|
@@ -149,7 +163,7 @@ export default function StylePresetField(props) {
|
|
|
149
163
|
if (!current.fonts) {
|
|
150
164
|
TYPO_KEYS.forEach((key)=>{
|
|
151
165
|
const value = preset.typography?.[key];
|
|
152
|
-
if (value !== undefined) writer.write(`${basePath}.typography.${key}`, value);
|
|
166
|
+
if (value !== undefined) writer.write(`${basePath}.typography.${key}`, normalizeStylePresetValue(`typography.${key}`, value));
|
|
153
167
|
});
|
|
154
168
|
}
|
|
155
169
|
}, [
|
|
@@ -363,48 +377,35 @@ export default function StylePresetField(props) {
|
|
|
363
377
|
preset.typography && /*#__PURE__*/ _jsxs("div", {
|
|
364
378
|
style: {
|
|
365
379
|
display: 'flex',
|
|
366
|
-
gap: '
|
|
380
|
+
gap: '8px',
|
|
367
381
|
alignItems: 'baseline',
|
|
368
|
-
marginBottom: '
|
|
369
|
-
|
|
382
|
+
marginBottom: '4px',
|
|
383
|
+
minWidth: 0
|
|
370
384
|
},
|
|
371
385
|
children: [
|
|
372
386
|
/*#__PURE__*/ _jsx("span", {
|
|
387
|
+
"aria-hidden": "true",
|
|
373
388
|
style: {
|
|
374
|
-
fontFamily: preset.typography.headingFont
|
|
389
|
+
fontFamily: fontStack(preset.typography.headingFont),
|
|
375
390
|
fontWeight: preset.typography.headingWeight ?? '700',
|
|
376
|
-
|
|
391
|
+
textTransform: preset.typography.letterSpacing === 'wider' ? 'uppercase' : 'none',
|
|
392
|
+
fontSize: '20px',
|
|
393
|
+
lineHeight: 1,
|
|
377
394
|
color: 'var(--theme-elevation-800)',
|
|
378
|
-
whiteSpace: 'nowrap',
|
|
379
|
-
overflow: 'hidden',
|
|
380
|
-
textOverflow: 'ellipsis',
|
|
381
|
-
maxWidth: '70px',
|
|
382
395
|
flexShrink: 0
|
|
383
396
|
},
|
|
384
|
-
|
|
385
|
-
children: (preset.typography.headingFont ?? '').split(',')[0]?.split(' ').slice(0, 2).join(' ') || 'Heading'
|
|
397
|
+
children: "Aa"
|
|
386
398
|
}),
|
|
387
|
-
|
|
399
|
+
/*#__PURE__*/ _jsx("span", {
|
|
388
400
|
style: {
|
|
389
|
-
|
|
390
|
-
fontSize: '11px',
|
|
401
|
+
fontSize: '12px',
|
|
391
402
|
color: 'var(--theme-elevation-600)',
|
|
392
403
|
whiteSpace: 'nowrap',
|
|
393
404
|
overflow: 'hidden',
|
|
394
405
|
textOverflow: 'ellipsis',
|
|
395
|
-
|
|
396
|
-
},
|
|
397
|
-
title: preset.typography.bodyFont,
|
|
398
|
-
children: (preset.typography.bodyFont ?? '').split(',')[0]?.split(' ').slice(0, 2).join(' ') || 'Body'
|
|
399
|
-
}),
|
|
400
|
-
preset.spacing && /*#__PURE__*/ _jsx("span", {
|
|
401
|
-
style: {
|
|
402
|
-
marginLeft: 'auto',
|
|
403
|
-
fontSize: '11px',
|
|
404
|
-
color: 'var(--theme-elevation-600)',
|
|
405
|
-
flexShrink: 0
|
|
406
|
+
minWidth: 0
|
|
406
407
|
},
|
|
407
|
-
children: preset.
|
|
408
|
+
children: preset.typography.bodyFont === preset.typography.headingFont ? fontName(preset.typography.headingFont) : `${fontName(preset.typography.headingFont)} · ${fontName(preset.typography.bodyFont)}`
|
|
408
409
|
})
|
|
409
410
|
]
|
|
410
411
|
}),
|