@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,2166 @@
1
+ <script lang="ts">
2
+ import type { ImageLike } from '@happyvertical/smrt-images/svelte';
3
+ import { ImageUploader } from '@happyvertical/smrt-images/svelte';
4
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
5
+ import { untrack } from 'svelte';
6
+ import { extractBodyImages, resolveBodyFormat } from '../../body-format';
7
+ import type {
8
+ ContentEditorAssistantActions,
9
+ ContentEditorAssistantContextChange,
10
+ ContentEditorAssistantFieldUpdateAllowList,
11
+ ContentEditorAssistantRegistration,
12
+ } from '../../content-editor-assistant';
13
+ import {
14
+ createContentEditorAssistantContext,
15
+ sanitizeContentEditorAssistantFieldUpdates,
16
+ } from '../../content-editor-assistant';
17
+ import type {
18
+ FactAuditResourceClaimData,
19
+ FactAuditStateData,
20
+ FactEvidenceStatus,
21
+ } from '../../mock-smrt-client';
22
+ import { joinApiUrl, normalizeApiBaseUrl } from '../api';
23
+ import {
24
+ type ContentEditorFormData,
25
+ getContentEditorInitialFormData,
26
+ getContentEditorSavePayload,
27
+ getContentEditorSnapshot,
28
+ normalizePublishDate,
29
+ } from '../content-editor-form';
30
+ import { M } from '../i18n.editor.js';
31
+ import ContentAgentChat from './ContentAgentChat.svelte';
32
+ import ContentBodyEditor, {
33
+ type ContentBodyEditorChange,
34
+ } from './ContentBodyEditor.svelte';
35
+ import ContentImageChooser from './ContentImageChooser.svelte';
36
+
37
+ const { t } = useI18n();
38
+
39
+ export interface Props {
40
+ apiBaseUrl?: string;
41
+ content?: any;
42
+ contentId?: string;
43
+ factAudit?: FactAuditStateData | null;
44
+ saveDisabled?: boolean;
45
+ saveNotice?: string | null;
46
+ agentChatEnabled?: boolean;
47
+ agentChatNotice?: string | null;
48
+ hideActions?: boolean;
49
+ hideChat?: boolean;
50
+ assistantFieldAllowList?: ContentEditorAssistantFieldUpdateAllowList;
51
+ onAssistantContextChange?: ContentEditorAssistantContextChange;
52
+ onChange?: (data: any) => void;
53
+ onFactAuditChange?: (state: FactAuditStateData | null) => void;
54
+ onSave: (data: any) => void;
55
+ onCancel: () => void;
56
+ }
57
+
58
+ let {
59
+ apiBaseUrl = '/api/v1',
60
+ content = undefined,
61
+ contentId = 'new',
62
+ factAudit = null,
63
+ saveDisabled = false,
64
+ saveNotice = null,
65
+ agentChatEnabled = true,
66
+ agentChatNotice = null,
67
+ hideActions = false,
68
+ hideChat = false,
69
+ assistantFieldAllowList = {},
70
+ onAssistantContextChange = undefined,
71
+ onChange = undefined,
72
+ onFactAuditChange = undefined,
73
+ onSave,
74
+ onCancel,
75
+ }: Props = $props();
76
+
77
+ let editForm = $state<HTMLFormElement | null>(null);
78
+
79
+ export function triggerSave() {
80
+ if (saveDisabled) return;
81
+ if (editForm?.requestSubmit) {
82
+ editForm.requestSubmit();
83
+ return;
84
+ }
85
+
86
+ onSave(getContentEditorSavePayload(formData));
87
+ }
88
+
89
+ let formData = $state<ContentEditorFormData>(
90
+ getContentEditorInitialFormData(undefined),
91
+ );
92
+ let lastContentKey = $state<string | undefined>(undefined);
93
+ let currentEditorState = $derived(formData.body || '');
94
+ let currentReferenceIds = $derived(formData.referenceIds || []);
95
+ const editorSnapshot = $derived(getContentEditorSnapshot(formData));
96
+ const showActions = $derived(!hideActions);
97
+ const showChatSidebar = $derived(!hideChat);
98
+ const showAgentChat = $derived(agentChatEnabled && showChatSidebar);
99
+ const agentChatContentId = $derived(content?.id ?? contentId);
100
+ const agentChatFields = $derived({
101
+ title: formData.title,
102
+ description: formData.description,
103
+ type: formData.type,
104
+ status: formData.status,
105
+ state: formData.state,
106
+ publish_date: normalizePublishDate(formData.publish_date) || '',
107
+ body: formData.body,
108
+ bodyFormat: formData.bodyFormat || '',
109
+ });
110
+ const assistantContext = $derived(
111
+ createContentEditorAssistantContext({
112
+ contentId: agentChatContentId,
113
+ title: agentChatFields.title,
114
+ editorKind: 'content',
115
+ fields: agentChatFields,
116
+ currentEditorState,
117
+ referenceIds: currentReferenceIds,
118
+ }),
119
+ );
120
+
121
+ // Undo stack for AI field edits — each entry stores the old values of changed fields
122
+ let fieldUndoStack = $state<Record<string, string>[]>([]);
123
+ let lastAppliedFields = $state<string[]>([]);
124
+ let showUndoBanner = $state(false);
125
+
126
+ const assistantActions: ContentEditorAssistantActions = {
127
+ triggerSave,
128
+ applyFieldUpdates,
129
+ undoLastFieldUpdate: undoLastApply,
130
+ };
131
+ const assistantRegistration = $derived({
132
+ context: assistantContext,
133
+ actions: assistantActions,
134
+ });
135
+ let activeAssistantContextCallback:
136
+ | ContentEditorAssistantContextChange
137
+ | undefined;
138
+ let lastAssistantContextCallback:
139
+ | ContentEditorAssistantContextChange
140
+ | undefined;
141
+ let lastAssistantRegistration:
142
+ | ContentEditorAssistantRegistration
143
+ | null
144
+ | undefined;
145
+
146
+ function publishAssistantRegistration(
147
+ registration: ContentEditorAssistantRegistration | null,
148
+ ) {
149
+ const callback = activeAssistantContextCallback;
150
+ if (!callback) {
151
+ return;
152
+ }
153
+
154
+ if (
155
+ callback === lastAssistantContextCallback &&
156
+ registration === lastAssistantRegistration
157
+ ) {
158
+ return;
159
+ }
160
+
161
+ callback(registration);
162
+ lastAssistantContextCallback = callback;
163
+ lastAssistantRegistration = registration;
164
+ }
165
+
166
+ // When content prop changes from outside (different item), reset formData.
167
+ $effect(() => {
168
+ const newKey = content?.id ?? contentId;
169
+ if (newKey !== lastContentKey) {
170
+ lastContentKey = newKey;
171
+ formData = getContentEditorInitialFormData(content);
172
+ fieldUndoStack = [];
173
+ showUndoBanner = false;
174
+ }
175
+ });
176
+
177
+ $effect(() => {
178
+ onChange?.(editorSnapshot);
179
+ });
180
+
181
+ $effect(() => {
182
+ const callback = onAssistantContextChange;
183
+ activeAssistantContextCallback = callback;
184
+ if (!callback) {
185
+ lastAssistantContextCallback = undefined;
186
+ lastAssistantRegistration = undefined;
187
+ return;
188
+ }
189
+
190
+ publishAssistantRegistration(untrack(() => assistantRegistration));
191
+
192
+ return () => {
193
+ if (activeAssistantContextCallback === callback) {
194
+ activeAssistantContextCallback = undefined;
195
+ }
196
+ if (lastAssistantContextCallback === callback) {
197
+ lastAssistantContextCallback = undefined;
198
+ lastAssistantRegistration = undefined;
199
+ }
200
+ callback(null);
201
+ };
202
+ });
203
+
204
+ $effect(() => {
205
+ const registration = assistantRegistration;
206
+ untrack(() => publishAssistantRegistration(registration));
207
+ });
208
+
209
+ /** Called by ContentAgentChat when AI wants to update form fields */
210
+ function applyFieldUpdates(fields: Record<string, string>) {
211
+ const safeFields = sanitizeContentEditorAssistantFieldUpdates(
212
+ fields,
213
+ assistantFieldAllowList,
214
+ );
215
+ if (Object.keys(safeFields).length === 0) {
216
+ return;
217
+ }
218
+
219
+ // Snapshot old values for undo
220
+ const oldValues: Record<string, string> = {};
221
+ for (const key of Object.keys(safeFields)) {
222
+ oldValues[key] = String(formData[key] ?? '');
223
+ formData[key] = safeFields[key];
224
+ }
225
+ fieldUndoStack = [...fieldUndoStack, oldValues];
226
+ lastAppliedFields = Object.keys(safeFields);
227
+ showUndoBanner = true;
228
+ }
229
+
230
+ function undoLastApply() {
231
+ if (fieldUndoStack.length === 0) return;
232
+ const oldValues = fieldUndoStack[fieldUndoStack.length - 1];
233
+ fieldUndoStack = fieldUndoStack.slice(0, -1);
234
+ for (const [key, val] of Object.entries(oldValues)) {
235
+ formData[key] = val;
236
+ }
237
+ lastAppliedFields = Object.keys(oldValues);
238
+ if (fieldUndoStack.length === 0) {
239
+ showUndoBanner = false;
240
+ }
241
+ }
242
+
243
+ function getReferenceLabel(reference: any): string {
244
+ return (
245
+ reference?.title ||
246
+ reference?.name ||
247
+ reference?.url ||
248
+ reference?.source ||
249
+ reference?.id ||
250
+ 'Reference'
251
+ );
252
+ }
253
+
254
+ function getReferenceUrl(reference: any): string | null {
255
+ return reference?.url || reference?.originalUrl || reference?.source || null;
256
+ }
257
+
258
+ function getResourceClaimsForReference(
259
+ reference: any,
260
+ ): FactAuditResourceClaimData[] {
261
+ const selector = getReferenceSourceSelector(reference);
262
+ const referenceId = String(reference?.id || '');
263
+ const referenceUrl = getReferenceUrl(reference) || '';
264
+ const resourceClaims: FactAuditResourceClaimData[] =
265
+ factAudit?.resourceClaims ?? [];
266
+
267
+ return resourceClaims.filter((claim: FactAuditResourceClaimData) => {
268
+ if (
269
+ selector &&
270
+ claim.sourceKind === selector.sourceKind &&
271
+ claim.sourceId === selector.sourceId
272
+ ) {
273
+ return true;
274
+ }
275
+
276
+ if (referenceId && claim.sourceId === referenceId) {
277
+ return true;
278
+ }
279
+
280
+ return Boolean(referenceUrl && claim.sourceUrl === referenceUrl);
281
+ });
282
+ }
283
+
284
+ function getAuditReferences(): any[] {
285
+ const references = [...(formData.references ?? [])];
286
+ const seen = new Set<string>();
287
+
288
+ for (const reference of references) {
289
+ const selector = getReferenceSourceSelector(reference);
290
+ const key = selector
291
+ ? `${selector.sourceKind}:${selector.sourceId}`
292
+ : `url:${getReferenceUrl(reference) || getReferenceLabel(reference)}`;
293
+ seen.add(key);
294
+ }
295
+
296
+ for (const claim of factAudit?.resourceClaims ?? []) {
297
+ const sourceKind = String(claim.sourceKind || '').trim();
298
+ const sourceId = String(claim.sourceId || '').trim();
299
+ if (!sourceKind || !sourceId) {
300
+ continue;
301
+ }
302
+
303
+ const key = `${sourceKind}:${sourceId}`;
304
+ if (seen.has(key)) {
305
+ continue;
306
+ }
307
+
308
+ seen.add(key);
309
+ references.push({
310
+ id: sourceId,
311
+ title: claim.sourceTitle || claim.sourceUrl || sourceId,
312
+ url: claim.sourceUrl || '',
313
+ source: claim.sourceUrl || '',
314
+ _auditOnly: true,
315
+ _auditSourceKind: sourceKind,
316
+ _auditSourceId: sourceId,
317
+ });
318
+ }
319
+
320
+ return references;
321
+ }
322
+
323
+ function getReferenceExpansionKey(reference: any, index: number): string {
324
+ return String(
325
+ reference?.id ||
326
+ reference?.url ||
327
+ reference?.originalUrl ||
328
+ reference?.title ||
329
+ `reference-${index}`,
330
+ );
331
+ }
332
+
333
+ function isReferenceClaimsExpanded(reference: any, index: number): boolean {
334
+ return expandedReferenceClaimKeys.includes(
335
+ getReferenceExpansionKey(reference, index),
336
+ );
337
+ }
338
+
339
+ function toggleReferenceClaims(reference: any, index: number) {
340
+ const key = getReferenceExpansionKey(reference, index);
341
+ expandedReferenceClaimKeys = expandedReferenceClaimKeys.includes(key)
342
+ ? expandedReferenceClaimKeys.filter((expandedKey) => expandedKey !== key)
343
+ : [...expandedReferenceClaimKeys, key];
344
+ }
345
+
346
+ // We need a simple way to enter reference IDs or mock selecting them
347
+ let newReferenceId = $state('');
348
+ let selectedEvidenceIds = $state<string[]>([]);
349
+ let evidenceBusy = $state<string | null>(null);
350
+ let evidenceError = $state<string | null>(null);
351
+ let evidenceNotice = $state<string | null>(null);
352
+ let bulkEvidenceStatus = $state<FactEvidenceStatus>('supports');
353
+ const evidenceStatuses: FactEvidenceStatus[] = [
354
+ 'supports',
355
+ 'contradicts',
356
+ 'unclear',
357
+ 'irrelevant',
358
+ 'invalid',
359
+ ];
360
+ const selectedEvidenceCount = $derived(selectedEvidenceIds.length);
361
+ const auditReferences = $derived(getAuditReferences());
362
+
363
+ function addReference() {
364
+ if (newReferenceId && !formData.referenceIds.includes(newReferenceId)) {
365
+ formData.referenceIds = [...formData.referenceIds, newReferenceId];
366
+ newReferenceId = '';
367
+ }
368
+ }
369
+
370
+ function getResourceClaimEvidenceIds(
371
+ claim: FactAuditResourceClaimData,
372
+ ): string[] {
373
+ return (claim.evidence ?? [])
374
+ .map((evidence) => evidence.id)
375
+ .filter((id): id is string => typeof id === 'string' && id.length > 0);
376
+ }
377
+
378
+ function getResourceClaimKey(
379
+ claim: FactAuditResourceClaimData,
380
+ index: number,
381
+ ): string {
382
+ const firstEvidence = claim.evidence?.[0];
383
+ return (
384
+ firstEvidence?.id ||
385
+ firstEvidence?.evidenceKey ||
386
+ [
387
+ claim.id,
388
+ claim.sourceKind,
389
+ claim.sourceId,
390
+ claim.quote,
391
+ claim.fact?.textRefined,
392
+ ]
393
+ .filter(
394
+ (value): value is string =>
395
+ typeof value === 'string' && value.length > 0,
396
+ )
397
+ .join(':') ||
398
+ `resource-claim-${index}`
399
+ );
400
+ }
401
+
402
+ function isResourceClaimSelected(claim: FactAuditResourceClaimData): boolean {
403
+ const evidenceIds = getResourceClaimEvidenceIds(claim);
404
+ return (
405
+ evidenceIds.length > 0 &&
406
+ evidenceIds.every((id) => selectedEvidenceIds.includes(id))
407
+ );
408
+ }
409
+
410
+ function toggleResourceClaimSelection(claim: FactAuditResourceClaimData) {
411
+ const evidenceIds = getResourceClaimEvidenceIds(claim);
412
+ if (evidenceIds.length === 0) {
413
+ return;
414
+ }
415
+
416
+ const selected = isResourceClaimSelected(claim);
417
+ selectedEvidenceIds = selected
418
+ ? selectedEvidenceIds.filter((id) => !evidenceIds.includes(id))
419
+ : [...new Set([...selectedEvidenceIds, ...evidenceIds])];
420
+ }
421
+
422
+ function getReferenceSourceSelector(reference: any): {
423
+ sourceKind: string;
424
+ sourceId: string;
425
+ } | null {
426
+ const sourceId = String(
427
+ reference?._auditSourceId || reference?.id || '',
428
+ ).trim();
429
+ const sourceKind = String(
430
+ reference?._auditSourceKind || 'content-reference',
431
+ ).trim();
432
+ return sourceId
433
+ ? {
434
+ sourceKind,
435
+ sourceId,
436
+ }
437
+ : null;
438
+ }
439
+
440
+ function getSelectedReferenceSourceSelectors(): Array<{
441
+ sourceKind: string;
442
+ sourceId: string;
443
+ }> {
444
+ const selected = new Set(selectedEvidenceIds);
445
+ const selectors: Array<{ sourceKind: string; sourceId: string }> = [];
446
+ const seen = new Set<string>();
447
+
448
+ for (const reference of getAuditReferences()) {
449
+ const selector = getReferenceSourceSelector(reference);
450
+ if (!selector) {
451
+ continue;
452
+ }
453
+
454
+ const hasSelectedEvidence = getResourceClaimsForReference(reference).some(
455
+ (claim) =>
456
+ getResourceClaimEvidenceIds(claim).some((id) => selected.has(id)),
457
+ );
458
+ const selectorKey = `${selector.sourceKind}:${selector.sourceId}`;
459
+ if (hasSelectedEvidence && !seen.has(selectorKey)) {
460
+ seen.add(selectorKey);
461
+ selectors.push(selector);
462
+ }
463
+ }
464
+
465
+ return selectors;
466
+ }
467
+
468
+ function getEvidenceStatusIcon(status: FactEvidenceStatus | null | undefined) {
469
+ switch (status) {
470
+ case 'supports':
471
+ return '✓';
472
+ case 'contradicts':
473
+ return '×';
474
+ case 'irrelevant':
475
+ return '⊘';
476
+ case 'invalid':
477
+ return '!';
478
+ default:
479
+ return '?';
480
+ }
481
+ }
482
+
483
+ async function runFactAuditAction(
484
+ path: string,
485
+ method: 'POST' | 'PUT',
486
+ body: Record<string, any>,
487
+ ): Promise<FactAuditStateData> {
488
+ if (!contentId || contentId === 'new') {
489
+ throw new Error('Save this content before updating evidence.');
490
+ }
491
+
492
+ const response = await fetch(
493
+ joinApiUrl(apiBaseUrl, `/contents/${contentId}/fact-audit/${path}`),
494
+ {
495
+ method,
496
+ headers: { 'Content-Type': 'application/json' },
497
+ body: JSON.stringify(body),
498
+ },
499
+ );
500
+ const payload = await response.json().catch(() => ({}));
501
+ if (!response.ok) {
502
+ throw new Error(payload?.message || payload?.error || response.statusText);
503
+ }
504
+
505
+ return (payload.data ?? payload.result ?? payload) as FactAuditStateData;
506
+ }
507
+
508
+ async function updateEvidenceStatus(
509
+ evidenceIds: string[],
510
+ status: FactEvidenceStatus,
511
+ ) {
512
+ if (evidenceIds.length === 0 || evidenceBusy) {
513
+ return;
514
+ }
515
+
516
+ evidenceBusy = 'status';
517
+ evidenceError = null;
518
+ evidenceNotice = null;
519
+
520
+ try {
521
+ const nextAudit = await runFactAuditAction('evidence/status', 'PUT', {
522
+ evidenceIds,
523
+ status,
524
+ });
525
+ selectedEvidenceIds = selectedEvidenceIds.filter(
526
+ (id) => !evidenceIds.includes(id),
527
+ );
528
+ evidenceNotice = `Updated ${evidenceIds.length} evidence item${evidenceIds.length === 1 ? '' : 's'}.`;
529
+ onFactAuditChange?.(nextAudit);
530
+ } catch (error: any) {
531
+ evidenceError = error.message || 'Failed to update evidence status.';
532
+ } finally {
533
+ evidenceBusy = null;
534
+ }
535
+ }
536
+
537
+ async function updateSelectedEvidenceStatus() {
538
+ await updateEvidenceStatus(selectedEvidenceIds, bulkEvidenceStatus);
539
+ }
540
+
541
+ async function repairReferenceEvidence(reference: any) {
542
+ const selector = getReferenceSourceSelector(reference);
543
+ if (!selector || evidenceBusy) {
544
+ return;
545
+ }
546
+
547
+ evidenceBusy = `repair:${selector.sourceId}`;
548
+ evidenceError = null;
549
+ evidenceNotice = null;
550
+
551
+ try {
552
+ const nextAudit = await runFactAuditAction('evidence/repair', 'POST', {
553
+ sources: [selector],
554
+ });
555
+ selectedEvidenceIds = [];
556
+ evidenceNotice = 'Reference evidence repaired.';
557
+ onFactAuditChange?.(nextAudit);
558
+ } catch (error: any) {
559
+ evidenceError = error.message || 'Failed to repair reference evidence.';
560
+ } finally {
561
+ evidenceBusy = null;
562
+ }
563
+ }
564
+
565
+ async function repairSelectedReferenceEvidence() {
566
+ const sources = getSelectedReferenceSourceSelectors();
567
+ if (sources.length === 0 || evidenceBusy) {
568
+ return;
569
+ }
570
+
571
+ evidenceBusy = 'repair-selected';
572
+ evidenceError = null;
573
+ evidenceNotice = null;
574
+
575
+ try {
576
+ const nextAudit = await runFactAuditAction('evidence/repair', 'POST', {
577
+ sources,
578
+ });
579
+ selectedEvidenceIds = [];
580
+ evidenceNotice = `Repaired ${sources.length} reference${sources.length === 1 ? '' : 's'}.`;
581
+ onFactAuditChange?.(nextAudit);
582
+ } catch (error: any) {
583
+ evidenceError = error.message || 'Failed to repair selected references.';
584
+ } finally {
585
+ evidenceBusy = null;
586
+ }
587
+ }
588
+
589
+ function removeReference(id: string) {
590
+ formData.referenceIds = formData.referenceIds.filter(
591
+ (refId: string) => refId !== id,
592
+ );
593
+ }
594
+
595
+ // AI Authoring State (Migrated to ContentAgentChat Sidebar)
596
+
597
+ let showImageUploader = $state(false);
598
+ let showInlineImageUploader = $state(false);
599
+ let expandedReferenceClaimKeys = $state<string[]>([]);
600
+ let bodyEditor = $state<any>(null);
601
+ let selectedBodyImageIndex = $state(-1);
602
+ const bodyImages = $derived(
603
+ extractBodyImages(
604
+ formData.body || '',
605
+ resolveBodyFormat(formData.bodyFormat, formData.body),
606
+ ),
607
+ );
608
+
609
+ // Drag-and-drop state
610
+ let imageDragOver = $state(false);
611
+ let refDragOver = $state(false);
612
+
613
+ function getImageRecord(payload: any) {
614
+ return payload?.data ?? payload;
615
+ }
616
+
617
+ // ---------- Image drag-and-drop ----------
618
+ function handleImageDragOver(e: DragEvent) {
619
+ e.preventDefault();
620
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
621
+ imageDragOver = true;
622
+ }
623
+
624
+ function handleImageDragLeave(e: DragEvent) {
625
+ // Only leave if we're actually leaving the drop zone
626
+ const relatedTarget = e.relatedTarget as Node | null;
627
+ const currentTarget = e.currentTarget as Node;
628
+ if (relatedTarget && currentTarget.contains(relatedTarget)) return;
629
+ imageDragOver = false;
630
+ }
631
+
632
+ function handleImageDrop(e: DragEvent) {
633
+ e.preventDefault();
634
+ imageDragOver = false;
635
+ if (!e.dataTransfer) return;
636
+
637
+ // Handle dropped files (images)
638
+ const files = Array.from(e.dataTransfer.files).filter((f) =>
639
+ f.type.startsWith('image/'),
640
+ );
641
+ for (const file of files) {
642
+ handleImageSelect(file);
643
+ }
644
+
645
+ // Handle dropped URLs
646
+ const url =
647
+ e.dataTransfer.getData('text/uri-list') ||
648
+ e.dataTransfer.getData('text/plain');
649
+ if (
650
+ !files.length &&
651
+ url &&
652
+ (url.startsWith('http://') || url.startsWith('https://'))
653
+ ) {
654
+ handleImageSelect(url);
655
+ }
656
+ }
657
+
658
+ // ---------- Reference drag-and-drop ----------
659
+ function handleRefDragOver(e: DragEvent) {
660
+ e.preventDefault();
661
+ if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
662
+ refDragOver = true;
663
+ }
664
+
665
+ function handleRefDragLeave(e: DragEvent) {
666
+ const relatedTarget = e.relatedTarget as Node | null;
667
+ const currentTarget = e.currentTarget as Node;
668
+ if (relatedTarget && currentTarget.contains(relatedTarget)) return;
669
+ refDragOver = false;
670
+ }
671
+
672
+ async function handleRefDrop(e: DragEvent) {
673
+ e.preventDefault();
674
+ refDragOver = false;
675
+ if (!e.dataTransfer) return;
676
+
677
+ // Handle dropped files — upload them as content and link as references
678
+ const files = Array.from(e.dataTransfer.files);
679
+ for (const file of files) {
680
+ try {
681
+ const resp = await fetch(joinApiUrl(apiBaseUrl, '/contents'), {
682
+ method: 'POST',
683
+ headers: { 'Content-Type': 'application/json' },
684
+ body: JSON.stringify({
685
+ name: file.name,
686
+ title: file.name,
687
+ type: 'document',
688
+ status: 'draft',
689
+ state: 'active',
690
+ source: 'upload',
691
+ fileKey: file.name,
692
+ body: `Uploaded reference placeholder for ${file.name}. Local drag-and-drop creates a reference record but does not upload the file contents.`,
693
+ metadata: {
694
+ upload: {
695
+ fileName: file.name,
696
+ mimeType: file.type || 'application/octet-stream',
697
+ size: file.size,
698
+ },
699
+ },
700
+ }),
701
+ });
702
+ if (resp.ok) {
703
+ const result = await resp.json();
704
+ const newId = result.data?.id || result.id;
705
+ if (newId && !formData.referenceIds.includes(newId)) {
706
+ formData.referenceIds = [...formData.referenceIds, newId];
707
+ }
708
+ } else {
709
+ }
710
+ } catch (err) {}
711
+ }
712
+
713
+ // Handle dropped URL or plain text (add as reference ID)
714
+ if (files.length === 0) {
715
+ const text =
716
+ e.dataTransfer.getData('text/uri-list') ||
717
+ e.dataTransfer.getData('text/plain');
718
+ if (text) {
719
+ const id = text.trim();
720
+ if (id && !formData.referenceIds.includes(id)) {
721
+ formData.referenceIds = [...formData.referenceIds, id];
722
+ }
723
+ }
724
+ }
725
+ }
726
+
727
+ function handleSubmit(e: Event) {
728
+ e.preventDefault();
729
+ if (saveDisabled) {
730
+ return;
731
+ }
732
+ onSave(getContentEditorSavePayload(formData));
733
+ }
734
+
735
+ function handleCancel() {
736
+ if (!showActions) {
737
+ return;
738
+ }
739
+
740
+ onCancel();
741
+ }
742
+
743
+ function parseTagsInput(value: string) {
744
+ formData.tags = value
745
+ .split(',')
746
+ .map((tag) => tag.trim())
747
+ .filter(Boolean);
748
+ }
749
+
750
+ function handleBodyChange(change: ContentBodyEditorChange) {
751
+ formData.body = change.body;
752
+ formData.bodyFormat = change.bodyFormat;
753
+ if (change.images.length === 0) {
754
+ selectedBodyImageIndex = -1;
755
+ } else if (selectedBodyImageIndex >= change.images.length) {
756
+ selectedBodyImageIndex = change.images.length - 1;
757
+ }
758
+ }
759
+
760
+ function focusBodyImage(index: number) {
761
+ selectedBodyImageIndex = index;
762
+ bodyEditor?.focusImage?.(index);
763
+ }
764
+
765
+ function readFileAsDataUrl(file: File): Promise<string> {
766
+ return new Promise((resolve, reject) => {
767
+ const reader = new FileReader();
768
+ reader.onload = (event) => resolve(event.target?.result as string);
769
+ reader.onerror = () => reject(reader.error);
770
+ reader.readAsDataURL(file);
771
+ });
772
+ }
773
+
774
+ async function createImageRecord(input: {
775
+ name: string;
776
+ sourceUri: string;
777
+ mimeType: string;
778
+ }): Promise<any | null> {
779
+ const resp = await fetch(joinApiUrl(apiBaseUrl, '/images'), {
780
+ method: 'POST',
781
+ headers: { 'Content-Type': 'application/json' },
782
+ body: JSON.stringify(input),
783
+ });
784
+
785
+ if (!resp.ok) {
786
+ throw new Error(await resp.text());
787
+ }
788
+
789
+ return getImageRecord(await resp.json());
790
+ }
791
+
792
+ async function resolveSelectedImage(
793
+ selected: ImageLike | File | string,
794
+ ): Promise<any | null> {
795
+ if (selected && typeof selected === 'object' && 'id' in selected) {
796
+ addSelectedAsset(selected);
797
+ return selected;
798
+ }
799
+
800
+ if (selected instanceof File) {
801
+ const asset = await createImageRecord({
802
+ name: selected.name || 'Uploaded Image',
803
+ sourceUri: await readFileAsDataUrl(selected),
804
+ mimeType: selected.type || 'image/png',
805
+ });
806
+ if (asset) {
807
+ addSelectedAsset(asset);
808
+ }
809
+ return asset;
810
+ }
811
+
812
+ if (typeof selected === 'string') {
813
+ const parsedUrl = new URL(selected);
814
+ const asset = await createImageRecord({
815
+ name: parsedUrl.pathname.split('/').pop() || 'External Image',
816
+ sourceUri: selected,
817
+ mimeType: 'image/jpeg',
818
+ });
819
+ if (asset) {
820
+ addSelectedAsset(asset);
821
+ }
822
+ return asset;
823
+ }
824
+
825
+ return null;
826
+ }
827
+
828
+ function handleImageSelect(selected: ImageLike | File | string) {
829
+ void (async () => {
830
+ try {
831
+ await resolveSelectedImage(selected);
832
+ } catch (err) {
833
+ } finally {
834
+ showImageUploader = false;
835
+ }
836
+ })();
837
+ }
838
+
839
+ function getAssetImageSource(asset: any): string {
840
+ return String(asset?.sourceUri || asset?.url || asset?.src || '');
841
+ }
842
+
843
+ function handleAttachedImageDragStart(event: DragEvent, asset: any) {
844
+ if (!event.dataTransfer) {
845
+ return;
846
+ }
847
+
848
+ const source = getAssetImageSource(asset);
849
+ const payload = {
850
+ ...asset,
851
+ sourceUri: asset?.sourceUri || source,
852
+ };
853
+
854
+ event.dataTransfer.effectAllowed = 'copy';
855
+ event.dataTransfer.setData(
856
+ 'application/x-smrt-image',
857
+ JSON.stringify(payload),
858
+ );
859
+ if (source) {
860
+ event.dataTransfer.setData('text/uri-list', source);
861
+ event.dataTransfer.setData('text/plain', source);
862
+ }
863
+ }
864
+
865
+ async function handleInlineImageSelect(selected: ImageLike | File | string) {
866
+ try {
867
+ const asset = await resolveSelectedImage(selected);
868
+ if (asset) {
869
+ bodyEditor?.insertImageAsset?.(asset);
870
+ selectedBodyImageIndex = Math.max(bodyImages.length, 0);
871
+ }
872
+ } catch (err) {
873
+ } finally {
874
+ showInlineImageUploader = false;
875
+ }
876
+ }
877
+
878
+ async function resolveBodyDropImage(selected: ImageLike | File | string) {
879
+ try {
880
+ return await resolveSelectedImage(selected);
881
+ } catch (err) {
882
+ return null;
883
+ }
884
+ }
885
+
886
+ function addSelectedAsset(asset: any) {
887
+ const assetId = asset.id;
888
+ if (!formData.assetIds.includes(assetId)) {
889
+ formData.assetIds = [...formData.assetIds, assetId];
890
+ formData.assets = [...formData.assets, asset];
891
+ }
892
+ if (!formData.thumbnailAssetId) {
893
+ formData.thumbnailAssetId = assetId;
894
+ }
895
+ }
896
+
897
+ function setThumbnail(id: string) {
898
+ formData.thumbnailAssetId = id;
899
+ }
900
+
901
+ function removeAsset(id: string) {
902
+ formData.assetIds = formData.assetIds.filter((aId: string) => aId !== id);
903
+ formData.assets = formData.assets.filter((a: any) => a.id !== id);
904
+ if (formData.thumbnailAssetId === id) {
905
+ formData.thumbnailAssetId = null;
906
+ }
907
+ }
908
+ </script>
909
+
910
+ <div class="form-container">
911
+ <div class="editor-grid" class:editor-grid--with-sidebar={showChatSidebar}>
912
+ <!-- LEFT COLUMN (Document Canvas) -->
913
+ <form
914
+ bind:this={editForm}
915
+ id="content-edit-form"
916
+ class="editor-main-col"
917
+ onsubmit={handleSubmit}
918
+ >
919
+ <div class="editor-toolbar">
920
+ <div class="editor-toolbar-left">
921
+ <div class="mui-field">
922
+ <select id="type-select" bind:value={formData.type} class="mui-input">
923
+ <option value="article">Article</option>
924
+ <option value="document">Document</option>
925
+ <option value="mirror">Mirror</option>
926
+ </select>
927
+ <label for="type-select">Type</label>
928
+ </div>
929
+ <div class="mui-field">
930
+ <select id="state-select" bind:value={formData.state} class="mui-input">
931
+ <option value="active">Active</option>
932
+ <option value="highlighted">Highlighted</option>
933
+ <option value="deprecated">Deprecated</option>
934
+ </select>
935
+ <label for="state-select">State</label>
936
+ </div>
937
+ <div class="mui-field">
938
+ <select id="status-select" bind:value={formData.status} class="mui-input">
939
+ <option value="draft">Draft</option>
940
+ <option value="published">Published</option>
941
+ <option value="archived">Archived</option>
942
+ </select>
943
+ <label for="status-select">Status</label>
944
+ </div>
945
+ <div class="mui-field">
946
+ <input id="publish-date-input" type="datetime-local" bind:value={formData.publish_date} class="mui-input" />
947
+ <label for="publish-date-input">{t(M['content.content_editor.publish_date'])}</label>
948
+ </div>
949
+ </div>
950
+ {#if showActions}
951
+ <div class="editor-toolbar-right">
952
+ <button type="submit" class="save-button" disabled={saveDisabled}>{content ? 'Update Content' : 'Save Content'}</button>
953
+ <button type="button" class="cancel-button" onclick={handleCancel}>
954
+ Cancel
955
+ </button>
956
+ </div>
957
+ {/if}
958
+ </div>
959
+
960
+ {#if saveNotice}
961
+ <p class="save-notice">{saveNotice}</p>
962
+ {/if}
963
+
964
+ <input
965
+ type="text"
966
+ class="document-title-input"
967
+ bind:value={formData.title}
968
+ placeholder={t(M['content.content_editor.document_title_placeholder'])}
969
+ required
970
+ />
971
+
972
+ {#if showUndoBanner}
973
+ <div class="undo-banner">
974
+ <span class="undo-banner__text">
975
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 14l-4-4 4-4"/><path d="M5 10h11a4 4 0 0 1 0 8h-1"/></svg>
976
+ {t(M['content.content_editor.ai_updated_fields'], {
977
+ count: lastAppliedFields.length,
978
+ fieldLabel: t(
979
+ lastAppliedFields.length !== 1
980
+ ? M['content.content_editor.field_plural']
981
+ : M['content.content_editor.field_singular'],
982
+ ),
983
+ fields: lastAppliedFields.join(', '),
984
+ })}
985
+ </span>
986
+ <button type="button" class="undo-banner__btn" onclick={undoLastApply}>
987
+ Undo{fieldUndoStack.length > 1 ? ` (${fieldUndoStack.length})` : ''}
988
+ </button>
989
+ </div>
990
+ {/if}
991
+
992
+ <div class="document-body-section">
993
+ <ContentBodyEditor
994
+ bind:this={bodyEditor}
995
+ value={formData.body || ''}
996
+ format={formData.bodyFormat}
997
+ selectedImageIndex={selectedBodyImageIndex}
998
+ onChange={handleBodyChange}
999
+ onOpenImageChooser={() => showInlineImageUploader = !showInlineImageUploader}
1000
+ onSelectImage={(index) => selectedBodyImageIndex = index}
1001
+ onUseImageAsThumbnail={setThumbnail}
1002
+ onResolveImage={resolveBodyDropImage}
1003
+ />
1004
+
1005
+ <div class="document-body-media-row">
1006
+ <ContentImageChooser
1007
+ body={formData.body || ''}
1008
+ format={formData.bodyFormat}
1009
+ selectedIndex={selectedBodyImageIndex}
1010
+ onSelect={focusBodyImage}
1011
+ />
1012
+ </div>
1013
+
1014
+ {#if showInlineImageUploader}
1015
+ <div class="inline-uploader-container body-inline-uploader">
1016
+ <ImageUploader
1017
+ apiBaseUrl={normalizeApiBaseUrl(apiBaseUrl)}
1018
+ allowedTabs={['gallery', 'upload', 'external']}
1019
+ enableDragToEditor={true}
1020
+ onSelect={(selected) => void handleInlineImageSelect(selected)}
1021
+ onCancel={() => showInlineImageUploader = false}
1022
+ />
1023
+ </div>
1024
+ {/if}
1025
+ </div>
1026
+
1027
+ <!-- Images & Media -->
1028
+ <details class="editor-drawer" open>
1029
+ <summary class="editor-drawer-header">
1030
+ Images & Media
1031
+ <svg class="drawer-icon" viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
1032
+ </summary>
1033
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
1034
+ <div
1035
+ class="editor-drawer-content drop-zone"
1036
+ class:drop-zone-active={imageDragOver}
1037
+ ondragover={handleImageDragOver}
1038
+ ondragleave={handleImageDragLeave}
1039
+ ondrop={handleImageDrop}
1040
+ >
1041
+ {#if imageDragOver}
1042
+ <div class="drop-overlay">
1043
+ <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
1044
+ </div>
1045
+ {/if}
1046
+ <div class="media-gallery">
1047
+ {#if formData.assets && formData.assets.length > 0}
1048
+ <div class="media-grid">
1049
+ {#each formData.assets as asset, index (asset.id || `asset-${index}`)}
1050
+ {@const assetId = typeof asset.id === 'string' ? asset.id : ''}
1051
+ <div
1052
+ class="media-item"
1053
+ class:is-thumbnail={assetId === formData.thumbnailAssetId}
1054
+ draggable="true"
1055
+ title={t(M['content.content_editor.drag_into_body'])}
1056
+ ondragstart={(event) => handleAttachedImageDragStart(event, asset)}
1057
+ >
1058
+ <img class="media-item-image" src={getAssetImageSource(asset)} alt={asset.name || 'Asset image'} />
1059
+ <div class="media-item-overlay">
1060
+ {#if assetId && assetId !== formData.thumbnailAssetId}
1061
+ <button type="button" class="btn-make-thumbnail" title={t(M['content.content_editor.make_thumbnail'])} onclick={() => setThumbnail(assetId)}>
1062
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
1063
+ </button>
1064
+ {/if}
1065
+ {#if assetId}
1066
+ <button type="button" class="btn-remove-asset" title={t(M['content.content_editor.remove'])} onclick={() => removeAsset(assetId)}>
1067
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
1068
+ </button>
1069
+ {/if}
1070
+ </div>
1071
+ {#if assetId === formData.thumbnailAssetId}
1072
+ <div class="thumbnail-badge">Thumbnail</div>
1073
+ {/if}
1074
+ </div>
1075
+ {/each}
1076
+ </div>
1077
+ {:else}
1078
+ <p class="no-media-text">{t(M['content.content_editor.no_images_attached'])}</p>
1079
+ {/if}
1080
+ {#if !showImageUploader}
1081
+ <button type="button" class="add-image-btn" onclick={() => showImageUploader = true} style="margin-top: 1rem;">
1082
+ <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
1083
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
1084
+ <circle cx="8.5" cy="8.5" r="1.5"></circle>
1085
+ <polyline points="21 15 16 10 5 21"></polyline>
1086
+ </svg>
1087
+ {t(M['content.content_editor.add_image'])}
1088
+ </button>
1089
+ {/if}
1090
+
1091
+ {#if showImageUploader}
1092
+ <div class="inline-uploader-container">
1093
+ <ImageUploader
1094
+ apiBaseUrl={normalizeApiBaseUrl(apiBaseUrl)}
1095
+ allowedTabs={['gallery', 'upload', 'external']}
1096
+ onSelect={handleImageSelect}
1097
+ onCancel={() => showImageUploader = false}
1098
+ />
1099
+ </div>
1100
+ {/if}
1101
+ </div>
1102
+ </div>
1103
+ </details>
1104
+
1105
+ <!-- Metadata Panel -->
1106
+ <details class="editor-drawer" open>
1107
+ <summary class="editor-drawer-header">
1108
+ Metadata
1109
+ <svg class="drawer-icon" viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
1110
+ </summary>
1111
+ <div class="editor-drawer-content">
1112
+ <label>
1113
+ Author:
1114
+ <input type="text" bind:value={formData.author} placeholder={t(M['content.content_editor.author_name_placeholder'])} />
1115
+ </label>
1116
+ <label>
1117
+ Description:
1118
+ <textarea bind:value={formData.description} rows="2" placeholder={t(M['content.content_editor.brief_summary_placeholder'])}></textarea>
1119
+ </label>
1120
+ <label>
1121
+ {t(M['content.content_editor.tags_comma_separated'])}
1122
+ <input
1123
+ type="text"
1124
+ value={(formData.tags || []).join(', ')}
1125
+ placeholder={t(M['content.content_editor.tags_placeholder'])}
1126
+ oninput={(event) => parseTagsInput((event.currentTarget as HTMLInputElement).value)}
1127
+ />
1128
+ </label>
1129
+ </div>
1130
+ </details>
1131
+
1132
+ <!-- References Panel -->
1133
+ <details class="editor-drawer" open>
1134
+ <summary class="editor-drawer-header">
1135
+ References
1136
+ <svg class="drawer-icon" viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
1137
+ </summary>
1138
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
1139
+ <div
1140
+ class="editor-drawer-content drop-zone"
1141
+ class:drop-zone-active={refDragOver}
1142
+ ondragover={handleRefDragOver}
1143
+ ondragleave={handleRefDragLeave}
1144
+ ondrop={handleRefDrop}
1145
+ >
1146
+ {#if refDragOver}
1147
+ <div class="drop-overlay">
1148
+ <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
1149
+ </div>
1150
+ {/if}
1151
+ <div class="references-section">
1152
+ <div class="references-list">
1153
+ {#each formData.referenceIds as refId}
1154
+ <div class="reference-badge">
1155
+ <span class="ref-id">{refId}</span>
1156
+ <button type="button" class="remove-ref-btn" onclick={() => removeReference(refId)}>×</button>
1157
+ </div>
1158
+ {/each}
1159
+ {#if formData.referenceIds.length === 0 && auditReferences.length === 0}
1160
+ <span class="no-refs">{t(M['content.content_editor.no_references'])}</span>
1161
+ {/if}
1162
+ </div>
1163
+ {#if auditReferences.length > 0}
1164
+ <div class="reference-detail-list">
1165
+ {#if evidenceError}
1166
+ <p class="evidence-message evidence-message--error">{evidenceError}</p>
1167
+ {/if}
1168
+ {#if evidenceNotice}
1169
+ <p class="evidence-message evidence-message--notice">{evidenceNotice}</p>
1170
+ {/if}
1171
+ <div class="evidence-bulk-toolbar">
1172
+ <span>{t(M['content.content_editor.evidence_items_selected'], {
1173
+ count: selectedEvidenceCount,
1174
+ itemLabel: t(
1175
+ selectedEvidenceCount === 1
1176
+ ? M['content.content_editor.evidence_item_singular']
1177
+ : M['content.content_editor.evidence_item_plural'],
1178
+ ),
1179
+ })}</span>
1180
+ <select bind:value={bulkEvidenceStatus} disabled={selectedEvidenceCount === 0 || Boolean(evidenceBusy)}>
1181
+ {#each evidenceStatuses as status}
1182
+ <option value={status}>{status}</option>
1183
+ {/each}
1184
+ </select>
1185
+ <button
1186
+ type="button"
1187
+ disabled={selectedEvidenceCount === 0 || Boolean(evidenceBusy)}
1188
+ onclick={() => void updateSelectedEvidenceStatus()}
1189
+ >
1190
+ Mark
1191
+ </button>
1192
+ <button
1193
+ type="button"
1194
+ disabled={selectedEvidenceCount === 0 || Boolean(evidenceBusy)}
1195
+ onclick={() => void repairSelectedReferenceEvidence()}
1196
+ >
1197
+ {t(M['content.content_editor.repair_resources'])}
1198
+ </button>
1199
+ </div>
1200
+ {#each auditReferences as reference, referenceIndex (reference._auditSourceId ?? reference.id ?? reference.url ?? reference.title)}
1201
+ {@const resourceClaims = getResourceClaimsForReference(reference)}
1202
+ {@const resourceClaimsExpanded = isReferenceClaimsExpanded(reference, referenceIndex)}
1203
+ {@const visibleResourceClaims = resourceClaimsExpanded ? resourceClaims : resourceClaims.slice(0, 6)}
1204
+ <div class="reference-detail">
1205
+ <div class="reference-detail-header">
1206
+ <div>
1207
+ <strong>{getReferenceLabel(reference)}</strong>
1208
+ {#if getReferenceUrl(reference)}
1209
+ <a href={getReferenceUrl(reference) ?? undefined} target="_blank" rel="noreferrer">
1210
+ {getReferenceUrl(reference)}
1211
+ </a>
1212
+ {/if}
1213
+ </div>
1214
+ <div class="reference-detail-actions">
1215
+ <span>{t(M['content.content_editor.evidence_claims'], {
1216
+ count: resourceClaims.length,
1217
+ claimLabel: t(
1218
+ resourceClaims.length === 1
1219
+ ? M['content.content_editor.evidence_claim_singular']
1220
+ : M['content.content_editor.evidence_claim_plural'],
1221
+ ),
1222
+ })}</span>
1223
+ <button
1224
+ type="button"
1225
+ disabled={Boolean(evidenceBusy)}
1226
+ onclick={() => void repairReferenceEvidence(reference)}
1227
+ >
1228
+ {evidenceBusy === `repair:${reference.id}` ? 'Repairing...' : 'Repair'}
1229
+ </button>
1230
+ </div>
1231
+ </div>
1232
+ {#if resourceClaims.length > 0}
1233
+ <div class="resource-claim-list">
1234
+ {#each visibleResourceClaims as claim, claimIndex (getResourceClaimKey(claim, claimIndex))}
1235
+ {@const evidenceIds = getResourceClaimEvidenceIds(claim)}
1236
+ <details class="resource-claim">
1237
+ <summary>
1238
+ <label class="evidence-select">
1239
+ <input
1240
+ type="checkbox"
1241
+ checked={isResourceClaimSelected(claim)}
1242
+ disabled={evidenceIds.length === 0}
1243
+ onchange={() => toggleResourceClaimSelection(claim)}
1244
+ />
1245
+ <span class={`evidence-status evidence-status--${claim.status || 'supports'}`}>
1246
+ {getEvidenceStatusIcon(claim.status)}
1247
+ </span>
1248
+ </label>
1249
+ <strong>{claim.fact?.textRefined || claim.fact?.textRaw || claim.quote}</strong>
1250
+ </summary>
1251
+ <div class="resource-claim-body">
1252
+ {#if claim.quote}
1253
+ <p>{claim.quote}</p>
1254
+ {/if}
1255
+ <dl>
1256
+ <div>
1257
+ <dt>Status</dt>
1258
+ <dd>{claim.status || 'supports'}</dd>
1259
+ </div>
1260
+ {#if claim.locator}
1261
+ <div>
1262
+ <dt>Locator</dt>
1263
+ <dd>{claim.locator}</dd>
1264
+ </div>
1265
+ {/if}
1266
+ {#if claim.sourceTitle}
1267
+ <div>
1268
+ <dt>Source</dt>
1269
+ <dd>{claim.sourceTitle}</dd>
1270
+ </div>
1271
+ {/if}
1272
+ {#if claim.confidence !== null && claim.confidence !== undefined}
1273
+ <div>
1274
+ <dt>Confidence</dt>
1275
+ <dd>{Math.round(claim.confidence * 100)}%</dd>
1276
+ </div>
1277
+ {/if}
1278
+ </dl>
1279
+ {#each claim.evidence ?? [] as evidence (evidence.id ?? evidence.evidenceKey)}
1280
+ <div class="evidence-detail">
1281
+ <span>{evidence.extractionMethod || 'extracted evidence'}</span>
1282
+ {#if evidence.quote}
1283
+ <p>{evidence.quote}</p>
1284
+ {/if}
1285
+ </div>
1286
+ {/each}
1287
+ <div class="resource-claim-actions">
1288
+ {#each evidenceStatuses as status}
1289
+ <button
1290
+ type="button"
1291
+ disabled={evidenceIds.length === 0 || Boolean(evidenceBusy)}
1292
+ onclick={() => void updateEvidenceStatus(evidenceIds, status)}
1293
+ >
1294
+ {status}
1295
+ </button>
1296
+ {/each}
1297
+ </div>
1298
+ </div>
1299
+ </details>
1300
+ {/each}
1301
+ {#if resourceClaims.length > 6}
1302
+ <button
1303
+ type="button"
1304
+ class="resource-claim-more"
1305
+ onclick={() => toggleReferenceClaims(reference, referenceIndex)}
1306
+ >
1307
+ {resourceClaimsExpanded ? 'Show fewer' : `+ ${resourceClaims.length - 6} more`}
1308
+ </button>
1309
+ {/if}
1310
+ </div>
1311
+ {/if}
1312
+ </div>
1313
+ {/each}
1314
+ </div>
1315
+ {/if}
1316
+ <div class="add-reference-row">
1317
+ <input type="text" bind:value={newReferenceId} placeholder={t(M['content.content_editor.reference_id_or_url_placeholder'])} />
1318
+ <button type="button" onclick={addReference}>Add</button>
1319
+ </div>
1320
+ </div>
1321
+
1322
+ <label>
1323
+ URL:
1324
+ <input type="url" bind:value={formData.url} />
1325
+ </label>
1326
+ <label>
1327
+ {t(M['content.content_editor.file_key'])}
1328
+ <input type="text" bind:value={formData.fileKey} />
1329
+ </label>
1330
+ </div>
1331
+ </details>
1332
+ </form>
1333
+
1334
+ {#if showChatSidebar}
1335
+ <aside class="editor-sidebar-col">
1336
+ <div class="chat-sidebar-section">
1337
+ {#if showAgentChat}
1338
+ <ContentAgentChat
1339
+ {apiBaseUrl}
1340
+ assistantContext={assistantContext}
1341
+ contentId={agentChatContentId}
1342
+ {currentEditorState}
1343
+ {currentReferenceIds}
1344
+ formFields={agentChatFields}
1345
+ {assistantFieldAllowList}
1346
+ onapplyfields={applyFieldUpdates}
1347
+ onclose={() => {}}
1348
+ />
1349
+ {:else}
1350
+ <div
1351
+ class="chat-sidebar-empty-state"
1352
+ data-testid="content-editor-agent-chat-disabled"
1353
+ >
1354
+ <h3>{t(M['content.content_editor.agent_chat_unavailable'])}</h3>
1355
+ <p>
1356
+ {agentChatNotice ||
1357
+ 'Run the content package dev server to use the agent chat sidebar for this editor.'}
1358
+ </p>
1359
+ </div>
1360
+ {/if}
1361
+ </div>
1362
+ </aside>
1363
+ {/if}
1364
+ </div>
1365
+ </div>
1366
+
1367
+ <style>
1368
+ .form-container {
1369
+ width: 100%;
1370
+ margin: 0 auto;
1371
+ padding: 1rem 0;
1372
+ }
1373
+
1374
+ .editor-grid {
1375
+ display: grid;
1376
+ grid-template-columns: 1fr;
1377
+ gap: 2rem;
1378
+ align-items: start;
1379
+ width: 100%;
1380
+ }
1381
+
1382
+ @media (min-width: 1024px) {
1383
+ .editor-grid--with-sidebar {
1384
+ grid-template-columns: 1fr auto;
1385
+ }
1386
+ .editor-sidebar-col {
1387
+ width: 380px;
1388
+ }
1389
+ }
1390
+
1391
+ .document-title-input {
1392
+ width: 100%;
1393
+ font-size: var(--smrt-typography-display-medium-size, 2.5rem);
1394
+ font-weight: var(--smrt-typography-weight-bold, 800);
1395
+ line-height: var(--smrt-typography-display-medium-line-height, 1.2);
1396
+ padding: 0;
1397
+ margin-bottom: 2rem;
1398
+ border: none;
1399
+ outline: none;
1400
+ background: transparent;
1401
+ color: var(--smrt-color-on-surface);
1402
+ resize: none;
1403
+ font-family: inherit;
1404
+ }
1405
+
1406
+ .document-title-input::placeholder {
1407
+ color: var(--smrt-color-outline-variant);
1408
+ }
1409
+
1410
+ .document-body-section {
1411
+ display: flex;
1412
+ flex-direction: column;
1413
+ gap: 0.85rem;
1414
+ margin-bottom: 2rem;
1415
+ }
1416
+
1417
+ .document-body-media-row {
1418
+ display: flex;
1419
+ justify-content: flex-end;
1420
+ min-height: 2.75rem;
1421
+ }
1422
+
1423
+ .editor-toolbar {
1424
+ display: flex;
1425
+ align-items: center;
1426
+ justify-content: space-between;
1427
+ padding-bottom: 1.5rem;
1428
+ margin-bottom: 1.5rem;
1429
+ border-bottom: 1px solid var(--smrt-color-outline-variant);
1430
+ }
1431
+
1432
+ .editor-toolbar-left {
1433
+ display: flex;
1434
+ align-items: center;
1435
+ gap: 1rem;
1436
+ }
1437
+
1438
+ .editor-toolbar-right {
1439
+ display: flex;
1440
+ align-items: center;
1441
+ gap: 0.75rem;
1442
+ }
1443
+
1444
+ .mui-field {
1445
+ position: relative;
1446
+ display: inline-flex;
1447
+ margin-top: 0.5rem;
1448
+ }
1449
+
1450
+ .mui-field label {
1451
+ position: absolute;
1452
+ top: -0.5rem;
1453
+ left: 0.5rem;
1454
+ background: var(--smrt-color-surface); /* Matches main surface background */
1455
+ padding: 0 0.25rem;
1456
+ font-size: var(--smrt-typography-label-small-size, 0.65rem);
1457
+ font-weight: var(--smrt-typography-weight-semibold, 600);
1458
+ color: var(--smrt-color-outline);
1459
+ pointer-events: none;
1460
+ text-transform: uppercase;
1461
+ letter-spacing: var(--smrt-typography-label-small-tracking, 0.05em);
1462
+ z-index: 1;
1463
+ }
1464
+
1465
+ .mui-input {
1466
+ padding: 0.5rem 0.75rem;
1467
+ border-radius: 0.375rem;
1468
+ border: 1px solid var(--smrt-color-outline-variant);
1469
+ background: transparent;
1470
+ color: var(--smrt-color-on-surface);
1471
+ font-size: var(--smrt-typography-body-medium-size, 0.8125rem);
1472
+ font-weight: var(--smrt-typography-weight-medium, 500);
1473
+ width: auto;
1474
+ font-family: inherit;
1475
+ box-sizing: border-box;
1476
+ transition: border-color 0.2s;
1477
+ }
1478
+
1479
+ .mui-input:focus {
1480
+ outline: none;
1481
+ border-color: var(--smrt-color-primary);
1482
+ }
1483
+
1484
+ .editor-main-col {
1485
+ display: flex;
1486
+ flex-direction: column;
1487
+ background: transparent;
1488
+ }
1489
+
1490
+ .editor-sidebar-col {
1491
+ display: flex;
1492
+ flex-direction: column;
1493
+ gap: 1.25rem;
1494
+ position: sticky;
1495
+ top: 2rem;
1496
+ }
1497
+
1498
+ .editor-drawer {
1499
+ margin: 0 0 2rem 0;
1500
+ padding: 0;
1501
+ }
1502
+
1503
+ .editor-drawer-header {
1504
+ display: flex;
1505
+ justify-content: space-between;
1506
+ align-items: center;
1507
+ padding: 0 0 1.25rem 0;
1508
+ font-size: var(--smrt-typography-headline-small-size, 1.5rem);
1509
+ font-weight: var(--smrt-typography-weight-bold, 700);
1510
+ color: var(--smrt-color-on-surface);
1511
+ cursor: pointer;
1512
+ list-style: none; /* Hide default triangle */
1513
+ user-select: none;
1514
+ transition: color 0.2s;
1515
+ margin: 0;
1516
+ }
1517
+
1518
+ /* Hide the default details marker */
1519
+ .editor-drawer-header::-webkit-details-marker {
1520
+ display: none;
1521
+ }
1522
+
1523
+ .editor-drawer-header:hover {
1524
+ color: var(--smrt-color-primary);
1525
+ }
1526
+
1527
+ .drawer-icon {
1528
+ color: var(--smrt-color-outline);
1529
+ transition: transform 0.3s ease;
1530
+ }
1531
+
1532
+ .editor-drawer[open] .drawer-icon {
1533
+ transform: rotate(180deg);
1534
+ }
1535
+
1536
+ .editor-drawer-content {
1537
+ padding: 0;
1538
+ display: flex;
1539
+ flex-direction: column;
1540
+ gap: 1.25rem;
1541
+ }
1542
+
1543
+ .chat-sidebar-section {
1544
+ height: 600px;
1545
+ background: var(--smrt-color-surface);
1546
+ border-radius: 1rem;
1547
+ border: 1px solid var(--smrt-color-outline-variant);
1548
+ overflow: hidden;
1549
+ }
1550
+
1551
+ .chat-sidebar-empty-state {
1552
+ display: grid;
1553
+ gap: 0.75rem;
1554
+ align-content: start;
1555
+ padding: 1.25rem;
1556
+ height: 100%;
1557
+ box-sizing: border-box;
1558
+ background: var(--smrt-color-surface-container-low, #f8fafc);
1559
+ color: var(--smrt-color-on-surface, #1f2937);
1560
+ }
1561
+
1562
+ .chat-sidebar-empty-state h3,
1563
+ .chat-sidebar-empty-state p {
1564
+ margin: 0;
1565
+ }
1566
+
1567
+ .form-container h3 {
1568
+ margin: 0;
1569
+ color: var(--smrt-color-on-surface);
1570
+ font-size: var(--smrt-typography-headline-small-size, 1.5rem);
1571
+ }
1572
+
1573
+ .form-container form {
1574
+ display: block;
1575
+ width: 100%;
1576
+ }
1577
+
1578
+ .form-container label {
1579
+ display: flex;
1580
+ flex-direction: column;
1581
+ gap: 0.5rem;
1582
+ color: var(--smrt-color-on-surface-variant);
1583
+ font-weight: var(--smrt-typography-weight-medium, 500);
1584
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
1585
+ }
1586
+
1587
+ .form-container input,
1588
+ .form-container select,
1589
+ .form-container textarea {
1590
+ padding: 0.75rem;
1591
+ border: 1px solid var(--smrt-color-outline);
1592
+ border-radius: 0.5rem;
1593
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
1594
+ transition: border-color 0.2s, box-shadow 0.2s;
1595
+ font-family: inherit;
1596
+ box-sizing: border-box;
1597
+ width: 100%;
1598
+ background: var(--smrt-color-surface-container-low);
1599
+ color: var(--smrt-color-on-surface);
1600
+ }
1601
+
1602
+ .form-container input:focus,
1603
+ .form-container select:focus,
1604
+ .form-container textarea:focus {
1605
+ outline: none;
1606
+ border-color: var(--smrt-color-primary);
1607
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--smrt-color-primary) 10%, transparent);
1608
+ }
1609
+
1610
+ .form-container textarea {
1611
+ resize: vertical;
1612
+ min-height: 120px;
1613
+ }
1614
+
1615
+ .references-section {
1616
+ display: flex;
1617
+ flex-direction: column;
1618
+ gap: 0.75rem;
1619
+ background: var(--smrt-color-surface-container-low);
1620
+ border: 1px solid var(--smrt-color-outline-variant);
1621
+ padding: 1rem;
1622
+ border-radius: 0.5rem;
1623
+ }
1624
+
1625
+ .references-list {
1626
+ display: flex;
1627
+ flex-wrap: wrap;
1628
+ gap: 0.5rem;
1629
+ }
1630
+
1631
+ .reference-badge {
1632
+ display: flex;
1633
+ align-items: center;
1634
+ background: var(--smrt-color-surface);
1635
+ border: 1px solid var(--smrt-color-outline);
1636
+ border-radius: var(--smrt-radius-full, 9999px);
1637
+ padding: 0.25rem 0.25rem 0.25rem 0.75rem;
1638
+ font-size: var(--smrt-typography-label-large-size, 0.875rem);
1639
+ color: var(--smrt-color-on-surface);
1640
+ }
1641
+
1642
+ .remove-ref-btn {
1643
+ background: none;
1644
+ border: none;
1645
+ color: var(--smrt-color-outline);
1646
+ cursor: pointer;
1647
+ font-size: var(--smrt-typography-title-medium-size, 1.125rem);
1648
+ line-height: 1;
1649
+ padding: 0 0.25rem;
1650
+ margin-left: 0.25rem;
1651
+ }
1652
+
1653
+ .remove-ref-btn:hover {
1654
+ color: var(--smrt-color-error);
1655
+ }
1656
+
1657
+ .no-refs {
1658
+ color: var(--smrt-color-outline);
1659
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
1660
+ font-style: italic;
1661
+ }
1662
+
1663
+ .reference-detail-list,
1664
+ .resource-claim-list {
1665
+ display: flex;
1666
+ flex-direction: column;
1667
+ gap: 0.65rem;
1668
+ }
1669
+
1670
+ .reference-detail {
1671
+ border: 1px solid var(--smrt-color-outline-variant);
1672
+ background: var(--smrt-color-surface);
1673
+ border-radius: 0.5rem;
1674
+ padding: 0.75rem;
1675
+ }
1676
+
1677
+ .reference-detail-header {
1678
+ display: flex;
1679
+ justify-content: space-between;
1680
+ align-items: flex-start;
1681
+ gap: 0.75rem;
1682
+ color: var(--smrt-color-on-surface);
1683
+ }
1684
+
1685
+ .reference-detail-header div,
1686
+ .resource-claim summary {
1687
+ display: flex;
1688
+ gap: 0.25rem;
1689
+ }
1690
+
1691
+ .reference-detail-header div {
1692
+ flex-direction: column;
1693
+ }
1694
+
1695
+ .reference-detail-actions {
1696
+ align-items: flex-end;
1697
+ display: flex;
1698
+ flex-direction: column;
1699
+ gap: 0.35rem;
1700
+ white-space: nowrap;
1701
+ }
1702
+
1703
+ .reference-detail-actions button,
1704
+ .resource-claim-actions button,
1705
+ .evidence-bulk-toolbar button {
1706
+ background: var(--smrt-color-surface);
1707
+ border: 1px solid var(--smrt-color-outline-variant);
1708
+ border-radius: 0.375rem;
1709
+ color: var(--smrt-color-on-surface);
1710
+ cursor: pointer;
1711
+ font-size: var(--smrt-typography-label-medium-size, 0.75rem);
1712
+ font-weight: var(--smrt-typography-weight-semibold, 600);
1713
+ padding: 0.25rem 0.5rem;
1714
+ }
1715
+
1716
+ .reference-detail-actions button:disabled,
1717
+ .resource-claim-actions button:disabled,
1718
+ .evidence-bulk-toolbar button:disabled {
1719
+ cursor: not-allowed;
1720
+ opacity: 0.55;
1721
+ }
1722
+
1723
+ .evidence-bulk-toolbar {
1724
+ align-items: center;
1725
+ background: var(--smrt-color-surface);
1726
+ border: 1px solid var(--smrt-color-outline-variant);
1727
+ border-radius: 0.5rem;
1728
+ display: flex;
1729
+ flex-wrap: wrap;
1730
+ gap: 0.5rem;
1731
+ padding: 0.5rem;
1732
+ }
1733
+
1734
+ .evidence-bulk-toolbar span,
1735
+ .evidence-message {
1736
+ color: var(--smrt-color-on-surface-variant);
1737
+ font-size: var(--smrt-typography-body-medium-size, 0.8125rem);
1738
+ }
1739
+
1740
+ .evidence-bulk-toolbar select {
1741
+ border: 1px solid var(--smrt-color-outline-variant);
1742
+ border-radius: 0.375rem;
1743
+ padding: 0.25rem 0.5rem;
1744
+ }
1745
+
1746
+ .evidence-message {
1747
+ margin: 0;
1748
+ }
1749
+
1750
+ .evidence-message--error {
1751
+ color: var(--smrt-color-error);
1752
+ }
1753
+
1754
+ .evidence-message--notice {
1755
+ color: var(--smrt-color-primary);
1756
+ }
1757
+
1758
+ .reference-detail-header a,
1759
+ .reference-detail-header span,
1760
+ .resource-claim-body,
1761
+ .resource-claim-more {
1762
+ color: var(--smrt-color-on-surface-variant);
1763
+ font-size: var(--smrt-typography-body-medium-size, 0.8125rem);
1764
+ }
1765
+
1766
+ .resource-claim-more {
1767
+ align-self: flex-start;
1768
+ border: 0;
1769
+ background: transparent;
1770
+ padding: 0;
1771
+ cursor: pointer;
1772
+ font-weight: var(--smrt-typography-weight-semibold, 600);
1773
+ }
1774
+
1775
+ .resource-claim-list {
1776
+ margin-top: 0.75rem;
1777
+ }
1778
+
1779
+ .resource-claim {
1780
+ border-top: 1px solid var(--smrt-color-outline-variant);
1781
+ padding-top: 0.65rem;
1782
+ }
1783
+
1784
+ .resource-claim summary {
1785
+ align-items: flex-start;
1786
+ cursor: pointer;
1787
+ list-style: none;
1788
+ }
1789
+
1790
+ .resource-claim summary::-webkit-details-marker {
1791
+ display: none;
1792
+ }
1793
+
1794
+ .evidence-select {
1795
+ align-items: center;
1796
+ display: flex;
1797
+ gap: 0.35rem;
1798
+ margin-top: 0.1rem;
1799
+ }
1800
+
1801
+ .evidence-status {
1802
+ align-items: center;
1803
+ border: 1px solid var(--smrt-color-outline-variant);
1804
+ border-radius: var(--smrt-radius-full, 9999px);
1805
+ display: inline-flex;
1806
+ font-size: var(--smrt-typography-label-medium-size, 0.75rem);
1807
+ font-weight: var(--smrt-typography-weight-bold, 800);
1808
+ height: 1.35rem;
1809
+ justify-content: center;
1810
+ line-height: 1;
1811
+ width: 1.35rem;
1812
+ }
1813
+
1814
+ .evidence-status--supports {
1815
+ background: var(--smrt-color-success-container);
1816
+ color: var(--smrt-color-on-success-container);
1817
+ }
1818
+
1819
+ .evidence-status--contradicts,
1820
+ .evidence-status--invalid {
1821
+ background: var(--smrt-color-error-container);
1822
+ color: var(--smrt-color-on-error-container);
1823
+ }
1824
+
1825
+ .evidence-status--unclear {
1826
+ background: var(--smrt-color-warning-container);
1827
+ color: var(--smrt-color-on-warning-container);
1828
+ }
1829
+
1830
+ .evidence-status--irrelevant {
1831
+ background: var(--smrt-color-surface-container);
1832
+ color: var(--smrt-color-on-surface-variant);
1833
+ }
1834
+
1835
+ .resource-claim-body {
1836
+ padding: 0.5rem 0 0 2.6rem;
1837
+ }
1838
+
1839
+ .resource-claim-body p {
1840
+ margin: 0.25rem 0 0;
1841
+ }
1842
+
1843
+ .resource-claim-body dl {
1844
+ display: grid;
1845
+ gap: 0.25rem;
1846
+ grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
1847
+ margin: 0.5rem 0;
1848
+ }
1849
+
1850
+ .resource-claim-body dl div {
1851
+ display: flex;
1852
+ flex-direction: column;
1853
+ gap: 0.1rem;
1854
+ }
1855
+
1856
+ .resource-claim-body dt {
1857
+ color: var(--smrt-color-outline);
1858
+ font-size: var(--smrt-typography-label-small-size, 0.7rem);
1859
+ font-weight: var(--smrt-typography-weight-bold, 700);
1860
+ text-transform: uppercase;
1861
+ }
1862
+
1863
+ .resource-claim-body dd {
1864
+ margin: 0;
1865
+ }
1866
+
1867
+ .evidence-detail {
1868
+ border-left: 2px solid var(--smrt-color-outline-variant);
1869
+ margin-top: 0.5rem;
1870
+ padding-left: 0.65rem;
1871
+ }
1872
+
1873
+ .evidence-detail span {
1874
+ color: var(--smrt-color-outline);
1875
+ font-size: var(--smrt-typography-label-medium-size, 0.75rem);
1876
+ font-weight: var(--smrt-typography-weight-bold, 700);
1877
+ }
1878
+
1879
+ .resource-claim-actions {
1880
+ display: flex;
1881
+ flex-wrap: wrap;
1882
+ gap: 0.4rem;
1883
+ margin-top: 0.65rem;
1884
+ }
1885
+
1886
+ .add-reference-row {
1887
+ display: flex;
1888
+ gap: 0.5rem;
1889
+ }
1890
+
1891
+ .add-reference-row input {
1892
+ flex: 1;
1893
+ }
1894
+
1895
+ .add-reference-row button {
1896
+ background: var(--smrt-color-surface);
1897
+ border: 1px solid var(--smrt-color-outline);
1898
+ color: var(--smrt-color-on-surface-variant);
1899
+ padding: 0 1rem;
1900
+ border-radius: 0.5rem;
1901
+ cursor: pointer;
1902
+ font-weight: var(--smrt-typography-weight-medium, 500);
1903
+ }
1904
+
1905
+ .add-reference-row button:hover {
1906
+ background: var(--smrt-color-surface-container-low, #f1f5f9);
1907
+ }
1908
+
1909
+ .add-image-btn {
1910
+ display: flex;
1911
+ align-items: center;
1912
+ gap: 0.5rem;
1913
+ background: var(--smrt-color-surface, white);
1914
+ border: 1px solid var(--smrt-color-outline, #cbd5e1);
1915
+ padding: 0.75rem 1.25rem;
1916
+ border-radius: var(--smrt-radius-md, 0.5rem);
1917
+ color: var(--smrt-color-on-surface-variant, #475569);
1918
+ font-weight: var(--smrt-typography-weight-medium, 500);
1919
+ cursor: pointer;
1920
+ transition: all 0.2s;
1921
+ }
1922
+
1923
+ .add-image-btn:hover {
1924
+ border-color: var(--smrt-color-primary, #94a3b8);
1925
+ color: var(--smrt-color-on-surface, #1e293b);
1926
+ background: var(--smrt-color-surface-container-low, #f1f5f9);
1927
+ }
1928
+
1929
+ .save-button {
1930
+ background: linear-gradient(
1931
+ 135deg,
1932
+ var(--smrt-color-primary) 0%,
1933
+ color-mix(in srgb, var(--smrt-color-primary) 80%, black) 100%
1934
+ );
1935
+ color: var(--smrt-color-on-primary);
1936
+ border: none;
1937
+ padding: 0.75rem 1.5rem;
1938
+ border-radius: 0.5rem;
1939
+ font-weight: var(--smrt-typography-weight-semibold, 600);
1940
+ cursor: pointer;
1941
+ transition: transform 0.2s, box-shadow 0.2s;
1942
+ }
1943
+
1944
+ .save-button:hover {
1945
+ transform: translateY(-1px);
1946
+ box-shadow: 0 4px 6px -1px color-mix(in srgb, var(--smrt-color-primary) 40%, transparent);
1947
+ }
1948
+
1949
+ .save-button:disabled {
1950
+ opacity: 0.65;
1951
+ transform: none;
1952
+ box-shadow: none;
1953
+ }
1954
+
1955
+ .cancel-button {
1956
+ background: var(--smrt-color-surface);
1957
+ color: var(--smrt-color-on-surface);
1958
+ border: 1px solid var(--smrt-color-outline-variant);
1959
+ padding: 0.75rem 1.25rem;
1960
+ border-radius: 0.5rem;
1961
+ font-weight: var(--smrt-typography-weight-semibold, 600);
1962
+ cursor: pointer;
1963
+ }
1964
+
1965
+ .save-notice {
1966
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
1967
+ color: var(--smrt-color-primary, #3b82f6);
1968
+ }
1969
+
1970
+ .inline-uploader-container {
1971
+ width: 100%;
1972
+ min-height: 400px; /* Reduced for inline view */
1973
+ max-height: 60vh;
1974
+ overflow-y: auto;
1975
+ background: var(--smrt-color-surface);
1976
+ border: 1px solid var(--smrt-color-outline-variant, #e2e8f0);
1977
+ margin-top: 1rem;
1978
+ border-radius: 0.75rem;
1979
+ box-shadow: var(--smrt-elevation-2, 0 4px 6px -1px color-mix(in srgb, var(--smrt-color-shadow) 5%, transparent), 0 2px 4px -1px color-mix(in srgb, var(--smrt-color-shadow) 3%, transparent));
1980
+ /* Svelte built-in slide animations will be handled if implemented */
1981
+ }
1982
+
1983
+ .body-inline-uploader {
1984
+ background: var(--smrt-color-surface-container);
1985
+ }
1986
+
1987
+ .undo-banner {
1988
+ display: flex;
1989
+ align-items: center;
1990
+ justify-content: space-between;
1991
+ gap: 0.75rem;
1992
+ padding: 0.625rem 1rem;
1993
+ background: linear-gradient(
1994
+ 135deg,
1995
+ var(--smrt-color-primary-container) 0%,
1996
+ color-mix(in srgb, var(--smrt-color-primary-container) 80%, var(--smrt-color-surface)) 100%
1997
+ );
1998
+ border: 1px solid color-mix(in srgb, var(--smrt-color-primary) 50%, transparent);
1999
+ border-radius: 0.5rem;
2000
+ animation: undo-slide-in 0.3s ease-out;
2001
+ }
2002
+
2003
+ @keyframes undo-slide-in {
2004
+ from { opacity: 0; transform: translateY(-8px); }
2005
+ to { opacity: 1; transform: translateY(0); }
2006
+ }
2007
+
2008
+ .undo-banner__text {
2009
+ display: flex;
2010
+ align-items: center;
2011
+ gap: 0.5rem;
2012
+ font-size: var(--smrt-typography-body-medium-size, 0.8125rem);
2013
+ font-weight: var(--smrt-typography-weight-medium, 500);
2014
+ color: var(--smrt-color-primary);
2015
+ }
2016
+
2017
+ .undo-banner__text svg {
2018
+ flex-shrink: 0;
2019
+ }
2020
+
2021
+ .undo-banner__btn {
2022
+ background: var(--smrt-color-surface);
2023
+ color: var(--smrt-color-primary);
2024
+ border: 1px solid var(--smrt-color-outline);
2025
+ padding: 0.375rem 0.875rem;
2026
+ border-radius: 0.375rem;
2027
+ font-size: var(--smrt-typography-label-large-size, 0.8125rem);
2028
+ font-weight: var(--smrt-typography-weight-semibold, 600);
2029
+ cursor: pointer;
2030
+ white-space: nowrap;
2031
+ transition: all 0.15s ease;
2032
+ }
2033
+
2034
+ .undo-banner__btn:hover {
2035
+ background: var(--smrt-color-surface-variant);
2036
+ border-color: var(--smrt-color-primary);
2037
+ }
2038
+
2039
+ /* Media Gallery */
2040
+ .media-grid {
2041
+ display: grid;
2042
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
2043
+ gap: 0.75rem;
2044
+ }
2045
+
2046
+ .media-item {
2047
+ position: relative;
2048
+ border-radius: 0.5rem;
2049
+ overflow: hidden;
2050
+ border: 2px solid transparent;
2051
+ background: var(--smrt-color-surface-container-high, #242424);
2052
+ transition: border-color 0.2s;
2053
+ }
2054
+
2055
+ .media-item[draggable='true'] {
2056
+ cursor: grab;
2057
+ }
2058
+
2059
+ .media-item[draggable='true']:active {
2060
+ cursor: grabbing;
2061
+ }
2062
+
2063
+ .media-item.is-thumbnail {
2064
+ border-color: var(--smrt-color-primary, #3b82f6);
2065
+ }
2066
+
2067
+ .media-item-image {
2068
+ width: 100%;
2069
+ height: 120px;
2070
+ object-fit: cover;
2071
+ display: block;
2072
+ }
2073
+
2074
+ .media-item-overlay {
2075
+ position: absolute;
2076
+ inset: 0;
2077
+ display: flex;
2078
+ gap: 0.5rem;
2079
+ align-items: center;
2080
+ justify-content: center;
2081
+ background: color-mix(in srgb, var(--smrt-color-scrim) 50%, transparent);
2082
+ opacity: 0;
2083
+ transition: opacity 0.2s;
2084
+ }
2085
+
2086
+ .media-item:hover .media-item-overlay {
2087
+ opacity: 1;
2088
+ }
2089
+
2090
+ .media-item-overlay button {
2091
+ padding: 0.4rem;
2092
+ border: none;
2093
+ border-radius: 0.375rem;
2094
+ background: color-mix(in srgb, var(--smrt-color-surface) 90%, transparent);
2095
+ color: var(--smrt-color-on-surface);
2096
+ cursor: pointer;
2097
+ transition: background 0.15s, transform 0.15s;
2098
+ display: flex;
2099
+ align-items: center;
2100
+ justify-content: center;
2101
+ }
2102
+
2103
+ .media-item-overlay button:hover {
2104
+ transform: scale(1.1);
2105
+ background: var(--smrt-color-surface);
2106
+ }
2107
+
2108
+ .btn-remove-asset:hover {
2109
+ background: var(--smrt-color-error-container) !important;
2110
+ color: var(--smrt-color-error) !important;
2111
+ border-color: var(--smrt-color-error) !important;
2112
+ }
2113
+
2114
+ .thumbnail-badge {
2115
+ position: absolute;
2116
+ top: 0.25rem;
2117
+ left: 0.25rem;
2118
+ background: var(--smrt-color-primary, #3b82f6);
2119
+ color: white;
2120
+ font-size: var(--smrt-typography-label-small-size, 0.65rem);
2121
+ font-weight: var(--smrt-typography-weight-semibold, 600);
2122
+ padding: 0.15rem 0.4rem;
2123
+ border-radius: 0.25rem;
2124
+ text-transform: uppercase;
2125
+ letter-spacing: var(--smrt-typography-label-small-tracking, 0.03em);
2126
+ }
2127
+
2128
+ /* ── Drag & Drop Zones ── */
2129
+ .drop-zone {
2130
+ position: relative;
2131
+ transition: border-color 0.2s, background 0.2s;
2132
+ border: 2px dashed var(--smrt-color-outline-variant, #e2e8f0);
2133
+ border-radius: 0.75rem;
2134
+ padding: 1.5rem;
2135
+ }
2136
+
2137
+ .drop-zone-active {
2138
+ border-color: var(--smrt-color-primary, #3b82f6);
2139
+ background: color-mix(in srgb, var(--smrt-color-primary, #3b82f6) 6%, transparent);
2140
+ }
2141
+
2142
+ .drop-overlay {
2143
+ position: absolute;
2144
+ inset: 0;
2145
+ z-index: 10;
2146
+ display: flex;
2147
+ flex-direction: column;
2148
+ align-items: center;
2149
+ justify-content: center;
2150
+ gap: 0.75rem;
2151
+ background: color-mix(in srgb, var(--smrt-color-primary, #3b82f6) 12%, var(--smrt-color-surface, white) 88%);
2152
+ border-radius: 0.5rem;
2153
+ pointer-events: none;
2154
+ animation: drop-pulse 0.3s ease-out;
2155
+ }
2156
+
2157
+ .drop-overlay svg {
2158
+ color: var(--smrt-color-primary, #3b82f6);
2159
+ opacity: 0.8;
2160
+ }
2161
+
2162
+ @keyframes drop-pulse {
2163
+ from { opacity: 0; transform: scale(0.96); }
2164
+ to { opacity: 1; transform: scale(1); }
2165
+ }
2166
+ </style>