@actuate-media/cms-admin 0.15.0 → 0.16.1

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 (308) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +148 -2
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/components/posts-badges.test.d.ts +2 -0
  5. package/dist/__tests__/components/posts-badges.test.d.ts.map +1 -0
  6. package/dist/__tests__/components/posts-badges.test.js +68 -0
  7. package/dist/__tests__/components/posts-badges.test.js.map +1 -0
  8. package/dist/__tests__/layout/sidebar-posts.test.d.ts +2 -0
  9. package/dist/__tests__/layout/sidebar-posts.test.d.ts.map +1 -0
  10. package/dist/__tests__/layout/sidebar-posts.test.js +162 -0
  11. package/dist/__tests__/layout/sidebar-posts.test.js.map +1 -0
  12. package/dist/__tests__/lib/page-editor-service.test.d.ts +2 -0
  13. package/dist/__tests__/lib/page-editor-service.test.d.ts.map +1 -0
  14. package/dist/__tests__/lib/page-editor-service.test.js +137 -0
  15. package/dist/__tests__/lib/page-editor-service.test.js.map +1 -0
  16. package/dist/__tests__/lib/pages-service.test.d.ts +2 -0
  17. package/dist/__tests__/lib/pages-service.test.d.ts.map +1 -0
  18. package/dist/__tests__/lib/pages-service.test.js +140 -0
  19. package/dist/__tests__/lib/pages-service.test.js.map +1 -0
  20. package/dist/__tests__/lib/post-editor-service.test.d.ts +2 -0
  21. package/dist/__tests__/lib/post-editor-service.test.d.ts.map +1 -0
  22. package/dist/__tests__/lib/post-editor-service.test.js +185 -0
  23. package/dist/__tests__/lib/post-editor-service.test.js.map +1 -0
  24. package/dist/__tests__/lib/posts-service.test.d.ts +2 -0
  25. package/dist/__tests__/lib/posts-service.test.d.ts.map +1 -0
  26. package/dist/__tests__/lib/posts-service.test.js +244 -0
  27. package/dist/__tests__/lib/posts-service.test.js.map +1 -0
  28. package/dist/__tests__/lib/useApiData.test.d.ts +2 -0
  29. package/dist/__tests__/lib/useApiData.test.d.ts.map +1 -0
  30. package/dist/__tests__/lib/useApiData.test.js +84 -0
  31. package/dist/__tests__/lib/useApiData.test.js.map +1 -0
  32. package/dist/__tests__/views/dashboard.test.d.ts +2 -0
  33. package/dist/__tests__/views/dashboard.test.d.ts.map +1 -0
  34. package/dist/__tests__/views/dashboard.test.js +138 -0
  35. package/dist/__tests__/views/dashboard.test.js.map +1 -0
  36. package/dist/__tests__/views/posts-list-view.test.d.ts +2 -0
  37. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  38. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  39. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  40. package/dist/actuate-admin.css +1 -1
  41. package/dist/components/pages/tag-badges.d.ts +21 -0
  42. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  43. package/dist/components/pages/tag-badges.js +56 -0
  44. package/dist/components/pages/tag-badges.js.map +1 -0
  45. package/dist/components/posts/badges.d.ts +25 -0
  46. package/dist/components/posts/badges.d.ts.map +1 -0
  47. package/dist/components/posts/badges.js +123 -0
  48. package/dist/components/posts/badges.js.map +1 -0
  49. package/dist/components/posts/icons.d.ts +12 -0
  50. package/dist/components/posts/icons.d.ts.map +1 -0
  51. package/dist/components/posts/icons.js +26 -0
  52. package/dist/components/posts/icons.js.map +1 -0
  53. package/dist/layout/Layout.js +36 -8
  54. package/dist/layout/Layout.js.map +1 -1
  55. package/dist/layout/Sidebar.d.ts +9 -0
  56. package/dist/layout/Sidebar.d.ts.map +1 -1
  57. package/dist/layout/Sidebar.js +100 -48
  58. package/dist/layout/Sidebar.js.map +1 -1
  59. package/dist/lib/page-editor-service.d.ts +45 -0
  60. package/dist/lib/page-editor-service.d.ts.map +1 -0
  61. package/dist/lib/page-editor-service.js +153 -0
  62. package/dist/lib/page-editor-service.js.map +1 -0
  63. package/dist/lib/pages-service.d.ts +143 -0
  64. package/dist/lib/pages-service.d.ts.map +1 -0
  65. package/dist/lib/pages-service.js +444 -0
  66. package/dist/lib/pages-service.js.map +1 -0
  67. package/dist/lib/post-editor-service.d.ts +57 -0
  68. package/dist/lib/post-editor-service.d.ts.map +1 -0
  69. package/dist/lib/post-editor-service.js +234 -0
  70. package/dist/lib/post-editor-service.js.map +1 -0
  71. package/dist/lib/posts-service.d.ts +73 -0
  72. package/dist/lib/posts-service.d.ts.map +1 -0
  73. package/dist/lib/posts-service.js +245 -0
  74. package/dist/lib/posts-service.js.map +1 -0
  75. package/dist/lib/preview-link.d.ts +32 -0
  76. package/dist/lib/preview-link.d.ts.map +1 -0
  77. package/dist/lib/preview-link.js +40 -0
  78. package/dist/lib/preview-link.js.map +1 -0
  79. package/dist/router/index.d.ts +1 -0
  80. package/dist/router/index.d.ts.map +1 -1
  81. package/dist/router/index.js +9 -1
  82. package/dist/router/index.js.map +1 -1
  83. package/dist/views/Dashboard.d.ts.map +1 -1
  84. package/dist/views/Dashboard.js +64 -20
  85. package/dist/views/Dashboard.js.map +1 -1
  86. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  87. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  88. package/dist/views/Pages/PageTagManager.js +145 -0
  89. package/dist/views/Pages/PageTagManager.js.map +1 -0
  90. package/dist/views/Pages/PagesListView.d.ts +18 -0
  91. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  92. package/dist/views/Pages/PagesListView.js +287 -0
  93. package/dist/views/Pages/PagesListView.js.map +1 -0
  94. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  95. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  96. package/dist/views/Posts/NewPostPicker.js +59 -0
  97. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  98. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  99. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  100. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  101. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  102. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  103. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  104. package/dist/views/Posts/PostTypesView.js +52 -0
  105. package/dist/views/Posts/PostTypesView.js.map +1 -0
  106. package/dist/views/Posts/PostsListView.d.ts +25 -0
  107. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  108. package/dist/views/Posts/PostsListView.js +239 -0
  109. package/dist/views/Posts/PostsListView.js.map +1 -0
  110. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  111. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  112. package/dist/views/Posts/post-type-presets.js +72 -0
  113. package/dist/views/Posts/post-type-presets.js.map +1 -0
  114. package/dist/views/page-builder/Widgets.d.ts +13 -0
  115. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  116. package/dist/views/page-builder/Widgets.js +15 -0
  117. package/dist/views/page-builder/Widgets.js.map +1 -0
  118. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  119. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  120. package/dist/views/page-editor/AddSectionModal.js +22 -0
  121. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  122. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  123. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  124. package/dist/views/page-editor/EditorCanvas.js +64 -0
  125. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  126. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  127. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  128. package/dist/views/page-editor/EditorTopBar.js +42 -0
  129. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  130. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  131. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  132. package/dist/views/page-editor/PagePreview.js +37 -0
  133. package/dist/views/page-editor/PagePreview.js.map +1 -0
  134. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  135. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  136. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  137. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  138. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  139. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  140. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  141. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  142. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  143. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  144. package/dist/views/page-editor/SectionInspector.js +98 -0
  145. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  146. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  147. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  148. package/dist/views/page-editor/SectionsPanel.js +43 -0
  149. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  150. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  151. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  152. package/dist/views/page-editor/ValidationSummary.js +10 -0
  153. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  154. package/dist/views/page-editor/section-icons.d.ts +4 -0
  155. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  156. package/dist/views/page-editor/section-icons.js +20 -0
  157. package/dist/views/page-editor/section-icons.js.map +1 -0
  158. package/dist/views/page-editor/section-types.d.ts +11 -0
  159. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  160. package/dist/views/page-editor/section-types.js +10 -0
  161. package/dist/views/page-editor/section-types.js.map +1 -0
  162. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  163. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  164. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  165. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  166. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  167. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  168. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  169. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  170. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  171. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  172. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  173. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  174. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  175. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  176. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  177. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  178. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  179. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  180. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  181. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  182. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  183. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  184. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  185. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  186. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  187. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  188. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  189. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  190. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  191. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  192. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  193. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  194. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  195. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  196. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  197. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  198. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  199. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  200. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  201. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  202. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  203. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  204. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  205. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  206. package/dist/views/page-editor/sections/index.d.ts +14 -0
  207. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  208. package/dist/views/page-editor/sections/index.js +12 -0
  209. package/dist/views/page-editor/sections/index.js.map +1 -0
  210. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  211. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  212. package/dist/views/page-editor/sections/parts.js +60 -0
  213. package/dist/views/page-editor/sections/parts.js.map +1 -0
  214. package/dist/views/page-editor/viewports.d.ts +14 -0
  215. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  216. package/dist/views/page-editor/viewports.js +12 -0
  217. package/dist/views/page-editor/viewports.js.map +1 -0
  218. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  219. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  220. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  221. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  222. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  223. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  224. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  225. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  226. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  227. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  228. package/dist/views/post-editor/PostHeader.js +27 -0
  229. package/dist/views/post-editor/PostHeader.js.map +1 -0
  230. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  231. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  232. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  233. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  234. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  235. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  236. package/dist/views/post-editor/PostPreview.js +53 -0
  237. package/dist/views/post-editor/PostPreview.js.map +1 -0
  238. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  239. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  240. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  241. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  242. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  243. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  244. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  245. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  246. package/package.json +2 -2
  247. package/src/AdminRoot.tsx +214 -2
  248. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  249. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  250. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  251. package/src/__tests__/lib/pages-service.test.ts +157 -0
  252. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  253. package/src/__tests__/lib/posts-service.test.ts +279 -0
  254. package/src/__tests__/lib/useApiData.test.ts +105 -0
  255. package/src/__tests__/views/dashboard.test.tsx +180 -0
  256. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  257. package/src/components/pages/tag-badges.tsx +104 -0
  258. package/src/components/posts/badges.tsx +170 -0
  259. package/src/components/posts/icons.ts +40 -0
  260. package/src/layout/Layout.tsx +66 -10
  261. package/src/layout/Sidebar.tsx +116 -50
  262. package/src/lib/page-editor-service.ts +233 -0
  263. package/src/lib/pages-service.ts +599 -0
  264. package/src/lib/post-editor-service.ts +342 -0
  265. package/src/lib/posts-service.ts +366 -0
  266. package/src/lib/preview-link.ts +63 -0
  267. package/src/router/index.ts +10 -1
  268. package/src/views/Dashboard.tsx +142 -89
  269. package/src/views/Pages/PageTagManager.tsx +436 -0
  270. package/src/views/Pages/PagesListView.tsx +944 -0
  271. package/src/views/Posts/NewPostPicker.tsx +147 -0
  272. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  273. package/src/views/Posts/PostTypesView.tsx +194 -0
  274. package/src/views/Posts/PostsListView.tsx +869 -0
  275. package/src/views/Posts/post-type-presets.ts +105 -0
  276. package/src/views/page-builder/Widgets.tsx +44 -0
  277. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  278. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  279. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  280. package/src/views/page-editor/PagePreview.tsx +96 -0
  281. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  282. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  283. package/src/views/page-editor/SectionInspector.tsx +599 -0
  284. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  285. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  286. package/src/views/page-editor/section-icons.ts +33 -0
  287. package/src/views/page-editor/section-types.ts +27 -0
  288. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  289. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  290. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  291. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  292. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  293. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  294. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  295. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  296. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  297. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  298. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  299. package/src/views/page-editor/sections/index.ts +13 -0
  300. package/src/views/page-editor/sections/parts.tsx +113 -0
  301. package/src/views/page-editor/viewports.ts +25 -0
  302. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  303. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  304. package/src/views/post-editor/PostHeader.tsx +87 -0
  305. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  306. package/src/views/post-editor/PostPreview.tsx +114 -0
  307. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  308. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -0,0 +1,599 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Eye, EyeOff, Plus, X } from 'lucide-react'
