@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.
Files changed (81) hide show
  1. package/README.md +37 -7
  2. package/dist/components/ServerThemeInjector.d.ts +13 -1
  3. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  4. package/dist/components/ServerThemeInjector.js +24 -1
  5. package/dist/components/ServerThemeInjector.js.map +1 -1
  6. package/dist/components/ThemeLivePreviewSync.d.ts +3 -0
  7. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  8. package/dist/components/ThemeLivePreviewSync.js +28 -4
  9. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  10. package/dist/components/typographyPreviewUtils.d.ts +5 -0
  11. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  12. package/dist/components/typographyPreviewUtils.js +16 -10
  13. package/dist/components/typographyPreviewUtils.js.map +1 -1
  14. package/dist/constants/themeFonts.d.ts +94 -168
  15. package/dist/constants/themeFonts.d.ts.map +1 -1
  16. package/dist/constants/themeFonts.js +260 -140
  17. package/dist/constants/themeFonts.js.map +1 -1
  18. package/dist/endpoints/themeFonts.d.ts +5 -0
  19. package/dist/endpoints/themeFonts.d.ts.map +1 -0
  20. package/dist/endpoints/themeFonts.js +138 -0
  21. package/dist/endpoints/themeFonts.js.map +1 -0
  22. package/dist/fields/AppearancePreviewField.d.ts.map +1 -1
  23. package/dist/fields/AppearancePreviewField.js +196 -133
  24. package/dist/fields/AppearancePreviewField.js.map +1 -1
  25. package/dist/fields/FontSelectField.css +155 -0
  26. package/dist/fields/FontSelectField.d.ts +4 -2
  27. package/dist/fields/FontSelectField.d.ts.map +1 -1
  28. package/dist/fields/FontSelectField.js +80 -118
  29. package/dist/fields/FontSelectField.js.map +1 -1
  30. package/dist/fields/StylePresetField.d.ts.map +1 -1
  31. package/dist/fields/StylePresetField.js +31 -30
  32. package/dist/fields/StylePresetField.js.map +1 -1
  33. package/dist/fields/ThemePreviewField.css +63 -1
  34. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  35. package/dist/fields/ThemePreviewField.js +2 -5
  36. package/dist/fields/ThemePreviewField.js.map +1 -1
  37. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  38. package/dist/fields/themeConfigurationField.js +320 -271
  39. package/dist/fields/themeConfigurationField.js.map +1 -1
  40. package/dist/hooks/useNativeLivePreview.d.ts +15 -0
  41. package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
  42. package/dist/hooks/useNativeLivePreview.js +18 -0
  43. package/dist/hooks/useNativeLivePreview.js.map +1 -0
  44. package/dist/hooks/useThemeFonts.d.ts +10 -0
  45. package/dist/hooks/useThemeFonts.d.ts.map +1 -0
  46. package/dist/hooks/useThemeFonts.js +36 -0
  47. package/dist/hooks/useThemeFonts.js.map +1 -0
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +9 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/next-fonts.d.ts +3 -0
  53. package/dist/next-fonts.d.ts.map +1 -0
  54. package/dist/next-fonts.js +360 -0
  55. package/dist/next-fonts.js.map +1 -0
  56. package/dist/payload-types.d.ts +10 -6
  57. package/dist/payload-types.d.ts.map +1 -1
  58. package/dist/payload-types.js.map +1 -1
  59. package/dist/presets.d.ts +6 -0
  60. package/dist/presets.d.ts.map +1 -1
  61. package/dist/presets.js.map +1 -1
  62. package/dist/style-presets.d.ts +27 -18
  63. package/dist/style-presets.d.ts.map +1 -1
  64. package/dist/style-presets.js +399 -217
  65. package/dist/style-presets.js.map +1 -1
  66. package/dist/translations.d.ts +5 -0
  67. package/dist/translations.d.ts.map +1 -1
  68. package/dist/translations.js +10 -0
  69. package/dist/translations.js.map +1 -1
  70. package/dist/utils/livePreviewThemeRuntime.d.ts +6 -1
  71. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  72. package/dist/utils/livePreviewThemeRuntime.js +11 -1
  73. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  74. package/dist/utils/resolveThemeConfiguration.d.ts +15 -0
  75. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  76. package/dist/utils/resolveThemeConfiguration.js +41 -8
  77. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  78. package/dist/utils/themeUtils.d.ts.map +1 -1
  79. package/dist/utils/themeUtils.js +60 -2
  80. package/dist/utils/themeUtils.js.map +1 -1
  81. 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. Preview fonts load for whichever option
