@nuasite/cms 0.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 (269) hide show
  1. package/README.md +237 -0
  2. package/dist/src/build-processor.d.ts +20 -0
  3. package/dist/src/build-processor.d.ts.map +1 -0
  4. package/dist/src/collection-scanner.d.ts +6 -0
  5. package/dist/src/collection-scanner.d.ts.map +1 -0
  6. package/dist/src/component-registry.d.ts +63 -0
  7. package/dist/src/component-registry.d.ts.map +1 -0
  8. package/dist/src/config.d.ts +24 -0
  9. package/dist/src/config.d.ts.map +1 -0
  10. package/dist/src/dev-middleware.d.ts +20 -0
  11. package/dist/src/dev-middleware.d.ts.map +1 -0
  12. package/dist/src/editor/ai.d.ts +60 -0
  13. package/dist/src/editor/ai.d.ts.map +1 -0
  14. package/dist/src/editor/api.d.ts +140 -0
  15. package/dist/src/editor/api.d.ts.map +1 -0
  16. package/dist/src/editor/color-utils.d.ts +106 -0
  17. package/dist/src/editor/color-utils.d.ts.map +1 -0
  18. package/dist/src/editor/components/ai-chat.d.ts +11 -0
  19. package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
  20. package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
  21. package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
  22. package/dist/src/editor/components/attribute-editor.d.ts +5 -0
  23. package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
  24. package/dist/src/editor/components/block-editor.d.ts +12 -0
  25. package/dist/src/editor/components/block-editor.d.ts.map +1 -0
  26. package/dist/src/editor/components/collections-browser.d.ts +2 -0
  27. package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
  28. package/dist/src/editor/components/color-toolbar.d.ts +12 -0
  29. package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
  30. package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
  31. package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
  32. package/dist/src/editor/components/create-page-modal.d.ts +2 -0
  33. package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
  34. package/dist/src/editor/components/editable-highlights.d.ts +9 -0
  35. package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
  36. package/dist/src/editor/components/error-boundary.d.ts +32 -0
  37. package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
  38. package/dist/src/editor/components/fields.d.ts +75 -0
  39. package/dist/src/editor/components/fields.d.ts.map +1 -0
  40. package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
  41. package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
  42. package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
  43. package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
  44. package/dist/src/editor/components/image-overlay.d.ts +12 -0
  45. package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
  46. package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
  47. package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
  48. package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
  49. package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
  50. package/dist/src/editor/components/media-library.d.ts +2 -0
  51. package/dist/src/editor/components/media-library.d.ts.map +1 -0
  52. package/dist/src/editor/components/outline.d.ts +21 -0
  53. package/dist/src/editor/components/outline.d.ts.map +1 -0
  54. package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
  55. package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
  56. package/dist/src/editor/components/seo-editor.d.ts +2 -0
  57. package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
  58. package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
  59. package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
  60. package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
  61. package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
  62. package/dist/src/editor/components/toast/toast.d.ts +7 -0
  63. package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
  64. package/dist/src/editor/components/toast/types.d.ts +7 -0
  65. package/dist/src/editor/components/toast/types.d.ts.map +1 -0
  66. package/dist/src/editor/components/toolbar.d.ts +21 -0
  67. package/dist/src/editor/components/toolbar.d.ts.map +1 -0
  68. package/dist/src/editor/config.d.ts +4 -0
  69. package/dist/src/editor/config.d.ts.map +1 -0
  70. package/dist/src/editor/constants.d.ts +101 -0
  71. package/dist/src/editor/constants.d.ts.map +1 -0
  72. package/dist/src/editor/context.d.ts +14 -0
  73. package/dist/src/editor/context.d.ts.map +1 -0
  74. package/dist/src/editor/dom.d.ts +77 -0
  75. package/dist/src/editor/dom.d.ts.map +1 -0
  76. package/dist/src/editor/editor.d.ts +64 -0
  77. package/dist/src/editor/editor.d.ts.map +1 -0
  78. package/dist/src/editor/history.d.ts +20 -0
  79. package/dist/src/editor/history.d.ts.map +1 -0
  80. package/dist/src/editor/hooks/index.d.ts +14 -0
  81. package/dist/src/editor/hooks/index.d.ts.map +1 -0
  82. package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
  83. package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
  84. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
  85. package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
  86. package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
  87. package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
  88. package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
  89. package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
  90. package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
  91. package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
  92. package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
  93. package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
  94. package/dist/src/editor/hooks/utils.d.ts +32 -0
  95. package/dist/src/editor/hooks/utils.d.ts.map +1 -0
  96. package/dist/src/editor/index.d.ts +12 -0
  97. package/dist/src/editor/index.d.ts.map +1 -0
  98. package/dist/src/editor/lib/cn.d.ts +3 -0
  99. package/dist/src/editor/lib/cn.d.ts.map +1 -0
  100. package/dist/src/editor/manifest.d.ts +19 -0
  101. package/dist/src/editor/manifest.d.ts.map +1 -0
  102. package/dist/src/editor/markdown-api.d.ts +36 -0
  103. package/dist/src/editor/markdown-api.d.ts.map +1 -0
  104. package/dist/src/editor/signals.d.ts +242 -0
  105. package/dist/src/editor/signals.d.ts.map +1 -0
  106. package/dist/src/editor/storage.d.ts +27 -0
  107. package/dist/src/editor/storage.d.ts.map +1 -0
  108. package/dist/src/editor/text-styling.d.ts +350 -0
  109. package/dist/src/editor/text-styling.d.ts.map +1 -0
  110. package/dist/src/editor/themes.d.ts +38 -0
  111. package/dist/src/editor/themes.d.ts.map +1 -0
  112. package/dist/src/editor/types.d.ts +454 -0
  113. package/dist/src/editor/types.d.ts.map +1 -0
  114. package/dist/src/error-collector.d.ts +56 -0
  115. package/dist/src/error-collector.d.ts.map +1 -0
  116. package/dist/src/handlers/component-ops.d.ts +34 -0
  117. package/dist/src/handlers/component-ops.d.ts.map +1 -0
  118. package/dist/src/handlers/markdown-ops.d.ts +41 -0
  119. package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
  120. package/dist/src/handlers/request-utils.d.ts +20 -0
  121. package/dist/src/handlers/request-utils.d.ts.map +1 -0
  122. package/dist/src/handlers/source-writer.d.ts +51 -0
  123. package/dist/src/handlers/source-writer.d.ts.map +1 -0
  124. package/dist/src/html-processor.d.ts +63 -0
  125. package/dist/src/html-processor.d.ts.map +1 -0
  126. package/dist/src/index.d.ts +41 -0
  127. package/dist/src/index.d.ts.map +1 -0
  128. package/dist/src/manifest-writer.d.ts +111 -0
  129. package/dist/src/manifest-writer.d.ts.map +1 -0
  130. package/dist/src/media/contember.d.ts +15 -0
  131. package/dist/src/media/contember.d.ts.map +1 -0
  132. package/dist/src/media/local.d.ts +9 -0
  133. package/dist/src/media/local.d.ts.map +1 -0
  134. package/dist/src/media/s3.d.ts +12 -0
  135. package/dist/src/media/s3.d.ts.map +1 -0
  136. package/dist/src/media/types.d.ts +40 -0
  137. package/dist/src/media/types.d.ts.map +1 -0
  138. package/dist/src/preview-generator.d.ts +19 -0
  139. package/dist/src/preview-generator.d.ts.map +1 -0
  140. package/dist/src/seo-processor.d.ts +23 -0
  141. package/dist/src/seo-processor.d.ts.map +1 -0
  142. package/dist/src/source-finder/ast-extractors.d.ts +35 -0
  143. package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
  144. package/dist/src/source-finder/ast-parser.d.ts +16 -0
  145. package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
  146. package/dist/src/source-finder/cache.d.ts +18 -0
  147. package/dist/src/source-finder/cache.d.ts.map +1 -0
  148. package/dist/src/source-finder/collection-finder.d.ts +29 -0
  149. package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
  150. package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
  151. package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
  152. package/dist/src/source-finder/element-finder.d.ts +42 -0
  153. package/dist/src/source-finder/element-finder.d.ts.map +1 -0
  154. package/dist/src/source-finder/image-finder.d.ts +24 -0
  155. package/dist/src/source-finder/image-finder.d.ts.map +1 -0
  156. package/dist/src/source-finder/index.d.ts +9 -0
  157. package/dist/src/source-finder/index.d.ts.map +1 -0
  158. package/dist/src/source-finder/search-index.d.ts +27 -0
  159. package/dist/src/source-finder/search-index.d.ts.map +1 -0
  160. package/dist/src/source-finder/snippet-utils.d.ts +90 -0
  161. package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
  162. package/dist/src/source-finder/source-lookup.d.ts +16 -0
  163. package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
  164. package/dist/src/source-finder/types.d.ts +167 -0
  165. package/dist/src/source-finder/types.d.ts.map +1 -0
  166. package/dist/src/source-finder/variable-extraction.d.ts +37 -0
  167. package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
  168. package/dist/src/tailwind-colors.d.ts +54 -0
  169. package/dist/src/tailwind-colors.d.ts.map +1 -0
  170. package/dist/src/tsconfig.tsbuildinfo +1 -0
  171. package/dist/src/types.d.ts +367 -0
  172. package/dist/src/types.d.ts.map +1 -0
  173. package/dist/src/utils.d.ts +61 -0
  174. package/dist/src/utils.d.ts.map +1 -0
  175. package/dist/src/vite-plugin.d.ts +14 -0
  176. package/dist/src/vite-plugin.d.ts.map +1 -0
  177. package/dist/types/tsconfig.tsbuildinfo +1 -0
  178. package/package.json +80 -0
  179. package/src/build-processor.ts +784 -0
  180. package/src/collection-scanner.ts +304 -0
  181. package/src/component-registry.ts +393 -0
  182. package/src/config.ts +74 -0
  183. package/src/dev-middleware.ts +525 -0
  184. package/src/dist/src/tsconfig.tsbuildinfo +1 -0
  185. package/src/editor/ai.ts +185 -0
  186. package/src/editor/api.ts +513 -0
  187. package/src/editor/color-utils.ts +556 -0
  188. package/src/editor/components/ai-chat.tsx +632 -0
  189. package/src/editor/components/ai-tooltip.tsx +179 -0
  190. package/src/editor/components/attribute-editor.tsx +596 -0
  191. package/src/editor/components/block-editor.tsx +546 -0
  192. package/src/editor/components/collections-browser.tsx +248 -0
  193. package/src/editor/components/color-toolbar.tsx +314 -0
  194. package/src/editor/components/confirm-dialog.tsx +69 -0
  195. package/src/editor/components/create-page-modal.tsx +163 -0
  196. package/src/editor/components/editable-highlights.tsx +260 -0
  197. package/src/editor/components/error-boundary.tsx +87 -0
  198. package/src/editor/components/fields.tsx +387 -0
  199. package/src/editor/components/frontmatter-fields.tsx +469 -0
  200. package/src/editor/components/highlight-overlay.ts +229 -0
  201. package/src/editor/components/image-overlay.tsx +230 -0
  202. package/src/editor/components/markdown-editor-overlay.tsx +505 -0
  203. package/src/editor/components/markdown-inline-editor.tsx +780 -0
  204. package/src/editor/components/media-library.tsx +297 -0
  205. package/src/editor/components/outline.tsx +402 -0
  206. package/src/editor/components/redirect-countdown.tsx +45 -0
  207. package/src/editor/components/seo-editor.tsx +498 -0
  208. package/src/editor/components/text-style-toolbar.tsx +362 -0
  209. package/src/editor/components/toast/toast-container.tsx +15 -0
  210. package/src/editor/components/toast/toast.tsx +49 -0
  211. package/src/editor/components/toast/types.ts +7 -0
  212. package/src/editor/components/toolbar.tsx +366 -0
  213. package/src/editor/config.ts +12 -0
  214. package/src/editor/constants.ts +106 -0
  215. package/src/editor/context.tsx +38 -0
  216. package/src/editor/dom.ts +357 -0
  217. package/src/editor/editor.ts +1510 -0
  218. package/src/editor/env.d.ts +4 -0
  219. package/src/editor/history.ts +355 -0
  220. package/src/editor/hooks/index.ts +19 -0
  221. package/src/editor/hooks/useAIHandlers.ts +345 -0
  222. package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
  223. package/src/editor/hooks/useElementDetection.ts +284 -0
  224. package/src/editor/hooks/useImageHoverDetection.ts +102 -0
  225. package/src/editor/hooks/useTextSelection.ts +187 -0
  226. package/src/editor/hooks/useTooltipState.ts +126 -0
  227. package/src/editor/hooks/utils.ts +101 -0
  228. package/src/editor/index.tsx +481 -0
  229. package/src/editor/lib/cn.ts +4 -0
  230. package/src/editor/manifest.ts +25 -0
  231. package/src/editor/markdown-api.ts +209 -0
  232. package/src/editor/signals.ts +1351 -0
  233. package/src/editor/storage.ts +266 -0
  234. package/src/editor/styles.css +465 -0
  235. package/src/editor/text-styling.ts +773 -0
  236. package/src/editor/themes.ts +210 -0
  237. package/src/editor/types.ts +591 -0
  238. package/src/error-collector.ts +106 -0
  239. package/src/handlers/component-ops.ts +463 -0
  240. package/src/handlers/markdown-ops.ts +202 -0
  241. package/src/handlers/request-utils.ts +151 -0
  242. package/src/handlers/source-writer.ts +649 -0
  243. package/src/html-processor.ts +1108 -0
  244. package/src/index.ts +284 -0
  245. package/src/manifest-writer.ts +371 -0
  246. package/src/media/contember.ts +84 -0
  247. package/src/media/local.ts +114 -0
  248. package/src/media/s3.ts +133 -0
  249. package/src/media/types.ts +33 -0
  250. package/src/preview-generator.ts +293 -0
  251. package/src/seo-processor.ts +567 -0
  252. package/src/source-finder/ast-extractors.ts +185 -0
  253. package/src/source-finder/ast-parser.ts +150 -0
  254. package/src/source-finder/cache.ts +76 -0
  255. package/src/source-finder/collection-finder.ts +335 -0
  256. package/src/source-finder/cross-file-tracker.ts +741 -0
  257. package/src/source-finder/element-finder.ts +387 -0
  258. package/src/source-finder/image-finder.ts +283 -0
  259. package/src/source-finder/index.ts +37 -0
  260. package/src/source-finder/search-index.ts +525 -0
  261. package/src/source-finder/snippet-utils.ts +668 -0
  262. package/src/source-finder/source-lookup.ts +200 -0
  263. package/src/source-finder/types.ts +210 -0
  264. package/src/source-finder/variable-extraction.ts +406 -0
  265. package/src/tailwind-colors.ts +874 -0
  266. package/src/tsconfig.json +25 -0
  267. package/src/types.ts +406 -0
  268. package/src/utils.ts +186 -0
  269. package/src/vite-plugin.ts +42 -0
