@drawnagency/primitives 0.1.113 → 0.1.115

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 (534) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/cookies.d.ts +39 -0
  3. package/dist/auth/cookies.d.ts.map +1 -1
  4. package/dist/auth/index.js +3 -3
  5. package/dist/chunk-4WQ36ZP7.js +83 -0
  6. package/dist/{chunk-REDDTTGJ.js → chunk-4Y2JQQZL.js} +19 -1
  7. package/dist/chunk-5GFACGSN.js +6 -0
  8. package/dist/{chunk-AHALSCQH.js → chunk-67P34WNW.js} +76 -131
  9. package/dist/{chunk-S2L3BPLS.js → chunk-7SX42OP4.js} +1 -1
  10. package/dist/{chunk-GGXZZ77Z.js → chunk-AASEGXNT.js} +18 -1
  11. package/dist/chunk-APIJPTFG.js +64 -0
  12. package/dist/{chunk-WKZ7OENC.js → chunk-BQ5TZZUE.js} +5 -1
  13. package/dist/chunk-CDBURDPX.js +90 -0
  14. package/dist/chunk-DFKTF3CH.js +0 -0
  15. package/dist/chunk-DIMDRBFK.js +11 -0
  16. package/dist/chunk-DJABADOO.js +131 -0
  17. package/dist/{chunk-NHN2LSIU.js → chunk-DJOGPSRU.js} +31 -68
  18. package/dist/{chunk-KR2MHTNJ.js → chunk-FHTKSELX.js} +610 -62
  19. package/dist/{chunk-WUJ3XNFI.js → chunk-GGYY3JQJ.js} +19 -24
  20. package/dist/chunk-GNF6KFQ7.js +18 -0
  21. package/dist/{chunk-FRE2CCLV.js → chunk-HHNPSKPT.js} +5 -79
  22. package/dist/{chunk-IINIZBQB.js → chunk-JCDZAFX2.js} +30 -10
  23. package/dist/chunk-JR24U7FJ.js +120 -0
  24. package/dist/{chunk-BDFTMGCL.js → chunk-JXATNKLV.js} +11 -10
  25. package/dist/{chunk-SW6PCQQD.js → chunk-KQA772WQ.js} +2 -1
  26. package/dist/{chunk-6BZ2KNTP.js → chunk-NX7TFHA4.js} +195 -65
  27. package/dist/{chunk-VWWY5TL5.js → chunk-O37AV27P.js} +14 -75
  28. package/dist/chunk-RIITGTV3.js +275 -0
  29. package/dist/{chunk-HBULBDGP.js → chunk-S37DZPHW.js} +1 -1
  30. package/dist/chunk-S7QRHUWO.js +8 -0
  31. package/dist/{chunk-Z7W3DQTR.js → chunk-T26HAKW3.js} +424 -42
  32. package/dist/{chunk-4I5OO5UB.js → chunk-TEOAKGZA.js} +0 -1
  33. package/dist/{chunk-GPPVYLEI.js → chunk-UUUSJDIG.js} +1 -1
  34. package/dist/chunk-VY3IFGE3.js +70 -0
  35. package/dist/chunk-WYOFQHKI.js +176 -0
  36. package/dist/{chunk-4RNF2DRG.js → chunk-XDEPXTBY.js} +0 -2
  37. package/dist/{chunk-DS4GCYV2.js → chunk-YV3YSZEB.js} +223 -16
  38. package/dist/collab/rebase.d.ts +2 -1
  39. package/dist/collab/rebase.d.ts.map +1 -1
  40. package/dist/collab/rebase.js +24 -9
  41. package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -1
  42. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  43. package/dist/components/chat/ChatWidget.d.ts.map +1 -1
  44. package/dist/components/docfile/DocFilePane.d.ts.map +1 -1
  45. package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -1
  46. package/dist/components/editor/ConflictDialog.d.ts.map +1 -1
  47. package/dist/components/editor/DragHandle.d.ts +12 -2
  48. package/dist/components/editor/DragHandle.d.ts.map +1 -1
  49. package/dist/components/editor/DropEdgeIndicator.d.ts +15 -6
  50. package/dist/components/editor/DropEdgeIndicator.d.ts.map +1 -1
  51. package/dist/components/editor/NavGroupRow.d.ts +4 -1
  52. package/dist/components/editor/NavGroupRow.d.ts.map +1 -1
  53. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  54. package/dist/components/editor/SectionWrapper.d.ts +1 -1
  55. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  56. package/dist/components/editor/SettingsForm.d.ts.map +1 -1
  57. package/dist/components/editor/TagIndicator.d.ts +15 -0
  58. package/dist/components/editor/TagIndicator.d.ts.map +1 -0
  59. package/dist/components/editor/index.js +150 -79
  60. package/dist/components/primitives/DocumentDropZone.d.ts +1 -1
  61. package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
  62. package/dist/components/primitives/EditableList.d.ts.map +1 -1
  63. package/dist/components/primitives/HeadingSection.d.ts +4 -1
  64. package/dist/components/primitives/HeadingSection.d.ts.map +1 -1
  65. package/dist/components/primitives/ResolvedMedia.d.ts +5 -1
  66. package/dist/components/primitives/ResolvedMedia.d.ts.map +1 -1
  67. package/dist/components/primitives/{defineHeadingSection.d.ts → define-heading-section.d.ts} +1 -1
  68. package/dist/components/primitives/define-heading-section.d.ts.map +1 -0
  69. package/dist/components/primitives/{gridDrop.d.ts → grid-drop.d.ts} +1 -1
  70. package/dist/components/primitives/grid-drop.d.ts.map +1 -0
  71. package/dist/components/primitives/index.d.ts +1 -1
  72. package/dist/components/primitives/index.d.ts.map +1 -1
  73. package/dist/components/primitives/index.js +22 -154
  74. package/dist/components/sections/Button/CTAButton.d.ts.map +1 -1
  75. package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts +2 -6
  76. package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts.map +1 -1
  77. package/dist/components/sections/DoDontMedia/index.d.ts +8 -0
  78. package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -1
  79. package/dist/components/sections/IconList/IconList.d.ts +2 -1
  80. package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
  81. package/dist/components/sections/IconList/IconListSettings.d.ts +2 -1
  82. package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
  83. package/dist/components/sections/IconList/index.d.ts +1 -0
  84. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  85. package/dist/components/sections/Media/MediaBlock.d.ts +2 -1
  86. package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -1
  87. package/dist/components/sections/Media/index.d.ts +8 -0
  88. package/dist/components/sections/Media/index.d.ts.map +1 -1
  89. package/dist/components/sections/Reference/ReferenceList.d.ts +35 -0
  90. package/dist/components/sections/Reference/ReferenceList.d.ts.map +1 -0
  91. package/dist/components/sections/Reference/ReferenceSettings.d.ts +12 -0
  92. package/dist/components/sections/Reference/ReferenceSettings.d.ts.map +1 -0
  93. package/dist/components/sections/Reference/index.d.ts +9 -0
  94. package/dist/components/sections/Reference/index.d.ts.map +1 -0
  95. package/dist/components/sections/SectionSummary/SectionSummaryBlock.d.ts +38 -0
  96. package/dist/components/sections/SectionSummary/SectionSummaryBlock.d.ts.map +1 -0
  97. package/dist/components/sections/SectionSummary/SectionSummarySettings.d.ts +20 -0
  98. package/dist/components/sections/SectionSummary/SectionSummarySettings.d.ts.map +1 -0
  99. package/dist/components/sections/SectionSummary/index.d.ts +33 -0
  100. package/dist/components/sections/SectionSummary/index.d.ts.map +1 -0
  101. package/dist/components/sections/all-sections.d.ts +53 -0
  102. package/dist/components/sections/all-sections.d.ts.map +1 -1
  103. package/dist/components/sections/brand-guide.d.ts +41 -2
  104. package/dist/components/sections/brand-guide.d.ts.map +1 -1
  105. package/dist/components/sections/brand-guide.js +17 -10
  106. package/dist/components/sections/core-sections.d.ts +14 -0
  107. package/dist/components/sections/core-sections.d.ts.map +1 -1
  108. package/dist/components/sections/register-schemas.js +25 -18
  109. package/dist/components/sections/register.js +25 -18
  110. package/dist/components/shared/BrandSwatchesContext.d.ts +9 -0
  111. package/dist/components/shared/BrandSwatchesContext.d.ts.map +1 -0
  112. package/dist/components/shared/Button.d.ts +11 -3
  113. package/dist/components/shared/Button.d.ts.map +1 -1
  114. package/dist/components/shared/Checkbox.d.ts +6 -0
  115. package/dist/components/shared/Checkbox.d.ts.map +1 -1
  116. package/dist/components/shared/ConfirmDialog.d.ts +25 -0
  117. package/dist/components/shared/ConfirmDialog.d.ts.map +1 -0
  118. package/dist/components/shared/ConfirmInline.d.ts +25 -0
  119. package/dist/components/shared/ConfirmInline.d.ts.map +1 -0
  120. package/dist/components/shared/ErrorBoundary.d.ts.map +1 -1
  121. package/dist/components/shared/Fab.d.ts +11 -2
  122. package/dist/components/shared/Fab.d.ts.map +1 -1
  123. package/dist/components/shared/FabStack.d.ts +22 -0
  124. package/dist/components/shared/FabStack.d.ts.map +1 -0
  125. package/dist/components/shared/HistoryChangesModal.d.ts.map +1 -1
  126. package/dist/components/shared/HistoryPopover.d.ts.map +1 -1
  127. package/dist/components/shared/ImageBackgroundField.d.ts +9 -0
  128. package/dist/components/shared/ImageBackgroundField.d.ts.map +1 -0
  129. package/dist/components/shared/{UploadRejectionAlert.d.ts → InlineAlert.d.ts} +4 -2
  130. package/dist/components/shared/InlineAlert.d.ts.map +1 -0
  131. package/dist/components/shared/Input.d.ts +10 -0
  132. package/dist/components/shared/Input.d.ts.map +1 -1
  133. package/dist/components/shared/Navigation.d.ts.map +1 -1
  134. package/dist/components/shared/Notice.d.ts +10 -0
  135. package/dist/components/shared/Notice.d.ts.map +1 -0
  136. package/dist/components/shared/Popover.d.ts.map +1 -1
  137. package/dist/components/shared/ReferenceSourcesContext.d.ts +25 -0
  138. package/dist/components/shared/ReferenceSourcesContext.d.ts.map +1 -0
  139. package/dist/components/shared/Select.d.ts +3 -2
  140. package/dist/components/shared/Select.d.ts.map +1 -1
  141. package/dist/components/shared/SplitButton.d.ts.map +1 -1
  142. package/dist/components/shared/SummaryAnchorsContext.d.ts +10 -0
  143. package/dist/components/shared/SummaryAnchorsContext.d.ts.map +1 -0
  144. package/dist/components/shared/Textarea.d.ts +7 -0
  145. package/dist/components/shared/Textarea.d.ts.map +1 -1
  146. package/dist/components/shared/ToolbarFrame.d.ts +17 -0
  147. package/dist/components/shared/ToolbarFrame.d.ts.map +1 -0
  148. package/dist/components/shared/copy.d.ts +13 -0
  149. package/dist/components/shared/copy.d.ts.map +1 -0
  150. package/dist/components/shared/icons.d.ts +0 -12
  151. package/dist/components/shared/icons.d.ts.map +1 -1
  152. package/dist/components/shell/AudienceDetailsPanel.d.ts +7 -1
  153. package/dist/components/shell/AudienceDetailsPanel.d.ts.map +1 -1
  154. package/dist/components/shell/AudienceRow.d.ts.map +1 -1
  155. package/dist/components/shell/BugReportFAB.d.ts +7 -1
  156. package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
  157. package/dist/components/shell/CredentialList.d.ts.map +1 -1
  158. package/dist/components/shell/DocInspector.d.ts.map +1 -1
  159. package/dist/components/shell/DocumentEditorOverlay.d.ts.map +1 -1
  160. package/dist/components/shell/EditorContext.d.ts +4 -0
  161. package/dist/components/shell/EditorContext.d.ts.map +1 -1
  162. package/dist/components/shell/EditorLoginForm.d.ts.map +1 -1
  163. package/dist/components/shell/EditorModal.d.ts.map +1 -1
  164. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  165. package/dist/components/shell/HistoryToolbar.d.ts.map +1 -1
  166. package/dist/components/shell/MediaFolderTile.d.ts +4 -1
  167. package/dist/components/shell/MediaFolderTile.d.ts.map +1 -1
  168. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  169. package/dist/components/shell/RestoreModal.d.ts.map +1 -1
  170. package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
  171. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  172. package/dist/components/shell/SiteSettingsModal.d.ts +3 -1
  173. package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
  174. package/dist/components/shell/SiteSettingsTags.d.ts +13 -0
  175. package/dist/components/shell/SiteSettingsTags.d.ts.map +1 -0
  176. package/dist/components/shell/SiteSettingsUsers.d.ts.map +1 -1
  177. package/dist/components/shell/ViewerLoginForm.d.ts.map +1 -1
  178. package/dist/components/shell/{blockMoveDispatch.d.ts → block-move-dispatch.d.ts} +1 -1
  179. package/dist/components/shell/block-move-dispatch.d.ts.map +1 -0
  180. package/dist/components/shell/reference-sources.d.ts +22 -0
  181. package/dist/components/shell/reference-sources.d.ts.map +1 -0
  182. package/dist/deck/index.d.ts +2 -2
  183. package/dist/deck/index.d.ts.map +1 -1
  184. package/dist/deck/index.js +1 -3
  185. package/dist/deck-builder/index.js +10 -17
  186. package/dist/document/index.d.ts +2 -2
  187. package/dist/document/index.d.ts.map +1 -1
  188. package/dist/hooks/useBlockDnd.d.ts +11 -0
  189. package/dist/hooks/useBlockDnd.d.ts.map +1 -1
  190. package/dist/hooks/useBuildStatus.d.ts +1 -1
  191. package/dist/hooks/useBuildStatus.d.ts.map +1 -1
  192. package/dist/hooks/useDocumentUploads.d.ts +1 -1
  193. package/dist/hooks/useDocumentUploads.d.ts.map +1 -1
  194. package/dist/hooks/useEditorPersistence.d.ts +5 -1
  195. package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
  196. package/dist/hooks/useEditorPublish.d.ts +6 -3
  197. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  198. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  199. package/dist/hooks/useNotice.d.ts +19 -0
  200. package/dist/hooks/useNotice.d.ts.map +1 -0
  201. package/dist/hooks/useSortableNavRow.d.ts +11 -0
  202. package/dist/hooks/useSortableNavRow.d.ts.map +1 -1
  203. package/dist/hooks/useSortableRow.d.ts +11 -0
  204. package/dist/hooks/useSortableRow.d.ts.map +1 -1
  205. package/dist/hooks/useStackedEscape.d.ts +32 -0
  206. package/dist/hooks/useStackedEscape.d.ts.map +1 -0
  207. package/dist/index.js +95 -48
  208. package/dist/lib/audience-secret.js +2 -2
  209. package/dist/lib/block-dnd.d.ts +5 -0
  210. package/dist/lib/block-dnd.d.ts.map +1 -1
  211. package/dist/lib/block-move.d.ts.map +1 -1
  212. package/dist/lib/brand-swatches.d.ts +12 -0
  213. package/dist/lib/brand-swatches.d.ts.map +1 -0
  214. package/dist/lib/container-ops.d.ts +3 -2
  215. package/dist/lib/container-ops.d.ts.map +1 -1
  216. package/dist/lib/dexie.d.ts +7 -1
  217. package/dist/lib/dexie.d.ts.map +1 -1
  218. package/dist/lib/dexie.js +12 -3
  219. package/dist/lib/env.d.ts +4 -0
  220. package/dist/lib/env.d.ts.map +1 -1
  221. package/dist/lib/env.js +5 -3
  222. package/dist/lib/events.d.ts +1 -2
  223. package/dist/lib/events.d.ts.map +1 -1
  224. package/dist/lib/image-background.d.ts +8 -0
  225. package/dist/lib/image-background.d.ts.map +1 -0
  226. package/dist/lib/image-variant-plan.d.ts +42 -0
  227. package/dist/lib/image-variant-plan.d.ts.map +1 -0
  228. package/dist/lib/image-variant-plan.js +53 -0
  229. package/dist/lib/index-ops.d.ts.map +1 -1
  230. package/dist/lib/index-ops.js +21 -17
  231. package/dist/lib/index.d.ts +7 -4
  232. package/dist/lib/index.d.ts.map +1 -1
  233. package/dist/lib/index.js +55 -26
  234. package/dist/lib/loader.d.ts +25 -12
  235. package/dist/lib/loader.d.ts.map +1 -1
  236. package/dist/lib/media-ops.d.ts +73 -0
  237. package/dist/lib/media-ops.d.ts.map +1 -0
  238. package/dist/lib/media-ops.js +60 -0
  239. package/dist/lib/nav-tree.d.ts +7 -0
  240. package/dist/lib/nav-tree.d.ts.map +1 -1
  241. package/dist/lib/nav.d.ts +38 -0
  242. package/dist/lib/nav.d.ts.map +1 -1
  243. package/dist/lib/pages.d.ts +2 -1
  244. package/dist/lib/pages.d.ts.map +1 -1
  245. package/dist/lib/platform-broker.d.ts +4 -2
  246. package/dist/lib/platform-broker.d.ts.map +1 -1
  247. package/dist/lib/platform-broker.js +21 -13
  248. package/dist/lib/reference-type.d.ts +2 -0
  249. package/dist/lib/reference-type.d.ts.map +1 -0
  250. package/dist/lib/reference-validation.d.ts +31 -0
  251. package/dist/lib/reference-validation.d.ts.map +1 -0
  252. package/dist/lib/reference-validation.js +39 -0
  253. package/dist/lib/references.d.ts +74 -0
  254. package/dist/lib/references.d.ts.map +1 -0
  255. package/dist/lib/registry.d.ts +35 -0
  256. package/dist/lib/registry.d.ts.map +1 -1
  257. package/dist/lib/{safeRedirect.d.ts → safe-redirect.d.ts} +1 -1
  258. package/dist/lib/safe-redirect.d.ts.map +1 -0
  259. package/dist/lib/sidecar-diagnostics.d.ts +20 -0
  260. package/dist/lib/sidecar-diagnostics.d.ts.map +1 -0
  261. package/dist/lib/sidecars.d.ts +118 -0
  262. package/dist/lib/sidecars.d.ts.map +1 -0
  263. package/dist/lib/sidecars.js +23 -0
  264. package/dist/lib/slug.d.ts +2 -0
  265. package/dist/lib/slug.d.ts.map +1 -0
  266. package/dist/lib/storage-tier.d.ts +17 -0
  267. package/dist/lib/storage-tier.d.ts.map +1 -0
  268. package/dist/lib/storage-tier.js +29 -0
  269. package/dist/lib/tags.d.ts +64 -0
  270. package/dist/lib/tags.d.ts.map +1 -0
  271. package/dist/lib/theme-vars.d.ts +9 -0
  272. package/dist/lib/theme-vars.d.ts.map +1 -1
  273. package/dist/media/index.d.ts +1 -1
  274. package/dist/media/index.d.ts.map +1 -1
  275. package/dist/media/index.js +18 -14
  276. package/dist/media/queue.d.ts +2 -0
  277. package/dist/media/queue.d.ts.map +1 -1
  278. package/dist/media/utils.d.ts +2 -0
  279. package/dist/media/utils.d.ts.map +1 -1
  280. package/dist/media/{videoPoster.d.ts → video-poster.d.ts} +1 -1
  281. package/dist/media/video-poster.d.ts.map +1 -0
  282. package/dist/media/worker.d.ts +1 -35
  283. package/dist/media/worker.d.ts.map +1 -1
  284. package/dist/schemas/audience.js +2 -2
  285. package/dist/schemas/index.d.ts +1 -0
  286. package/dist/schemas/index.d.ts.map +1 -1
  287. package/dist/schemas/index.js +25 -13
  288. package/dist/schemas/media.d.ts +4 -0
  289. package/dist/schemas/media.d.ts.map +1 -1
  290. package/dist/schemas/shared.d.ts +26 -0
  291. package/dist/schemas/shared.d.ts.map +1 -1
  292. package/dist/schemas/site-config.d.ts +6 -0
  293. package/dist/schemas/site-config.d.ts.map +1 -1
  294. package/dist/schemas/tags.d.ts +27 -0
  295. package/dist/schemas/tags.d.ts.map +1 -0
  296. package/dist/storage/index.js +2 -1
  297. package/dist/storage/memory.d.ts +21 -0
  298. package/dist/storage/memory.d.ts.map +1 -0
  299. package/dist/storage/memory.js +129 -0
  300. package/dist/storage/types.d.ts +9 -0
  301. package/dist/storage/types.d.ts.map +1 -1
  302. package/dist/types/database.d.ts.map +1 -1
  303. package/package.json +26 -1
  304. package/src/auth/cookies.ts +111 -0
  305. package/src/collab/rebase.ts +29 -9
  306. package/src/components/brandguide/ColorSwatchSettings.tsx +1 -1
  307. package/src/components/brandguide/ColorsEditable.tsx +1 -0
  308. package/src/components/chat/ChatPanel.tsx +2 -3
  309. package/src/components/chat/ChatWidget.tsx +2 -3
  310. package/src/components/docfile/DocFilePane.tsx +4 -12
  311. package/src/components/editor/ChildBlockWrapper.tsx +13 -19
  312. package/src/components/editor/ConflictDialog.tsx +27 -35
  313. package/src/components/editor/DeleteButton.tsx +2 -2
  314. package/src/components/editor/DragHandle.tsx +16 -3
  315. package/src/components/editor/DropEdgeIndicator.tsx +36 -10
  316. package/src/components/editor/InsertButton.tsx +2 -2
  317. package/src/components/editor/NavGroupRow.tsx +43 -14
  318. package/src/components/editor/PagesModal.tsx +99 -26
  319. package/src/components/editor/SectionWrapper.tsx +64 -23
  320. package/src/components/editor/SettingsButton.tsx +2 -2
  321. package/src/components/editor/SettingsForm.tsx +19 -2
  322. package/src/components/editor/StatusIndicator.tsx +4 -4
  323. package/src/components/editor/TagIndicator.tsx +80 -0
  324. package/src/components/primitives/DocumentDropZone.tsx +1 -1
  325. package/src/components/primitives/EditableGrid.tsx +15 -14
  326. package/src/components/primitives/EditableList.tsx +27 -74
  327. package/src/components/primitives/HeadingSection.tsx +5 -2
  328. package/src/components/primitives/ResolvedMedia.tsx +13 -0
  329. package/src/components/primitives/{defineHeadingSection.tsx → define-heading-section.tsx} +1 -0
  330. package/src/components/primitives/index.ts +1 -1
  331. package/src/components/primitives/useEditablePlainText.ts +5 -5
  332. package/src/components/sections/Button/CTAButton.tsx +3 -1
  333. package/src/components/sections/DoDontMedia/DoDontMediaBlock.tsx +4 -4
  334. package/src/components/sections/DoDontMedia/index.tsx +12 -3
  335. package/src/components/sections/IconList/DoDontToggle.tsx +2 -2
  336. package/src/components/sections/IconList/IconList.tsx +70 -37
  337. package/src/components/sections/IconList/IconListSettings.tsx +55 -17
  338. package/src/components/sections/IconList/index.tsx +10 -3
  339. package/src/components/sections/LinkHeading/index.tsx +1 -1
  340. package/src/components/sections/Media/MediaBlock.tsx +13 -2
  341. package/src/components/sections/Media/index.tsx +12 -4
  342. package/src/components/sections/Reference/ReferenceList.tsx +214 -0
  343. package/src/components/sections/Reference/ReferenceSettings.tsx +159 -0
  344. package/src/components/sections/Reference/index.tsx +47 -0
  345. package/src/components/sections/SectionSummary/SectionSummaryBlock.tsx +476 -0
  346. package/src/components/sections/SectionSummary/SectionSummarySettings.tsx +126 -0
  347. package/src/components/sections/SectionSummary/index.tsx +149 -0
  348. package/src/components/sections/SubHeading/index.tsx +1 -1
  349. package/src/components/sections/SubSubHeading/index.tsx +1 -1
  350. package/src/components/sections/brand-guide.ts +4 -3
  351. package/src/components/sections/core-sections.ts +2 -0
  352. package/src/components/shared/BrandSwatchesContext.ts +13 -0
  353. package/src/components/shared/Button.tsx +47 -34
  354. package/src/components/shared/Checkbox.tsx +21 -12
  355. package/src/components/shared/ConfirmDialog.tsx +87 -0
  356. package/src/components/shared/ConfirmInline.tsx +90 -0
  357. package/src/components/shared/ErrorBoundary.tsx +6 -18
  358. package/src/components/shared/Fab.tsx +15 -5
  359. package/src/components/shared/FabStack.tsx +28 -0
  360. package/src/components/shared/HistoryChangesModal.tsx +43 -41
  361. package/src/components/shared/HistoryPopover.tsx +7 -20
  362. package/src/components/shared/ImageBackgroundField.tsx +92 -0
  363. package/src/components/shared/{UploadRejectionAlert.tsx → InlineAlert.tsx} +21 -5
  364. package/src/components/shared/Input.tsx +23 -7
  365. package/src/components/shared/Navigation.tsx +22 -17
  366. package/src/components/shared/Notice.tsx +59 -0
  367. package/src/components/shared/Popover.tsx +7 -11
  368. package/src/components/shared/ReferenceSourcesContext.ts +27 -0
  369. package/src/components/shared/Select.tsx +5 -2
  370. package/src/components/shared/SplitButton.tsx +11 -17
  371. package/src/components/shared/SummaryAnchorsContext.ts +11 -0
  372. package/src/components/shared/Textarea.tsx +17 -2
  373. package/src/components/shared/ToolbarFrame.tsx +28 -0
  374. package/src/components/shared/VideoSourceField.tsx +1 -1
  375. package/src/components/shared/copy.ts +18 -0
  376. package/src/components/shared/icons.tsx +1 -17
  377. package/src/components/shell/AudienceAddForm.tsx +2 -2
  378. package/src/components/shell/AudienceDetailsPanel.tsx +23 -5
  379. package/src/components/shell/AudienceRow.tsx +22 -15
  380. package/src/components/shell/BugReportFAB.tsx +50 -40
  381. package/src/components/shell/CredentialList.tsx +33 -13
  382. package/src/components/shell/DocInspector.tsx +3 -12
  383. package/src/components/shell/DocumentEditorOverlay.tsx +33 -23
  384. package/src/components/shell/EditorContext.tsx +7 -0
  385. package/src/components/shell/EditorLoginForm.tsx +26 -36
  386. package/src/components/shell/EditorModal.tsx +8 -10
  387. package/src/components/shell/EditorShell.tsx +501 -338
  388. package/src/components/shell/HistoryToolbar.tsx +24 -26
  389. package/src/components/shell/MediaFolderTile.tsx +8 -3
  390. package/src/components/shell/MediaLibraryModal.tsx +59 -60
  391. package/src/components/shell/ProcessingIndicator.tsx +2 -2
  392. package/src/components/shell/RestoreModal.tsx +17 -16
  393. package/src/components/shell/SetPasswordForm.tsx +3 -3
  394. package/src/components/shell/SiteSettingsChatbot.tsx +61 -42
  395. package/src/components/shell/SiteSettingsDisplay.tsx +10 -20
  396. package/src/components/shell/SiteSettingsModal.tsx +24 -2
  397. package/src/components/shell/SiteSettingsTags.tsx +143 -0
  398. package/src/components/shell/SiteSettingsUsers.tsx +36 -26
  399. package/src/components/shell/SiteSettingsViewerAccess.tsx +2 -2
  400. package/src/components/shell/ViewerLoginForm.tsx +5 -4
  401. package/src/components/shell/reference-sources.ts +36 -0
  402. package/src/deck/index.ts +2 -2
  403. package/src/document/index.ts +2 -2
  404. package/src/hooks/useBlockDnd.ts +11 -0
  405. package/src/hooks/useBuildStatus.ts +53 -6
  406. package/src/hooks/useDocumentUploads.ts +1 -1
  407. package/src/hooks/useEditorPersistence.ts +141 -35
  408. package/src/hooks/useEditorPublish.ts +27 -31
  409. package/src/hooks/useMediaPipeline.ts +30 -24
  410. package/src/hooks/useNotice.ts +45 -0
  411. package/src/hooks/useSortableNavRow.ts +11 -0
  412. package/src/hooks/useSortableRow.ts +22 -2
  413. package/src/hooks/useStackedEscape.ts +62 -0
  414. package/src/lib/block-dnd.ts +6 -1
  415. package/src/lib/block-move.ts +3 -0
  416. package/src/lib/brand-swatches.ts +32 -0
  417. package/src/lib/container-ops.ts +5 -3
  418. package/src/lib/dexie.ts +24 -2
  419. package/src/lib/env.ts +9 -0
  420. package/src/lib/events.ts +1 -2
  421. package/src/lib/image-background.ts +31 -0
  422. package/src/lib/image-variant-plan.ts +92 -0
  423. package/src/lib/index-ops.ts +9 -18
  424. package/src/lib/index.ts +20 -4
  425. package/src/lib/loader.ts +28 -88
  426. package/src/lib/media-ops.ts +112 -0
  427. package/src/lib/nav-tree.ts +104 -4
  428. package/src/lib/nav.ts +196 -36
  429. package/src/lib/pages.ts +2 -3
  430. package/src/lib/platform-broker.ts +32 -14
  431. package/src/lib/reference-type.ts +3 -0
  432. package/src/lib/reference-validation.ts +69 -0
  433. package/src/lib/references.ts +130 -0
  434. package/src/lib/registry.ts +37 -0
  435. package/src/lib/sidecar-diagnostics.ts +29 -0
  436. package/src/lib/sidecars.ts +221 -0
  437. package/src/lib/slug.ts +6 -0
  438. package/src/lib/storage-tier.ts +38 -0
  439. package/src/lib/tags.ts +165 -0
  440. package/src/lib/theme-vars.ts +23 -0
  441. package/src/media/index.ts +1 -1
  442. package/src/media/queue.ts +3 -0
  443. package/src/media/utils.ts +19 -1
  444. package/src/media/{videoPoster.ts → video-poster.ts} +2 -2
  445. package/src/media/worker.ts +17 -8
  446. package/src/schemas/index.ts +1 -0
  447. package/src/schemas/media.ts +7 -2
  448. package/src/schemas/shared.ts +24 -1
  449. package/src/schemas/site-config.ts +6 -1
  450. package/src/schemas/tags.ts +33 -0
  451. package/src/storage/memory.ts +178 -0
  452. package/src/storage/types.ts +16 -3
  453. package/src/types/database.ts +2 -0
  454. package/dist/chunk-DNY72JZN.js +0 -86
  455. package/dist/chunk-F2LYQPXV.js +0 -77
  456. package/dist/chunk-JH762VUH.js +0 -128
  457. package/dist/chunk-K7DYZXVU.js +0 -138
  458. package/dist/components/primitives/defineHeadingSection.d.ts.map +0 -1
  459. package/dist/components/primitives/gridDrop.d.ts.map +0 -1
  460. package/dist/components/shared/UploadRejectionAlert.d.ts.map +0 -1
  461. package/dist/components/shell/BuildStatusIndicator.d.ts +0 -7
  462. package/dist/components/shell/BuildStatusIndicator.d.ts.map +0 -1
  463. package/dist/components/shell/blockMoveDispatch.d.ts.map +0 -1
  464. package/dist/hooks/index.d.ts +0 -9
  465. package/dist/hooks/index.d.ts.map +0 -1
  466. package/dist/lib/grid.d.ts +0 -2
  467. package/dist/lib/grid.d.ts.map +0 -1
  468. package/dist/lib/safeRedirect.d.ts.map +0 -1
  469. package/dist/media/videoPoster.d.ts.map +0 -1
  470. package/src/components/brandguide/ColorSwatchSettings.d.ts.map +0 -1
  471. package/src/components/brandguide/Colors.d.ts.map +0 -1
  472. package/src/components/primitives/CustomParagraph.d.ts.map +0 -1
  473. package/src/components/primitives/EditableGrid.d.ts.map +0 -1
  474. package/src/components/primitives/EditablePlainText.d.ts.map +0 -1
  475. package/src/components/primitives/EditableRichText.d.ts.map +0 -1
  476. package/src/components/primitives/HeadingSection.d.ts.map +0 -1
  477. package/src/components/primitives/IconPicker.d.ts.map +0 -1
  478. package/src/components/primitives/LinkPopover.d.ts.map +0 -1
  479. package/src/components/primitives/MediaSettingsForms.d.ts.map +0 -1
  480. package/src/components/primitives/ResolvedMedia.d.ts.map +0 -1
  481. package/src/components/primitives/RichTextToolbar.d.ts.map +0 -1
  482. package/src/components/primitives/tiptap-presets.d.ts.map +0 -1
  483. package/src/components/primitives/useEditableCollection.d.ts.map +0 -1
  484. package/src/components/primitives/useEditablePlainText.d.ts.map +0 -1
  485. package/src/components/primitives/useEditableRichText.d.ts.map +0 -1
  486. package/src/components/sections/Button/CTAButton.d.ts.map +0 -1
  487. package/src/components/sections/Button/index.d.ts.map +0 -1
  488. package/src/components/sections/Colors/index.d.ts.map +0 -1
  489. package/src/components/sections/IconList/IconList.d.ts.map +0 -1
  490. package/src/components/sections/IconList/IconListSettings.d.ts.map +0 -1
  491. package/src/components/sections/IconList/index.d.ts.map +0 -1
  492. package/src/components/sections/LinkHeading/index.d.ts.map +0 -1
  493. package/src/components/sections/Prose/Prose.d.ts.map +0 -1
  494. package/src/components/sections/Prose/index.d.ts.map +0 -1
  495. package/src/components/sections/SubHeading/index.d.ts.map +0 -1
  496. package/src/components/sections/SubSubHeading/index.d.ts.map +0 -1
  497. package/src/components/sections/register-schemas.d.ts.map +0 -1
  498. package/src/components/shared/Button.d.ts.map +0 -1
  499. package/src/components/shared/Checkbox.d.ts.map +0 -1
  500. package/src/components/shared/FormLabel.d.ts.map +0 -1
  501. package/src/components/shared/IconButton.d.ts.map +0 -1
  502. package/src/components/shared/Input.d.ts.map +0 -1
  503. package/src/components/shared/Select.d.ts.map +0 -1
  504. package/src/components/shared/icons.d.ts.map +0 -1
  505. package/src/components/shell/BuildStatusIndicator.tsx +0 -59
  506. package/src/components/shell/MediaLibraryContext.d.ts.map +0 -1
  507. package/src/hooks/index.ts +0 -8
  508. package/src/hooks/useResolvedMedia.d.ts.map +0 -1
  509. package/src/lib/cn.d.ts.map +0 -1
  510. package/src/lib/grid.d.ts.map +0 -1
  511. package/src/lib/grid.ts +0 -7
  512. package/src/lib/icons.d.ts.map +0 -1
  513. package/src/lib/loader.d.ts.map +0 -1
  514. package/src/lib/nav.d.ts.map +0 -1
  515. package/src/lib/registry.d.ts.map +0 -1
  516. package/src/lib/sanitize.d.ts.map +0 -1
  517. package/src/media/github.d.ts.map +0 -1
  518. package/src/media/index.d.ts.map +0 -1
  519. package/src/media/queue.d.ts.map +0 -1
  520. package/src/media/resolve.d.ts.map +0 -1
  521. package/src/media/types.d.ts.map +0 -1
  522. package/src/media/utils.d.ts.map +0 -1
  523. package/src/media/videoPoster.d.ts.map +0 -1
  524. package/src/schemas/auth.d.ts.map +0 -1
  525. package/src/schemas/media-grid-options.d.ts.map +0 -1
  526. package/src/schemas/media.d.ts.map +0 -1
  527. package/src/schemas/sections.d.ts.map +0 -1
  528. package/src/schemas/shared.d.ts.map +0 -1
  529. package/src/schemas/site-config.d.ts.map +0 -1
  530. /package/dist/{chunk-B7VBSE5X.js → chunk-3JDTXOIG.js} +0 -0
  531. /package/dist/{chunk-CXZ5YJG4.js → chunk-L45X7DKX.js} +0 -0
  532. /package/src/components/primitives/{gridDrop.ts → grid-drop.ts} +0 -0
  533. /package/src/components/shell/{blockMoveDispatch.ts → block-move-dispatch.ts} +0 -0
  534. /package/src/lib/{safeRedirect.ts → safe-redirect.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { Fragment, Suspense, lazy, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from "react";
1
+ import { Fragment, Suspense, lazy, useContext, useState, useCallback, useEffect, useRef, useMemo, type ReactNode } from "react";
2
2
 
3
3
  import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
4
4
  import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
@@ -23,7 +23,7 @@ import { SiteConfigSchema } from "../../schemas/site-config";
23
23
  import { ensureSanitizer } from "../../lib/sanitize";
24
24
  import { resolveChatbotName } from "../../lib/chatbot";
25
25
  import { EditorProvider, useEditorContext, type HistoryState } from "./EditorContext";
26
- import { makeBlockMoveDispatch } from "./blockMoveDispatch";
26
+ import { makeBlockMoveDispatch } from "./block-move-dispatch";
27
27
  import { ROOT_CONTAINER_ID, resolveDropIndex } from "../../lib/block-dnd";
28
28
  import type { BlockMove } from "../../lib/block-move";
29
29
  import { EditorModalProvider, useEditorModal } from "./EditorModalContext";
@@ -36,7 +36,7 @@ import { ProcessingIndicator } from "./ProcessingIndicator";
36
36
  import { SectionSkeleton } from "./SectionSkeleton";
37
37
  import { SectionTypePicker } from "./SectionTypePicker";
38
38
  import { ensureSectionsRegistered } from "../sections/register";
39
- import { getSection, getAllSections, getSchema } from "../../lib/registry";
39
+ import { getSection, getAllSections, getSchema, type SiteTagging } from "../../lib/registry";
40
40
  import { cornerRadiusValue } from "../../lib/corner-radius";
41
41
  import { primaryVarValue, accentVarValue, onPrimaryVarValue } from "../../lib/theme-vars";
42
42
  import { rebaseMediaManifest } from "../../lib/media-folders";
@@ -87,10 +87,14 @@ import { useContentLifecycle } from "../../hooks/useContentLifecycle";
87
87
  import { useBuildStatus } from "../../hooks/useBuildStatus";
88
88
  import { useMediaPipeline } from "../../hooks/useMediaPipeline";
89
89
  import { formatTimestamp } from "../../lib/timestamp";
90
- import { generateSiteNav } from "../../lib/nav";
90
+ import { generateSiteNav, summaryNavAnchors, pageReferenceCopies } from "../../lib/nav";
91
+ import { expandReferences, type ReferenceContent } from "../../lib/references";
92
+ import { REFERENCE_TYPE } from "../../lib/reference-type";
93
+ import { referenceShowsInPreview } from "../sections/Reference/ReferenceList";
91
94
  import { deriveSiteDocuments, type SiteDocument } from "../../lib/documents";
92
95
  import { setSiteDocuments, closeDocFile } from "../docfile/doc-file-state";
93
- import { pagesInNavOrder, moveNavEntry, addGroup, renameGroup, setGroupStartCollapsed, deleteGroup, type NavLocation } from "../../lib/nav-tree";
96
+ import { pagesInNavOrder, moveNavEntry, describeNavMove, addGroup, renameGroup, setGroupStartCollapsed, deleteGroup, type NavLocation } from "../../lib/nav-tree";
97
+ import { siteTags, addTag, renameTag, deleteTag, setSectionTagged, findTagByLabel } from "../../lib/tags";
94
98
  import { siteNavChangeEvent, pageSelectEvent, darkModeEvent, siteNameChangeEvent, historySelectEvent, historyRestoreEvent, historyRestoreFailedEvent, editorDirtyEvent, audienceDetailsEvent } from "../../lib/events";
95
99
  import { AudienceDetailsPanel, type AudienceDetail } from "./AudienceDetailsPanel";
96
100
  import { buildAudienceOverview, type SectionDisplay } from "../../lib/audience-overview";
@@ -103,18 +107,28 @@ import {
103
107
  moveSection as moveSectionReducer,
104
108
  } from "../../lib/pages";
105
109
  import { PagesContext } from "../shared/PagesContext";
110
+ import { ReferenceSourcesContext, type ReferenceSources } from "../shared/ReferenceSourcesContext";
111
+ import { publishedReferenceSources } from "./reference-sources";
112
+ import { BrandSwatchesContext } from "../shared/BrandSwatchesContext";
113
+ import { collectBrandSwatches } from "../../lib/brand-swatches";
114
+ import { SummaryAnchorsContext } from "../shared/SummaryAnchorsContext";
106
115
  import { PagesModal } from "../editor/PagesModal";
107
116
  import { cn } from "../../lib/cn";
108
- import { UploadRejectionAlert, formatRejections } from "../shared/UploadRejectionAlert";
117
+ import { InlineAlert, formatRejections } from "../shared/InlineAlert";
118
+ import { Notice } from "../shared/Notice";
119
+ import { ConfirmDialog } from "../shared/ConfirmDialog";
120
+ import type { NoticeState } from "../../hooks/useNotice";
121
+ import { useNotice } from "../../hooks/useNotice";
109
122
  import { Button } from "../shared/Button";
110
123
  import { SplitButton } from "../shared/SplitButton";
124
+ import { ToolbarFrame } from "../shared/ToolbarFrame";
111
125
  import { IconButton } from "../shared/IconButton";
112
126
  import Fab from "../shared/Fab";
127
+ import { FabStack } from "../shared/FabStack";
113
128
  import { Tooltip } from "../shared/Tooltip";
114
129
  import { SegmentedControl } from "../shared/SegmentedControl";
115
130
  import { computeDisplaySections } from "./view-branch";
116
- import { SettingsIcon } from "../shared/icons";
117
- import { ImageIcon, ListOrderedIcon, FilesIcon, JoystickIcon, X } from "lucide-react";
131
+ import { ImageIcon, ListOrderedIcon, FilesIcon, JoystickIcon, Settings, X, FileText, Presentation, Eye, Pencil } from "lucide-react";
118
132
  import { ErrorBoundary } from "../shared/ErrorBoundary";
119
133
  import { HistoryToolbar } from "./HistoryToolbar";
120
134
  import { RestoreModal } from "./RestoreModal";
@@ -149,6 +163,9 @@ interface SavedContent {
149
163
  * Their bodies are absent from `sections` while their index slots survive —
150
164
  * the editor must surface them, not silently render around them. */
151
165
  unparseableSectionIds: string[];
166
+ /** tags.json held entries this version couldn't represent: tags are
167
+ * read-only this session (the loader's tagsDegraded contract). */
168
+ tagsDegraded: boolean;
152
169
  }
153
170
 
154
171
  interface Props {
@@ -241,6 +258,14 @@ export default function EditorShell({
241
258
  const deletedSectionsRef = useRef<Set<string>>(deletedSections);
242
259
  useEffect(() => { deletedSectionsRef.current = deletedSections; }, [deletedSections]);
243
260
  const [pendingDeleteSectionId, setPendingDeleteSectionId] = useState<string | null>(null);
261
+ // One shared aria-live region for every reorder — sections (via
262
+ // onReorderSections, the dispatch point shared by SectionOrderingModal's
263
+ // drag and SectionWrapper's Move up/down) and pages/groups (via
264
+ // handleMoveNavEntry, shared the same way by PagesModal/NavGroupRow's drag
265
+ // and their own Move up/down). The section canvas's own per-block drag
266
+ // (onBlockMove) is a separate, unlisted reorder surface and does not
267
+ // announce here.
268
+ const [reorderAnnouncement, setReorderAnnouncement] = useState("");
244
269
  const [savedSha, setSavedSha] = useState<string | null>(null);
245
270
  const [mainSha, setMainSha] = useState<string | null>(null);
246
271
  // Mirror savedSha into a ref so the save handler reads the latest draft
@@ -259,6 +284,13 @@ export default function EditorShell({
259
284
  const mainIndexRef = useRef<SiteIndex | null>(null);
260
285
  useEffect(() => { mainIndexRef.current = mainIndex; }, [mainIndex]);
261
286
  const [viewSections, setViewSections] = useState<LoadedSection[] | null>(null);
287
+ // The fetched main index that goes with viewSections — References resolve
288
+ // against it in the Published preview, exactly as the viewer does.
289
+ const [viewIndex, setViewIndex] = useState<SiteIndex | null>(null);
290
+ const setView = useCallback((view: { sections: LoadedSection[]; index: SiteIndex | null } | null) => {
291
+ setViewSections(view?.sections ?? null);
292
+ setViewIndex(view?.index ?? null);
293
+ }, []);
262
294
  const [showRestoreModal, setShowRestoreModal] = useState(false);
263
295
  const [showOrderingModal, setShowOrderingModal] = useState(false);
264
296
  const [showPagesModal, setShowPagesModal] = useState(false);
@@ -278,6 +310,7 @@ export default function EditorShell({
278
310
  // — surfaced as a persistent banner so a "missing" section isn't mistaken for
279
311
  // a deleted one and repaired by data loss.
280
312
  const [unparseableSectionIds, setUnparseableSectionIds] = useState<string[]>([]);
313
+ const [tagsDegraded, setTagsDegraded] = useState(false);
281
314
  // Non-fatal warnings from the last successful save (e.g. nav.json replacement
282
315
  // declined by the degraded-nav guard) — without a surface, the save reads as
283
316
  // fully landed when part of it was refused.
@@ -299,15 +332,8 @@ export default function EditorShell({
299
332
  >(null);
300
333
  // Collaboration: transient toast ("Drew took over Hero …") and the section a
301
334
  // takeover confirmation is pending for.
302
- const [remoteNotice, setRemoteNotice] = useState<string | null>(null);
335
+ const { notice: remoteNotice, show: showCollabNotice } = useNotice();
303
336
  const [pendingTakeoverId, setPendingTakeoverId] = useState<string | null>(null);
304
- // Lifted from EditorContext.historyState by HistoryWatcher (mirrors
305
- // ViewBranchWatcher's setViewSections lift) — the shell body renders the
306
- // EditorProvider, so it can't read historyState via useEditorContext directly.
307
- // Drives the Document FAB gate below: history is presentation only, never
308
- // editing (branch-preview already routes to DocumentPresentOverlay; this
309
- // covers the separate historySelectEvent commit view).
310
- const [historyActive, setHistoryActive] = useState(false);
311
337
 
312
338
  const siteIndexRef = useRef<SiteIndex>({ siteId, pages: [], sections: {} });
313
339
  // Ids of sections that exist on the remote (loaded from server, or persisted by
@@ -352,8 +378,6 @@ export default function EditorShell({
352
378
  // Mirror of the live collab session, read by handlers/callbacks defined before
353
379
  // useCollabSession is instantiated (breaks the definition-order cycle).
354
380
  const collabRef = useRef<UseCollabSessionResult | null>(null);
355
- const noticeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
356
- useEffect(() => () => { if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current); }, []);
357
381
  // Per-section memoized onEditIntent closures — SectionWrapper re-binds its
358
382
  // focus listeners whenever onEditIntent's identity changes, so each section
359
383
  // must get a STABLE function instance across renders.
@@ -363,12 +387,14 @@ export default function EditorShell({
363
387
  // one's lock is released if it carries no unsaved work.
364
388
  const focusedSectionIdRef = useRef<string | null>(null);
365
389
 
366
- // Transient collaboration toast (single slot, 8s auto-dismiss).
367
- const showRemoteNotice = useCallback((text: string) => {
368
- setRemoteNotice(text);
369
- if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current);
370
- noticeTimerRef.current = setTimeout(() => setRemoteNotice(null), 8000);
371
- }, []);
390
+ // Transient collaboration toast (single slot, 8s auto-dismiss unless
391
+ // `tone` is "error", which useNotice leaves sticky — a load failure needs a
392
+ // deliberate dismiss, not an 8s countdown). Its own useNotice instance —
393
+ // must stay independent of toolbar publishFeedback, which can be visible at
394
+ // the same time.
395
+ const showRemoteNotice = useCallback((text: string, tone: NoticeState["tone"] = "info") => {
396
+ showCollabNotice(tone, text, tone === "error" ? undefined : { ttlMs: 8000 });
397
+ }, [showCollabNotice]);
372
398
 
373
399
  // Broadcast piggyback: after a debounced flush, forward held-lock section
374
400
  // edits + index changes to peers. Only sections THIS editor holds are
@@ -401,6 +427,19 @@ export default function EditorShell({
401
427
  return resolvedActivePage.order.map((id) => sectionsById[id]).filter((s): s is LoadedSection => !!s);
402
428
  }, [resolvedActivePage, sectionsById]);
403
429
 
430
+ // The page as its nav sees it: each Reference replaced by the copies it
431
+ // would publish, so their headings join the sidebar as on the viewer. The
432
+ // canvas renders those copies from referenceCopies, stamped over the same
433
+ // list, so every sidebar link lands on a rendered id.
434
+ const navSections = useMemo(
435
+ () => expandReferences(activeSections, { index: siteIndex, allSections: sections, audience: null }),
436
+ [activeSections, siteIndex, sections],
437
+ );
438
+ const referenceCopies = useMemo(
439
+ () => pageReferenceCopies(activeSections, { index: siteIndex, allSections: sections }),
440
+ [activeSections, siteIndex, sections],
441
+ );
442
+
404
443
  // Same model the /audiences route builds server-side, from the shell's own
405
444
  // live state — so an editor sees their SAVED draft's access, not the published
406
445
  // snapshot the route would show.
@@ -487,6 +526,8 @@ export default function EditorShell({
487
526
  [siteIndex.pages, getPageHeadings],
488
527
  );
489
528
 
529
+ const brandSwatches = useMemo(() => collectBrandSwatches(sections.map((s) => s.section)), [sections]);
530
+
490
531
  const mediaPipeline = useMediaPipeline({
491
532
  siteConfig,
492
533
  mediaManifest,
@@ -513,7 +554,7 @@ export default function EditorShell({
513
554
  // draft head the same way.
514
555
  const fetchSavedContent = useCallback(async (): Promise<SavedContent> => {
515
556
  const response = await fetch("/api/content?branch=saved");
516
- if (!response.ok) throw new Error(`Failed to load content: ${response.status}`);
557
+ if (!response.ok) throw new Error(`Couldn't load content: ${response.status}`);
517
558
  const data = await response.json();
518
559
  return {
519
560
  sha: data.sha,
@@ -526,6 +567,7 @@ export default function EditorShell({
526
567
  mediaManifest: data.mediaManifest as MediaManifest | undefined,
527
568
  documentManifest: data.documentManifest as DocumentManifest | undefined,
528
569
  unparseableSectionIds: (data.unparseableSectionIds ?? []) as string[],
570
+ tagsDegraded: data.tagsDegraded === true,
529
571
  };
530
572
  }, []);
531
573
 
@@ -539,6 +581,7 @@ export default function EditorShell({
539
581
  try {
540
582
  const remote = await fetchSavedContent();
541
583
  setUnparseableSectionIds(remote.unparseableSectionIds);
584
+ setTagsDegraded(remote.tagsDegraded);
542
585
  const dirtyRows = await getDirtySectionRows();
543
586
  const dirtyIds = new Set(dirtyRows.map((r) => r.sectionId));
544
587
 
@@ -646,6 +689,7 @@ export default function EditorShell({
646
689
  media: remote.mediaManifest ?? mediaManifestRef.current,
647
690
  documents: remote.documentManifest ?? documentManifestRef.current ?? {},
648
691
  },
692
+ remote.tagsDegraded,
649
693
  {
650
694
  savedBranchSha: remote.savedBranchSha,
651
695
  changedSectionIds: remote.changedSectionIds,
@@ -732,6 +776,7 @@ export default function EditorShell({
732
776
  }
733
777
  setSections(result.sections);
734
778
  setSiteIndex(result.index);
779
+ setTagsDegraded(remote.tagsDegraded);
735
780
  setSavedSha(remote.savedBranchSha);
736
781
  setChangedSectionIds(new Set(remote.changedSectionIds));
737
782
  setMainIndex(remote.mainIndex);
@@ -757,6 +802,7 @@ export default function EditorShell({
757
802
  media: remote.mediaManifest ?? mediaManifestRef.current,
758
803
  documents: remote.documentManifest ?? documentManifestRef.current ?? {},
759
804
  },
805
+ remote.tagsDegraded,
760
806
  {
761
807
  savedBranchSha: remote.savedBranchSha,
762
808
  changedSectionIds: remote.changedSectionIds,
@@ -914,6 +960,7 @@ export default function EditorShell({
914
960
  clearConfigDirty: persistence.clearConfigDirty,
915
961
  siteIndexRef,
916
962
  siteConfig,
963
+ tagsDegraded,
917
964
  sections,
918
965
  deletedSectionIds: Array.from(deletedSections),
919
966
  onSaveWarnings: setSaveWarnings,
@@ -1026,6 +1073,7 @@ export default function EditorShell({
1026
1073
  // onMediaPublished / onDocumentsPublished callbacks only refresh through a
1027
1074
  // state effect and can still lag by a render at this point.
1028
1075
  { media: committedManifest, documents: committedDocumentManifest },
1076
+ tagsDegraded,
1029
1077
  {
1030
1078
  savedBranchSha: sha,
1031
1079
  changedSectionIds: [...changed],
@@ -1085,7 +1133,7 @@ export default function EditorShell({
1085
1133
  useEffect(() => {
1086
1134
  if (!resolvedActivePage) return;
1087
1135
  const nav = generateSiteNav({
1088
- index: siteIndex, activeSections,
1136
+ index: siteIndex, activeSections: navSections,
1089
1137
  // The Audience Details surface is not a page: clearing the active id drops
1090
1138
  // the previous page's highlight (and its heading tree), so the sidebar
1091
1139
  // never shows you in two places at once.
@@ -1094,7 +1142,7 @@ export default function EditorShell({
1094
1142
  documents: siteDocuments ?? [],
1095
1143
  });
1096
1144
  siteNavChangeEvent.dispatch(nav);
1097
- }, [siteIndex, activeSections, resolvedActivePage, siteDocuments, showAudienceDetails]);
1145
+ }, [siteIndex, navSections, resolvedActivePage, siteDocuments, showAudienceDetails]);
1098
1146
 
1099
1147
  const goToPage = useCallback((pageId: string) => {
1100
1148
  const page = pageById(siteIndexRef.current, pageId);
@@ -1112,6 +1160,30 @@ export default function EditorShell({
1112
1160
 
1113
1161
  // Nav island → shell page selection.
1114
1162
  useEffect(() => pageSelectEvent.listen((id) => { setShowAudienceDetails(false); goToPage(id); }), [goToPage]);
1163
+
1164
+ // A Reference's "Edit source": open the source's page, then scroll to it once
1165
+ // that page's sections have rendered.
1166
+ const [scrollToSectionId, setScrollToSectionId] = useState<string | null>(null);
1167
+ const jumpToSection = useCallback((sectionId: string) => {
1168
+ const page = siteIndexRef.current.pages.find((p) => p.order.includes(sectionId));
1169
+ if (!page) return;
1170
+ pageSelectEvent.dispatch(page.id);
1171
+ setScrollToSectionId(sectionId);
1172
+ }, []);
1173
+ // One attempt, cleared either way: a section the canvas isn't showing (a
1174
+ // draft in the published preview) must not leave a scroll armed for later.
1175
+ useEffect(() => {
1176
+ if (!scrollToSectionId) return;
1177
+ document.querySelector(`[data-section-id="${CSS.escape(scrollToSectionId)}"]`)
1178
+ ?.scrollIntoView({ behavior: "smooth", block: "center" });
1179
+ setScrollToSectionId(null);
1180
+ }, [scrollToSectionId, activeSections]);
1181
+
1182
+ const referenceSources = useMemo<ReferenceSources>(() => publishedReferenceSources({
1183
+ viewSections, viewIndex, mainIndex, activePageId: resolvedActivePage?.id ?? null, audiences, mediaManifest, jumpToSection,
1184
+ }) ?? { index: siteIndex, sections, audiences, mediaManifest, copies: referenceCopies, jumpToSection },
1185
+ [viewSections, viewIndex, mainIndex, resolvedActivePage, siteIndex, sections, audiences, mediaManifest, referenceCopies, jumpToSection]);
1186
+
1115
1187
  useEffect(() => audienceDetailsEvent.listen(() => setShowAudienceDetails(true)), []);
1116
1188
 
1117
1189
  // Fetched on open rather than at mount: most editing sessions never look at
@@ -1254,6 +1326,7 @@ export default function EditorShell({
1254
1326
  await cacheContent(
1255
1327
  data.sha, data.sections, data.index, data.siteConfig,
1256
1328
  { media: loadedManifest, documents: data.documentManifest ?? {} },
1329
+ data.tagsDegraded,
1257
1330
  {
1258
1331
  savedBranchSha: data.savedBranchSha,
1259
1332
  changedSectionIds: data.changedSectionIds,
@@ -1269,6 +1342,7 @@ export default function EditorShell({
1269
1342
  setChangedSectionIds(new Set(data.changedSectionIds));
1270
1343
  setMainIndex(data.mainIndex);
1271
1344
  setUnparseableSectionIds(data.unparseableSectionIds);
1345
+ setTagsDegraded(data.tagsDegraded);
1272
1346
  }
1273
1347
 
1274
1348
  if (cancelled) return;
@@ -1321,7 +1395,7 @@ export default function EditorShell({
1321
1395
 
1322
1396
  loadContent().catch((err) => {
1323
1397
  console.error("Failed to load editor content:", err);
1324
- setShellState({ phase: "error", message: err instanceof Error ? err.message : "Failed to load content" });
1398
+ setShellState({ phase: "error", message: err instanceof Error ? err.message : "Couldn't load content" });
1325
1399
  });
1326
1400
  return () => { cancelled = true; };
1327
1401
  }, [headSha, draftHeadSha, siteId, applySiteConfigPreview, fetchSavedContent]);
@@ -1354,7 +1428,12 @@ export default function EditorShell({
1354
1428
  return null;
1355
1429
  }).filter((s): s is LoadedSection => s !== null),
1356
1430
  );
1357
- setSiteIndex(restoredIndex);
1431
+ // A row written before tags shipped carries none, and that build could
1432
+ // not have edited them — keep the freshly fetched registry rather than
1433
+ // installing an index that reads as "tags never loaded".
1434
+ setSiteIndex(restoredIndex.tags === undefined && siteIndexRef.current.tags !== undefined
1435
+ ? { ...restoredIndex, tags: siteIndexRef.current.tags }
1436
+ : restoredIndex);
1358
1437
  // Pending deletions come from the snapshot's own persisted intent, and are
1359
1438
  // intersected with what the branch still has so an already-applied deletion
1360
1439
  // can't be replayed.
@@ -1470,6 +1549,11 @@ export default function EditorShell({
1470
1549
  );
1471
1550
 
1472
1551
  const handleDiscard = useCallback(async () => {
1552
+ // Drop Site Settings' debounced config write too, or its trailing edge
1553
+ // would write the discarded config back into Dexie.
1554
+ if (configFlushTimerRef.current) { clearTimeout(configFlushTimerRef.current); configFlushTimerRef.current = null; }
1555
+ pendingConfigRef.current = null;
1556
+ await persistence.discardPending();
1473
1557
  await discardLocalChanges();
1474
1558
  setLocalChangesExist(false);
1475
1559
  setDirtySectionIds(new Set());
@@ -1614,6 +1698,9 @@ export default function EditorShell({
1614
1698
  setSiteIndex((prev) => reorderVisibleSectionInPage(prev, resolvedActivePage.id, visibleIds, fromIndex, toIndex));
1615
1699
  persistence.markIndexDirty();
1616
1700
  setLocalChangesExist(true);
1701
+ const movedType = activeSections[fromIndex]?.section.type;
1702
+ const label = (movedType && getSection(movedType)?.label) || "Section";
1703
+ setReorderAnnouncement(`Moved ${label} to position ${toIndex + 1} of ${visibleIds.length}`);
1617
1704
  },
1618
1705
  [persistence, resolvedActivePage, activeSections],
1619
1706
  );
@@ -1655,8 +1742,16 @@ export default function EditorShell({
1655
1742
  }, [persistence]);
1656
1743
 
1657
1744
  const handleMoveNavEntry = useCallback((from: NavLocation, to: NavLocation) => {
1658
- setSiteIndex((prev) => moveNavEntry(prev, from, to));
1745
+ // Read the pre-move index from the ref (mirrors onBlockMove's getState
1746
+ // pattern) rather than the updater's `prev`, so describeNavMove can
1747
+ // resolve `from`'s label/kind against the SAME snapshot moveNavEntry acts
1748
+ // on, and the two calls can't observe different states.
1749
+ const prevIndex = siteIndexRef.current;
1750
+ const nextIndex = moveNavEntry(prevIndex, from, to);
1751
+ setSiteIndex(() => nextIndex);
1659
1752
  persistence.markIndexDirty(); setLocalChangesExist(true);
1753
+ const message = describeNavMove(prevIndex, nextIndex, from);
1754
+ if (message) setReorderAnnouncement(message);
1660
1755
  }, [persistence]);
1661
1756
 
1662
1757
  const handleAddGroup = useCallback(() => {
@@ -1682,6 +1777,48 @@ export default function EditorShell({
1682
1777
  persistence.markIndexDirty(); setLocalChangesExist(true);
1683
1778
  }, [persistence]);
1684
1779
 
1780
+ // Section tags: the same index-state edit + markIndexDirty as the nav group
1781
+ // handlers above. Every save then carries the runtime `tags`, which
1782
+ // content-ops writes to the tags.json sidecar.
1783
+ const handleCreateTag = useCallback((label: string, sectionId?: string) => {
1784
+ const id = typeof crypto !== "undefined" && crypto.randomUUID ? `tag_${crypto.randomUUID()}` : `tag_${Date.now()}`;
1785
+ setSiteIndex((prev) => {
1786
+ // Typing an existing tag's name reuses it rather than minting a twin.
1787
+ const existing = findTagByLabel(siteTags(prev), label);
1788
+ if (existing) return sectionId ? setSectionTagged(prev, sectionId, existing.id, true) : prev;
1789
+ return addTag(prev, id, label, sectionId);
1790
+ });
1791
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1792
+ }, [persistence]);
1793
+
1794
+ const handleRenameTag = useCallback((tagId: string, label: string) => {
1795
+ setSiteIndex((prev) => renameTag(prev, tagId, label));
1796
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1797
+ }, [persistence]);
1798
+
1799
+ const handleDeleteTag = useCallback((tagId: string) => {
1800
+ setSiteIndex((prev) => deleteTag(prev, tagId));
1801
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1802
+ }, [persistence]);
1803
+
1804
+ const handleSectionTagToggle = useCallback((sectionId: string, tagId: string, tagged: boolean) => {
1805
+ setSiteIndex((prev) => setSectionTagged(prev, sectionId, tagId, tagged));
1806
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1807
+ }, [persistence]);
1808
+
1809
+ const tags = useMemo(() => siteTags(siteIndex), [siteIndex]);
1810
+ // Absent `tags` means the registry was never loaded (a Dexie row or cache
1811
+ // entry from a build that predates tags) — NOT an empty registry. Editing it
1812
+ // would post `tags: [new]` and wholesale-replace the real tags.json, so
1813
+ // every tag edit is off until a load that read the sidecar. A degraded read
1814
+ // (tags.json held entries this version couldn't parse) is the same hazard:
1815
+ // saving would drop the entries it skipped.
1816
+ const tagsEditable = siteIndex.tags !== undefined && !tagsDegraded;
1817
+ const tagging = useMemo<SiteTagging>(() => ({
1818
+ tags, editable: tagsEditable,
1819
+ onCreate: handleCreateTag, onRename: handleRenameTag, onDelete: handleDeleteTag, onToggle: handleSectionTagToggle,
1820
+ }), [tags, tagsEditable, handleCreateTag, handleRenameTag, handleDeleteTag, handleSectionTagToggle]);
1821
+
1685
1822
  const handleSetHome = useCallback((id: string) => {
1686
1823
  setSiteIndex((prev) => setHomePage(prev, id));
1687
1824
  persistence.markIndexDirty(); setLocalChangesExist(true);
@@ -1902,7 +2039,7 @@ export default function EditorShell({
1902
2039
  return historyRestoreEvent.listen(async ({ sha }) => {
1903
2040
  try {
1904
2041
  const res = await fetch(`/api/content/history?sha=${sha}`);
1905
- if (!res.ok) throw new Error(`Failed to load version: ${res.status}`);
2042
+ if (!res.ok) throw new Error(`Couldn't load version: ${res.status}`);
1906
2043
  const data = await res.json();
1907
2044
  const ok = await handleHistoryRestore({
1908
2045
  sections: data.sections,
@@ -1933,7 +2070,7 @@ export default function EditorShell({
1933
2070
  return (
1934
2071
  <div className="flex min-h-[50vh] items-center justify-center">
1935
2072
  <div className="text-center">
1936
- <p className="text-sm text-red-600">{shellState.message}</p>
2073
+ <p className="text-sm text-danger">{shellState.message}</p>
1937
2074
  <Button variant="secondary" size="md" className="mt-3" onClick={() => window.location.reload()}>
1938
2075
  Retry
1939
2076
  </Button>
@@ -1963,15 +2100,21 @@ export default function EditorShell({
1963
2100
 
1964
2101
  return (
1965
2102
  <PagesContext.Provider value={pagesContextValue}>
2103
+ <ReferenceSourcesContext.Provider value={referenceSources}>
2104
+ <BrandSwatchesContext.Provider value={brandSwatches}>
1966
2105
  <EditorProvider>
1967
2106
  <ViewBranchWatcher
1968
- setViewSections={setViewSections}
1969
- onError={setRemoteNotice}
2107
+ setView={setView}
2108
+ onError={showRemoteNotice}
1970
2109
  />
1971
- <HistoryWatcher onHistoryActiveChange={setHistoryActive} />
2110
+ <HistoryWatcher />
1972
2111
  <EditorModalProvider>
1973
2112
  <MediaLibraryContext.Provider value={mediaLibraryValue}>
1974
2113
  <div className="editor-shell relative">
2114
+ {/* Announces every section/page/group reorder — from drag or the
2115
+ Move up/down buttons — since neither leaves a visible confirmation
2116
+ a screen reader user would otherwise get. */}
2117
+ <p aria-live="polite" className="sr-only">{reorderAnnouncement}</p>
1975
2118
  <EditorToolbar
1976
2119
  buttonState={buttonState}
1977
2120
  localChangesExist={localChangesExist}
@@ -2000,42 +2143,22 @@ export default function EditorShell({
2000
2143
  audiences={audiences}
2001
2144
  authoringDefaults={authoringDefaults}
2002
2145
  onAuthoringDefaultsChange={onAuthoringDefaultsChange}
2146
+ docEditEnabled={Boolean(resolvedActivePage?.document?.enabled)}
2147
+ onDocEditClick={() => setShowDocumentEditor(true)}
2148
+ // Bug reports persist via Supabase. On the password-only
2149
+ // (zero-Supabase) path there's no backend and the editor session
2150
+ // has userId: null, so the button would always 401 — hide it there.
2151
+ showBugReport={!capabilities.passwordOnly}
2003
2152
  />
2004
2153
 
2005
- {/* Bug reports persist via Supabase. On the password-only (zero-Supabase)
2006
- path there's no backend and the editor session has userId: null, so
2007
- the button would always 401 — hide it there. */}
2008
- {!capabilities.passwordOnly && <BugReportFAB />}
2009
-
2010
- {/* Document editor: opens the full-screen editing surface for the
2011
- active page when its document mode is enabled. Mirrors the viewer
2012
- document FAB's icon; sits above BugReportFAB (bottom-40 clears the
2013
- bottom-28 bug button + gap) so the two never collide.
2014
- !historyActive: history is presentation only, never editing — while
2015
- viewing a specific historical commit (historySelectEvent →
2016
- EditorContext.historyState, lifted here by HistoryWatcher since this
2017
- scope renders the provider and can't read the context directly),
2018
- hide the FAB so it can't open the draft editor over a history view. */}
2019
- {resolvedActivePage?.document?.enabled && !historyActive && (
2020
- <Fab label="Edit as document" column="editor" className="bottom-40" onClick={() => setShowDocumentEditor(true)}>
2021
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
2022
- <rect x="3" y="5" width="18" height="14" rx="1" />
2023
- <path d="M7 10h10" />
2024
- <path d="M7 14h7" />
2025
- </svg>
2026
- </Fab>
2027
- )}
2028
2154
  {/* Deck Builder (#34). Editors are super-admins over decks and are
2029
2155
  never gated by deckBuilder.audiences, so this is unconditional.
2030
2156
  An <a>, not a click handler: builder mode is its own route.
2031
- bottom-52 clears the bottom-40 document FAB + gap. Taking over the
2157
+ Standalone (not in EditorToolbar's FabStack): bottom-60 clears
2158
+ the stack's three members with room to spare. Taking over the
2032
2159
  editor top bar in place is #38 — this just goes there. */}
2033
- <Fab href="/decks?next=%2Fedit" label="Build a deck" column="editor" className="bottom-52">
2034
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
2035
- <rect x="3" y="4" width="18" height="12" rx="1" />
2036
- <path d="M12 16v4" />
2037
- <path d="M9 20h6" />
2038
- </svg>
2160
+ <Fab href="/decks?next=%2Fedit" label="Build a deck" column="editor" className="bottom-60">
2161
+ <Presentation size={20} aria-hidden="true" />
2039
2162
  </Fab>
2040
2163
  {showDocumentEditor && resolvedActivePage?.document?.enabled && (
2041
2164
  <Suspense fallback={<DocumentOverlayFallback />}>
@@ -2047,8 +2170,9 @@ export default function EditorShell({
2047
2170
  editor. NOTE: viewSections drives the "Published" branch preview
2048
2171
  only; the separate historySelectEvent commit view
2049
2172
  (EditorContext.historyState) is the OTHER read-only case — the
2050
- historyActive-gated FAB above is what keeps this overlay from
2051
- ever being opened onto it in the first place. */}
2173
+ document FAB in EditorToolbar is gated on !historyState, which
2174
+ keeps this overlay from ever being opened onto it in the
2175
+ first place. */}
2052
2176
  {viewSections !== null ? (
2053
2177
  <DocumentPresentOverlay
2054
2178
  page={resolvedActivePage}
@@ -2086,7 +2210,7 @@ export default function EditorShell({
2086
2210
 
2087
2211
  {rejectedUploads.length > 0 && (
2088
2212
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2089
- <UploadRejectionAlert
2213
+ <InlineAlert
2090
2214
  message={formatRejections(rejectedUploads, siteConfig?.media.maxFileSize)}
2091
2215
  onDismiss={() => setRejectedUploads([])}
2092
2216
  />
@@ -2100,7 +2224,7 @@ export default function EditorShell({
2100
2224
  never show the same message twice. */}
2101
2225
  {uploadErrorMessage && !showMediaLibrary && (
2102
2226
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2103
- <UploadRejectionAlert
2227
+ <InlineAlert
2104
2228
  message={uploadErrorMessage}
2105
2229
  onDismiss={() => setUploadErrorMessage(null)}
2106
2230
  />
@@ -2109,29 +2233,34 @@ export default function EditorShell({
2109
2233
 
2110
2234
  {unparseableSectionIds.length > 0 && (
2111
2235
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2112
- <UploadRejectionAlert
2236
+ <InlineAlert
2113
2237
  message={`${unparseableSectionIds.length} section file${unparseableSectionIds.length === 1 ? "" : "s"} on this site cannot be read (${unparseableSectionIds.join(", ")}). Damaged sections keep their place on the page and are not deleted by saving — repair the file to restore them.`}
2114
2238
  onDismiss={() => setUnparseableSectionIds([])}
2115
2239
  />
2116
2240
  </div>
2117
2241
  )}
2118
2242
 
2119
- {saveWarnings.length > 0 && (
2120
- <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2121
- <UploadRejectionAlert
2122
- message={saveWarnings.join(" ")}
2123
- onDismiss={() => setSaveWarnings([])}
2124
- />
2243
+ {/* One sticky container so the two banners stack instead of overlapping. */}
2244
+ {(persistence.persistenceError || saveWarnings.length > 0) && (
2245
+ <div className="sticky top-2 z-30 mx-auto flex w-full max-w-3xl flex-col gap-2 px-4">
2246
+ {persistence.persistenceError && (
2247
+ <Notice tone="error" placement="inline">
2248
+ {persistence.persistenceError}
2249
+ </Notice>
2250
+ )}
2251
+ {saveWarnings.length > 0 && (
2252
+ <InlineAlert
2253
+ message={saveWarnings.join(" ")}
2254
+ onDismiss={() => setSaveWarnings([])}
2255
+ />
2256
+ )}
2125
2257
  </div>
2126
2258
  )}
2127
2259
 
2128
2260
  {remoteNotice && (
2129
- <div
2130
- role="status"
2131
- className="fixed left-1/2 top-14 z-50 -translate-x-1/2 rounded-md border border-base-200 bg-base px-4 py-2 text-xs font-medium text-base-content shadow-lg"
2132
- >
2133
- {remoteNotice}
2134
- </div>
2261
+ <Notice tone={remoteNotice.tone} placement="toast">
2262
+ {remoteNotice.message}
2263
+ </Notice>
2135
2264
  )}
2136
2265
 
2137
2266
  {/* bp-band-shift: rides the chat dock's FLIP glide (base.css). On
@@ -2164,7 +2293,7 @@ export default function EditorShell({
2164
2293
  ) : (
2165
2294
  <HistoryOrEditorContent
2166
2295
  index={siteIndex}
2167
- activeSections={activeSections}
2296
+ activeSections={navSections}
2168
2297
  activePageId={resolvedActivePage?.id ?? null}
2169
2298
  >
2170
2299
  <EditorContent
@@ -2181,6 +2310,7 @@ export default function EditorShell({
2181
2310
  onBlockMove={onBlockMove}
2182
2311
  onMoveSection={siteIndex.pages.length > 1 ? setMovingSectionId : undefined}
2183
2312
  onAccessChange={onAccessChange}
2313
+ tagging={tagging}
2184
2314
  onStatusChange={onStatusChange}
2185
2315
  mainIndex={mainIndex}
2186
2316
  changedSectionIds={changedSectionIds}
@@ -2203,41 +2333,34 @@ export default function EditorShell({
2203
2333
  chatbotPlatformEnabled={chatbotPlatformEnabled}
2204
2334
  capabilities={capabilities}
2205
2335
  currentUser={currentUser}
2336
+ tagging={tagging}
2206
2337
  />
2207
- <EditorModal
2338
+ <ConfirmDialog
2208
2339
  isOpen={pendingDeleteSectionId !== null}
2209
- onClose={() => setPendingDeleteSectionId(null)}
2210
2340
  title="Delete Section"
2211
- >
2212
- <p className="mb-4 text-sm text-base-contrast-light">
2213
- Delete this section? It will be removed from the page now and deleted permanently when you save or publish.
2214
- </p>
2215
- <div className="flex justify-end gap-3">
2216
- <Button variant="secondary" size="md" onClick={() => setPendingDeleteSectionId(null)}>
2217
- Cancel
2218
- </Button>
2219
- <Button variant="destructive" size="md" onClick={confirmDeleteSection}>
2220
- Delete
2221
- </Button>
2222
- </div>
2223
- </EditorModal>
2224
- <EditorModal
2341
+ body={
2342
+ <p className="text-sm text-base-contrast-light">
2343
+ Delete this section? It will be removed from the page now and deleted permanently when you save or publish.
2344
+ </p>
2345
+ }
2346
+ confirmLabel="Delete"
2347
+ tone="destructive"
2348
+ onConfirm={confirmDeleteSection}
2349
+ onCancel={() => setPendingDeleteSectionId(null)}
2350
+ />
2351
+ <ConfirmDialog
2225
2352
  isOpen={showDiscardConfirm}
2226
- onClose={() => setShowDiscardConfirm(false)}
2227
2353
  title="Discard Changes"
2228
- >
2229
- <p className="mb-4 text-sm text-base-contrast-light">
2230
- Discard all unsaved changes? This will reload with the last published content.
2231
- </p>
2232
- <div className="flex justify-end gap-3">
2233
- <Button variant="secondary" size="md" onClick={() => setShowDiscardConfirm(false)}>
2234
- Cancel
2235
- </Button>
2236
- <Button variant="destructive" size="md" onClick={handleToolbarDiscard}>
2237
- Discard
2238
- </Button>
2239
- </div>
2240
- </EditorModal>
2354
+ body={
2355
+ <p className="text-sm text-base-contrast-light">
2356
+ Discard all unsaved changes? This will reload with the last published content.
2357
+ </p>
2358
+ }
2359
+ confirmLabel="Discard"
2360
+ tone="destructive"
2361
+ onConfirm={handleToolbarDiscard}
2362
+ onCancel={() => setShowDiscardConfirm(false)}
2363
+ />
2241
2364
  <EditorModal
2242
2365
  isOpen={showMediaLibrary}
2243
2366
  onClose={() => {
@@ -2357,16 +2480,18 @@ export default function EditorShell({
2357
2480
  </MediaLibraryContext.Provider>
2358
2481
  </EditorModalProvider>
2359
2482
  </EditorProvider>
2483
+ </BrandSwatchesContext.Provider>
2484
+ </ReferenceSourcesContext.Provider>
2360
2485
  </PagesContext.Provider>
2361
2486
  );
2362
2487
  }
2363
2488
 
2364
2489
  function ViewBranchWatcher({
2365
- setViewSections,
2490
+ setView,
2366
2491
  onError,
2367
2492
  }: {
2368
- setViewSections: (sections: LoadedSection[] | null) => void;
2369
- onError: (message: string) => void;
2493
+ setView: (view: { sections: LoadedSection[]; index: SiteIndex | null } | null) => void;
2494
+ onError: (message: string, tone?: NoticeState["tone"]) => void;
2370
2495
  }) {
2371
2496
  const { isEditMode, viewBranch } = useEditorContext();
2372
2497
 
@@ -2376,7 +2501,7 @@ function ViewBranchWatcher({
2376
2501
  // fetch (fetching `saved` would 404 after a publish and fall back to main,
2377
2502
  // clobbering unsaved edits). Null viewSections routes to the in-memory path.
2378
2503
  if (isEditMode || viewBranch !== "live") {
2379
- setViewSections(null);
2504
+ setView(null);
2380
2505
  return;
2381
2506
  }
2382
2507
 
@@ -2387,42 +2512,34 @@ function ViewBranchWatcher({
2387
2512
  .then((res) => {
2388
2513
  // A non-JSON 5xx used to parse to `undefined` and render an empty
2389
2514
  // "Published" view with nothing explaining it.
2390
- if (!res.ok) throw new Error(`Failed to load the published view: ${res.status}`);
2515
+ if (!res.ok) throw new Error(`Couldn't load the published view: ${res.status}`);
2391
2516
  return res.json();
2392
2517
  })
2393
2518
  .then((data) => {
2394
2519
  if (!Array.isArray(data?.sections)) throw new Error("Malformed published-view response");
2395
- setViewSections(data.sections);
2520
+ setView({ sections: data.sections, index: data.index ?? null });
2396
2521
  })
2397
2522
  .catch((err) => {
2398
2523
  if (err?.name === "AbortError") return;
2399
2524
  console.error(err);
2400
2525
  // Fall back to the in-memory path rather than showing an empty page.
2401
- setViewSections(null);
2402
- onError("Couldn't load the published view — showing your current edits.");
2526
+ setView(null);
2527
+ onError("Couldn't load the published view — showing your current edits.", "error");
2403
2528
  });
2404
2529
  return () => controller.abort();
2405
- }, [viewBranch, isEditMode, setViewSections, onError]);
2530
+ }, [viewBranch, isEditMode, setView, onError]);
2406
2531
 
2407
2532
  return null;
2408
2533
  }
2409
2534
 
2410
- function HistoryWatcher({
2411
- onHistoryActiveChange,
2412
- }: {
2413
- // Lifts historyState's active/inactive status to the shell body (mirrors
2414
- // ViewBranchWatcher's setViewSections lift) — the body renders the
2415
- // EditorProvider this component reads from, so it can't call
2416
- // useEditorContext() itself to get at historyState.
2417
- onHistoryActiveChange?: (active: boolean) => void;
2418
- }) {
2419
- const { historyState, setHistoryState } = useEditorContext();
2535
+ function HistoryWatcher() {
2536
+ const { setHistoryState } = useEditorContext();
2420
2537
 
2421
2538
  useEffect(() => {
2422
2539
  const unlisten = historySelectEvent.listen(async ({ sha, date }) => {
2423
2540
  try {
2424
2541
  const res = await fetch(`/api/content/history?sha=${sha}`);
2425
- if (!res.ok) throw new Error(`Failed to load history: ${res.status}`);
2542
+ if (!res.ok) throw new Error(`Couldn't load history: ${res.status}`);
2426
2543
  const data = await res.json();
2427
2544
  setHistoryState({
2428
2545
  sha,
@@ -2442,10 +2559,6 @@ function HistoryWatcher({
2442
2559
  return unlisten;
2443
2560
  }, [setHistoryState]);
2444
2561
 
2445
- useEffect(() => {
2446
- onHistoryActiveChange?.(historyState !== null);
2447
- }, [historyState, onHistoryActiveChange]);
2448
-
2449
2562
  return null;
2450
2563
  }
2451
2564
 
@@ -2556,6 +2669,7 @@ function EditorContent({
2556
2669
  onBlockMove,
2557
2670
  onMoveSection,
2558
2671
  onAccessChange,
2672
+ tagging,
2559
2673
  onStatusChange,
2560
2674
  mainIndex,
2561
2675
  changedSectionIds,
@@ -2577,6 +2691,7 @@ function EditorContent({
2577
2691
  onBlockMove: (move: BlockMove) => void;
2578
2692
  onMoveSection?: (sectionId: string) => void;
2579
2693
  onAccessChange: (sectionId: string, access: string[]) => void;
2694
+ tagging: SiteTagging;
2580
2695
  onStatusChange: (sectionId: string, status: "draft" | "live" | "archived") => void;
2581
2696
  mainIndex: SiteIndex | null;
2582
2697
  changedSectionIds: Set<string>;
@@ -2589,6 +2704,7 @@ function EditorContent({
2589
2704
  }) {
2590
2705
  const { isEditMode, viewBranch, previewAudience } = useEditorContext();
2591
2706
  const { openModal, closeModal } = useEditorModal();
2707
+ const referenceSources = useContext(ReferenceSourcesContext);
2592
2708
  const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
2593
2709
  const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
2594
2710
 
@@ -2606,6 +2722,10 @@ function EditorContent({
2606
2722
  viewSections,
2607
2723
  activePageSectionIds,
2608
2724
  });
2725
+ // TL;DR / Summary plates render their nav anchor from this. Derived from the
2726
+ // same list the sidebar nav is built from (not displaySections, which preview
2727
+ // modes filter), so a plate's id always matches the href its nav entry links to.
2728
+ const summaryAnchors = useMemo(() => summaryNavAnchors(sections), [sections]);
2609
2729
 
2610
2730
  useEffect(() => {
2611
2731
  return combine(
@@ -2651,157 +2771,174 @@ function EditorContent({
2651
2771
  // `.section-wrapper:first-child` zeroes the 2.25rem that every other section
2652
2772
  // reserves above itself for its hover chrome, so the first section's controls
2653
2773
  // would sit under the fixed toolbar. Re-reserve it on the list instead.
2654
- <div className="pt-9">
2655
- {displaySections.map(({ section, meta }, index) => {
2656
- const definition = getSection(section.type);
2657
- if (!definition) {
2774
+ <SummaryAnchorsContext.Provider value={summaryAnchors}>
2775
+ <div className="pt-9">
2776
+ {displaySections.map(({ section, meta }, index) => {
2777
+ const definition = getSection(section.type);
2778
+ if (!definition) {
2779
+ return (
2780
+ <div
2781
+ key={section.id}
2782
+ className="border border-warning bg-warning-bg px-4 py-3 text-sm text-warning"
2783
+ role="alert"
2784
+ >
2785
+ Unknown section type: <code className="font-mono font-semibold">{section.type}</code>
2786
+ </div>
2787
+ );
2788
+ }
2789
+ const Component = definition.component;
2790
+ // The viewer renders no wrapper for a Reference with nothing to show,
2791
+ // so neither does the preview — its spacing would otherwise remain.
2792
+ if (
2793
+ !isEditMode && section.type === REFERENCE_TYPE && referenceSources &&
2794
+ !referenceShowsInPreview(referenceSources, section.id, section.content as unknown as ReferenceContent, viewBranch)
2795
+ ) {
2796
+ return null;
2797
+ }
2798
+
2658
2799
  return (
2659
- <div
2660
- key={section.id}
2661
- className="border border-amber-400 bg-amber-50 px-4 py-3 text-sm text-amber-800 dark:border-amber-600 dark:bg-amber-900/30 dark:text-amber-300"
2662
- role="alert"
2663
- >
2664
- Unknown section type: <code className="font-mono font-semibold">{section.type}</code>
2665
- </div>
2666
- );
2667
- }
2668
- const Component = definition.component;
2669
-
2670
- return (
2671
- <Fragment key={section.id}>
2672
- {editingEnabled && pendingInsertIndex === index && (
2673
- <SectionSkeleton
2674
- types={typeOptions}
2675
- onSelect={(type) => {
2676
- onAddSection(index, type);
2677
- setPendingInsertIndex(null);
2678
- }}
2679
- onDismiss={dismissPendingInsert}
2680
- />
2681
- )}
2800
+ <Fragment key={section.id}>
2801
+ {editingEnabled && pendingInsertIndex === index && (
2802
+ <SectionSkeleton
2803
+ types={typeOptions}
2804
+ onSelect={(type) => {
2805
+ onAddSection(index, type);
2806
+ setPendingInsertIndex(null);
2807
+ }}
2808
+ onDismiss={dismissPendingInsert}
2809
+ />
2810
+ )}
2682
2811
 
2683
- <ErrorBoundary label={`${definition.label} (${section.type})`}>
2684
- <SectionLayout
2685
- type={section.type}
2686
- status={meta.status}
2687
- dimNonPublished={!isEditMode}
2688
- dimmed={previewAudience !== null && !audiencePasses(meta.access, previewAudience)}
2689
- >
2690
- <SectionWrapper
2691
- sectionId={section.id}
2692
- sectionType={section.type}
2812
+ <ErrorBoundary label={`${definition.label} (${section.type})`}>
2813
+ <SectionLayout
2814
+ type={section.type}
2693
2815
  status={meta.status}
2694
- dirty={dirtySectionIds.has(section.id)}
2695
- index={index}
2696
- isLast={index === displaySections.length - 1}
2697
- containerId={ROOT_CONTAINER_ID}
2698
- isContainerBlock={section.type === "container"}
2699
- definition={definition}
2700
- mainStatus={mainIndex?.sections[section.id]?.status ?? null}
2701
- contentDiffersFromMain={changedSectionIds.has(section.id) || dirtySectionIds.has(section.id)}
2702
- isLocalOnly={dirtySectionIds.has(section.id) && !changedSectionIds.has(section.id)}
2703
- lockedBy={locks.get(section.id) ? { email: locks.get(section.id)!.email } : null}
2704
- onTakeover={() => onTakeover(section.id)}
2705
- onEditIntent={getEditIntent(section.id)}
2706
- options={{
2707
- ...(section.content as Record<string, unknown>),
2708
- ...("options" in section ? (section.options as Record<string, unknown>) : {}),
2709
- }}
2710
- audiences={audiences}
2711
- access={meta.access}
2712
- onAccessChange={editingEnabled ? (a) => onAccessChange(section.id, a) : undefined}
2713
- onStatusChange={editingEnabled ? (s) => onStatusChange(section.id, s) : undefined}
2714
- onSectionChange={editingEnabled ? (settingsResult) => {
2715
- const result = settingsResult as Record<string, unknown>;
2716
- if (result && typeof result === "object" && "content" in result && "options" in result) {
2717
- const contentPatch = result.content as Record<string, unknown>;
2718
- const optionsPatch = result.options as Record<string, unknown>;
2719
- onSectionChange(section.id, {
2720
- ...section,
2721
- content: { ...section.content, ...contentPatch },
2722
- options: { ...("options" in section ? section.options : {}), ...optionsPatch },
2723
- } as SectionContent);
2724
- } else {
2725
- onSectionChange(section.id, {
2726
- ...section,
2727
- ...result,
2728
- } as SectionContent);
2729
- }
2730
- } : undefined}
2731
- onReorder={editingEnabled ? onReorderSections : undefined}
2732
- onMoveSection={editingEnabled && onMoveSection ? () => onMoveSection(section.id) : undefined}
2733
- onRequestInsert={editingEnabled ? (i) => setPendingInsertIndex(i) : undefined}
2734
- onDelete={editingEnabled ? () => onDeleteSection(section.id) : undefined}
2735
- onAddChild={
2736
- editingEnabled && section.type === "container"
2737
- ? () => {
2738
- openModal(
2739
- "Add block",
2740
- <SectionTypePicker
2741
- types={childInsertableTypes()}
2742
- onClose={closeModal}
2743
- onSelect={(type) => {
2744
- const def = getSection(type);
2745
- if (def) {
2746
- const id =
2747
- typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
2748
- ? crypto.randomUUID()
2749
- : `child-${Date.now()}`;
2750
- const child = { id, ...(def.defaults() as object) } as Section;
2751
- const containerSection: Section = {
2752
- id: section.id,
2753
- type: "container",
2754
- content: section.content as Record<string, unknown>,
2755
- };
2756
- const childCount =
2757
- (section.content as { children?: unknown[] }).children?.length ?? 0;
2758
- const next = insertChildAt(containerSection, child, childCount);
2759
- onSectionChange(section.id, {
2760
- ...section,
2761
- content: next.content,
2762
- } as SectionContent);
2763
- }
2764
- closeModal();
2765
- }}
2766
- />,
2767
- );
2768
- }
2769
- : undefined
2770
- }
2816
+ dimNonPublished={!isEditMode}
2817
+ dimmed={previewAudience !== null && !audiencePasses(meta.access, previewAudience)}
2771
2818
  >
2772
- <Component
2773
- content={section}
2774
- options={"options" in section ? (section.options as Record<string, unknown>) : undefined}
2775
- onChange={editingEnabled ? (newContent) => onSectionChange(section.id, newContent as SectionContent) : undefined}
2776
- isEditMode={isEditMode}
2777
- openModal={editingEnabled ? openModal : undefined}
2778
- // Immediate (no confirm modal): used when a colors section's
2779
- // last swatch is dragged out, auto-removing the empty section.
2780
- onDelete={editingEnabled ? () => onDeleteSectionImmediate(section.id) : undefined}
2781
- />
2782
- </SectionWrapper>
2783
- </SectionLayout>
2784
- </ErrorBoundary>
2785
- </Fragment>
2786
- );
2787
- })}
2788
-
2789
- {/* Trailing selector: shown when an insert was requested after the last
2790
- section, or always on an empty page — an empty page has no section
2791
- chrome to insert from, so the selector IS its default state (not
2792
- dismissible; there is nothing behind it). */}
2793
- {editingEnabled && (displaySections.length === 0 || pendingInsertIndex === displaySections.length) && (
2794
- <SectionSkeleton
2795
- types={typeOptions}
2796
- onSelect={(type) => {
2797
- onAddSection(displaySections.length, type);
2798
- setPendingInsertIndex(null);
2799
- }}
2800
- onDismiss={dismissPendingInsert}
2801
- />
2802
- )}
2819
+ <SectionWrapper
2820
+ sectionId={section.id}
2821
+ sectionType={section.type}
2822
+ status={meta.status}
2823
+ dirty={dirtySectionIds.has(section.id)}
2824
+ index={index}
2825
+ isLast={index === displaySections.length - 1}
2826
+ sectionCount={displaySections.length}
2827
+ containerId={ROOT_CONTAINER_ID}
2828
+ isContainerBlock={section.type === "container"}
2829
+ definition={definition}
2830
+ mainStatus={mainIndex?.sections[section.id]?.status ?? null}
2831
+ contentDiffersFromMain={changedSectionIds.has(section.id) || dirtySectionIds.has(section.id)}
2832
+ isLocalOnly={dirtySectionIds.has(section.id) && !changedSectionIds.has(section.id)}
2833
+ lockedBy={locks.get(section.id) ? { email: locks.get(section.id)!.email } : null}
2834
+ onTakeover={() => onTakeover(section.id)}
2835
+ onEditIntent={getEditIntent(section.id)}
2836
+ options={{
2837
+ ...(section.content as Record<string, unknown>),
2838
+ ...("options" in section ? (section.options as Record<string, unknown>) : {}),
2839
+ }}
2840
+ audiences={audiences}
2841
+ access={meta.access}
2842
+ onAccessChange={editingEnabled ? (a) => onAccessChange(section.id, a) : undefined}
2843
+ tagging={{
2844
+ tags: tagging.tags,
2845
+ assignedIds: tagging.tags.filter((t) => t.sectionIds.includes(section.id)).map((t) => t.id),
2846
+ onToggle: editingEnabled && tagging.editable ? (tagId, tagged) => tagging.onToggle(section.id, tagId, tagged) : undefined,
2847
+ onCreate: editingEnabled && tagging.editable ? (label) => tagging.onCreate(label, section.id) : undefined,
2848
+ }}
2849
+ onStatusChange={editingEnabled ? (s) => onStatusChange(section.id, s) : undefined}
2850
+ onSectionChange={editingEnabled ? (settingsResult) => {
2851
+ const result = settingsResult as Record<string, unknown>;
2852
+ if (result && typeof result === "object" && "content" in result && "options" in result) {
2853
+ const contentPatch = result.content as Record<string, unknown>;
2854
+ const optionsPatch = result.options as Record<string, unknown>;
2855
+ onSectionChange(section.id, {
2856
+ ...section,
2857
+ content: { ...section.content, ...contentPatch },
2858
+ options: { ...("options" in section ? section.options : {}), ...optionsPatch },
2859
+ } as SectionContent);
2860
+ } else {
2861
+ onSectionChange(section.id, {
2862
+ ...section,
2863
+ ...result,
2864
+ } as SectionContent);
2865
+ }
2866
+ } : undefined}
2867
+ onReorder={editingEnabled ? onReorderSections : undefined}
2868
+ onMoveSection={editingEnabled && onMoveSection ? () => onMoveSection(section.id) : undefined}
2869
+ onRequestInsert={editingEnabled ? (i) => setPendingInsertIndex(i) : undefined}
2870
+ onDelete={editingEnabled ? () => onDeleteSection(section.id) : undefined}
2871
+ onAddChild={
2872
+ editingEnabled && section.type === "container"
2873
+ ? () => {
2874
+ openModal(
2875
+ "Add block",
2876
+ <SectionTypePicker
2877
+ types={childInsertableTypes()}
2878
+ onClose={closeModal}
2879
+ onSelect={(type) => {
2880
+ const def = getSection(type);
2881
+ if (def) {
2882
+ const id =
2883
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
2884
+ ? crypto.randomUUID()
2885
+ : `child-${Date.now()}`;
2886
+ const child = { id, ...(def.defaults() as object) } as Section;
2887
+ const containerSection: Section = {
2888
+ id: section.id,
2889
+ type: "container",
2890
+ content: section.content as Record<string, unknown>,
2891
+ };
2892
+ const childCount =
2893
+ (section.content as { children?: unknown[] }).children?.length ?? 0;
2894
+ const next = insertChildAt(containerSection, child, childCount);
2895
+ onSectionChange(section.id, {
2896
+ ...section,
2897
+ content: next.content,
2898
+ } as SectionContent);
2899
+ }
2900
+ closeModal();
2901
+ }}
2902
+ />,
2903
+ );
2904
+ }
2905
+ : undefined
2906
+ }
2907
+ >
2908
+ <Component
2909
+ content={section}
2910
+ options={"options" in section ? (section.options as Record<string, unknown>) : undefined}
2911
+ onChange={editingEnabled ? (newContent) => onSectionChange(section.id, newContent as SectionContent) : undefined}
2912
+ isEditMode={isEditMode}
2913
+ openModal={editingEnabled ? openModal : undefined}
2914
+ // Immediate (no confirm modal): used when a colors section's
2915
+ // last swatch is dragged out, auto-removing the empty section.
2916
+ onDelete={editingEnabled ? () => onDeleteSectionImmediate(section.id) : undefined}
2917
+ />
2918
+ </SectionWrapper>
2919
+ </SectionLayout>
2920
+ </ErrorBoundary>
2921
+ </Fragment>
2922
+ );
2923
+ })}
2924
+
2925
+ {/* Trailing selector: shown when an insert was requested after the last
2926
+ section, or always on an empty page — an empty page has no section
2927
+ chrome to insert from, so the selector IS its default state (not
2928
+ dismissible; there is nothing behind it). */}
2929
+ {editingEnabled && (displaySections.length === 0 || pendingInsertIndex === displaySections.length) && (
2930
+ <SectionSkeleton
2931
+ types={typeOptions}
2932
+ onSelect={(type) => {
2933
+ onAddSection(displaySections.length, type);
2934
+ setPendingInsertIndex(null);
2935
+ }}
2936
+ onDismiss={dismissPendingInsert}
2937
+ />
2938
+ )}
2803
2939
 
2804
- </div>
2940
+ </div>
2941
+ </SummaryAnchorsContext.Provider>
2805
2942
  );
2806
2943
  }
2807
2944
 
@@ -2847,7 +2984,7 @@ function formatElapsed(seconds: number): string {
2847
2984
  function DismissButton({ onClick }: { onClick: () => void }) {
2848
2985
  return (
2849
2986
  <button type="button" onClick={onClick} className="cursor-pointer" aria-label="Dismiss">
2850
- <X size={12} className="text-base-content/30 hover:text-base-content/60" />
2987
+ <X size={12} className="text-base-contrast/30 hover:text-base-contrast/60" />
2851
2988
  </button>
2852
2989
  );
2853
2990
  }
@@ -2861,7 +2998,7 @@ function StatusText({
2861
2998
  onDismiss,
2862
2999
  }: {
2863
3000
  publishAction: "idle" | "saving" | "publishing";
2864
- publishFeedback: string | null;
3001
+ publishFeedback: NoticeState | null;
2865
3002
  saveProgress: string | null;
2866
3003
  buildState: "idle" | "building" | "ready" | "error" | "fading";
2867
3004
  buildElapsed: number;
@@ -2869,14 +3006,14 @@ function StatusText({
2869
3006
  }) {
2870
3007
  if (publishAction === "saving") {
2871
3008
  return (
2872
- <span className="relative text-xs font-medium text-base-content/60">
3009
+ <span className="relative text-xs font-medium text-base-contrast/60">
2873
3010
  {saveProgress ?? <>Saving changes<LoadingEllipsis /></>}
2874
3011
  </span>
2875
3012
  );
2876
3013
  }
2877
3014
  if (publishAction === "publishing" || buildState === "building") {
2878
3015
  return (
2879
- <span className="text-xs font-medium text-orange-600">
3016
+ <span className="text-xs font-medium text-warning">
2880
3017
  Publishing ({formatElapsed(buildElapsed)})
2881
3018
  </span>
2882
3019
  );
@@ -2884,7 +3021,7 @@ function StatusText({
2884
3021
  if (buildState === "ready" || buildState === "fading") {
2885
3022
  return (
2886
3023
  <span className={cn(
2887
- "inline-flex items-center gap-1.5 text-xs font-medium text-green-600 transition-opacity duration-1000",
3024
+ "inline-flex items-center gap-1.5 text-xs font-medium text-success transition-opacity duration-1000",
2888
3025
  buildState === "fading" ? "opacity-0" : "opacity-100",
2889
3026
  )}>
2890
3027
  Published in {formatElapsed(buildElapsed)}
@@ -2894,19 +3031,22 @@ function StatusText({
2894
3031
  }
2895
3032
  if (buildState === "error") {
2896
3033
  return (
2897
- <span className="inline-flex items-center gap-1.5 text-xs font-medium text-red-600">
3034
+ <span className="inline-flex items-center gap-1.5 text-xs font-medium text-danger">
2898
3035
  Publish failed
2899
3036
  <DismissButton onClick={onDismiss} />
2900
3037
  </span>
2901
3038
  );
2902
3039
  }
2903
3040
  if (publishFeedback) {
3041
+ const toneClass: Record<NoticeState["tone"], string> = {
3042
+ success: "text-success",
3043
+ info: "text-base-contrast",
3044
+ warning: "text-warning",
3045
+ error: "text-danger",
3046
+ };
2904
3047
  return (
2905
- <span className={cn(
2906
- "text-xs font-medium",
2907
- publishFeedback === "Saved" ? "text-green-600" : "text-red-600",
2908
- )}>
2909
- {publishFeedback}
3048
+ <span className={cn("text-xs font-medium", toneClass[publishFeedback.tone])}>
3049
+ {publishFeedback.message}
2910
3050
  </span>
2911
3051
  );
2912
3052
  }
@@ -2938,11 +3078,14 @@ function EditorToolbar({
2938
3078
  audiences,
2939
3079
  authoringDefaults,
2940
3080
  onAuthoringDefaultsChange,
3081
+ docEditEnabled,
3082
+ onDocEditClick,
3083
+ showBugReport,
2941
3084
  }: {
2942
3085
  buttonState: "synced" | "publish" | "saveAndPublish";
2943
3086
  localChangesExist: boolean;
2944
3087
  publishAction: "idle" | "saving" | "publishing";
2945
- publishFeedback: string | null;
3088
+ publishFeedback: NoticeState | null;
2946
3089
  saveProgress: string | null;
2947
3090
  onSave: () => void;
2948
3091
  onPublish: () => void;
@@ -2963,6 +3106,14 @@ function EditorToolbar({
2963
3106
  audiences: Audience[];
2964
3107
  authoringDefaults: AuthoringDefaults;
2965
3108
  onAuthoringDefaultsChange: (next: AuthoringDefaults) => void;
3109
+ /** Whether the active page has document mode on — gates the "Edit as
3110
+ * document" FAB this toolbar stacks alongside its own mode-toggle FAB.
3111
+ * Owned by EditorShell (resolvedActivePage lives above the Provider). */
3112
+ docEditEnabled: boolean;
3113
+ onDocEditClick: () => void;
3114
+ /** Whether to render BugReportFAB in this toolbar's FabStack — false on
3115
+ * the password-only (zero-Supabase) path, which has no backend for it. */
3116
+ showBugReport: boolean;
2966
3117
  }) {
2967
3118
  const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState, historyPageId, setHistoryPageId } = useEditorContext();
2968
3119
 
@@ -2983,13 +3134,13 @@ function EditorToolbar({
2983
3134
  return (
2984
3135
  <>
2985
3136
  {isEditMode && (
2986
- // Full-width and static on purpose: the docked chat sidebar slides in
2987
- // BELOW this bar (ChatPanel docks at top-16 on /edit), so the toolbar
2988
- // never moves during the chat open/close transition. h-16 matches the
2989
- // panel's top offset (and the body's pt-16) exactly.
2990
- <div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
2991
- <div className="mx-auto max-w-screen-xl grid h-16 grid-cols-3 items-center px-4">
2992
- <div className="flex items-center gap-2">
3137
+ // The docked chat sidebar slides in BELOW this bar (ChatPanel docks
3138
+ // at top-16 on /edit), so the toolbar never moves during the chat
3139
+ // open/close transition — ToolbarFrame's h-16 matches the panel's
3140
+ // top offset (and the body's pt-16) exactly.
3141
+ <ToolbarFrame
3142
+ left={
3143
+ <>
2993
3144
  {buttonState === "saveAndPublish" && (
2994
3145
  <SplitButton
2995
3146
  label="Save & Publish"
@@ -3024,18 +3175,20 @@ function EditorToolbar({
3024
3175
  </Button>
3025
3176
  )}
3026
3177
  <PresenceAvatars peers={collabPeers} selfUserId={collabSelfId} status={collabStatus} />
3027
- </div>
3028
- <div className="flex items-center justify-center">
3029
- <StatusText
3030
- publishAction={publishAction}
3031
- publishFeedback={publishFeedback}
3032
- saveProgress={saveProgress}
3033
- buildState={buildState}
3034
- buildElapsed={buildElapsed}
3035
- onDismiss={onBuildDismiss}
3036
- />
3037
- </div>
3038
- <div className="flex items-center justify-end gap-2">
3178
+ </>
3179
+ }
3180
+ center={
3181
+ <StatusText
3182
+ publishAction={publishAction}
3183
+ publishFeedback={publishFeedback}
3184
+ saveProgress={saveProgress}
3185
+ buildState={buildState}
3186
+ buildElapsed={buildElapsed}
3187
+ onDismiss={onBuildDismiss}
3188
+ />
3189
+ }
3190
+ right={
3191
+ <>
3039
3192
  <ProcessingIndicator items={processingItems} />
3040
3193
  <Tooltip side="bottom" content="Authoring defaults">
3041
3194
  <AuthoringDefaultsMenu defaults={authoringDefaults} audiences={audiences} onChange={onAuthoringDefaultsChange} />
@@ -3057,15 +3210,15 @@ function EditorToolbar({
3057
3210
  <ShowAllChromeToggle />
3058
3211
  </Tooltip>
3059
3212
  <Tooltip side="bottom-clamp" content="Site settings">
3060
- <IconButton icon={<SettingsIcon size={16} />} label="Site settings" size="md" onClick={onSettingsClick} className="border border-base-200 bg-base-accent" />
3213
+ <IconButton icon={<Settings size={16} />} label="Site settings" size="md" onClick={onSettingsClick} className="border border-base-200 bg-base-accent" />
3061
3214
  </Tooltip>
3062
- </div>
3063
- </div>
3064
- </div>
3215
+ </>
3216
+ }
3217
+ />
3065
3218
  )}
3066
3219
  {!isEditMode && (
3067
- <div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
3068
- <div className="mx-auto max-w-screen-xl flex h-16 items-center justify-center px-4">
3220
+ <ToolbarFrame
3221
+ center={
3069
3222
  <SegmentedControl
3070
3223
  options={[
3071
3224
  { value: "live", label: "Published" },
@@ -3074,27 +3227,37 @@ function EditorToolbar({
3074
3227
  value={viewBranch}
3075
3228
  onChange={(v) => setViewBranch(v as "saved" | "live")}
3076
3229
  />
3077
- </div>
3078
- </div>
3230
+ }
3231
+ />
3079
3232
  )}
3080
3233
  {!historyState && (
3081
- <Fab
3082
- label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
3083
- column="editor"
3084
- className="bottom-16"
3085
- onClick={toggleEditMode}
3086
- >
3087
- {isEditMode ? (
3088
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
3089
- <path d="M10 12a2 2 0 100-4 2 2 0 000 4z" />
3090
- <path fillRule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clipRule="evenodd" />
3091
- </svg>
3092
- ) : (
3093
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
3094
- <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
3095
- </svg>
3234
+ <FabStack>
3235
+ {/* Bug reports persist via Supabase; hidden on the password-only
3236
+ path (showBugReport, threaded from EditorShell's capabilities).
3237
+ History is presentation only, never editing (branch-preview
3238
+ already routes to DocumentPresentOverlay; !historyState covers
3239
+ the separate historySelectEvent commit view) — FabStack
3240
+ unmounts with the rest of this block whenever history is
3241
+ active, which hides all three FABs together. */}
3242
+ {showBugReport && <BugReportFAB stacked />}
3243
+ {docEditEnabled && (
3244
+ <Fab stacked label="Edit as document" column="editor" onClick={onDocEditClick}>
3245
+ <FileText size={20} aria-hidden="true" />
3246
+ </Fab>
3096
3247
  )}
3097
- </Fab>
3248
+ <Fab
3249
+ stacked
3250
+ label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
3251
+ column="editor"
3252
+ onClick={toggleEditMode}
3253
+ >
3254
+ {isEditMode ? (
3255
+ <Eye className="h-5 w-5" aria-hidden="true" />
3256
+ ) : (
3257
+ <Pencil className="h-5 w-5" aria-hidden="true" />
3258
+ )}
3259
+ </Fab>
3260
+ </FabStack>
3098
3261
  )}
3099
3262
  </>
3100
3263
  );