@kilivi/payloadcms-theme-management 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/README.md +346 -845
  2. package/dist/components/AppearanceSessionControls.d.ts +15 -0
  3. package/dist/components/AppearanceSessionControls.d.ts.map +1 -0
  4. package/dist/components/AppearanceSessionControls.js +152 -0
  5. package/dist/components/AppearanceSessionControls.js.map +1 -0
  6. package/dist/components/ColorModeToggle.css +42 -0
  7. package/dist/components/ColorModeToggle.d.ts +40 -0
  8. package/dist/components/ColorModeToggle.d.ts.map +1 -0
  9. package/dist/components/ColorModeToggle.js +111 -0
  10. package/dist/components/ColorModeToggle.js.map +1 -0
  11. package/dist/components/ServerThemeInjector.d.ts +13 -1
  12. package/dist/components/ServerThemeInjector.d.ts.map +1 -1
  13. package/dist/components/ServerThemeInjector.js +65 -15
  14. package/dist/components/ServerThemeInjector.js.map +1 -1
  15. package/dist/components/ThemeLivePreviewSync.d.ts +4 -0
  16. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  17. package/dist/components/ThemeLivePreviewSync.js +28 -4
  18. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  19. package/dist/components/ThemeSwitcher.d.ts +36 -0
  20. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  21. package/dist/components/ThemeSwitcher.js +89 -0
  22. package/dist/components/ThemeSwitcher.js.map +1 -0
  23. package/dist/components/typographyPreviewUtils.d.ts +5 -0
  24. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  25. package/dist/components/typographyPreviewUtils.js +24 -11
  26. package/dist/components/typographyPreviewUtils.js.map +1 -1
  27. package/dist/constants/devicePreviews.d.ts +35 -0
  28. package/dist/constants/devicePreviews.d.ts.map +1 -0
  29. package/dist/constants/devicePreviews.js +43 -0
  30. package/dist/constants/devicePreviews.js.map +1 -0
  31. package/dist/constants/themeFonts.d.ts +94 -168
  32. package/dist/constants/themeFonts.d.ts.map +1 -1
  33. package/dist/constants/themeFonts.js +260 -140
  34. package/dist/constants/themeFonts.js.map +1 -1
  35. package/dist/endpoints/themeFonts.d.ts +5 -0
  36. package/dist/endpoints/themeFonts.d.ts.map +1 -0
  37. package/dist/endpoints/themeFonts.js +138 -0
  38. package/dist/endpoints/themeFonts.js.map +1 -0
  39. package/dist/extended-presets.d.ts +0 -4
  40. package/dist/extended-presets.d.ts.map +1 -1
  41. package/dist/extended-presets.js +1 -18
  42. package/dist/extended-presets.js.map +1 -1
  43. package/dist/extended-themes.d.ts.map +1 -1
  44. package/dist/extended-themes.js +428 -0
  45. package/dist/extended-themes.js.map +1 -1
  46. package/dist/fields/AccessibilityAuditField.d.ts +2 -0
  47. package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
  48. package/dist/fields/AccessibilityAuditField.js +303 -0
  49. package/dist/fields/AccessibilityAuditField.js.map +1 -0
  50. package/dist/fields/AppearancePreviewField.d.ts +2 -0
  51. package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
  52. package/dist/fields/AppearancePreviewField.js +826 -0
  53. package/dist/fields/AppearancePreviewField.js.map +1 -0
  54. package/dist/fields/ColorPickerField.d.ts.map +1 -1
  55. package/dist/fields/ColorPickerField.js +89 -85
  56. package/dist/fields/ColorPickerField.js.map +1 -1
  57. package/dist/fields/FontSelectField.css +155 -0
  58. package/dist/fields/FontSelectField.d.ts +8 -0
  59. package/dist/fields/FontSelectField.d.ts.map +1 -1
  60. package/dist/fields/FontSelectField.js +223 -188
  61. package/dist/fields/FontSelectField.js.map +1 -1
  62. package/dist/fields/PaletteGeneratorField.d.ts +7 -0
  63. package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
  64. package/dist/fields/PaletteGeneratorField.js +327 -0
  65. package/dist/fields/PaletteGeneratorField.js.map +1 -0
  66. package/dist/fields/StylePresetField.d.ts +3 -0
  67. package/dist/fields/StylePresetField.d.ts.map +1 -0
  68. package/dist/fields/StylePresetField.js +463 -0
  69. package/dist/fields/StylePresetField.js.map +1 -0
  70. package/dist/fields/ThemeColorPickerField.css +115 -35
  71. package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
  72. package/dist/fields/ThemeColorPickerField.js +204 -70
  73. package/dist/fields/ThemeColorPickerField.js.map +1 -1
  74. package/dist/fields/ThemeExportField.d.ts +6 -0
  75. package/dist/fields/ThemeExportField.d.ts.map +1 -0
  76. package/dist/fields/ThemeExportField.js +202 -0
  77. package/dist/fields/ThemeExportField.js.map +1 -0
  78. package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
  79. package/dist/fields/ThemePresetImportField.js +56 -25
  80. package/dist/fields/ThemePresetImportField.js.map +1 -1
  81. package/dist/fields/ThemePreviewField.css +444 -5
  82. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  83. package/dist/fields/ThemePreviewField.js +265 -170
  84. package/dist/fields/ThemePreviewField.js.map +1 -1
  85. package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
  86. package/dist/fields/ThemeTokenSelectField.js +278 -80
  87. package/dist/fields/ThemeTokenSelectField.js.map +1 -1
  88. package/dist/fields/colorFieldHelpers.d.ts +90 -0
  89. package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
  90. package/dist/fields/colorFieldHelpers.js +133 -0
  91. package/dist/fields/colorFieldHelpers.js.map +1 -0
  92. package/dist/fields/colorModeFields.d.ts +1 -1
  93. package/dist/fields/colorModeFields.d.ts.map +1 -1
  94. package/dist/fields/colorModeFields.js +49 -7
  95. package/dist/fields/colorModeFields.js.map +1 -1
  96. package/dist/fields/themeConfigurationField.d.ts +8 -1
  97. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  98. package/dist/fields/themeConfigurationField.js +1209 -228
  99. package/dist/fields/themeConfigurationField.js.map +1 -1
  100. package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
  101. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
  102. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
  103. package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
  104. package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
  105. package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
  106. package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
  107. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
  108. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
  109. package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
  110. package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
  111. package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
  112. package/dist/globals/ThemeSettings/config.js +76 -79
  113. package/dist/globals/ThemeSettings/config.js.map +1 -1
  114. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
  115. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
  116. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
  117. package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
  118. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
  119. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
  120. package/dist/hooks/useAppearanceSession.d.ts +50 -0
  121. package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
  122. package/dist/hooks/useAppearanceSession.js +91 -0
  123. package/dist/hooks/useAppearanceSession.js.map +1 -0
  124. package/dist/hooks/useNativeLivePreview.d.ts +15 -0
  125. package/dist/hooks/useNativeLivePreview.d.ts.map +1 -0
  126. package/dist/hooks/useNativeLivePreview.js +18 -0
  127. package/dist/hooks/useNativeLivePreview.js.map +1 -0
  128. package/dist/hooks/useThemeFonts.d.ts +10 -0
  129. package/dist/hooks/useThemeFonts.d.ts.map +1 -0
  130. package/dist/hooks/useThemeFonts.js +36 -0
  131. package/dist/hooks/useThemeFonts.js.map +1 -0
  132. package/dist/hooks/useThemeTranslations.d.ts +13 -0
  133. package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
  134. package/dist/hooks/useThemeTranslations.js +22 -0
  135. package/dist/hooks/useThemeTranslations.js.map +1 -0
  136. package/dist/i18n.d.ts +53 -0
  137. package/dist/i18n.d.ts.map +1 -0
  138. package/dist/i18n.js +53 -0
  139. package/dist/i18n.js.map +1 -0
  140. package/dist/index.d.ts +13 -4
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +59 -29
  143. package/dist/index.js.map +1 -1
  144. package/dist/next-fonts.d.ts +3 -0
  145. package/dist/next-fonts.d.ts.map +1 -0
  146. package/dist/next-fonts.js +360 -0
  147. package/dist/next-fonts.js.map +1 -0
  148. package/dist/payload-types.d.ts +34 -6
  149. package/dist/payload-types.d.ts.map +1 -1
  150. package/dist/payload-types.js.map +1 -1
  151. package/dist/presets.d.ts +14 -0
  152. package/dist/presets.d.ts.map +1 -1
  153. package/dist/presets.js.map +1 -1
  154. package/dist/providers/Theme/index.d.ts.map +1 -1
  155. package/dist/providers/Theme/index.js +75 -11
  156. package/dist/providers/Theme/index.js.map +1 -1
  157. package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
  158. package/dist/providers/Theme/themeConfig.js +104 -0
  159. package/dist/providers/Theme/themeConfig.js.map +1 -1
  160. package/dist/providers/Theme/types.d.ts +1 -1
  161. package/dist/providers/Theme/types.d.ts.map +1 -1
  162. package/dist/providers/Theme/types.js +10 -1
  163. package/dist/providers/Theme/types.js.map +1 -1
  164. package/dist/style-presets.d.ts +75 -0
  165. package/dist/style-presets.d.ts.map +1 -0
  166. package/dist/style-presets.js +738 -0
  167. package/dist/style-presets.js.map +1 -0
  168. package/dist/translations.d.ts +164 -1
  169. package/dist/translations.d.ts.map +1 -1
  170. package/dist/translations.js +320 -9
  171. package/dist/translations.js.map +1 -1
  172. package/dist/tweakcn-special-themes.d.ts.map +1 -1
  173. package/dist/tweakcn-special-themes.js +585 -9
  174. package/dist/tweakcn-special-themes.js.map +1 -1
  175. package/dist/types/css.d.js +3 -0
  176. package/dist/types/css.d.js.map +1 -0
  177. package/dist/types.d.ts +26 -0
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js +1 -1
  180. package/dist/types.js.map +1 -1
  181. package/dist/utilities/getThemeSettings.d.ts.map +1 -1
  182. package/dist/utilities/getThemeSettings.js +2 -14
  183. package/dist/utilities/getThemeSettings.js.map +1 -1
  184. package/dist/utils/contrast.d.ts +63 -0
  185. package/dist/utils/contrast.d.ts.map +1 -0
  186. package/dist/utils/contrast.js +158 -0
  187. package/dist/utils/contrast.js.map +1 -0
  188. package/dist/utils/exportTokens.d.ts +30 -0
  189. package/dist/utils/exportTokens.d.ts.map +1 -0
  190. package/dist/utils/exportTokens.js +183 -0
  191. package/dist/utils/exportTokens.js.map +1 -0
  192. package/dist/utils/generatePalette.d.ts +46 -0
  193. package/dist/utils/generatePalette.d.ts.map +1 -0
  194. package/dist/utils/generatePalette.js +127 -0
  195. package/dist/utils/generatePalette.js.map +1 -0
  196. package/dist/utils/generateThemeCSS.d.ts.map +1 -1
  197. package/dist/utils/generateThemeCSS.js +2 -40
  198. package/dist/utils/generateThemeCSS.js.map +1 -1
  199. package/dist/utils/livePreviewThemeRuntime.d.ts +22 -3
  200. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  201. package/dist/utils/livePreviewThemeRuntime.js +48 -15
  202. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  203. package/dist/utils/localizedText.d.ts +8 -0
  204. package/dist/utils/localizedText.d.ts.map +1 -0
  205. package/dist/utils/localizedText.js +21 -0
  206. package/dist/utils/localizedText.js.map +1 -0
  207. package/dist/utils/resolveThemeConfiguration.d.ts +60 -1
  208. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  209. package/dist/utils/resolveThemeConfiguration.js +53 -13
  210. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  211. package/dist/utils/themeColors.d.ts +3 -0
  212. package/dist/utils/themeColors.d.ts.map +1 -1
  213. package/dist/utils/themeColors.js +30 -1
  214. package/dist/utils/themeColors.js.map +1 -1
  215. package/dist/utils/themeHtmlAttributes.d.ts +32 -12
  216. package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
  217. package/dist/utils/themeHtmlAttributes.js +38 -5
  218. package/dist/utils/themeHtmlAttributes.js.map +1 -1
  219. package/dist/utils/themeUtils.d.ts +3 -5
  220. package/dist/utils/themeUtils.d.ts.map +1 -1
  221. package/dist/utils/themeUtils.js +474 -6
  222. package/dist/utils/themeUtils.js.map +1 -1
  223. package/dist/utils/tweakcnConverter.d.ts +1 -3
  224. package/dist/utils/tweakcnConverter.d.ts.map +1 -1
  225. package/dist/utils/tweakcnConverter.js +1 -3
  226. package/dist/utils/tweakcnConverter.js.map +1 -1
  227. package/package.json +15 -5
  228. package/dist/components/ThemePreview.d.ts +0 -3
  229. package/dist/components/ThemePreview.d.ts.map +0 -1
  230. package/dist/components/ThemePreview.js +0 -321
  231. package/dist/components/ThemePreview.js.map +0 -1
  232. package/dist/plugin.d.ts +0 -2
  233. package/dist/plugin.d.ts.map +0 -1
  234. package/dist/plugin.js +0 -3
  235. package/dist/plugin.js.map +0 -1
