@nuasite/cms 0.7.0 → 0.7.2
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 +4583 -4573
- package/package.json +1 -1
- package/src/color-patterns.ts +312 -0
- package/src/editor/color-utils.ts +45 -235
- package/src/editor/components/block-editor.tsx +19 -6
- package/src/editor/components/outline.tsx +1 -1
- package/src/editor/dom.ts +9 -0
- package/src/tailwind-colors.ts +21 -310
|
@@ -79,33 +79,46 @@ export function BlockEditor({
|
|
|
79
79
|
|
|
80
80
|
const updatePosition = () => {
|
|
81
81
|
const editorWidth = LAYOUT.BLOCK_EDITOR_WIDTH
|
|
82
|
+
const editorHeight = LAYOUT.BLOCK_EDITOR_HEIGHT
|
|
82
83
|
const padding = LAYOUT.VIEWPORT_PADDING
|
|
83
84
|
const viewportWidth = window.innerWidth
|
|
84
85
|
const viewportHeight = window.innerHeight
|
|
85
86
|
|
|
86
87
|
let top: number
|
|
87
88
|
let left: number
|
|
89
|
+
let maxHeight: number
|
|
88
90
|
|
|
89
91
|
if (cursor) {
|
|
90
|
-
top = cursor.y
|
|
91
92
|
left = cursor.x
|
|
92
93
|
|
|
93
|
-
// Keep within viewport bounds
|
|
94
|
+
// Keep within viewport bounds horizontally
|
|
94
95
|
if (left + editorWidth > viewportWidth - padding) {
|
|
95
96
|
left = viewportWidth - editorWidth - padding
|
|
96
97
|
}
|
|
97
98
|
if (left < padding) {
|
|
98
99
|
left = padding
|
|
99
100
|
}
|
|
101
|
+
|
|
102
|
+
const spaceBelow = viewportHeight - cursor.y - padding
|
|
103
|
+
const spaceAbove = cursor.y - padding
|
|
104
|
+
|
|
105
|
+
if (spaceBelow >= editorHeight || spaceBelow >= spaceAbove) {
|
|
106
|
+
// Open below cursor
|
|
107
|
+
top = Math.max(padding, Math.min(cursor.y, viewportHeight - padding - 100))
|
|
108
|
+
maxHeight = viewportHeight - top - padding
|
|
109
|
+
} else {
|
|
110
|
+
// Open above cursor — anchor bottom of panel to cursor position
|
|
111
|
+
const panelHeight = Math.min(spaceAbove, editorHeight)
|
|
112
|
+
top = cursor.y - panelHeight
|
|
113
|
+
top = Math.max(padding, top)
|
|
114
|
+
maxHeight = cursor.y - top
|
|
115
|
+
}
|
|
100
116
|
} else {
|
|
101
117
|
top = viewportHeight / 2
|
|
102
118
|
left = (viewportWidth - editorWidth) / 2
|
|
119
|
+
maxHeight = viewportHeight - top - padding
|
|
103
120
|
}
|
|
104
121
|
|
|
105
|
-
// Clamp top so the panel never extends past the viewport bottom
|
|
106
|
-
top = Math.max(padding, Math.min(top, viewportHeight - padding - 100))
|
|
107
|
-
const maxHeight = viewportHeight - top - padding
|
|
108
|
-
|
|
109
122
|
setEditorPosition({ top, left, maxHeight })
|
|
110
123
|
}
|
|
111
124
|
|
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
|
|
package/src/tailwind-colors.ts
CHANGED
|
@@ -1,45 +1,26 @@
|
|
|
1
1
|
import fs from 'node:fs/promises'
|
|
2
2
|
import path from 'node:path'
|
|
3
|
+
import { DEFAULT_TAILWIND_COLORS, SPECIAL_COLORS } from './color-patterns'
|
|
3
4
|
import { getProjectRoot } from './config'
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
'cyan',
|
|
24
|
-
'sky',
|
|
25
|
-
'blue',
|
|
26
|
-
'indigo',
|
|
27
|
-
'violet',
|
|
28
|
-
'purple',
|
|
29
|
-
'fuchsia',
|
|
30
|
-
'pink',
|
|
31
|
-
'rose',
|
|
32
|
-
] as const
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Standard Tailwind color shades.
|
|
36
|
-
*/
|
|
37
|
-
export const STANDARD_SHADES = ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'] as const
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Special color values that don't have shades.
|
|
41
|
-
*/
|
|
42
|
-
export const SPECIAL_COLORS = ['transparent', 'current', 'inherit', 'white', 'black'] as const
|
|
5
|
+
import type { AvailableColors, AvailableTextStyles, TailwindColor, TextStyleValue } from './types'
|
|
6
|
+
|
|
7
|
+
// Re-export all shared detection logic from color-patterns
|
|
8
|
+
export {
|
|
9
|
+
buildColorClass,
|
|
10
|
+
COLOR_CLASS_PATTERNS,
|
|
11
|
+
DEFAULT_TAILWIND_COLORS,
|
|
12
|
+
extractColorClasses,
|
|
13
|
+
extractTextStyleClasses,
|
|
14
|
+
getColorType,
|
|
15
|
+
GRADIENT_CLASS_PATTERNS,
|
|
16
|
+
isColorClass,
|
|
17
|
+
OPACITY_CLASS_PATTERNS,
|
|
18
|
+
parseColorClass,
|
|
19
|
+
replaceColorClass,
|
|
20
|
+
SPECIAL_COLORS,
|
|
21
|
+
STANDARD_SHADES,
|
|
22
|
+
TEXT_STYLE_CLASS_PATTERNS,
|
|
23
|
+
} from './color-patterns'
|
|
43
24
|
|
|
44
25
|
/**
|
|
45
26
|
* Complete Tailwind v4 default color palette with all shade values.
|
|
@@ -396,71 +377,6 @@ const DEFAULT_FONT_STYLES: TextStyleValue[] = [
|
|
|
396
377
|
{ class: 'italic', label: 'Italic', css: { fontStyle: 'italic' } },
|
|
397
378
|
]
|
|
398
379
|
|
|
399
|
-
/**
|
|
400
|
-
* Non-color utility suffixes that should not be matched as custom colors.
|
|
401
|
-
* These follow the pattern `prefix-word-number` but are not colors.
|
|
402
|
-
*/
|
|
403
|
-
const NON_COLOR_SUFFIXES = ['opacity'] as const
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Build a regex pattern for matching color classes.
|
|
407
|
-
* Matches:
|
|
408
|
-
* - Default colors with optional shades: bg-blue-500, bg-white
|
|
409
|
-
* - Custom theme colors with shades: bg-primary-500
|
|
410
|
-
* - Arbitrary hex values: bg-[#41b883], bg-[#fff]
|
|
411
|
-
* - Arbitrary rgb/hsl values: bg-[rgb(255,0,0)], bg-[hsl(0,100%,50%)]
|
|
412
|
-
* Excludes non-color utilities like opacity.
|
|
413
|
-
*/
|
|
414
|
-
function buildColorPattern(prefix: string): RegExp {
|
|
415
|
-
const colorNames = [...DEFAULT_TAILWIND_COLORS, ...SPECIAL_COLORS].join('|')
|
|
416
|
-
const excluded = NON_COLOR_SUFFIXES.join('|')
|
|
417
|
-
// Arbitrary value patterns for colors
|
|
418
|
-
const arbitraryHex = '\\[#[0-9a-fA-F]{3,8}\\]'
|
|
419
|
-
const arbitraryFunc = '\\[(?:rgba?|hsla?)\\([^\\]]+\\)\\]'
|
|
420
|
-
// Match: prefix-(colorName[-shade]?) OR prefix-(customColor-shade) OR prefix-[arbitrary] but NOT prefix-(excluded-number)
|
|
421
|
-
return new RegExp(`^${prefix}-((?:${colorNames})(?:-(\\d+))?|(?!(?:${excluded})-)(\\w+)-(\\d+)|${arbitraryHex}|${arbitraryFunc})$`)
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Build a regex pattern for matching opacity classes.
|
|
426
|
-
*/
|
|
427
|
-
function buildOpacityPattern(prefix: string): RegExp {
|
|
428
|
-
return new RegExp(`^${prefix}-opacity-(\\d+)$`)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Regex patterns to match Tailwind color classes.
|
|
433
|
-
*/
|
|
434
|
-
const COLOR_CLASS_PATTERNS = {
|
|
435
|
-
bg: buildColorPattern('bg'),
|
|
436
|
-
text: buildColorPattern('text'),
|
|
437
|
-
border: buildColorPattern('border'),
|
|
438
|
-
hoverBg: buildColorPattern('hover:bg'),
|
|
439
|
-
hoverText: buildColorPattern('hover:text'),
|
|
440
|
-
hoverBorder: buildColorPattern('hover:border'),
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Regex patterns to match Tailwind opacity classes.
|
|
445
|
-
*/
|
|
446
|
-
const OPACITY_CLASS_PATTERNS = {
|
|
447
|
-
bgOpacity: buildOpacityPattern('bg'),
|
|
448
|
-
textOpacity: buildOpacityPattern('text'),
|
|
449
|
-
borderOpacity: buildOpacityPattern('border'),
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
/**
|
|
453
|
-
* Regex patterns to match Tailwind gradient color classes.
|
|
454
|
-
*/
|
|
455
|
-
const GRADIENT_CLASS_PATTERNS = {
|
|
456
|
-
from: buildColorPattern('from'),
|
|
457
|
-
via: buildColorPattern('via'),
|
|
458
|
-
to: buildColorPattern('to'),
|
|
459
|
-
hoverFrom: buildColorPattern('hover:from'),
|
|
460
|
-
hoverVia: buildColorPattern('hover:via'),
|
|
461
|
-
hoverTo: buildColorPattern('hover:to'),
|
|
462
|
-
}
|
|
463
|
-
|
|
464
380
|
/**
|
|
465
381
|
* Parse Tailwind v4 CSS config to extract available colors with their values.
|
|
466
382
|
*/
|
|
@@ -541,7 +457,7 @@ function extractColorsFromCss(content: string): TailwindColor[] {
|
|
|
541
457
|
if (!colorName || !value) continue
|
|
542
458
|
|
|
543
459
|
// Skip if it's a default color (we already have values for those)
|
|
544
|
-
if (DEFAULT_TAILWIND_COLORS.includes(colorName
|
|
460
|
+
if ((DEFAULT_TAILWIND_COLORS as readonly string[]).includes(colorName)) {
|
|
545
461
|
continue
|
|
546
462
|
}
|
|
547
463
|
|
|
@@ -700,208 +616,3 @@ function extractTextStylesFromCss(content: string): Partial<AvailableTextStyles>
|
|
|
700
616
|
fontSize: fontSizes.length > 0 ? fontSizes : undefined,
|
|
701
617
|
}
|
|
702
618
|
}
|
|
703
|
-
|
|
704
|
-
/** Flat key names for color class categories */
|
|
705
|
-
const COLOR_FLAT_KEYS: Record<string, string> = {
|
|
706
|
-
// COLOR_CLASS_PATTERNS keys map directly
|
|
707
|
-
bg: 'bg',
|
|
708
|
-
text: 'text',
|
|
709
|
-
border: 'border',
|
|
710
|
-
hoverBg: 'hoverBg',
|
|
711
|
-
hoverText: 'hoverText',
|
|
712
|
-
hoverBorder: 'hoverBorder',
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
const GRADIENT_FLAT_KEYS: Record<string, string> = {
|
|
716
|
-
from: 'gradientFrom',
|
|
717
|
-
via: 'gradientVia',
|
|
718
|
-
to: 'gradientTo',
|
|
719
|
-
hoverFrom: 'hoverGradientFrom',
|
|
720
|
-
hoverVia: 'hoverGradientVia',
|
|
721
|
-
hoverTo: 'hoverGradientTo',
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Extract color classes from an element's class attribute.
|
|
726
|
-
* Returns a flat Record<string, Attribute> with keys like bg, text, gradientFrom, bgOpacity, etc.
|
|
727
|
-
*/
|
|
728
|
-
export function extractColorClasses(classAttr: string | null | undefined): Record<string, Attribute> | undefined {
|
|
729
|
-
if (!classAttr) return undefined
|
|
730
|
-
|
|
731
|
-
const classes = classAttr.split(/\s+/).filter(Boolean)
|
|
732
|
-
const result: Record<string, Attribute> = {}
|
|
733
|
-
|
|
734
|
-
for (const cls of classes) {
|
|
735
|
-
let matched = false
|
|
736
|
-
|
|
737
|
-
// Check color patterns
|
|
738
|
-
for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
|
|
739
|
-
if (pattern.test(cls)) {
|
|
740
|
-
const flatKey = COLOR_FLAT_KEYS[key]
|
|
741
|
-
if (flatKey && !(flatKey in result)) {
|
|
742
|
-
result[flatKey] = { value: cls }
|
|
743
|
-
}
|
|
744
|
-
matched = true
|
|
745
|
-
break
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Check gradient patterns
|
|
750
|
-
if (!matched) {
|
|
751
|
-
for (const [key, pattern] of Object.entries(GRADIENT_CLASS_PATTERNS)) {
|
|
752
|
-
if (pattern.test(cls)) {
|
|
753
|
-
const flatKey = GRADIENT_FLAT_KEYS[key]
|
|
754
|
-
if (flatKey && !(flatKey in result)) {
|
|
755
|
-
result[flatKey] = { value: cls }
|
|
756
|
-
}
|
|
757
|
-
matched = true
|
|
758
|
-
break
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Check opacity patterns
|
|
764
|
-
if (!matched) {
|
|
765
|
-
for (const [key, pattern] of Object.entries(OPACITY_CLASS_PATTERNS)) {
|
|
766
|
-
if (pattern.test(cls)) {
|
|
767
|
-
if (!(key in result)) {
|
|
768
|
-
result[key] = { value: cls }
|
|
769
|
-
}
|
|
770
|
-
break
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
return Object.keys(result).length > 0 ? result : undefined
|
|
777
|
-
}
|
|
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
|
-
|
|
812
|
-
/**
|
|
813
|
-
* Check if a class is a color class (including gradient colors).
|
|
814
|
-
*/
|
|
815
|
-
export function isColorClass(className: string): boolean {
|
|
816
|
-
return Object.values(COLOR_CLASS_PATTERNS).some(pattern => pattern.test(className))
|
|
817
|
-
|| Object.values(GRADIENT_CLASS_PATTERNS).some(pattern => pattern.test(className))
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
/**
|
|
821
|
-
* Generate a new class string with a color class replaced.
|
|
822
|
-
*/
|
|
823
|
-
export function replaceColorClass(
|
|
824
|
-
currentClasses: string,
|
|
825
|
-
oldColorClass: string,
|
|
826
|
-
newColorClass: string,
|
|
827
|
-
): string {
|
|
828
|
-
const classes = currentClasses.split(/\s+/).filter(Boolean)
|
|
829
|
-
const newClasses = classes.map(cls => cls === oldColorClass ? newColorClass : cls)
|
|
830
|
-
return newClasses.join(' ')
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
/**
|
|
834
|
-
* Get the color type from a color class.
|
|
835
|
-
* Returns the flat key name (e.g., 'bg', 'gradientFrom', 'bgOpacity').
|
|
836
|
-
*/
|
|
837
|
-
export function getColorType(colorClass: string): string | undefined {
|
|
838
|
-
for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
|
|
839
|
-
if (pattern.test(colorClass)) {
|
|
840
|
-
return COLOR_FLAT_KEYS[key]
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
for (const [key, pattern] of Object.entries(GRADIENT_CLASS_PATTERNS)) {
|
|
844
|
-
if (pattern.test(colorClass)) {
|
|
845
|
-
return GRADIENT_FLAT_KEYS[key]
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
for (const [key, pattern] of Object.entries(OPACITY_CLASS_PATTERNS)) {
|
|
849
|
-
if (pattern.test(colorClass)) {
|
|
850
|
-
return key
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
return undefined
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Parse a color class into its components.
|
|
858
|
-
*/
|
|
859
|
-
export function parseColorClass(colorClass: string): {
|
|
860
|
-
prefix: string
|
|
861
|
-
colorName: string
|
|
862
|
-
shade?: string
|
|
863
|
-
isHover: boolean
|
|
864
|
-
isArbitrary?: boolean
|
|
865
|
-
} | undefined {
|
|
866
|
-
const isHover = colorClass.startsWith('hover:')
|
|
867
|
-
const classWithoutHover = isHover ? colorClass.slice(6) : colorClass
|
|
868
|
-
|
|
869
|
-
// Try matching standard color classes (default colors, custom theme colors, and gradients)
|
|
870
|
-
const standardMatch = classWithoutHover.match(/^(bg|text|border|from|via|to)-([a-z]+)(?:-(\d+))?$/)
|
|
871
|
-
if (standardMatch) {
|
|
872
|
-
return {
|
|
873
|
-
prefix: isHover ? `hover:${standardMatch[1]}` : standardMatch[1]!,
|
|
874
|
-
colorName: standardMatch[2]!,
|
|
875
|
-
shade: standardMatch[3],
|
|
876
|
-
isHover,
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Try matching arbitrary value classes like bg-[#41b883] or from-[#41b883]
|
|
881
|
-
const arbitraryMatch = classWithoutHover.match(/^(bg|text|border|from|via|to)-(\[.+\])$/)
|
|
882
|
-
if (arbitraryMatch) {
|
|
883
|
-
return {
|
|
884
|
-
prefix: isHover ? `hover:${arbitraryMatch[1]}` : arbitraryMatch[1]!,
|
|
885
|
-
colorName: arbitraryMatch[2]!,
|
|
886
|
-
shade: undefined,
|
|
887
|
-
isHover,
|
|
888
|
-
isArbitrary: true,
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
return undefined
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/**
|
|
896
|
-
* Build a color class from components.
|
|
897
|
-
*/
|
|
898
|
-
export function buildColorClass(
|
|
899
|
-
prefix: string,
|
|
900
|
-
colorName: string,
|
|
901
|
-
shade?: string,
|
|
902
|
-
): string {
|
|
903
|
-
if (shade) {
|
|
904
|
-
return `${prefix}-${colorName}-${shade}`
|
|
905
|
-
}
|
|
906
|
-
return `${prefix}-${colorName}`
|
|
907
|
-
}
|