@drawnagency/primitives 0.1.113 → 0.1.114

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 (483) 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-GPPVYLEI.js → chunk-45FA7Z55.js} +1 -1
  6. package/dist/{chunk-REDDTTGJ.js → chunk-4Y2JQQZL.js} +19 -1
  7. package/dist/chunk-5GFACGSN.js +6 -0
  8. package/dist/chunk-5KSAS4VY.js +275 -0
  9. package/dist/{chunk-Z7W3DQTR.js → chunk-7SAAJVDE.js} +408 -28
  10. package/dist/{chunk-S2L3BPLS.js → chunk-7SX42OP4.js} +1 -1
  11. package/dist/{chunk-GGXZZ77Z.js → chunk-AASEGXNT.js} +18 -1
  12. package/dist/{chunk-WKZ7OENC.js → chunk-BQ5TZZUE.js} +5 -1
  13. package/dist/chunk-DIMDRBFK.js +11 -0
  14. package/dist/{chunk-WUJ3XNFI.js → chunk-GGYY3JQJ.js} +19 -24
  15. package/dist/{chunk-NHN2LSIU.js → chunk-IP2SEBAJ.js} +5 -1
  16. package/dist/{chunk-IINIZBQB.js → chunk-JCDZAFX2.js} +30 -10
  17. package/dist/chunk-JR24U7FJ.js +120 -0
  18. package/dist/{chunk-SW6PCQQD.js → chunk-KQA772WQ.js} +2 -1
  19. package/dist/{chunk-KR2MHTNJ.js → chunk-N3G7DOU3.js} +609 -61
  20. package/dist/chunk-OAGJBCSH.js +67 -0
  21. package/dist/{chunk-6BZ2KNTP.js → chunk-PLANHXS7.js} +191 -61
  22. package/dist/{chunk-VWWY5TL5.js → chunk-PNSCDBU7.js} +38 -56
  23. package/dist/{chunk-AHALSCQH.js → chunk-PTY2RG2K.js} +1 -1
  24. package/dist/{chunk-BDFTMGCL.js → chunk-PZPGJZRG.js} +9 -8
  25. package/dist/{chunk-FRE2CCLV.js → chunk-QHMKIODQ.js} +17 -4
  26. package/dist/{chunk-HBULBDGP.js → chunk-S37DZPHW.js} +1 -1
  27. package/dist/{chunk-4I5OO5UB.js → chunk-TEOAKGZA.js} +0 -1
  28. package/dist/chunk-VUYCRBJK.js +174 -0
  29. package/dist/{chunk-4RNF2DRG.js → chunk-XDEPXTBY.js} +0 -2
  30. package/dist/{chunk-DS4GCYV2.js → chunk-Y5QECJX7.js} +223 -16
  31. package/dist/collab/rebase.d.ts +2 -1
  32. package/dist/collab/rebase.d.ts.map +1 -1
  33. package/dist/collab/rebase.js +20 -5
  34. package/dist/components/brandguide/ColorsEditable.d.ts.map +1 -1
  35. package/dist/components/chat/ChatPanel.d.ts.map +1 -1
  36. package/dist/components/chat/ChatWidget.d.ts.map +1 -1
  37. package/dist/components/docfile/DocFilePane.d.ts.map +1 -1
  38. package/dist/components/editor/ChildBlockWrapper.d.ts.map +1 -1
  39. package/dist/components/editor/ConflictDialog.d.ts.map +1 -1
  40. package/dist/components/editor/DragHandle.d.ts +12 -2
  41. package/dist/components/editor/DragHandle.d.ts.map +1 -1
  42. package/dist/components/editor/DropEdgeIndicator.d.ts +15 -6
  43. package/dist/components/editor/DropEdgeIndicator.d.ts.map +1 -1
  44. package/dist/components/editor/NavGroupRow.d.ts +4 -1
  45. package/dist/components/editor/NavGroupRow.d.ts.map +1 -1
  46. package/dist/components/editor/PagesModal.d.ts.map +1 -1
  47. package/dist/components/editor/SectionWrapper.d.ts +1 -1
  48. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  49. package/dist/components/editor/SettingsForm.d.ts.map +1 -1
  50. package/dist/components/editor/TagIndicator.d.ts +15 -0
  51. package/dist/components/editor/TagIndicator.d.ts.map +1 -0
  52. package/dist/components/editor/index.js +148 -77
  53. package/dist/components/primitives/DocumentDropZone.d.ts +1 -1
  54. package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
  55. package/dist/components/primitives/EditableList.d.ts.map +1 -1
  56. package/dist/components/primitives/HeadingSection.d.ts +4 -1
  57. package/dist/components/primitives/HeadingSection.d.ts.map +1 -1
  58. package/dist/components/primitives/ResolvedMedia.d.ts +5 -1
  59. package/dist/components/primitives/ResolvedMedia.d.ts.map +1 -1
  60. package/dist/components/primitives/{defineHeadingSection.d.ts → define-heading-section.d.ts} +1 -1
  61. package/dist/components/primitives/define-heading-section.d.ts.map +1 -0
  62. package/dist/components/primitives/{gridDrop.d.ts → grid-drop.d.ts} +1 -1
  63. package/dist/components/primitives/grid-drop.d.ts.map +1 -0
  64. package/dist/components/primitives/index.d.ts +1 -1
  65. package/dist/components/primitives/index.d.ts.map +1 -1
  66. package/dist/components/primitives/index.js +17 -154
  67. package/dist/components/sections/Button/CTAButton.d.ts.map +1 -1
  68. package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts +2 -6
  69. package/dist/components/sections/DoDontMedia/DoDontMediaBlock.d.ts.map +1 -1
  70. package/dist/components/sections/DoDontMedia/index.d.ts +8 -0
  71. package/dist/components/sections/DoDontMedia/index.d.ts.map +1 -1
  72. package/dist/components/sections/IconList/IconList.d.ts +2 -1
  73. package/dist/components/sections/IconList/IconList.d.ts.map +1 -1
  74. package/dist/components/sections/IconList/IconListSettings.d.ts +2 -1
  75. package/dist/components/sections/IconList/IconListSettings.d.ts.map +1 -1
  76. package/dist/components/sections/IconList/index.d.ts +1 -0
  77. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  78. package/dist/components/sections/Media/MediaBlock.d.ts +2 -1
  79. package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -1
  80. package/dist/components/sections/Media/index.d.ts +8 -0
  81. package/dist/components/sections/Media/index.d.ts.map +1 -1
  82. package/dist/components/sections/Reference/ReferenceList.d.ts +35 -0
  83. package/dist/components/sections/Reference/ReferenceList.d.ts.map +1 -0
  84. package/dist/components/sections/Reference/ReferenceSettings.d.ts +12 -0
  85. package/dist/components/sections/Reference/ReferenceSettings.d.ts.map +1 -0
  86. package/dist/components/sections/Reference/index.d.ts +9 -0
  87. package/dist/components/sections/Reference/index.d.ts.map +1 -0
  88. package/dist/components/sections/SectionSummary/SectionSummaryBlock.d.ts +38 -0
  89. package/dist/components/sections/SectionSummary/SectionSummaryBlock.d.ts.map +1 -0
  90. package/dist/components/sections/SectionSummary/SectionSummarySettings.d.ts +20 -0
  91. package/dist/components/sections/SectionSummary/SectionSummarySettings.d.ts.map +1 -0
  92. package/dist/components/sections/SectionSummary/index.d.ts +33 -0
  93. package/dist/components/sections/SectionSummary/index.d.ts.map +1 -0
  94. package/dist/components/sections/all-sections.d.ts +53 -0
  95. package/dist/components/sections/all-sections.d.ts.map +1 -1
  96. package/dist/components/sections/brand-guide.d.ts +41 -2
  97. package/dist/components/sections/brand-guide.d.ts.map +1 -1
  98. package/dist/components/sections/brand-guide.js +12 -10
  99. package/dist/components/sections/core-sections.d.ts +14 -0
  100. package/dist/components/sections/core-sections.d.ts.map +1 -1
  101. package/dist/components/sections/register-schemas.js +19 -17
  102. package/dist/components/sections/register.js +19 -17
  103. package/dist/components/shared/BrandSwatchesContext.d.ts +9 -0
  104. package/dist/components/shared/BrandSwatchesContext.d.ts.map +1 -0
  105. package/dist/components/shared/Button.d.ts +11 -3
  106. package/dist/components/shared/Button.d.ts.map +1 -1
  107. package/dist/components/shared/Checkbox.d.ts +6 -0
  108. package/dist/components/shared/Checkbox.d.ts.map +1 -1
  109. package/dist/components/shared/ConfirmDialog.d.ts +25 -0
  110. package/dist/components/shared/ConfirmDialog.d.ts.map +1 -0
  111. package/dist/components/shared/ConfirmInline.d.ts +25 -0
  112. package/dist/components/shared/ConfirmInline.d.ts.map +1 -0
  113. package/dist/components/shared/ErrorBoundary.d.ts.map +1 -1
  114. package/dist/components/shared/Fab.d.ts +11 -2
  115. package/dist/components/shared/Fab.d.ts.map +1 -1
  116. package/dist/components/shared/FabStack.d.ts +22 -0
  117. package/dist/components/shared/FabStack.d.ts.map +1 -0
  118. package/dist/components/shared/HistoryChangesModal.d.ts.map +1 -1
  119. package/dist/components/shared/HistoryPopover.d.ts.map +1 -1
  120. package/dist/components/shared/ImageBackgroundField.d.ts +9 -0
  121. package/dist/components/shared/ImageBackgroundField.d.ts.map +1 -0
  122. package/dist/components/shared/{UploadRejectionAlert.d.ts → InlineAlert.d.ts} +4 -2
  123. package/dist/components/shared/InlineAlert.d.ts.map +1 -0
  124. package/dist/components/shared/Input.d.ts +10 -0
  125. package/dist/components/shared/Input.d.ts.map +1 -1
  126. package/dist/components/shared/Navigation.d.ts.map +1 -1
  127. package/dist/components/shared/Notice.d.ts +10 -0
  128. package/dist/components/shared/Notice.d.ts.map +1 -0
  129. package/dist/components/shared/Popover.d.ts.map +1 -1
  130. package/dist/components/shared/ReferenceSourcesContext.d.ts +25 -0
  131. package/dist/components/shared/ReferenceSourcesContext.d.ts.map +1 -0
  132. package/dist/components/shared/Select.d.ts +3 -2
  133. package/dist/components/shared/Select.d.ts.map +1 -1
  134. package/dist/components/shared/SplitButton.d.ts.map +1 -1
  135. package/dist/components/shared/SummaryAnchorsContext.d.ts +10 -0
  136. package/dist/components/shared/SummaryAnchorsContext.d.ts.map +1 -0
  137. package/dist/components/shared/Textarea.d.ts +7 -0
  138. package/dist/components/shared/Textarea.d.ts.map +1 -1
  139. package/dist/components/shared/ToolbarFrame.d.ts +17 -0
  140. package/dist/components/shared/ToolbarFrame.d.ts.map +1 -0
  141. package/dist/components/shared/copy.d.ts +13 -0
  142. package/dist/components/shared/copy.d.ts.map +1 -0
  143. package/dist/components/shared/icons.d.ts +0 -12
  144. package/dist/components/shared/icons.d.ts.map +1 -1
  145. package/dist/components/shell/AudienceDetailsPanel.d.ts +7 -1
  146. package/dist/components/shell/AudienceDetailsPanel.d.ts.map +1 -1
  147. package/dist/components/shell/AudienceRow.d.ts.map +1 -1
  148. package/dist/components/shell/BugReportFAB.d.ts +7 -1
  149. package/dist/components/shell/BugReportFAB.d.ts.map +1 -1
  150. package/dist/components/shell/CredentialList.d.ts.map +1 -1
  151. package/dist/components/shell/DocInspector.d.ts.map +1 -1
  152. package/dist/components/shell/DocumentEditorOverlay.d.ts.map +1 -1
  153. package/dist/components/shell/EditorContext.d.ts +4 -0
  154. package/dist/components/shell/EditorContext.d.ts.map +1 -1
  155. package/dist/components/shell/EditorLoginForm.d.ts.map +1 -1
  156. package/dist/components/shell/EditorModal.d.ts.map +1 -1
  157. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  158. package/dist/components/shell/HistoryToolbar.d.ts.map +1 -1
  159. package/dist/components/shell/MediaFolderTile.d.ts +4 -1
  160. package/dist/components/shell/MediaFolderTile.d.ts.map +1 -1
  161. package/dist/components/shell/MediaLibraryModal.d.ts.map +1 -1
  162. package/dist/components/shell/RestoreModal.d.ts.map +1 -1
  163. package/dist/components/shell/SiteSettingsChatbot.d.ts.map +1 -1
  164. package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
  165. package/dist/components/shell/SiteSettingsModal.d.ts +3 -1
  166. package/dist/components/shell/SiteSettingsModal.d.ts.map +1 -1
  167. package/dist/components/shell/SiteSettingsTags.d.ts +13 -0
  168. package/dist/components/shell/SiteSettingsTags.d.ts.map +1 -0
  169. package/dist/components/shell/SiteSettingsUsers.d.ts.map +1 -1
  170. package/dist/components/shell/ViewerLoginForm.d.ts.map +1 -1
  171. package/dist/components/shell/{blockMoveDispatch.d.ts → block-move-dispatch.d.ts} +1 -1
  172. package/dist/components/shell/block-move-dispatch.d.ts.map +1 -0
  173. package/dist/components/shell/reference-sources.d.ts +22 -0
  174. package/dist/components/shell/reference-sources.d.ts.map +1 -0
  175. package/dist/deck/index.d.ts +2 -2
  176. package/dist/deck/index.d.ts.map +1 -1
  177. package/dist/deck/index.js +1 -3
  178. package/dist/deck-builder/index.js +11 -18
  179. package/dist/document/index.d.ts +2 -2
  180. package/dist/document/index.d.ts.map +1 -1
  181. package/dist/hooks/useBlockDnd.d.ts +11 -0
  182. package/dist/hooks/useBlockDnd.d.ts.map +1 -1
  183. package/dist/hooks/useBuildStatus.d.ts +1 -1
  184. package/dist/hooks/useBuildStatus.d.ts.map +1 -1
  185. package/dist/hooks/useDocumentUploads.d.ts +1 -1
  186. package/dist/hooks/useDocumentUploads.d.ts.map +1 -1
  187. package/dist/hooks/useEditorPersistence.d.ts +5 -1
  188. package/dist/hooks/useEditorPersistence.d.ts.map +1 -1
  189. package/dist/hooks/useEditorPublish.d.ts +2 -2
  190. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  191. package/dist/hooks/useNotice.d.ts +19 -0
  192. package/dist/hooks/useNotice.d.ts.map +1 -0
  193. package/dist/hooks/useSortableNavRow.d.ts +11 -0
  194. package/dist/hooks/useSortableNavRow.d.ts.map +1 -1
  195. package/dist/hooks/useSortableRow.d.ts +11 -0
  196. package/dist/hooks/useSortableRow.d.ts.map +1 -1
  197. package/dist/hooks/useStackedEscape.d.ts +32 -0
  198. package/dist/hooks/useStackedEscape.d.ts.map +1 -0
  199. package/dist/index.js +67 -49
  200. package/dist/lib/audience-secret.js +2 -2
  201. package/dist/lib/block-dnd.d.ts +5 -0
  202. package/dist/lib/block-dnd.d.ts.map +1 -1
  203. package/dist/lib/block-move.d.ts.map +1 -1
  204. package/dist/lib/brand-swatches.d.ts +12 -0
  205. package/dist/lib/brand-swatches.d.ts.map +1 -0
  206. package/dist/lib/container-ops.d.ts +3 -2
  207. package/dist/lib/container-ops.d.ts.map +1 -1
  208. package/dist/lib/dexie.d.ts.map +1 -1
  209. package/dist/lib/dexie.js +9 -1
  210. package/dist/lib/env.d.ts +4 -0
  211. package/dist/lib/env.d.ts.map +1 -1
  212. package/dist/lib/env.js +5 -3
  213. package/dist/lib/events.d.ts +1 -2
  214. package/dist/lib/events.d.ts.map +1 -1
  215. package/dist/lib/image-background.d.ts +8 -0
  216. package/dist/lib/image-background.d.ts.map +1 -0
  217. package/dist/lib/index-ops.js +3 -3
  218. package/dist/lib/index.d.ts +6 -4
  219. package/dist/lib/index.d.ts.map +1 -1
  220. package/dist/lib/index.js +29 -21
  221. package/dist/lib/loader.d.ts +21 -7
  222. package/dist/lib/loader.d.ts.map +1 -1
  223. package/dist/lib/nav-tree.d.ts +1 -0
  224. package/dist/lib/nav-tree.d.ts.map +1 -1
  225. package/dist/lib/nav.d.ts +38 -0
  226. package/dist/lib/nav.d.ts.map +1 -1
  227. package/dist/lib/platform-broker.d.ts +4 -2
  228. package/dist/lib/platform-broker.d.ts.map +1 -1
  229. package/dist/lib/platform-broker.js +21 -13
  230. package/dist/lib/reference-type.d.ts +2 -0
  231. package/dist/lib/reference-type.d.ts.map +1 -0
  232. package/dist/lib/reference-validation.d.ts +31 -0
  233. package/dist/lib/reference-validation.d.ts.map +1 -0
  234. package/dist/lib/reference-validation.js +39 -0
  235. package/dist/lib/references.d.ts +74 -0
  236. package/dist/lib/references.d.ts.map +1 -0
  237. package/dist/lib/registry.d.ts +35 -0
  238. package/dist/lib/registry.d.ts.map +1 -1
  239. package/dist/lib/{safeRedirect.d.ts → safe-redirect.d.ts} +1 -1
  240. package/dist/lib/safe-redirect.d.ts.map +1 -0
  241. package/dist/lib/tags.d.ts +58 -0
  242. package/dist/lib/tags.d.ts.map +1 -0
  243. package/dist/lib/theme-vars.d.ts +9 -0
  244. package/dist/lib/theme-vars.d.ts.map +1 -1
  245. package/dist/media/index.d.ts +1 -1
  246. package/dist/media/index.d.ts.map +1 -1
  247. package/dist/media/index.js +8 -4
  248. package/dist/media/utils.d.ts +2 -0
  249. package/dist/media/utils.d.ts.map +1 -1
  250. package/dist/media/{videoPoster.d.ts → video-poster.d.ts} +1 -1
  251. package/dist/media/video-poster.d.ts.map +1 -0
  252. package/dist/schemas/audience.js +2 -2
  253. package/dist/schemas/index.d.ts +1 -0
  254. package/dist/schemas/index.d.ts.map +1 -1
  255. package/dist/schemas/index.js +14 -8
  256. package/dist/schemas/shared.d.ts +26 -0
  257. package/dist/schemas/shared.d.ts.map +1 -1
  258. package/dist/schemas/site-config.d.ts +6 -0
  259. package/dist/schemas/site-config.d.ts.map +1 -1
  260. package/dist/schemas/tags.d.ts +27 -0
  261. package/dist/schemas/tags.d.ts.map +1 -0
  262. package/dist/storage/index.js +1 -1
  263. package/dist/types/database.d.ts.map +1 -1
  264. package/package.json +6 -1
  265. package/src/auth/cookies.ts +111 -0
  266. package/src/collab/rebase.ts +26 -2
  267. package/src/components/brandguide/ColorSwatchSettings.tsx +1 -1
  268. package/src/components/brandguide/ColorsEditable.tsx +1 -0
  269. package/src/components/chat/ChatPanel.tsx +2 -3
  270. package/src/components/chat/ChatWidget.tsx +2 -3
  271. package/src/components/docfile/DocFilePane.tsx +4 -12
  272. package/src/components/editor/ChildBlockWrapper.tsx +13 -19
  273. package/src/components/editor/ConflictDialog.tsx +27 -35
  274. package/src/components/editor/DeleteButton.tsx +2 -2
  275. package/src/components/editor/DragHandle.tsx +16 -3
  276. package/src/components/editor/DropEdgeIndicator.tsx +36 -10
  277. package/src/components/editor/InsertButton.tsx +2 -2
  278. package/src/components/editor/NavGroupRow.tsx +43 -14
  279. package/src/components/editor/PagesModal.tsx +99 -26
  280. package/src/components/editor/SectionWrapper.tsx +64 -23
  281. package/src/components/editor/SettingsButton.tsx +2 -2
  282. package/src/components/editor/SettingsForm.tsx +19 -2
  283. package/src/components/editor/StatusIndicator.tsx +4 -4
  284. package/src/components/editor/TagIndicator.tsx +80 -0
  285. package/src/components/primitives/DocumentDropZone.tsx +1 -1
  286. package/src/components/primitives/EditableGrid.tsx +15 -14
  287. package/src/components/primitives/EditableList.tsx +27 -74
  288. package/src/components/primitives/HeadingSection.tsx +5 -2
  289. package/src/components/primitives/ResolvedMedia.tsx +13 -0
  290. package/src/components/primitives/{defineHeadingSection.tsx → define-heading-section.tsx} +1 -0
  291. package/src/components/primitives/index.ts +1 -1
  292. package/src/components/primitives/useEditablePlainText.ts +5 -5
  293. package/src/components/sections/Button/CTAButton.tsx +3 -1
  294. package/src/components/sections/DoDontMedia/DoDontMediaBlock.tsx +4 -4
  295. package/src/components/sections/DoDontMedia/index.tsx +12 -3
  296. package/src/components/sections/IconList/DoDontToggle.tsx +2 -2
  297. package/src/components/sections/IconList/IconList.tsx +70 -37
  298. package/src/components/sections/IconList/IconListSettings.tsx +55 -17
  299. package/src/components/sections/IconList/index.tsx +10 -3
  300. package/src/components/sections/LinkHeading/index.tsx +1 -1
  301. package/src/components/sections/Media/MediaBlock.tsx +13 -2
  302. package/src/components/sections/Media/index.tsx +12 -4
  303. package/src/components/sections/Reference/ReferenceList.tsx +214 -0
  304. package/src/components/sections/Reference/ReferenceSettings.tsx +159 -0
  305. package/src/components/sections/Reference/index.tsx +47 -0
  306. package/src/components/sections/SectionSummary/SectionSummaryBlock.tsx +476 -0
  307. package/src/components/sections/SectionSummary/SectionSummarySettings.tsx +126 -0
  308. package/src/components/sections/SectionSummary/index.tsx +149 -0
  309. package/src/components/sections/SubHeading/index.tsx +1 -1
  310. package/src/components/sections/SubSubHeading/index.tsx +1 -1
  311. package/src/components/sections/brand-guide.ts +4 -3
  312. package/src/components/sections/core-sections.ts +2 -0
  313. package/src/components/shared/BrandSwatchesContext.ts +13 -0
  314. package/src/components/shared/Button.tsx +47 -34
  315. package/src/components/shared/Checkbox.tsx +21 -12
  316. package/src/components/shared/ConfirmDialog.tsx +87 -0
  317. package/src/components/shared/ConfirmInline.tsx +90 -0
  318. package/src/components/shared/ErrorBoundary.tsx +6 -18
  319. package/src/components/shared/Fab.tsx +15 -5
  320. package/src/components/shared/FabStack.tsx +28 -0
  321. package/src/components/shared/HistoryChangesModal.tsx +43 -41
  322. package/src/components/shared/HistoryPopover.tsx +7 -20
  323. package/src/components/shared/ImageBackgroundField.tsx +92 -0
  324. package/src/components/shared/{UploadRejectionAlert.tsx → InlineAlert.tsx} +21 -5
  325. package/src/components/shared/Input.tsx +23 -7
  326. package/src/components/shared/Navigation.tsx +22 -17
  327. package/src/components/shared/Notice.tsx +59 -0
  328. package/src/components/shared/Popover.tsx +7 -11
  329. package/src/components/shared/ReferenceSourcesContext.ts +27 -0
  330. package/src/components/shared/Select.tsx +5 -2
  331. package/src/components/shared/SplitButton.tsx +11 -17
  332. package/src/components/shared/SummaryAnchorsContext.ts +11 -0
  333. package/src/components/shared/Textarea.tsx +17 -2
  334. package/src/components/shared/ToolbarFrame.tsx +28 -0
  335. package/src/components/shared/VideoSourceField.tsx +1 -1
  336. package/src/components/shared/copy.ts +18 -0
  337. package/src/components/shared/icons.tsx +1 -17
  338. package/src/components/shell/AudienceAddForm.tsx +2 -2
  339. package/src/components/shell/AudienceDetailsPanel.tsx +23 -5
  340. package/src/components/shell/AudienceRow.tsx +22 -15
  341. package/src/components/shell/BugReportFAB.tsx +50 -40
  342. package/src/components/shell/CredentialList.tsx +33 -13
  343. package/src/components/shell/DocInspector.tsx +3 -12
  344. package/src/components/shell/DocumentEditorOverlay.tsx +33 -23
  345. package/src/components/shell/EditorContext.tsx +7 -0
  346. package/src/components/shell/EditorLoginForm.tsx +26 -36
  347. package/src/components/shell/EditorModal.tsx +8 -10
  348. package/src/components/shell/EditorShell.tsx +499 -339
  349. package/src/components/shell/HistoryToolbar.tsx +24 -26
  350. package/src/components/shell/MediaFolderTile.tsx +8 -3
  351. package/src/components/shell/MediaLibraryModal.tsx +59 -60
  352. package/src/components/shell/ProcessingIndicator.tsx +2 -2
  353. package/src/components/shell/RestoreModal.tsx +17 -16
  354. package/src/components/shell/SetPasswordForm.tsx +3 -3
  355. package/src/components/shell/SiteSettingsChatbot.tsx +61 -42
  356. package/src/components/shell/SiteSettingsDisplay.tsx +10 -20
  357. package/src/components/shell/SiteSettingsModal.tsx +24 -2
  358. package/src/components/shell/SiteSettingsTags.tsx +143 -0
  359. package/src/components/shell/SiteSettingsUsers.tsx +36 -26
  360. package/src/components/shell/SiteSettingsViewerAccess.tsx +2 -2
  361. package/src/components/shell/ViewerLoginForm.tsx +5 -4
  362. package/src/components/shell/reference-sources.ts +36 -0
  363. package/src/deck/index.ts +2 -2
  364. package/src/document/index.ts +2 -2
  365. package/src/hooks/useBlockDnd.ts +11 -0
  366. package/src/hooks/useBuildStatus.ts +53 -6
  367. package/src/hooks/useDocumentUploads.ts +1 -1
  368. package/src/hooks/useEditorPersistence.ts +141 -35
  369. package/src/hooks/useEditorPublish.ts +18 -27
  370. package/src/hooks/useMediaPipeline.ts +1 -1
  371. package/src/hooks/useNotice.ts +45 -0
  372. package/src/hooks/useSortableNavRow.ts +11 -0
  373. package/src/hooks/useSortableRow.ts +22 -2
  374. package/src/hooks/useStackedEscape.ts +62 -0
  375. package/src/lib/block-dnd.ts +6 -1
  376. package/src/lib/block-move.ts +3 -0
  377. package/src/lib/brand-swatches.ts +32 -0
  378. package/src/lib/container-ops.ts +5 -3
  379. package/src/lib/dexie.ts +15 -1
  380. package/src/lib/env.ts +9 -0
  381. package/src/lib/events.ts +1 -2
  382. package/src/lib/image-background.ts +31 -0
  383. package/src/lib/index.ts +5 -4
  384. package/src/lib/loader.ts +54 -55
  385. package/src/lib/nav-tree.ts +71 -0
  386. package/src/lib/nav.ts +196 -36
  387. package/src/lib/platform-broker.ts +32 -14
  388. package/src/lib/reference-type.ts +3 -0
  389. package/src/lib/reference-validation.ts +69 -0
  390. package/src/lib/references.ts +130 -0
  391. package/src/lib/registry.ts +37 -0
  392. package/src/lib/tags.ts +148 -0
  393. package/src/lib/theme-vars.ts +23 -0
  394. package/src/media/index.ts +1 -1
  395. package/src/media/utils.ts +18 -0
  396. package/src/media/{videoPoster.ts → video-poster.ts} +2 -2
  397. package/src/schemas/index.ts +1 -0
  398. package/src/schemas/shared.ts +24 -1
  399. package/src/schemas/site-config.ts +6 -1
  400. package/src/schemas/tags.ts +33 -0
  401. package/src/storage/types.ts +1 -1
  402. package/src/types/database.ts +2 -0
  403. package/dist/chunk-DNY72JZN.js +0 -86
  404. package/dist/chunk-F2LYQPXV.js +0 -77
  405. package/dist/chunk-JH762VUH.js +0 -128
  406. package/dist/chunk-K7DYZXVU.js +0 -138
  407. package/dist/components/primitives/defineHeadingSection.d.ts.map +0 -1
  408. package/dist/components/primitives/gridDrop.d.ts.map +0 -1
  409. package/dist/components/shared/UploadRejectionAlert.d.ts.map +0 -1
  410. package/dist/components/shell/BuildStatusIndicator.d.ts +0 -7
  411. package/dist/components/shell/BuildStatusIndicator.d.ts.map +0 -1
  412. package/dist/components/shell/blockMoveDispatch.d.ts.map +0 -1
  413. package/dist/hooks/index.d.ts +0 -9
  414. package/dist/hooks/index.d.ts.map +0 -1
  415. package/dist/lib/grid.d.ts +0 -2
  416. package/dist/lib/grid.d.ts.map +0 -1
  417. package/dist/lib/safeRedirect.d.ts.map +0 -1
  418. package/dist/media/videoPoster.d.ts.map +0 -1
  419. package/src/components/brandguide/ColorSwatchSettings.d.ts.map +0 -1
  420. package/src/components/brandguide/Colors.d.ts.map +0 -1
  421. package/src/components/primitives/CustomParagraph.d.ts.map +0 -1
  422. package/src/components/primitives/EditableGrid.d.ts.map +0 -1
  423. package/src/components/primitives/EditablePlainText.d.ts.map +0 -1
  424. package/src/components/primitives/EditableRichText.d.ts.map +0 -1
  425. package/src/components/primitives/HeadingSection.d.ts.map +0 -1
  426. package/src/components/primitives/IconPicker.d.ts.map +0 -1
  427. package/src/components/primitives/LinkPopover.d.ts.map +0 -1
  428. package/src/components/primitives/MediaSettingsForms.d.ts.map +0 -1
  429. package/src/components/primitives/ResolvedMedia.d.ts.map +0 -1
  430. package/src/components/primitives/RichTextToolbar.d.ts.map +0 -1
  431. package/src/components/primitives/tiptap-presets.d.ts.map +0 -1
  432. package/src/components/primitives/useEditableCollection.d.ts.map +0 -1
  433. package/src/components/primitives/useEditablePlainText.d.ts.map +0 -1
  434. package/src/components/primitives/useEditableRichText.d.ts.map +0 -1
  435. package/src/components/sections/Button/CTAButton.d.ts.map +0 -1
  436. package/src/components/sections/Button/index.d.ts.map +0 -1
  437. package/src/components/sections/Colors/index.d.ts.map +0 -1
  438. package/src/components/sections/IconList/IconList.d.ts.map +0 -1
  439. package/src/components/sections/IconList/IconListSettings.d.ts.map +0 -1
  440. package/src/components/sections/IconList/index.d.ts.map +0 -1
  441. package/src/components/sections/LinkHeading/index.d.ts.map +0 -1
  442. package/src/components/sections/Prose/Prose.d.ts.map +0 -1
  443. package/src/components/sections/Prose/index.d.ts.map +0 -1
  444. package/src/components/sections/SubHeading/index.d.ts.map +0 -1
  445. package/src/components/sections/SubSubHeading/index.d.ts.map +0 -1
  446. package/src/components/sections/register-schemas.d.ts.map +0 -1
  447. package/src/components/shared/Button.d.ts.map +0 -1
  448. package/src/components/shared/Checkbox.d.ts.map +0 -1
  449. package/src/components/shared/FormLabel.d.ts.map +0 -1
  450. package/src/components/shared/IconButton.d.ts.map +0 -1
  451. package/src/components/shared/Input.d.ts.map +0 -1
  452. package/src/components/shared/Select.d.ts.map +0 -1
  453. package/src/components/shared/icons.d.ts.map +0 -1
  454. package/src/components/shell/BuildStatusIndicator.tsx +0 -59
  455. package/src/components/shell/MediaLibraryContext.d.ts.map +0 -1
  456. package/src/hooks/index.ts +0 -8
  457. package/src/hooks/useResolvedMedia.d.ts.map +0 -1
  458. package/src/lib/cn.d.ts.map +0 -1
  459. package/src/lib/grid.d.ts.map +0 -1
  460. package/src/lib/grid.ts +0 -7
  461. package/src/lib/icons.d.ts.map +0 -1
  462. package/src/lib/loader.d.ts.map +0 -1
  463. package/src/lib/nav.d.ts.map +0 -1
  464. package/src/lib/registry.d.ts.map +0 -1
  465. package/src/lib/sanitize.d.ts.map +0 -1
  466. package/src/media/github.d.ts.map +0 -1
  467. package/src/media/index.d.ts.map +0 -1
  468. package/src/media/queue.d.ts.map +0 -1
  469. package/src/media/resolve.d.ts.map +0 -1
  470. package/src/media/types.d.ts.map +0 -1
  471. package/src/media/utils.d.ts.map +0 -1
  472. package/src/media/videoPoster.d.ts.map +0 -1
  473. package/src/schemas/auth.d.ts.map +0 -1
  474. package/src/schemas/media-grid-options.d.ts.map +0 -1
  475. package/src/schemas/media.d.ts.map +0 -1
  476. package/src/schemas/sections.d.ts.map +0 -1
  477. package/src/schemas/shared.d.ts.map +0 -1
  478. package/src/schemas/site-config.d.ts.map +0 -1
  479. /package/dist/{chunk-B7VBSE5X.js → chunk-3JDTXOIG.js} +0 -0
  480. /package/dist/{chunk-CXZ5YJG4.js → chunk-L45X7DKX.js} +0 -0
  481. /package/src/components/primitives/{gridDrop.ts → grid-drop.ts} +0 -0
  482. /package/src/components/shell/{blockMoveDispatch.ts → block-move-dispatch.ts} +0 -0
  483. /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
 
