@nuasite/cms 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/editor.js +7331 -7166
- package/package.json +1 -1
- package/src/build-processor.ts +6 -0
- package/src/editor/components/outline.tsx +165 -6
- package/src/editor/components/text-style-toolbar.tsx +1 -1
- package/src/editor/editor.ts +8 -8
- package/src/editor/index.tsx +96 -3
- package/src/editor/types.ts +2 -2
- package/src/handlers/source-writer.ts +6 -2
- package/src/html-processor.ts +8 -4
- package/src/source-finder/snippet-utils.ts +3 -0
- package/src/tailwind-colors.ts +33 -0
- package/src/types.ts +3 -0
package/package.json
CHANGED
package/src/build-processor.ts
CHANGED
|
@@ -403,6 +403,9 @@ async function processFile(
|
|
|
403
403
|
entry.variableName = mdSource.variableName
|
|
404
404
|
entry.collectionName = mdSource.collectionName
|
|
405
405
|
entry.collectionSlug = mdSource.collectionSlug
|
|
406
|
+
if (mdSource.variableName) {
|
|
407
|
+
entry.allowStyling = false
|
|
408
|
+
}
|
|
406
409
|
return
|
|
407
410
|
}
|
|
408
411
|
}
|
|
@@ -414,6 +417,9 @@ async function processFile(
|
|
|
414
417
|
entry.sourceLine = sourceLocation.line
|
|
415
418
|
entry.sourceSnippet = sourceLocation.snippet
|
|
416
419
|
entry.variableName = sourceLocation.variableName
|
|
420
|
+
if (sourceLocation.variableName) {
|
|
421
|
+
entry.allowStyling = false
|
|
422
|
+
}
|
|
417
423
|
|
|
418
424
|
// Update attribute and colorClasses source information if we have an opening tag
|
|
419
425
|
if (sourceLocation.openingTagSnippet) {
|
|
@@ -3,6 +3,7 @@ import { getColorPreview, parseColorClass } from '../color-utils'
|
|
|
3
3
|
import { Z_INDEX } from '../constants'
|
|
4
4
|
import { isPageDark } from '../dom'
|
|
5
5
|
import * as signals from '../signals'
|
|
6
|
+
import type { Attribute } from '../../types'
|
|
6
7
|
|
|
7
8
|
export interface OutlineProps {
|
|
8
9
|
visible: boolean
|
|
@@ -14,10 +15,14 @@ export interface OutlineProps {
|
|
|
14
15
|
element?: HTMLElement | null
|
|
15
16
|
/** CMS ID of the hovered element */
|
|
16
17
|
cmsId?: string | null
|
|
18
|
+
/** Current text style classes from pending changes (reactive) */
|
|
19
|
+
textStyleClasses?: Record<string, Attribute>
|
|
17
20
|
/** Callback when a color swatch is clicked */
|
|
18
21
|
onColorClick?: (cmsId: string, rect: DOMRect) => void
|
|
19
22
|
/** Callback when an attribute indicator is clicked */
|
|
20
23
|
onAttributeClick?: (cmsId: string, rect: DOMRect) => void
|
|
24
|
+
/** Callback when a text style toggle is clicked */
|
|
25
|
+
onTextStyleChange?: (cmsId: string, styleType: string, oldClass: string, newClass: string) => void
|
|
21
26
|
}
|
|
22
27
|
|
|
23
28
|
// Minimum space needed to show label outside the element
|
|
@@ -30,7 +35,7 @@ const STICKY_PADDING = 8
|
|
|
30
35
|
* Uses a custom element with Shadow DOM to avoid style conflicts.
|
|
31
36
|
*/
|
|
32
37
|
export function Outline(
|
|
33
|
-
{ visible, rect, isComponent = false, componentName, tagName, element, cmsId, onColorClick, onAttributeClick }: OutlineProps,
|
|
38
|
+
{ visible, rect, isComponent = false, componentName, tagName, element, cmsId, textStyleClasses, onColorClick, onAttributeClick, onTextStyleChange }: OutlineProps,
|
|
34
39
|
) {
|
|
35
40
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
36
41
|
const shadowRootRef = useRef<ShadowRoot | null>(null)
|
|
@@ -182,6 +187,60 @@ export function Outline(
|
|
|
182
187
|
.attr-button:hover svg {
|
|
183
188
|
color: #DFFF40;
|
|
184
189
|
}
|
|
190
|
+
|
|
191
|
+
.text-style-btn {
|
|
192
|
+
width: 28px;
|
|
193
|
+
height: 28px;
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
background: transparent;
|
|
198
|
+
border: 1px solid transparent;
|
|
199
|
+
border-radius: 6px;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
transition: all 150ms ease;
|
|
202
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
203
|
+
font-size: 13px;
|
|
204
|
+
color: rgba(255,255,255,0.7);
|
|
205
|
+
padding: 0;
|
|
206
|
+
line-height: 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.text-style-btn:hover {
|
|
210
|
+
background: rgba(255,255,255,0.1);
|
|
211
|
+
color: #DFFF40;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.text-style-btn.active {
|
|
215
|
+
background: rgba(223, 255, 64, 0.15);
|
|
216
|
+
border-color: rgba(223, 255, 64, 0.4);
|
|
217
|
+
color: #DFFF40;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.text-size-select {
|
|
221
|
+
height: 28px;
|
|
222
|
+
background: transparent;
|
|
223
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
224
|
+
border-radius: 6px;
|
|
225
|
+
color: rgba(255,255,255,0.7);
|
|
226
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
|
227
|
+
font-size: 11px;
|
|
228
|
+
padding: 0 4px;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition: all 150ms ease;
|
|
231
|
+
-webkit-appearance: none;
|
|
232
|
+
appearance: none;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.text-size-select:hover {
|
|
236
|
+
border-color: rgba(223, 255, 64, 0.4);
|
|
237
|
+
color: #DFFF40;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.text-size-select:focus {
|
|
241
|
+
outline: none;
|
|
242
|
+
border-color: #DFFF40;
|
|
243
|
+
}
|
|
185
244
|
`
|
|
186
245
|
|
|
187
246
|
overlayRef.current = document.createElement('div')
|
|
@@ -305,15 +364,15 @@ export function Outline(
|
|
|
305
364
|
|
|
306
365
|
// Check for color swatches and attribute button
|
|
307
366
|
const manifest = signals.manifest.value
|
|
308
|
-
const pendingColorChange = cmsId ? signals.pendingColorChanges.value.get(cmsId) : null
|
|
309
367
|
const entry = cmsId ? manifest.entries[cmsId] : null
|
|
310
|
-
const colorClasses =
|
|
368
|
+
const colorClasses = textStyleClasses ?? entry?.colorClasses
|
|
311
369
|
|
|
312
370
|
const hasColorSwatches = colorClasses && (colorClasses.bg?.value || colorClasses.text?.value) && onColorClick
|
|
313
371
|
const hasEditableAttributes = entry?.attributes && Object.keys(entry.attributes).length > 0
|
|
372
|
+
const needsElementLevelStyling = entry?.allowStyling === false && onTextStyleChange
|
|
314
373
|
|
|
315
|
-
// Show unified toolbar if there are swatches or
|
|
316
|
-
if ((hasColorSwatches || hasEditableAttributes) && (onColorClick || onAttributeClick)) {
|
|
374
|
+
// Show unified toolbar if there are swatches, attribute button, or element-level text styling
|
|
375
|
+
if ((hasColorSwatches || hasEditableAttributes || needsElementLevelStyling) && (onColorClick || onAttributeClick || onTextStyleChange)) {
|
|
317
376
|
toolbarRef.current.className = 'element-toolbar'
|
|
318
377
|
toolbarRef.current.innerHTML = ''
|
|
319
378
|
|
|
@@ -390,11 +449,111 @@ export function Outline(
|
|
|
390
449
|
}
|
|
391
450
|
toolbarRef.current.appendChild(attrButton)
|
|
392
451
|
}
|
|
452
|
+
|
|
453
|
+
// Add text style buttons for elements where inline styling is unavailable
|
|
454
|
+
if (needsElementLevelStyling && cmsId) {
|
|
455
|
+
// Add divider if there are other toolbar items before
|
|
456
|
+
if (hasColorSwatches || hasEditableAttributes) {
|
|
457
|
+
const divider = document.createElement('div')
|
|
458
|
+
divider.className = 'toolbar-divider'
|
|
459
|
+
toolbarRef.current.appendChild(divider)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const currentClasses = textStyleClasses ?? colorClasses ?? {}
|
|
463
|
+
|
|
464
|
+
// Bold toggle
|
|
465
|
+
const boldBtn = document.createElement('button')
|
|
466
|
+
const isBold = currentClasses.fontWeight?.value === 'font-bold'
|
|
467
|
+
boldBtn.className = `text-style-btn${isBold ? ' active' : ''}`
|
|
468
|
+
boldBtn.innerHTML = '<strong>B</strong>'
|
|
469
|
+
boldBtn.title = isBold ? 'Remove bold' : 'Bold'
|
|
470
|
+
boldBtn.onclick = (e) => {
|
|
471
|
+
e.stopPropagation()
|
|
472
|
+
const oldClass = currentClasses.fontWeight?.value || ''
|
|
473
|
+
const newClass = isBold ? 'font-normal' : 'font-bold'
|
|
474
|
+
onTextStyleChange!(cmsId!, 'fontWeight', oldClass, newClass)
|
|
475
|
+
}
|
|
476
|
+
toolbarRef.current.appendChild(boldBtn)
|
|
477
|
+
|
|
478
|
+
// Italic toggle
|
|
479
|
+
const italicBtn = document.createElement('button')
|
|
480
|
+
const isItalic = currentClasses.fontStyle?.value === 'italic'
|
|
481
|
+
italicBtn.className = `text-style-btn${isItalic ? ' active' : ''}`
|
|
482
|
+
italicBtn.innerHTML = '<em>I</em>'
|
|
483
|
+
italicBtn.title = isItalic ? 'Remove italic' : 'Italic'
|
|
484
|
+
italicBtn.onclick = (e) => {
|
|
485
|
+
e.stopPropagation()
|
|
486
|
+
const oldClass = currentClasses.fontStyle?.value || ''
|
|
487
|
+
const newClass = isItalic ? 'not-italic' : 'italic'
|
|
488
|
+
onTextStyleChange!(cmsId!, 'fontStyle', oldClass, newClass)
|
|
489
|
+
}
|
|
490
|
+
toolbarRef.current.appendChild(italicBtn)
|
|
491
|
+
|
|
492
|
+
// Underline toggle
|
|
493
|
+
const underlineBtn = document.createElement('button')
|
|
494
|
+
const isUnderline = currentClasses.textDecoration?.value === 'underline'
|
|
495
|
+
underlineBtn.className = `text-style-btn${isUnderline ? ' active' : ''}`
|
|
496
|
+
underlineBtn.innerHTML = '<span style="text-decoration:underline">U</span>'
|
|
497
|
+
underlineBtn.title = isUnderline ? 'Remove underline' : 'Underline'
|
|
498
|
+
underlineBtn.onclick = (e) => {
|
|
499
|
+
e.stopPropagation()
|
|
500
|
+
const oldClass = currentClasses.textDecoration?.value || ''
|
|
501
|
+
const newClass = isUnderline ? 'no-underline' : 'underline'
|
|
502
|
+
onTextStyleChange!(cmsId!, 'textDecoration', oldClass, newClass)
|
|
503
|
+
}
|
|
504
|
+
toolbarRef.current.appendChild(underlineBtn)
|
|
505
|
+
|
|
506
|
+
// Strikethrough toggle
|
|
507
|
+
const strikeBtn = document.createElement('button')
|
|
508
|
+
const isStrike = currentClasses.textDecoration?.value === 'line-through'
|
|
509
|
+
strikeBtn.className = `text-style-btn${isStrike ? ' active' : ''}`
|
|
510
|
+
strikeBtn.innerHTML = '<span style="text-decoration:line-through">S</span>'
|
|
511
|
+
strikeBtn.title = isStrike ? 'Remove strikethrough' : 'Strikethrough'
|
|
512
|
+
strikeBtn.onclick = (e) => {
|
|
513
|
+
e.stopPropagation()
|
|
514
|
+
const oldClass = currentClasses.textDecoration?.value || ''
|
|
515
|
+
const newClass = isStrike ? 'no-underline' : 'line-through'
|
|
516
|
+
onTextStyleChange!(cmsId!, 'textDecoration', oldClass, newClass)
|
|
517
|
+
}
|
|
518
|
+
toolbarRef.current.appendChild(strikeBtn)
|
|
519
|
+
|
|
520
|
+
// Font size dropdown
|
|
521
|
+
const sizeSelect = document.createElement('select')
|
|
522
|
+
sizeSelect.className = 'text-size-select'
|
|
523
|
+
sizeSelect.title = 'Font size'
|
|
524
|
+
const sizeOptions = [
|
|
525
|
+
{ value: '', label: 'Size' },
|
|
526
|
+
{ value: 'text-xs', label: 'XS' },
|
|
527
|
+
{ value: 'text-sm', label: 'SM' },
|
|
528
|
+
{ value: 'text-base', label: 'Base' },
|
|
529
|
+
{ value: 'text-lg', label: 'LG' },
|
|
530
|
+
{ value: 'text-xl', label: 'XL' },
|
|
531
|
+
{ value: 'text-2xl', label: '2XL' },
|
|
532
|
+
{ value: 'text-3xl', label: '3XL' },
|
|
533
|
+
]
|
|
534
|
+
const currentSize = currentClasses.fontSize?.value || ''
|
|
535
|
+
for (const opt of sizeOptions) {
|
|
536
|
+
const option = document.createElement('option')
|
|
537
|
+
option.value = opt.value
|
|
538
|
+
option.textContent = opt.label
|
|
539
|
+
if (opt.value === currentSize) option.selected = true
|
|
540
|
+
sizeSelect.appendChild(option)
|
|
541
|
+
}
|
|
542
|
+
sizeSelect.onchange = (e) => {
|
|
543
|
+
e.stopPropagation()
|
|
544
|
+
const newClass = (e.target as HTMLSelectElement).value
|
|
545
|
+
if (newClass) {
|
|
546
|
+
const oldClass = currentClasses.fontSize?.value || ''
|
|
547
|
+
onTextStyleChange!(cmsId!, 'fontSize', oldClass, newClass)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
toolbarRef.current.appendChild(sizeSelect)
|
|
551
|
+
}
|
|
393
552
|
} else {
|
|
394
553
|
toolbarRef.current.className = 'element-toolbar hidden'
|
|
395
554
|
}
|
|
396
555
|
}
|
|
397
|
-
}, [visible, rect, isComponent, componentName, tagName, cmsId, onColorClick, onAttributeClick])
|
|
556
|
+
}, [visible, rect, isComponent, componentName, tagName, cmsId, textStyleClasses, onColorClick, onAttributeClick, onTextStyleChange])
|
|
398
557
|
|
|
399
558
|
return (
|
|
400
559
|
<div
|
|
@@ -196,7 +196,7 @@ export function TextStyleToolbar({ visible, rect, element, onStyleChange }: Text
|
|
|
196
196
|
return (
|
|
197
197
|
<div
|
|
198
198
|
data-cms-ui
|
|
199
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
199
|
+
onMouseDown={(e) => { e.preventDefault(); e.stopPropagation() }}
|
|
200
200
|
onClick={(e) => e.stopPropagation()}
|
|
201
201
|
style={{
|
|
202
202
|
position: 'fixed',
|
package/src/editor/editor.ts
CHANGED
|
@@ -619,14 +619,14 @@ export async function saveAllChanges(
|
|
|
619
619
|
// For each color type that changed, add a separate change entry
|
|
620
620
|
const { originalClasses, newClasses } = change
|
|
621
621
|
const entry = manifest.entries[cmsId]
|
|
622
|
-
const
|
|
622
|
+
const classTypes = ['bg', 'text', 'border', 'hoverBg', 'hoverText', 'fontWeight', 'fontStyle', 'textDecoration', 'fontSize'] as const
|
|
623
623
|
|
|
624
624
|
// Find the best source info from any color type that has it
|
|
625
625
|
// (all color types share the same class attribute on the same element)
|
|
626
626
|
let sharedSourcePath: string | undefined
|
|
627
627
|
let sharedSourceLine: number | undefined
|
|
628
628
|
let sharedSourceSnippet: string | undefined
|
|
629
|
-
for (const ct of
|
|
629
|
+
for (const ct of classTypes) {
|
|
630
630
|
const orig = originalClasses[ct]
|
|
631
631
|
const curr = newClasses[ct]
|
|
632
632
|
const sp = curr?.sourcePath ?? orig?.sourcePath
|
|
@@ -639,10 +639,10 @@ export async function saveAllChanges(
|
|
|
639
639
|
}
|
|
640
640
|
}
|
|
641
641
|
|
|
642
|
-
for (const
|
|
643
|
-
const origAttr = originalClasses[
|
|
644
|
-
const newAttr = newClasses[
|
|
645
|
-
if (newAttr
|
|
642
|
+
for (const classType of classTypes) {
|
|
643
|
+
const origAttr = originalClasses[classType]
|
|
644
|
+
const newAttr = newClasses[classType]
|
|
645
|
+
if (newAttr && newAttr.value !== (origAttr?.value ?? '')) {
|
|
646
646
|
const bestSourcePath = newAttr.sourcePath ?? origAttr?.sourcePath ?? sharedSourcePath
|
|
647
647
|
const bestSourceLine = newAttr.sourceLine ?? origAttr?.sourceLine ?? sharedSourceLine
|
|
648
648
|
const bestSourceSnippet = newAttr.sourceSnippet ?? origAttr?.sourceSnippet ?? sharedSourceSnippet
|
|
@@ -656,7 +656,7 @@ export async function saveAllChanges(
|
|
|
656
656
|
colorChange: {
|
|
657
657
|
oldClass: origAttr?.value || '',
|
|
658
658
|
newClass: newAttr.value,
|
|
659
|
-
type:
|
|
659
|
+
type: classType,
|
|
660
660
|
sourcePath: bestSourcePath,
|
|
661
661
|
sourceLine: bestSourceLine,
|
|
662
662
|
sourceSnippet: bestSourceSnippet,
|
|
@@ -1033,7 +1033,7 @@ function setupColorTracking(
|
|
|
1033
1033
|
export function handleColorChange(
|
|
1034
1034
|
config: CmsConfig,
|
|
1035
1035
|
cmsId: string,
|
|
1036
|
-
colorType:
|
|
1036
|
+
colorType: string,
|
|
1037
1037
|
oldClass: string,
|
|
1038
1038
|
newClass: string,
|
|
1039
1039
|
onStateChange?: () => void,
|
package/src/editor/index.tsx
CHANGED
|
@@ -55,6 +55,26 @@ import { hasPendingEntryNavigation, loadEditingState, loadSettingsFromStorage, s
|
|
|
55
55
|
import CMS_STYLES from './styles.css?inline'
|
|
56
56
|
import { generateCSSVariables, resolveTheme } from './themes'
|
|
57
57
|
|
|
58
|
+
/** Inline CSS values for Tailwind text style classes (for preview before save) */
|
|
59
|
+
const TEXT_STYLE_INLINE_CSS: Record<string, Record<string, string>> = {
|
|
60
|
+
'font-normal': { fontWeight: '400' },
|
|
61
|
+
'font-medium': { fontWeight: '500' },
|
|
62
|
+
'font-semibold': { fontWeight: '600' },
|
|
63
|
+
'font-bold': { fontWeight: '700' },
|
|
64
|
+
'italic': { fontStyle: 'italic' },
|
|
65
|
+
'not-italic': { fontStyle: 'normal' },
|
|
66
|
+
'underline': { textDecoration: 'underline' },
|
|
67
|
+
'line-through': { textDecoration: 'line-through' },
|
|
68
|
+
'no-underline': { textDecoration: 'none' },
|
|
69
|
+
'text-xs': { fontSize: '0.75rem', lineHeight: '1rem' },
|
|
70
|
+
'text-sm': { fontSize: '0.875rem', lineHeight: '1.25rem' },
|
|
71
|
+
'text-base': { fontSize: '1rem', lineHeight: '1.5rem' },
|
|
72
|
+
'text-lg': { fontSize: '1.125rem', lineHeight: '1.75rem' },
|
|
73
|
+
'text-xl': { fontSize: '1.25rem', lineHeight: '1.75rem' },
|
|
74
|
+
'text-2xl': { fontSize: '1.5rem', lineHeight: '2rem' },
|
|
75
|
+
'text-3xl': { fontSize: '1.875rem', lineHeight: '2.25rem' },
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
const CmsUI = () => {
|
|
59
79
|
const config = signals.config.value
|
|
60
80
|
const outlineState = useElementDetection()
|
|
@@ -218,6 +238,65 @@ const CmsUI = () => {
|
|
|
218
238
|
signals.openAttributeEditor(cmsId, rect)
|
|
219
239
|
}, [])
|
|
220
240
|
|
|
241
|
+
// Handle text style change from outline (element-level styling via class toggle)
|
|
242
|
+
const handleOutlineTextStyleChange = useCallback((cmsId: string, styleType: string, oldClass: string, newClass: string) => {
|
|
243
|
+
let change = signals.pendingColorChanges.value.get(cmsId)
|
|
244
|
+
|
|
245
|
+
// Create pending color change entry if it doesn't exist yet
|
|
246
|
+
// (elements with allowStyling=false may lack colorClasses but still need text style tracking)
|
|
247
|
+
if (!change) {
|
|
248
|
+
const el = document.querySelector(`[data-cms-id="${cmsId}"]`) as HTMLElement
|
|
249
|
+
if (!el) return
|
|
250
|
+
|
|
251
|
+
const entry = signals.manifest.value.entries[cmsId]
|
|
252
|
+
const originalClasses: Record<string, import('../types').Attribute> = {}
|
|
253
|
+
const newClasses: Record<string, import('../types').Attribute> = {}
|
|
254
|
+
if (entry?.colorClasses) {
|
|
255
|
+
for (const [key, attr] of Object.entries(entry.colorClasses)) {
|
|
256
|
+
originalClasses[key] = { ...attr }
|
|
257
|
+
newClasses[key] = { ...attr }
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
signals.setPendingColorChange(cmsId, {
|
|
262
|
+
element: el,
|
|
263
|
+
cmsId,
|
|
264
|
+
originalClasses,
|
|
265
|
+
newClasses,
|
|
266
|
+
isDirty: false,
|
|
267
|
+
})
|
|
268
|
+
change = signals.pendingColorChanges.value.get(cmsId)!
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Apply the class change on the DOM element
|
|
272
|
+
const el = change.element
|
|
273
|
+
const previousClassName = el.className
|
|
274
|
+
const previousStyleCssText = el.style.cssText
|
|
275
|
+
|
|
276
|
+
if (oldClass) {
|
|
277
|
+
el.classList.remove(oldClass)
|
|
278
|
+
const oldCss = TEXT_STYLE_INLINE_CSS[oldClass]
|
|
279
|
+
if (oldCss) {
|
|
280
|
+
for (const prop of Object.keys(oldCss)) {
|
|
281
|
+
;(el.style as any)[prop] = ''
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
el.classList.add(newClass)
|
|
286
|
+
|
|
287
|
+
// Apply inline styles for immediate visual preview
|
|
288
|
+
// (Tailwind classes not present in source won't be in the compiled CSS)
|
|
289
|
+
const newCss = TEXT_STYLE_INLINE_CSS[newClass]
|
|
290
|
+
if (newCss) {
|
|
291
|
+
for (const [prop, value] of Object.entries(newCss)) {
|
|
292
|
+
;(el.style as any)[prop] = value
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Delegate to handleColorChange (same class-replacement mechanism)
|
|
297
|
+
handleColorChange(config, cmsId, styleType, oldClass, newClass, updateUI, previousClassName, previousStyleCssText)
|
|
298
|
+
}, [config, updateUI])
|
|
299
|
+
|
|
221
300
|
// Handle attribute editor close
|
|
222
301
|
const handleAttributeEditorClose = useCallback(() => {
|
|
223
302
|
signals.closeAttributeEditor()
|
|
@@ -235,12 +314,24 @@ const CmsUI = () => {
|
|
|
235
314
|
const showEditableHighlights = signals.showEditableHighlights.value
|
|
236
315
|
const hasSeoData = !!(manifest as any).seo
|
|
237
316
|
|
|
317
|
+
// Check if selected text element allows inline styling
|
|
318
|
+
const selectedElementCmsId = textSelectionState.element?.getAttribute('data-cms-id')
|
|
319
|
+
const selectedEntry = selectedElementCmsId ? manifest.entries[selectedElementCmsId] : undefined
|
|
320
|
+
const isTextStylingAllowed = selectedEntry?.allowStyling !== false
|
|
321
|
+
|
|
238
322
|
// Get color toolbar data
|
|
323
|
+
const pendingColorChanges = signals.pendingColorChanges.value
|
|
239
324
|
const colorEditorElement = colorEditorState.targetElementId
|
|
240
|
-
?
|
|
325
|
+
? pendingColorChanges.get(colorEditorState.targetElementId)?.element ?? null
|
|
241
326
|
: null
|
|
242
327
|
const colorEditorCurrentClasses = colorEditorState.targetElementId
|
|
243
|
-
?
|
|
328
|
+
? pendingColorChanges.get(colorEditorState.targetElementId)?.newClasses
|
|
329
|
+
: undefined
|
|
330
|
+
|
|
331
|
+
// Get current text style classes for the outlined element (reactive - triggers re-render on change)
|
|
332
|
+
const outlineCmsId = outlineState.cmsId
|
|
333
|
+
const outlineTextStyleClasses = outlineCmsId
|
|
334
|
+
? pendingColorChanges.get(outlineCmsId)?.newClasses
|
|
244
335
|
: undefined
|
|
245
336
|
|
|
246
337
|
return (
|
|
@@ -258,8 +349,10 @@ const CmsUI = () => {
|
|
|
258
349
|
tagName={outlineState.tagName}
|
|
259
350
|
element={outlineState.element}
|
|
260
351
|
cmsId={outlineState.cmsId}
|
|
352
|
+
textStyleClasses={outlineTextStyleClasses}
|
|
261
353
|
onColorClick={handleOutlineColorClick}
|
|
262
354
|
onAttributeClick={handleOutlineAttributeClick}
|
|
355
|
+
onTextStyleChange={handleOutlineTextStyleChange}
|
|
263
356
|
/>
|
|
264
357
|
</ErrorBoundary>
|
|
265
358
|
|
|
@@ -309,7 +402,7 @@ const CmsUI = () => {
|
|
|
309
402
|
|
|
310
403
|
<ErrorBoundary componentName="Text Style Toolbar">
|
|
311
404
|
<TextStyleToolbar
|
|
312
|
-
visible={textSelectionState.hasSelection && isEditing && !isAIProcessing}
|
|
405
|
+
visible={textSelectionState.hasSelection && isEditing && !isAIProcessing && isTextStylingAllowed}
|
|
313
406
|
rect={textSelectionState.rect}
|
|
314
407
|
element={textSelectionState.element}
|
|
315
408
|
onStyleChange={updateUI}
|
package/src/editor/types.ts
CHANGED
|
@@ -138,8 +138,8 @@ export interface ColorChangePayload {
|
|
|
138
138
|
oldClass: string
|
|
139
139
|
/** The new color class (e.g., 'bg-red-500') */
|
|
140
140
|
newClass: string
|
|
141
|
-
/** Type of color change
|
|
142
|
-
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText'
|
|
141
|
+
/** Type of color/style change */
|
|
142
|
+
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText' | 'fontWeight' | 'fontStyle' | 'textDecoration' | 'fontSize'
|
|
143
143
|
/** Path to the source file where the color class is defined */
|
|
144
144
|
sourcePath?: string
|
|
145
145
|
/** Line number where the color class is defined */
|
|
@@ -8,7 +8,7 @@ import { acquireFileLock, escapeReplacement, normalizePagePath, resolveAndValida
|
|
|
8
8
|
export interface ColorChangePayload {
|
|
9
9
|
oldClass: string
|
|
10
10
|
newClass: string
|
|
11
|
-
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText'
|
|
11
|
+
type: 'bg' | 'text' | 'border' | 'hoverBg' | 'hoverText' | 'fontWeight' | 'fontStyle' | 'textDecoration' | 'fontSize'
|
|
12
12
|
sourcePath?: string
|
|
13
13
|
sourceLine?: number
|
|
14
14
|
sourceSnippet?: string
|
|
@@ -360,7 +360,11 @@ function replaceClassInAttribute(
|
|
|
360
360
|
const idx = classes.indexOf(oldClass)
|
|
361
361
|
if (idx === -1) return null
|
|
362
362
|
|
|
363
|
-
|
|
363
|
+
if (newClass) {
|
|
364
|
+
classes[idx] = newClass
|
|
365
|
+
} else {
|
|
366
|
+
classes.splice(idx, 1)
|
|
367
|
+
}
|
|
364
368
|
return line.replace(classAttrPattern, `${prefix}${quote}${classes.join(' ')}${quote}`)
|
|
365
369
|
}
|
|
366
370
|
|
package/src/html-processor.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type HTMLElement as ParsedHTMLElement, parse } from 'node-html-parser'
|
|
2
2
|
import { processSeoFromHtml } from './seo-processor'
|
|
3
3
|
import { enhanceManifestWithSourceSnippets } from './source-finder'
|
|
4
|
-
import { extractColorClasses } from './tailwind-colors'
|
|
4
|
+
import { extractColorClasses, extractTextStyleClasses } from './tailwind-colors'
|
|
5
5
|
import type { Attribute, ComponentInstance, ImageMetadata, ManifestEntry, PageSeoData, SeoOptions } from './types'
|
|
6
6
|
import { generateStableId } from './utils'
|
|
7
7
|
|
|
@@ -830,9 +830,13 @@ export async function processHtml(
|
|
|
830
830
|
// Generate stable ID based on content and context
|
|
831
831
|
const stableId = generateStableId(tag, entryText, entrySourcePath)
|
|
832
832
|
|
|
833
|
-
// Extract color classes for buttons and other elements
|
|
833
|
+
// Extract color classes and text style classes for buttons and other elements
|
|
834
834
|
const classAttr = node.getAttribute('class')
|
|
835
835
|
const colorClasses = extractColorClasses(classAttr)
|
|
836
|
+
const textStyleClasses = extractTextStyleClasses(classAttr)
|
|
837
|
+
const allTrackedClasses = colorClasses || textStyleClasses
|
|
838
|
+
? { ...colorClasses, ...textStyleClasses }
|
|
839
|
+
: undefined
|
|
836
840
|
|
|
837
841
|
// Extract all relevant attributes for git diff tracking
|
|
838
842
|
const attributes = extractAllAttributes(node)
|
|
@@ -856,8 +860,8 @@ export async function processHtml(
|
|
|
856
860
|
stableId,
|
|
857
861
|
// Image metadata for image entries
|
|
858
862
|
imageMetadata: imageInfo?.metadata,
|
|
859
|
-
// Color classes for buttons/styled elements
|
|
860
|
-
colorClasses,
|
|
863
|
+
// Color and text style classes for buttons/styled elements
|
|
864
|
+
colorClasses: allTrackedClasses,
|
|
861
865
|
// All attributes with resolved values (isStatic will be updated later from source)
|
|
862
866
|
attributes,
|
|
863
867
|
}
|
|
@@ -618,6 +618,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
618
618
|
sourceLine: matchingDef.line,
|
|
619
619
|
sourceSnippet: defSnippet,
|
|
620
620
|
variableName: buildDefinitionPath(matchingDef),
|
|
621
|
+
allowStyling: false,
|
|
621
622
|
attributes,
|
|
622
623
|
colorClasses,
|
|
623
624
|
sourceHash,
|
|
@@ -666,6 +667,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
666
667
|
sourceLine: result.line,
|
|
667
668
|
sourceSnippet: propSnippet,
|
|
668
669
|
variableName: result.variableName,
|
|
670
|
+
allowStyling: false,
|
|
669
671
|
attributes,
|
|
670
672
|
colorClasses,
|
|
671
673
|
sourceHash: propSourceHash,
|
|
@@ -691,6 +693,7 @@ export async function enhanceManifestWithSourceSnippets(
|
|
|
691
693
|
sourceLine: result.line,
|
|
692
694
|
sourceSnippet: parentSnippet,
|
|
693
695
|
variableName: result.variableName,
|
|
696
|
+
allowStyling: false,
|
|
694
697
|
attributes,
|
|
695
698
|
colorClasses,
|
|
696
699
|
sourceHash: propSourceHash,
|
package/src/tailwind-colors.ts
CHANGED
|
@@ -776,6 +776,39 @@ export function extractColorClasses(classAttr: string | null | undefined): Recor
|
|
|
776
776
|
return Object.keys(result).length > 0 ? result : undefined
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Regex patterns for matching text style classes on elements.
|
|
781
|
+
*/
|
|
782
|
+
const TEXT_STYLE_CLASS_PATTERNS: Record<string, RegExp> = {
|
|
783
|
+
fontWeight: /^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,
|
|
784
|
+
fontStyle: /^(italic|not-italic)$/,
|
|
785
|
+
textDecoration: /^(underline|overline|line-through|no-underline)$/,
|
|
786
|
+
fontSize: /^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Extract text style classes from an element's class attribute.
|
|
791
|
+
* Returns a Record<string, Attribute> with keys: fontWeight, fontStyle, textDecoration, fontSize.
|
|
792
|
+
* Pattern: same as extractColorClasses — scan classes, match patterns, return first match per category.
|
|
793
|
+
*/
|
|
794
|
+
export function extractTextStyleClasses(classAttr: string | null | undefined): Record<string, Attribute> | undefined {
|
|
795
|
+
if (!classAttr) return undefined
|
|
796
|
+
|
|
797
|
+
const classes = classAttr.split(/\s+/).filter(Boolean)
|
|
798
|
+
const result: Record<string, Attribute> = {}
|
|
799
|
+
|
|
800
|
+
for (const cls of classes) {
|
|
801
|
+
for (const [key, pattern] of Object.entries(TEXT_STYLE_CLASS_PATTERNS)) {
|
|
802
|
+
if (pattern.test(cls) && !(key in result)) {
|
|
803
|
+
result[key] = { value: cls }
|
|
804
|
+
break
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return Object.keys(result).length > 0 ? result : undefined
|
|
810
|
+
}
|
|
811
|
+
|
|
779
812
|
/**
|
|
780
813
|
* Check if a class is a color class (including gradient colors).
|
|
781
814
|
*/
|
package/src/types.ts
CHANGED
|
@@ -161,6 +161,9 @@ export interface ManifestEntry {
|
|
|
161
161
|
colorClasses?: Record<string, Attribute>
|
|
162
162
|
/** All HTML attributes with source information */
|
|
163
163
|
attributes?: Record<string, Attribute>
|
|
164
|
+
/** Whether inline text styling (bold, italic, etc.) can be applied.
|
|
165
|
+
* False when text comes from a string variable/prop that cannot contain HTML markup. */
|
|
166
|
+
allowStyling?: boolean
|
|
164
167
|
}
|
|
165
168
|
|
|
166
169
|
export interface ComponentInstance {
|