@nuasite/cms 0.7.1 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "directory": "packages/astro-cms"
15
15
  },
16
16
  "license": "Apache-2.0",
17
- "version": "0.7.1",
17
+ "version": "0.7.3",
18
18
  "module": "src/index.ts",
19
19
  "types": "src/index.ts",
20
20
  "type": "module",
@@ -0,0 +1,312 @@
1
+ import type { Attribute } from './types'
2
+
3
+ /**
4
+ * Default Tailwind CSS v4 color names.
5
+ */
6
+ export const DEFAULT_TAILWIND_COLORS = [
7
+ 'slate',
8
+ 'gray',
9
+ 'zinc',
10
+ 'neutral',
11
+ 'stone',
12
+ 'red',
13
+ 'orange',
14
+ 'amber',
15
+ 'yellow',
16
+ 'lime',
17
+ 'green',
18
+ 'emerald',
19
+ 'teal',
20
+ 'cyan',
21
+ 'sky',
22
+ 'blue',
23
+ 'indigo',
24
+ 'violet',
25
+ 'purple',
26
+ 'fuchsia',
27
+ 'pink',
28
+ 'rose',
29
+ ] as const
30
+
31
+ /**
32
+ * Standard Tailwind color shades.
33
+ */
34
+ export const STANDARD_SHADES = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'] as const
35
+
36
+ /**
37
+ * Special color values that don't have shades.
38
+ */
39
+ export const SPECIAL_COLORS = ['transparent', 'current', 'inherit', 'white', 'black'] as const
40
+
41
+ /**
42
+ * Non-color utility suffixes that should not be matched as custom colors.
43
+ * These follow the pattern `prefix-word-number` but are not colors.
44
+ */
45
+ const NON_COLOR_SUFFIXES = ['opacity'] as const
46
+
47
+ /**
48
+ * Build a regex pattern for matching color classes.
49
+ * Matches:
50
+ * - Default colors with optional shades: bg-blue-500, bg-white
51
+ * - Custom theme colors with shades: bg-primary-500
52
+ * - Arbitrary hex values: bg-[#41b883], bg-[#fff]
53
+ * - Arbitrary rgb/hsl values: bg-[rgb(255,0,0)], bg-[hsl(0,100%,50%)]
54
+ * Excludes non-color utilities like opacity.
55
+ */
56
+ function buildColorPattern(prefix: string): RegExp {
57
+ const colorNames = [...DEFAULT_TAILWIND_COLORS, ...SPECIAL_COLORS].join('|')
58
+ const excluded = NON_COLOR_SUFFIXES.join('|')
59
+ // Arbitrary value patterns for colors
60
+ const arbitraryHex = '\\[#[0-9a-fA-F]{3,8}\\]'
61
+ const arbitraryFunc = '\\[(?:rgba?|hsla?)\\([^\\]]+\\)\\]'
62
+ // Match: prefix-(colorName[-shade]?) OR prefix-(customColor-shade) OR prefix-[arbitrary] but NOT prefix-(excluded-number)
63
+ return new RegExp(`^${prefix}-((?:${colorNames})(?:-(\\d+))?|(?!(?:${excluded})-)(\\w+)-(\\d+)|${arbitraryHex}|${arbitraryFunc})$`)
64
+ }
65
+
66
+ /**
67
+ * Build a regex pattern for matching opacity classes.
68
+ */
69
+ function buildOpacityPattern(prefix: string): RegExp {
70
+ return new RegExp(`^${prefix}-opacity-(\\d+)$`)
71
+ }
72
+
73
+ /**
74
+ * Regex patterns to match Tailwind color classes.
75
+ */
76
+ export const COLOR_CLASS_PATTERNS = {
77
+ bg: buildColorPattern('bg'),
78
+ text: buildColorPattern('text'),
79
+ border: buildColorPattern('border'),
80
+ hoverBg: buildColorPattern('hover:bg'),
81
+ hoverText: buildColorPattern('hover:text'),
82
+ hoverBorder: buildColorPattern('hover:border'),
83
+ }
84
+
85
+ /**
86
+ * Regex patterns to match Tailwind opacity classes.
87
+ */
88
+ export const OPACITY_CLASS_PATTERNS = {
89
+ bgOpacity: buildOpacityPattern('bg'),
90
+ textOpacity: buildOpacityPattern('text'),
91
+ borderOpacity: buildOpacityPattern('border'),
92
+ }
93
+
94
+ /**
95
+ * Regex patterns to match Tailwind gradient color classes.
96
+ */
97
+ export const GRADIENT_CLASS_PATTERNS = {
98
+ from: buildColorPattern('from'),
99
+ via: buildColorPattern('via'),
100
+ to: buildColorPattern('to'),
101
+ hoverFrom: buildColorPattern('hover:from'),
102
+ hoverVia: buildColorPattern('hover:via'),
103
+ hoverTo: buildColorPattern('hover:to'),
104
+ }
105
+
106
+ /** Flat key names for color class categories */
107
+ const COLOR_FLAT_KEYS: Record<string, string> = {
108
+ bg: 'bg',
109
+ text: 'text',
110
+ border: 'border',
111
+ hoverBg: 'hoverBg',
112
+ hoverText: 'hoverText',
113
+ hoverBorder: 'hoverBorder',
114
+ }
115
+
116
+ const GRADIENT_FLAT_KEYS: Record<string, string> = {
117
+ from: 'gradientFrom',
118
+ via: 'gradientVia',
119
+ to: 'gradientTo',
120
+ hoverFrom: 'hoverGradientFrom',
121
+ hoverVia: 'hoverGradientVia',
122
+ hoverTo: 'hoverGradientTo',
123
+ }
124
+
125
+ /**
126
+ * Extract color classes from an element's class attribute.
127
+ * Returns a flat Record<string, Attribute> with keys like bg, text, gradientFrom, bgOpacity, etc.
128
+ */
129
+ export function extractColorClasses(classAttr: string | null | undefined): Record<string, Attribute> | undefined {
130
+ if (!classAttr) return undefined
131
+
132
+ const classes = classAttr.split(/\s+/).filter(Boolean)
133
+ const result: Record<string, Attribute> = {}
134
+
135
+ for (const cls of classes) {
136
+ let matched = false
137
+
138
+ // Check color patterns
139
+ for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
140
+ if (pattern.test(cls)) {
141
+ const flatKey = COLOR_FLAT_KEYS[key]
142
+ if (flatKey && !(flatKey in result)) {
143
+ result[flatKey] = { value: cls }
144
+ }
145
+ matched = true
146
+ break
147
+ }
148
+ }
149
+
150
+ // Check gradient patterns
151
+ if (!matched) {
152
+ for (const [key, pattern] of Object.entries(GRADIENT_CLASS_PATTERNS)) {
153
+ if (pattern.test(cls)) {
154
+ const flatKey = GRADIENT_FLAT_KEYS[key]
155
+ if (flatKey && !(flatKey in result)) {
156
+ result[flatKey] = { value: cls }
157
+ }
158
+ matched = true
159
+ break
160
+ }
161
+ }
162
+ }
163
+
164
+ // Check opacity patterns
165
+ if (!matched) {
166
+ for (const [key, pattern] of Object.entries(OPACITY_CLASS_PATTERNS)) {
167
+ if (pattern.test(cls)) {
168
+ if (!(key in result)) {
169
+ result[key] = { value: cls }
170
+ }
171
+ break
172
+ }
173
+ }
174
+ }
175
+ }
176
+
177
+ return Object.keys(result).length > 0 ? result : undefined
178
+ }
179
+
180
+ /**
181
+ * Regex patterns for matching text style classes on elements.
182
+ */
183
+ export const TEXT_STYLE_CLASS_PATTERNS: Record<string, RegExp> = {
184
+ fontWeight: /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,
185
+ fontStyle: /^(italic|not-italic)$/,
186
+ textDecoration: /^(underline|overline|line-through|no-underline)$/,
187
+ fontSize: /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
188
+ }
189
+
190
+ /**
191
+ * Extract text style classes from an element's class attribute.
192
+ * Returns a Record<string, Attribute> with keys: fontWeight, fontStyle, textDecoration, fontSize.
193
+ */
194
+ export function extractTextStyleClasses(classAttr: string | null | undefined): Record<string, Attribute> | undefined {
195
+ if (!classAttr) return undefined
196
+
197
+ const classes = classAttr.split(/\s+/).filter(Boolean)
198
+ const result: Record<string, Attribute> = {}
199
+
200
+ for (const cls of classes) {
201
+ for (const [key, pattern] of Object.entries(TEXT_STYLE_CLASS_PATTERNS)) {
202
+ if (pattern.test(cls) && !(key in result)) {
203
+ result[key] = { value: cls }
204
+ break
205
+ }
206
+ }
207
+ }
208
+
209
+ return Object.keys(result).length > 0 ? result : undefined
210
+ }
211
+
212
+ /**
213
+ * Check if a class is a color class (including gradient colors).
214
+ */
215
+ export function isColorClass(className: string): boolean {
216
+ return Object.values(COLOR_CLASS_PATTERNS).some(pattern => pattern.test(className))
217
+ || Object.values(GRADIENT_CLASS_PATTERNS).some(pattern => pattern.test(className))
218
+ }
219
+
220
+ /**
221
+ * Generate a new class string with a color class replaced.
222
+ */
223
+ export function replaceColorClass(
224
+ currentClasses: string,
225
+ oldColorClass: string,
226
+ newColorClass: string,
227
+ ): string {
228
+ const classes = currentClasses.split(/\s+/).filter(Boolean)
229
+ const newClasses = classes.map(cls => cls === oldColorClass ? newColorClass : cls)
230
+ return newClasses.join(' ')
231
+ }
232
+
233
+ /**
234
+ * Get the color type from a color class.
235
+ * Returns the flat key name (e.g., 'bg', 'gradientFrom', 'bgOpacity').
236
+ */
237
+ export function getColorType(colorClass: string): string | undefined {
238
+ for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
239
+ if (pattern.test(colorClass)) {
240
+ return COLOR_FLAT_KEYS[key]
241
+ }
242
+ }
243
+ for (const [key, pattern] of Object.entries(GRADIENT_CLASS_PATTERNS)) {
244
+ if (pattern.test(colorClass)) {
245
+ return GRADIENT_FLAT_KEYS[key]
246
+ }
247
+ }
248
+ for (const [key, pattern] of Object.entries(OPACITY_CLASS_PATTERNS)) {
249
+ if (pattern.test(colorClass)) {
250
+ return key
251
+ }
252
+ }
253
+ return undefined
254
+ }
255
+
256
+ /**
257
+ * Parse a color class into its components.
258
+ */
259
+ export function parseColorClass(colorClass: string): {
260
+ prefix: string
261
+ colorName: string
262
+ shade?: string
263
+ isHover: boolean
264
+ isArbitrary?: boolean
265
+ } | undefined {
266
+ // Verify this is actually a color class using the comprehensive patterns
267
+ const isColor = Object.values(COLOR_CLASS_PATTERNS).some(p => p.test(colorClass))
268
+ || Object.values(GRADIENT_CLASS_PATTERNS).some(p => p.test(colorClass))
269
+ if (!isColor) return undefined
270
+
271
+ const isHover = colorClass.startsWith('hover:')
272
+ const classWithoutHover = isHover ? colorClass.slice(6) : colorClass
273
+
274
+ // Try matching standard color classes (default colors, custom theme colors, and gradients)
275
+ const standardMatch = classWithoutHover.match(/^(bg|text|border|from|via|to)-([a-z]+)(?:-(\d+))?$/)
276
+ if (standardMatch) {
277
+ return {
278
+ prefix: isHover ? `hover:${standardMatch[1]}` : standardMatch[1]!,
279
+ colorName: standardMatch[2]!,
280
+ shade: standardMatch[3],
281
+ isHover,
282
+ }
283
+ }
284
+
285
+ // Try matching arbitrary value classes like bg-[#41b883] or from-[#41b883]
286
+ const arbitraryMatch = classWithoutHover.match(/^(bg|text|border|from|via|to)-(\[.+\])$/)
287
+ if (arbitraryMatch) {
288
+ return {
289
+ prefix: isHover ? `hover:${arbitraryMatch[1]}` : arbitraryMatch[1]!,
290
+ colorName: arbitraryMatch[2]!,
291
+ shade: undefined,
292
+ isHover,
293
+ isArbitrary: true,
294
+ }
295
+ }
296
+
297
+ return undefined
298
+ }
299
+
300
+ /**
301
+ * Build a color class from components.
302
+ */
303
+ export function buildColorClass(
304
+ prefix: string,
305
+ colorName: string,
306
+ shade?: string,
307
+ ): string {
308
+ if (shade) {
309
+ return `${prefix}-${colorName}-${shade}`
310
+ }
311
+ return `${prefix}-${colorName}`
312
+ }
@@ -1,42 +1,27 @@
1
- import type { Attribute, AvailableColors, TailwindColor } from './types'
2
-
3
- /**
4
- * Default Tailwind CSS v4 color names.
5
- */
6
- export const DEFAULT_TAILWIND_COLORS = [
7
- 'slate',
8
- 'gray',
9
- 'zinc',
10
- 'neutral',
11
- 'stone',
12
- 'red',
13
- 'orange',
14
- 'amber',
15
- 'yellow',
16
- 'lime',
17
- 'green',
18
- 'emerald',
19
- 'teal',
20
- 'cyan',
21
- 'sky',
22
- 'blue',
23
- 'indigo',
24
- 'violet',
25
- 'purple',
26
- 'fuchsia',
27
- 'pink',
28
- 'rose',
29
- ] as const
30
-
31
- /**
32
- * Standard Tailwind color shades.
33
- */
34
- export const STANDARD_SHADES = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'] as const
35
-
36
- /**
37
- * Special color values that don't have shades.
38
- */
39
- export const SPECIAL_COLORS = ['transparent', 'current', 'inherit', 'white', 'black'] as const
1
+ import {
2
+ buildColorClass,
3
+ COLOR_CLASS_PATTERNS,
4
+ DEFAULT_TAILWIND_COLORS,
5
+ extractColorClasses,
6
+ isColorClass,
7
+ parseColorClass,
8
+ SPECIAL_COLORS,
9
+ STANDARD_SHADES,
10
+ } from '../color-patterns'
11
+ import type { Attribute, AvailableColors } from './types'
12
+
13
+ // Re-export shared detection logic for consumers
14
+ export {
15
+ buildColorClass,
16
+ COLOR_CLASS_PATTERNS,
17
+ DEFAULT_TAILWIND_COLORS,
18
+ extractColorClasses,
19
+ getColorType,
20
+ isColorClass,
21
+ parseColorClass,
22
+ SPECIAL_COLORS,
23
+ STANDARD_SHADES,
24
+ } from '../color-patterns'
40
25
 