@@ -1085,7 +1128,7 @@ export default function EditorShell({
1085
1128
  useEffect(() => {
1086
1129
  if (!resolvedActivePage) return;
1087
1130
  const nav = generateSiteNav({
1088
- index: siteIndex, activeSections,
1131
+ index: siteIndex, activeSections: navSections,
1089
1132
  // The Audience Details surface is not a page: clearing the active id drops
1090
1133
  // the previous page's highlight (and its heading tree), so the sidebar
1091
1134
  // never shows you in two places at once.
@@ -1094,7 +1137,7 @@ export default function EditorShell({
1094
1137
  documents: siteDocuments ?? [],
1095
1138
  });
1096
1139
  siteNavChangeEvent.dispatch(nav);
1097
- }, [siteIndex, activeSections, resolvedActivePage, siteDocuments, showAudienceDetails]);
1140
+ }, [siteIndex, navSections, resolvedActivePage, siteDocuments, showAudienceDetails]);
1098
1141
 
1099
1142
  const goToPage = useCallback((pageId: string) => {
1100
1143
  const page = pageById(siteIndexRef.current, pageId);
@@ -1112,6 +1155,30 @@ export default function EditorShell({
1112
1155
 
1113
1156
  // Nav island → shell page selection.
1114
1157
  useEffect(() => pageSelectEvent.listen((id) => { setShowAudienceDetails(false); goToPage(id); }), [goToPage]);
1158
+
1159
+ // A Reference's "Edit source": open the source's page, then scroll to it once
1160
+ // that page's sections have rendered.
1161
+ const [scrollToSectionId, setScrollToSectionId] = useState<string | null>(null);
1162
+ const jumpToSection = useCallback((sectionId: string) => {
1163
+ const page = siteIndexRef.current.pages.find((p) => p.order.includes(sectionId));
1164
+ if (!page) return;
1165
+ pageSelectEvent.dispatch(page.id);
1166
+ setScrollToSectionId(sectionId);
1167
+ }, []);
1168
+ // One attempt, cleared either way: a section the canvas isn't showing (a
1169
+ // draft in the published preview) must not leave a scroll armed for later.
1170
+ useEffect(() => {
1171
+ if (!scrollToSectionId) return;
1172
+ document.querySelector(`[data-section-id="${CSS.escape(scrollToSectionId)}"]`)
1173
+ ?.scrollIntoView({ behavior: "smooth", block: "center" });
1174
+ setScrollToSectionId(null);
1175
+ }, [scrollToSectionId, activeSections]);
1176
+
1177
+ const referenceSources = useMemo<ReferenceSources>(() => publishedReferenceSources({
1178
+ viewSections, viewIndex, mainIndex, activePageId: resolvedActivePage?.id ?? null, audiences, mediaManifest, jumpToSection,
1179
+ }) ?? { index: siteIndex, sections, audiences, mediaManifest, copies: referenceCopies, jumpToSection },
1180
+ [viewSections, viewIndex, mainIndex, resolvedActivePage, siteIndex, sections, audiences, mediaManifest, referenceCopies, jumpToSection]);
1181
+
1115
1182
  useEffect(() => audienceDetailsEvent.listen(() => setShowAudienceDetails(true)), []);
1116
1183
 
1117
1184
  // Fetched on open rather than at mount: most editing sessions never look at
@@ -1251,8 +1318,11 @@ export default function EditorShell({
1251
1318
  loadedIndex = data.index;
1252
1319
  loadedConfig = SiteConfigSchema.parse(data.siteConfig);
1253
1320
  if (data.mediaManifest) loadedManifest = data.mediaManifest;
1321
+ // A degraded tags read is cached as "not loaded", so a later cache-hit
1322
+ // load stays read-only for tags instead of trusting the partial list.
1323
+ const { tags: _partialTags, ...untaggedIndex } = data.index;
1254
1324
  await cacheContent(
1255
- data.sha, data.sections, data.index, data.siteConfig,
1325
+ data.sha, data.sections, data.tagsDegraded ? untaggedIndex : data.index, data.siteConfig,
1256
1326
  { media: loadedManifest, documents: data.documentManifest ?? {} },
1257
1327
  {
1258
1328
  savedBranchSha: data.savedBranchSha,
@@ -1269,6 +1339,7 @@ export default function EditorShell({
1269
1339
  setChangedSectionIds(new Set(data.changedSectionIds));
1270
1340
  setMainIndex(data.mainIndex);
1271
1341
  setUnparseableSectionIds(data.unparseableSectionIds);
1342
+ setTagsDegraded(data.tagsDegraded);
1272
1343
  }
1273
1344
 
1274
1345
  if (cancelled) return;
@@ -1321,7 +1392,7 @@ export default function EditorShell({
1321
1392
 
1322
1393
  loadContent().catch((err) => {
1323
1394
  console.error("Failed to load editor content:", err);
1324
- setShellState({ phase: "error", message: err instanceof Error ? err.message : "Failed to load content" });
1395
+ setShellState({ phase: "error", message: err instanceof Error ? err.message : "Couldn't load content" });
1325
1396
  });
1326
1397
  return () => { cancelled = true; };
1327
1398
  }, [headSha, draftHeadSha, siteId, applySiteConfigPreview, fetchSavedContent]);
@@ -1354,7 +1425,12 @@ export default function EditorShell({
1354
1425
  return null;
1355
1426
  }).filter((s): s is LoadedSection => s !== null),
1356
1427
  );
1357
- setSiteIndex(restoredIndex);
1428
+ // A row written before tags shipped carries none, and that build could
1429
+ // not have edited them — keep the freshly fetched registry rather than
1430
+ // installing an index that reads as "tags never loaded".
1431
+ setSiteIndex(restoredIndex.tags === undefined && siteIndexRef.current.tags !== undefined
1432
+ ? { ...restoredIndex, tags: siteIndexRef.current.tags }
1433
+ : restoredIndex);
1358
1434
  // Pending deletions come from the snapshot's own persisted intent, and are
1359
1435
  // intersected with what the branch still has so an already-applied deletion
1360
1436
  // can't be replayed.
@@ -1470,6 +1546,11 @@ export default function EditorShell({
1470
1546
  );
1471
1547
 
1472
1548
  const handleDiscard = useCallback(async () => {
1549
+ // Drop Site Settings' debounced config write too, or its trailing edge
1550
+ // would write the discarded config back into Dexie.
1551
+ if (configFlushTimerRef.current) { clearTimeout(configFlushTimerRef.current); configFlushTimerRef.current = null; }
1552
+ pendingConfigRef.current = null;
1553
+ await persistence.discardPending();
1473
1554
  await discardLocalChanges();
1474
1555
  setLocalChangesExist(false);
1475
1556
  setDirtySectionIds(new Set());
@@ -1614,6 +1695,9 @@ export default function EditorShell({
1614
1695
  setSiteIndex((prev) => reorderVisibleSectionInPage(prev, resolvedActivePage.id, visibleIds, fromIndex, toIndex));
1615
1696
  persistence.markIndexDirty();
1616
1697
  setLocalChangesExist(true);
1698
+ const movedType = activeSections[fromIndex]?.section.type;
1699
+ const label = (movedType && getSection(movedType)?.label) || "Section";
1700
+ setReorderAnnouncement(`Moved ${label} to position ${toIndex + 1} of ${visibleIds.length}`);
1617
1701
  },
1618
1702
  [persistence, resolvedActivePage, activeSections],
1619
1703
  );
@@ -1655,8 +1739,16 @@ export default function EditorShell({
1655
1739
  }, [persistence]);
1656
1740
 
1657
1741
  const handleMoveNavEntry = useCallback((from: NavLocation, to: NavLocation) => {
1658
- setSiteIndex((prev) => moveNavEntry(prev, from, to));
1742
+ // Read the pre-move index from the ref (mirrors onBlockMove's getState
1743
+ // pattern) rather than the updater's `prev`, so describeNavMove can
1744
+ // resolve `from`'s label/kind against the SAME snapshot moveNavEntry acts
1745
+ // on, and the two calls can't observe different states.
1746
+ const prevIndex = siteIndexRef.current;
1747
+ const nextIndex = moveNavEntry(prevIndex, from, to);
1748
+ setSiteIndex(() => nextIndex);
1659
1749
  persistence.markIndexDirty(); setLocalChangesExist(true);
1750
+ const message = describeNavMove(prevIndex, nextIndex, from);
1751
+ if (message) setReorderAnnouncement(message);
1660
1752
  }, [persistence]);
1661
1753
 
1662
1754
  const handleAddGroup = useCallback(() => {
@@ -1682,6 +1774,48 @@ export default function EditorShell({
1682
1774
  persistence.markIndexDirty(); setLocalChangesExist(true);
1683
1775
  }, [persistence]);
1684
1776
 
1777
+ // Section tags: the same index-state edit + markIndexDirty as the nav group
1778
+ // handlers above. Every save then carries the runtime `tags`, which
1779
+ // content-ops writes to the tags.json sidecar.
1780
+ const handleCreateTag = useCallback((label: string, sectionId?: string) => {
1781
+ const id = typeof crypto !== "undefined" && crypto.randomUUID ? `tag_${crypto.randomUUID()}` : `tag_${Date.now()}`;
1782
+ setSiteIndex((prev) => {
1783
+ // Typing an existing tag's name reuses it rather than minting a twin.
1784
+ const existing = findTagByLabel(siteTags(prev), label);
1785
+ if (existing) return sectionId ? setSectionTagged(prev, sectionId, existing.id, true) : prev;
1786
+ return addTag(prev, id, label, sectionId);
1787
+ });
1788
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1789
+ }, [persistence]);
1790
+
1791
+ const handleRenameTag = useCallback((tagId: string, label: string) => {
1792
+ setSiteIndex((prev) => renameTag(prev, tagId, label));
1793
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1794
+ }, [persistence]);
1795
+
1796
+ const handleDeleteTag = useCallback((tagId: string) => {
1797
+ setSiteIndex((prev) => deleteTag(prev, tagId));
1798
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1799
+ }, [persistence]);
1800
+
1801
+ const handleSectionTagToggle = useCallback((sectionId: string, tagId: string, tagged: boolean) => {
1802
+ setSiteIndex((prev) => setSectionTagged(prev, sectionId, tagId, tagged));
1803
+ persistence.markIndexDirty(); setLocalChangesExist(true);
1804
+ }, [persistence]);
1805
+
1806
+ const tags = useMemo(() => siteTags(siteIndex), [siteIndex]);
1807
+ // Absent `tags` means the registry was never loaded (a Dexie row or cache
1808
+ // entry from a build that predates tags) — NOT an empty registry. Editing it
1809
+ // would post `tags: [new]` and wholesale-replace the real tags.json, so
1810
+ // every tag edit is off until a load that read the sidecar. A degraded read
1811
+ // (tags.json held entries this version couldn't parse) is the same hazard:
1812
+ // saving would drop the entries it skipped.
1813
+ const tagsEditable = siteIndex.tags !== undefined && !tagsDegraded;
1814
+ const tagging = useMemo<SiteTagging>(() => ({
1815
+ tags, editable: tagsEditable,
1816
+ onCreate: handleCreateTag, onRename: handleRenameTag, onDelete: handleDeleteTag, onToggle: handleSectionTagToggle,
1817
+ }), [tags, tagsEditable, handleCreateTag, handleRenameTag, handleDeleteTag, handleSectionTagToggle]);
1818
+
1685
1819
  const handleSetHome = useCallback((id: string) => {
1686
1820
  setSiteIndex((prev) => setHomePage(prev, id));
1687
1821
  persistence.markIndexDirty(); setLocalChangesExist(true);
@@ -1902,7 +2036,7 @@ export default function EditorShell({
1902
2036
  return historyRestoreEvent.listen(async ({ sha }) => {
1903
2037
  try {
1904
2038
  const res = await fetch(`/api/content/history?sha=${sha}`);
1905
- if (!res.ok) throw new Error(`Failed to load version: ${res.status}`);
2039
+ if (!res.ok) throw new Error(`Couldn't load version: ${res.status}`);
1906
2040
  const data = await res.json();
1907
2041
  const ok = await handleHistoryRestore({
1908
2042
  sections: data.sections,
@@ -1933,7 +2067,7 @@ export default function EditorShell({
1933
2067
  return (
1934
2068
  <div className="flex min-h-[50vh] items-center justify-center">
1935
2069
  <div className="text-center">
1936
- <p className="text-sm text-red-600">{shellState.message}</p>
2070
+ <p className="text-sm text-danger">{shellState.message}</p>
1937
2071
  <Button variant="secondary" size="md" className="mt-3" onClick={() => window.location.reload()}>
1938
2072
  Retry
1939
2073
  </Button>
@@ -1963,15 +2097,21 @@ export default function EditorShell({
1963
2097
 
1964
2098
  return (
1965
2099
  <PagesContext.Provider value={pagesContextValue}>
2100
+ <ReferenceSourcesContext.Provider value={referenceSources}>
2101
+ <BrandSwatchesContext.Provider value={brandSwatches}>
1966
2102
  <EditorProvider>
1967
2103
  <ViewBranchWatcher
1968
- setViewSections={setViewSections}
1969
- onError={setRemoteNotice}
2104
+ setView={setView}
2105
+ onError={showRemoteNotice}
1970
2106
  />
1971
- <HistoryWatcher onHistoryActiveChange={setHistoryActive} />
2107
+ <HistoryWatcher />
1972
2108
  <EditorModalProvider>
1973
2109
  <MediaLibraryContext.Provider value={mediaLibraryValue}>
1974
2110
  <div className="editor-shell relative">
2111
+ {/* Announces every section/page/group reorder — from drag or the
2112
+ Move up/down buttons — since neither leaves a visible confirmation
2113
+ a screen reader user would otherwise get. */}
2114
+ <p aria-live="polite" className="sr-only">{reorderAnnouncement}</p>
1975
2115
  <EditorToolbar
1976
2116
  buttonState={buttonState}
1977
2117
  localChangesExist={localChangesExist}
@@ -2000,42 +2140,22 @@ export default function EditorShell({
2000
2140
  audiences={audiences}
2001
2141
  authoringDefaults={authoringDefaults}
2002
2142
  onAuthoringDefaultsChange={onAuthoringDefaultsChange}
2143
+ docEditEnabled={Boolean(resolvedActivePage?.document?.enabled)}
2144
+ onDocEditClick={() => setShowDocumentEditor(true)}
2145
+ // Bug reports persist via Supabase. On the password-only
2146
+ // (zero-Supabase) path there's no backend and the editor session
2147
+ // has userId: null, so the button would always 401 — hide it there.
2148
+ showBugReport={!capabilities.passwordOnly}
2003
2149
  />
2004
2150
 
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
2151
  {/* Deck Builder (#34). Editors are super-admins over decks and are
2029
2152
  never gated by deckBuilder.audiences, so this is unconditional.
2030
2153
  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
2154
+ Standalone (not in EditorToolbar's FabStack): bottom-60 clears
2155
+ the stack's three members with room to spare. Taking over the
2032
2156
  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>
2157
+ <Fab href="/decks?next=%2Fedit" label="Build a deck" column="editor" className="bottom-60">
2158
+ <Presentation size={20} aria-hidden="true" />
2039
2159
  </Fab>
2040
2160
  {showDocumentEditor && resolvedActivePage?.document?.enabled && (
2041
2161
  <Suspense fallback={<DocumentOverlayFallback />}>
@@ -2047,8 +2167,9 @@ export default function EditorShell({
2047
2167
  editor. NOTE: viewSections drives the "Published" branch preview
2048
2168
  only; the separate historySelectEvent commit view
2049
2169
  (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. */}
2170
+ document FAB in EditorToolbar is gated on !historyState, which
2171
+ keeps this overlay from ever being opened onto it in the
2172
+ first place. */}
2052
2173
  {viewSections !== null ? (
2053
2174
  <DocumentPresentOverlay
2054
2175
  page={resolvedActivePage}
@@ -2086,7 +2207,7 @@ export default function EditorShell({
2086
2207
 
2087
2208
  {rejectedUploads.length > 0 && (
2088
2209
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2089
- <UploadRejectionAlert
2210
+ <InlineAlert
2090
2211
  message={formatRejections(rejectedUploads, siteConfig?.media.maxFileSize)}
2091
2212
  onDismiss={() => setRejectedUploads([])}
2092
2213
  />
@@ -2100,7 +2221,7 @@ export default function EditorShell({
2100
2221
  never show the same message twice. */}
2101
2222
  {uploadErrorMessage && !showMediaLibrary && (
2102
2223
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2103
- <UploadRejectionAlert
2224
+ <InlineAlert
2104
2225
  message={uploadErrorMessage}
2105
2226
  onDismiss={() => setUploadErrorMessage(null)}
2106
2227
  />
@@ -2109,29 +2230,34 @@ export default function EditorShell({
2109
2230
 
2110
2231
  {unparseableSectionIds.length > 0 && (
2111
2232
  <div className="sticky top-2 z-30 mx-auto w-full max-w-3xl px-4">
2112
- <UploadRejectionAlert
2233
+ <InlineAlert
2113
2234
  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
2235
  onDismiss={() => setUnparseableSectionIds([])}
2115
2236
  />
2116
2237
  </div>
2117
2238
  )}
2118
2239
 
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
- />
2240
+ {/* One sticky container so the two banners stack instead of overlapping. */}
2241
+ {(persistence.persistenceError || saveWarnings.length > 0) && (
2242
+ <div className="sticky top-2 z-30 mx-auto flex w-full max-w-3xl flex-col gap-2 px-4">
2243
+ {persistence.persistenceError && (
2244
+ <Notice tone="error" placement="inline">
2245
+ {persistence.persistenceError}
2246
+ </Notice>
2247
+ )}
2248
+ {saveWarnings.length > 0 && (
2249
+ <InlineAlert
2250
+ message={saveWarnings.join(" ")}
2251
+ onDismiss={() => setSaveWarnings([])}
2252
+ />
2253
+ )}
2125
2254
  </div>
2126
2255
  )}
2127
2256
 
2128
2257
  {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>
2258
+ <Notice tone={remoteNotice.tone} placement="toast">
2259
+ {remoteNotice.message}
2260
+ </Notice>
2135
2261
  )}
2136
2262
 
2137
2263
  {/* bp-band-shift: rides the chat dock's FLIP glide (base.css). On
@@ -2164,7 +2290,7 @@ export default function EditorShell({
2164
2290
  ) : (
2165
2291
  <HistoryOrEditorContent
2166
2292
  index={siteIndex}
2167
- activeSections={activeSections}
2293
+ activeSections={navSections}
2168
2294
  activePageId={resolvedActivePage?.id ?? null}
2169
2295
  >
2170
2296
  <EditorContent
@@ -2181,6 +2307,7 @@ export default function EditorShell({
2181
2307
  onBlockMove={onBlockMove}
2182
2308
  onMoveSection={siteIndex.pages.length > 1 ? setMovingSectionId : undefined}
2183
2309
  onAccessChange={onAccessChange}
2310
+ tagging={tagging}
2184
2311
  onStatusChange={onStatusChange}
2185
2312
  mainIndex={mainIndex}
2186
2313
  changedSectionIds={changedSectionIds}
@@ -2203,41 +2330,34 @@ export default function EditorShell({
2203
2330
  chatbotPlatformEnabled={chatbotPlatformEnabled}
2204
2331
  capabilities={capabilities}
2205
2332
  currentUser={currentUser}
2333
+ tagging={tagging}
2206
2334
  />
2207
- <EditorModal
2335
+ <ConfirmDialog
2208
2336
  isOpen={pendingDeleteSectionId !== null}
2209
- onClose={() => setPendingDeleteSectionId(null)}
2210
2337
  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
2338
+ body={
2339
+ <p className="text-sm text-base-contrast-light">
2340
+ Delete this section? It will be removed from the page now and deleted permanently when you save or publish.
2341
+ </p>
2342
+ }
2343
+ confirmLabel="Delete"
2344
+ tone="destructive"
2345
+ onConfirm={confirmDeleteSection}
2346
+ onCancel={() => setPendingDeleteSectionId(null)}
2347
+ />
2348
+ <ConfirmDialog
2225
2349
  isOpen={showDiscardConfirm}
2226
- onClose={() => setShowDiscardConfirm(false)}
2227
2350
  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>
2351
+ body={
2352
+ <p className="text-sm text-base-contrast-light">
2353
+ Discard all unsaved changes? This will reload with the last published content.
2354
+ </p>
2355
+ }
2356
+ confirmLabel="Discard"
2357
+ tone="destructive"
2358
+ onConfirm={handleToolbarDiscard}
2359
+ onCancel={() => setShowDiscardConfirm(false)}
2360
+ />
2241
2361
  <EditorModal
2242
2362
  isOpen={showMediaLibrary}
2243
2363
  onClose={() => {
@@ -2357,16 +2477,18 @@ export default function EditorShell({
2357
2477
  </MediaLibraryContext.Provider>
2358
2478
  </EditorModalProvider>
2359
2479
  </EditorProvider>
2480
+ </BrandSwatchesContext.Provider>
2481
+ </ReferenceSourcesContext.Provider>
2360
2482
  </PagesContext.Provider>
2361
2483
  );
2362
2484
  }
2363
2485
 
2364
2486
  function ViewBranchWatcher({
2365
- setViewSections,
2487
+ setView,
2366
2488
  onError,
2367
2489
  }: {
2368
- setViewSections: (sections: LoadedSection[] | null) => void;
2369
- onError: (message: string) => void;
2490
+ setView: (view: { sections: LoadedSection[]; index: SiteIndex | null } | null) => void;
2491
+ onError: (message: string, tone?: NoticeState["tone"]) => void;
2370
2492
  }) {
2371
2493
  const { isEditMode, viewBranch } = useEditorContext();
2372
2494
 
@@ -2376,7 +2498,7 @@ function ViewBranchWatcher({
2376
2498
  // fetch (fetching `saved` would 404 after a publish and fall back to main,
2377
2499
  // clobbering unsaved edits). Null viewSections routes to the in-memory path.
2378
2500
  if (isEditMode || viewBranch !== "live") {
2379
- setViewSections(null);
2501
+ setView(null);
2380
2502
  return;
2381
2503
  }
2382
2504
 
@@ -2387,42 +2509,34 @@ function ViewBranchWatcher({
2387
2509
  .then((res) => {
2388
2510
  // A non-JSON 5xx used to parse to `undefined` and render an empty
2389
2511
  // "Published" view with nothing explaining it.
2390
- if (!res.ok) throw new Error(`Failed to load the published view: ${res.status}`);
2512
+ if (!res.ok) throw new Error(`Couldn't load the published view: ${res.status}`);
2391
2513
  return res.json();
2392
2514
  })
2393
2515
  .then((data) => {
2394
2516
  if (!Array.isArray(data?.sections)) throw new Error("Malformed published-view response");
2395
- setViewSections(data.sections);
2517
+ setView({ sections: data.sections, index: data.index ?? null });
2396
2518
  })
2397
2519
  .catch((err) => {
2398
2520
  if (err?.name === "AbortError") return;
2399
2521
  console.error(err);
2400
2522
  // 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.");
2523
+ setView(null);
2524
+ onError("Couldn't load the published view — showing your current edits.", "error");
2403
2525
  });
2404
2526
  return () => controller.abort();
2405
- }, [viewBranch, isEditMode, setViewSections, onError]);
2527
+ }, [viewBranch, isEditMode, setView, onError]);
2406
2528
 
2407
2529
  return null;
2408
2530
  }
2409
2531
 
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();
2532
+ function HistoryWatcher() {
2533
+ const { setHistoryState } = useEditorContext();
2420
2534
 
2421
2535
  useEffect(() => {
2422
2536
  const unlisten = historySelectEvent.listen(async ({ sha, date }) => {
2423
2537
  try {
2424
2538
  const res = await fetch(`/api/content/history?sha=${sha}`);
2425
- if (!res.ok) throw new Error(`Failed to load history: ${res.status}`);
2539
+ if (!res.ok) throw new Error(`Couldn't load history: ${res.status}`);
2426
2540
  const data = await res.json();
2427
2541
  setHistoryState({
2428
2542
  sha,
@@ -2442,10 +2556,6 @@ function HistoryWatcher({
2442
2556
  return unlisten;
2443
2557
  }, [setHistoryState]);
2444
2558
 
2445
- useEffect(() => {
2446
- onHistoryActiveChange?.(historyState !== null);
2447
- }, [historyState, onHistoryActiveChange]);
2448
-
2449
2559
  return null;
2450
2560
  }
2451
2561
 
@@ -2556,6 +2666,7 @@ function EditorContent({
2556
2666
  onBlockMove,
2557
2667
  onMoveSection,
2558
2668
  onAccessChange,
2669
+ tagging,
2559
2670
  onStatusChange,
2560
2671
  mainIndex,
2561
2672
  changedSectionIds,
@@ -2577,6 +2688,7 @@ function EditorContent({
2577
2688
  onBlockMove: (move: BlockMove) => void;
2578
2689
  onMoveSection?: (sectionId: string) => void;
2579
2690
  onAccessChange: (sectionId: string, access: string[]) => void;
2691
+ tagging: SiteTagging;
2580
2692
  onStatusChange: (sectionId: string, status: "draft" | "live" | "archived") => void;
2581
2693
  mainIndex: SiteIndex | null;
2582
2694
  changedSectionIds: Set<string>;
@@ -2589,6 +2701,7 @@ function EditorContent({
2589
2701
  }) {
2590
2702
  const { isEditMode, viewBranch, previewAudience } = useEditorContext();
2591
2703
  const { openModal, closeModal } = useEditorModal();
2704
+ const referenceSources = useContext(ReferenceSourcesContext);
2592
2705
  const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null);
2593
2706
  const dismissPendingInsert = useCallback(() => setPendingInsertIndex(null), []);
2594
2707
 
@@ -2606,6 +2719,10 @@ function EditorContent({
2606
2719
  viewSections,
2607
2720
  activePageSectionIds,
2608
2721
  });
2722
+ // TL;DR / Summary plates render their nav anchor from this. Derived from the
2723
+ // same list the sidebar nav is built from (not displaySections, which preview
2724
+ // modes filter), so a plate's id always matches the href its nav entry links to.
2725
+ const summaryAnchors = useMemo(() => summaryNavAnchors(sections), [sections]);
2609
2726
 
2610
2727
  useEffect(() => {
2611
2728
  return combine(
@@ -2651,157 +2768,174 @@ function EditorContent({
2651
2768
  // `.section-wrapper:first-child` zeroes the 2.25rem that every other section
2652
2769
  // reserves above itself for its hover chrome, so the first section's controls
2653
2770
  // 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) {
2771
+ <SummaryAnchorsContext.Provider value={summaryAnchors}>
2772
+ <div className="pt-9">
2773
+ {displaySections.map(({ section, meta }, index) => {
2774
+ const definition = getSection(section.type);
2775
+ if (!definition) {
2776
+ return (
2777
+ <div
2778
+ key={section.id}
2779
+ className="border border-warning bg-warning-bg px-4 py-3 text-sm text-warning"
2780
+ role="alert"
2781
+ >
2782
+ Unknown section type: <code className="font-mono font-semibold">{section.type}</code>
2783
+ </div>
2784
+ );
2785
+ }
2786
+ const Component = definition.component;
2787
+ // The viewer renders no wrapper for a Reference with nothing to show,
2788
+ // so neither does the preview — its spacing would otherwise remain.
2789
+ if (
2790
+ !isEditMode && section.type === REFERENCE_TYPE && referenceSources &&
2791
+ !referenceShowsInPreview(referenceSources, section.id, section.content as unknown as ReferenceContent, viewBranch)
2792
+ ) {
2793
+ return null;
2794
+ }
2795
+
2658
2796
  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
- )}
2797
+ <Fragment key={section.id}>
2798
+ {editingEnabled && pendingInsertIndex === index && (
2799
+ <SectionSkeleton
2800
+ types={typeOptions}
2801
+ onSelect={(type) => {
2802
+ onAddSection(index, type);
2803
+ setPendingInsertIndex(null);
2804
+ }}
2805
+ onDismiss={dismissPendingInsert}
2806
+ />
2807
+ )}
2682
2808
 
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}
2809
+ <ErrorBoundary label={`${definition.label} (${section.type})`}>
2810
+ <SectionLayout
2811
+ type={section.type}
2693
2812
  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
- }
2813
+ dimNonPublished={!isEditMode}
2814
+ dimmed={previewAudience !== null && !audiencePasses(meta.access, previewAudience)}
2771
2815
  >
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
- )}
2816
+ <SectionWrapper
2817
+ sectionId={section.id}
2818
+ sectionType={section.type}
2819
+ status={meta.status}
2820
+ dirty={dirtySectionIds.has(section.id)}
2821
+ index={index}
2822
+ isLast={index === displaySections.length - 1}
2823
+ sectionCount={displaySections.length}
2824
+ containerId={ROOT_CONTAINER_ID}
2825
+ isContainerBlock={section.type === "container"}
2826
+ definition={definition}
2827
+ mainStatus={mainIndex?.sections[section.id]?.status ?? null}
2828
+ contentDiffersFromMain={changedSectionIds.has(section.id) || dirtySectionIds.has(section.id)}
2829
+ isLocalOnly={dirtySectionIds.has(section.id) && !changedSectionIds.has(section.id)}
2830
+ lockedBy={locks.get(section.id) ? { email: locks.get(section.id)!.email } : null}
2831
+ onTakeover={() => onTakeover(section.id)}
2832
+ onEditIntent={getEditIntent(section.id)}
2833
+ options={{
2834
+ ...(section.content as Record<string, unknown>),
2835
+ ...("options" in section ? (section.options as Record<string, unknown>) : {}),
2836
+ }}
2837
+ audiences={audiences}
2838
+ access={meta.access}
2839
+ onAccessChange={editingEnabled ? (a) => onAccessChange(section.id, a) : undefined}
2840
+ tagging={{
2841
+ tags: tagging.tags,
2842
+ assignedIds: tagging.tags.filter((t) => t.sectionIds.includes(section.id)).map((t) => t.id),
2843
+ onToggle: editingEnabled && tagging.editable ? (tagId, tagged) => tagging.onToggle(section.id, tagId, tagged) : undefined,
2844
+ onCreate: editingEnabled && tagging.editable ? (label) => tagging.onCreate(label, section.id) : undefined,
2845
+ }}
2846
+ onStatusChange={editingEnabled ? (s) => onStatusChange(section.id, s) : undefined}
2847
+ onSectionChange={editingEnabled ? (settingsResult) => {
2848
+ const result = settingsResult as Record<string, unknown>;
2849
+ if (result && typeof result === "object" && "content" in result && "options" in result) {
2850
+ const contentPatch = result.content as Record<string, unknown>;
2851
+ const optionsPatch = result.options as Record<string, unknown>;
2852
+ onSectionChange(section.id, {
2853
+ ...section,
2854
+ content: { ...section.content, ...contentPatch },
2855
+ options: { ...("options" in section ? section.options : {}), ...optionsPatch },
2856
+ } as SectionContent);
2857
+ } else {
2858
+ onSectionChange(section.id, {
2859
+ ...section,
2860
+ ...result,
2861
+ } as SectionContent);
2862
+ }
2863
+ } : undefined}
2864
+ onReorder={editingEnabled ? onReorderSections : undefined}
2865
+ onMoveSection={editingEnabled && onMoveSection ? () => onMoveSection(section.id) : undefined}
2866
+ onRequestInsert={editingEnabled ? (i) => setPendingInsertIndex(i) : undefined}
2867
+ onDelete={editingEnabled ? () => onDeleteSection(section.id) : undefined}
2868
+ onAddChild={
2869
+ editingEnabled && section.type === "container"
2870
+ ? () => {
2871
+ openModal(
2872
+ "Add block",
2873
+ <SectionTypePicker
2874
+ types={childInsertableTypes()}
2875
+ onClose={closeModal}
2876
+ onSelect={(type) => {
2877
+ const def = getSection(type);
2878
+ if (def) {
2879
+ const id =
2880
+ typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
2881
+ ? crypto.randomUUID()
2882
+ : `child-${Date.now()}`;
2883
+ const child = { id, ...(def.defaults() as object) } as Section;
2884
+ const containerSection: Section = {
2885
+ id: section.id,
2886
+ type: "container",
2887
+ content: section.content as Record<string, unknown>,
2888
+ };
2889
+ const childCount =
2890
+ (section.content as { children?: unknown[] }).children?.length ?? 0;
2891
+ const next = insertChildAt(containerSection, child, childCount);
2892
+ onSectionChange(section.id, {
2893
+ ...section,
2894
+ content: next.content,
2895
+ } as SectionContent);
2896
+ }
2897
+ closeModal();
2898
+ }}
2899
+ />,
2900
+ );
2901
+ }
2902
+ : undefined
2903
+ }
2904
+ >
2905
+ <Component
2906
+ content={section}
2907
+ options={"options" in section ? (section.options as Record<string, unknown>) : undefined}
2908
+ onChange={editingEnabled ? (newContent) => onSectionChange(section.id, newContent as SectionContent) : undefined}
2909
+ isEditMode={isEditMode}
2910
+ openModal={editingEnabled ? openModal : undefined}
2911
+ // Immediate (no confirm modal): used when a colors section's
2912
+ // last swatch is dragged out, auto-removing the empty section.
2913
+ onDelete={editingEnabled ? () => onDeleteSectionImmediate(section.id) : undefined}
2914
+ />
2915
+ </SectionWrapper>
2916
+ </SectionLayout>
2917
+ </ErrorBoundary>
2918
+ </Fragment>
2919
+ );
2920
+ })}
2921
+
2922
+ {/* Trailing selector: shown when an insert was requested after the last
2923
+ section, or always on an empty page — an empty page has no section
2924
+ chrome to insert from, so the selector IS its default state (not
2925
+ dismissible; there is nothing behind it). */}
2926
+ {editingEnabled && (displaySections.length === 0 || pendingInsertIndex === displaySections.length) && (
2927
+ <SectionSkeleton
2928
+ types={typeOptions}
2929
+ onSelect={(type) => {
2930
+ onAddSection(displaySections.length, type);
2931
+ setPendingInsertIndex(null);
2932
+ }}
2933
+ onDismiss={dismissPendingInsert}
2934
+ />
2935
+ )}
2803
2936
 
2804
- </div>
2937
+ </div>
2938
+ </SummaryAnchorsContext.Provider>
2805
2939
  );
2806
2940
  }
2807
2941
 
@@ -2847,7 +2981,7 @@ function formatElapsed(seconds: number): string {
2847
2981
  function DismissButton({ onClick }: { onClick: () => void }) {
2848
2982
  return (
2849
2983
  <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" />
2984
+ <X size={12} className="text-base-contrast/30 hover:text-base-contrast/60" />
2851
2985
  </button>
2852
2986
  );
2853
2987
  }
@@ -2861,7 +2995,7 @@ function StatusText({
2861
2995
  onDismiss,
2862
2996
  }: {
2863
2997
  publishAction: "idle" | "saving" | "publishing";
2864
- publishFeedback: string | null;
2998
+ publishFeedback: NoticeState | null;
2865
2999
  saveProgress: string | null;
2866
3000
  buildState: "idle" | "building" | "ready" | "error" | "fading";
2867
3001
  buildElapsed: number;
@@ -2869,14 +3003,14 @@ function StatusText({
2869
3003
  }) {
2870
3004
  if (publishAction === "saving") {
2871
3005
  return (
2872
- <span className="relative text-xs font-medium text-base-content/60">
3006
+ <span className="relative text-xs font-medium text-base-contrast/60">
2873
3007
  {saveProgress ?? <>Saving changes<LoadingEllipsis /></>}
2874
3008
  </span>
2875
3009
  );
2876
3010
  }
2877
3011
  if (publishAction === "publishing" || buildState === "building") {
2878
3012
  return (
2879
- <span className="text-xs font-medium text-orange-600">
3013
+ <span className="text-xs font-medium text-warning">
2880
3014
  Publishing ({formatElapsed(buildElapsed)})
2881
3015
  </span>
2882
3016
  );
@@ -2884,7 +3018,7 @@ function StatusText({
2884
3018
  if (buildState === "ready" || buildState === "fading") {
2885
3019
  return (
2886
3020
  <span className={cn(
2887
- "inline-flex items-center gap-1.5 text-xs font-medium text-green-600 transition-opacity duration-1000",
3021
+ "inline-flex items-center gap-1.5 text-xs font-medium text-success transition-opacity duration-1000",
2888
3022
  buildState === "fading" ? "opacity-0" : "opacity-100",
2889
3023
  )}>
2890
3024
  Published in {formatElapsed(buildElapsed)}
@@ -2894,19 +3028,22 @@ function StatusText({
2894
3028
  }
2895
3029
  if (buildState === "error") {
2896
3030
  return (
2897
- <span className="inline-flex items-center gap-1.5 text-xs font-medium text-red-600">
3031
+ <span className="inline-flex items-center gap-1.5 text-xs font-medium text-danger">
2898
3032
  Publish failed
2899
3033
  <DismissButton onClick={onDismiss} />
2900
3034
  </span>
2901
3035
  );
2902
3036
  }
2903
3037
  if (publishFeedback) {
3038
+ const toneClass: Record<NoticeState["tone"], string> = {
3039
+ success: "text-success",
3040
+ info: "text-base-contrast",
3041
+ warning: "text-warning",
3042
+ error: "text-danger",
3043
+ };
2904
3044
  return (
2905
- <span className={cn(
2906
- "text-xs font-medium",
2907
- publishFeedback === "Saved" ? "text-green-600" : "text-red-600",
2908
- )}>
2909
- {publishFeedback}
3045
+ <span className={cn("text-xs font-medium", toneClass[publishFeedback.tone])}>
3046
+ {publishFeedback.message}
2910
3047
  </span>
2911
3048
  );
2912
3049
  }
@@ -2938,11 +3075,14 @@ function EditorToolbar({
2938
3075
  audiences,
2939
3076
  authoringDefaults,
2940
3077
  onAuthoringDefaultsChange,
3078
+ docEditEnabled,
3079
+ onDocEditClick,
3080
+ showBugReport,
2941
3081
  }: {
2942
3082
  buttonState: "synced" | "publish" | "saveAndPublish";
2943
3083
  localChangesExist: boolean;
2944
3084
  publishAction: "idle" | "saving" | "publishing";
2945
- publishFeedback: string | null;
3085
+ publishFeedback: NoticeState | null;
2946
3086
  saveProgress: string | null;
2947
3087
  onSave: () => void;
2948
3088
  onPublish: () => void;
@@ -2963,6 +3103,14 @@ function EditorToolbar({
2963
3103
  audiences: Audience[];
2964
3104
  authoringDefaults: AuthoringDefaults;
2965
3105
  onAuthoringDefaultsChange: (next: AuthoringDefaults) => void;
3106
+ /** Whether the active page has document mode on — gates the "Edit as
3107
+ * document" FAB this toolbar stacks alongside its own mode-toggle FAB.
3108
+ * Owned by EditorShell (resolvedActivePage lives above the Provider). */
3109
+ docEditEnabled: boolean;
3110
+ onDocEditClick: () => void;
3111
+ /** Whether to render BugReportFAB in this toolbar's FabStack — false on
3112
+ * the password-only (zero-Supabase) path, which has no backend for it. */
3113
+ showBugReport: boolean;
2966
3114
  }) {
2967
3115
  const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState, historyPageId, setHistoryPageId } = useEditorContext();
2968
3116
 
@@ -2983,13 +3131,13 @@ function EditorToolbar({
2983
3131
  return (
2984
3132
  <>
2985
3133
  {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">
3134
+ // The docked chat sidebar slides in BELOW this bar (ChatPanel docks
3135
+ // at top-16 on /edit), so the toolbar never moves during the chat
3136
+ // open/close transition — ToolbarFrame's h-16 matches the panel's
3137
+ // top offset (and the body's pt-16) exactly.
3138
+ <ToolbarFrame
3139
+ left={
3140
+ <>
2993
3141
  {buttonState === "saveAndPublish" && (
2994
3142
  <SplitButton
2995
3143
  label="Save & Publish"
@@ -3024,18 +3172,20 @@ function EditorToolbar({
3024
3172
  </Button>
3025
3173
  )}
3026
3174
  <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">
3175
+ </>
3176
+ }
3177
+ center={
3178
+ <StatusText
3179
+ publishAction={publishAction}
3180
+ publishFeedback={publishFeedback}
3181
+ saveProgress={saveProgress}
3182
+ buildState={buildState}
3183
+ buildElapsed={buildElapsed}
3184
+ onDismiss={onBuildDismiss}
3185
+ />
3186
+ }
3187
+ right={
3188
+ <>
3039
3189
  <ProcessingIndicator items={processingItems} />
3040
3190
  <Tooltip side="bottom" content="Authoring defaults">
3041
3191
  <AuthoringDefaultsMenu defaults={authoringDefaults} audiences={audiences} onChange={onAuthoringDefaultsChange} />
@@ -3057,15 +3207,15 @@ function EditorToolbar({
3057
3207
  <ShowAllChromeToggle />
3058
3208
  </Tooltip>
3059
3209
  <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" />
3210
+ <IconButton icon={<Settings size={16} />} label="Site settings" size="md" onClick={onSettingsClick} className="border border-base-200 bg-base-accent" />
3061
3211
  </Tooltip>
3062
- </div>
3063
- </div>
3064
- </div>
3212
+ </>
3213
+ }
3214
+ />
3065
3215
  )}
3066
3216
  {!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">
3217
+ <ToolbarFrame
3218
+ center={
3069
3219
  <SegmentedControl
3070
3220
  options={[
3071
3221
  { value: "live", label: "Published" },
@@ -3074,27 +3224,37 @@ function EditorToolbar({
3074
3224
  value={viewBranch}
3075
3225
  onChange={(v) => setViewBranch(v as "saved" | "live")}
3076
3226
  />
3077
- </div>
3078
- </div>
3227
+ }
3228
+ />
3079
3229
  )}
3080
3230
  {!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>
3231
+ <FabStack>
3232
+ {/* Bug reports persist via Supabase; hidden on the password-only
3233
+ path (showBugReport, threaded from EditorShell's capabilities).
3234
+ History is presentation only, never editing (branch-preview
3235
+ already routes to DocumentPresentOverlay; !historyState covers
3236
+ the separate historySelectEvent commit view) — FabStack
3237
+ unmounts with the rest of this block whenever history is
3238
+ active, which hides all three FABs together. */}
3239
+ {showBugReport && <BugReportFAB stacked />}
3240
+ {docEditEnabled && (
3241
+ <Fab stacked label="Edit as document" column="editor" onClick={onDocEditClick}>
3242
+ <FileText size={20} aria-hidden="true" />
3243
+ </Fab>
3096
3244
  )}
3097
- </Fab>
3245
+ <Fab
3246
+ stacked
3247
+ label={isEditMode ? "Switch to view mode" : "Switch to edit mode"}
3248
+ column="editor"
3249
+ onClick={toggleEditMode}
3250
+ >
3251
+ {isEditMode ? (
3252
+ <Eye className="h-5 w-5" aria-hidden="true" />
3253
+ ) : (
3254
+ <Pencil className="h-5 w-5" aria-hidden="true" />
3255
+ )}
3256
+ </Fab>
3257
+ </FabStack>
3098
3258
  )}
3099
3259
  </>
3100
3260
  );