@kilivi/payloadcms-theme-management 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +318 -847
  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.map +1 -1
  12. package/dist/components/ServerThemeInjector.js +41 -14
  13. package/dist/components/ServerThemeInjector.js.map +1 -1
  14. package/dist/components/ThemeLivePreviewSync.d.ts +1 -0
  15. package/dist/components/ThemeLivePreviewSync.d.ts.map +1 -1
  16. package/dist/components/ThemeLivePreviewSync.js +1 -1
  17. package/dist/components/ThemeLivePreviewSync.js.map +1 -1
  18. package/dist/components/ThemeSwitcher.d.ts +36 -0
  19. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  20. package/dist/components/ThemeSwitcher.js +89 -0
  21. package/dist/components/ThemeSwitcher.js.map +1 -0
  22. package/dist/components/typographyPreviewUtils.d.ts.map +1 -1
  23. package/dist/components/typographyPreviewUtils.js +8 -1
  24. package/dist/components/typographyPreviewUtils.js.map +1 -1
  25. package/dist/constants/devicePreviews.d.ts +35 -0
  26. package/dist/constants/devicePreviews.d.ts.map +1 -0
  27. package/dist/constants/devicePreviews.js +43 -0
  28. package/dist/constants/devicePreviews.js.map +1 -0
  29. package/dist/constants/themeFonts.js +9 -9
  30. package/dist/constants/themeFonts.js.map +1 -1
  31. package/dist/extended-presets.d.ts +0 -4
  32. package/dist/extended-presets.d.ts.map +1 -1
  33. package/dist/extended-presets.js +1 -18
  34. package/dist/extended-presets.js.map +1 -1
  35. package/dist/extended-themes.d.ts.map +1 -1
  36. package/dist/extended-themes.js +428 -0
  37. package/dist/extended-themes.js.map +1 -1
  38. package/dist/fields/AccessibilityAuditField.d.ts +2 -0
  39. package/dist/fields/AccessibilityAuditField.d.ts.map +1 -0
  40. package/dist/fields/AccessibilityAuditField.js +303 -0
  41. package/dist/fields/AccessibilityAuditField.js.map +1 -0
  42. package/dist/fields/AppearancePreviewField.d.ts +2 -0
  43. package/dist/fields/AppearancePreviewField.d.ts.map +1 -0
  44. package/dist/fields/AppearancePreviewField.js +763 -0
  45. package/dist/fields/AppearancePreviewField.js.map +1 -0
  46. package/dist/fields/ColorPickerField.d.ts.map +1 -1
  47. package/dist/fields/ColorPickerField.js +89 -85
  48. package/dist/fields/ColorPickerField.js.map +1 -1
  49. package/dist/fields/FontSelectField.d.ts +6 -0
  50. package/dist/fields/FontSelectField.d.ts.map +1 -1
  51. package/dist/fields/FontSelectField.js +195 -122
  52. package/dist/fields/FontSelectField.js.map +1 -1
  53. package/dist/fields/PaletteGeneratorField.d.ts +7 -0
  54. package/dist/fields/PaletteGeneratorField.d.ts.map +1 -0
  55. package/dist/fields/PaletteGeneratorField.js +327 -0
  56. package/dist/fields/PaletteGeneratorField.js.map +1 -0
  57. package/dist/fields/StylePresetField.d.ts +3 -0
  58. package/dist/fields/StylePresetField.d.ts.map +1 -0
  59. package/dist/fields/StylePresetField.js +462 -0
  60. package/dist/fields/StylePresetField.js.map +1 -0
  61. package/dist/fields/ThemeColorPickerField.css +115 -35
  62. package/dist/fields/ThemeColorPickerField.d.ts.map +1 -1
  63. package/dist/fields/ThemeColorPickerField.js +204 -70
  64. package/dist/fields/ThemeColorPickerField.js.map +1 -1
  65. package/dist/fields/ThemeExportField.d.ts +6 -0
  66. package/dist/fields/ThemeExportField.d.ts.map +1 -0
  67. package/dist/fields/ThemeExportField.js +202 -0
  68. package/dist/fields/ThemeExportField.js.map +1 -0
  69. package/dist/fields/ThemePresetImportField.d.ts.map +1 -1
  70. package/dist/fields/ThemePresetImportField.js +56 -25
  71. package/dist/fields/ThemePresetImportField.js.map +1 -1
  72. package/dist/fields/ThemePreviewField.css +382 -5
  73. package/dist/fields/ThemePreviewField.d.ts.map +1 -1
  74. package/dist/fields/ThemePreviewField.js +268 -170
  75. package/dist/fields/ThemePreviewField.js.map +1 -1
  76. package/dist/fields/ThemeTokenSelectField.d.ts.map +1 -1
  77. package/dist/fields/ThemeTokenSelectField.js +278 -80
  78. package/dist/fields/ThemeTokenSelectField.js.map +1 -1
  79. package/dist/fields/colorFieldHelpers.d.ts +90 -0
  80. package/dist/fields/colorFieldHelpers.d.ts.map +1 -0
  81. package/dist/fields/colorFieldHelpers.js +133 -0
  82. package/dist/fields/colorFieldHelpers.js.map +1 -0
  83. package/dist/fields/colorModeFields.d.ts +1 -1
  84. package/dist/fields/colorModeFields.d.ts.map +1 -1
  85. package/dist/fields/colorModeFields.js +49 -7
  86. package/dist/fields/colorModeFields.js.map +1 -1
  87. package/dist/fields/themeConfigurationField.d.ts +8 -1
  88. package/dist/fields/themeConfigurationField.d.ts.map +1 -1
  89. package/dist/fields/themeConfigurationField.js +961 -29
  90. package/dist/fields/themeConfigurationField.js.map +1 -1
  91. package/dist/globals/ThemeSettings/FontPicker/FontPreview.d.ts.map +1 -1
  92. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js +8 -4
  93. package/dist/globals/ThemeSettings/FontPicker/FontPreview.js.map +1 -1
  94. package/dist/globals/ThemeSettings/FontPicker/index.d.ts.map +1 -1
  95. package/dist/globals/ThemeSettings/FontPicker/index.js +81 -26
  96. package/dist/globals/ThemeSettings/FontPicker/index.js.map +1 -1
  97. package/dist/globals/ThemeSettings/FontPicker/styles.css +98 -41
  98. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts +6 -0
  99. package/dist/globals/ThemeSettings/FontPicker/translations.d.ts.map +1 -1
  100. package/dist/globals/ThemeSettings/FontPicker/translations.js +14 -2
  101. package/dist/globals/ThemeSettings/FontPicker/translations.js.map +1 -1
  102. package/dist/globals/ThemeSettings/config.d.ts.map +1 -1
  103. package/dist/globals/ThemeSettings/config.js +76 -79
  104. package/dist/globals/ThemeSettings/config.js.map +1 -1
  105. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.d.ts.map +1 -1
  106. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js +63 -62
  107. package/dist/globals/ThemeSettings/fields/FontLoadingGuideField.js.map +1 -1
  108. package/dist/globals/ThemeSettings/fields/TypographyInfoField.d.ts.map +1 -1
  109. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js +22 -25
  110. package/dist/globals/ThemeSettings/fields/TypographyInfoField.js.map +1 -1
  111. package/dist/hooks/useAppearanceSession.d.ts +50 -0
  112. package/dist/hooks/useAppearanceSession.d.ts.map +1 -0
  113. package/dist/hooks/useAppearanceSession.js +91 -0
  114. package/dist/hooks/useAppearanceSession.js.map +1 -0
  115. package/dist/hooks/useThemeTranslations.d.ts +13 -0
  116. package/dist/hooks/useThemeTranslations.d.ts.map +1 -0
  117. package/dist/hooks/useThemeTranslations.js +22 -0
  118. package/dist/hooks/useThemeTranslations.js.map +1 -0
  119. package/dist/i18n.d.ts +53 -0
  120. package/dist/i18n.d.ts.map +1 -0
  121. package/dist/i18n.js +53 -0
  122. package/dist/i18n.js.map +1 -0
  123. package/dist/index.d.ts +10 -4
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +50 -29
  126. package/dist/index.js.map +1 -1
  127. package/dist/payload-types.d.ts +24 -0
  128. package/dist/payload-types.d.ts.map +1 -1
  129. package/dist/payload-types.js.map +1 -1
  130. package/dist/presets.d.ts +8 -0
  131. package/dist/presets.d.ts.map +1 -1
  132. package/dist/presets.js.map +1 -1
  133. package/dist/providers/Theme/index.d.ts.map +1 -1
  134. package/dist/providers/Theme/index.js +75 -11
  135. package/dist/providers/Theme/index.js.map +1 -1
  136. package/dist/providers/Theme/themeConfig.d.ts.map +1 -1
  137. package/dist/providers/Theme/themeConfig.js +104 -0
  138. package/dist/providers/Theme/themeConfig.js.map +1 -1
  139. package/dist/providers/Theme/types.d.ts +1 -1
  140. package/dist/providers/Theme/types.d.ts.map +1 -1
  141. package/dist/providers/Theme/types.js +10 -1
  142. package/dist/providers/Theme/types.js.map +1 -1
  143. package/dist/style-presets.d.ts +66 -0
  144. package/dist/style-presets.d.ts.map +1 -0
  145. package/dist/style-presets.js +556 -0
  146. package/dist/style-presets.js.map +1 -0
  147. package/dist/translations.d.ts +159 -1
  148. package/dist/translations.d.ts.map +1 -1
  149. package/dist/translations.js +310 -9
  150. package/dist/translations.js.map +1 -1
  151. package/dist/tweakcn-special-themes.d.ts.map +1 -1
  152. package/dist/tweakcn-special-themes.js +585 -9
  153. package/dist/tweakcn-special-themes.js.map +1 -1
  154. package/dist/types/css.d.js +3 -0
  155. package/dist/types/css.d.js.map +1 -0
  156. package/dist/types.d.ts +26 -0
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/types.js +1 -1
  159. package/dist/types.js.map +1 -1
  160. package/dist/utilities/getThemeSettings.d.ts.map +1 -1
  161. package/dist/utilities/getThemeSettings.js +2 -14
  162. package/dist/utilities/getThemeSettings.js.map +1 -1
  163. package/dist/utils/contrast.d.ts +63 -0
  164. package/dist/utils/contrast.d.ts.map +1 -0
  165. package/dist/utils/contrast.js +158 -0
  166. package/dist/utils/contrast.js.map +1 -0
  167. package/dist/utils/exportTokens.d.ts +30 -0
  168. package/dist/utils/exportTokens.d.ts.map +1 -0
  169. package/dist/utils/exportTokens.js +183 -0
  170. package/dist/utils/exportTokens.js.map +1 -0
  171. package/dist/utils/generatePalette.d.ts +46 -0
  172. package/dist/utils/generatePalette.d.ts.map +1 -0
  173. package/dist/utils/generatePalette.js +127 -0
  174. package/dist/utils/generatePalette.js.map +1 -0
  175. package/dist/utils/generateThemeCSS.d.ts.map +1 -1
  176. package/dist/utils/generateThemeCSS.js +2 -40
  177. package/dist/utils/generateThemeCSS.js.map +1 -1
  178. package/dist/utils/livePreviewThemeRuntime.d.ts +16 -2
  179. package/dist/utils/livePreviewThemeRuntime.d.ts.map +1 -1
  180. package/dist/utils/livePreviewThemeRuntime.js +37 -14
  181. package/dist/utils/livePreviewThemeRuntime.js.map +1 -1
  182. package/dist/utils/localizedText.d.ts +8 -0
  183. package/dist/utils/localizedText.d.ts.map +1 -0
  184. package/dist/utils/localizedText.js +21 -0
  185. package/dist/utils/localizedText.js.map +1 -0
  186. package/dist/utils/resolveThemeConfiguration.d.ts +45 -1
  187. package/dist/utils/resolveThemeConfiguration.d.ts.map +1 -1
  188. package/dist/utils/resolveThemeConfiguration.js +12 -5
  189. package/dist/utils/resolveThemeConfiguration.js.map +1 -1
  190. package/dist/utils/themeColors.d.ts +3 -0
  191. package/dist/utils/themeColors.d.ts.map +1 -1
  192. package/dist/utils/themeColors.js +30 -1
  193. package/dist/utils/themeColors.js.map +1 -1
  194. package/dist/utils/themeHtmlAttributes.d.ts +32 -12
  195. package/dist/utils/themeHtmlAttributes.d.ts.map +1 -1
  196. package/dist/utils/themeHtmlAttributes.js +38 -5
  197. package/dist/utils/themeHtmlAttributes.js.map +1 -1
  198. package/dist/utils/themeUtils.d.ts +3 -5
  199. package/dist/utils/themeUtils.d.ts.map +1 -1
  200. package/dist/utils/themeUtils.js +416 -6
  201. package/dist/utils/themeUtils.js.map +1 -1
  202. package/dist/utils/tweakcnConverter.d.ts +1 -3
  203. package/dist/utils/tweakcnConverter.d.ts.map +1 -1
  204. package/dist/utils/tweakcnConverter.js +1 -3
  205. package/dist/utils/tweakcnConverter.js.map +1 -1
  206. package/package.json +11 -5
  207. package/dist/components/ThemePreview.d.ts +0 -3
  208. package/dist/components/ThemePreview.d.ts.map +0 -1
  209. package/dist/components/ThemePreview.js +0 -321
  210. package/dist/components/ThemePreview.js.map +0 -1
  211. package/dist/plugin.d.ts +0 -2
  212. package/dist/plugin.d.ts.map +0 -1
  213. package/dist/plugin.js +0 -3
  214. package/dist/plugin.js.map +0 -1