41
26
  /**
42
27
  * Map of Tailwind color names to their CSS color values for preview.
@@ -97,140 +82,12 @@ export const SHADE_LIGHTNESS: Record<string, number> = {
97
82
  '950': 0.05,
98
83
  }
99
84
 
100
- /**
101
- * Non-color text-* utility classes that should NOT be treated as color classes.
102
- * These are Tailwind utilities like text alignment, sizing, wrapping, etc.
103
- */
104
- const NON_COLOR_TEXT_CLASSES = new Set([
105
- // Text alignment
106
- 'text-left',
107
- 'text-center',
108
- 'text-right',
109
- 'text-justify',
110
- 'text-start',
111
- 'text-end',
112
- // Text wrapping
113
- 'text-wrap',
114
- 'text-nowrap',
115
- 'text-balance',
116
- 'text-pretty',
117
- // Text overflow
118
- 'text-ellipsis',
119
- 'text-clip',
120
- // Text transform (these don't start with text- but just in case)
121
- ])
122
-
123
- /**
124
- * Regex patterns to match Tailwind color classes.
125
- */
126
- const COLOR_CLASS_PATTERNS = {
127
- bg: /^bg-([a-z]+)(?:-(\d+))?$/,
128
- text: /^text-([a-z]+)(?:-(\d+))?$/,
129
- border: /^border-([a-z]+)(?:-(\d+))?$/,
130
- hoverBg: /^hover:bg-([a-z]+)(?:-(\d+))?$/,
131
- hoverText: /^hover:text-([a-z]+)(?:-(\d+))?$/,
132
- }
133
-
134
- /**
135
- * Check if a class is a text color class (not a non-color text utility).
136
- */
137
- function isTextColorClass(className: string): boolean {
138
- if (NON_COLOR_TEXT_CLASSES.has(className)) {
139
- return false
140
- }
141
- return COLOR_CLASS_PATTERNS.text.test(className)
142
- }
143
-
144
- /**
145
- * Parse a color class into its components.
146
- */
147
- export function parseColorClass(colorClass: string): {
148
- prefix: string
149
- colorName: string
150
- shade?: string
151
- isHover: boolean
152
- } | undefined {
153
- // Exclude non-color text utility classes
154
- if (NON_COLOR_TEXT_CLASSES.has(colorClass)) {
155
- return undefined
156
- }
157
-
158
- const isHover = colorClass.startsWith('hover:')
159
- const classWithoutHover = isHover ? colorClass.slice(6) : colorClass
160
-
161
- // Also check hover variants of non-color text classes
162
- if (isHover && NON_COLOR_TEXT_CLASSES.has(`text-${classWithoutHover.slice(5)}`)) {
163
- return undefined
164
- }
165
-
166
- const match = classWithoutHover.match(/^(bg|text|border)-([a-z]+)(?:-(\d+))?$/)
167
-
168
- if (!match) return undefined
169
-
170
- return {
171
- prefix: isHover ? `hover:${match[1]}` : match[1]!,
172
- colorName: match[2]!,
173
- shade: match[3],
174
- isHover,
175
- }
176
- }
177
-
178
- /**
179
- * Build a color class from components.
180
- */
181
- export function buildColorClass(
182
- prefix: string,
183
- colorName: string,
184
- shade?: string,
185
- ): string {
186
- if (shade) {
187
- return `${prefix}-${colorName}-${shade}`
188
- }
189
- return `${prefix}-${colorName}`
190
- }
191
-
192
- /**
193
- * Get the color type from a color class.
194
- */
195
- export function getColorType(colorClass: string): keyof typeof COLOR_CLASS_PATTERNS | undefined {
196
- for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
197
- // For text type, use the helper to exclude non-color text utilities
198
- if (key === 'text' || key === 'hoverText') {
199
- if (key === 'text' && isTextColorClass(colorClass)) {
200
- return key as keyof typeof COLOR_CLASS_PATTERNS
201
- }
202
- if (key === 'hoverText' && colorClass.startsWith('hover:') && isTextColorClass(colorClass.slice(6))) {
203
- return key as keyof typeof COLOR_CLASS_PATTERNS
204
- }
205
- } else if (pattern.test(colorClass)) {
206
- return key as keyof typeof COLOR_CLASS_PATTERNS
207
- }
208
- }
209
- return undefined
210
- }
211
-
212
- /**
213
- * Check if a class is a color class.
214
- */
215
- export function isColorClass(className: string): boolean {
216
- // Check text color separately to exclude non-color text utilities
217
- if (COLOR_CLASS_PATTERNS.text.test(className)) {
218
- return isTextColorClass(className)
219
- }
220
- if (COLOR_CLASS_PATTERNS.hoverText.test(className)) {
221
- return isTextColorClass(className.slice(6)) // Remove 'hover:' prefix
222
- }
223
- return Object.entries(COLOR_CLASS_PATTERNS)
224
- .filter(([key]) => key !== 'text' && key !== 'hoverText')
225
- .some(([, pattern]) => pattern.test(className))
226
- }
227
-
228
85
  /**
229
86
  * Get a preview CSS color for a Tailwind color.
230
87
  */
