@happyvertical/smrt-content 0.30.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 (291) hide show
  1. package/AGENTS.md +194 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +634 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/asset-associable.d.ts +115 -0
  8. package/dist/asset-associable.d.ts.map +1 -0
  9. package/dist/body-format.d.ts +29 -0
  10. package/dist/body-format.d.ts.map +1 -0
  11. package/dist/body-format.js +604 -0
  12. package/dist/body-format.js.map +1 -0
  13. package/dist/content-asset.d.ts +17 -0
  14. package/dist/content-asset.d.ts.map +1 -0
  15. package/dist/content-assets.d.ts +10 -0
  16. package/dist/content-assets.d.ts.map +1 -0
  17. package/dist/content-chat-handlers.d.ts +115 -0
  18. package/dist/content-chat-handlers.d.ts.map +1 -0
  19. package/dist/content-chat-prompts.d.ts +3 -0
  20. package/dist/content-chat-prompts.d.ts.map +1 -0
  21. package/dist/content-chat-session.d.ts +26 -0
  22. package/dist/content-chat-session.d.ts.map +1 -0
  23. package/dist/content-contribution-attachment.d.ts +42 -0
  24. package/dist/content-contribution-attachment.d.ts.map +1 -0
  25. package/dist/content-contribution-attachments.d.ts +8 -0
  26. package/dist/content-contribution-attachments.d.ts.map +1 -0
  27. package/dist/content-contribution-config.d.ts +84 -0
  28. package/dist/content-contribution-config.d.ts.map +1 -0
  29. package/dist/content-contribution-revision.d.ts +38 -0
  30. package/dist/content-contribution-revision.d.ts.map +1 -0
  31. package/dist/content-contribution-revisions.d.ts +8 -0
  32. package/dist/content-contribution-revisions.d.ts.map +1 -0
  33. package/dist/content-contribution-type.d.ts +51 -0
  34. package/dist/content-contribution-type.d.ts.map +1 -0
  35. package/dist/content-contribution-types.d.ts +7 -0
  36. package/dist/content-contribution-types.d.ts.map +1 -0
  37. package/dist/content-contribution.d.ts +161 -0
  38. package/dist/content-contribution.d.ts.map +1 -0
  39. package/dist/content-contributions.d.ts +53 -0
  40. package/dist/content-contributions.d.ts.map +1 -0
  41. package/dist/content-contributor.d.ts +30 -0
  42. package/dist/content-contributor.d.ts.map +1 -0
  43. package/dist/content-contributors.d.ts +13 -0
  44. package/dist/content-contributors.d.ts.map +1 -0
  45. package/dist/content-correction.d.ts +39 -0
  46. package/dist/content-correction.d.ts.map +1 -0
  47. package/dist/content-corrections.d.ts +9 -0
  48. package/dist/content-corrections.d.ts.map +1 -0
  49. package/dist/content-editor-assistant.d.ts +68 -0
  50. package/dist/content-editor-assistant.d.ts.map +1 -0
  51. package/dist/content-editor-assistant.js +97 -0
  52. package/dist/content-editor-assistant.js.map +1 -0
  53. package/dist/content-feed-parser.d.ts +19 -0
  54. package/dist/content-feed-parser.d.ts.map +1 -0
  55. package/dist/content-feed-source.d.ts +52 -0
  56. package/dist/content-feed-source.d.ts.map +1 -0
  57. package/dist/content-feed-sources.d.ts +11 -0
  58. package/dist/content-feed-sources.d.ts.map +1 -0
  59. package/dist/content-feed-sync.d.ts +23 -0
  60. package/dist/content-feed-sync.d.ts.map +1 -0
  61. package/dist/content-governance-assignment.d.ts +42 -0
  62. package/dist/content-governance-assignment.d.ts.map +1 -0
  63. package/dist/content-governance-assignments.d.ts +11 -0
  64. package/dist/content-governance-assignments.d.ts.map +1 -0
  65. package/dist/content-governance-policies.d.ts +7 -0
  66. package/dist/content-governance-policies.d.ts.map +1 -0
  67. package/dist/content-governance-policy.d.ts +29 -0
  68. package/dist/content-governance-policy.d.ts.map +1 -0
  69. package/dist/content-governance-profile.d.ts +31 -0
  70. package/dist/content-governance-profile.d.ts.map +1 -0
  71. package/dist/content-governance-profiles.d.ts +7 -0
  72. package/dist/content-governance-profiles.d.ts.map +1 -0
  73. package/dist/content-governance.d.ts +188 -0
  74. package/dist/content-governance.d.ts.map +1 -0
  75. package/dist/content-prompts.d.ts +10 -0
  76. package/dist/content-prompts.d.ts.map +1 -0
  77. package/dist/content-reference.d.ts +17 -0
  78. package/dist/content-reference.d.ts.map +1 -0
  79. package/dist/content-references.d.ts +55 -0
  80. package/dist/content-references.d.ts.map +1 -0
  81. package/dist/content-review.d.ts +34 -0
  82. package/dist/content-review.d.ts.map +1 -0
  83. package/dist/content-reviews.d.ts +21 -0
  84. package/dist/content-reviews.d.ts.map +1 -0
  85. package/dist/content-transparency.d.ts +72 -0
  86. package/dist/content-transparency.d.ts.map +1 -0
  87. package/dist/content-types.d.ts +51 -0
  88. package/dist/content-types.d.ts.map +1 -0
  89. package/dist/content-version.d.ts +38 -0
  90. package/dist/content-version.d.ts.map +1 -0
  91. package/dist/content-versions.d.ts +16 -0
  92. package/dist/content-versions.d.ts.map +1 -0
  93. package/dist/content.d.ts +736 -0
  94. package/dist/content.d.ts.map +1 -0
  95. package/dist/contents.d.ts +292 -0
  96. package/dist/contents.d.ts.map +1 -0
  97. package/dist/database-utils.d.ts +3 -0
  98. package/dist/database-utils.d.ts.map +1 -0
  99. package/dist/index.d.ts +78 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +11602 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/manifest.json +12308 -0
  104. package/dist/mock-smrt-client.d.ts +493 -0
  105. package/dist/mock-smrt-client.d.ts.map +1 -0
  106. package/dist/mock-smrt-client.js +390 -0
  107. package/dist/mock-smrt-client.js.map +1 -0
  108. package/dist/playground.d.ts +2 -0
  109. package/dist/playground.d.ts.map +1 -0
  110. package/dist/playground.js +454 -0
  111. package/dist/playground.js.map +1 -0
  112. package/dist/publish-readiness.d.ts +30 -0
  113. package/dist/publish-readiness.d.ts.map +1 -0
  114. package/dist/publish-readiness.js +74 -0
  115. package/dist/publish-readiness.js.map +1 -0
  116. package/dist/safe-remote-url.d.ts +52 -0
  117. package/dist/safe-remote-url.d.ts.map +1 -0
  118. package/dist/serialization.d.ts +78 -0
  119. package/dist/serialization.d.ts.map +1 -0
  120. package/dist/smrt-knowledge.json +6130 -0
  121. package/dist/svelte/api.d.ts +3 -0
  122. package/dist/svelte/api.d.ts.map +1 -0
  123. package/dist/svelte/api.js +10 -0
  124. package/dist/svelte/components/ArticleCard.svelte +159 -0
  125. package/dist/svelte/components/ArticleCard.svelte.d.ts +17 -0
  126. package/dist/svelte/components/ArticleCard.svelte.d.ts.map +1 -0
  127. package/dist/svelte/components/ArticleList.svelte +75 -0
  128. package/dist/svelte/components/ArticleList.svelte.d.ts +21 -0
  129. package/dist/svelte/components/ArticleList.svelte.d.ts.map +1 -0
  130. package/dist/svelte/components/ContentAgentChat.svelte +652 -0
  131. package/dist/svelte/components/ContentAgentChat.svelte.d.ts +17 -0
  132. package/dist/svelte/components/ContentAgentChat.svelte.d.ts.map +1 -0
  133. package/dist/svelte/components/ContentBodyEditor.svelte +1446 -0
  134. package/dist/svelte/components/ContentBodyEditor.svelte.d.ts +25 -0
  135. package/dist/svelte/components/ContentBodyEditor.svelte.d.ts.map +1 -0
  136. package/dist/svelte/components/ContentBodyRenderer.svelte +152 -0
  137. package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts +10 -0
  138. package/dist/svelte/components/ContentBodyRenderer.svelte.d.ts.map +1 -0
  139. package/dist/svelte/components/ContentClaimAuditTool.svelte +441 -0
  140. package/dist/svelte/components/ContentClaimAuditTool.svelte.d.ts +12 -0
  141. package/dist/svelte/components/ContentClaimAuditTool.svelte.d.ts.map +1 -0
  142. package/dist/svelte/components/ContentContributionForm.svelte +226 -0
  143. package/dist/svelte/components/ContentContributionForm.svelte.d.ts +23 -0
  144. package/dist/svelte/components/ContentContributionForm.svelte.d.ts.map +1 -0
  145. package/dist/svelte/components/ContentContributionInbox.svelte +322 -0
  146. package/dist/svelte/components/ContentContributionInbox.svelte.d.ts +22 -0
  147. package/dist/svelte/components/ContentContributionInbox.svelte.d.ts.map +1 -0
  148. package/dist/svelte/components/ContentContributionPortal.svelte +182 -0
  149. package/dist/svelte/components/ContentContributionPortal.svelte.d.ts +12 -0
  150. package/dist/svelte/components/ContentContributionPortal.svelte.d.ts.map +1 -0
  151. package/dist/svelte/components/ContentContributionTypeManager.svelte +281 -0
  152. package/dist/svelte/components/ContentContributionTypeManager.svelte.d.ts +10 -0
  153. package/dist/svelte/components/ContentContributionTypeManager.svelte.d.ts.map +1 -0
  154. package/dist/svelte/components/ContentContributorManager.svelte +140 -0
  155. package/dist/svelte/components/ContentContributorManager.svelte.d.ts +10 -0
  156. package/dist/svelte/components/ContentContributorManager.svelte.d.ts.map +1 -0
  157. package/dist/svelte/components/ContentCorrectionsTool.svelte +361 -0
  158. package/dist/svelte/components/ContentCorrectionsTool.svelte.d.ts +11 -0
  159. package/dist/svelte/components/ContentCorrectionsTool.svelte.d.ts.map +1 -0
  160. package/dist/svelte/components/ContentEditor.svelte +2166 -0
  161. package/dist/svelte/components/ContentEditor.svelte.d.ts +26 -0
  162. package/dist/svelte/components/ContentEditor.svelte.d.ts.map +1 -0
  163. package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte +199 -0
  164. package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte.d.ts +11 -0
  165. package/dist/svelte/components/ContentGovernanceAssignmentEditor.svelte.d.ts.map +1 -0
  166. package/dist/svelte/components/ContentGovernanceManager.svelte +340 -0
  167. package/dist/svelte/components/ContentGovernanceManager.svelte.d.ts +11 -0
  168. package/dist/svelte/components/ContentGovernanceManager.svelte.d.ts.map +1 -0
  169. package/dist/svelte/components/ContentGovernancePanel.svelte +2244 -0
  170. package/dist/svelte/components/ContentGovernancePanel.svelte.d.ts +26 -0
  171. package/dist/svelte/components/ContentGovernancePanel.svelte.d.ts.map +1 -0
  172. package/dist/svelte/components/ContentGovernancePolicyEditor.svelte +110 -0
  173. package/dist/svelte/components/ContentGovernancePolicyEditor.svelte.d.ts +10 -0
  174. package/dist/svelte/components/ContentGovernancePolicyEditor.svelte.d.ts.map +1 -0
  175. package/dist/svelte/components/ContentGovernanceProfileEditor.svelte +185 -0
  176. package/dist/svelte/components/ContentGovernanceProfileEditor.svelte.d.ts +11 -0
  177. package/dist/svelte/components/ContentGovernanceProfileEditor.svelte.d.ts.map +1 -0
  178. package/dist/svelte/components/ContentGovernanceTool.svelte +56 -0
  179. package/dist/svelte/components/ContentGovernanceTool.svelte.d.ts +13 -0
  180. package/dist/svelte/components/ContentGovernanceTool.svelte.d.ts.map +1 -0
  181. package/dist/svelte/components/ContentImageBrowser.svelte +243 -0
  182. package/dist/svelte/components/ContentImageBrowser.svelte.d.ts +18 -0
  183. package/dist/svelte/components/ContentImageBrowser.svelte.d.ts.map +1 -0
  184. package/dist/svelte/components/ContentImageChooser.svelte +134 -0
  185. package/dist/svelte/components/ContentImageChooser.svelte.d.ts +11 -0
  186. package/dist/svelte/components/ContentImageChooser.svelte.d.ts.map +1 -0
  187. package/dist/svelte/components/ContentList.svelte +906 -0
  188. package/dist/svelte/components/ContentList.svelte.d.ts +16 -0
  189. package/dist/svelte/components/ContentList.svelte.d.ts.map +1 -0
  190. package/dist/svelte/components/ContentMetadataFields.svelte +107 -0
  191. package/dist/svelte/components/ContentMetadataFields.svelte.d.ts +8 -0
  192. package/dist/svelte/components/ContentMetadataFields.svelte.d.ts.map +1 -0
  193. package/dist/svelte/components/ContentReferencesPanel.svelte +221 -0
  194. package/dist/svelte/components/ContentReferencesPanel.svelte.d.ts +20 -0
  195. package/dist/svelte/components/ContentReferencesPanel.svelte.d.ts.map +1 -0
  196. package/dist/svelte/components/ContentReviewStatusTray.svelte +151 -0
  197. package/dist/svelte/components/ContentReviewStatusTray.svelte.d.ts +20 -0
  198. package/dist/svelte/components/ContentReviewStatusTray.svelte.d.ts.map +1 -0
  199. package/dist/svelte/components/ContentStatusFields.svelte +85 -0
  200. package/dist/svelte/components/ContentStatusFields.svelte.d.ts +8 -0
  201. package/dist/svelte/components/ContentStatusFields.svelte.d.ts.map +1 -0
  202. package/dist/svelte/components/ContentTitleField.svelte +54 -0
  203. package/dist/svelte/components/ContentTitleField.svelte.d.ts +10 -0
  204. package/dist/svelte/components/ContentTitleField.svelte.d.ts.map +1 -0
  205. package/dist/svelte/components/ContentTransparencyReport.svelte +322 -0
  206. package/dist/svelte/components/ContentTransparencyReport.svelte.d.ts +10 -0
  207. package/dist/svelte/components/ContentTransparencyReport.svelte.d.ts.map +1 -0
  208. package/dist/svelte/components/ContentTransparencyTool.svelte +314 -0
  209. package/dist/svelte/components/ContentTransparencyTool.svelte.d.ts +10 -0
  210. package/dist/svelte/components/ContentTransparencyTool.svelte.d.ts.map +1 -0
  211. package/dist/svelte/components/ContentVersionsTool.svelte +291 -0
  212. package/dist/svelte/components/ContentVersionsTool.svelte.d.ts +10 -0
  213. package/dist/svelte/components/ContentVersionsTool.svelte.d.ts.map +1 -0
  214. package/dist/svelte/components/GovernedContentEditor.svelte +409 -0
  215. package/dist/svelte/components/GovernedContentEditor.svelte.d.ts +35 -0
  216. package/dist/svelte/components/GovernedContentEditor.svelte.d.ts.map +1 -0
  217. package/dist/svelte/components/ImageThumbnail.cache.d.ts +14 -0
  218. package/dist/svelte/components/ImageThumbnail.cache.d.ts.map +1 -0
  219. package/dist/svelte/components/ImageThumbnail.cache.js +36 -0
  220. package/dist/svelte/components/ImageThumbnail.svelte +159 -0
  221. package/dist/svelte/components/ImageThumbnail.svelte.d.ts +8 -0
  222. package/dist/svelte/components/ImageThumbnail.svelte.d.ts.map +1 -0
  223. package/dist/svelte/components/Markdown.svelte +125 -0
  224. package/dist/svelte/components/Markdown.svelte.d.ts +11 -0
  225. package/dist/svelte/components/Markdown.svelte.d.ts.map +1 -0
  226. package/dist/svelte/content-editor-form.d.ts +63 -0
  227. package/dist/svelte/content-editor-form.d.ts.map +1 -0
  228. package/dist/svelte/content-editor-form.js +94 -0
  229. package/dist/svelte/content-editor-media.d.ts +12 -0
  230. package/dist/svelte/content-editor-media.d.ts.map +1 -0
  231. package/dist/svelte/content-editor-media.js +84 -0
  232. package/dist/svelte/content-editor-state.svelte.d.ts +35 -0
  233. package/dist/svelte/content-editor-state.svelte.d.ts.map +1 -0
  234. package/dist/svelte/content-editor-state.svelte.js +141 -0
  235. package/dist/svelte/governance-manager-client.d.ts +22 -0
  236. package/dist/svelte/governance-manager-client.d.ts.map +1 -0
  237. package/dist/svelte/governance-manager-client.js +1 -0
  238. package/dist/svelte/i18n.contribution.d.ts +57 -0
  239. package/dist/svelte/i18n.contribution.d.ts.map +1 -0
  240. package/dist/svelte/i18n.contribution.js +64 -0
  241. package/dist/svelte/i18n.editor.d.ts +71 -0
  242. package/dist/svelte/i18n.editor.d.ts.map +1 -0
  243. package/dist/svelte/i18n.editor.js +87 -0
  244. package/dist/svelte/i18n.governance.d.ts +66 -0
  245. package/dist/svelte/i18n.governance.d.ts.map +1 -0
  246. package/dist/svelte/i18n.governance.js +66 -0
  247. package/dist/svelte/i18n.routes.d.ts +66 -0
  248. package/dist/svelte/i18n.routes.d.ts.map +1 -0
  249. package/dist/svelte/i18n.routes.js +75 -0
  250. package/dist/svelte/i18n.tools.d.ts +81 -0
  251. package/dist/svelte/i18n.tools.d.ts.map +1 -0
  252. package/dist/svelte/i18n.tools.js +90 -0
  253. package/dist/svelte/index.d.ts +101 -0
  254. package/dist/svelte/index.d.ts.map +1 -0
  255. package/dist/svelte/index.js +63 -0
  256. package/dist/svelte/playground.d.ts +281 -0
  257. package/dist/svelte/playground.d.ts.map +1 -0
  258. package/dist/svelte/playground.js +438 -0
  259. package/dist/svelte/routes/ContentContributionsRoute.svelte +809 -0
  260. package/dist/svelte/routes/ContentContributionsRoute.svelte.d.ts +10 -0
  261. package/dist/svelte/routes/ContentContributionsRoute.svelte.d.ts.map +1 -0
  262. package/dist/svelte/routes/ContentFactsRoute.svelte +612 -0
  263. package/dist/svelte/routes/ContentFactsRoute.svelte.d.ts +11 -0
  264. package/dist/svelte/routes/ContentFactsRoute.svelte.d.ts.map +1 -0
  265. package/dist/svelte/routes/ContentGovernanceRoute.svelte +218 -0
  266. package/dist/svelte/routes/ContentGovernanceRoute.svelte.d.ts +10 -0
  267. package/dist/svelte/routes/ContentGovernanceRoute.svelte.d.ts.map +1 -0
  268. package/dist/svelte/routes/ContentWorkspaceRoute.svelte +431 -0
  269. package/dist/svelte/routes/ContentWorkspaceRoute.svelte.d.ts +12 -0
  270. package/dist/svelte/routes/ContentWorkspaceRoute.svelte.d.ts.map +1 -0
  271. package/dist/svelte/routes/PublishedArticleRoute.svelte +194 -0
  272. package/dist/svelte/routes/PublishedArticleRoute.svelte.d.ts +10 -0
  273. package/dist/svelte/routes/PublishedArticleRoute.svelte.d.ts.map +1 -0
  274. package/dist/svelte/routes/index.d.ts +8 -0
  275. package/dist/svelte/routes/index.d.ts.map +1 -0
  276. package/dist/svelte/routes/index.js +6 -0
  277. package/dist/svelte/routes/shared.d.ts +90 -0
  278. package/dist/svelte/routes/shared.d.ts.map +1 -0
  279. package/dist/svelte/routes/shared.js +104 -0
  280. package/dist/svelte/types.d.ts +69 -0
  281. package/dist/svelte/types.d.ts.map +1 -0
  282. package/dist/svelte/types.js +6 -0
  283. package/dist/thumbnail-generator.d.ts +174 -0
  284. package/dist/thumbnail-generator.d.ts.map +1 -0
  285. package/dist/ui.d.ts +10 -0
  286. package/dist/ui.d.ts.map +1 -0
  287. package/dist/ui.js +42 -0
  288. package/dist/ui.js.map +1 -0
  289. package/dist/utils.d.ts +18 -0
  290. package/dist/utils.d.ts.map +1 -0
  291. package/package.json +119 -0