@@ -19,7 +19,7 @@
19
19
 
20
20
  .theme-color-picker-field .field-description {
21
21
  font-size: 0.75rem;
22
- color: var(--theme-elevation-400, #94a3b8);
22
+ color: var(--theme-elevation-600, #475569);
23
23
  margin-top: 0.25rem;
24
24
  line-height: 1.4;
25
25
  }
@@ -42,7 +42,7 @@
42
42
  border: 2px solid var(--theme-elevation-200, #e2e8f0);
43
43
  border-radius: 8px;
44
44
  cursor: pointer;
45
- transition: all 0.2s ease;
45
+ transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out, transform 0.15s ease-out;
46
46
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
47
47
  flex-shrink: 0;
48
48
  }
@@ -53,9 +53,9 @@
53
53
  transform: scale(1.05);
54
54
  }
55
55
 
56
- .theme-color-picker-field .color-swatch-btn:focus {
57
- outline: 2px solid var(--theme-elevation-500, #3b82f6);
58
- outline-offset: 2px;
56
+ .theme-color-picker-field .color-swatch-btn:focus-visible {
57
+ outline: var(--accessibility-outline, 2px solid var(--theme-text));
58
+ outline-offset: var(--accessibility-outline-offset, 2px);
59
59
  }
60
60
 
61
61
  /* Text Input */
@@ -67,23 +67,85 @@
67
67
  font-size: 0.875rem;
68
68
  font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
69
69
  color: var(--theme-text, #1e293b);
70
- background: var(--theme-elevation-0, #ffffff);
71
- transition: all 0.2s ease;
70
+ background: var(--theme-input-bg, var(--theme-elevation-0));
71
+ transition: border-color 0.15s ease-out;
72
72
  }
73
73
 
74
74
  .theme-color-picker-field .color-text-input:hover {
75
75
  border-color: var(--theme-elevation-300, #cbd5e1);
76
76
  }
77
77
 
78
- .theme-color-picker-field .color-text-input:focus {
79
- outline: none;
80
- border-color: var(--theme-elevation-500, #3b82f6);
81
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
78
+ .theme-color-picker-field .color-text-input:focus-visible {
79
+ outline: var(--accessibility-outline, 2px solid var(--theme-text));
80
+ outline-offset: 1px;
81
+ border-color: var(--theme-elevation-500);
82
82
  }
83
83
 
84
84
  .theme-color-picker-field .color-text-input::placeholder {
85
- color: var(--theme-elevation-400, #94a3b8);
86
- opacity: 0.7;
85
+ color: var(--theme-elevation-500);
86
+ }
87
+
88
+ /* WCAG Contrast Badges */
89
+ .theme-color-picker-field .color-contrast-row {
90
+ display: flex;
91
+ flex-wrap: wrap;
92
+ gap: 0.5rem;
93
+ margin-top: 0.5rem;
94
+ }
95
+
96
+ .theme-color-picker-field .contrast-chip {
97
+ display: inline-flex;
98
+ align-items: stretch;
99
+ border-radius: 6px;
100
+ overflow: hidden;
101
+ border: 1px solid var(--theme-elevation-150, #f1f5f9);
102
+ font-size: 0.75rem;
103
+ line-height: 1;
104
+ }
105
+
106
+ .theme-color-picker-field .contrast-sample {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ width: 26px;
111
+ font-weight: 700;
112
+ border-inline-end: 1px solid var(--theme-elevation-150);
113
+ }
114
+
115
+ .theme-color-picker-field .contrast-ratio {
116
+ display: inline-flex;
117
+ align-items: center;
118
+ padding: 0.25rem 0.5rem;
119
+ font-weight: 600;
120
+ white-space: nowrap;
121
+ }
122
+
123
+ /* Semantic status colours come from Payload, so they flip with the admin theme. */
124
+ .theme-color-picker-field .contrast-ratio--aa {
125
+ background: var(--theme-success-100);
126
+ color: var(--theme-success-800);
127
+ }
128
+
129
+ .theme-color-picker-field .contrast-ratio--aaLarge {
130
+ background: var(--theme-warning-100);
131
+ color: var(--theme-warning-800);
132
+ }
133
+
134
+ .theme-color-picker-field .contrast-ratio--low {
135
+ background: var(--theme-error-100);
136
+ color: var(--theme-error-800);
137
+ }
138
+
139
+ .theme-color-picker-field .sr-only {
140
+ position: absolute;
141
+ width: 1px;
142
+ height: 1px;
143
+ padding: 0;
144
+ margin: -1px;
145
+ overflow: hidden;
146
+ clip: rect(0, 0, 0, 0);
147
+ white-space: nowrap;
148
+ border: 0;
87
149
  }
88
150
 
89
151
  /* Color Picker Popover */
@@ -97,8 +159,8 @@
97
159
  border-radius: 12px;
98
160
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.1);
99
161
  z-index: 1000;
100
- width: 280px;
101
- animation: slideDown 0.2s ease;
162
+ width: min(280px, calc(100vw - 2rem));
163
+ animation: slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1);
102
164
  }
103
165
 
104
166
  @keyframes slideDown {
@@ -132,16 +194,19 @@
132
194
  display: flex;
133
195
  align-items: center;
134
196
  justify-content: center;
135
- width: 24px;
136
- height: 24px;
197
+ width: 32px;
198
+ height: 32px;
137
199
  border: none;
138
200
  background: transparent;
139
- color: var(--theme-elevation-400, #94a3b8);
201
+ color: var(--theme-elevation-600);
140
202
  cursor: pointer;
141
- border-radius: 4px;
142
- font-size: 1.125rem;
143
- line-height: 1;
144
- transition: all 0.15s ease;
203
+ border-radius: 6px;
204
+ transition: background-color 0.15s ease-out, color 0.15s ease-out;
205
+ }
206
+
207
+ .theme-color-picker-field .picker-close-btn:focus-visible {
208
+ outline: var(--accessibility-outline, 2px solid var(--theme-text));
209
+ outline-offset: 0;
145
210
  }
146
211
 
147
212
  .theme-color-picker-field .picker-close-btn:hover {
@@ -172,7 +237,7 @@
172
237
  text-align: center;
173
238
  color: var(--theme-text, #1e293b);
174
239
  background: var(--theme-elevation-50, #f8fafc);
175
- transition: all 0.15s ease;
240
+ transition: border-color 0.15s ease-out, background-color 0.15s ease-out;
176
241
  }
177
242
 
178
243
  .theme-color-picker-field .hex-input:hover {
@@ -180,11 +245,11 @@
180
245
  background: var(--theme-elevation-0, #ffffff);
181
246
  }
182
247
 
183
- .theme-color-picker-field .hex-input:focus {
184
- outline: none;
185
- border-color: var(--theme-elevation-500, #3b82f6);
186
- background: var(--theme-elevation-0, #ffffff);
187
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
248
+ .theme-color-picker-field .hex-input:focus-visible {
249
+ outline: var(--accessibility-outline, 2px solid var(--theme-text));
250
+ outline-offset: 1px;
251
+ border-color: var(--theme-elevation-500);
252
+ background: var(--theme-elevation-0);
188
253
  }
189
254
 
190
255
  /* Color Swatches */
@@ -195,11 +260,9 @@
195
260
  }
196
261
 
197
262
  .theme-color-picker-field .swatches-label {
198
- font-size: 0.6875rem;
263
+ font-size: 0.75rem;
199
264
  font-weight: 600;
200
- text-transform: uppercase;
201
- letter-spacing: 0.05em;
202
- color: var(--theme-elevation-500, #64748b);
265
+ color: var(--theme-elevation-600);
203
266
  margin-bottom: 0.625rem;
204
267
  }
205
268
 
@@ -215,7 +278,7 @@
215
278
  border: 1.5px solid var(--theme-elevation-150, #f1f5f9);
216
279
  border-radius: 6px;
217
280
  cursor: pointer;
218
- transition: all 0.15s ease;
281
+ transition: border-color 0.15s ease-out, box-shadow 0.15s ease-out, transform 0.15s ease-out;
219
282
  position: relative;
220
283
  }
221
284
 
@@ -232,11 +295,16 @@
232
295
  transform: scale(1.05);
233
296
  }
234
297
 
235
- .theme-color-picker-field .swatch-btn:focus {
236
- outline: 2px solid var(--theme-elevation-500, #3b82f6);
298
+ .theme-color-picker-field .swatch-btn:focus-visible {
299
+ outline: var(--accessibility-outline, 2px solid var(--theme-text));
237
300
  outline-offset: 2px;
238
301
  }
239
302
 
303
+ /* The saturation/hue surfaces are keyboard-operable in react-colorful. */
304
+ .theme-color-picker-field .react-colorful__interactive:focus-visible .react-colorful__pointer {
305
+ transform: translate(-50%, -50%) scale(1.2);
306
+ }
307
+
240
308
  /* React Colorful Overrides */
241
309
  .theme-color-picker-field .react-colorful {
242
310
  border-radius: 8px;
@@ -257,3 +325,15 @@
257
325
  border: 2px solid #ffffff;
258
326
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
259
327
  }
328
+
329
+ @media (prefers-reduced-motion: reduce) {
330
+ .theme-color-picker-field .color-picker-popover {
331
+ animation: none;
332
+ }
333
+
334
+ .theme-color-picker-field .color-swatch-btn:hover,
335
+ .theme-color-picker-field .swatch-btn:hover,
336
+ .theme-color-picker-field .swatch-btn.selected {
337
+ transform: none;
338
+ }
339
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeColorPickerField.d.ts","sourceRoot":"","sources":["../../src/fields/ThemeColorPickerField.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAmCvD,QAAA,MAAM,qBAAqB,EAAE,wBAuJ5B,CAAA;AAED,eAAe,qBAAqB,CAAA"}
1
+ {"version":3,"file":"ThemeColorPickerField.d.ts","sourceRoot":"","sources":["../../src/fields/ThemeColorPickerField.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAgEvD,QAAA,MAAM,qBAAqB,EAAE,wBAwP5B,CAAA;AAED,eAAe,qBAAqB,CAAA"}
@@ -1,61 +1,128 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useField } from '@payloadcms/ui';
4
- import Color from 'color';
3
+ import { useField, useFormFields } from '@payloadcms/ui';
4
+ import { X } from 'lucide-react';
5
5
  import { HexColorInput, HexColorPicker } from 'react-colorful';
6
- import { useCallback, useEffect, useRef, useState } from 'react';
6
+ import { useCallback, useEffect, useId, useRef, useState } from 'react';
7
+ import { useThemeLanguage, useThemeTranslations } from '../hooks/useThemeTranslations.js';
8
+ import { cssColorToHex as toHex, getContrastRatio } from '../utils/contrast.js';
9
+ import { resolveLocalizedText } from '../utils/localizedText.js';
7
10
  // Import CSS for styling
8
11
  if (typeof window !== 'undefined') {
9
- // @ts-expect-error - Dynamic CSS import
10
12
  import('./ThemeColorPickerField.css').catch(()=>{});
11
13
  }
12
- function resolveLocalizedValue(value, fallback) {
13
- if (typeof value === 'string') {
14
- return value;
15
- }
16
- if (typeof value === 'object' && value !== null) {
17
- const localized = value;
18
- const resolved = localized.en ?? localized.cs ?? localized[''];
19
- if (typeof resolved === 'string') {
20
- return resolved;
21
- }
22
- }
23
- return fallback;
24
- }
25
14
  /**
26
- * Convert various color formats to HEX for the color picker
27
- */ function toHex(color) {
28
- if (!color) return '#000000';
29
- return Color(color).hex().toString();
15
+ * Convert any CSS color string to hex for the picker, with a neutral fallback so
16
+ * the swatch always renders. Delegates to the shared {@link toHex} util
17
+ * (color package + canvas fallback for oklch/lch).
18
+ */ function cssColorToHex(cssColor) {
19
+ if (!cssColor) return '#000000';
20
+ return toHex(cssColor) ?? '#888888';
21
+ }
22
+ /** WCAG contrast ratio between two CSS colours (1–21), or null if unparseable. */ const contrastRatio = getContrastRatio;
23
+ function contrastLevel(ratio) {
24
+ if (ratio >= 4.5) return 'aa';
25
+ if (ratio >= 3) return 'aaLarge';
26
+ return 'low';
30
27
  }
28
+ const format = (template, values)=>template.replace(/\{(\w+)\}/g, (match, key)=>values[key] ?? match);
29
+ const SURFACE_TOKENS = [
30
+ 'card',
31
+ 'popover',
32
+ 'primary',
33
+ 'secondary',
34
+ 'muted',
35
+ 'accent',
36
+ 'destructive'
37
+ ];
38
+ const isForegroundToken = (name)=>name === 'foreground' || name.endsWith('Foreground');
39
+ /** The token a colour is read against: surfaces ↔ their `…Foreground`, background ↔ foreground. */ function contrastPartner(name) {
40
+ if (name === 'background') return 'foreground';
41
+ if (name === 'foreground') return 'background';
42
+ if (SURFACE_TOKENS.includes(name)) return `${name}Foreground`;
43
+ const base = name.endsWith('Foreground') ? name.slice(0, -'Foreground'.length) : '';
44
+ return SURFACE_TOKENS.includes(base) ? base : null;
45
+ }
46
+ const QUICK_COLORS = [
47
+ '#ef4444',
48
+ '#f97316',
49
+ '#f59e0b',
50
+ '#84cc16',
51
+ '#10b981',
52
+ '#06b6d4',
53
+ '#3b82f6',
54
+ '#8b5cf6',
55
+ '#ec4899',
56
+ '#64748b',
57
+ '#000000',
58
+ '#ffffff'
59
+ ];
31
60
  const ThemeColorPickerField = ({ field, path })=>{
32
61
  const { value, setValue } = useField({
33
62
  path
34
63
  });
64
+ const t = useThemeTranslations();
65
+ const language = useThemeLanguage();
66
+ const inputId = useId();
67
+ const descriptionId = useId();
68
+ const popoverId = useId();
35
69
  const [localValue, setLocalValue] = useState(value || '');
36
70
  const [showPicker, setShowPicker] = useState(false);
37
- const [hexValue, setHexValue] = useState(toHex(value || ''));
71
+ const [hexValue, setHexValue] = useState(cssColorToHex(value || ''));
38
72
  const [isDragging, setIsDragging] = useState(false);
39
73
  const pickerRef = useRef(null);
74
+ const swatchRef = useRef(null);
40
75
  useEffect(()=>{
41
76
  setLocalValue(value || '');
42
- setHexValue(toHex(value || ''));
77
+ setHexValue(cssColorToHex(value || ''));
43
78
  }, [
44
79
  value
45
80
  ]);
46
- // Close picker when clicking outside
81
+ const closePicker = useCallback((restoreFocus)=>{
82
+ setShowPicker(false);
83
+ setIsDragging(false);
84
+ if (restoreFocus) swatchRef.current?.focus();
85
+ }, []);
86
+ // Close on outside pointer, Escape; move focus into the popover when it opens.
47
87
  useEffect(()=>{
48
- function handleClickOutside(event) {
49
- if (pickerRef.current && !pickerRef.current.contains(event.target)) {
50
- setShowPicker(false);
51
- }
88
+ if (!showPicker) return;
89
+ function handlePointerOutside(event) {
90
+ const target = event.target;
91
+ if (pickerRef.current?.contains(target) || swatchRef.current?.contains(target)) return;
92
+ closePicker(false);
52
93
  }
53
- if (showPicker) {
54
- document.addEventListener('mousedown', handleClickOutside);
55
- return ()=>document.removeEventListener('mousedown', handleClickOutside);
94
+ function handleKeyDown(event) {
95
+ if (event.key === 'Escape') {
96
+ event.stopPropagation();
97
+ closePicker(true);
98
+ }
56
99
  }
100
+ document.addEventListener('pointerdown', handlePointerOutside);
101
+ document.addEventListener('keydown', handleKeyDown);
102
+ pickerRef.current?.querySelector('.react-colorful__interactive')?.focus();
103
+ return ()=>{
104
+ document.removeEventListener('pointerdown', handlePointerOutside);
105
+ document.removeEventListener('keydown', handleKeyDown);
106
+ };
107
+ }, [
108
+ showPicker,
109
+ closePicker
110
+ ]);
111
+ // A drag ends wherever the pointer is released, when the browser cancels the
112
+ // gesture (e.g. to scroll on touch), or when the window loses focus.
113
+ useEffect(()=>{
114
+ if (!isDragging) return;
115
+ const endDrag = ()=>setIsDragging(false);
116
+ window.addEventListener('pointerup', endDrag);
117
+ window.addEventListener('pointercancel', endDrag);
118
+ window.addEventListener('blur', endDrag);
119
+ return ()=>{
120
+ window.removeEventListener('pointerup', endDrag);
121
+ window.removeEventListener('pointercancel', endDrag);
122
+ window.removeEventListener('blur', endDrag);
123
+ };
57
124
  }, [
58
- showPicker
125
+ isDragging
59
126
  ]);
60
127
  // Only update parent setValue when drag completes
61
128
  useEffect(()=>{
@@ -73,17 +140,36 @@ const ThemeColorPickerField = ({ field, path })=>{
73
140
  }, []);
74
141
  const handleTextChange = useCallback((nextValue)=>{
75
142
  setLocalValue(nextValue);
76
- setHexValue(toHex(nextValue));
77
- }, []);
78
- const handlePickerMouseDown = useCallback(()=>{
79
- setIsDragging(true);
80
- }, []);
81
- const handlePickerMouseUp = useCallback(()=>{
82
- setIsDragging(false);
143
+ setHexValue(cssColorToHex(nextValue));
83
144
  }, []);
84
145
  const fieldName = path.split('.').pop() || '';
85
- const label = resolveLocalizedValue(field.label, fieldName);
86
- const description = resolveLocalizedValue(field.admin?.description, '');
146
+ const label = resolveLocalizedText(field.label, language, fieldName);
147
+ const description = resolveLocalizedText(field.admin?.description, language);
148
+ // Contrast is only meaningful against the token this colour is paired with
149
+ // (e.g. primary ↔ primaryForeground), so measure that pair instead of white/black.
150
+ const pairKey = contrastPartner(fieldName);
151
+ const pairPath = pairKey ? [
152
+ ...path.split('.').slice(0, -1),
153
+ pairKey
154
+ ].join('.') : null;
155
+ const pairValue = useFormFields(([fields])=>pairPath ? fields?.[pairPath]?.value : undefined);
156
+ const ownValue = localValue || hexValue;
157
+ const pairSample = pairKey && typeof pairValue === 'string' && pairValue.trim() ? isForegroundToken(fieldName) ? {
158
+ foreground: ownValue,
159
+ background: pairValue,
160
+ foregroundLabel: label,
161
+ backgroundLabel: pairKey
162
+ } : {
163
+ foreground: pairValue,
164
+ background: ownValue,
165
+ foregroundLabel: pairKey,
166
+ backgroundLabel: label
167
+ } : null;
168
+ const levelLabel = {
169
+ aa: t.colorPicker.levelAA,
170
+ aaLarge: t.colorPicker.levelAALarge,
171
+ low: t.colorPicker.levelLow
172
+ };
87
173
  return /*#__PURE__*/ _jsxs("div", {
88
174
  className: "theme-color-picker-field",
89
175
  children: [
@@ -92,10 +178,12 @@ const ThemeColorPickerField = ({ field, path })=>{
92
178
  children: [
93
179
  /*#__PURE__*/ _jsx("label", {
94
180
  className: "field-label",
181
+ htmlFor: inputId,
95
182
  children: label
96
183
  }),
97
184
  description && /*#__PURE__*/ _jsx("div", {
98
185
  className: "field-description",
186
+ id: descriptionId,
99
187
  children: description
100
188
  })
101
189
  ]
@@ -107,44 +195,98 @@ const ThemeColorPickerField = ({ field, path })=>{
107
195
  className: "color-input-row",
108
196
  children: [
109
197
  /*#__PURE__*/ _jsx("button", {
198
+ ref: swatchRef,
110
199
  type: "button",
111
200
  className: "color-swatch-btn",
112
201
  style: {
113
- backgroundColor: hexValue
202
+ backgroundColor: localValue || hexValue
114
203
  },
115
- onClick: ()=>setShowPicker(!showPicker),
116
- "aria-label": "Open color picker",
204
+ onClick: ()=>setShowPicker((open)=>!open),
205
+ "aria-label": `${t.colorPicker.openPicker}: ${label}`,
206
+ "aria-expanded": showPicker,
207
+ "aria-controls": showPicker ? popoverId : undefined,
117
208
  title: hexValue
118
209
  }),
119
210
  /*#__PURE__*/ _jsx("input", {
211
+ id: inputId,
120
212
  type: "text",
121
213
  value: localValue || '',
122
214
  onChange: (event)=>handleTextChange(event.target.value),
123
- placeholder: "oklch(...) or #hex",
215
+ placeholder: t.colorPicker.placeholder,
216
+ "aria-describedby": description ? descriptionId : undefined,
217
+ spellCheck: false,
124
218
  className: "color-text-input"
125
219
  })
126
220
  ]
127
221
  }),
222
+ pairSample ? /*#__PURE__*/ _jsx("div", {
223
+ className: "color-contrast-row",
224
+ children: (()=>{
225
+ const ratio = contrastRatio(pairSample.foreground, pairSample.background);
226
+ if (ratio == null) return null;
227
+ const level = contrastLevel(ratio);
228
+ const title = format(t.colorPicker.pairContrastTitle, {
229
+ foreground: pairSample.foregroundLabel,
230
+ background: pairSample.backgroundLabel
231
+ });
232
+ return /*#__PURE__*/ _jsxs("span", {
233
+ className: "contrast-chip",
234
+ title: `${title}: ${ratio.toFixed(2)}:1`,
235
+ children: [
236
+ /*#__PURE__*/ _jsx("span", {
237
+ className: "contrast-sample",
238
+ style: {
239
+ background: pairSample.background,
240
+ color: pairSample.foreground
241
+ },
242
+ "aria-hidden": "true",
243
+ children: "Aa"
244
+ }),
245
+ /*#__PURE__*/ _jsxs("span", {
246
+ className: `contrast-ratio contrast-ratio--${level}`,
247
+ children: [
248
+ /*#__PURE__*/ _jsxs("span", {
249
+ className: "sr-only",
250
+ children: [
251
+ title,
252
+ ": "
253
+ ]
254
+ }),
255
+ t.colorPicker.pairContrast,
256
+ " ",
257
+ ratio.toFixed(1),
258
+ " · ",
259
+ levelLabel[level]
260
+ ]
261
+ })
262
+ ]
263
+ });
264
+ })()
265
+ }) : null,
128
266
  showPicker && /*#__PURE__*/ _jsxs("div", {
129
267
  ref: pickerRef,
268
+ id: popoverId,
269
+ role: "dialog",
270
+ "aria-label": `${t.colorPicker.title}: ${label}`,
130
271
  className: "color-picker-popover",
131
- onMouseDown: handlePickerMouseDown,
132
- onMouseUp: handlePickerMouseUp,
133
- onMouseLeave: handlePickerMouseUp,
272
+ onPointerDown: ()=>setIsDragging(true),
134
273
  children: [
135
274
  /*#__PURE__*/ _jsxs("div", {
136
275
  className: "picker-header",
137
276
  children: [
138
277
  /*#__PURE__*/ _jsx("span", {
139
278
  className: "picker-title",
140
- children: "Color Picker"
279
+ children: t.colorPicker.title
141
280
  }),
142
281
  /*#__PURE__*/ _jsx("button", {
143
282
  type: "button",
144
- onClick: ()=>setShowPicker(false),
283
+ onClick: ()=>closePicker(true),
145
284
  className: "picker-close-btn",
146
- "aria-label": "Close",
147
- children: "✕"
285
+ "aria-label": t.colorPicker.close,
286
+ children: /*#__PURE__*/ _jsx(X, {
287
+ size: 16,
288
+ "aria-hidden": "true"
289
+ })
148
290
  })
149
291
  ]
150
292
  }),
@@ -160,6 +302,7 @@ const ThemeColorPickerField = ({ field, path })=>{
160
302
  onChange: handleHexChange,
161
303
  prefixed: true,
162
304
  placeholder: "#000000",
305
+ "aria-label": "HEX",
163
306
  className: "hex-input"
164
307
  })
165
308
  }),
@@ -168,36 +311,27 @@ const ThemeColorPickerField = ({ field, path })=>{
168
311
  children: [
169
312
  /*#__PURE__*/ _jsx("div", {
170
313
  className: "swatches-label",
171
- children: "Quick Colors"
314
+ children: t.colorPicker.quickColors
172
315
  }),
173
316
  /*#__PURE__*/ _jsx("div", {
174
317
  className: "swatches-grid",
175
- children: [
176
- '#ef4444',
177
- '#f97316',
178
- '#f59e0b',
179
- '#84cc16',
180
- '#10b981',
181
- '#06b6d4',
182
- '#3b82f6',
183
- '#8b5cf6',
184
- '#ec4899',
185
- '#64748b',
186
- '#000000',
187
- '#ffffff'
188
- ].map((color)=>/*#__PURE__*/ _jsx("button", {
318
+ children: QUICK_COLORS.map((color)=>{
319
+ const selected = hexValue.toLowerCase() === color.toLowerCase();
320
+ return /*#__PURE__*/ _jsx("button", {
189
321
  type: "button",
190
322
  onClick: ()=>{
191
323
  handleHexChange(color);
192
324
  setValue(color);
193
325
  },
194
- className: `swatch-btn ${hexValue.toLowerCase() === color.toLowerCase() ? 'selected' : ''}`,
326
+ className: `swatch-btn ${selected ? 'selected' : ''}`,
195
327
  style: {
196
328
  backgroundColor: color
197
329
  },
198
- "aria-label": `Select ${color}`,
330
+ "aria-label": `${t.colorPicker.selectColor} ${color}`,
331
+ "aria-pressed": selected,
199
332
  title: color
200
- }, color))
333
+ }, color);
334
+ })
201
335
  })
202
336
  ]
203
337
  })