5
+ import { MediaPickerModal } from '../../components/MediaPickerModal.js'
6
+ import type { PageSection } from '../../lib/page-editor-service.js'
7
+ import type { SectionSettings } from './section-types.js'
8
+ import { getSectionType, type SectionFieldDef } from './section-types.js'
9
+
10
+ interface SectionInspectorProps {
11
+ section: PageSection
12
+ canEdit: boolean
13
+ onClose: () => void
14
+ onContentChange: (patch: Record<string, unknown>) => void
15
+ onSettingsChange: (patch: Partial<SectionSettings>) => void
16
+ onMetaChange: (patch: { name?: string; internalLabel?: string }) => void
17
+ onToggleVisibility: () => void
18
+ }
19
+
20
+ const LABEL = 'text-foreground mb-1 block text-xs font-medium'
21
+ const INPUT =
22
+ 'border-input bg-input-background text-foreground focus-visible:ring-ring w-full rounded-md border px-2.5 py-1.5 text-sm focus-visible:ring-2 focus-visible:outline-none'
23
+
24
+ export function SectionInspector({
25
+ section,
26
+ canEdit,
27
+ onClose,
28
+ onContentChange,
29
+ onSettingsChange,
30
+ onMetaChange,
31
+ onToggleVisibility,
32
+ }: SectionInspectorProps) {
33
+ const def = getSectionType(section.sectionType)
34
+
35
+ // Escape closes the inspector.
36
+ useEffect(() => {
37
+ function onKey(e: KeyboardEvent) {
38
+ if (e.key === 'Escape') onClose()
39
+ }
40
+ document.addEventListener('keydown', onKey)
41
+ return () => document.removeEventListener('keydown', onKey)
42
+ }, [onClose])
43
+
44
+ return (
45
+ <aside
46
+ role="dialog"
47
+ aria-label={`Edit ${section.name}`}
48
+ className="bg-card border-border flex h-full w-80 shrink-0 flex-col border-l"
49
+ >
50
+ <div className="border-border flex items-center justify-between border-b px-4 py-3">
51
+ <div className="min-w-0">
52
+ <h2 className="text-foreground truncate text-sm font-medium">{section.name}</h2>
53
+ <p className="text-muted-foreground text-xs">{def?.name ?? 'Section'}</p>
54
+ </div>
55
+ <button
56
+ type="button"
57
+ onClick={onClose}
58
+ aria-label="Close inspector"
59
+ className="text-muted-foreground hover:text-foreground hover:bg-accent rounded p-1"
60
+ >
61
+ <X className="h-4 w-4" aria-hidden />
62
+ </button>
63
+ </div>
64
+
65
+ <div className="flex-1 space-y-6 overflow-y-auto p-4">
66
+ {!canEdit && (
67
+ <p className="border-border bg-muted text-muted-foreground rounded-md border p-2 text-xs">
68
+ You have read-only access to this page.
69
+ </p>
70
+ )}
71
+
72
+ {/* Content fields (schema-driven) */}
73
+ <fieldset disabled={!canEdit} className="space-y-3">
74
+ <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
75
+ Content
76
+ </legend>
77
+ {def?.fields.map((field) => (
78
+ <ContentField
79
+ key={field.key}
80
+ field={field}
81
+ value={section.content[field.key]}
82
+ onChange={(v) => onContentChange({ [field.key]: v })}
83
+ />
84
+ ))}
85
+ </fieldset>
86
+
87
+ {/* Section meta + visibility */}
88
+ <fieldset disabled={!canEdit} className="space-y-3">
89
+ <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
90
+ Section settings
91
+ </legend>
92
+ <div>
93
+ <label className={LABEL} htmlFor="insp-name">
94
+ Section name
95
+ </label>
96
+ <input
97
+ id="insp-name"
98
+ className={INPUT}
99
+ value={section.name}
100
+ onChange={(e) => onMetaChange({ name: e.target.value })}
101
+ />
102
+ </div>
103
+ <div>
104
+ <label className={LABEL} htmlFor="insp-internal">
105
+ Internal label
106
+ </label>
107
+ <input
108
+ id="insp-internal"
109
+ className={INPUT}
110
+ placeholder="Team-only note (optional)"
111
+ value={section.internalLabel ?? ''}
112
+ onChange={(e) => onMetaChange({ internalLabel: e.target.value })}
113
+ />
114
+ </div>
115
+ <button
116
+ type="button"
117
+ onClick={onToggleVisibility}
118
+ disabled={!canEdit}
119
+ aria-pressed={!section.visible}
120
+ className="border-border text-foreground hover:bg-accent flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm disabled:opacity-50"
121
+ >
122
+ <span className="flex items-center gap-2">
123
+ {section.visible ? (
124
+ <Eye className="h-4 w-4" aria-hidden />
125
+ ) : (
126
+ <EyeOff className="h-4 w-4" aria-hidden />
127
+ )}
128
+ {section.visible ? 'Visible on page' : 'Hidden from page'}
129
+ </span>
130
+ <span className="text-muted-foreground text-xs">
131
+ {section.visible ? 'Hide' : 'Show'}
132
+ </span>
133
+ </button>
134
+ </fieldset>
135
+
136
+ {/* Appearance */}
137
+ <fieldset disabled={!canEdit} className="space-y-3">
138
+ <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
139
+ Appearance
140
+ </legend>
141
+ <div>
142
+ <label className={LABEL} htmlFor="insp-bg">
143
+ Background
144
+ </label>
145
+ <select
146
+ id="insp-bg"
147
+ className={INPUT}
148
+ value={section.settings.backgroundType ?? 'none'}
149
+ onChange={(e) =>
150
+ onSettingsChange({
151
+ backgroundType: e.target.value as SectionSettings['backgroundType'],
152
+ })
153
+ }
154
+ >
155
+ <option value="none">White</option>
156
+ <option value="solid">Solid color</option>
157
+ <option value="gradient">Gradient</option>
158
+ </select>
159
+ </div>
160
+ {section.settings.backgroundType && section.settings.backgroundType !== 'none' && (
161
+ <div>
162
+ <label className={LABEL} htmlFor="insp-bgcolor">
163
+ Color
164
+ </label>
165
+ <select
166
+ id="insp-bgcolor"
167
+ className={INPUT}
168
+ value={section.settings.backgroundColor ?? 'purple'}
169
+ onChange={(e) => onSettingsChange({ backgroundColor: e.target.value })}
170
+ >
171
+ <option value="purple">Purple</option>
172
+ <option value="muted">Light gray</option>
173
+ </select>
174
+ </div>
175
+ )}
176
+ <div>
177
+ <label className={LABEL} htmlFor="insp-pad">
178
+ Spacing
179
+ </label>
180
+ <select
181
+ id="insp-pad"
182
+ className={INPUT}
183
+ value={section.settings.paddingY ?? 'lg'}
184
+ onChange={(e) =>
185
+ onSettingsChange({ paddingY: e.target.value as SectionSettings['paddingY'] })
186
+ }
187
+ >
188
+ <option value="sm">Small</option>
189
+ <option value="md">Medium</option>
190
+ <option value="lg">Large</option>
191
+ <option value="xl">Extra large</option>
192
+ </select>
193
+ </div>
194
+ </fieldset>
195
+
196
+ {/* Advanced */}
197
+ <fieldset disabled={!canEdit} className="space-y-3">
198
+ <legend className="text-muted-foreground mb-2 text-[10px] font-semibold tracking-wider uppercase">
199
+ Advanced
200
+ </legend>
201
+ <div>
202
+ <label className={LABEL} htmlFor="insp-anchor">
203
+ Anchor ID
204
+ </label>
205
+ <input
206
+ id="insp-anchor"
207
+ className={INPUT}
208
+ placeholder="e.g. features"
209
+ value={section.settings.anchorId ?? ''}
210
+ onChange={(e) => onSettingsChange({ anchorId: e.target.value })}
211
+ />
212
+ </div>
213
+ <div>
214
+ <label className={LABEL} htmlFor="insp-css">
215
+ CSS class
216
+ </label>
217
+ <input
218
+ id="insp-css"
219
+ className={INPUT}
220
+ placeholder="custom-class"
221
+ value={section.settings.cssClass ?? ''}
222
+ onChange={(e) => onSettingsChange({ cssClass: e.target.value })}
223
+ />
224
+ </div>
225
+ </fieldset>
226
+ </div>
227
+ </aside>
228
+ )
229
+ }
230
+
231
+ // ─── Field renderers ─────────────────────────────────────────────────
232
+
233
+ function ContentField({
234
+ field,
235
+ value,
236
+ onChange,
237
+ }: {
238
+ field: SectionFieldDef
239
+ value: unknown
240
+ onChange: (v: unknown) => void
241
+ }) {
242
+ const id = `insp-field-${field.key}`
243
+ const text = typeof value === 'string' ? value : ''
244
+
245
+ if (field.type === 'stats') {
246
+ return <StatsField label={field.label} value={value} onChange={onChange} />
247
+ }
248
+
249
+ if (field.type === 'gallery') {
250
+ return <GalleryField label={field.label} value={value} onChange={onChange} />
251
+ }
252
+
253
+ if (field.type === 'boolean') {
254
+ return <BooleanField id={id} field={field} value={value === true} onChange={onChange} />
255
+ }
256
+
257
+ if (field.type === 'relationship') {
258
+ return <RelationshipField id={id} field={field} value={value} onChange={onChange} />
259
+ }
260
+
261
+ if (field.type === 'media') {
262
+ return <MediaField id={id} field={field} value={text} onChange={onChange} />
263
+ }
264
+
265
+ if (field.type === 'select') {
266
+ return (
267
+ <div>
268
+ <label className={LABEL} htmlFor={id}>
269
+ {field.label}
270
+ </label>
271
+ <select id={id} className={INPUT} value={text} onChange={(e) => onChange(e.target.value)}>
272
+ {field.options?.map((o) => (
273
+ <option key={o.value} value={o.value}>
274
+ {o.label}
275
+ </option>
276
+ ))}
277
+ </select>
278
+ </div>
279
+ )
280
+ }
281
+
282
+ if (field.type === 'textarea' || field.type === 'richText') {
283
+ return (
284
+ <div>
285
+ <label className={LABEL} htmlFor={id}>
286
+ {field.label}
287
+ {field.required && <span className="text-destructive ml-0.5">*</span>}
288
+ </label>
289
+ <textarea
290
+ id={id}
291
+ className={`${INPUT} resize-y ${field.type === 'richText' ? 'min-h-[140px]' : 'min-h-[72px]'}`}
292
+ value={text}
293
+ placeholder={field.placeholder}
294
+ onChange={(e) => onChange(e.target.value)}
295
+ />
296
+ {field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
297
+ </div>
298
+ )
299
+ }
300
+
301
+ return (
302
+ <div>
303
+ <label className={LABEL} htmlFor={id}>
304
+ {field.label}
305
+ {field.required && <span className="text-destructive ml-0.5">*</span>}
306
+ </label>
307
+ <input
308
+ id={id}
309
+ type={field.type === 'url' ? 'url' : 'text'}
310
+ className={INPUT}
311
+ value={text}
312
+ placeholder={field.placeholder}
313
+ onChange={(e) => onChange(e.target.value)}
314
+ />
315
+ {field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
316
+ </div>
317
+ )
318
+ }
319
+
320
+ function MediaField({
321
+ id,
322
+ field,
323
+ value,
324
+ onChange,
325
+ }: {
326
+ id: string
327
+ field: SectionFieldDef
328
+ value: string
329
+ onChange: (v: unknown) => void
330
+ }) {
331
+ const [open, setOpen] = useState(false)
332
+ return (
333
+ <div>
334
+ <label className={LABEL} htmlFor={id}>
335
+ {field.label}
336
+ </label>
337
+ <div className="flex gap-2">
338
+ <input
339
+ id={id}
340
+ className={`${INPUT} flex-1`}
341
+ placeholder="https://…"
342
+ value={value}
343
+ onChange={(e) => onChange(e.target.value)}
344
+ />
345
+ <button
346
+ type="button"
347
+ onClick={() => setOpen(true)}
348
+ className="bg-accent text-foreground border-border hover:bg-accent/80 rounded-md border px-3 py-1.5 text-xs font-medium"
349
+ >
350
+ Browse
351
+ </button>
352
+ </div>
353
+ {value && (
354
+ <div className="border-border bg-muted mt-2 overflow-hidden rounded-md border">
355
+ <img src={value} alt="" className="h-28 w-full object-contain" />
356
+ </div>
357
+ )}
358
+ <MediaPickerModal
359
+ open={open}
360
+ onClose={() => setOpen(false)}
361
+ onSelect={(url) => {
362
+ onChange(url)
363
+ setOpen(false)
364
+ }}
365
+ accept="image/*"
366
+ />
367
+ </div>
368
+ )
369
+ }
370
+
371
+ function BooleanField({
372
+ id,
373
+ field,
374
+ value,
375
+ onChange,
376
+ }: {
377
+ id: string
378
+ field: SectionFieldDef
379
+ value: boolean
380
+ onChange: (v: unknown) => void
381
+ }) {
382
+ return (
383
+ <div>
384
+ <button
385
+ type="button"
386
+ id={id}
387
+ role="switch"
388
+ aria-checked={value}
389
+ onClick={() => onChange(!value)}
390
+ className="border-border text-foreground hover:bg-accent flex w-full items-center justify-between rounded-md border px-2.5 py-1.5 text-sm"
391
+ >
392
+ <span>{field.label}</span>
393
+ <span
394
+ aria-hidden
395
+ className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${
396
+ value ? 'bg-primary' : 'bg-muted'
397
+ }`}
398
+ >
399
+ <span
400
+ className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
401
+ value ? 'translate-x-4' : 'translate-x-0.5'
402
+ }`}
403
+ />
404
+ </span>
405
+ </button>
406
+ {field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
407
+ </div>
408
+ )
409
+ }
410
+
411
+ function RelationshipField({
412
+ id,
413
+ field,
414
+ value,
415
+ onChange,
416
+ }: {
417
+ id: string
418
+ field: SectionFieldDef
419
+ value: unknown
420
+ onChange: (v: unknown) => void
421
+ }) {
422
+ // Stored as an array of slugs; edited as a newline/comma-separated list
423
+ // so authors can hand-pick referenced documents without a live picker.
424
+ const slugs = Array.isArray(value) ? (value as string[]) : []
425
+ const text = slugs.join('\n')
426
+ return (
427
+ <div>
428
+ <label className={LABEL} htmlFor={id}>
429
+ {field.label}
430
+ </label>
431
+ <textarea
432
+ id={id}
433
+ className={`${INPUT} min-h-[72px] resize-y`}
434
+ placeholder="one-slug-per-line"
435
+ value={text}
436
+ onChange={(e) =>
437
+ onChange(
438
+ e.target.value
439
+ .split(/[\n,]/)
440
+ .map((s) => s.trim())
441
+ .filter(Boolean),
442
+ )
443
+ }
444
+ />
445
+ {field.help && <p className="text-muted-foreground mt-1 text-xs">{field.help}</p>}
446
+ </div>
447
+ )
448
+ }
449
+
450
+ interface ImageRow {
451
+ url?: string
452
+ alt?: string
453
+ caption?: string
454
+ }
455
+
456
+ function GalleryField({
457
+ label,
458
+ value,
459
+ onChange,
460
+ }: {
461
+ label: string
462
+ value: unknown
463
+ onChange: (v: unknown) => void
464
+ }) {
465
+ const rows: ImageRow[] = Array.isArray(value) ? (value as ImageRow[]) : []
466
+
467
+ function update(index: number, patch: Partial<ImageRow>) {
468
+ onChange(rows.map((r, i) => (i === index ? { ...r, ...patch } : r)))
469
+ }
470
+ function remove(index: number) {
471
+ onChange(rows.filter((_, i) => i !== index))
472
+ }
473
+ function add() {
474
+ onChange([...rows, { url: '', alt: '', caption: '' }])
475
+ }
476
+
477
+ return (
478
+ <div>
479
+ <span className={LABEL}>{label}</span>
480
+ <div className="space-y-3">
481
+ {rows.map((row, i) => (
482
+ <div key={i} className="border-border rounded-md border p-2">
483
+ <div className="mb-2 flex items-center justify-between">
484
+ <span className="text-muted-foreground text-xs">Image {i + 1}</span>
485
+ <button
486
+ type="button"
487
+ onClick={() => remove(i)}
488
+ aria-label={`Remove image ${i + 1}`}
489
+ className="text-muted-foreground hover:text-destructive rounded p-0.5"
490
+ >
491
+ <X className="h-3.5 w-3.5" aria-hidden />
492
+ </button>
493
+ </div>
494
+ <div className="space-y-2">
495
+ <input
496
+ className={INPUT}
497
+ placeholder="Image URL (https://…)"
498
+ value={row.url ?? ''}
499
+ onChange={(e) => update(i, { url: e.target.value })}
500
+ aria-label={`Image ${i + 1} URL`}
501
+ />
502
+ <input
503
+ className={INPUT}
504
+ placeholder="Alt text (for accessibility)"
505
+ value={row.alt ?? ''}
506
+ onChange={(e) => update(i, { alt: e.target.value })}
507
+ aria-label={`Image ${i + 1} alt text`}
508
+ />
509
+ </div>
510
+ </div>
511
+ ))}
512
+ </div>
513
+ <button
514
+ type="button"
515
+ onClick={add}
516
+ className="border-border text-foreground hover:bg-accent mt-2 flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed px-2 py-1.5 text-xs font-medium"
517
+ >
518
+ <Plus className="h-3.5 w-3.5" aria-hidden />
519
+ Add image
520
+ </button>
521
+ </div>
522
+ )
523
+ }
524
+
525
+ interface StatRow {
526
+ value?: string
527
+ label?: string
528
+ description?: string
529
+ }
530
+
531
+ function StatsField({
532
+ label,
533
+ value,
534
+ onChange,
535
+ }: {
536
+ label: string
537
+ value: unknown
538
+ onChange: (v: unknown) => void
539
+ }) {
540
+ const rows: StatRow[] = Array.isArray(value) ? (value as StatRow[]) : []
541
+
542
+ function update(index: number, patch: Partial<StatRow>) {
543
+ const next = rows.map((r, i) => (i === index ? { ...r, ...patch } : r))
544
+ onChange(next)
545
+ }
546
+ function remove(index: number) {
547
+ onChange(rows.filter((_, i) => i !== index))
548
+ }
549
+ function add() {
550
+ onChange([...rows, { value: '', label: '', description: '' }])
551
+ }
552
+
553
+ return (
554
+ <div>
555
+ <span className={LABEL}>{label}</span>
556
+ <div className="space-y-3">
557
+ {rows.map((row, i) => (
558
+ <div key={i} className="border-border rounded-md border p-2">
559
+ <div className="mb-2 flex items-center justify-between">
560
+ <span className="text-muted-foreground text-xs">Stat {i + 1}</span>
561
+ <button
562
+ type="button"
563
+ onClick={() => remove(i)}
564
+ aria-label={`Remove stat ${i + 1}`}
565
+ className="text-muted-foreground hover:text-destructive rounded p-0.5"
566
+ >
567
+ <X className="h-3.5 w-3.5" aria-hidden />
568
+ </button>
569
+ </div>
570
+ <div className="space-y-2">
571
+ <input
572
+ className={INPUT}
573
+ placeholder="Value (e.g. 99.99%)"
574
+ value={row.value ?? ''}
575
+ onChange={(e) => update(i, { value: e.target.value })}
576
+ aria-label={`Stat ${i + 1} value`}
577
+ />
578
+ <input
579
+ className={INPUT}
580
+ placeholder="Label (e.g. Uptime)"
581
+ value={row.label ?? ''}
582
+ onChange={(e) => update(i, { label: e.target.value })}
583
+ aria-label={`Stat ${i + 1} label`}
584
+ />
585
+ </div>
586
+ </div>
587
+ ))}
588
+ </div>
589
+ <button
590
+ type="button"
591
+ onClick={add}
592
+ className="border-border text-foreground hover:bg-accent mt-2 flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed px-2 py-1.5 text-xs font-medium"
593
+ >
594
+ <Plus className="h-3.5 w-3.5" aria-hidden />
595
+ Add stat
596
+ </button>
597
+ </div>
598
+ )
599
+ }