@@ -0,0 +1,155 @@
1
+ /* Font select, styled like Payload's own select (react-select `.rs__control`). */
2
+
3
+ .tm-font-select {
4
+ position: relative;
5
+ min-width: 0;
6
+ margin-bottom: var(--spacing-field, 1rem);
7
+ }
8
+
9
+ .tm-font-select__trigger {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ gap: 8px;
14
+ width: 100%;
15
+ min-height: 40px;
16
+ padding: 7px 12px;
17
+ border: 1px solid var(--theme-elevation-150);
18
+ border-radius: var(--style-radius-s);
19
+ background: var(--theme-input-bg);
20
+ color: var(--theme-elevation-800);
21
+ box-shadow: 0 2px 2px -1px #0000001a;
22
+ font-family: var(--font-body);
23
+ font-size: 1rem;
24
+ line-height: 20px;
25
+ text-align: left;
26
+ cursor: pointer;
27
+ transition: border-color 100ms ease;
28
+ }
29
+
30
+ .tm-font-select__trigger:hover {
31
+ border-color: var(--theme-elevation-250);
32
+ }
33
+
34
+ .tm-font-select__trigger:focus-visible {
35
+ outline: var(--accessibility-outline, 2px solid var(--theme-elevation-800));
36
+ outline-offset: var(--accessibility-outline-offset, 1px);
37
+ }
38
+
39
+ .tm-font-select__value {
40
+ flex: 1 1 auto;
41
+ min-width: 0;
42
+ overflow: hidden;
43
+ text-overflow: ellipsis;
44
+ white-space: nowrap;
45
+ }
46
+
47
+ .tm-font-select__chevron {
48
+ flex-shrink: 0;
49
+ color: var(--theme-elevation-500);
50
+ transition: transform 150ms ease;
51
+ }
52
+
53
+ .tm-font-select__chevron.is-open {
54
+ transform: rotate(180deg);
55
+ }
56
+
57
+ .tm-font-select__listbox {
58
+ position: absolute;
59
+ top: 100%;
60
+ left: 0;
61
+ z-index: 1000;
62
+ /* Wide enough for the sample line even when the field sits in a half-width column. */
63
+ width: max(100%, 300px);
64
+ max-width: calc(100vw - 32px);
65
+ max-height: 340px;
66
+ margin-top: 4px;
67
+ padding: 4px 0;
68
+ overflow-x: hidden;
69
+ overflow-y: auto;
70
+ overscroll-behavior: contain;
71
+ border: 1px solid var(--theme-elevation-150);
72
+ border-radius: var(--style-radius-m, 6px);
73
+ background: var(--theme-elevation-0);
74
+ box-shadow: 0 10px 28px -6px rgba(0, 0, 0, 0.22);
75
+ }
76
+
77
+ .tm-font-select__listbox:focus-visible {
78
+ outline: none;
79
+ }
80
+
81
+ .tm-font-select__group {
82
+ padding: 10px 12px 4px 36px;
83
+ font-family: var(--font-body);
84
+ font-size: 12px;
85
+ font-weight: 600;
86
+ color: var(--theme-elevation-500);
87
+ }
88
+
89
+ .tm-font-select__option {
90
+ display: flex;
91
+ align-items: flex-start;
92
+ gap: 8px;
93
+ padding: 7px 12px;
94
+ color: var(--theme-elevation-800);
95
+ cursor: pointer;
96
+ }
97
+
98
+ .tm-font-select__option.is-active {
99
+ background: var(--theme-elevation-100);
100
+ /* The keyboard position never relies on the fill alone. */
101
+ box-shadow: inset 2px 0 0 var(--theme-elevation-800);
102
+ }
103
+
104
+ .tm-font-select__check {
105
+ display: inline-flex;
106
+ flex: 0 0 16px;
107
+ justify-content: center;
108
+ padding-top: 3px;
109
+ color: var(--theme-elevation-800);
110
+ }
111
+
112
+ .tm-font-select__text {
113
+ display: grid;
114
+ gap: 1px;
115
+ min-width: 0;
116
+ flex: 1 1 auto;
117
+ }
118
+
119
+ .tm-font-select__name {
120
+ font-size: 15px;
121
+ line-height: 1.35;
122
+ white-space: nowrap;
123
+ overflow: hidden;
124
+ text-overflow: ellipsis;
125
+ }
126
+
127
+ .tm-font-select__option.is-selected .tm-font-select__name {
128
+ font-weight: 600;
129
+ }
130
+
131
+ .tm-font-select__sample {
132
+ font-size: 13px;
133
+ line-height: 1.4;
134
+ color: var(--theme-elevation-500);
135
+ white-space: nowrap;
136
+ overflow: hidden;
137
+ text-overflow: ellipsis;
138
+ }
139
+
140
+ .tm-font-select__sample.is-hint {
141
+ font-family: var(--font-body);
142
+ font-size: 12px;
143
+ }
144
+
145
+ @media (prefers-reduced-motion: reduce) {
146
+ .tm-font-select__chevron,
147
+ .tm-font-select__trigger {
148
+ transition: none;
149
+ }
150
+ }
151
+
152
+ .tm-font-select__listbox.is-align-end {
153
+ left: auto;
154
+ right: 0;
155
+ }
@@ -1,4 +1,12 @@
1
+ import './FontSelectField.css';
1
2
  import type { SelectFieldClientComponent } from 'payload';
