@actuate-media/cms-admin 0.14.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) 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__/views/posts-list-view.test.d.ts +2 -0
  29. package/dist/__tests__/views/posts-list-view.test.d.ts.map +1 -0
  30. package/dist/__tests__/views/posts-list-view.test.js +98 -0
  31. package/dist/__tests__/views/posts-list-view.test.js.map +1 -0
  32. package/dist/actuate-admin.css +1 -1
  33. package/dist/components/pages/tag-badges.d.ts +21 -0
  34. package/dist/components/pages/tag-badges.d.ts.map +1 -0
  35. package/dist/components/pages/tag-badges.js +56 -0
  36. package/dist/components/pages/tag-badges.js.map +1 -0
  37. package/dist/components/posts/badges.d.ts +25 -0
  38. package/dist/components/posts/badges.d.ts.map +1 -0
  39. package/dist/components/posts/badges.js +123 -0
  40. package/dist/components/posts/badges.js.map +1 -0
  41. package/dist/components/posts/icons.d.ts +12 -0
  42. package/dist/components/posts/icons.d.ts.map +1 -0
  43. package/dist/components/posts/icons.js +26 -0
  44. package/dist/components/posts/icons.js.map +1 -0
  45. package/dist/layout/Header.d.ts +14 -1
  46. package/dist/layout/Header.d.ts.map +1 -1
  47. package/dist/layout/Header.js +10 -3
  48. package/dist/layout/Header.js.map +1 -1
  49. package/dist/layout/Layout.d.ts.map +1 -1
  50. package/dist/layout/Layout.js +78 -2
  51. package/dist/layout/Layout.js.map +1 -1
  52. package/dist/layout/Sidebar.d.ts +16 -0
  53. package/dist/layout/Sidebar.d.ts.map +1 -1
  54. package/dist/layout/Sidebar.js +176 -49
  55. package/dist/layout/Sidebar.js.map +1 -1
  56. package/dist/lib/page-editor-service.d.ts +45 -0
  57. package/dist/lib/page-editor-service.d.ts.map +1 -0
  58. package/dist/lib/page-editor-service.js +153 -0
  59. package/dist/lib/page-editor-service.js.map +1 -0
  60. package/dist/lib/pages-service.d.ts +143 -0
  61. package/dist/lib/pages-service.d.ts.map +1 -0
  62. package/dist/lib/pages-service.js +444 -0
  63. package/dist/lib/pages-service.js.map +1 -0
  64. package/dist/lib/post-editor-service.d.ts +57 -0
  65. package/dist/lib/post-editor-service.d.ts.map +1 -0
  66. package/dist/lib/post-editor-service.js +234 -0
  67. package/dist/lib/post-editor-service.js.map +1 -0
  68. package/dist/lib/posts-service.d.ts +73 -0
  69. package/dist/lib/posts-service.d.ts.map +1 -0
  70. package/dist/lib/posts-service.js +245 -0
  71. package/dist/lib/posts-service.js.map +1 -0
  72. package/dist/lib/preview-link.d.ts +32 -0
  73. package/dist/lib/preview-link.d.ts.map +1 -0
  74. package/dist/lib/preview-link.js +40 -0
  75. package/dist/lib/preview-link.js.map +1 -0
  76. package/dist/router/index.d.ts +1 -0
  77. package/dist/router/index.d.ts.map +1 -1
  78. package/dist/router/index.js +9 -1
  79. package/dist/router/index.js.map +1 -1
  80. package/dist/views/Dashboard.d.ts.map +1 -1
  81. package/dist/views/Dashboard.js +147 -62
  82. package/dist/views/Dashboard.js.map +1 -1
  83. package/dist/views/Pages/PageTagManager.d.ts +14 -0
  84. package/dist/views/Pages/PageTagManager.d.ts.map +1 -0
  85. package/dist/views/Pages/PageTagManager.js +145 -0
  86. package/dist/views/Pages/PageTagManager.js.map +1 -0
  87. package/dist/views/Pages/PagesListView.d.ts +18 -0
  88. package/dist/views/Pages/PagesListView.d.ts.map +1 -0
  89. package/dist/views/Pages/PagesListView.js +287 -0
  90. package/dist/views/Pages/PagesListView.js.map +1 -0
  91. package/dist/views/Posts/NewPostPicker.d.ts +16 -0
  92. package/dist/views/Posts/NewPostPicker.d.ts.map +1 -0
  93. package/dist/views/Posts/NewPostPicker.js +59 -0
  94. package/dist/views/Posts/NewPostPicker.js.map +1 -0
  95. package/dist/views/Posts/NewPostTypeInstructions.d.ts +18 -0
  96. package/dist/views/Posts/NewPostTypeInstructions.d.ts.map +1 -0
  97. package/dist/views/Posts/NewPostTypeInstructions.js +147 -0
  98. package/dist/views/Posts/NewPostTypeInstructions.js.map +1 -0
  99. package/dist/views/Posts/PostTypesView.d.ts +26 -0
  100. package/dist/views/Posts/PostTypesView.d.ts.map +1 -0
  101. package/dist/views/Posts/PostTypesView.js +52 -0
  102. package/dist/views/Posts/PostTypesView.js.map +1 -0
  103. package/dist/views/Posts/PostsListView.d.ts +25 -0
  104. package/dist/views/Posts/PostsListView.d.ts.map +1 -0
  105. package/dist/views/Posts/PostsListView.js +239 -0
  106. package/dist/views/Posts/PostsListView.js.map +1 -0
  107. package/dist/views/Posts/post-type-presets.d.ts +32 -0
  108. package/dist/views/Posts/post-type-presets.d.ts.map +1 -0
  109. package/dist/views/Posts/post-type-presets.js +72 -0
  110. package/dist/views/Posts/post-type-presets.js.map +1 -0
  111. package/dist/views/page-builder/Widgets.d.ts +13 -0
  112. package/dist/views/page-builder/Widgets.d.ts.map +1 -0
  113. package/dist/views/page-builder/Widgets.js +15 -0
  114. package/dist/views/page-builder/Widgets.js.map +1 -0
  115. package/dist/views/page-editor/AddSectionModal.d.ts +20 -0
  116. package/dist/views/page-editor/AddSectionModal.d.ts.map +1 -0
  117. package/dist/views/page-editor/AddSectionModal.js +22 -0
  118. package/dist/views/page-editor/AddSectionModal.js.map +1 -0
  119. package/dist/views/page-editor/EditorCanvas.d.ts +18 -0
  120. package/dist/views/page-editor/EditorCanvas.d.ts.map +1 -0
  121. package/dist/views/page-editor/EditorCanvas.js +64 -0
  122. package/dist/views/page-editor/EditorCanvas.js.map +1 -0
  123. package/dist/views/page-editor/EditorTopBar.d.ts +30 -0
  124. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -0
  125. package/dist/views/page-editor/EditorTopBar.js +42 -0
  126. package/dist/views/page-editor/EditorTopBar.js.map +1 -0
  127. package/dist/views/page-editor/PagePreview.d.ts +13 -0
  128. package/dist/views/page-editor/PagePreview.d.ts.map +1 -0
  129. package/dist/views/page-editor/PagePreview.js +37 -0
  130. package/dist/views/page-editor/PagePreview.js.map +1 -0
  131. package/dist/views/page-editor/PageSectionEditor.d.ts +16 -0
  132. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -0
  133. package/dist/views/page-editor/PageSectionEditor.js +272 -0
  134. package/dist/views/page-editor/PageSectionEditor.js.map +1 -0
  135. package/dist/views/page-editor/PageSettingsModal.d.ts +12 -0
  136. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -0
  137. package/dist/views/page-editor/PageSettingsModal.js +52 -0
  138. package/dist/views/page-editor/PageSettingsModal.js.map +1 -0
  139. package/dist/views/page-editor/SectionInspector.d.ts +17 -0
  140. package/dist/views/page-editor/SectionInspector.d.ts.map +1 -0
  141. package/dist/views/page-editor/SectionInspector.js +98 -0
  142. package/dist/views/page-editor/SectionInspector.js.map +1 -0
  143. package/dist/views/page-editor/SectionsPanel.d.ts +15 -0
  144. package/dist/views/page-editor/SectionsPanel.d.ts.map +1 -0
  145. package/dist/views/page-editor/SectionsPanel.js +43 -0
  146. package/dist/views/page-editor/SectionsPanel.js.map +1 -0
  147. package/dist/views/page-editor/ValidationSummary.d.ts +9 -0
  148. package/dist/views/page-editor/ValidationSummary.d.ts.map +1 -0
  149. package/dist/views/page-editor/ValidationSummary.js +10 -0
  150. package/dist/views/page-editor/ValidationSummary.js.map +1 -0
  151. package/dist/views/page-editor/section-icons.d.ts +4 -0
  152. package/dist/views/page-editor/section-icons.d.ts.map +1 -0
  153. package/dist/views/page-editor/section-icons.js +20 -0
  154. package/dist/views/page-editor/section-icons.js.map +1 -0
  155. package/dist/views/page-editor/section-types.d.ts +11 -0
  156. package/dist/views/page-editor/section-types.d.ts.map +1 -0
  157. package/dist/views/page-editor/section-types.js +10 -0
  158. package/dist/views/page-editor/section-types.js.map +1 -0
  159. package/dist/views/page-editor/sections/ArticleBodySection.d.ts +13 -0
  160. package/dist/views/page-editor/sections/ArticleBodySection.d.ts.map +1 -0
  161. package/dist/views/page-editor/sections/ArticleBodySection.js +23 -0
  162. package/dist/views/page-editor/sections/ArticleBodySection.js.map +1 -0
  163. package/dist/views/page-editor/sections/AuthorBioSection.d.ts +8 -0
  164. package/dist/views/page-editor/sections/AuthorBioSection.d.ts.map +1 -0
  165. package/dist/views/page-editor/sections/AuthorBioSection.js +21 -0
  166. package/dist/views/page-editor/sections/AuthorBioSection.js.map +1 -0
  167. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts +5 -0
  168. package/dist/views/page-editor/sections/ByTheNumbersSection.d.ts.map +1 -0
  169. package/dist/views/page-editor/sections/ByTheNumbersSection.js +10 -0
  170. package/dist/views/page-editor/sections/ByTheNumbersSection.js.map +1 -0
  171. package/dist/views/page-editor/sections/CtaBannerSection.d.ts +5 -0
  172. package/dist/views/page-editor/sections/CtaBannerSection.d.ts.map +1 -0
  173. package/dist/views/page-editor/sections/CtaBannerSection.js +10 -0
  174. package/dist/views/page-editor/sections/CtaBannerSection.js.map +1 -0
  175. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts +5 -0
  176. package/dist/views/page-editor/sections/FeatureHighlightSection.d.ts.map +1 -0
  177. package/dist/views/page-editor/sections/FeatureHighlightSection.js +18 -0
  178. package/dist/views/page-editor/sections/FeatureHighlightSection.js.map +1 -0
  179. package/dist/views/page-editor/sections/GallerySection.d.ts +6 -0
  180. package/dist/views/page-editor/sections/GallerySection.d.ts.map +1 -0
  181. package/dist/views/page-editor/sections/GallerySection.js +17 -0
  182. package/dist/views/page-editor/sections/GallerySection.js.map +1 -0
  183. package/dist/views/page-editor/sections/HeroBannerSection.d.ts +5 -0
  184. package/dist/views/page-editor/sections/HeroBannerSection.d.ts.map +1 -0
  185. package/dist/views/page-editor/sections/HeroBannerSection.js +11 -0
  186. package/dist/views/page-editor/sections/HeroBannerSection.js.map +1 -0
  187. package/dist/views/page-editor/sections/MissionSection.d.ts +6 -0
  188. package/dist/views/page-editor/sections/MissionSection.d.ts.map +1 -0
  189. package/dist/views/page-editor/sections/MissionSection.js +12 -0
  190. package/dist/views/page-editor/sections/MissionSection.js.map +1 -0
  191. package/dist/views/page-editor/sections/PullQuoteSection.d.ts +6 -0
  192. package/dist/views/page-editor/sections/PullQuoteSection.d.ts.map +1 -0
  193. package/dist/views/page-editor/sections/PullQuoteSection.js +13 -0
  194. package/dist/views/page-editor/sections/PullQuoteSection.js.map +1 -0
  195. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts +12 -0
  196. package/dist/views/page-editor/sections/RelatedPostsSection.d.ts.map +1 -0
  197. package/dist/views/page-editor/sections/RelatedPostsSection.js +23 -0
  198. package/dist/views/page-editor/sections/RelatedPostsSection.js.map +1 -0
  199. package/dist/views/page-editor/sections/SectionRenderer.d.ts +22 -0
  200. package/dist/views/page-editor/sections/SectionRenderer.d.ts.map +1 -0
  201. package/dist/views/page-editor/sections/SectionRenderer.js +52 -0
  202. package/dist/views/page-editor/sections/SectionRenderer.js.map +1 -0
  203. package/dist/views/page-editor/sections/index.d.ts +14 -0
  204. package/dist/views/page-editor/sections/index.d.ts.map +1 -0
  205. package/dist/views/page-editor/sections/index.js +12 -0
  206. package/dist/views/page-editor/sections/index.js.map +1 -0
  207. package/dist/views/page-editor/sections/parts.d.ts +55 -0
  208. package/dist/views/page-editor/sections/parts.d.ts.map +1 -0
  209. package/dist/views/page-editor/sections/parts.js +60 -0
  210. package/dist/views/page-editor/sections/parts.js.map +1 -0
  211. package/dist/views/page-editor/viewports.d.ts +14 -0
  212. package/dist/views/page-editor/viewports.d.ts.map +1 -0
  213. package/dist/views/page-editor/viewports.js +12 -0
  214. package/dist/views/page-editor/viewports.js.map +1 -0
  215. package/dist/views/post-editor/PostEditorCanvas.d.ts +21 -0
  216. package/dist/views/post-editor/PostEditorCanvas.d.ts.map +1 -0
  217. package/dist/views/post-editor/PostEditorCanvas.js +58 -0
  218. package/dist/views/post-editor/PostEditorCanvas.js.map +1 -0
  219. package/dist/views/post-editor/PostFieldsModal.d.ts +12 -0
  220. package/dist/views/post-editor/PostFieldsModal.d.ts.map +1 -0
  221. package/dist/views/post-editor/PostFieldsModal.js +53 -0
  222. package/dist/views/post-editor/PostFieldsModal.js.map +1 -0
  223. package/dist/views/post-editor/PostHeader.d.ts +15 -0
  224. package/dist/views/post-editor/PostHeader.d.ts.map +1 -0
  225. package/dist/views/post-editor/PostHeader.js +27 -0
  226. package/dist/views/post-editor/PostHeader.js.map +1 -0
  227. package/dist/views/post-editor/PostHeaderPanel.d.ts +11 -0
  228. package/dist/views/post-editor/PostHeaderPanel.d.ts.map +1 -0
  229. package/dist/views/post-editor/PostHeaderPanel.js +21 -0
  230. package/dist/views/post-editor/PostHeaderPanel.js.map +1 -0
  231. package/dist/views/post-editor/PostPreview.d.ts +14 -0
  232. package/dist/views/post-editor/PostPreview.d.ts.map +1 -0
  233. package/dist/views/post-editor/PostPreview.js +53 -0
  234. package/dist/views/post-editor/PostPreview.js.map +1 -0
  235. package/dist/views/post-editor/PostSectionEditor.d.ts +17 -0
  236. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -0
  237. package/dist/views/post-editor/PostSectionEditor.js +290 -0
  238. package/dist/views/post-editor/PostSectionEditor.js.map +1 -0
  239. package/dist/views/post-editor/PostTemplateEditor.d.ts +9 -0
  240. package/dist/views/post-editor/PostTemplateEditor.d.ts.map +1 -0
  241. package/dist/views/post-editor/PostTemplateEditor.js +144 -0
  242. package/dist/views/post-editor/PostTemplateEditor.js.map +1 -0
  243. package/package.json +2 -2
  244. package/src/AdminRoot.tsx +213 -2
  245. package/src/__tests__/components/posts-badges.test.tsx +77 -0
  246. package/src/__tests__/layout/sidebar-posts.test.ts +179 -0
  247. package/src/__tests__/lib/page-editor-service.test.ts +173 -0
  248. package/src/__tests__/lib/pages-service.test.ts +157 -0
  249. package/src/__tests__/lib/post-editor-service.test.ts +215 -0
  250. package/src/__tests__/lib/posts-service.test.ts +279 -0
  251. package/src/__tests__/views/posts-list-view.test.ts +110 -0
  252. package/src/components/pages/tag-badges.tsx +104 -0
  253. package/src/components/posts/badges.tsx +170 -0
  254. package/src/components/posts/icons.ts +40 -0
  255. package/src/layout/Header.tsx +90 -43
  256. package/src/layout/Layout.tsx +166 -1
  257. package/src/layout/Sidebar.tsx +287 -101
  258. package/src/lib/page-editor-service.ts +233 -0
  259. package/src/lib/pages-service.ts +599 -0
  260. package/src/lib/post-editor-service.ts +342 -0
  261. package/src/lib/posts-service.ts +366 -0
  262. package/src/lib/preview-link.ts +63 -0
  263. package/src/router/index.ts +10 -1
  264. package/src/views/Dashboard.tsx +270 -131
  265. package/src/views/Pages/PageTagManager.tsx +436 -0
  266. package/src/views/Pages/PagesListView.tsx +944 -0
  267. package/src/views/Posts/NewPostPicker.tsx +147 -0
  268. package/src/views/Posts/NewPostTypeInstructions.tsx +344 -0
  269. package/src/views/Posts/PostTypesView.tsx +194 -0
  270. package/src/views/Posts/PostsListView.tsx +869 -0
  271. package/src/views/Posts/post-type-presets.ts +105 -0
  272. package/src/views/page-builder/Widgets.tsx +44 -0
  273. package/src/views/page-editor/AddSectionModal.tsx +56 -0
  274. package/src/views/page-editor/EditorCanvas.tsx +127 -0
  275. package/src/views/page-editor/EditorTopBar.tsx +233 -0
  276. package/src/views/page-editor/PagePreview.tsx +96 -0
  277. package/src/views/page-editor/PageSectionEditor.tsx +476 -0
  278. package/src/views/page-editor/PageSettingsModal.tsx +154 -0
  279. package/src/views/page-editor/SectionInspector.tsx +599 -0
  280. package/src/views/page-editor/SectionsPanel.tsx +270 -0
  281. package/src/views/page-editor/ValidationSummary.tsx +53 -0
  282. package/src/views/page-editor/section-icons.ts +33 -0
  283. package/src/views/page-editor/section-types.ts +27 -0
  284. package/src/views/page-editor/sections/ArticleBodySection.tsx +57 -0
  285. package/src/views/page-editor/sections/AuthorBioSection.tsx +61 -0
  286. package/src/views/page-editor/sections/ByTheNumbersSection.tsx +41 -0
  287. package/src/views/page-editor/sections/CtaBannerSection.tsx +29 -0
  288. package/src/views/page-editor/sections/FeatureHighlightSection.tsx +63 -0
  289. package/src/views/page-editor/sections/GallerySection.tsx +54 -0
  290. package/src/views/page-editor/sections/HeroBannerSection.tsx +44 -0
  291. package/src/views/page-editor/sections/MissionSection.tsx +22 -0
  292. package/src/views/page-editor/sections/PullQuoteSection.tsx +27 -0
  293. package/src/views/page-editor/sections/RelatedPostsSection.tsx +55 -0
  294. package/src/views/page-editor/sections/SectionRenderer.tsx +102 -0
  295. package/src/views/page-editor/sections/index.ts +13 -0
  296. package/src/views/page-editor/sections/parts.tsx +113 -0
  297. package/src/views/page-editor/viewports.ts +25 -0
  298. package/src/views/post-editor/PostEditorCanvas.tsx +123 -0
  299. package/src/views/post-editor/PostFieldsModal.tsx +196 -0
  300. package/src/views/post-editor/PostHeader.tsx +87 -0
  301. package/src/views/post-editor/PostHeaderPanel.tsx +107 -0
  302. package/src/views/post-editor/PostPreview.tsx +114 -0
  303. package/src/views/post-editor/PostSectionEditor.tsx +506 -0
  304. package/src/views/post-editor/PostTemplateEditor.tsx +347 -0