231
88
  export function getColorPreview(colorName: string, shade?: string): string {
232
89
  // Special colors without shades
233
- if (SPECIAL_COLORS.includes(colorName as any)) {
90
+ if ((SPECIAL_COLORS as readonly string[]).includes(colorName)) {
234
91
  return COLOR_PREVIEW_MAP[colorName] || colorName
235
92
  }
236
93
 
@@ -293,27 +150,19 @@ export function replaceColorClass(
293
150
  const classes = element.className.split(/\s+/).filter(Boolean)
294
151
  const pattern = COLOR_CLASS_PATTERNS[colorType]
295
152
 
153
+ const prefix = colorType === 'hoverBg'
154
+ ? 'hover:bg'
155
+ : colorType === 'hoverText'
156
+ ? 'hover:text'
157
+ : colorType
158
+ const newClass = buildColorClass(prefix, newColorName, newShade)
159
+
296
160
  let oldClass: string | undefined
297
161
  const newClasses: string[] = []
298
162
 
299
163
  for (const cls of classes) {
300
- // For text types, use the helper to exclude non-color text utilities
301
- let isColorMatch: boolean
302
- if (colorType === 'text') {
303
- isColorMatch = isTextColorClass(cls)
304
- } else if (colorType === 'hoverText') {
305
- isColorMatch = cls.startsWith('hover:') && isTextColorClass(cls.slice(6))
306
- } else {
307
- isColorMatch = pattern.test(cls)
308
- }
309
- if (isColorMatch) {
164
+ if (pattern.test(cls)) {
310
165
  oldClass = cls
311
- // Build the new class with the same prefix
312
- const isHover = colorType.startsWith('hover')
313
- const prefix = isHover
314
- ? colorType.replace('hover', 'hover:').toLowerCase().replace('hover:bg', 'hover:bg').replace('hover:text', 'hover:text')
315
- : colorType
316
- const newClass = buildColorClass(prefix, newColorName, newShade)
317
166
  newClasses.push(newClass)
318
167
  } else {
319
168
  newClasses.push(cls)
@@ -324,12 +173,6 @@ export function replaceColorClass(
324
173
  return undefined
325
174
  }
326
175
 
327
- const newClass = buildColorClass(
328
- colorType.startsWith('hover') ? `hover:${colorType.slice(5).toLowerCase()}` : colorType,
329
- newColorName,
330
- newShade,
331
- )
332
-
333
176
  element.className = newClasses.join(' ')
334
177
 
335
178
  return { oldClass, newClass }
@@ -339,30 +182,7 @@ export function replaceColorClass(
339
182
  * Get the current color classes from an element as Record<string, Attribute>.
340
183
  */
341
184
  export function getElementColorClasses(element: HTMLElement): Record<string, Attribute> {
342
- const classes = element.className.split(/\s+/).filter(Boolean)
343
- const colorClasses: Record<string, Attribute> = {}
344
-
345
- for (const cls of classes) {
346
- for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
347
- // For text types, use the helper to exclude non-color text utilities
348
- let isColorMatch: boolean
349
- if (key === 'text') {
350
- isColorMatch = isTextColorClass(cls)
351
- } else if (key === 'hoverText') {
352
- isColorMatch = cls.startsWith('hover:') && isTextColorClass(cls.slice(6))
353
- } else {
354
- isColorMatch = pattern.test(cls)
355
- }
356
- if (isColorMatch) {
357
- if (!(key in colorClasses)) {
358
- colorClasses[key] = { value: cls }
359
- }
360
- break
361
- }
362
- }
363
- }
364
-
365
- return colorClasses
185
+ return extractColorClasses(element.className) ?? {}
366
186
  }
367
187
 
368
188
  /**
@@ -401,30 +221,20 @@ export function applyColorChange(
401
221
  const classes = element.className.split(/\s+/).filter(Boolean)
402
222
  const pattern = COLOR_CLASS_PATTERNS[colorType]
403
223
 
404
- let oldClass: string | undefined
405
- const newClasses: string[] = []
406
-
407
224
  // Determine the new class prefix
408
225
  const prefix = colorType === 'hoverBg'
409
226
  ? 'hover:bg'
410
227
  : colorType === 'hoverText'
411
- ? 'hover:text'
412
- : colorType
228
+ ? 'hover:text'
229
+ : colorType
413
230
  const newClass = buildColorClass(prefix, newColorName, newShade)
414
231
 
232
+ let oldClass: string | undefined
233
+ const newClasses: string[] = []
234
+
415
235
  for (const cls of classes) {
416
- // For text types, use the helper to exclude non-color text utilities
417
- let isColorMatch: boolean
418
- if (colorType === 'text') {
419
- isColorMatch = isTextColorClass(cls)
420
- } else if (colorType === 'hoverText') {
421
- isColorMatch = cls.startsWith('hover:') && isTextColorClass(cls.slice(6))
422
- } else {
423
- isColorMatch = pattern.test(cls)
424
- }
425
- if (isColorMatch) {
236
+ if (pattern.test(cls)) {
426
237
  oldClass = cls
427
- // Replace with new class
428
238
  newClasses.push(newClass)
429
239
  } else {
430
240
  newClasses.push(cls)
@@ -456,10 +266,10 @@ export function applyColorChange(
456
266
  const styleProperty = colorType === 'bg'
457
267
  ? 'backgroundColor'
458
268
  : colorType === 'text'
459
- ? 'color'
460
- : colorType === 'border'
461
- ? 'borderColor'
462
- : 'color'
269
+ ? 'color'
270
+ : colorType === 'border'
271
+ ? 'borderColor'
272
+ : 'color'
463
273
  element.style[styleProperty] = cssValue
464
274
  }
465
275
  }
@@ -540,7 +350,7 @@ export function getAllColorsWithShades(availableColors: AvailableColors | undefi
540
350
 
541
351
  return availableColors.colors.map(color => {
542
352
  const shades = Object.keys(color.values)
543
- const isSpecial = SPECIAL_COLORS.includes(color.name as any)
353
+ const isSpecial = (SPECIAL_COLORS as readonly string[]).includes(color.name)
544
354
 
545
355
  return {
546
356
  name: color.name,
package/src/editor/dom.ts CHANGED
@@ -231,6 +231,15 @@ export function getEditableHtmlFromElement(el: HTMLElement): string {
231
231
  child.removeAttribute('contenteditable')
232
232
  })
233
233
 
234
+ // Clean up styled spans — strip editor-only attributes and inline preview styles
235
+ // (Tailwind classes are the source of truth for styling)
236
+ clone.querySelectorAll('[data-cms-styled]').forEach(span => {
237
+ span.removeAttribute('style')
238
+ span.removeAttribute('data-cms-styled')
239
+ span.removeAttribute('data-cms-hover-bg')
240
+ span.removeAttribute('data-cms-hover-text')
241
+ })
242
+
234
243
  return clone.innerHTML
235
244
  }
236
245