24
- * is active, so keyboard users get the same previews as mouse users.
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
- const loadedFonts = useRef(new Set());
40
- const options = field.options || [];
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
- const ensureFontLoaded = useCallback((fontValue)=>{
47
- if (!fontValue || NON_GOOGLE_VALUES.includes(fontValue)) return;
48
- if (loadedFonts.current.has(fontValue) || typeof document === 'undefined') return;
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
- // Load the preview font for the active option and keep it scrolled into view.
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
- className: "font-select-container",
162
- style: {
163
- position: 'relative',
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
- style: {
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-select-trigger",
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
- flex: 1,
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: 14,
195
+ size: 16,
217
196
  "aria-hidden": true,
218
- style: {
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: "font-select-listbox",
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
- return /*#__PURE__*/ _jsxs("div", {
252
- id: optionId(index),
253
- role: "option",
254
- "aria-selected": isSelected,
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__*/ _jsxs("span", {
273
- style: {
274
- fontWeight: isSelected ? 600 : 500,
275
- display: 'flex',
276
- alignItems: 'center',
277
- gap: '8px'
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
- isSelected && /*#__PURE__*/ _jsx(Check, {
281
- size: 14,
282
- color: "var(--theme-elevation-600)",
283
- "aria-hidden": true
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
- getOptionLabel(option)
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":"AAAA;;AAEA,OAAO,wBAAuB;AAC9B,SAASA,QAAQ,QAAQ,iBAAgB;AACzC,SAASC,gBAAgB,QAAQ,wBAAuB;AACxD,SAASC,KAAK,EAAEC,WAAW,QAAQ,eAAc;AAEjD,OAAOC,SAASC,WAAW,EAAEC,SAAS,EAAEC,KAAK,EAAEC,MAAM,EAAEC,QAAQ,QAA4B,QAAO;AAClG,SACEC,oBAAoB,EACpBC,mBAAmB,QAEd,6BAA4B;AACnC,SAASC,aAAa,QAAQ,4BAA2B;AACzD,SAASC,gBAAgB,EAAEC,oBAAoB,QAAQ,mCAAkC;AACzF,SAASC,oBAAoB,QAAQ,4BAA2B;AAWhE,SAASC,cAAcC,MAAkB;IACvC,IAAIA,OAAOC,UAAU,EAAE,OAAOD,OAAOC,UAAU;IAC/C,IAAID,OAAOE,QAAQ,KAAK,cAAc,OAAO;IAC7C,IAAIF,OAAOE,QAAQ,KAAK,SAAS,OAAO;IACxC,IAAIF,OAAOE,QAAQ,KAAK,aAAa,OAAO;IAC5C,OAAO;AACT;AAEA;;;;;;CAMC,GACD,MAAMC,kBAA8C,CAAC,EAAEC,KAAK,EAAEC,IAAI,EAAE;IAClE,MAAM,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAGxB,SAAiB;QAAEsB;IAAK;IACpD,MAAM,CAACG,QAAQC,UAAU,GAAGjB,SAAS;IACrC,MAAM,CAACkB,aAAaC,eAAe,GAAGnB,SAAS,CAAC;IAChD,MAAMoB,OAAOhB;IACb,MAAMiB,IAAIhB;IACV,MAAMiB,SAASxB;IACf,MAAMyB,UAAU,GAAGD,OAAO,MAAM,CAAC;IACjC,MAAME,YAAY,GAAGF,OAAO,QAAQ,CAAC;IACrC,MAAMG,eAAe1B,OAAuB;IAC5C,MAAM2B,aAAa3B,OAA0B;IAC7C,MAAM4B,aAAa5B,OAAuB;IAE1C,+EAA+E;IAC/E,gDAAgD;IAChD,MAAM6B,UAAU,AAAC,CAAA,AAAChB,MAAMgB,OAAO,IAAqB,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACrB;QAC3D,MAAMsB,QAAQ5B,oBAAoBM,OAAOM,KAAK;QAC9C,OAAOgB,QAAQ;YAAE,GAAGtB,MAAM;YAAEC,YAAYqB,MAAMrB,UAAU;YAAEC,UAAUoB,MAAMpB,QAAQ;QAAC,IAAIF;IACzF;IACA,MAAM,CAACuB,UAAUC,YAAY,GAAGhC,SAAS;IACzC,MAAMiC,gBAAgBL,QAAQM,SAAS,CAAC,CAACC,MAAQA,IAAIrB,KAAK,KAAKA;IAC/D,MAAMsB,iBAAiBH,iBAAiB,IAAIL,OAAO,CAACK,cAAc,GAAGI;IAErE,MAAMC,iBAAiB,CAACH,MAAoB7B,qBAAqB6B,IAAII,KAAK,EAAEnB,MAAM;IAClF,MAAMmB,QAAQjC,qBAAqBM,MAAM2B,KAAK,EAAEnB,MAAM;IACtD,MAAMoB,WAAW,CAACC,QAAkB,GAAGnB,OAAO,QAAQ,EAAEmB,OAAO;IAE/DtC,cAAca,SAASY,QAAQC,GAAG,CAAC,CAACrB,SAAWA,OAAOM,KAAK,IAAI;QAACA;KAAM;IAEtE,MAAM4B,OAAO9C,YACX,CAAC6C,QAAQR,iBAAiB,IAAIA,gBAAgB,CAAC;QAC7C,kFAAkF;QAClF,MAAMU,UAAUjB,WAAWkB,OAAO,EAAEC;QACpC,MAAMC,SAASrB,aAAamB,OAAO,EAC/BG,QAAQ,uCACRF;QACJb,YAAYgB,QAAQL,WAAWG,UAAUH,QAAQM,IAAI,GAAG,MAAMH,OAAOI,KAAK;QAC1E/B,eAAesB;QACfxB,UAAU;IACZ,GACA;QAACgB;KAAc;IAGjB,MAAMkB,QAAQvD,YAAY,CAACwD;QACzBnC,UAAU;QACV,IAAImC,cAAc1B,WAAWkB,OAAO,EAAES;IACxC,GAAG,EAAE;IAEL,MAAMC,eAAe,CAACb;QACpB,MAAMjC,SAASoB,OAAO,CAACa,MAAM;QAC7B,IAAI,CAACjC,QAAQ;QACbO,SAASP,OAAOM,KAAK;QACrBqC,MAAM;IACR;IAEA,6CAA6C;IAC7CtD,UAAU;QACR,IAAI,CAACmB,UAAUE,cAAc,GAAG;QAChCS,WAAWiB,OAAO,EACdW,cAA2B,CAAC,CAAC,EAAEC,IAAIC,MAAM,CAACjB,SAAStB,eAAe,GAClEwC,eAAe;YAAEC,OAAO;QAAU;IACxC,GAAG;QAAC3C;QAAQE;KAAY;IAExB,4EAA4E;IAC5ErB,UAAU;QACR,IAAImB,QAAQW,WAAWiB,OAAO,EAAES;IAClC,GAAG;QAACrC;KAAO;IAEXnB,UAAU;QACR,IAAI,CAACmB,QAAQ;QACb,MAAM4C,uBAAuB,CAACC;YAC5B,IAAI,CAACpC,aAAamB,OAAO,EAAEkB,SAASD,EAAEE,MAAM,GAAWZ,MAAM;QAC/D;QACAa,SAASC,gBAAgB,CAAC,eAAeL;QACzC,OAAO,IAAMI,SAASE,mBAAmB,CAAC,eAAeN;IAC3D,GAAG;QAAC5C;QAAQmC;KAAM;IAElB,MAAMgB,uBAAuB,CAACC;QAC5B,IAAI;YAAC;YAAa;YAAW;YAAS;SAAI,CAACC,QAAQ,CAACD,MAAME,GAAG,GAAG;YAC9DF,MAAMG,cAAc;YACpB7B,KAAK0B,MAAME,GAAG,KAAK,aAAarC,gBAAgB,IAAIL,QAAQ4C,MAAM,GAAG,IAAInC;QAC3E;IACF;IAEA,MAAMoC,uBAAuB,CAACL;QAC5B,MAAMM,OAAO9C,QAAQ4C,MAAM,GAAG;QAC9B,OAAQJ,MAAME,GAAG;YACf,KAAK;gBACHF,MAAMG,cAAc;gBACpBpD,eAAe,CAACwD,IAAMC,KAAKC,GAAG,CAACH,MAAMC,IAAI;gBACzC;YACF,KAAK;gBACHP,MAAMG,cAAc;gBACpBpD,eAAe,CAACwD,IAAMC,KAAKE,GAAG,CAAC,GAAGH,IAAI;gBACtC;YACF,KAAK;gBACHP,MAAMG,cAAc;gBACpBpD,eAAe;gBACf;YACF,KAAK;gBACHiD,MAAMG,cAAc;gBACpBpD,eAAeuD;gBACf;YACF,KAAK;YACL,KAAK;gBACHN,MAAMG,cAAc;gBACpBjB,aAAapC;gBACb;YACF,KAAK;gBACHkD,MAAMG,cAAc;gBACpBH,MAAMW,eAAe;gBACrB5B,MAAM;gBACN;YACF,KAAK;gBACHA,MAAM;gBACN;YACF;gBACE,uEAAuE;gBACvE,IAAIiB,MAAME,GAAG,CAACE,MAAM,KAAK,KAAK,KAAKQ,IAAI,CAACZ,MAAME,GAAG,GAAG;oBAClD,MAAMW,SAASb,MAAME,GAAG,CAACY,WAAW;oBACpC,MAAMC,QAAQjE,cAAc;oBAC5B,MAAMkE,UAAU;2BAAIxD,QAAQyD,KAAK,CAACF;2BAAWvD,QAAQyD,KAAK,CAAC,GAAGF;qBAAO;oBACrE,MAAMG,QAAQF,QAAQG,IAAI,CAAC,CAACpD,MAAQG,eAAeH,KAAK+C,WAAW,GAAGM,UAAU,CAACP;oBACjF,IAAIK,OAAOnE,eAAeS,QAAQ6D,OAAO,CAACH;gBAC5C;QACJ;IACF;IAEA,MAAMI,YAAY,CAAClF,SAAuBA,OAAOM,KAAK,KAAK,YAAYN,OAAOM,KAAK,KAAK;IAExF,qBACE,MAAC6E;QACCC,KAAKnE;QACL,wEAAwE;QACxEoE,WAAU;QACVC,OAAOtG,iBAAiBoB;;0BAExB,KAAC+E;gBAAII,IAAIxE;gBAASsE,WAAU;0BACzBtD;;0BAGH,MAACyD;gBACCJ,KAAKlE;gBACLuE,MAAK;gBACLJ,WAAU;gBACVK,MAAK;gBACLC,iBAAc;gBACdC,iBAAepF;gBACfqF,iBAAerF,SAASQ,YAAYa;gBACpCiE,mBAAiB,GAAG/E,QAAQ,CAAC,EAAED,OAAO,MAAM,CAAC;gBAC7CiF,SAAS,IAAOvF,SAASmC,MAAM,SAAST;gBACxC8D,WAAWrC;;kCAEX,KAACsC;wBACCV,IAAI,GAAGzE,OAAO,MAAM,CAAC;wBACrBuE,WAAU;wBACVC,OAAO;4BACLrF,YACE2B,kBAAkB,CAACsD,UAAUtD,kBAAkB7B,cAAc6B,kBAAkBC;wBACnF;kCAECD,iBAAiBE,eAAeF,kBAAkBf,EAAEqF,EAAE,CAACC,qBAAqB;;kCAE/E,KAACjH;wBACCkH,MAAM;wBACNC,aAAW;wBACXhB,WAAW,CAAC,uBAAuB,EAAE7E,SAAS,aAAa,IAAI;;;;YAIlEA,wBACC,KAAC2E;gBACCC,KAAKjE;gBACLoE,IAAIvE;gBACJ0E,MAAK;gBACLY,UAAU,CAAC;gBACXR,mBAAiB/E;gBACjBwF,yBAAuB7F,eAAe,IAAIsB,SAAStB,eAAemB;gBAClEmE,WAAW/B;gBACXoB,WAAW,CAAC,uBAAuB,EAAE9D,WAAW,kBAAkB,IAAI;0BAErEH,QAAQC,GAAG,CAAC,CAACrB,QAAQiC;oBACpB,MAAMuE,aAAaxG,OAAOM,KAAK,KAAKA;oBACpC,MAAMmG,WAAWxE,UAAUvB;oBAC3B,MAAMgG,cACJ1G,OAAOE,QAAQ,KAAK2B,aAAa7B,OAAOE,QAAQ,KAAKkB,OAAO,CAACa,QAAQ,EAAE,EAAE/B;oBAC3E,MAAMyG,UAAUzB,UAAUlF;oBAC1B,MAAM4G,SAASD,UAAU9E,YAAY9B,cAAcC;oBAEnD,qBACE,MAACb,MAAM0H,QAAQ;;4BACZH,eAAe1G,OAAOE,QAAQ,iBAC7B,KAACiF;gCAAIO,MAAK;gCAAeL,WAAU;0CAChCvF,qBAAqBL,oBAAoB,CAACO,OAAOE,QAAQ,CAAC,EAAEU,MAAM;iCAEnE;0CACJ,MAACuE;gCACCI,IAAIvD,SAASC;gCACbyD,MAAK;gCACLoB,iBAAeN;gCACfT,SAAS,IAAMjD,aAAab;gCAC5B8E,eAAe,IAAMpG,eAAesB;gCACpCoD,WAAW,CAAC,sBAAsB,EAAEoB,WAAW,eAAe,KAC5DD,aAAa,iBAAiB,IAC9B;;kDAEF,KAACP;wCAAKZ,WAAU;wCAAwBgB,aAAW;kDAChDG,2BAAa,KAACvH;4CAAMmH,MAAM;6CAAS;;kDAEtC,MAACH;wCAAKZ,WAAU;;0DACd,KAACY;gDAAKZ,WAAU;gDAAuBC,OAAO;oDAAErF,YAAY2G;gDAAO;0DAChE9E,eAAe9B;;0DAElB,KAACiG;gDACCI,aAAW;gDACXhB,WAAW,CAAC,sBAAsB,EAAEsB,UAAU,aAAa,IAAI;gDAC/DrB,OAAO;oDAAErF,YAAY2G;gDAAO;0DAE3B5G,OAAOM,KAAK,KAAK,WACdO,EAAEqF,EAAE,CAACc,SAAS,GACdhH,OAAOM,KAAK,KAAK,WACfO,EAAEqF,EAAE,CAACe,aAAa,GAClBpG,EAAEqF,EAAE,CAACgB,cAAc;;;;;;;uBAhCZlH,OAAOM,KAAK;gBAsCrC;;;;AAKV;AAEA,eAAeH,gBAAe"}
@@ -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;AA+HnD,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,KAAK,EAAE,oBAAoB,+BAgZnE"}
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: '6px',
380
+ gap: '8px',
367
381
  alignItems: 'baseline',
368
- marginBottom: '3px',
369
- overflow: 'hidden'
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 ?? 'inherit',
389
+ fontFamily: fontStack(preset.typography.headingFont),
375
390
  fontWeight: preset.typography.headingWeight ?? '700',
376
- fontSize: '12px',
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
- title: preset.typography.headingFont,
385
- children: (preset.typography.headingFont ?? '').split(',')[0]?.split(' ').slice(0, 2).join(' ') || 'Heading'
397
+ children: "Aa"
386
398
  }),
387
- preset.typography.bodyFont !== preset.typography.headingFont && /*#__PURE__*/ _jsx("span", {
399
+ /*#__PURE__*/ _jsx("span", {
388
400
  style: {
389
- fontFamily: preset.typography.bodyFont ?? 'inherit',
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
- maxWidth: '70px'
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.spacing
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
  }),