@@ -0,0 +1,270 @@
1
+ 'use client'
2
+
3
+ import {
4
+ Copy,
5
+ Eye,
6
+ EyeOff,
7
+ GripVertical,
8
+ type LucideIcon,
9
+ MoreVertical,
10
+ Plus,
11
+ Trash2,
12
+ } from 'lucide-react'
13
+ import {
14
+ DndContext,
15
+ KeyboardSensor,
16
+ PointerSensor,
17
+ closestCenter,
18
+ useSensor,
19
+ useSensors,
20
+ type DragEndEvent,
21
+ } from '@dnd-kit/core'
22
+ import {
23
+ SortableContext,
24
+ sortableKeyboardCoordinates,
25
+ useSortable,
26
+ verticalListSortingStrategy,
27
+ } from '@dnd-kit/sortable'
28
+ import { CSS } from '@dnd-kit/utilities'
29
+ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
30
+ import type { PageSection } from '../../lib/page-editor-service.js'
31
+ import { sectionIconFor } from './section-icons.js'
32
+
33
+ interface SectionsPanelProps {
34
+ sections: PageSection[]
35
+ selectedId: string | null
36
+ canEdit: boolean
37
+ onSelect: (id: string) => void
38
+ onReorder: (orderedIds: string[]) => void
39
+ onToggleVisibility: (id: string) => void
40
+ onDuplicate: (id: string) => void
41
+ onRequestDelete: (id: string) => void
42
+ onAdd: () => void
43
+ }
44
+
45
+ export function SectionsPanel({
46
+ sections,
47
+ selectedId,
48
+ canEdit,
49
+ onSelect,
50
+ onReorder,
51
+ onToggleVisibility,
52
+ onDuplicate,
53
+ onRequestDelete,
54
+ onAdd,
55
+ }: SectionsPanelProps) {
56
+ const sensors = useSensors(
57
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
58
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
59
+ )
60
+
61
+ function handleDragEnd(event: DragEndEvent) {
62
+ const { active, over } = event
63
+ if (!over || active.id === over.id) return
64
+ const ids = sections.map((s) => s.id)
65
+ const from = ids.indexOf(String(active.id))
66
+ const to = ids.indexOf(String(over.id))
67
+ if (from === -1 || to === -1) return
68
+ const next = ids.slice()
69
+ const [moved] = next.splice(from, 1)
70
+ next.splice(to, 0, moved!)
71
+ onReorder(next)
72
+ }
73
+
74
+ return (
75
+ <aside
76
+ className="bg-card border-border flex h-full w-72 shrink-0 flex-col border-r"
77
+ aria-label="Page sections"
78
+ >
79
+ <div className="border-border flex items-center justify-between border-b px-4 py-3">
80
+ <div className="flex items-center gap-2">
81
+ <h2 className="text-foreground text-sm font-medium">Sections</h2>
82
+ <span className="bg-muted text-muted-foreground rounded-full px-2 py-0.5 text-xs font-medium tabular-nums">
83
+ {sections.length}
84
+ </span>
85
+ </div>
86
+ </div>
87
+
88
+ <div className="flex-1 overflow-y-auto p-2">
89
+ {sections.length === 0 ? (
90
+ <p className="text-muted-foreground px-2 py-6 text-center text-sm">
91
+ No sections yet. Add one below to get started.
92
+ </p>
93
+ ) : (
94
+ <DndContext
95
+ sensors={sensors}
96
+ collisionDetection={closestCenter}
97
+ onDragEnd={handleDragEnd}
98
+ >
99
+ <SortableContext
100
+ items={sections.map((s) => s.id)}
101
+ strategy={verticalListSortingStrategy}
102
+ >
103
+ <ul className="space-y-1">
104
+ {sections.map((section) => (
105
+ <SortableSectionItem
106
+ key={section.id}
107
+ section={section}
108
+ selected={section.id === selectedId}
109
+ canEdit={canEdit}
110
+ onSelect={onSelect}
111
+ onToggleVisibility={onToggleVisibility}
112
+ onDuplicate={onDuplicate}
113
+ onRequestDelete={onRequestDelete}
114
+ />
115
+ ))}
116
+ </ul>
117
+ </SortableContext>
118
+ </DndContext>
119
+ )}
120
+ </div>
121
+
122
+ <div className="border-border border-t p-3">
123
+ <button
124
+ type="button"
125
+ onClick={onAdd}
126
+ disabled={!canEdit}
127
+ className="border-border text-foreground hover:bg-accent flex w-full items-center justify-center gap-2 rounded-lg border border-dashed px-3 py-2 text-sm font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-50"
128
+ >
129
+ <Plus className="h-4 w-4" aria-hidden />
130
+ Add Section
131
+ </button>
132
+ </div>
133
+ </aside>
134
+ )
135
+ }
136
+
137
+ interface SortableItemProps {
138
+ section: PageSection
139
+ selected: boolean
140
+ canEdit: boolean
141
+ onSelect: (id: string) => void
142
+ onToggleVisibility: (id: string) => void
143
+ onDuplicate: (id: string) => void
144
+ onRequestDelete: (id: string) => void
145
+ }
146
+
147
+ function SortableSectionItem({
148
+ section,
149
+ selected,
150
+ canEdit,
151
+ onSelect,
152
+ onToggleVisibility,
153
+ onDuplicate,
154
+ onRequestDelete,
155
+ }: SortableItemProps) {
156
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
157
+ id: section.id,
158
+ disabled: !canEdit,
159
+ })
160
+ const Icon = sectionIconFor(section.sectionType)
161
+
162
+ const style = {
163
+ transform: CSS.Transform.toString(transform),
164
+ transition,
165
+ zIndex: isDragging ? 10 : undefined,
166
+ }
167
+
168
+ return (
169
+ <li ref={setNodeRef} style={style}>
170
+ <div
171
+ className={`group flex items-center gap-2 rounded-lg border px-2 py-2 ${
172
+ selected ? 'border-primary bg-accent' : 'hover:bg-accent/60 border-transparent'
173
+ } ${isDragging ? 'shadow-md' : ''}`}
174
+ >
175
+ <button
176
+ type="button"
177
+ className="text-muted-foreground hover:text-foreground shrink-0 cursor-grab touch-none disabled:cursor-not-allowed disabled:opacity-40"
178
+ aria-label={`Drag to reorder ${section.name}`}
179
+ disabled={!canEdit}
180
+ {...attributes}
181
+ {...listeners}
182
+ >
183
+ <GripVertical className="h-4 w-4" aria-hidden />
184
+ </button>
185
+
186
+ <button
187
+ type="button"
188
+ onClick={() => onSelect(section.id)}
189
+ className="flex min-w-0 flex-1 items-center gap-2 text-left"
190
+ >
191
+ <span className="bg-muted text-muted-foreground flex h-7 w-7 shrink-0 items-center justify-center rounded-md">
192
+ <Icon className="h-4 w-4" aria-hidden />
193
+ </span>
194
+ <span
195
+ className={`truncate text-sm ${section.visible ? 'text-foreground' : 'text-muted-foreground line-through'}`}
196
+ >
197
+ {section.name}
198
+ </span>
199
+ </button>
200
+
201
+ <button
202
+ type="button"
203
+ onClick={() => onToggleVisibility(section.id)}
204
+ disabled={!canEdit}
205
+ aria-pressed={!section.visible}
206
+ aria-label={section.visible ? `Hide ${section.name}` : `Show ${section.name}`}
207
+ className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 disabled:opacity-40"
208
+ >
209
+ {section.visible ? (
210
+ <Eye className="h-4 w-4" aria-hidden />
211
+ ) : (
212
+ <EyeOff className="h-4 w-4" aria-hidden />
213
+ )}
214
+ </button>
215
+
216
+ <DropdownMenu.Root>
217
+ <DropdownMenu.Trigger asChild>
218
+ <button
219
+ type="button"
220
+ disabled={!canEdit}
221
+ aria-label={`More actions for ${section.name}`}
222
+ className="text-muted-foreground hover:text-foreground shrink-0 rounded p-1 disabled:opacity-40"
223
+ >
224
+ <MoreVertical className="h-4 w-4" aria-hidden />
225
+ </button>
226
+ </DropdownMenu.Trigger>
227
+ <DropdownMenu.Portal>
228
+ <DropdownMenu.Content
229
+ align="end"
230
+ sideOffset={4}
231
+ className="border-border bg-popover z-[70] min-w-44 rounded-lg border p-1 shadow-md"
232
+ >
233
+ <MenuItem onSelect={() => onDuplicate(section.id)} icon={Copy}>
234
+ Duplicate
235
+ </MenuItem>
236
+ <DropdownMenu.Separator className="bg-border my-1 h-px" />
237
+ <MenuItem onSelect={() => onRequestDelete(section.id)} icon={Trash2} destructive>
238
+ Delete
239
+ </MenuItem>
240
+ </DropdownMenu.Content>
241
+ </DropdownMenu.Portal>
242
+ </DropdownMenu.Root>
243
+ </div>
244
+ </li>
245
+ )
246
+ }
247
+
248
+ function MenuItem({
249
+ children,
250
+ icon: Icon,
251
+ onSelect,
252
+ destructive,
253
+ }: {
254
+ children: React.ReactNode
255
+ icon: LucideIcon
256
+ onSelect: () => void
257
+ destructive?: boolean
258
+ }) {
259
+ return (
260
+ <DropdownMenu.Item
261
+ onSelect={onSelect}
262
+ className={`flex cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none ${
263
+ destructive ? 'text-destructive focus:bg-destructive/10' : 'text-foreground focus:bg-accent'
264
+ }`}
265
+ >
266
+ <Icon className="h-4 w-4" aria-hidden />
267
+ {children}
268
+ </DropdownMenu.Item>
269
+ )
270
+ }
@@ -0,0 +1,53 @@
1
+ 'use client'
2
+
3
+ import { TriangleAlert, X } from 'lucide-react'
4
+
5
+ interface ValidationSummaryProps {
6
+ errors: string[]
7
+ warnings: string[]
8
+ onDismiss: () => void
9
+ }
10
+
11
+ /** Inline banner listing blocking errors (and non-blocking warnings). */
12
+ export function ValidationSummary({ errors, warnings, onDismiss }: ValidationSummaryProps) {
13
+ if (errors.length === 0 && warnings.length === 0) return null
14
+ return (
15
+ <div
16
+ role="alert"
17
+ className="border-destructive/30 bg-destructive/5 mx-4 mt-3 rounded-lg border p-3"
18
+ >
19
+ <div className="flex items-start gap-2">
20
+ <TriangleAlert className="text-destructive mt-0.5 h-4 w-4 shrink-0" aria-hidden />
21
+ <div className="min-w-0 flex-1">
22
+ {errors.length > 0 && (
23
+ <>
24
+ <p className="text-foreground text-sm font-medium">
25
+ Resolve {errors.length} issue{errors.length === 1 ? '' : 's'} before publishing
26
+ </p>
27
+ <ul className="text-muted-foreground mt-1 list-disc space-y-0.5 pl-4 text-xs">
28
+ {errors.map((e, i) => (
29
+ <li key={`e-${i}`}>{e}</li>
30
+ ))}
31
+ </ul>
32
+ </>
33
+ )}
34
+ {warnings.length > 0 && (
35
+ <ul className="text-muted-foreground mt-2 list-disc space-y-0.5 pl-4 text-xs">
36
+ {warnings.map((w, i) => (
37
+ <li key={`w-${i}`}>{w}</li>
38
+ ))}
39
+ </ul>
40
+ )}
41
+ </div>
42
+ <button
43
+ type="button"
44
+ onClick={onDismiss}
45
+ aria-label="Dismiss"
46
+ className="text-muted-foreground hover:text-foreground rounded p-0.5"
47
+ >
48
+ <X className="h-4 w-4" aria-hidden />
49
+ </button>
50
+ </div>
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ AlignLeft,
3
+ BarChart3,
4
+ Images,
5
+ LayoutGrid,
6
+ LayoutTemplate,
7
+ type LucideIcon,
8
+ Megaphone,
9
+ PanelsTopLeft,
10
+ Quote,
11
+ Type,
12
+ User,
13
+ } from 'lucide-react'
14
+ import { getSectionType } from './section-types.js'
15
+
16
+ const SECTION_ICONS: Record<string, LucideIcon> = {
17
+ 'layout-template': LayoutTemplate,
18
+ 'align-left': AlignLeft,
19
+ 'panels-top-left': PanelsTopLeft,
20
+ 'bar-chart-3': BarChart3,
21
+ megaphone: Megaphone,
22
+ text: Type,
23
+ quote: Quote,
24
+ user: User,
25
+ 'layout-grid': LayoutGrid,
26
+ images: Images,
27
+ }
28
+
29
+ /** Resolve a lucide icon for a section type id (with a safe fallback). */
30
+ export function sectionIconFor(sectionType: string): LucideIcon {
31
+ const name = getSectionType(sectionType)?.icon
32
+ return (name && SECTION_ICONS[name]) || LayoutTemplate
33
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Section-type registry for the flat, section-based Page Editor.
3
+ *
4
+ * The definitions now live in `@actuate-media/cms-core/page-sections` so the
5
+ * admin editor, the REST API, and the MCP server validate against ONE source
6
+ * of truth. This module re-exports them to preserve the historical import
7
+ * path (`../views/page-editor/section-types.js`) used across the editor.
8
+ */
9
+ export {
10
+ SECTION_TYPES,
11
+ SECTION_TYPE_LIST,
12
+ getSectionType,
13
+ getSectionTypesForScope,
14
+ sectionTypeLabel,
15
+ isKnownSectionType,
16
+ } from '@actuate-media/cms-core/page-sections'
17
+
18
+ export type {
19
+ SectionTypeId,
20
+ SectionScope,
21
+ SectionFieldType,
22
+ SectionFieldDef,
23
+ SectionSettings,
24
+ SectionTypeDef,
25
+ PostHeaderConfig,
26
+ PostTemplate,
27
+ } from '@actuate-media/cms-core/page-sections'
@@ -0,0 +1,57 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { bool, str, type PostRenderContext } from './parts.js'
3
+
4
+ /**
5
+ * Renders the post's main body. When `source === 'field'` it shows the
6
+ * post's own Body field (passed via context); otherwise it renders the
7
+ * section's own `body` copy. Body text is treated as plain paragraphs split
8
+ * on blank lines — the renderer never injects raw HTML.
9
+ */
10
+ export function ArticleBodySection({
11
+ section,
12
+ context,
13
+ }: {
14
+ section: PageSection
15
+ context?: PostRenderContext
16
+ }) {
17
+ const c = section.content
18
+ const source = str(c, 'source', 'field')
19
+ const raw = source === 'field' ? (context?.body ?? '') : str(c, 'body')
20
+ const wide = str(c, 'width', 'normal') === 'wide'
21
+ const showToc = bool(c, 'showToc')
22
+
23
+ const paragraphs = raw
24
+ .split(/\n{2,}/)
25
+ .map((p) => p.trim())
26
+ .filter(Boolean)
27
+
28
+ return (
29
+ <div className={`mx-auto px-6 ${wide ? 'max-w-4xl' : 'max-w-2xl'}`}>
30
+ {showToc && (
31
+ <div className="mb-8 rounded-lg border border-zinc-200 bg-zinc-50 p-4">
32
+ <p className="text-xs font-semibold tracking-wider text-zinc-500 uppercase">
33
+ On this page
34
+ </p>
35
+ <p className="mt-1 text-sm text-zinc-400">
36
+ A table of contents will be generated from the body headings.
37
+ </p>
38
+ </div>
39
+ )}
40
+ {paragraphs.length > 0 ? (
41
+ <div className="space-y-4">
42
+ {paragraphs.map((p, i) => (
43
+ <p key={i} className="text-base leading-relaxed text-zinc-700">
44
+ {p}
45
+ </p>
46
+ ))}
47
+ </div>
48
+ ) : (
49
+ <p className="text-base leading-relaxed text-zinc-400 italic">
50
+ {source === 'field'
51
+ ? "This will show the post's Body content."
52
+ : 'Add body copy in the inspector.'}
53
+ </p>
54
+ )}
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,61 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { bool, str, type PostRenderContext } from './parts.js'
3
+
4
+ /** A short author biography block, driven by the post's author context. */
5
+ export function AuthorBioSection({
6
+ section,
7
+ context,
8
+ }: {
9
+ section: PageSection
10
+ context?: PostRenderContext
11
+ }) {
12
+ const c = section.content
13
+ const heading = str(c, 'heading', 'About the author')
14
+ const showAvatar = bool(c, 'showAvatar', true)
15
+ const showSocial = bool(c, 'showSocial', true)
16
+
17
+ const name = context?.author?.name || 'Author name'
18
+ const bio =
19
+ context?.author?.bio || "The author's bio will appear here once it is set on their profile."
20
+ const avatarUrl = context?.author?.avatarUrl
21
+ const initials = name
22
+ .split(/\s+/)
23
+ .map((p) => p[0] ?? '')
24
+ .join('')
25
+ .slice(0, 2)
26
+ .toUpperCase()
27
+
28
+ return (
29
+ <div className="mx-auto max-w-3xl px-6">
30
+ <div className="flex items-start gap-4 rounded-xl border border-zinc-200 bg-zinc-50 p-5">
31
+ {showAvatar &&
32
+ (avatarUrl ? (
33
+ <img
34
+ src={avatarUrl}
35
+ alt={name}
36
+ className="h-14 w-14 shrink-0 rounded-full object-cover"
37
+ />
38
+ ) : (
39
+ <span className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-violet-100 text-sm font-medium text-violet-700">
40
+ {initials || '?'}
41
+ </span>
42
+ ))}
43
+ <div className="min-w-0">
44
+ {heading && (
45
+ <p className="text-xs font-semibold tracking-wider text-zinc-500 uppercase">
46
+ {heading}
47
+ </p>
48
+ )}
49
+ <p className="mt-0.5 text-base font-medium text-zinc-900">{name}</p>
50
+ <p className="mt-1 text-sm leading-relaxed text-zinc-600">{bio}</p>
51
+ {showSocial && (
52
+ <div className="mt-2 flex gap-3 text-sm text-violet-600">
53
+ <span>Twitter</span>
54
+ <span>LinkedIn</span>
55
+ </div>
56
+ )}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ )
61
+ }
@@ -0,0 +1,41 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { str } from './parts.js'
3
+
4
+ interface Stat {
5
+ value?: unknown
6
+ label?: unknown
7
+ description?: unknown
8
+ }
9
+
10
+ export function ByTheNumbersSection({ section }: { section: PageSection }) {
11
+ const c = section.content
12
+ const heading = str(c, 'heading')
13
+ const body = str(c, 'body')
14
+ const stats = Array.isArray(c.stats) ? (c.stats as Stat[]) : []
15
+
16
+ return (
17
+ <div className="mx-auto max-w-5xl px-6 text-center">
18
+ {heading && (
19
+ <h2 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">
20
+ {heading}
21
+ </h2>
22
+ )}
23
+ {body && <p className="mx-auto mt-4 max-w-2xl text-base text-zinc-600">{body}</p>}
24
+ {stats.length > 0 && (
25
+ <dl className="mt-10 grid grid-cols-1 gap-8 sm:grid-cols-3">
26
+ {stats.map((s, i) => (
27
+ <div key={i}>
28
+ <dt className="text-4xl font-semibold tracking-tight text-violet-600">
29
+ {String(s.value ?? '')}
30
+ </dt>
31
+ <dd className="mt-2 text-sm font-medium text-zinc-700">{String(s.label ?? '')}</dd>
32
+ {s.description ? (
33
+ <dd className="mt-1 text-xs text-zinc-500">{String(s.description)}</dd>
34
+ ) : null}
35
+ </div>
36
+ ))}
37
+ </dl>
38
+ )}
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,29 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { PreviewButton, isDarkBackground, str } from './parts.js'
3
+
4
+ export function CtaBannerSection({ section }: { section: PageSection }) {
5
+ const c = section.content
6
+ const dark = isDarkBackground(section.settings)
7
+ const heading = str(c, 'heading')
8
+ const body = str(c, 'body')
9
+
10
+ return (
11
+ <div className="mx-auto max-w-3xl px-6 text-center">
12
+ <h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">{heading}</h2>
13
+ {body && (
14
+ <p
15
+ className={`mx-auto mt-3 max-w-xl text-base ${dark ? 'text-violet-100' : 'text-zinc-600'}`}
16
+ >
17
+ {body}
18
+ </p>
19
+ )}
20
+ <div className="mt-6 flex justify-center">
21
+ <PreviewButton
22
+ label={str(c, 'buttonLabel')}
23
+ href={str(c, 'buttonUrl')}
24
+ tone={dark ? 'onDark' : 'primary'}
25
+ />
26
+ </div>
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,63 @@
1
+ import { ImageIcon } from 'lucide-react'
2
+ import type { PageSection } from '../../../lib/page-editor-service.js'
3
+ import { PreviewButton, str } from './parts.js'
4
+
5
+ export function FeatureHighlightSection({ section }: { section: PageSection }) {
6
+ const c = section.content
7
+ const label = str(c, 'label')
8
+ const heading = str(c, 'heading')
9
+ const body = str(c, 'body')
10
+ const imageUrl = str(c, 'imageUrl')
11
+ const imageAlt = str(c, 'imageAlt')
12
+ const imageLeft = str(c, 'imagePosition', 'right') === 'left'
13
+
14
+ const text = (
15
+ <div className="flex-1">
16
+ {label && (
17
+ <span className="mb-3 inline-block rounded bg-violet-600 px-2 py-0.5 text-xs font-medium text-white">
18
+ {label}
19
+ </span>
20
+ )}
21
+ <h2 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl">{heading}</h2>
22
+ {body && <p className="mt-4 text-base leading-relaxed text-zinc-600">{body}</p>}
23
+ <div className="mt-6">
24
+ <PreviewButton label={str(c, 'buttonLabel')} href={str(c, 'buttonUrl')} tone="primary" />
25
+ </div>
26
+ </div>
27
+ )
28
+
29
+ const media = (
30
+ <div className="flex-1">
31
+ {imageUrl ? (
32
+ // Admin-only preview; next/image isn't in scope here.
33
+ <img
34
+ src={imageUrl}
35
+ alt={imageAlt}
36
+ className="aspect-[4/3] w-full rounded-xl object-cover"
37
+ />
38
+ ) : (
39
+ <div className="flex aspect-[4/3] w-full items-center justify-center rounded-xl bg-violet-50">
40
+ <ImageIcon className="h-10 w-10 text-violet-300" aria-hidden />
41
+ </div>
42
+ )}
43
+ </div>
44
+ )
45
+
46
+ return (
47
+ <div className="mx-auto max-w-5xl px-6">
48
+ <div className="flex flex-col items-center gap-10 md:flex-row">
49
+ {imageLeft ? (
50
+ <>
51
+ {media}
52
+ {text}
53
+ </>
54
+ ) : (
55
+ <>
56
+ {text}
57
+ {media}
58
+ </>
59
+ )}
60
+ </div>
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,54 @@
1
+ import type { PageSection } from '../../../lib/page-editor-service.js'
2
+ import { str } from './parts.js'
3
+
4
+ interface GalleryImage {
5
+ url?: string
6
+ alt?: string
7
+ caption?: string
8
+ }
9
+
10
+ /** A responsive image grid with captions. */
11
+ export function GallerySection({ section }: { section: PageSection }) {
12
+ const c = section.content
13
+ const heading = str(c, 'heading')
14
+ const columns = Number.parseInt(str(c, 'columns', '3'), 10) || 3
15
+ const images: GalleryImage[] = Array.isArray(c.images) ? (c.images as GalleryImage[]) : []
16
+ const withUrl = images.filter((img) => img && typeof img.url === 'string' && img.url)
17
+
18
+ const cols =
19
+ columns === 2
20
+ ? 'sm:grid-cols-2'
21
+ : columns === 4
22
+ ? 'sm:grid-cols-2 lg:grid-cols-4'
23
+ : 'sm:grid-cols-3'
24
+
25
+ return (
26
+ <div className="mx-auto max-w-5xl px-6">
27
+ {heading && (
28
+ <h2 className="mb-6 text-2xl font-semibold tracking-tight text-zinc-900">{heading}</h2>
29
+ )}
30
+ {withUrl.length > 0 ? (
31
+ <div className={`grid grid-cols-1 gap-3 ${cols}`}>
32
+ {withUrl.map((img, i) => (
33
+ <figure key={i} className="overflow-hidden rounded-lg border border-zinc-200">
34
+ <img
35
+ src={img.url}
36
+ alt={img.alt ?? ''}
37
+ className="aspect-square w-full object-cover"
38
+ />
39
+ {img.caption && (
40
+ <figcaption className="px-2 py-1.5 text-xs text-zinc-500">{img.caption}</figcaption>
41
+ )}
42
+ </figure>
43
+ ))}
44
+ </div>
45
+ ) : (
46
+ <div className={`grid grid-cols-1 gap-3 ${cols}`}>
47
+ {Array.from({ length: columns }, (_, i) => (
48
+ <div key={i} className="aspect-square rounded-lg bg-zinc-100" aria-hidden />
49
+ ))}
50
+ </div>
51
+ )}
52
+ </div>
53
+ )
54
+ }