3
+ /**
4
+ * Font select with live font previews. Implements the WAI-ARIA "select-only
5
+ * combobox" pattern: a button trigger plus a listbox navigated with the arrow
6
+ * keys, Home/End, Enter/Space and Escape. Options are grouped by font category.
7
+ * Preview fonts come from the plugin's self-hosted font endpoint: the selected
8
+ * face always, every face while the list is open.
9
+ */
2
10
  declare const FontSelectField: SelectFieldClientComponent;
3
11
  export default FontSelectField;
4
12
  //# sourceMappingURL=FontSelectField.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FontSelectField.d.ts","sourceRoot":"","sources":["../../src/fields/FontSelectField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AAczD,QAAA,MAAM,eAAe,EAAE,0BAyOtB,CAAA;AAED,eAAe,eAAe,CAAA"}
1
+ {"version":3,"file":"FontSelectField.d.ts","sourceRoot":"","sources":["../../src/fields/FontSelectField.tsx"],"names":[],"mappings":"AAEA,OAAO,uBAAuB,CAAA;AAI9B,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,SAAS,CAAA;AA4BzD;;;;;;GAMG;AACH,QAAA,MAAM,eAAe,EAAE,0BAyOtB,CAAA;AAED,eAAe,eAAe,CAAA"}
@@ -1,226 +1,261 @@
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';
4
- import { useEffect, useState } from 'react';
5
- import { getTranslations } from '../translations.js';
6
- import { getAdminLanguage } from '../utils/getAdminLanguage.js';
7
- const FontSelectField = ({ field, path })=>{
5
+ import { mergeFieldStyles } from '@payloadcms/ui/shared';
6
+ import { Check, ChevronDown } from 'lucide-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';
10
+ import { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js';
11
+ import { resolveLocalizedText } from '../utils/localizedText.js';
12
+ function previewFamily(option) {
13
+ if (option.fontFamily) return option.fontFamily;
14
+ if (option.category === 'sans-serif') return 'system-ui, sans-serif';
15
+ if (option.category === 'serif') return 'Georgia, serif';
16
+ if (option.category === 'monospace') return 'monospace';
17
+ return 'inherit';
18
+ }
19
+ /**
20
+ * Font select with live font previews. Implements the WAI-ARIA "select-only
21
+ * combobox" pattern: a button trigger plus a listbox navigated with the arrow
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
+ */ const FontSelectField = ({ field, path })=>{
8
26
  const { value, setValue } = useField({
9
27
  path
10
28
  });
11
29
  const [isOpen, setIsOpen] = useState(false);
12
- const options = field.options || [];
13
- const selectedOption = options.find((opt)=>opt.value === value);
14
- const [loadedFonts, setLoadedFonts] = useState(new Set());
15
- const getOptionLabel = (opt)=>{
16
- if (typeof opt.label === 'string') return opt.label;
17
- const lang = getAdminLanguage();
18
- return opt.label[lang] || opt.label.en || opt.label.cs || 'Font';
19
- };
20
- const ensureFontLoaded = (fontValue)=>{
21
- if (!fontValue || [
22
- 'preset',
23
- 'custom',
24
- 'system-ui'
25
- ].includes(fontValue)) return;
26
- // Avoid duplicate loads
27
- if (loadedFonts.has(fontValue)) return;
28
- if (typeof document === 'undefined') return;
29
- const linkId = `font-${fontValue.replace(/\s+/g, '-')}`;
30
- if (document.getElementById(linkId)) {
31
- setLoadedFonts((prev)=>new Set(prev).add(fontValue));
32
- return;
33
- }
34
- const weights = '400;700' // reasonable default weights
35
- ;
36
- const fontUrl = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(fontValue)}:wght@${weights}&display=swap`;
37
- const link = document.createElement('link');
38
- link.id = linkId;
39
- link.rel = 'stylesheet';
40
- link.href = fontUrl;
41
- link.onload = ()=>setLoadedFonts((prev)=>new Set(prev).add(fontValue));
42
- document.head.appendChild(link);
43
- };
44
- const handleSelect = (optionValue)=>{
45
- setValue(optionValue);
30
+ const [activeIndex, setActiveIndex] = useState(-1);
31
+ const lang = useThemeLanguage();
32
+ const t = useThemeTranslations();
33
+ const baseId = useId();
34
+ const labelId = `${baseId}-label`;
35
+ const listboxId = `${baseId}-listbox`;
36
+ const containerRef = useRef(null);
37
+ const triggerRef = useRef(null);
38
+ const listboxRef = useRef(null);
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);
50
+ const selectedIndex = options.findIndex((opt)=>opt.value === value);
51
+ const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : undefined;
52
+ const getOptionLabel = (opt)=>resolveLocalizedText(opt.label, lang, 'Font');
53
+ const label = resolveLocalizedText(field.label, lang, 'Font');
54
+ const optionId = (index)=>`${baseId}-option-${index}`;
55
+ useThemeFonts(isOpen ? options.map((option)=>option.value) : [
56
+ value
57
+ ]);
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));
63
+ setActiveIndex(index);
64
+ setIsOpen(true);
65
+ }, [
66
+ selectedIndex
67
+ ]);
68
+ const close = useCallback((restoreFocus)=>{
46
69
  setIsOpen(false);
70
+ if (restoreFocus) triggerRef.current?.focus();
71
+ }, []);
72
+ const handleSelect = (index)=>{
73
+ const option = options[index];
74
+ if (!option) return;
75
+ setValue(option.value);
76
+ close(true);
47
77
  };
78
+ // Keep the active option scrolled into view.
48
79
  useEffect(()=>{
49
- const handleClickOutside = (e)=>{
50
- const target = e.target;
51
- if (!target.closest('.font-select-container')) {
52
- setIsOpen(false);
53
- }
54
- };
55
- if (isOpen) {
56
- document.addEventListener('mousedown', handleClickOutside);
57
- return ()=>document.removeEventListener('mousedown', handleClickOutside);
58
- }
80
+ if (!isOpen || activeIndex < 0) return;
81
+ listboxRef.current?.querySelector(`#${CSS.escape(optionId(activeIndex))}`)?.scrollIntoView({
82
+ block: 'nearest'
83
+ });
84
+ }, [
85
+ isOpen,
86
+ activeIndex
87
+ ]);
88
+ // Focus the listbox when it opens so keyboard navigation works immediately.
89
+ useEffect(()=>{
90
+ if (isOpen) listboxRef.current?.focus();
59
91
  }, [
60
92
  isOpen
61
93
  ]);