@@ -0,0 +1,773 @@
1
+ /**
2
+ * Text styling utilities for applying Tailwind classes to text selections.
3
+ * Supports inline styling of partial text content within CMS elements.
4
+ * Uses inline styles for immediate visual feedback.
5
+ * CSS values can be overridden by AvailableTextStyles from the manifest.
6
+ */
7
+
8
+ import type { AvailableTextStyles, TextStyleValue as ManifestTextStyleValue } from './types'
9
+
10
+ /**
11
+ * Tailwind text style categories with their class mappings and CSS values.
12
+ * These are fallback defaults when manifest styles are not available.
13
+ */
14
+ export const TAILWIND_STYLES = {
15
+ weight: {
16
+ normal: { class: 'font-normal', label: 'Normal', css: { fontWeight: '400' } },
17
+ medium: { class: 'font-medium', label: 'Medium', css: { fontWeight: '500' } },
18
+ semibold: { class: 'font-semibold', label: 'Semibold', css: { fontWeight: '600' } },
19
+ bold: { class: 'font-bold', label: 'Bold', css: { fontWeight: '700' } },
20
+ },
21
+ decoration: {
22
+ none: { class: 'no-underline', label: 'None', css: { textDecoration: 'none' } },
23
+ underline: { class: 'underline', label: 'Underline', css: { textDecoration: 'underline' } },
24
+ lineThrough: { class: 'line-through', label: 'Strikethrough', css: { textDecoration: 'line-through' } },
25
+ },
26
+ style: {
27
+ normal: { class: 'not-italic', label: 'Normal', css: { fontStyle: 'normal' } },
28
+ italic: { class: 'italic', label: 'Italic', css: { fontStyle: 'italic' } },
29
+ },
30
+ color: {
31
+ inherit: { class: 'text-inherit', label: 'Inherit', css: { color: 'inherit' } },
32
+ slate: { class: 'text-slate-700', label: 'Slate', css: { color: '#334155' } },
33
+ gray: { class: 'text-gray-700', label: 'Gray', css: { color: '#374151' } },
34
+ red: { class: 'text-red-600', label: 'Red', css: { color: '#dc2626' } },
35
+ orange: { class: 'text-orange-600', label: 'Orange', css: { color: '#ea580c' } },
36
+ amber: { class: 'text-amber-600', label: 'Amber', css: { color: '#d97706' } },
37
+ green: { class: 'text-green-600', label: 'Green', css: { color: '#16a34a' } },
38
+ blue: { class: 'text-blue-600', label: 'Blue', css: { color: '#2563eb' } },
39
+ purple: { class: 'text-purple-600', label: 'Purple', css: { color: '#9333ea' } },
40
+ },
41
+ highlight: {
42
+ none: { class: '', label: 'None', css: { backgroundColor: 'transparent' } },
43
+ yellow: { class: 'bg-yellow-200', label: 'Yellow', css: { backgroundColor: '#fef08a' } },
44
+ green: { class: 'bg-green-200', label: 'Green', css: { backgroundColor: '#bbf7d0' } },
45
+ blue: { class: 'bg-blue-200', label: 'Blue', css: { backgroundColor: '#bfdbfe' } },
46
+ pink: { class: 'bg-pink-200', label: 'Pink', css: { backgroundColor: '#fbcfe8' } },
47
+ },
48
+ size: {
49
+ xs: { class: 'text-xs', label: 'XS', css: { fontSize: '0.75rem', lineHeight: '1rem' } },
50
+ sm: { class: 'text-sm', label: 'SM', css: { fontSize: '0.875rem', lineHeight: '1.25rem' } },
51
+ base: { class: 'text-base', label: 'Base', css: { fontSize: '1rem', lineHeight: '1.5rem' } },
52
+ lg: { class: 'text-lg', label: 'LG', css: { fontSize: '1.125rem', lineHeight: '1.75rem' } },
53
+ xl: { class: 'text-xl', label: 'XL', css: { fontSize: '1.25rem', lineHeight: '1.75rem' } },
54
+ '2xl': { class: 'text-2xl', label: '2XL', css: { fontSize: '1.5rem', lineHeight: '2rem' } },
55
+ },
56
+ } as const
57
+
58
+ export type StyleCategory = keyof typeof TAILWIND_STYLES
59
+ export type StyleValue<C extends StyleCategory> = keyof (typeof TAILWIND_STYLES)[C]
60
+
61
+ export interface TextStyle {
62
+ weight?: StyleValue<'weight'>
63
+ decoration?: StyleValue<'decoration'>
64
+ style?: StyleValue<'style'>
65
+ color?: StyleValue<'color'>
66
+ highlight?: StyleValue<'highlight'>
67
+ size?: StyleValue<'size'>
68
+ }
69
+
70
+ export interface TextSelection {
71
+ startOffset: number
72
+ endOffset: number
73
+ text: string
74
+ range: Range
75
+ anchorNode: Node
76
+ focusNode: Node
77
+ }
78
+
79
+ /** Default values for each style category (no visual styling) */
80
+ const DEFAULT_VALUES: Record<StyleCategory, string> = {
81
+ weight: 'normal',
82
+ decoration: 'none',
83
+ style: 'normal',
84
+ color: 'inherit',
85
+ highlight: 'none',
86
+ size: 'base',
87
+ }
88
+
89
+ /** Pre-computed reverse lookup: class name -> { category, key } */
90
+ const CLASS_TO_STYLE_MAP = new Map<string, { category: StyleCategory; key: string }>()
91
+
92
+ // Build the reverse lookup map once at module load
93
+ for (const [category, values] of Object.entries(TAILWIND_STYLES)) {
94
+ for (const [key, config] of Object.entries(values)) {
95
+ if (config.class) {
96
+ CLASS_TO_STYLE_MAP.set(config.class, { category: category as StyleCategory, key })
97
+ }
98
+ }
99
+ }
100
+
101
+ /** Set of all known styling classes for quick lookup */
102
+ const KNOWN_STYLE_CLASSES = new Set(CLASS_TO_STYLE_MAP.keys())
103
+
104
+ /**
105
+ * Get the current text selection within a CMS element
106
+ */
107
+ export function getTextSelection(cmsElement: HTMLElement): TextSelection | null {
108
+ const selection = window.getSelection()
109
+ if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
110
+ return null
111
+ }
112
+
113
+ const range = selection.getRangeAt(0)
114
+
115
+ // Check if selection is within the CMS element
116
+ if (!cmsElement.contains(range.commonAncestorContainer)) {
117
+ return null
118
+ }
119
+
120
+ const text = selection.toString()
121
+ if (!text.trim()) {
122
+ return null
123
+ }
124
+
125
+ const anchorNode = selection.anchorNode
126
+ const focusNode = selection.focusNode
127
+ if (!anchorNode || !focusNode) {
128
+ return null
129
+ }
130
+
131
+ return {
132
+ startOffset: range.startOffset,
133
+ endOffset: range.endOffset,
134
+ text,
135
+ range,
136
+ anchorNode,
137
+ focusNode,
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Build the class string from a TextStyle object
143
+ */
144
+ export function buildStyleClasses(style: TextStyle): string {
145
+ const classes: string[] = []
146
+
147
+ for (const [category, value] of Object.entries(style)) {
148
+ if (value === undefined) continue
149
+
150
+ const defaultValue = DEFAULT_VALUES[category as StyleCategory]
151
+ if (value === defaultValue) continue
152
+
153
+ const styleConfig = TAILWIND_STYLES[category as StyleCategory]
154
+ const config = styleConfig[value as keyof typeof styleConfig] as { class: string } | undefined
155
+ if (config?.class) {
156
+ classes.push(config.class)
157
+ }
158
+ }
159
+
160
+ return classes.join(' ')
161
+ }
162
+
163
+ /**
164
+ * Map style category to manifest property name
165
+ */
166
+ const CATEGORY_TO_MANIFEST: Record<StyleCategory, keyof AvailableTextStyles | null> = {
167
+ weight: 'fontWeight',
168
+ size: 'fontSize',
169
+ decoration: 'textDecoration',
170
+ style: 'fontStyle',
171
+ color: null, // Colors are handled separately via availableColors
172
+ highlight: null, // Highlights are handled separately via availableColors
173
+ }
174
+
175
+ /**
176
+ * Resolve CSS properties for a class name from the manifest.
177
+ * Falls back to hardcoded TAILWIND_STYLES if not found in manifest.
178
+ */
179
+ function resolveCssFromManifest(
180
+ className: string,
181
+ availableTextStyles: AvailableTextStyles | undefined,
182
+ ): Record<string, string> | undefined {
183
+ if (availableTextStyles) {
184
+ // Check each category in the manifest
185
+ for (const category of ['fontWeight', 'fontSize', 'textDecoration', 'fontStyle'] as const) {
186
+ const styles = availableTextStyles[category]
187
+ if (styles) {
188
+ const found = styles.find(s => s.class === className)
189
+ if (found) {
190
+ return found.css
191
+ }
192
+ }
193
+ }
194
+ }
195
+ return undefined
196
+ }
197
+
198
+ /**
199
+ * Build inline CSS styles from a TextStyle object.
200
+ * Uses manifest styles when available, falls back to hardcoded defaults.
201
+ */
202
+ export function buildInlineStyles(
203
+ style: TextStyle,
204
+ availableTextStyles?: AvailableTextStyles,
205
+ ): Record<string, string> {
206
+ const cssStyles: Record<string, string> = {}
207
+
208
+ for (const [category, value] of Object.entries(style)) {
209
+ if (value === undefined) continue
210
+
211
+ const styleConfig = TAILWIND_STYLES[category as StyleCategory]
212
+ const config = styleConfig[value as keyof typeof styleConfig] as { class?: string; css?: Record<string, string> } | undefined
213
+
214
+ if (config?.class) {
215
+ // Try to resolve from manifest first
216
+ const manifestCss = resolveCssFromManifest(config.class, availableTextStyles)
217
+ if (manifestCss) {
218
+ Object.assign(cssStyles, manifestCss)
219
+ } else if (config.css) {
220
+ // Fall back to hardcoded defaults
221
+ Object.assign(cssStyles, config.css)
222
+ }
223
+ }
224
+ }
225
+
226
+ return cssStyles
227
+ }
228
+
229
+ /** Stored reference to available text styles for internal functions */
230
+ let _availableTextStyles: AvailableTextStyles | undefined
231
+
232
+ /**
233
+ * Set the available text styles from manifest.
234
+ * Call this when manifest is loaded.
235
+ */
236
+ export function setAvailableTextStyles(styles: AvailableTextStyles | undefined): void {
237
+ _availableTextStyles = styles
238
+ }
239
+
240
+ /**
241
+ * Apply inline styles to an element from a TextStyle object
242
+ */
243
+ function applyInlineStyles(element: HTMLElement, style: TextStyle): void {
244
+ const cssStyles = buildInlineStyles(style, _availableTextStyles)
245
+ for (const [property, value] of Object.entries(cssStyles)) {
246
+ element.style[property as any] = value
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Clear inline styles that correspond to text styling
252
+ */
253
+ function clearTextInlineStyles(element: HTMLElement): void {
254
+ element.style.fontWeight = ''
255
+ element.style.textDecoration = ''
256
+ element.style.fontStyle = ''
257
+ element.style.color = ''
258
+ element.style.backgroundColor = ''
259
+ element.style.fontSize = ''
260
+ element.style.lineHeight = ''
261
+ }
262
+
263
+ /**
264
+ * Parse Tailwind classes from a class string back to TextStyle.
265
+ * Uses O(n) lookup via pre-computed map instead of O(n*m) nested loops.
266
+ */
267
+ export function parseStyleClasses(classString: string): TextStyle {
268
+ if (!classString) return {}
269
+
270
+ const classes = classString.split(/\s+/).filter(Boolean)
271
+ const style: TextStyle = {}
272
+
273
+ for (const cls of classes) {
274
+ const mapping = CLASS_TO_STYLE_MAP.get(cls)
275
+ if (mapping) {
276
+ // Type-safe assignment using the mapping
277
+ ;(style as Record<string, string>)[mapping.category] = mapping.key
278
+ }
279
+ }
280
+
281
+ return style
282
+ }
283
+
284
+ /**
285
+ * Separate styling classes from non-styling classes
286
+ */
287
+ export function separateClasses(classString: string): { styleClasses: string[]; otherClasses: string[] } {
288
+ const classes = classString.split(/\s+/).filter(Boolean)
289
+ const styleClasses: string[] = []
290
+ const otherClasses: string[] = []
291
+
292
+ for (const cls of classes) {
293
+ if (KNOWN_STYLE_CLASSES.has(cls)) {
294
+ styleClasses.push(cls)
295
+ } else {
296
+ otherClasses.push(cls)
297
+ }
298
+ }
299
+
300
+ return { styleClasses, otherClasses }
301
+ }
302
+
303
+ /**
304
+ * Check if an element is a styled span created by the CMS
305
+ */
306
+ export function isStyledSpan(element: Element | null): element is HTMLElement {
307
+ return element instanceof HTMLElement && element.hasAttribute('data-cms-styled')
308
+ }
309
+
310
+ /**
311
+ * Create a new styled span element with both classes and inline styles
312
+ */
313
+ function createStyledSpan(style: TextStyle): HTMLSpanElement {
314
+ const span = document.createElement('span')
315
+ span.setAttribute('data-cms-styled', 'true')
316
+ const classString = buildStyleClasses(style)
317
+ if (classString) {
318
+ span.className = classString
319
+ }
320
+ // Apply inline styles for immediate visual feedback
321
+ applyInlineStyles(span, style)
322
+ return span
323
+ }
324
+
325
+ /**
326
+ * Get the styled span element if the selection is entirely within one.
327
+ * Returns null if selection spans multiple elements or is not in a styled span.
328
+ */
329
+ export function getStyledSpanFromSelection(cmsElement: HTMLElement): HTMLElement | null {
330
+ const selection = window.getSelection()
331
+ if (!selection || selection.rangeCount === 0) {
332
+ return null
333
+ }
334
+
335
+ const range = selection.getRangeAt(0)
336
+ const container = range.commonAncestorContainer
337
+
338
+ // Start from the container (or its parent if it's a text node)
339
+ let currentElement: HTMLElement | null = container.nodeType === Node.TEXT_NODE
340
+ ? (container.parentElement as HTMLElement | null)
341
+ : (container as HTMLElement)
342
+
343
+ // Walk up the tree to find a styled span
344
+ while (currentElement !== null && currentElement !== cmsElement) {
345
+ if (currentElement.hasAttribute('data-cms-styled')) {
346
+ return currentElement
347
+ }
348
+ currentElement = currentElement.parentElement
349
+ }
350
+
351
+ return null
352
+ }
353
+
354
+ /**
355
+ * Remove styling from a styled span element, keeping only the text content.
356
+ * Used to "unstyle" previously styled text.
357
+ */
358
+ export function removeStyleFromElement(styledSpan: HTMLElement): void {
359
+ const parent = styledSpan.parentNode
360
+ if (!parent) return
361
+
362
+ // Move all children out of the span
363
+ while (styledSpan.firstChild) {
364
+ parent.insertBefore(styledSpan.firstChild, styledSpan)
365
+ }
366
+
367
+ // Remove the now-empty span
368
+ parent.removeChild(styledSpan)
369
+
370
+ // Normalize to merge adjacent text nodes
371
+ parent.normalize()
372
+ }
373
+
374
+ /**
375
+ * Update styles on an existing styled span.
376
+ * Preserves non-styling classes and applies inline styles for immediate feedback.
377
+ */
378
+ export function updateStyledSpan(span: HTMLElement, newStyle: Partial<TextStyle>): void {
379
+ const existingStyle = parseStyleClasses(span.className)
380
+ const { otherClasses } = separateClasses(span.className)
381
+
382
+ // Merge new style with existing
383
+ const mergedStyle = { ...existingStyle, ...newStyle }
384
+
385
+ // Build new class string
386
+ const newStyleClasses = buildStyleClasses(mergedStyle)
387
+
388
+ if (newStyleClasses || otherClasses.length > 0) {
389
+ span.className = [...otherClasses, newStyleClasses].filter(Boolean).join(' ')
390
+ // Clear existing inline styles and reapply
391
+ clearTextInlineStyles(span)
392
+ applyInlineStyles(span, mergedStyle)
393
+ } else {
394
+ // No styles left, remove the span wrapper
395
+ removeStyleFromElement(span)
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Flatten nested styled spans into a single span with merged styles.
401
+ * Processes the fragment recursively.
402
+ */
403
+ function flattenStyledSpans(fragment: DocumentFragment, targetSpan: HTMLSpanElement): void {
404
+ const processNode = (node: Node): void => {
405
+ if (node.nodeType === Node.ELEMENT_NODE) {
406
+ const htmlElement = node as HTMLElement
407
+
408
+ if (htmlElement.hasAttribute('data-cms-styled')) {
409
+ // Merge classes from nested styled span
410
+ const nestedStyle = parseStyleClasses(htmlElement.className)
411
+ const currentStyle = parseStyleClasses(targetSpan.className)
412
+ // Nested span's styles take precedence
413
+ const mergedStyle = { ...currentStyle, ...nestedStyle }
414
+ targetSpan.className = buildStyleClasses(mergedStyle) || ''
415
+ // Apply inline styles for immediate visual feedback
416
+ clearTextInlineStyles(targetSpan)
417
+ applyInlineStyles(targetSpan, mergedStyle)
418
+
419
+ // Recursively process children of the nested span
420
+ const children = Array.from(htmlElement.childNodes) as ChildNode[]
421
+ for (const child of children) {
422
+ processNode(child)
423
+ }
424
+ } else {
425
+ // Clone the element (without children) and process children recursively
426
+ const clonedElement = htmlElement.cloneNode(false) as HTMLElement
427
+ const children = Array.from(htmlElement.childNodes) as ChildNode[]
428
+ for (const child of children) {
429
+ // For regular elements, we need to keep their structure
430
+ if (child.nodeType === Node.ELEMENT_NODE) {
431
+ const childElement = child as HTMLElement
432
+ if (childElement.hasAttribute('data-cms-styled')) {
433
+ // Flatten nested styled spans within regular elements
434
+ const innerChildren = Array.from(childElement.childNodes) as ChildNode[]
435
+ for (const innerChild of innerChildren) {
436
+ clonedElement.appendChild(innerChild.cloneNode(true))
437
+ }
438
+ // Merge styles
439
+ const nestedStyle = parseStyleClasses(childElement.className)
440
+ const currentStyle = parseStyleClasses(targetSpan.className)
441
+ const mergedStyle = { ...currentStyle, ...nestedStyle }
442
+ targetSpan.className = buildStyleClasses(mergedStyle) || ''
443
+ // Apply inline styles
444
+ clearTextInlineStyles(targetSpan)
445
+ applyInlineStyles(targetSpan, mergedStyle)
446
+ } else {
447
+ clonedElement.appendChild(child.cloneNode(true))
448
+ }
449
+ } else {
450
+ clonedElement.appendChild(child.cloneNode(true))
451
+ }
452
+ }
453
+ targetSpan.appendChild(clonedElement)
454
+ }
455
+ } else {
456
+ // Text nodes and other node types - just append
457
+ targetSpan.appendChild(node.cloneNode(true))
458
+ }
459
+ }
460
+
461
+ // Process all children of the fragment
462
+ const nodes = Array.from(fragment.childNodes) as ChildNode[]
463
+ for (const node of nodes) {
464
+ processNode(node)
465
+ }
466
+ }
467
+
468
+ /**
469
+ * Check if two elements are directly adjacent (no meaningful content between them)
470
+ */
471
+ function isDirectlyAdjacent(first: Element, second: Element): boolean {
472
+ let node: Node | null = first.nextSibling
473
+
474
+ while (node && node !== second) {
475
+ if (node.nodeType === Node.TEXT_NODE) {
476
+ // Only whitespace is allowed between
477
+ if (node.textContent && node.textContent.trim() !== '') {
478
+ return false
479
+ }
480
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
481
+ return false
482
+ }
483
+ node = node.nextSibling
484
+ }
485
+
486
+ return node === second
487
+ }
488
+
489
+ /**
490
+ * Remove empty styled spans from the element
491
+ */
492
+ export function cleanupEmptyStyledSpans(cmsElement: HTMLElement): void {
493
+ const styledSpans = Array.from(cmsElement.querySelectorAll('[data-cms-styled]'))
494
+
495
+ for (const span of styledSpans) {
496
+ if (!span.textContent && !span.querySelector('*')) {
497
+ span.remove()
498
+ }
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Merge adjacent styled spans that have the same classes
504
+ */
505
+ export function mergeAdjacentStyledSpans(cmsElement: HTMLElement): void {
506
+ let styledSpans = Array.from(cmsElement.querySelectorAll('[data-cms-styled]'))
507
+ let merged = true
508
+
509
+ // Keep merging until no more merges are possible
510
+ while (merged) {
511
+ merged = false
512
+ styledSpans = Array.from(cmsElement.querySelectorAll('[data-cms-styled]'))
513
+
514
+ for (let i = 0; i < styledSpans.length - 1; i++) {
515
+ const current = styledSpans[i] as HTMLElement
516
+ const next = styledSpans[i + 1] as HTMLElement
517
+
518
+ if (!current.parentNode || !next.parentNode) continue
519
+
520
+ // Check if they are truly adjacent (no significant content between them)
521
+ const isAdjacent = isDirectlyAdjacent(current, next)
522
+
523
+ if (isAdjacent && current.className === next.className) {
524
+ // Move all children from next to current
525
+ while (next.firstChild) {
526
+ current.appendChild(next.firstChild)
527
+ }
528
+ // Remove the empty next span
529
+ next.remove()
530
+ merged = true
531
+ break // Restart the loop after merging
532
+ }
533
+ }
534
+ }
535
+
536
+ // Clean up empty styled spans
537
+ cleanupEmptyStyledSpans(cmsElement)
538
+
539
+ cmsElement.normalize()
540
+ }
541
+
542
+ /**
543
+ * Wrap selected text in a span with Tailwind classes.
544
+ * If the selection is already inside a styled span, just update that span's classes.
545
+ * Returns the styled span element or null if failed.
546
+ */
547
+ export function wrapSelectionWithStyle(cmsElement: HTMLElement, selection: TextSelection, style: TextStyle): HTMLSpanElement | null {
548
+ const classString = buildStyleClasses(style)
549
+
550
+ // Check if we're already inside a styled span
551
+ const existingSpan = getStyledSpanFromSelection(cmsElement)
552
+ if (existingSpan) {
553
+ // Check if we're selecting the entire span content
554
+ const selectionRange = selection.range
555
+ const spanRange = document.createRange()
556
+ spanRange.selectNodeContents(existingSpan)
557
+
558
+ const isFullSelection = selectionRange.compareBoundaryPoints(Range.START_TO_START, spanRange) === 0
559
+ && selectionRange.compareBoundaryPoints(Range.END_TO_END, spanRange) === 0
560
+
561
+ if (isFullSelection) {
562
+ // Update the entire span's styles
563
+ updateStyledSpan(existingSpan, style)
564
+ return existingSpan
565
+ }
566
+
567
+ // Partial selection within existing span - need to split
568
+ return splitAndStyleSelection(cmsElement, existingSpan, selection, style)
569
+ }
570
+
571
+ if (!classString) {
572
+ return null
573
+ }
574
+
575
+ try {
576
+ // Create the wrapper span
577
+ const span = createStyledSpan(style)
578
+
579
+ // Extract the selected content and wrap it
580
+ const contents = selection.range.extractContents()
581
+
582
+ // Flatten any nested styled spans from the extracted content
583
+ flattenStyledSpans(contents, span)
584
+
585
+ selection.range.insertNode(span)
586
+
587
+ // Normalize the parent to merge adjacent text nodes
588
+ cmsElement.normalize()
589
+
590
+ // Merge adjacent styled spans with same classes
591
+ mergeAdjacentStyledSpans(cmsElement)
592
+
593
+ return span
594
+ } catch (error) {
595
+ console.error('[CMS] Failed to wrap selection:', error)
596
+ return null
597
+ }
598
+ }
599
+
600
+ /**
601
+ * Handle partial selection within an existing styled span by splitting it
602
+ */
603
+ function splitAndStyleSelection(
604
+ cmsElement: HTMLElement,
605
+ existingSpan: HTMLElement,
606
+ selection: TextSelection,
607
+ newStyle: TextStyle,
608
+ ): HTMLSpanElement | null {
609
+ try {
610
+ const range = selection.range
611
+ const existingStyle = parseStyleClasses(existingSpan.className)
612
+
613
+ // Create a range for the content before the selection
614
+ const beforeRange = document.createRange()
615
+ beforeRange.setStart(existingSpan, 0)
616
+ beforeRange.setEnd(range.startContainer, range.startOffset)
617
+
618
+ // Create a range for the content after the selection
619
+ const afterRange = document.createRange()
620
+ afterRange.setStart(range.endContainer, range.endOffset)
621
+ afterRange.setEndAfter(existingSpan.lastChild || existingSpan)
622
+
623
+ // Extract contents
624
+ const beforeContents = beforeRange.extractContents()
625
+ const selectedContents = range.extractContents()
626
+ const afterContents = afterRange.extractContents()
627
+
628
+ // Get parent for insertion
629
+ const parent = existingSpan.parentNode
630
+ if (!parent) return null
631
+
632
+ // Create spans for each section
633
+ const beforeSpan = beforeContents.textContent?.trim() || beforeContents.querySelector('*')
634
+ ? createStyledSpan(existingStyle)
635
+ : null
636
+
637
+ const selectedSpan = createStyledSpan({ ...existingStyle, ...newStyle })
638
+
639
+ const afterSpan = afterContents.textContent?.trim() || afterContents.querySelector('*')
640
+ ? createStyledSpan(existingStyle)
641
+ : null
642
+
643
+ // Populate spans
644
+ if (beforeSpan) {
645
+ beforeSpan.appendChild(beforeContents)
646
+ }
647
+ selectedSpan.appendChild(selectedContents)
648
+ if (afterSpan) {
649
+ afterSpan.appendChild(afterContents)
650
+ }
651
+
652
+ // Insert new spans before the existing one
653
+ if (beforeSpan) parent.insertBefore(beforeSpan, existingSpan)
654
+ parent.insertBefore(selectedSpan, existingSpan)
655
+ if (afterSpan) parent.insertBefore(afterSpan, existingSpan)
656
+
657
+ // Remove the original span
658
+ existingSpan.remove()
659
+
660
+ // Cleanup and merge
661
+ cleanupEmptyStyledSpans(cmsElement)
662
+ mergeAdjacentStyledSpans(cmsElement)
663
+ cmsElement.normalize()
664
+
665
+ return selectedSpan
666
+ } catch (error) {
667
+ console.error('[CMS] Failed to split and style selection:', error)
668
+ return null
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Apply a specific style category to the current selection.
674
+ * This is a convenience function for single-style application.
675
+ */
676
+ export function applyStyleToSelection<C extends StyleCategory>(
677
+ cmsElement: HTMLElement,
678
+ category: C,
679
+ value: StyleValue<C>,
680
+ ): HTMLSpanElement | null {
681
+ const selection = getTextSelection(cmsElement)
682
+ if (!selection) {
683
+ return null
684
+ }
685
+
686
+ const style: TextStyle = { [category]: value }
687
+ return wrapSelectionWithStyle(cmsElement, selection, style)
688
+ }
689
+
690
+ /**
691
+ * Toggle a style value on the selection or styled element.
692
+ * If the style is already applied, it removes it; otherwise applies it.
693
+ */
694
+ export function toggleStyle<C extends StyleCategory>(cmsElement: HTMLElement, category: C, value: StyleValue<C>): HTMLSpanElement | null {
695
+ const styledSpan = getStyledSpanFromSelection(cmsElement)
696
+
697
+ if (styledSpan) {
698
+ const currentStyle = parseStyleClasses(styledSpan.className)
699
+ const currentValue = currentStyle[category] as string | undefined
700
+
701
+ // Check if this exact value is already applied
702
+ if (currentValue === value) {
703
+ // Remove this style (set to default)
704
+ const defaultValue = DEFAULT_VALUES[category]
705
+ updateStyledSpan(styledSpan, { [category]: defaultValue } as TextStyle)
706
+ return null
707
+ } else {
708
+ // Update to the new value
709
+ updateStyledSpan(styledSpan, { [category]: value } as TextStyle)
710
+ return styledSpan
711
+ }
712
+ }
713
+
714
+ // No existing styled span, create a new one
715
+ return applyStyleToSelection(cmsElement, category, value)
716
+ }
717
+
718
+ /**
719
+ * Check if a specific style is currently applied to the selection
720
+ */
721
+ export function hasStyle<C extends StyleCategory>(cmsElement: HTMLElement, category: C, value: StyleValue<C>): boolean {
722
+ const styledSpan = getStyledSpanFromSelection(cmsElement)
723
+ if (!styledSpan) {
724
+ return false
725
+ }
726
+
727
+ const currentStyle = parseStyleClasses(styledSpan.className)
728
+ const currentValue = currentStyle[category] as string | undefined
729
+ return currentValue === value
730
+ }
731
+
732
+ /**
733
+ * Get the current style of the selection
734
+ */
735
+ export function getCurrentStyle(cmsElement: HTMLElement): TextStyle {
736
+ const styledSpan = getStyledSpanFromSelection(cmsElement)
737
+ if (!styledSpan) {
738
+ return {}
739
+ }
740
+ return parseStyleClasses(styledSpan.className)
741
+ }
742
+
743
+ /**
744
+ * Remove all styling from the current selection
745
+ */
746
+ export function clearAllStyles(cmsElement: HTMLElement): void {
747
+ const styledSpan = getStyledSpanFromSelection(cmsElement)
748
+ if (styledSpan) {
749
+ removeStyleFromElement(styledSpan)
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Check if a style value is valid for a given category
755
+ */
756
+ export function isValidStyleValue<C extends StyleCategory>(category: C, value: unknown): value is StyleValue<C> {
757
+ const categoryStyles = TAILWIND_STYLES[category]
758
+ return typeof value === 'string' && value in categoryStyles
759
+ }
760
+
761
+ /**
762
+ * Get the default value for a style category
763
+ */
764
+ export function getDefaultValue(category: StyleCategory): string {
765
+ return DEFAULT_VALUES[category]
766
+ }
767
+
768
+ /**
769
+ * Get all Tailwind classes used for text styling.
770
+ */
771
+ export function getAllStyleClasses(): string[] {
772
+ return Array.from(KNOWN_STYLE_CLASSES)
773
+ }