@@ -0,0 +1,1446 @@
1
+ <script lang="ts">
2
+ import type { ImageLike } from '@happyvertical/smrt-images/svelte';
3
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
4
+ import {
5
+ bodyToEditorHtml,
6
+ type ContentBodyFormat,
7
+ type ContentBodyImage,
8
+ type ContentBodyImagePlacement,
9
+ editorHtmlToBody,
10
+ extractBodyImages,
11
+ imageAssetToHtml,
12
+ normalizeEditorHtml,
13
+ resolveBodyFormat,
14
+ } from '../../body-format';
15
+ import { M } from '../i18n.editor.js';
16
+
17
+ const { t } = useI18n();
18
+
19
+ export interface ContentBodyEditorChange {
20
+ body: string;
21
+ bodyFormat: ContentBodyFormat;
22
+ images: ContentBodyImage[];
23
+ }
24
+
25
+ export interface Props {
26
+ value: string;
27
+ format?: ContentBodyFormat | null;
28
+ placeholder?: string;
29
+ selectedImageIndex?: number;
30
+ onChange?: (change: ContentBodyEditorChange) => void;
31
+ onOpenImageChooser?: () => void;
32
+ onSelectImage?: (index: number) => void;
33
+ onUseImageAsThumbnail?: (assetId: string) => void;
34
+ onResolveImage?: (
35
+ selected: ImageLike | File | string,
36
+ ) => Promise<any | null> | any | null;
37
+ }
38
+
39
+ let {
40
+ value,
41
+ format = null,
42
+ placeholder = 'Start writing...',
43
+ selectedImageIndex = -1,
44
+ onChange = undefined,
45
+ onOpenImageChooser = undefined,
46
+ onSelectImage = undefined,
47
+ onUseImageAsThumbnail = undefined,
48
+ onResolveImage = undefined,
49
+ }: Props = $props();
50
+
51
+ const MIN_IMAGE_WIDTH = 120;
52
+ const IMAGE_WIDTH_STEP = 80;
53
+ const INPUT_CHANGE_DEBOUNCE_MS = 120;
54
+ const ELEMENT_NODE = 1;
55
+ const TEXT_NODE = 3;
56
+ const EDITOR_TEXT_BLOCK_TAGS = new Set([
57
+ 'P',
58
+ 'DIV',
59
+ 'LI',
60
+ 'H1',
61
+ 'H2',
62
+ 'H3',
63
+ 'H4',
64
+ 'H5',
65
+ 'H6',
66
+ 'BLOCKQUOTE',
67
+ 'PRE',
68
+ ]);
69
+
70
+ type ImageBox = {
71
+ top: number;
72
+ left: number;
73
+ width: number;
74
+ height: number;
75
+ };
76
+
77
+ let editorElement = $state<HTMLDivElement | null>(null);
78
+ let rootElement = $state<HTMLDivElement | null>(null);
79
+ let currentFormat = $state<ContentBodyFormat>('html');
80
+ let editorHtml = $state('');
81
+ let lastExternalKey = $state('');
82
+ let isFocused = $state(false);
83
+ let isDragging = $state(false);
84
+ let selectedImageIndexState = $state(-1);
85
+ let selectedImageBox = $state<ImageBox | null>(null);
86
+ let selectedImagePlacement = $state<ContentBodyImagePlacement>('block');
87
+ let selectedImageAssetId = $state<string | null>(null);
88
+ let savedRange: Range | null = null;
89
+ let movingImageIndex: number | null = null;
90
+ let resizeState: {
91
+ frame: HTMLElement;
92
+ startX: number;
93
+ startWidth: number;
94
+ } | null = null;
95
+ let moveState: {
96
+ imageIndex: number;
97
+ frame: HTMLElement;
98
+ } | null = null;
99
+ let lastAppliedSelectedImageIndex: number | null = null;
100
+ let inputChangeTimer: ReturnType<typeof setTimeout> | null = null;
101
+
102
+ function makeExternalKey(body: string, bodyFormat: ContentBodyFormat) {
103
+ return `${bodyFormat}\u0000${body || ''}`;
104
+ }
105
+
106
+ $effect(() => {
107
+ const resolvedFormat = resolveBodyFormat(format, value);
108
+ const externalKey = makeExternalKey(value || '', resolvedFormat);
109
+ if (externalKey === lastExternalKey) {
110
+ return;
111
+ }
112
+
113
+ currentFormat = resolvedFormat;
114
+ editorHtml = bodyToEditorHtml(value || '', resolvedFormat);
115
+ lastExternalKey = externalKey;
116
+
117
+ if (editorElement && !isFocused && editorElement.innerHTML !== editorHtml) {
118
+ editorElement.innerHTML = editorHtml;
119
+ }
120
+ });
121
+
122
+ $effect(() => {
123
+ const nextSelectedImageIndex = selectedImageIndex;
124
+ if (nextSelectedImageIndex === lastAppliedSelectedImageIndex) {
125
+ return;
126
+ }
127
+
128
+ lastAppliedSelectedImageIndex = nextSelectedImageIndex;
129
+ if (nextSelectedImageIndex >= 0) {
130
+ focusImage(nextSelectedImageIndex);
131
+ } else {
132
+ clearImageSelection({ notify: false });
133
+ }
134
+ });
135
+
136
+ $effect(() => {
137
+ if (typeof window === 'undefined') {
138
+ return;
139
+ }
140
+
141
+ const update = () => {
142
+ if (selectedImageIndexState < 0) {
143
+ return;
144
+ }
145
+ refreshSelectedImageChrome();
146
+ };
147
+ window.addEventListener('resize', update);
148
+ window.addEventListener('scroll', update, true);
149
+
150
+ return () => {
151
+ window.removeEventListener('resize', update);
152
+ window.removeEventListener('scroll', update, true);
153
+ };
154
+ });
155
+
156
+ $effect(() => {
157
+ return () => clearPendingInputChange();
158
+ });
159
+
160
+ function saveSelection() {
161
+ if (typeof window === 'undefined' || !editorElement) {
162
+ return;
163
+ }
164
+
165
+ const selection = window.getSelection();
166
+ if (!selection || selection.rangeCount === 0) {
167
+ return;
168
+ }
169
+
170
+ const range = selection.getRangeAt(0);
171
+ if (editorElement.contains(range.commonAncestorContainer)) {
172
+ savedRange = range.cloneRange();
173
+ }
174
+ }
175
+
176
+ function restoreSelection() {
177
+ if (typeof window === 'undefined' || !editorElement) {
178
+ return;
179
+ }
180
+
181
+ editorElement.focus();
182
+ const selection = window.getSelection();
183
+ if (!selection) {
184
+ return;
185
+ }
186
+
187
+ selection.removeAllRanges();
188
+ if (savedRange) {
189
+ selection.addRange(savedRange);
190
+ return;
191
+ }
192
+
193
+ const range = document.createRange();
194
+ range.selectNodeContents(editorElement);
195
+ range.collapse(false);
196
+ selection.addRange(range);
197
+ savedRange = range.cloneRange();
198
+ }
199
+
200
+ function getElementFromNode(node: Node | null): HTMLElement | null {
201
+ if (!node) {
202
+ return null;
203
+ }
204
+
205
+ if (node.nodeType === ELEMENT_NODE) {
206
+ return node as HTMLElement;
207
+ }
208
+
209
+ return node.parentElement;
210
+ }
211
+
212
+ function findEditableBlock(node: Node | null): HTMLElement | null {
213
+ let element = getElementFromNode(node);
214
+
215
+ while (element && element !== editorElement) {
216
+ if (EDITOR_TEXT_BLOCK_TAGS.has(element.tagName)) {
217
+ return element;
218
+ }
219
+
220
+ element = element.parentElement;
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ function isImageOnlyBlock(element: HTMLElement): boolean {
227
+ if (!EDITOR_TEXT_BLOCK_TAGS.has(element.tagName)) {
228
+ return false;
229
+ }
230
+
231
+ if (element.querySelectorAll('img').length !== 1) {
232
+ return false;
233
+ }
234
+
235
+ return Array.from(element.childNodes).every((child) => {
236
+ if (child.nodeType === TEXT_NODE) {
237
+ return !child.textContent?.trim();
238
+ }
239
+
240
+ if (child.nodeType !== ELEMENT_NODE) {
241
+ return true;
242
+ }
243
+
244
+ const childElement = child as HTMLElement;
245
+ return childElement.tagName === 'IMG' || childElement.tagName === 'BR';
246
+ });
247
+ }
248
+
249
+ function isEmptyEditableBlock(element: HTMLElement): boolean {
250
+ return (
251
+ EDITOR_TEXT_BLOCK_TAGS.has(element.tagName) &&
252
+ !element.textContent?.trim() &&
253
+ !element.querySelector('img, video, iframe, table')
254
+ );
255
+ }
256
+
257
+ function getInsertionAnchor(block: HTMLElement): HTMLElement {
258
+ if (block.tagName === 'LI') {
259
+ const list = block.closest('ul, ol') as HTMLElement | null;
260
+ if (list && editorElement?.contains(list)) {
261
+ return list;
262
+ }
263
+ }
264
+
265
+ return block;
266
+ }
267
+
268
+ function createTrailingTextBlock(): HTMLParagraphElement {
269
+ const paragraph = document.createElement('p');
270
+ paragraph.appendChild(document.createElement('br'));
271
+ return paragraph;
272
+ }
273
+
274
+ function insertNodesAtEditableRange(range: Range, nodes: Node[]) {
275
+ const activeBlock = findEditableBlock(range.commonAncestorContainer);
276
+
277
+ if (activeBlock) {
278
+ const anchor = getInsertionAnchor(activeBlock);
279
+ if (isEmptyEditableBlock(activeBlock) && activeBlock === anchor) {
280
+ activeBlock.replaceWith(...nodes);
281
+ } else {
282
+ anchor.after(...nodes);
283
+ }
284
+ return;
285
+ }
286
+
287
+ const fragment = document.createDocumentFragment();
288
+ fragment.append(...nodes);
289
+ range.insertNode(fragment);
290
+ }
291
+
292
+ function setCaretInsideElement(element: HTMLElement, atStart = true) {
293
+ if (typeof window === 'undefined') {
294
+ return;
295
+ }
296
+
297
+ const selection = window.getSelection();
298
+ if (!selection) {
299
+ return;
300
+ }
301
+
302
+ const range = document.createRange();
303
+ range.selectNodeContents(element);
304
+ range.collapse(atStart);
305
+ selection.removeAllRanges();
306
+ selection.addRange(range);
307
+ savedRange = range.cloneRange();
308
+ }
309
+
310
+ function setCaretAfterNode(node: Node) {
311
+ if (typeof window === 'undefined') {
312
+ return;
313
+ }
314
+
315
+ const selection = window.getSelection();
316
+ if (!selection) {
317
+ return;
318
+ }
319
+
320
+ const range = document.createRange();
321
+ range.setStartAfter(node);
322
+ range.collapse(true);
323
+ selection.removeAllRanges();
324
+ selection.addRange(range);
325
+ savedRange = range.cloneRange();
326
+ }
327
+
328
+ function getEditorImages(): HTMLImageElement[] {
329
+ if (!editorElement) {
330
+ return [];
331
+ }
332
+
333
+ return Array.from(editorElement.querySelectorAll('img'));
334
+ }
335
+
336
+ function getImageFrame(image: HTMLImageElement): HTMLElement {
337
+ const figure = image.closest(
338
+ 'figure[data-smrt-inline-image]',
339
+ ) as HTMLElement | null;
340
+
341
+ if (figure && editorElement?.contains(figure)) {
342
+ return figure;
343
+ }
344
+
345
+ return image;
346
+ }
347
+
348
+ function getImageLayoutRoot(image: HTMLImageElement): HTMLElement {
349
+ const frame = getImageFrame(image);
350
+ if (frame.tagName === 'FIGURE') {
351
+ return frame;
352
+ }
353
+
354
+ const block = findEditableBlock(frame);
355
+ return block && isImageOnlyBlock(block) ? block : frame;
356
+ }
357
+
358
+ function getSelectedImage(): HTMLImageElement | null {
359
+ const images = getEditorImages();
360
+ return selectedImageIndexState >= 0
361
+ ? images[selectedImageIndexState] || null
362
+ : null;
363
+ }
364
+
365
+ function getImagePlacement(frame: HTMLElement): ContentBodyImagePlacement {
366
+ const placement = frame.getAttribute('data-smrt-placement');
367
+ return placement === 'left' ||
368
+ placement === 'right' ||
369
+ placement === 'center' ||
370
+ placement === 'full' ||
371
+ placement === 'block'
372
+ ? placement
373
+ : 'block';
374
+ }
375
+
376
+ function getImageWidth(frame: HTMLElement): number {
377
+ const attributeWidth = Number(frame.getAttribute('data-smrt-width'));
378
+ if (Number.isFinite(attributeWidth) && attributeWidth > 0) {
379
+ return attributeWidth;
380
+ }
381
+
382
+ const rectWidth = Math.round(frame.getBoundingClientRect().width);
383
+ if (rectWidth > 0) {
384
+ return rectWidth;
385
+ }
386
+
387
+ return 360;
388
+ }
389
+
390
+ function getMaxImageWidth(): number {
391
+ const editorWidth = editorElement?.getBoundingClientRect().width || 720;
392
+ return Math.max(MIN_IMAGE_WIDTH, Math.round(editorWidth - 32));
393
+ }
394
+
395
+ function setImageWidth(frame: HTMLElement, width: number) {
396
+ const clamped = Math.min(
397
+ getMaxImageWidth(),
398
+ Math.max(MIN_IMAGE_WIDTH, Math.round(width)),
399
+ );
400
+
401
+ frame.setAttribute('data-smrt-width', String(clamped));
402
+ frame.style.width = `${clamped}px`;
403
+ frame.style.maxWidth = '100%';
404
+
405
+ if (getImagePlacement(frame) === 'full') {
406
+ frame.setAttribute('data-smrt-placement', 'block');
407
+ }
408
+ }
409
+
410
+ function getRangeFromPoint(x: number, y: number): Range | null {
411
+ if (typeof document === 'undefined' || !editorElement) {
412
+ return null;
413
+ }
414
+
415
+ const doc = editorElement.ownerDocument as Document & {
416
+ caretRangeFromPoint?: (x: number, y: number) => Range | null;
417
+ caretPositionFromPoint?: (
418
+ x: number,
419
+ y: number,
420
+ ) => { offsetNode: Node; offset: number } | null;
421
+ };
422
+
423
+ if (typeof doc.caretRangeFromPoint === 'function') {
424
+ const range = doc.caretRangeFromPoint(x, y);
425
+ if (range && editorElement.contains(range.commonAncestorContainer)) {
426
+ return range;
427
+ }
428
+ }
429
+
430
+ if (typeof doc.caretPositionFromPoint === 'function') {
431
+ const position = doc.caretPositionFromPoint(x, y);
432
+ if (position && editorElement.contains(position.offsetNode)) {
433
+ const range = doc.createRange();
434
+ range.setStart(position.offsetNode, position.offset);
435
+ range.collapse(true);
436
+ return range;
437
+ }
438
+ }
439
+
440
+ return savedRange?.cloneRange() || null;
441
+ }
442
+
443
+ function placementFromPoint(x: number): ContentBodyImagePlacement | null {
444
+ if (!editorElement) {
445
+ return null;
446
+ }
447
+
448
+ const rect = editorElement.getBoundingClientRect();
449
+ const relativeX = (x - rect.left) / rect.width;
450
+ if (relativeX < 0.28) {
451
+ return 'left';
452
+ }
453
+ if (relativeX > 0.72) {
454
+ return 'right';
455
+ }
456
+ return null;
457
+ }
458
+
459
+ function refreshSelectedImageChrome() {
460
+ if (selectedImageIndexState < 0) {
461
+ selectedImageBox = null;
462
+ selectedImageAssetId = null;
463
+ selectedImagePlacement = 'block';
464
+ return;
465
+ }
466
+
467
+ const image = getSelectedImage();
468
+ if (!image || !rootElement || !editorElement?.contains(image)) {
469
+ selectedImageBox = null;
470
+ selectedImageAssetId = null;
471
+ selectedImagePlacement = 'block';
472
+ return;
473
+ }
474
+
475
+ const frame = getImageFrame(image);
476
+ const imageRect = frame.getBoundingClientRect();
477
+ const rootRect = rootElement.getBoundingClientRect();
478
+
479
+ selectedImageBox = {
480
+ top: imageRect.top - rootRect.top,
481
+ left: imageRect.left - rootRect.left,
482
+ width: imageRect.width,
483
+ height: imageRect.height,
484
+ };
485
+ selectedImagePlacement = getImagePlacement(frame);
486
+ selectedImageAssetId = image.getAttribute('data-smrt-asset-id');
487
+ }
488
+
489
+ function placeCaretAfterImage(image: HTMLImageElement) {
490
+ if (!editorElement) {
491
+ return;
492
+ }
493
+
494
+ const root = getImageLayoutRoot(image);
495
+ const parent = root.parentElement;
496
+ if (!parent || parent !== editorElement) {
497
+ setCaretAfterNode(root);
498
+ return;
499
+ }
500
+
501
+ let next = root.nextSibling;
502
+ while (next?.nodeType === TEXT_NODE && !next.textContent?.trim()) {
503
+ next = next.nextSibling;
504
+ }
505
+
506
+ if (
507
+ next?.nodeType === ELEMENT_NODE &&
508
+ EDITOR_TEXT_BLOCK_TAGS.has((next as HTMLElement).tagName)
509
+ ) {
510
+ setCaretInsideElement(next as HTMLElement);
511
+ return;
512
+ }
513
+
514
+ const trailingBlock = createTrailingTextBlock();
515
+ root.after(trailingBlock);
516
+ setCaretInsideElement(trailingBlock);
517
+ }
518
+
519
+ function selectImageElement(
520
+ image: HTMLImageElement,
521
+ options: { notify?: boolean } = {},
522
+ ) {
523
+ const images = getEditorImages();
524
+ const index = images.indexOf(image);
525
+ if (index < 0) {
526
+ return;
527
+ }
528
+
529
+ for (const candidate of images) {
530
+ candidate.removeAttribute('data-smrt-selected');
531
+ }
532
+
533
+ image.setAttribute('data-smrt-selected', 'true');
534
+ selectedImageIndexState = index;
535
+ refreshSelectedImageChrome();
536
+ placeCaretAfterImage(image);
537
+
538
+ if (options.notify !== false) {
539
+ onSelectImage?.(index);
540
+ }
541
+ }
542
+
543
+ function clearImageSelection(options: { notify?: boolean } = {}) {
544
+ const hadSelection =
545
+ selectedImageIndexState >= 0 ||
546
+ getEditorImages().some((image) => image.hasAttribute('data-smrt-selected'));
547
+
548
+ for (const image of getEditorImages()) {
549
+ image.removeAttribute('data-smrt-selected');
550
+ }
551
+ selectedImageIndexState = -1;
552
+ selectedImageBox = null;
553
+ selectedImageAssetId = null;
554
+
555
+ if (hadSelection && options.notify !== false) {
556
+ onSelectImage?.(-1);
557
+ }
558
+ }
559
+
560
+ function clearPendingInputChange() {
561
+ if (inputChangeTimer) {
562
+ clearTimeout(inputChangeTimer);
563
+ inputChangeTimer = null;
564
+ }
565
+ }
566
+
567
+ function scheduleInputChange() {
568
+ clearPendingInputChange();
569
+ inputChangeTimer = setTimeout(() => {
570
+ inputChangeTimer = null;
571
+ emitChange();
572
+ }, INPUT_CHANGE_DEBOUNCE_MS);
573
+ }
574
+
575
+ function emitChange(options: { syncDom?: boolean } = {}) {
576
+ if (!editorElement) {
577
+ return;
578
+ }
579
+
580
+ clearPendingInputChange();
581
+ const rawHtml = editorElement.innerHTML;
582
+ const normalizedHtml = normalizeEditorHtml(rawHtml);
583
+ if (options.syncDom && editorElement.innerHTML !== normalizedHtml) {
584
+ editorElement.innerHTML = normalizedHtml;
585
+ editorHtml = normalizedHtml;
586
+ }
587
+
588
+ const body = editorHtmlToBody(normalizedHtml, currentFormat);
589
+ lastExternalKey = makeExternalKey(body, currentFormat);
590
+ onChange?.({
591
+ body,
592
+ bodyFormat: currentFormat,
593
+ images: extractBodyImages(body, currentFormat),
594
+ });
595
+ refreshSelectedImageChrome();
596
+ }
597
+
598
+ function runCommand(command: string, value?: string) {
599
+ restoreSelection();
600
+ document.execCommand(command, false, value);
601
+ saveSelection();
602
+ emitChange();
603
+ }
604
+
605
+ function setBodyFormat(nextFormat: ContentBodyFormat) {
606
+ currentFormat = nextFormat;
607
+ emitChange();
608
+ }
609
+
610
+ function insertHtml(html: string) {
611
+ if (!html || !editorElement) {
612
+ return;
613
+ }
614
+
615
+ restoreSelection();
616
+ const selection = window.getSelection();
617
+ if (!selection || selection.rangeCount === 0) {
618
+ return;
619
+ }
620
+
621
+ const range = selection.getRangeAt(0);
622
+ range.deleteContents();
623
+ const fragment = range.createContextualFragment(html);
624
+ const lastNode = fragment.lastChild;
625
+ range.insertNode(fragment);
626
+
627
+ if (lastNode) {
628
+ range.setStartAfter(lastNode);
629
+ range.collapse(true);
630
+ selection.removeAllRanges();
631
+ selection.addRange(range);
632
+ }
633
+
634
+ saveSelection();
635
+ emitChange();
636
+ }
637
+
638
+ function insertImageHtml(html: string) {
639
+ if (!html || !editorElement) {
640
+ return;
641
+ }
642
+
643
+ restoreSelection();
644
+ const selection = window.getSelection();
645
+ if (!selection || selection.rangeCount === 0) {
646
+ return;
647
+ }
648
+
649
+ const range = selection.getRangeAt(0);
650
+ range.deleteContents();
651
+
652
+ const template = document.createElement('template');
653
+ template.innerHTML = html.trim();
654
+ const image = template.content.querySelector('img');
655
+ if (!image) {
656
+ insertHtml(html);
657
+ return;
658
+ }
659
+
660
+ const insertedImage = image.cloneNode(true) as HTMLImageElement;
661
+ const trailingBlock = createTrailingTextBlock();
662
+ insertNodesAtEditableRange(range, [insertedImage, trailingBlock]);
663
+ setCaretInsideElement(trailingBlock);
664
+ saveSelection();
665
+ emitChange();
666
+ }
667
+
668
+ export function insertImageAsset(asset: any) {
669
+ insertImageHtml(imageAssetToHtml(asset));
670
+ }
671
+
672
+ export function focusImage(index: number) {
673
+ if (!editorElement || index < 0) {
674
+ return;
675
+ }
676
+
677
+ const images = Array.from(editorElement.querySelectorAll('img'));
678
+ const target = images[index];
679
+ for (const image of images) {
680
+ image.removeAttribute('data-smrt-selected');
681
+ }
682
+
683
+ if (!target) {
684
+ return;
685
+ }
686
+
687
+ target.setAttribute('data-smrt-selected', 'true');
688
+ selectedImageIndexState = index;
689
+ target.scrollIntoView?.({ block: 'center', inline: 'nearest' });
690
+
691
+ placeCaretAfterImage(target);
692
+ refreshSelectedImageChrome();
693
+ }
694
+
695
+ function applyImagePlacement(placement: ContentBodyImagePlacement) {
696
+ const image = getSelectedImage();
697
+ if (!image) {
698
+ return;
699
+ }
700
+
701
+ const frame = getImageFrame(image);
702
+ frame.setAttribute('data-smrt-inline-image', 'true');
703
+ frame.setAttribute('data-smrt-placement', placement);
704
+
705
+ if (placement === 'full') {
706
+ frame.removeAttribute('data-smrt-width');
707
+ frame.style.width = '100%';
708
+ frame.style.maxWidth = '100%';
709
+ } else if (placement === 'left' || placement === 'right') {
710
+ const wrappedWidth = Math.min(360, Math.round(getMaxImageWidth() * 0.45));
711
+ setImageWidth(frame, Math.min(getImageWidth(frame), wrappedWidth));
712
+ } else if (frame.style.width === '100%') {
713
+ frame.style.removeProperty('width');
714
+ }
715
+
716
+ selectImageElement(image, { notify: false });
717
+ emitChange();
718
+ }
719
+
720
+ function resizeSelectedImage(delta: number) {
721
+ const image = getSelectedImage();
722
+ if (!image) {
723
+ return;
724
+ }
725
+
726
+ const frame = getImageFrame(image);
727
+ setImageWidth(frame, getImageWidth(frame) + delta);
728
+ selectImageElement(image, { notify: false });
729
+ emitChange();
730
+ }
731
+
732
+ function removeSelectedImage() {
733
+ const image = getSelectedImage();
734
+ if (!image) {
735
+ return;
736
+ }
737
+
738
+ const frame = getImageFrame(image);
739
+ frame.remove();
740
+ clearImageSelection();
741
+ emitChange();
742
+ }
743
+
744
+ function useSelectedImageAsThumbnail() {
745
+ if (selectedImageAssetId) {
746
+ onUseImageAsThumbnail?.(selectedImageAssetId);
747
+ }
748
+ }
749
+
750
+ function handleResizePointerMove(event: PointerEvent) {
751
+ if (!resizeState) {
752
+ return;
753
+ }
754
+
755
+ const delta = event.clientX - resizeState.startX;
756
+ setImageWidth(resizeState.frame, resizeState.startWidth + delta);
757
+ refreshSelectedImageChrome();
758
+ }
759
+
760
+ function handleResizePointerUp() {
761
+ if (!resizeState) {
762
+ return;
763
+ }
764
+
765
+ resizeState.frame.removeAttribute('data-smrt-resizing');
766
+ resizeState = null;
767
+ window.removeEventListener('pointermove', handleResizePointerMove);
768
+ window.removeEventListener('pointerup', handleResizePointerUp);
769
+ emitChange();
770
+ }
771
+
772
+ function startImageResize(event: PointerEvent) {
773
+ event.preventDefault();
774
+ event.stopPropagation();
775
+
776
+ const image = getSelectedImage();
777
+ if (!image) {
778
+ return;
779
+ }
780
+
781
+ const frame = getImageFrame(image);
782
+ resizeState = {
783
+ frame,
784
+ startX: event.clientX,
785
+ startWidth: getImageWidth(frame),
786
+ };
787
+ frame.setAttribute('data-smrt-resizing', 'true');
788
+ window.addEventListener('pointermove', handleResizePointerMove);
789
+ window.addEventListener('pointerup', handleResizePointerUp);
790
+ }
791
+
792
+ function moveImageToRange(
793
+ imageIndex: number,
794
+ range: Range | null,
795
+ clientX?: number,
796
+ ) {
797
+ if (!editorElement || !range) {
798
+ return;
799
+ }
800
+
801
+ const image = getEditorImages()[imageIndex];
802
+ if (!image) {
803
+ return;
804
+ }
805
+
806
+ const frame = getImageFrame(image);
807
+ const layoutRoot = getImageLayoutRoot(image);
808
+ if (layoutRoot.contains(range.commonAncestorContainer)) {
809
+ return;
810
+ }
811
+
812
+ const nextPlacement =
813
+ typeof clientX === 'number' ? placementFromPoint(clientX) : null;
814
+
815
+ layoutRoot.remove();
816
+ insertNodesAtEditableRange(range, [layoutRoot]);
817
+ range.setStartAfter(layoutRoot);
818
+ range.collapse(true);
819
+ savedRange = range.cloneRange();
820
+
821
+ if (nextPlacement) {
822
+ frame.setAttribute('data-smrt-placement', nextPlacement);
823
+ }
824
+
825
+ selectImageElement(image);
826
+ emitChange();
827
+ }
828
+
829
+ function handleMovePointerUp(event: PointerEvent) {
830
+ if (!moveState) {
831
+ return;
832
+ }
833
+
834
+ moveState.frame.removeAttribute('data-smrt-moving');
835
+ const imageIndex = moveState.imageIndex;
836
+ moveState = null;
837
+ window.removeEventListener('pointerup', handleMovePointerUp);
838
+ moveImageToRange(
839
+ imageIndex,
840
+ getRangeFromPoint(event.clientX, event.clientY),
841
+ event.clientX,
842
+ );
843
+ }
844
+
845
+ function startImageMove(event: PointerEvent) {
846
+ event.preventDefault();
847
+ event.stopPropagation();
848
+
849
+ const image = getSelectedImage();
850
+ if (!image || selectedImageIndexState < 0) {
851
+ return;
852
+ }
853
+
854
+ const frame = getImageFrame(image);
855
+ frame.setAttribute('data-smrt-moving', 'true');
856
+ moveState = {
857
+ imageIndex: selectedImageIndexState,
858
+ frame,
859
+ };
860
+ window.addEventListener('pointerup', handleMovePointerUp);
861
+ }
862
+
863
+ async function resolveAndInsertImage(selected: ImageLike | File | string) {
864
+ const asset = onResolveImage ? await onResolveImage(selected) : selected;
865
+ if (asset) {
866
+ insertImageAsset(asset);
867
+ }
868
+ }
869
+
870
+ function parseDraggedImage(dataTransfer: DataTransfer): ImageLike | null {
871
+ const payload = dataTransfer.getData('application/x-smrt-image');
872
+ if (!payload) {
873
+ return null;
874
+ }
875
+
876
+ try {
877
+ return JSON.parse(payload) as ImageLike;
878
+ } catch {
879
+ return null;
880
+ }
881
+ }
882
+
883
+ function handleDragOver(event: DragEvent) {
884
+ event.preventDefault();
885
+ isDragging = true;
886
+ if (event.dataTransfer) {
887
+ event.dataTransfer.dropEffect = movingImageIndex === null ? 'copy' : 'move';
888
+ }
889
+ }
890
+
891
+ function handleDragLeave(event: DragEvent) {
892
+ const relatedTarget = event.relatedTarget as Node | null;
893
+ const currentTarget = event.currentTarget as Node;
894
+ if (relatedTarget && currentTarget.contains(relatedTarget)) {
895
+ return;
896
+ }
897
+
898
+ isDragging = false;
899
+ }
900
+
901
+ async function handleDrop(event: DragEvent) {
902
+ event.preventDefault();
903
+ isDragging = false;
904
+ if (!event.dataTransfer) {
905
+ return;
906
+ }
907
+
908
+ const dropRange = getRangeFromPoint(event.clientX, event.clientY);
909
+ const bodyImageIndex = event.dataTransfer.getData(
910
+ 'application/x-smrt-body-image-index',
911
+ );
912
+ const parsedBodyImageIndex =
913
+ bodyImageIndex === '' ? movingImageIndex : Number(bodyImageIndex);
914
+ if (
915
+ typeof parsedBodyImageIndex === 'number' &&
916
+ Number.isInteger(parsedBodyImageIndex) &&
917
+ parsedBodyImageIndex >= 0
918
+ ) {
919
+ moveImageToRange(parsedBodyImageIndex, dropRange, event.clientX);
920
+ movingImageIndex = null;
921
+ return;
922
+ }
923
+
924
+ const draggedImage = parseDraggedImage(event.dataTransfer);
925
+ if (draggedImage) {
926
+ if (dropRange) {
927
+ savedRange = dropRange.cloneRange();
928
+ }
929
+ await resolveAndInsertImage(draggedImage);
930
+ return;
931
+ }
932
+
933
+ const imageFiles = Array.from(event.dataTransfer.files).filter((file) =>
934
+ file.type.startsWith('image/'),
935
+ );
936
+ if (imageFiles.length > 0) {
937
+ if (dropRange) {
938
+ savedRange = dropRange.cloneRange();
939
+ }
940
+ for (const file of imageFiles) {
941
+ await resolveAndInsertImage(file);
942
+ }
943
+ return;
944
+ }
945
+
946
+ const url =
947
+ event.dataTransfer.getData('text/uri-list') ||
948
+ event.dataTransfer.getData('text/plain');
949
+ if (url && /^https?:\/\//i.test(url.trim())) {
950
+ if (dropRange) {
951
+ savedRange = dropRange.cloneRange();
952
+ }
953
+ await resolveAndInsertImage(url.trim());
954
+ }
955
+ }
956
+
957
+ function handleSurfaceClick(event: MouseEvent) {
958
+ const target = event.target as Element | null;
959
+ const image = target?.closest('img') as HTMLImageElement | null;
960
+ if (image && editorElement?.contains(image)) {
961
+ event.preventDefault();
962
+ selectImageElement(image);
963
+ return;
964
+ }
965
+
966
+ clearImageSelection();
967
+ saveSelection();
968
+ }
969
+
970
+ function handleEditorDragStart(event: DragEvent) {
971
+ const target = event.target as Element | null;
972
+ const image =
973
+ (target?.closest('img') as HTMLImageElement | null) ||
974
+ (target
975
+ ?.closest('figure[data-smrt-inline-image]')
976
+ ?.querySelector('img') as HTMLImageElement | null);
977
+ if (!image || !editorElement?.contains(image) || !event.dataTransfer) {
978
+ return;
979
+ }
980
+
981
+ selectImageElement(image);
982
+ movingImageIndex = selectedImageIndexState;
983
+ event.dataTransfer.effectAllowed = 'move';
984
+ event.dataTransfer.setData(
985
+ 'application/x-smrt-body-image-index',
986
+ String(selectedImageIndexState),
987
+ );
988
+ }
989
+
990
+ function handleEditorDragEnd() {
991
+ movingImageIndex = null;
992
+ isDragging = false;
993
+ refreshSelectedImageChrome();
994
+ }
995
+ </script>
996
+
997
+ <div bind:this={rootElement} class="content-body-editor">
998
+ <div class="body-editor-toolbar" aria-label={t(M['content.content_body_editor.toolbar'])}>
999
+ <button type="button" title={t(M['content.content_body_editor.bold'])} aria-label={t(M['content.content_body_editor.bold'])} onclick={() => runCommand('bold')}>
1000
+ <strong>B</strong>
1001
+ </button>
1002
+ <button type="button" title={t(M['content.content_body_editor.italic'])} aria-label={t(M['content.content_body_editor.italic'])} onclick={() => runCommand('italic')}>
1003
+ <em>I</em>
1004
+ </button>
1005
+ <button type="button" title={t(M['content.content_body_editor.heading'])} aria-label={t(M['content.content_body_editor.heading'])} onclick={() => runCommand('formatBlock', 'h2')}>
1006
+ H2
1007
+ </button>
1008
+ <button type="button" title={t(M['content.content_body_editor.bulleted_list'])} aria-label={t(M['content.content_body_editor.bulleted_list'])} onclick={() => runCommand('insertUnorderedList')}>
1009
+ <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1010
+ <line x1="9" y1="6" x2="21" y2="6"></line>
1011
+ <line x1="9" y1="12" x2="21" y2="12"></line>
1012
+ <line x1="9" y1="18" x2="21" y2="18"></line>
1013
+ <circle cx="4" cy="6" r="1"></circle>
1014
+ <circle cx="4" cy="12" r="1"></circle>
1015
+ <circle cx="4" cy="18" r="1"></circle>
1016
+ </svg>
1017
+ </button>
1018
+ <button type="button" title={t(M['content.content_body_editor.insert_image'])} aria-label={t(M['content.content_body_editor.insert_image'])} onclick={() => onOpenImageChooser?.()}>
1019
+ <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1020
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
1021
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
1022
+ <polyline points="21 15 16 10 5 21"></polyline>
1023
+ </svg>
1024
+ </button>
1025
+
1026
+ <label class="format-select">
1027
+ <span>{t(M['content.content_body_editor.save_as'])}</span>
1028
+ <select
1029
+ value={currentFormat}
1030
+ onchange={(event) => setBodyFormat((event.currentTarget as HTMLSelectElement).value as ContentBodyFormat)}
1031
+ >
1032
+ <option value="html">HTML</option>
1033
+ <option value="markdown">Markdown</option>
1034
+ </select>
1035
+ </label>
1036
+ </div>
1037
+
1038
+ {#if selectedImageBox}
1039
+ <div
1040
+ class="image-control-popover"
1041
+ style={`top: ${Math.max(44, selectedImageBox.top + 8)}px; left: ${selectedImageBox.left + selectedImageBox.width / 2}px;`}
1042
+ aria-label={t(M['content.content_body_editor.selected_image_controls'])}
1043
+ >
1044
+ <button type="button" title={t(M['content.content_body_editor.move_image'])} aria-label={t(M['content.content_body_editor.move_image'])} onpointerdown={startImageMove}>
1045
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1046
+ <path d="M12 2v20"></path>
1047
+ <path d="M2 12h20"></path>
1048
+ <path d="m5 9-3 3 3 3"></path>
1049
+ <path d="m19 9 3 3-3 3"></path>
1050
+ <path d="m9 5 3-3 3 3"></path>
1051
+ <path d="m9 19 3 3 3-3"></path>
1052
+ </svg>
1053
+ </button>
1054
+ <span class="image-control-divider"></span>
1055
+ <button
1056
+ type="button"
1057
+ title={t(M['content.content_body_editor.wrap_text_on_right'])}
1058
+ aria-label={t(M['content.content_body_editor.wrap_text_on_right'])}
1059
+ class:active={selectedImagePlacement === 'left'}
1060
+ onclick={() => applyImagePlacement('left')}
1061
+ >
1062
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1063
+ <rect x="3" y="5" width="8" height="8" rx="1"></rect>
1064
+ <path d="M14 6h7"></path>
1065
+ <path d="M14 10h7"></path>
1066
+ <path d="M3 17h18"></path>
1067
+ <path d="M3 21h14"></path>
1068
+ </svg>
1069
+ </button>
1070
+ <button
1071
+ type="button"
1072
+ title={t(M['content.content_body_editor.center_image'])}
1073
+ aria-label={t(M['content.content_body_editor.center_image'])}
1074
+ class:active={selectedImagePlacement === 'center' || selectedImagePlacement === 'block'}
1075
+ onclick={() => applyImagePlacement('center')}
1076
+ >
1077
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1078
+ <path d="M4 6h16"></path>
1079
+ <rect x="7" y="9" width="10" height="6" rx="1"></rect>
1080
+ <path d="M4 18h16"></path>
1081
+ </svg>
1082
+ </button>
1083
+ <button
1084
+ type="button"
1085
+ title={t(M['content.content_body_editor.wrap_text_on_left'])}
1086
+ aria-label={t(M['content.content_body_editor.wrap_text_on_left'])}
1087
+ class:active={selectedImagePlacement === 'right'}
1088
+ onclick={() => applyImagePlacement('right')}
1089
+ >
1090
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1091
+ <rect x="13" y="5" width="8" height="8" rx="1"></rect>
1092
+ <path d="M3 6h7"></path>
1093
+ <path d="M3 10h7"></path>
1094
+ <path d="M3 17h18"></path>
1095
+ <path d="M7 21h14"></path>
1096
+ </svg>
1097
+ </button>
1098
+ <button
1099
+ type="button"
1100
+ title={t(M['content.content_body_editor.full_width'])}
1101
+ aria-label={t(M['content.content_body_editor.full_width'])}
1102
+ class:active={selectedImagePlacement === 'full'}
1103
+ onclick={() => applyImagePlacement('full')}
1104
+ >
1105
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1106
+ <path d="M3 5h18"></path>
1107
+ <rect x="4" y="8" width="16" height="8" rx="1"></rect>
1108
+ <path d="M3 19h18"></path>
1109
+ </svg>
1110
+ </button>
1111
+ <span class="image-control-divider"></span>
1112
+ <button type="button" title={t(M['content.content_body_editor.make_smaller'])} aria-label={t(M['content.content_body_editor.make_image_smaller'])} onclick={() => resizeSelectedImage(-IMAGE_WIDTH_STEP)}>
1113
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1114
+ <path d="M5 12h14"></path>
1115
+ <path d="M9 8 5 12l4 4"></path>
1116
+ <path d="m15 8 4 4-4 4"></path>
1117
+ </svg>
1118
+ </button>
1119
+ <button type="button" title={t(M['content.content_body_editor.make_larger'])} aria-label={t(M['content.content_body_editor.make_image_larger'])} onclick={() => resizeSelectedImage(IMAGE_WIDTH_STEP)}>
1120
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1121
+ <path d="M3 12h18"></path>
1122
+ <path d="m7 8-4 4 4 4"></path>
1123
+ <path d="m17 8 4 4-4 4"></path>
1124
+ </svg>
1125
+ </button>
1126
+ {#if selectedImageAssetId && onUseImageAsThumbnail}
1127
+ <button type="button" title={t(M['content.content_body_editor.use_as_primary_image'])} aria-label={t(M['content.content_body_editor.use_as_primary_image'])} onclick={useSelectedImageAsThumbnail}>
1128
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round">
1129
+ <path d="m12 3 2.8 5.7 6.2.9-4.5 4.4 1.1 6.2L12 17.3 6.4 20.2 7.5 14 3 9.6l6.2-.9L12 3Z"></path>
1130
+ </svg>
1131
+ </button>
1132
+ {/if}
1133
+ <button type="button" title={t(M['content.content_body_editor.remove_image'])} aria-label={t(M['content.content_body_editor.remove_image'])} onclick={removeSelectedImage}>
1134
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1135
+ <path d="M3 6h18"></path>
1136
+ <path d="M8 6V4h8v2"></path>
1137
+ <path d="M19 6l-1 14H6L5 6"></path>
1138
+ </svg>
1139
+ </button>
1140
+ </div>
1141
+
1142
+ <button
1143
+ type="button"
1144
+ class="image-resize-handle"
1145
+ title={t(M['content.content_body_editor.resize_image'])}
1146
+ aria-label={t(M['content.content_body_editor.resize_selected_image'])}
1147
+ style={`top: ${selectedImageBox.top + selectedImageBox.height - 12}px; left: ${selectedImageBox.left + selectedImageBox.width - 12}px;`}
1148
+ onpointerdown={startImageResize}
1149
+ >
1150
+ <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
1151
+ <path d="M8 20h12V8"></path>
1152
+ <path d="M12 20l8-8"></path>
1153
+ </svg>
1154
+ </button>
1155
+ {/if}
1156
+
1157
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
1158
+ <div
1159
+ bind:this={editorElement}
1160
+ id="content-body-input"
1161
+ class="body-editor-surface"
1162
+ class:body-editor-surface--dragging={isDragging}
1163
+ contenteditable="true"
1164
+ role="textbox"
1165
+ aria-multiline="true"
1166
+ tabindex="0"
1167
+ data-placeholder={placeholder}
1168
+ oninput={scheduleInputChange}
1169
+ onblur={() => {
1170
+ isFocused = false;
1171
+ saveSelection();
1172
+ emitChange({ syncDom: true });
1173
+ }}
1174
+ onfocus={() => {
1175
+ isFocused = true;
1176
+ saveSelection();
1177
+ }}
1178
+ onclick={handleSurfaceClick}
1179
+ onkeyup={saveSelection}
1180
+ ondragover={handleDragOver}
1181
+ ondragleave={handleDragLeave}
1182
+ ondragstart={handleEditorDragStart}
1183
+ ondragend={handleEditorDragEnd}
1184
+ ondrop={(event) => void handleDrop(event)}
1185
+ >
1186
+ {@html editorHtml}
1187
+ </div>
1188
+ </div>
1189
+
1190
+ <style>
1191
+ .content-body-editor {
1192
+ position: relative;
1193
+ display: flex;
1194
+ flex-direction: column;
1195
+ border: 1px solid var(--smrt-color-outline-variant);
1196
+ border-radius: 0.85rem;
1197
+ background: var(--smrt-color-surface);
1198
+ overflow: visible;
1199
+ }
1200
+
1201
+ .body-editor-toolbar {
1202
+ display: flex;
1203
+ align-items: center;
1204
+ gap: 0.35rem;
1205
+ padding: 0.45rem 0.55rem;
1206
+ border-bottom: 1px solid var(--smrt-color-outline-variant);
1207
+ background: var(--smrt-color-surface-container-low, var(--smrt-color-surface-container));
1208
+ }
1209
+
1210
+ .body-editor-toolbar button {
1211
+ width: 2rem;
1212
+ height: 2rem;
1213
+ display: inline-grid;
1214
+ place-items: center;
1215
+ border: 1px solid transparent;
1216
+ border-radius: 0.45rem;
1217
+ background: transparent;
1218
+ color: var(--smrt-color-on-surface);
1219
+ cursor: pointer;
1220
+ }
1221
+
1222
+ .body-editor-toolbar button:hover {
1223
+ border-color: var(--smrt-color-outline-variant);
1224
+ background: var(--smrt-color-surface-container);
1225
+ }
1226
+
1227
+ .format-select {
1228
+ margin-left: auto;
1229
+ display: inline-flex;
1230
+ align-items: center;
1231
+ gap: 0.4rem;
1232
+ font-size: var(--smrt-typography-label-medium-size, 0.78rem);
1233
+ color: var(--smrt-color-on-surface-variant);
1234
+ }
1235
+
1236
+ .format-select select {
1237
+ min-height: 2rem;
1238
+ border: 1px solid var(--smrt-color-outline-variant);
1239
+ border-radius: 0.45rem;
1240
+ background: var(--smrt-color-surface);
1241
+ color: var(--smrt-color-on-surface);
1242
+ padding: 0 0.5rem;
1243
+ }
1244
+
1245
+ .body-editor-surface {
1246
+ min-height: 22rem;
1247
+ padding: clamp(1rem, 2vw, 1.5rem);
1248
+ outline: none;
1249
+ color: var(--smrt-color-on-surface);
1250
+ font-size: var(--smrt-typography-body-large-size, 1.05rem);
1251
+ line-height: var(--smrt-typography-body-large-line-height, 1.65);
1252
+ overflow: auto;
1253
+ overflow-wrap: anywhere;
1254
+ }
1255
+
1256
+ .body-editor-surface::after {
1257
+ content: '';
1258
+ display: block;
1259
+ clear: both;
1260
+ }
1261
+
1262
+ .body-editor-surface:empty::before {
1263
+ content: attr(data-placeholder);
1264
+ color: var(--smrt-color-on-surface-variant);
1265
+ pointer-events: none;
1266
+ }
1267
+
1268
+ .body-editor-surface--dragging {
1269
+ box-shadow: inset 0 0 0 2px var(--smrt-color-primary);
1270
+ background: color-mix(in srgb, var(--smrt-color-primary) 6%, var(--smrt-color-surface));
1271
+ }
1272
+
1273
+ .body-editor-surface :global(p:first-child),
1274
+ .body-editor-surface :global(h1:first-child),
1275
+ .body-editor-surface :global(h2:first-child),
1276
+ .body-editor-surface :global(h3:first-child) {
1277
+ margin-top: 0;
1278
+ }
1279
+
1280
+ .body-editor-surface :global(p:last-child) {
1281
+ margin-bottom: 0;
1282
+ }
1283
+
1284
+ .body-editor-surface :global(p) {
1285
+ min-height: 1.65em;
1286
+ }
1287
+
1288
+ .body-editor-surface :global(h1),
1289
+ .body-editor-surface :global(h2),
1290
+ .body-editor-surface :global(h3) {
1291
+ line-height: 1.2;
1292
+ margin: 1.25em 0 0.5em;
1293
+ }
1294
+
1295
+ .body-editor-surface :global(figure[data-smrt-inline-image]) {
1296
+ display: block;
1297
+ width: min(100%, 32rem);
1298
+ max-width: 100%;
1299
+ margin: 1.25rem 0;
1300
+ clear: none;
1301
+ }
1302
+
1303
+ .body-editor-surface :global(figure[data-smrt-inline-image] img) {
1304
+ display: block;
1305
+ width: 100%;
1306
+ max-width: 100%;
1307
+ }
1308
+
1309
+ .body-editor-surface :global(figure[data-smrt-placement='left']),
1310
+ .body-editor-surface :global(img[data-smrt-placement='left']) {
1311
+ float: left;
1312
+ width: min(45%, 22rem);
1313
+ margin: 0.4rem 1.25rem 0.75rem 0;
1314
+ }
1315
+
1316
+ .body-editor-surface :global(figure[data-smrt-placement='right']),
1317
+ .body-editor-surface :global(img[data-smrt-placement='right']) {
1318
+ float: right;
1319
+ width: min(45%, 22rem);
1320
+ margin: 0.4rem 0 0.75rem 1.25rem;
1321
+ }
1322
+
1323
+ .body-editor-surface :global(figure[data-smrt-placement='center']),
1324
+ .body-editor-surface :global(figure[data-smrt-placement='block']),
1325
+ .body-editor-surface :global(img[data-smrt-placement='center']),
1326
+ .body-editor-surface :global(img[data-smrt-placement='block']) {
1327
+ float: none;
1328
+ display: block;
1329
+ clear: both;
1330
+ margin-left: auto;
1331
+ margin-right: auto;
1332
+ }
1333
+
1334
+ .body-editor-surface :global(figure[data-smrt-placement='full']),
1335
+ .body-editor-surface :global(img[data-smrt-placement='full']) {
1336
+ float: none;
1337
+ width: 100% !important;
1338
+ max-width: 100%;
1339
+ margin-left: 0;
1340
+ margin-right: 0;
1341
+ clear: both;
1342
+ }
1343
+
1344
+ .body-editor-surface :global(img) {
1345
+ display: block;
1346
+ max-width: min(100%, 44rem);
1347
+ height: auto;
1348
+ border-radius: 0.55rem;
1349
+ }
1350
+
1351
+ .body-editor-surface :global(img[data-smrt-inline-image]) {
1352
+ width: min(100%, 32rem);
1353
+ max-width: 100%;
1354
+ margin-top: 1.25rem;
1355
+ margin-bottom: 1.25rem;
1356
+ clear: both;
1357
+ }
1358
+
1359
+ .body-editor-surface :global(img[data-smrt-inline-image][data-smrt-placement='left']),
1360
+ .body-editor-surface :global(img[data-smrt-inline-image][data-smrt-placement='right']) {
1361
+ width: min(45%, 22rem);
1362
+ clear: none;
1363
+ }
1364
+
1365
+ .body-editor-surface :global(figure[data-smrt-inline-image] img) {
1366
+ width: 100%;
1367
+ max-width: 100%;
1368
+ margin: 0;
1369
+ }
1370
+
1371
+ .body-editor-surface :global(img[data-smrt-placement='full']) {
1372
+ width: 100%;
1373
+ max-width: 100%;
1374
+ }
1375
+
1376
+ .body-editor-surface :global(img[data-smrt-selected='true']) {
1377
+ outline: 3px solid var(--smrt-color-primary);
1378
+ outline-offset: 3px;
1379
+ }
1380
+
1381
+ .body-editor-surface :global([data-smrt-moving='true']),
1382
+ .body-editor-surface :global([data-smrt-resizing='true']) {
1383
+ opacity: 0.78;
1384
+ }
1385
+
1386
+ .image-control-popover {
1387
+ position: absolute;
1388
+ z-index: 20;
1389
+ display: inline-flex;
1390
+ align-items: center;
1391
+ gap: 0.1rem;
1392
+ padding: 0.25rem;
1393
+ border: 1px solid var(--smrt-color-outline-variant);
1394
+ border-radius: var(--smrt-radius-full, 9999px);
1395
+ background: color-mix(in srgb, var(--smrt-color-surface) 96%, transparent);
1396
+ color: var(--smrt-color-on-surface);
1397
+ box-shadow: var(--smrt-elevation-5, 0 0.75rem 1.75rem color-mix(in srgb, var(--smrt-color-shadow) 18%, transparent));
1398
+ transform: translateX(-50%);
1399
+ backdrop-filter: blur(10px);
1400
+ }
1401
+
1402
+ .image-control-popover button,
1403
+ .image-resize-handle {
1404
+ display: inline-grid;
1405
+ place-items: center;
1406
+ border: 0;
1407
+ background: transparent;
1408
+ color: inherit;
1409
+ cursor: pointer;
1410
+ }
1411
+
1412
+ .image-control-popover button {
1413
+ width: 1.85rem;
1414
+ height: 1.85rem;
1415
+ border-radius: var(--smrt-radius-full, 9999px);
1416
+ }
1417
+
1418
+ .image-control-popover button:hover,
1419
+ .image-control-popover button.active {
1420
+ background: var(--smrt-color-surface-container);
1421
+ color: var(--smrt-color-primary);
1422
+ }
1423
+
1424
+ .image-control-divider {
1425
+ width: 1px;
1426
+ height: 1.15rem;
1427
+ margin: 0 0.15rem;
1428
+ background: var(--smrt-color-outline-variant);
1429
+ }
1430
+
1431
+ .image-resize-handle {
1432
+ position: absolute;
1433
+ z-index: 21;
1434
+ width: 1.45rem;
1435
+ height: 1.45rem;
1436
+ border: 1px solid var(--smrt-color-outline-variant);
1437
+ border-radius: var(--smrt-radius-full, 9999px);
1438
+ background: var(--smrt-color-surface);
1439
+ box-shadow: var(--smrt-elevation-4, 0 0.45rem 1rem color-mix(in srgb, var(--smrt-color-shadow) 18%, transparent));
1440
+ cursor: nwse-resize;
1441
+ }
1442
+
1443
+ .image-resize-handle:hover {
1444
+ color: var(--smrt-color-primary);
1445
+ }
1446
+ </style>