62
- const t = getTranslations(getAdminLanguage());
63
- const label = typeof field.label === 'string' ? field.label : typeof field.label === 'object' ? field.label?.en || field.label?.cs : field.label || 'Font';
94
+ useEffect(()=>{
95
+ if (!isOpen) return;
96
+ const handlePointerOutside = (e)=>{
97
+ if (!containerRef.current?.contains(e.target)) close(false);
98
+ };
99
+ document.addEventListener('pointerdown', handlePointerOutside);
100
+ return ()=>document.removeEventListener('pointerdown', handlePointerOutside);
101
+ }, [
102
+ isOpen,
103
+ close
104
+ ]);
105
+ const handleTriggerKeyDown = (event)=>{
106
+ if ([
107
+ 'ArrowDown',
108
+ 'ArrowUp',
109
+ 'Enter',
110
+ ' '
111
+ ].includes(event.key)) {
112
+ event.preventDefault();
113
+ open(event.key === 'ArrowUp' && selectedIndex < 0 ? options.length - 1 : undefined);
114
+ }
115
+ };
116
+ const handleListboxKeyDown = (event)=>{
117
+ const last = options.length - 1;
118
+ switch(event.key){
119
+ case 'ArrowDown':
120
+ event.preventDefault();
121
+ setActiveIndex((i)=>Math.min(last, i + 1));
122
+ break;
123
+ case 'ArrowUp':
124
+ event.preventDefault();
125
+ setActiveIndex((i)=>Math.max(0, i - 1));
126
+ break;
127
+ case 'Home':
128
+ event.preventDefault();
129
+ setActiveIndex(0);
130
+ break;
131
+ case 'End':
132
+ event.preventDefault();
133
+ setActiveIndex(last);
134
+ break;
135
+ case 'Enter':
136
+ case ' ':
137
+ event.preventDefault();
138
+ handleSelect(activeIndex);
139
+ break;
140
+ case 'Escape':
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ close(true);
144
+ break;
145
+ case 'Tab':
146
+ close(false);
147
+ break;
148
+ default:
149
+ // Type-ahead: jump to the first option starting with the typed letter.
150
+ if (event.key.length === 1 && /\S/.test(event.key)) {
151
+ const letter = event.key.toLowerCase();
152
+ const start = activeIndex + 1;
153
+ const ordered = [
154
+ ...options.slice(start),
155
+ ...options.slice(0, start)
156
+ ];
157
+ const match = ordered.find((opt)=>getOptionLabel(opt).toLowerCase().startsWith(letter));
158
+ if (match) setActiveIndex(options.indexOf(match));
159
+ }
160
+ }
161
+ };
162
+ const isSpecial = (option)=>option.value === 'preset' || option.value === 'custom';
64
163
  return /*#__PURE__*/ _jsxs("div", {
65
- className: "font-select-container",
66
- style: {
67
- position: 'relative',
68
- marginBottom: '12px'
69
- },
164
+ ref: containerRef,
165
+ // Payload rows size each field through --field-width on the field root.
166
+ className: "field-type tm-font-select",
167
+ style: mergeFieldStyles(field),
70
168
  children: [
71
- /*#__PURE__*/ _jsx("label", {
72
- style: {
73
- display: 'block',
74
- marginBottom: '6px',
75
- fontSize: '13px',
76
- fontWeight: 600,
77
- color: 'var(--theme-elevation-800)'
78
- },
169
+ /*#__PURE__*/ _jsx("div", {
170
+ id: labelId,
171
+ className: "field-label tm-font-select__label",
79
172
  children: label
80
173
  }),
81
174
  /*#__PURE__*/ _jsxs("button", {
175
+ ref: triggerRef,
82
176
  type: "button",
83
- onClick: ()=>setIsOpen(!isOpen),
84
- style: {
85
- width: '300px',
86
- padding: '10px 14px',
87
- borderRadius: '6px',
88
- border: '1px solid var(--theme-elevation-200)',
89
- backgroundColor: 'var(--theme-input-bg)',
90
- color: 'var(--theme-elevation-800)',
91
- textAlign: 'left',
92
- cursor: 'pointer',
93
- display: 'flex',
94
- justifyContent: 'space-between',
95
- alignItems: 'center',
96
- fontSize: '14px',
97
- fontFamily: selectedOption?.fontFamily || 'inherit',
98
- transition: 'border-color 0.2s ease'
99
- },
100
- onMouseEnter: (e)=>{
101
- e.currentTarget.style.borderColor = 'var(--theme-elevation-400)';
102
- },
103
- onMouseLeave: (e)=>{
104
- e.currentTarget.style.borderColor = 'var(--theme-elevation-200)';
105
- },
177
+ className: "tm-font-select__trigger",
178
+ role: "combobox",
179
+ "aria-haspopup": "listbox",
180
+ "aria-expanded": isOpen,
181
+ "aria-controls": isOpen ? listboxId : undefined,
182
+ "aria-labelledby": `${labelId} ${baseId}-value`,
183
+ onClick: ()=>isOpen ? close(false) : open(),
184
+ onKeyDown: handleTriggerKeyDown,
106
185
  children: [
107
186
  /*#__PURE__*/ _jsx("span", {
187
+ id: `${baseId}-value`,
188
+ className: "tm-font-select__value",
108
189
  style: {
109
- flex: 1
190
+ fontFamily: selectedOption && !isSpecial(selectedOption) ? previewFamily(selectedOption) : undefined
110
191
  },
111
192
  children: selectedOption ? getOptionLabel(selectedOption) : t.ui.selectFontPlaceholder
112
193
  }),
113
- ' ',
114
- /*#__PURE__*/ _jsx("svg", {
115
- width: "12",
116
- height: "12",
117
- viewBox: "0 0 12 12",
118
- fill: "none",
119
- style: {
120
- transition: 'transform 0.2s ease',
121
- transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)'
122
- },
123
- children: /*#__PURE__*/ _jsx("path", {
124
- d: "M3 4.5L6 7.5L9 4.5",
125
- stroke: "currentColor",
126
- strokeWidth: "1.5",
127
- strokeLinecap: "round",
128
- strokeLinejoin: "round"
129
- })
194
+ /*#__PURE__*/ _jsx(ChevronDown, {
195
+ size: 16,
196
+ "aria-hidden": true,
197
+ className: `tm-font-select__chevron${isOpen ? ' is-open' : ''}`
130
198
  })
131
199
  ]
132
200
  }),
133
201
  isOpen && /*#__PURE__*/ _jsx("div", {
134
- style: {
135
- position: 'absolute',
136
- top: '100%',
137
- left: 0,
138
- right: 0,
139
- marginTop: '4px',
140
- maxHeight: '280px',
141
- overflowY: 'auto',
142
- backgroundColor: 'var(--theme-elevation-0)',
143
- border: '1px solid var(--theme-elevation-200)',
144
- borderRadius: '8px',
145
- boxShadow: '0 8px 24px rgba(15, 23, 42, 0.15)',
146
- zIndex: 1000
147
- },
148
- children: options.map((option)=>{
202
+ ref: listboxRef,
203
+ id: listboxId,
204
+ role: "listbox",
205
+ tabIndex: -1,
206
+ "aria-labelledby": labelId,
207
+ "aria-activedescendant": activeIndex >= 0 ? optionId(activeIndex) : undefined,
208
+ onKeyDown: handleListboxKeyDown,
209
+ className: `tm-font-select__listbox${alignEnd ? ' is-align-end' : ''}`,
210
+ children: options.map((option, index)=>{
149
211
  const isSelected = option.value === value;
150
- let fontFamily = option.fontFamily;
151
- if (!fontFamily) {
152
- if (option.category === 'sans-serif') fontFamily = 'system-ui, sans-serif';
153
- else if (option.category === 'serif') fontFamily = 'Georgia, serif';
154
- else if (option.category === 'monospace') fontFamily = 'monospace';
155
- else fontFamily = 'inherit';
156
- }
157
- return /*#__PURE__*/ _jsxs("button", {
158
- type: "button",
159
- onClick: ()=>handleSelect(option.value),
160
- style: {
161
- width: '250px',
162
- padding: '12px 14px',
163
- border: 'none',
164
- backgroundColor: isSelected ? 'var(--theme-elevation-100)' : 'transparent',
165
- color: 'var(--theme-elevation-800)',
166
- textAlign: 'left',
167
- cursor: 'pointer',
168
- fontSize: '14px',
169
- fontFamily,
170
- display: 'flex',
171
- flexDirection: 'column',
172
- gap: '4px',
173
- transition: 'background-color 0.15s ease'
174
- },
175
- onMouseEnter: (e)=>{
176
- if (!isSelected) {
177
- e.currentTarget.style.backgroundColor = 'var(--theme-elevation-50)';
178
- }
179
- // Lazy-load a preview font when hovering
180
- ensureFontLoaded(option.value);
181
- },
182
- onMouseLeave: (e)=>{
183
- if (!isSelected) {
184
- e.currentTarget.style.backgroundColor = 'transparent';
185
- }
186
- },
212
+ const isActive = index === activeIndex;
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, {
187
217
  children: [
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,
188
223
  /*#__PURE__*/ _jsxs("div", {
189
- style: {
190
- fontWeight: isSelected ? 600 : 500,
191
- display: 'flex',
192
- alignItems: 'center',
193
- gap: '8px'
194
- },
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' : ''}`,
195
230
  children: [
196
- isSelected && /*#__PURE__*/ _jsx("svg", {
197
- width: "14",
198
- height: "14",
199
- viewBox: "0 0 14 14",
200
- fill: "none",
201
- children: /*#__PURE__*/ _jsx("path", {
202
- d: "M11.6667 3.5L5.25 9.91667L2.33334 7",
203
- stroke: "var(--theme-elevation-500)",
204
- strokeWidth: "2",
205
- strokeLinecap: "round",
206
- strokeLinejoin: "round"
207
- })
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
208
237
  }),
209
- 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
+ })
210
258
  ]
211
- }),
212
- /*#__PURE__*/ _jsx("div", {
213
- style: {
214
- fontSize: '16px',
215
- color: 'var(--theme-elevation-600)',
216
- fontStyle: option.value === 'preset' || option.value === 'custom' ? 'italic' : 'normal',
217
- fontFamily: option.fontFamily || undefined
218
- },
219
- children: (()=>{
220
- if (option.value === 'preset') return t.ui.usePreset;
221
- if (option.value === 'custom') return t.ui.specifyCustom;
222
- return t.ui.sampleSentence;
223
- })()
224
259
  })
225
260
  ]
226
261
  }, option.value);