@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.
@@ -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
 
@@ -121,7 +121,7 @@ export function Outline(
121
121
  .element-toolbar::before {
122
122
  content: '';
123
123
  position: absolute;
124
- top: -25px;
124
+ top: -13px;
125
125
  left: -50px;
126
126
  right: -50px;
127
127
  bottom: 0;
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
 
@@ -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 { Attribute, AvailableColors, AvailableTextStyles, TailwindColor, TextStyleValue } from './types'
5
-
6
- /**
7
- * Default Tailwind CSS v4 color names.
8
- */
9
- export const DEFAULT_TAILWIND_COLORS = [
10
- 'slate',
11
- 'gray',
12
- 'zinc',
13
- 'neutral',
14
- 'stone',
15
- 'red',
16
- 'orange',
17
- 'amber',
18
- 'yellow',
19
- 'lime',
20
- 'green',
21
- 'emerald',
22
- 'teal',
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 as any)) {
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
- }