@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,362 @@
1
+ import { useCallback, useEffect, useState } from 'preact/hooks'
2
+ import { CSS, Z_INDEX } from '../constants'
3
+ import { cn } from '../lib/cn'
4
+ import * as signals from '../signals'
5
+ import {
6
+ getCurrentStyle,
7
+ getStyledSpanFromSelection,
8
+ getTextSelection,
9
+ isValidStyleValue,
10
+ parseStyleClasses,
11
+ type StyleCategory,
12
+ type StyleValue,
13
+ TAILWIND_STYLES,
14
+ type TextStyle,
15
+ toggleStyle,
16
+ } from '../text-styling'
17
+
18
+ export interface TextStyleToolbarProps {
19
+ visible: boolean
20
+ rect: DOMRect | null
21
+ element: HTMLElement | null
22
+ onStyleChange?: () => void
23
+ }
24
+
25
+ interface StyleButtonProps<C extends StyleCategory> {
26
+ category: C
27
+ value: StyleValue<C>
28
+ label: string
29
+ icon: preact.ComponentChildren
30
+ isActive: boolean
31
+ onClick: () => void
32
+ }
33
+
34
+ function StyleButton<C extends StyleCategory>({
35
+ label,
36
+ icon,
37
+ isActive,
38
+ onClick,
39
+ }: StyleButtonProps<C>) {
40
+ return (
41
+ <button
42
+ type="button"
43
+ onClick={onClick}
44
+ title={label}
45
+ class={cn(
46
+ 'w-8 h-8 flex items-center justify-center rounded-cms-sm transition-colors cursor-pointer',
47
+ isActive
48
+ ? 'bg-cms-primary text-cms-primary-text'
49
+ : 'hover:bg-white/20 text-white/80 hover:text-white',
50
+ )}
51
+ >
52
+ {icon}
53
+ </button>
54
+ )
55
+ }
56
+
57
+ interface ColorButtonProps {
58
+ color: string
59
+ tailwindClass: string
60
+ label: string
61
+ isActive: boolean
62
+ onClick: () => void
63
+ }
64
+
65
+ function ColorButton({ color, label, isActive, onClick }: ColorButtonProps) {
66
+ return (
67
+ <button
68
+ type="button"
69
+ onClick={onClick}
70
+ title={label}
71
+ class={cn(
72
+ 'w-5 h-5 rounded-full border-2 transition-transform cursor-pointer',
73
+ isActive ? 'border-cms-primary scale-125 ring-2 ring-cms-primary/30' : 'border-transparent hover:scale-110',
74
+ )}
75
+ style={{ backgroundColor: color }}
76
+ />
77
+ )
78
+ }
79
+
80
+ // Map Tailwind color names to actual colors for preview
81
+ const COLOR_MAP: Record<string, string> = {
82
+ inherit: '#374151',
83
+ slate: '#334155',
84
+ gray: '#374151',
85
+ red: '#dc2626',
86
+ orange: '#ea580c',
87
+ amber: '#d97706',
88
+ green: '#16a34a',
89
+ blue: '#2563eb',
90
+ purple: '#9333ea',
91
+ }
92
+
93
+ const HIGHLIGHT_MAP: Record<string, string> = {
94
+ none: 'transparent',
95
+ yellow: '#fef08a',
96
+ green: '#bbf7d0',
97
+ blue: '#bfdbfe',
98
+ pink: '#fbcfe8',
99
+ }
100
+
101
+ export function TextStyleToolbar({ visible, rect, element, onStyleChange }: TextStyleToolbarProps) {
102
+ const [currentStyle, setCurrentStyle] = useState<TextStyle>({})
103
+ const [showColorPicker, setShowColorPicker] = useState(false)
104
+ const [showHighlightPicker, setShowHighlightPicker] = useState(false)
105
+
106
+ // Update current style when selection changes
107
+ useEffect(() => {
108
+ if (!visible || !element) {
109
+ setCurrentStyle({})
110
+ setShowColorPicker(false)
111
+ setShowHighlightPicker(false)
112
+ return
113
+ }
114
+
115
+ const updateStyle = () => {
116
+ setCurrentStyle(getCurrentStyle(element))
117
+ }
118
+
119
+ updateStyle()
120
+ document.addEventListener('selectionchange', updateStyle)
121
+ return () => document.removeEventListener('selectionchange', updateStyle)
122
+ }, [visible, element])
123
+
124
+ // Handle style toggle
125
+ const handleToggleStyle = useCallback(
126
+ <C extends StyleCategory>(category: C, value: StyleValue<C>) => {
127
+ if (!element) return
128
+
129
+ // Validate the style value
130
+ if (!isValidStyleValue(category, value)) {
131
+ console.warn(`[CMS] Invalid style value: ${category}=${String(value)}`)
132
+ return
133
+ }
134
+
135
+ const selection = getTextSelection(element)
136
+ const existingSpan = getStyledSpanFromSelection(element)
137
+ if (!selection && !existingSpan) {
138
+ return
139
+ }
140
+
141
+ // Ensure currentEditingId is set so input event is processed
142
+ const cmsId = element.getAttribute(CSS.ID_ATTRIBUTE)
143
+ if (cmsId) {
144
+ signals.setCurrentEditingId(cmsId)
145
+ }
146
+
147
+ // Apply the style and get the resulting span
148
+ const resultSpan = toggleStyle(element, category, value)
149
+
150
+ // Trigger change event on the element to update pending changes
151
+ element.dispatchEvent(new Event('input', { bubbles: true }))
152
+
153
+ // Update current style display from the resulting span (not from selection which may be gone)
154
+ if (resultSpan) {
155
+ // Style was applied/updated - read from the span directly
156
+ setCurrentStyle(parseStyleClasses(resultSpan.className))
157
+ } else if (existingSpan) {
158
+ // Style was toggled off - the existing span might still exist with other styles
159
+ // Check if the span is still in the DOM and has remaining styles
160
+ if (existingSpan.parentElement) {
161
+ setCurrentStyle(parseStyleClasses(existingSpan.className))
162
+ } else {
163
+ // Span was removed completely
164
+ setCurrentStyle({})
165
+ }
166
+ } else {
167
+ // Fallback to selection-based detection
168
+ setCurrentStyle(getCurrentStyle(element))
169
+ }
170
+
171
+ onStyleChange?.()
172
+ },
173
+ [element, onStyleChange],
174
+ )
175
+
176
+ if (!visible || !rect) {
177
+ return null
178
+ }
179
+
180
+ // Position toolbar above the selection
181
+ const toolbarHeight = 44
182
+ const toolbarWidth = 320
183
+ let left = rect.left + rect.width / 2 - toolbarWidth / 2
184
+ let top = rect.top - toolbarHeight - 8
185
+
186
+ const padding = 10
187
+ const maxLeft = window.innerWidth - toolbarWidth - padding
188
+ const minLeft = padding
189
+
190
+ left = Math.max(minLeft, Math.min(left, maxLeft))
191
+
192
+ if (top < padding) {
193
+ top = rect.bottom + 8
194
+ }
195
+
196
+ return (
197
+ <div
198
+ data-cms-ui
199
+ onMouseDown={(e) => e.stopPropagation()}
200
+ onClick={(e) => e.stopPropagation()}
201
+ style={{
202
+ position: 'fixed',
203
+ left: `${left}px`,
204
+ top: `${top}px`,
205
+ zIndex: Z_INDEX.MODAL,
206
+ fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, sans-serif',
207
+ fontSize: '12px',
208
+ }}
209
+ >
210
+ <div class="flex items-center gap-1 px-3 py-2 bg-cms-dark border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.3)] rounded-cms-xl">
211
+ {/* Bold */}
212
+ <StyleButton
213
+ category="weight"
214
+ value="bold"
215
+ label="Bold"
216
+ isActive={currentStyle.weight === 'bold'}
217
+ onClick={() => handleToggleStyle('weight', 'bold')}
218
+ icon={<span class="font-bold text-sm">B</span>}
219
+ />
220
+
221
+ {/* Italic */}
222
+ <StyleButton
223
+ category="style"
224
+ value="italic"
225
+ label="Italic"
226
+ isActive={currentStyle.style === 'italic'}
227
+ onClick={() => handleToggleStyle('style', 'italic')}
228
+ icon={<span class="italic text-sm">I</span>}
229
+ />
230
+
231
+ {/* Underline */}
232
+ <StyleButton
233
+ category="decoration"
234
+ value="underline"
235
+ label="Underline"
236
+ isActive={currentStyle.decoration === 'underline'}
237
+ onClick={() => handleToggleStyle('decoration', 'underline')}
238
+ icon={<span class="underline text-sm">U</span>}
239
+ />
240
+
241
+ {/* Strikethrough */}
242
+ <StyleButton
243
+ category="decoration"
244
+ value="lineThrough"
245
+ label="Strikethrough"
246
+ isActive={currentStyle.decoration === 'lineThrough'}
247
+ onClick={() => handleToggleStyle('decoration', 'lineThrough')}
248
+ icon={<span class="line-through text-sm">S</span>}
249
+ />
250
+
251
+ <div class="w-px h-5 bg-white/20 mx-1" />
252
+
253
+ {/* Text Color Picker */}
254
+ <div class="relative">
255
+ <button
256
+ type="button"
257
+ onClick={() => {
258
+ setShowColorPicker(!showColorPicker)
259
+ setShowHighlightPicker(false)
260
+ }}
261
+ title="Text Color"
262
+ class={cn(
263
+ 'w-8 h-8 flex items-center justify-center rounded-cms-sm transition-colors cursor-pointer',
264
+ showColorPicker ? 'bg-white/20' : 'hover:bg-white/20',
265
+ )}
266
+ >
267
+ <span
268
+ class="text-sm"
269
+ style={{
270
+ color: currentStyle.color ? COLOR_MAP[currentStyle.color] : '#ffffff',
271
+ }}
272
+ >
273
+ A
274
+ </span>
275
+ <div
276
+ class="absolute bottom-1 left-2 right-2 h-0.5 rounded-full"
277
+ style={{ backgroundColor: currentStyle.color ? COLOR_MAP[currentStyle.color] : '#ffffff' }}
278
+ />
279
+ </button>
280
+
281
+ {showColorPicker && (
282
+ <div class="absolute top-full left-0 mt-2 p-3 bg-cms-dark border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.3)] rounded-cms-md flex gap-2 flex-wrap w-36 z-10">
283
+ {Object.entries(COLOR_MAP).map(([key, color]) => (
284
+ <ColorButton
285
+ key={key}
286
+ color={color}
287
+ tailwindClass={TAILWIND_STYLES.color[key as keyof typeof TAILWIND_STYLES.color]?.class || ''}
288
+ label={TAILWIND_STYLES.color[key as keyof typeof TAILWIND_STYLES.color]?.label || key}
289
+ isActive={currentStyle.color === key}
290
+ onClick={() => {
291
+ handleToggleStyle('color', key as StyleValue<'color'>)
292
+ setShowColorPicker(false)
293
+ }}
294
+ />
295
+ ))}
296
+ </div>
297
+ )}
298
+ </div>
299
+
300
+ {/* Highlight Color Picker */}
301
+ <div class="relative">
302
+ <button
303
+ type="button"
304
+ onClick={() => {
305
+ setShowHighlightPicker(!showHighlightPicker)
306
+ setShowColorPicker(false)
307
+ }}
308
+ title="Highlight"
309
+ class={cn(
310
+ 'w-8 h-8 flex items-center justify-center rounded-cms-sm transition-colors cursor-pointer text-white/80 hover:text-white',
311
+ showHighlightPicker ? 'bg-white/20' : 'hover:bg-white/20',
312
+ )}
313
+ >
314
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
315
+ <path d="M12 20h9" />
316
+ <path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z" />
317
+ </svg>
318
+ <div
319
+ class="absolute bottom-1 left-1.5 right-1.5 h-1.5 rounded-full"
320
+ style={{
321
+ backgroundColor: currentStyle.highlight ? HIGHLIGHT_MAP[currentStyle.highlight] : 'transparent',
322
+ border: currentStyle.highlight && currentStyle.highlight !== 'none' ? 'none' : '1px solid rgba(255,255,255,0.3)',
323
+ }}
324
+ />
325
+ </button>
326
+
327
+ {showHighlightPicker && (
328
+ <div class="absolute top-full left-0 mt-2 p-3 bg-cms-dark border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.3)] rounded-cms-md flex gap-2 flex-wrap w-32 z-10">
329
+ {Object.entries(HIGHLIGHT_MAP).map(([key, color]) => (
330
+ <button
331
+ key={key}
332
+ type="button"
333
+ onClick={() => {
334
+ handleToggleStyle('highlight', key as StyleValue<'highlight'>)
335
+ setShowHighlightPicker(false)
336
+ }}
337
+ title={TAILWIND_STYLES.highlight[key as keyof typeof TAILWIND_STYLES.highlight]?.label || key}
338
+ class={cn(
339
+ 'w-5 h-5 rounded-full border-2 transition-transform cursor-pointer',
340
+ currentStyle.highlight === key ? 'border-cms-primary scale-125 ring-2 ring-cms-primary/30' : 'border-white/20 hover:scale-110',
341
+ )}
342
+ style={{ backgroundColor: color === 'transparent' ? '#333' : color }}
343
+ />
344
+ ))}
345
+ </div>
346
+ )}
347
+ </div>
348
+
349
+ <div class="w-px h-5 bg-white/20 mx-1" />
350
+
351
+ {/* Font Size */}
352
+ <select
353
+ value={currentStyle.size || 'base'}
354
+ onChange={(e) => handleToggleStyle('size', (e.target as HTMLSelectElement).value as StyleValue<'size'>)}
355
+ class="h-8 px-2 text-xs border border-white/20 rounded-cms-sm bg-white/10 text-white cursor-pointer hover:border-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/10"
356
+ >
357
+ {Object.entries(TAILWIND_STYLES.size).map(([key, { label }]) => <option key={key} value={key}>{label}</option>)}
358
+ </select>
359
+ </div>
360
+ </div>
361
+ )
362
+ }
@@ -0,0 +1,15 @@
1
+ import { Toast } from './toast'
2
+ import type { ToastMessage } from './types'
3
+
4
+ export interface ToastContainerProps {
5
+ toasts: ToastMessage[]
6
+ onRemove: (id: string) => void
7
+ }
8
+
9
+ export const ToastContainer = ({ toasts, onRemove }: ToastContainerProps) => {
10
+ return (
11
+ <div class="fixed left-1/2 -translate-x-1/2 bottom-28 z-2147483648 flex flex-col gap-2 items-center">
12
+ {toasts.map(toast => <Toast key={toast.id} {...toast} onRemove={onRemove} />)}
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,49 @@
1
+ import { useEffect, useState } from 'preact/hooks'
2
+ import { TIMING } from '../../constants'
3
+ import type { ToastMessage } from './types'
4
+
5
+ interface ToastProps extends ToastMessage {
6
+ onRemove: (id: string) => void
7
+ }
8
+
9
+ export const Toast = ({ id, message, type, onRemove }: ToastProps) => {
10
+ const [isVisible, setIsVisible] = useState(true)
11
+
12
+ useEffect(() => {
13
+ const hideTimer = setTimeout(() => {
14
+ setIsVisible(false)
15
+ }, TIMING.TOAST_VISIBLE_DURATION_MS)
16
+
17
+ const removeTimer = setTimeout(() => {
18
+ onRemove(id)
19
+ }, TIMING.TOAST_VISIBLE_DURATION_MS + TIMING.TOAST_FADE_DURATION_MS)
20
+
21
+ return () => {
22
+ clearTimeout(hideTimer)
23
+ clearTimeout(removeTimer)
24
+ }
25
+ }, [id, onRemove])
26
+
27
+ const typeClasses = {
28
+ error: 'bg-cms-dark border-l-4 border-l-cms-error text-white',
29
+ success: 'bg-cms-dark border-l-4 border-l-cms-primary text-white',
30
+ info: 'bg-cms-dark border-l-4 border-l-white/50 text-white',
31
+ }
32
+
33
+ return (
34
+ <div
35
+ class={`
36
+ px-3.5 py-2.5 font-sans text-[13px] font-medium
37
+ shadow-[0_8px_32px_rgba(0,0,0,0.25)] border border-white/10 rounded-cms
38
+ transition-all duration-300 ease-out flex items-center gap-3
39
+ ${typeClasses[type]}
40
+ ${isVisible ? 'opacity-100 translate-y-0 scale-100' : 'opacity-0 translate-y-2 scale-95'}
41
+ `}
42
+ >
43
+ {type === 'success' && <span class="text-cms-primary text-lg">✓</span>}
44
+ {type === 'error' && <span class="text-cms-error text-lg">✕</span>}
45
+ {type === 'info' && <span class="w-2.5 h-2.5 rounded-full bg-white/50 shrink-0" />}
46
+ {message}
47
+ </div>
48
+ )
49
+ }
@@ -0,0 +1,7 @@
1
+ export type ToastType = 'info' | 'success' | 'error'
2
+
3
+ export interface ToastMessage {
4
+ id: string
5
+ message: string
6
+ type: ToastType
7
+ }