@actuate-media/cms-admin 0.8.0 → 0.8.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 (432) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +44 -42
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/__tests__/lib/search.test.js +10 -10
  5. package/dist/__tests__/lib/search.test.js.map +1 -1
  6. package/dist/__tests__/lib/utils.test.js.map +1 -1
  7. package/dist/__tests__/router/match-route.test.js.map +1 -1
  8. package/dist/__tests__/router/strip-base.test.js.map +1 -1
  9. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  10. package/dist/components/Breadcrumbs.js +2 -4
  11. package/dist/components/Breadcrumbs.js.map +1 -1
  12. package/dist/components/CommandPalette.d.ts.map +1 -1
  13. package/dist/components/CommandPalette.js +7 -3
  14. package/dist/components/CommandPalette.js.map +1 -1
  15. package/dist/components/ContentOverviewChart.d.ts.map +1 -1
  16. package/dist/components/ContentOverviewChart.js.map +1 -1
  17. package/dist/components/ErrorBoundary.d.ts.map +1 -1
  18. package/dist/components/ErrorBoundary.js.map +1 -1
  19. package/dist/components/FocalPointPicker.d.ts.map +1 -1
  20. package/dist/components/FocalPointPicker.js +4 -2
  21. package/dist/components/FocalPointPicker.js.map +1 -1
  22. package/dist/components/FolderTree.d.ts.map +1 -1
  23. package/dist/components/FolderTree.js +18 -10
  24. package/dist/components/FolderTree.js.map +1 -1
  25. package/dist/components/LivePreview.d.ts +1 -1
  26. package/dist/components/LivePreview.d.ts.map +1 -1
  27. package/dist/components/LivePreview.js +6 -2
  28. package/dist/components/LivePreview.js.map +1 -1
  29. package/dist/components/LocaleProvider.d.ts.map +1 -1
  30. package/dist/components/LocaleProvider.js.map +1 -1
  31. package/dist/components/LocaleSwitcher.d.ts.map +1 -1
  32. package/dist/components/LocaleSwitcher.js +1 -1
  33. package/dist/components/LocaleSwitcher.js.map +1 -1
  34. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  35. package/dist/components/MediaPickerModal.js.map +1 -1
  36. package/dist/components/PresenceIndicator.d.ts.map +1 -1
  37. package/dist/components/PresenceIndicator.js +5 -2
  38. package/dist/components/PresenceIndicator.js.map +1 -1
  39. package/dist/components/SEOPanel.d.ts +1 -1
  40. package/dist/components/SEOPanel.d.ts.map +1 -1
  41. package/dist/components/SEOPanel.js +110 -24
  42. package/dist/components/SEOPanel.js.map +1 -1
  43. package/dist/components/SEOPerformance.d.ts.map +1 -1
  44. package/dist/components/SEOPerformance.js +2 -2
  45. package/dist/components/SEOPerformance.js.map +1 -1
  46. package/dist/components/ThemeProvider.d.ts.map +1 -1
  47. package/dist/components/ThemeProvider.js.map +1 -1
  48. package/dist/components/TipTapEditor.d.ts.map +1 -1
  49. package/dist/components/TipTapEditor.js +5 -1
  50. package/dist/components/TipTapEditor.js.map +1 -1
  51. package/dist/components/VersionHistory.d.ts +1 -1
  52. package/dist/components/VersionHistory.d.ts.map +1 -1
  53. package/dist/components/VersionHistory.js +1 -1
  54. package/dist/components/VersionHistory.js.map +1 -1
  55. package/dist/components/ui/Avatar.d.ts.map +1 -1
  56. package/dist/components/ui/Avatar.js.map +1 -1
  57. package/dist/components/ui/Badge.d.ts.map +1 -1
  58. package/dist/components/ui/Badge.js.map +1 -1
  59. package/dist/components/ui/Button.d.ts.map +1 -1
  60. package/dist/components/ui/Button.js.map +1 -1
  61. package/dist/components/ui/CommandPalette.d.ts.map +1 -1
  62. package/dist/components/ui/CommandPalette.js +8 -2
  63. package/dist/components/ui/CommandPalette.js.map +1 -1
  64. package/dist/components/ui/ConfirmDialog.d.ts.map +1 -1
  65. package/dist/components/ui/ConfirmDialog.js.map +1 -1
  66. package/dist/components/ui/DataTable.d.ts.map +1 -1
  67. package/dist/components/ui/DataTable.js +1 -3
  68. package/dist/components/ui/DataTable.js.map +1 -1
  69. package/dist/components/ui/EmptyState.d.ts.map +1 -1
  70. package/dist/components/ui/EmptyState.js +1 -1
  71. package/dist/components/ui/EmptyState.js.map +1 -1
  72. package/dist/components/ui/Modal.d.ts.map +1 -1
  73. package/dist/components/ui/Modal.js.map +1 -1
  74. package/dist/components/ui/Pagination.d.ts +1 -1
  75. package/dist/components/ui/Pagination.d.ts.map +1 -1
  76. package/dist/components/ui/Pagination.js +7 -2
  77. package/dist/components/ui/Pagination.js.map +1 -1
  78. package/dist/components/ui/SearchInput.d.ts.map +1 -1
  79. package/dist/components/ui/SearchInput.js.map +1 -1
  80. package/dist/components/ui/Skeleton.d.ts.map +1 -1
  81. package/dist/components/ui/Skeleton.js.map +1 -1
  82. package/dist/components/ui/Toast.d.ts.map +1 -1
  83. package/dist/components/ui/Toast.js.map +1 -1
  84. package/dist/components/ui/index.d.ts.map +1 -1
  85. package/dist/components/ui/index.js.map +1 -1
  86. package/dist/fields/ArrayField.d.ts.map +1 -1
  87. package/dist/fields/ArrayField.js +1 -1
  88. package/dist/fields/ArrayField.js.map +1 -1
  89. package/dist/fields/BlockBuilderField.d.ts.map +1 -1
  90. package/dist/fields/BlockBuilderField.js +7 -7
  91. package/dist/fields/BlockBuilderField.js.map +1 -1
  92. package/dist/fields/DateField.d.ts.map +1 -1
  93. package/dist/fields/DateField.js +1 -1
  94. package/dist/fields/DateField.js.map +1 -1
  95. package/dist/fields/FieldRenderer.d.ts.map +1 -1
  96. package/dist/fields/FieldRenderer.js.map +1 -1
  97. package/dist/fields/GroupField.d.ts.map +1 -1
  98. package/dist/fields/GroupField.js +1 -1
  99. package/dist/fields/GroupField.js.map +1 -1
  100. package/dist/fields/MediaField.d.ts.map +1 -1
  101. package/dist/fields/MediaField.js +1 -1
  102. package/dist/fields/MediaField.js.map +1 -1
  103. package/dist/fields/NavBuilderField.d.ts.map +1 -1
  104. package/dist/fields/NavBuilderField.js +2 -5
  105. package/dist/fields/NavBuilderField.js.map +1 -1
  106. package/dist/fields/NumberField.d.ts +1 -1
  107. package/dist/fields/NumberField.d.ts.map +1 -1
  108. package/dist/fields/NumberField.js +2 -2
  109. package/dist/fields/NumberField.js.map +1 -1
  110. package/dist/fields/RelationshipField.d.ts.map +1 -1
  111. package/dist/fields/RelationshipField.js +7 -3
  112. package/dist/fields/RelationshipField.js.map +1 -1
  113. package/dist/fields/RichTextField.d.ts +1 -1
  114. package/dist/fields/RichTextField.d.ts.map +1 -1
  115. package/dist/fields/RichTextField.js +2 -2
  116. package/dist/fields/RichTextField.js.map +1 -1
  117. package/dist/fields/SelectField.d.ts.map +1 -1
  118. package/dist/fields/SelectField.js +9 -7
  119. package/dist/fields/SelectField.js.map +1 -1
  120. package/dist/fields/SlugField.d.ts.map +1 -1
  121. package/dist/fields/SlugField.js +1 -1
  122. package/dist/fields/SlugField.js.map +1 -1
  123. package/dist/fields/TextField.d.ts +1 -1
  124. package/dist/fields/TextField.d.ts.map +1 -1
  125. package/dist/fields/TextField.js +2 -2
  126. package/dist/fields/TextField.js.map +1 -1
  127. package/dist/fields/ToggleField.d.ts.map +1 -1
  128. package/dist/fields/ToggleField.js +1 -1
  129. package/dist/fields/ToggleField.js.map +1 -1
  130. package/dist/fields/block-types.d.ts.map +1 -1
  131. package/dist/fields/block-types.js +28 -8
  132. package/dist/fields/block-types.js.map +1 -1
  133. package/dist/fields/index.d.ts.map +1 -1
  134. package/dist/fields/index.js.map +1 -1
  135. package/dist/hooks/useBuilderState.d.ts.map +1 -1
  136. package/dist/hooks/useBuilderState.js.map +1 -1
  137. package/dist/hooks/useContentLock.d.ts.map +1 -1
  138. package/dist/hooks/useContentLock.js.map +1 -1
  139. package/dist/hooks/useDebounce.js.map +1 -1
  140. package/dist/hooks/useKeyboardShortcuts.d.ts.map +1 -1
  141. package/dist/hooks/useKeyboardShortcuts.js.map +1 -1
  142. package/dist/index.d.ts +2 -2
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js.map +1 -1
  145. package/dist/layout/Header.d.ts.map +1 -1
  146. package/dist/layout/Header.js.map +1 -1
  147. package/dist/layout/Layout.d.ts.map +1 -1
  148. package/dist/layout/Layout.js.map +1 -1
  149. package/dist/layout/Sidebar.d.ts +1 -1
  150. package/dist/layout/Sidebar.d.ts.map +1 -1
  151. package/dist/layout/Sidebar.js +5 -8
  152. package/dist/layout/Sidebar.js.map +1 -1
  153. package/dist/lib/api.js.map +1 -1
  154. package/dist/lib/search.d.ts.map +1 -1
  155. package/dist/lib/search.js +3 -5
  156. package/dist/lib/search.js.map +1 -1
  157. package/dist/lib/useApiData.d.ts.map +1 -1
  158. package/dist/lib/useApiData.js.map +1 -1
  159. package/dist/lib/utils.d.ts.map +1 -1
  160. package/dist/lib/utils.js.map +1 -1
  161. package/dist/router/index.d.ts.map +1 -1
  162. package/dist/router/index.js +1 -3
  163. package/dist/router/index.js.map +1 -1
  164. package/dist/views/CollectionList.d.ts.map +1 -1
  165. package/dist/views/CollectionList.js +56 -17
  166. package/dist/views/CollectionList.js.map +1 -1
  167. package/dist/views/Dashboard.d.ts.map +1 -1
  168. package/dist/views/Dashboard.js +26 -13
  169. package/dist/views/Dashboard.js.map +1 -1
  170. package/dist/views/DocumentEdit.d.ts +1 -1
  171. package/dist/views/DocumentEdit.d.ts.map +1 -1
  172. package/dist/views/DocumentEdit.js +33 -15
  173. package/dist/views/DocumentEdit.js.map +1 -1
  174. package/dist/views/ForgotPassword.d.ts.map +1 -1
  175. package/dist/views/ForgotPassword.js.map +1 -1
  176. package/dist/views/FormEditor.d.ts.map +1 -1
  177. package/dist/views/FormEditor.js +8 -2
  178. package/dist/views/FormEditor.js.map +1 -1
  179. package/dist/views/FormSubmissions.d.ts.map +1 -1
  180. package/dist/views/FormSubmissions.js +6 -6
  181. package/dist/views/FormSubmissions.js.map +1 -1
  182. package/dist/views/Forms.d.ts.map +1 -1
  183. package/dist/views/Forms.js.map +1 -1
  184. package/dist/views/Login.d.ts.map +1 -1
  185. package/dist/views/Login.js +5 -2
  186. package/dist/views/Login.js.map +1 -1
  187. package/dist/views/MediaBrowser.d.ts.map +1 -1
  188. package/dist/views/MediaBrowser.js +39 -19
  189. package/dist/views/MediaBrowser.js.map +1 -1
  190. package/dist/views/PageEditor.d.ts.map +1 -1
  191. package/dist/views/PageEditor.js.map +1 -1
  192. package/dist/views/Pages.d.ts.map +1 -1
  193. package/dist/views/Pages.js +20 -10
  194. package/dist/views/Pages.js.map +1 -1
  195. package/dist/views/PostEditor.d.ts.map +1 -1
  196. package/dist/views/PostEditor.js.map +1 -1
  197. package/dist/views/Posts.d.ts.map +1 -1
  198. package/dist/views/Posts.js +13 -7
  199. package/dist/views/Posts.js.map +1 -1
  200. package/dist/views/Redirects.d.ts.map +1 -1
  201. package/dist/views/Redirects.js +17 -5
  202. package/dist/views/Redirects.js.map +1 -1
  203. package/dist/views/ResetPassword.d.ts.map +1 -1
  204. package/dist/views/ResetPassword.js.map +1 -1
  205. package/dist/views/SEO.d.ts.map +1 -1
  206. package/dist/views/SEO.js +39 -16
  207. package/dist/views/SEO.js.map +1 -1
  208. package/dist/views/ScriptTagEditor.d.ts.map +1 -1
  209. package/dist/views/ScriptTagEditor.js +2 -1
  210. package/dist/views/ScriptTagEditor.js.map +1 -1
  211. package/dist/views/ScriptTags.d.ts.map +1 -1
  212. package/dist/views/ScriptTags.js.map +1 -1
  213. package/dist/views/Settings.d.ts.map +1 -1
  214. package/dist/views/Settings.js +38 -11
  215. package/dist/views/Settings.js.map +1 -1
  216. package/dist/views/SetupWizard.d.ts.map +1 -1
  217. package/dist/views/SetupWizard.js.map +1 -1
  218. package/dist/views/Users.d.ts.map +1 -1
  219. package/dist/views/Users.js +5 -3
  220. package/dist/views/Users.js.map +1 -1
  221. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -1
  222. package/dist/views/page-builder/AIBlockAssist.js +1 -1
  223. package/dist/views/page-builder/AIBlockAssist.js.map +1 -1
  224. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -1
  225. package/dist/views/page-builder/AIGenerateDialog.js +4 -1
  226. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -1
  227. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -1
  228. package/dist/views/page-builder/BlockEditor.js +1 -1
  229. package/dist/views/page-builder/BlockEditor.js.map +1 -1
  230. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -1
  231. package/dist/views/page-builder/BlockPicker.js.map +1 -1
  232. package/dist/views/page-builder/BottomBar.d.ts.map +1 -1
  233. package/dist/views/page-builder/BottomBar.js.map +1 -1
  234. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -1
  235. package/dist/views/page-builder/BuilderToolbar.js.map +1 -1
  236. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -1
  237. package/dist/views/page-builder/ContextPanel.js +4 -1
  238. package/dist/views/page-builder/ContextPanel.js.map +1 -1
  239. package/dist/views/page-builder/DesignScore.d.ts.map +1 -1
  240. package/dist/views/page-builder/DesignScore.js.map +1 -1
  241. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -1
  242. package/dist/views/page-builder/NodeSettings.js +1 -1
  243. package/dist/views/page-builder/NodeSettings.js.map +1 -1
  244. package/dist/views/page-builder/PageBuilder.d.ts +1 -1
  245. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -1
  246. package/dist/views/page-builder/PageBuilder.js +4 -2
  247. package/dist/views/page-builder/PageBuilder.js.map +1 -1
  248. package/dist/views/page-builder/PageSettings.d.ts.map +1 -1
  249. package/dist/views/page-builder/PageSettings.js.map +1 -1
  250. package/dist/views/page-builder/PageTemplates.d.ts.map +1 -1
  251. package/dist/views/page-builder/PageTemplates.js.map +1 -1
  252. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -1
  253. package/dist/views/page-builder/SEOPanel.js +1 -3
  254. package/dist/views/page-builder/SEOPanel.js.map +1 -1
  255. package/dist/views/page-builder/SavedSections.d.ts.map +1 -1
  256. package/dist/views/page-builder/SavedSections.js +3 -7
  257. package/dist/views/page-builder/SavedSections.js.map +1 -1
  258. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -1
  259. package/dist/views/page-builder/TemplatePicker.js.map +1 -1
  260. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -1
  261. package/dist/views/page-builder/block-renderers/CTAPreview.js +1 -1
  262. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -1
  263. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -1
  264. package/dist/views/page-builder/block-renderers/CardsPreview.js +1 -1
  265. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -1
  266. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -1
  267. package/dist/views/page-builder/block-renderers/CodePreview.js +1 -5
  268. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -1
  269. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -1
  270. package/dist/views/page-builder/block-renderers/FAQPreview.js +4 -1
  271. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -1
  272. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -1
  273. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -1
  274. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -1
  275. package/dist/views/page-builder/block-renderers/FormPreview.js +2 -2
  276. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -1
  277. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -1
  278. package/dist/views/page-builder/block-renderers/GalleryPreview.js +1 -3
  279. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -1
  280. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -1
  281. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -1
  282. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -1
  283. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -1
  284. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -1
  285. package/dist/views/page-builder/block-renderers/TextPreview.js +2 -6
  286. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -1
  287. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -1
  288. package/dist/views/page-builder/block-renderers/VideoPreview.js +2 -5
  289. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -1
  290. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -1
  291. package/dist/views/page-builder/block-renderers/index.js.map +1 -1
  292. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -1
  293. package/dist/views/page-builder/canvas/BlockRenderer.js +1 -5
  294. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -1
  295. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -1
  296. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -1
  297. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -1
  298. package/dist/views/page-builder/canvas/ColumnRenderer.js +1 -5
  299. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -1
  300. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -1
  301. package/dist/views/page-builder/canvas/ContainerRenderer.js +1 -5
  302. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -1
  303. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -1
  304. package/dist/views/page-builder/canvas/RowRenderer.js +1 -5
  305. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -1
  306. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -1
  307. package/dist/views/page-builder/canvas/SectionRenderer.js +1 -5
  308. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -1
  309. package/dist/views/page-builder/canvas/index.d.ts.map +1 -1
  310. package/dist/views/page-builder/canvas/index.js.map +1 -1
  311. package/package.json +2 -2
  312. package/src/AdminRoot.tsx +263 -191
  313. package/src/__tests__/lib/search.test.ts +60 -69
  314. package/src/__tests__/lib/utils.test.ts +12 -12
  315. package/src/__tests__/router/match-route.test.ts +24 -26
  316. package/src/__tests__/router/strip-base.test.ts +15 -15
  317. package/src/components/Breadcrumbs.tsx +27 -24
  318. package/src/components/CommandPalette.tsx +115 -99
  319. package/src/components/ContentOverviewChart.tsx +19 -14
  320. package/src/components/ErrorBoundary.tsx +13 -13
  321. package/src/components/FocalPointPicker.tsx +31 -20
  322. package/src/components/FolderTree.tsx +172 -139
  323. package/src/components/LivePreview.tsx +68 -41
  324. package/src/components/LocaleProvider.tsx +26 -20
  325. package/src/components/LocaleSwitcher.tsx +9 -11
  326. package/src/components/MediaPickerModal.tsx +46 -45
  327. package/src/components/PresenceIndicator.tsx +30 -27
  328. package/src/components/SEOPanel.tsx +378 -228
  329. package/src/components/SEOPerformance.tsx +52 -30
  330. package/src/components/ThemeProvider.tsx +46 -46
  331. package/src/components/TipTapEditor.tsx +60 -64
  332. package/src/components/VersionHistory.tsx +63 -52
  333. package/src/components/ui/Avatar.tsx +8 -8
  334. package/src/components/ui/Badge.tsx +7 -5
  335. package/src/components/ui/Button.tsx +24 -13
  336. package/src/components/ui/CommandPalette.tsx +56 -42
  337. package/src/components/ui/ConfirmDialog.tsx +14 -14
  338. package/src/components/ui/DataTable.tsx +37 -39
  339. package/src/components/ui/EmptyState.tsx +9 -11
  340. package/src/components/ui/Modal.tsx +21 -15
  341. package/src/components/ui/Pagination.tsx +34 -19
  342. package/src/components/ui/SearchInput.tsx +17 -7
  343. package/src/components/ui/Skeleton.tsx +7 -7
  344. package/src/components/ui/Toast.tsx +29 -22
  345. package/src/components/ui/index.ts +24 -24
  346. package/src/fields/ArrayField.tsx +43 -25
  347. package/src/fields/BlockBuilderField.tsx +80 -99
  348. package/src/fields/DateField.tsx +20 -12
  349. package/src/fields/FieldRenderer.tsx +34 -34
  350. package/src/fields/GroupField.tsx +8 -10
  351. package/src/fields/MediaField.tsx +8 -10
  352. package/src/fields/NavBuilderField.tsx +24 -25
  353. package/src/fields/NumberField.tsx +21 -14
  354. package/src/fields/RelationshipField.tsx +105 -91
  355. package/src/fields/RichTextField.tsx +16 -12
  356. package/src/fields/SelectField.tsx +42 -34
  357. package/src/fields/SlugField.tsx +29 -17
  358. package/src/fields/TextField.tsx +24 -16
  359. package/src/fields/ToggleField.tsx +7 -9
  360. package/src/fields/block-types.ts +50 -24
  361. package/src/fields/index.ts +17 -17
  362. package/src/hooks/useBuilderState.ts +260 -221
  363. package/src/hooks/useContentLock.ts +23 -20
  364. package/src/hooks/useDebounce.ts +7 -7
  365. package/src/hooks/useKeyboardShortcuts.ts +16 -16
  366. package/src/index.ts +69 -58
  367. package/src/layout/Header.tsx +21 -20
  368. package/src/layout/Layout.tsx +22 -24
  369. package/src/layout/Sidebar.tsx +107 -72
  370. package/src/lib/api.ts +34 -34
  371. package/src/lib/search.ts +30 -34
  372. package/src/lib/useApiData.ts +65 -62
  373. package/src/lib/utils.ts +3 -3
  374. package/src/router/index.ts +33 -35
  375. package/src/styles/build-input.css +2 -2
  376. package/src/styles/tailwind.css +1 -1
  377. package/src/styles/theme.css +7 -1
  378. package/src/views/CollectionList.tsx +275 -121
  379. package/src/views/Dashboard.tsx +164 -117
  380. package/src/views/DocumentEdit.tsx +298 -253
  381. package/src/views/ForgotPassword.tsx +27 -23
  382. package/src/views/FormEditor.tsx +165 -99
  383. package/src/views/FormSubmissions.tsx +261 -117
  384. package/src/views/Forms.tsx +56 -26
  385. package/src/views/Login.tsx +107 -84
  386. package/src/views/MediaBrowser.tsx +717 -523
  387. package/src/views/PageEditor.tsx +44 -46
  388. package/src/views/Pages.tsx +312 -149
  389. package/src/views/PostEditor.tsx +57 -51
  390. package/src/views/Posts.tsx +206 -74
  391. package/src/views/Redirects.tsx +173 -117
  392. package/src/views/ResetPassword.tsx +43 -32
  393. package/src/views/SEO.tsx +589 -160
  394. package/src/views/ScriptTagEditor.tsx +69 -69
  395. package/src/views/ScriptTags.tsx +54 -42
  396. package/src/views/Settings.tsx +430 -220
  397. package/src/views/SetupWizard.tsx +69 -46
  398. package/src/views/Users.tsx +154 -120
  399. package/src/views/page-builder/AIBlockAssist.tsx +21 -25
  400. package/src/views/page-builder/AIGenerateDialog.tsx +134 -127
  401. package/src/views/page-builder/BlockEditor.tsx +94 -96
  402. package/src/views/page-builder/BlockPicker.tsx +73 -88
  403. package/src/views/page-builder/BottomBar.tsx +15 -11
  404. package/src/views/page-builder/BuilderToolbar.tsx +32 -29
  405. package/src/views/page-builder/ContextPanel.tsx +57 -57
  406. package/src/views/page-builder/DesignScore.tsx +52 -59
  407. package/src/views/page-builder/NodeSettings.tsx +59 -59
  408. package/src/views/page-builder/PageBuilder.tsx +156 -155
  409. package/src/views/page-builder/PageSettings.tsx +16 -15
  410. package/src/views/page-builder/PageTemplates.tsx +23 -17
  411. package/src/views/page-builder/SEOPanel.tsx +90 -111
  412. package/src/views/page-builder/SavedSections.tsx +99 -105
  413. package/src/views/page-builder/TemplatePicker.tsx +44 -48
  414. package/src/views/page-builder/block-renderers/CTAPreview.tsx +11 -13
  415. package/src/views/page-builder/block-renderers/CardsPreview.tsx +13 -15
  416. package/src/views/page-builder/block-renderers/CodePreview.tsx +16 -16
  417. package/src/views/page-builder/block-renderers/FAQPreview.tsx +20 -23
  418. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +5 -5
  419. package/src/views/page-builder/block-renderers/FormPreview.tsx +9 -13
  420. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +22 -28
  421. package/src/views/page-builder/block-renderers/HeroPreview.tsx +17 -30
  422. package/src/views/page-builder/block-renderers/ImagePreview.tsx +12 -12
  423. package/src/views/page-builder/block-renderers/TextPreview.tsx +22 -22
  424. package/src/views/page-builder/block-renderers/VideoPreview.tsx +13 -18
  425. package/src/views/page-builder/block-renderers/index.ts +17 -17
  426. package/src/views/page-builder/canvas/BlockRenderer.tsx +19 -23
  427. package/src/views/page-builder/canvas/BuilderCanvas.tsx +17 -20
  428. package/src/views/page-builder/canvas/ColumnRenderer.tsx +22 -26
  429. package/src/views/page-builder/canvas/ContainerRenderer.tsx +20 -24
  430. package/src/views/page-builder/canvas/RowRenderer.tsx +19 -23
  431. package/src/views/page-builder/canvas/SectionRenderer.tsx +30 -34
  432. package/src/views/page-builder/canvas/index.ts +2 -2
@@ -1,7 +1,7 @@
1
- 'use client';
1
+ 'use client'
2
2
 
3
- import * as Select from '@radix-ui/react-select';
4
- import { useState, useMemo } from 'react';
3
+ import * as Select from '@radix-ui/react-select'
4
+ import { useState, useMemo } from 'react'
5
5
  import {
6
6
  Search,
7
7
  RefreshCw,
@@ -19,84 +19,92 @@ import {
19
19
  XCircle,
20
20
  BarChart3,
21
21
  Star,
22
- } from 'lucide-react';
22
+ } from 'lucide-react'
23
23
 
24
24
  export interface SEOData {
25
- metaTitle?: string;
26
- metaDescription?: string;
27
- focusKeyphrase?: string;
28
- canonical?: string;
29
- robotsPolicy?: 'inherit' | 'index-follow' | 'noindex-follow' | 'index-nofollow' | 'noindex-nofollow';
30
- noIndex?: boolean;
31
- noFollow?: boolean;
32
- ogTitle?: string;
33
- ogDescription?: string;
34
- ogImage?: string;
35
- twitterTitle?: string;
36
- twitterDescription?: string;
37
- twitterImage?: string;
38
- isCornerstone?: boolean;
39
- schemaType?: string;
25
+ metaTitle?: string
26
+ metaDescription?: string
27
+ focusKeyphrase?: string
28
+ canonical?: string
29
+ robotsPolicy?:
30
+ | 'inherit'
31
+ | 'index-follow'
32
+ | 'noindex-follow'
33
+ | 'index-nofollow'
34
+ | 'noindex-nofollow'
35
+ noIndex?: boolean
36
+ noFollow?: boolean
37
+ ogTitle?: string
38
+ ogDescription?: string
39
+ ogImage?: string
40
+ twitterTitle?: string
41
+ twitterDescription?: string
42
+ twitterImage?: string
43
+ isCornerstone?: boolean
44
+ schemaType?: string
40
45
  }
41
46
 
42
47
  export interface SEOPanelProps {
43
- title: string;
44
- slug: string;
45
- content?: string;
46
- seoData: SEOData;
47
- onChange: (data: SEOData) => void;
48
- siteUrl?: string;
48
+ title: string
49
+ slug: string
50
+ content?: string
51
+ seoData: SEOData
52
+ onChange: (data: SEOData) => void
53
+ siteUrl?: string
49
54
  }
50
55
 
51
- const ROBOTS_POLICY_OPTIONS: Array<{ value: NonNullable<SEOData['robotsPolicy']>; label: string }> = [
52
- { value: 'inherit', label: 'Inherit site default' },
53
- { value: 'index-follow', label: 'Force index, follow' },
54
- { value: 'noindex-follow', label: 'Force noindex, follow' },
55
- { value: 'index-nofollow', label: 'Force index, nofollow' },
56
- { value: 'noindex-nofollow', label: 'Force noindex, nofollow' },
57
- ];
56
+ const ROBOTS_POLICY_OPTIONS: Array<{ value: NonNullable<SEOData['robotsPolicy']>; label: string }> =
57
+ [
58
+ { value: 'inherit', label: 'Inherit site default' },
59
+ { value: 'index-follow', label: 'Force index, follow' },
60
+ { value: 'noindex-follow', label: 'Force noindex, follow' },
61
+ { value: 'index-nofollow', label: 'Force index, nofollow' },
62
+ { value: 'noindex-nofollow', label: 'Force noindex, nofollow' },
63
+ ]
58
64
 
59
65
  function getRobotsPolicy(seoData: SEOData): NonNullable<SEOData['robotsPolicy']> {
60
- if (seoData.robotsPolicy) return seoData.robotsPolicy;
66
+ if (seoData.robotsPolicy) return seoData.robotsPolicy
61
67
  if (typeof seoData.noIndex === 'boolean' || typeof seoData.noFollow === 'boolean') {
62
- const noIndex = seoData.noIndex === true;
63
- const noFollow = seoData.noFollow === true;
64
- if (noIndex && noFollow) return 'noindex-nofollow';
65
- if (noIndex) return 'noindex-follow';
66
- if (noFollow) return 'index-nofollow';
67
- return 'index-follow';
68
+ const noIndex = seoData.noIndex === true
69
+ const noFollow = seoData.noFollow === true
70
+ if (noIndex && noFollow) return 'noindex-nofollow'
71
+ if (noIndex) return 'noindex-follow'
72
+ if (noFollow) return 'index-nofollow'
73
+ return 'index-follow'
68
74
  }
69
- return 'inherit';
75
+ return 'inherit'
70
76
  }
71
77
 
72
- function robotsPolicyToBooleans(policy: NonNullable<SEOData['robotsPolicy']>): Pick<SEOData, 'noIndex' | 'noFollow'> {
78
+ function robotsPolicyToBooleans(
79
+ policy: NonNullable<SEOData['robotsPolicy']>,
80
+ ): Pick<SEOData, 'noIndex' | 'noFollow'> {
73
81
  switch (policy) {
74
82
  case 'index-follow':
75
- return { noIndex: false, noFollow: false };
83
+ return { noIndex: false, noFollow: false }
76
84
  case 'noindex-follow':
77
- return { noIndex: true, noFollow: false };
85
+ return { noIndex: true, noFollow: false }
78
86
  case 'index-nofollow':
79
- return { noIndex: false, noFollow: true };
87
+ return { noIndex: false, noFollow: true }
80
88
  case 'noindex-nofollow':
81
- return { noIndex: true, noFollow: true };
89
+ return { noIndex: true, noFollow: true }
82
90
  default:
83
- return { noIndex: undefined, noFollow: undefined };
91
+ return { noIndex: undefined, noFollow: undefined }
84
92
  }
85
93
  }
86
94
 
87
95
  interface SEOCheck {
88
- id: string;
89
- label: string;
90
- status: 'good' | 'ok' | 'bad';
91
- detail: string;
96
+ id: string
97
+ label: string
98
+ status: 'good' | 'ok' | 'bad'
99
+ detail: string
92
100
  }
93
101
 
94
102
  interface ReadabilityResult {
95
- fleschScore: number;
96
- avgSentenceLength: number;
97
- wordCount: number;
98
- readingTime: number;
99
- passiveEstimate: number;
103
+ fleschScore: number
104
+ avgSentenceLength: number
105
+ wordCount: number
106
+ readingTime: number
107
+ passiveEstimate: number
100
108
  }
101
109
 
102
110
  const SCHEMA_TYPES = [
@@ -109,216 +117,284 @@ const SCHEMA_TYPES = [
109
117
  'LocalBusiness',
110
118
  'HowTo',
111
119
  'Recipe',
112
- ] as const;
120
+ ] as const
113
121
 
114
122
  function stripHtml(html: string): string {
115
- return html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
123
+ return html
124
+ .replace(/<[^>]*>/g, ' ')
125
+ .replace(/\s+/g, ' ')
126
+ .trim()
116
127
  }
117
128
 
118
129
  function countWords(text: string): number {
119
- if (!text.trim()) return 0;
120
- return text.trim().split(/\s+/).length;
130
+ if (!text.trim()) return 0
131
+ return text.trim().split(/\s+/).length
121
132
  }
122
133
 
123
134
  function countSentences(text: string): number {
124
- if (!text.trim()) return 0;
125
- const matches = text.match(/[.!?]+/g);
126
- return matches ? matches.length : 1;
135
+ if (!text.trim()) return 0
136
+ const matches = text.match(/[.!?]+/g)
137
+ return matches ? matches.length : 1
127
138
  }
128
139
 
129
140
  function countSyllables(word: string): number {
130
- const w = word.toLowerCase().replace(/[^a-z]/g, '');
131
- if (w.length <= 3) return 1;
132
- let count = 0;
133
- const vowels = 'aeiouy';
134
- let prevVowel = false;
141
+ const w = word.toLowerCase().replace(/[^a-z]/g, '')
142
+ if (w.length <= 3) return 1
143
+ let count = 0
144
+ const vowels = 'aeiouy'
145
+ let prevVowel = false
135
146
  for (let i = 0; i < w.length; i++) {
136
- const isVowel = vowels.includes(w[i] ?? '');
137
- if (isVowel && !prevVowel) count++;
138
- prevVowel = isVowel;
147
+ const isVowel = vowels.includes(w[i] ?? '')
148
+ if (isVowel && !prevVowel) count++
149
+ prevVowel = isVowel
139
150
  }
140
- if (w.endsWith('e') && count > 1) count--;
141
- return Math.max(count, 1);
151
+ if (w.endsWith('e') && count > 1) count--
152
+ return Math.max(count, 1)
142
153
  }
143
154
 
144
155
  function analyzeReadability(text: string): ReadabilityResult {
145
- const plainText = stripHtml(text);
146
- const wordCount = countWords(plainText);
147
- const sentenceCount = countSentences(plainText);
148
- const avgSentenceLength = sentenceCount > 0 ? wordCount / sentenceCount : 0;
149
- const readingTime = Math.max(1, Math.ceil(wordCount / 200));
156
+ const plainText = stripHtml(text)
157
+ const wordCount = countWords(plainText)
158
+ const sentenceCount = countSentences(plainText)
159
+ const avgSentenceLength = sentenceCount > 0 ? wordCount / sentenceCount : 0
160
+ const readingTime = Math.max(1, Math.ceil(wordCount / 200))
150
161
 
151
- let fleschScore = 0;
162
+ let fleschScore = 0
152
163
  if (wordCount > 0 && sentenceCount > 0) {
153
- const words = plainText.split(/\s+/);
154
- const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0);
164
+ const words = plainText.split(/\s+/)
165
+ const totalSyllables = words.reduce((sum, w) => sum + countSyllables(w), 0)
155
166
  fleschScore = Math.round(
156
- 206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)
157
- );
158
- fleschScore = Math.max(0, Math.min(100, fleschScore));
167
+ 206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount),
168
+ )
169
+ fleschScore = Math.max(0, Math.min(100, fleschScore))
159
170
  }
160
171
 
161
- const passivePatterns = /\b(is|are|was|were|been|being|be)\s+\w+ed\b/gi;
162
- const passiveMatches = plainText.match(passivePatterns);
172
+ const passivePatterns = /\b(is|are|was|were|been|being|be)\s+\w+ed\b/gi
173
+ const passiveMatches = plainText.match(passivePatterns)
163
174
  const passiveEstimate = passiveMatches
164
175
  ? Math.round((passiveMatches.length / Math.max(sentenceCount, 1)) * 100)
165
- : 0;
176
+ : 0
166
177
 
167
- return { fleschScore, avgSentenceLength, wordCount, readingTime, passiveEstimate };
178
+ return { fleschScore, avgSentenceLength, wordCount, readingTime, passiveEstimate }
168
179
  }
169
180
 
170
- function runSEOChecks(
171
- seoData: SEOData,
172
- title: string,
173
- slug: string,
174
- content: string
175
- ): SEOCheck[] {
176
- const checks: SEOCheck[] = [];
177
- const plainText = stripHtml(content);
178
- const wordCount = countWords(plainText);
179
- const keyphrase = (seoData.focusKeyphrase ?? '').toLowerCase().trim();
180
- const metaTitle = seoData.metaTitle ?? '';
181
- const metaDesc = seoData.metaDescription ?? '';
181
+ function runSEOChecks(seoData: SEOData, title: string, slug: string, content: string): SEOCheck[] {
182
+ const checks: SEOCheck[] = []
183
+ const plainText = stripHtml(content)
184
+ const wordCount = countWords(plainText)
185
+ const keyphrase = (seoData.focusKeyphrase ?? '').toLowerCase().trim()
186
+ const metaTitle = seoData.metaTitle ?? ''
187
+ const metaDesc = seoData.metaDescription ?? ''
182
188
 
183
189
  // Meta title length
184
190
  if (!metaTitle) {
185
- checks.push({ id: 'title-missing', label: 'Meta title', status: 'bad', detail: 'No meta title set' });
191
+ checks.push({
192
+ id: 'title-missing',
193
+ label: 'Meta title',
194
+ status: 'bad',
195
+ detail: 'No meta title set',
196
+ })
186
197
  } else if (metaTitle.length >= 30 && metaTitle.length <= 60) {
187
- checks.push({ id: 'title-length', label: 'Meta title length', status: 'good', detail: `${metaTitle.length} chars (ideal: 30-60)` });
198
+ checks.push({
199
+ id: 'title-length',
200
+ label: 'Meta title length',
201
+ status: 'good',
202
+ detail: `${metaTitle.length} chars (ideal: 30-60)`,
203
+ })
188
204
  } else if (metaTitle.length > 60) {
189
- checks.push({ id: 'title-length', label: 'Meta title length', status: 'ok', detail: `${metaTitle.length} chars — too long, may be truncated` });
205
+ checks.push({
206
+ id: 'title-length',
207
+ label: 'Meta title length',
208
+ status: 'ok',
209
+ detail: `${metaTitle.length} chars — too long, may be truncated`,
210
+ })
190
211
  } else {
191
- checks.push({ id: 'title-length', label: 'Meta title length', status: 'ok', detail: `${metaTitle.length} chars — quite short` });
212
+ checks.push({
213
+ id: 'title-length',
214
+ label: 'Meta title length',
215
+ status: 'ok',
216
+ detail: `${metaTitle.length} chars — quite short`,
217
+ })
192
218
  }
193
219
 
194
220
  // Meta description length
195
221
  if (!metaDesc) {
196
- checks.push({ id: 'desc-missing', label: 'Meta description', status: 'bad', detail: 'No meta description set' });
222
+ checks.push({
223
+ id: 'desc-missing',
224
+ label: 'Meta description',
225
+ status: 'bad',
226
+ detail: 'No meta description set',
227
+ })
197
228
  } else if (metaDesc.length >= 120 && metaDesc.length <= 160) {
198
- checks.push({ id: 'desc-length', label: 'Meta description length', status: 'good', detail: `${metaDesc.length} chars (ideal: 120-160)` });
229
+ checks.push({
230
+ id: 'desc-length',
231
+ label: 'Meta description length',
232
+ status: 'good',
233
+ detail: `${metaDesc.length} chars (ideal: 120-160)`,
234
+ })
199
235
  } else if (metaDesc.length > 160) {
200
- checks.push({ id: 'desc-length', label: 'Meta description length', status: 'ok', detail: `${metaDesc.length} chars — may be truncated` });
236
+ checks.push({
237
+ id: 'desc-length',
238
+ label: 'Meta description length',
239
+ status: 'ok',
240
+ detail: `${metaDesc.length} chars — may be truncated`,
241
+ })
201
242
  } else {
202
- checks.push({ id: 'desc-length', label: 'Meta description length', status: 'ok', detail: `${metaDesc.length} chars — could be longer` });
243
+ checks.push({
244
+ id: 'desc-length',
245
+ label: 'Meta description length',
246
+ status: 'ok',
247
+ detail: `${metaDesc.length} chars — could be longer`,
248
+ })
203
249
  }
204
250
 
205
251
  // Content length
206
252
  if (wordCount >= 300) {
207
- checks.push({ id: 'content-length', label: 'Content length', status: 'good', detail: `${wordCount} words` });
253
+ checks.push({
254
+ id: 'content-length',
255
+ label: 'Content length',
256
+ status: 'good',
257
+ detail: `${wordCount} words`,
258
+ })
208
259
  } else if (wordCount >= 150) {
209
- checks.push({ id: 'content-length', label: 'Content length', status: 'ok', detail: `${wordCount} words — aim for 300+` });
260
+ checks.push({
261
+ id: 'content-length',
262
+ label: 'Content length',
263
+ status: 'ok',
264
+ detail: `${wordCount} words — aim for 300+`,
265
+ })
210
266
  } else {
211
- checks.push({ id: 'content-length', label: 'Content length', status: 'bad', detail: `${wordCount} words — too short` });
267
+ checks.push({
268
+ id: 'content-length',
269
+ label: 'Content length',
270
+ status: 'bad',
271
+ detail: `${wordCount} words — too short`,
272
+ })
212
273
  }
213
274
 
214
275
  // Focus keyphrase checks
215
276
  if (!keyphrase) {
216
- checks.push({ id: 'keyphrase-missing', label: 'Focus keyphrase', status: 'bad', detail: 'No focus keyphrase set' });
277
+ checks.push({
278
+ id: 'keyphrase-missing',
279
+ label: 'Focus keyphrase',
280
+ status: 'bad',
281
+ detail: 'No focus keyphrase set',
282
+ })
217
283
  } else {
218
- const inTitle = metaTitle.toLowerCase().includes(keyphrase);
284
+ const inTitle = metaTitle.toLowerCase().includes(keyphrase)
219
285
  checks.push({
220
286
  id: 'keyphrase-title',
221
287
  label: 'Keyphrase in title',
222
288
  status: inTitle ? 'good' : 'bad',
223
289
  detail: inTitle ? 'Found in meta title' : 'Not found in meta title',
224
- });
290
+ })
225
291
 
226
- const inDesc = metaDesc.toLowerCase().includes(keyphrase);
292
+ const inDesc = metaDesc.toLowerCase().includes(keyphrase)
227
293
  checks.push({
228
294
  id: 'keyphrase-desc',
229
295
  label: 'Keyphrase in description',
230
296
  status: inDesc ? 'good' : 'ok',
231
297
  detail: inDesc ? 'Found in meta description' : 'Not found in meta description',
232
- });
298
+ })
233
299
 
234
- const inSlug = slug.toLowerCase().includes(keyphrase.replace(/\s+/g, '-'));
300
+ const inSlug = slug.toLowerCase().includes(keyphrase.replace(/\s+/g, '-'))
235
301
  checks.push({
236
302
  id: 'keyphrase-slug',
237
303
  label: 'Keyphrase in slug',
238
304
  status: inSlug ? 'good' : 'ok',
239
305
  detail: inSlug ? 'Found in URL slug' : 'Not found in URL slug',
240
- });
306
+ })
241
307
 
242
- const firstParagraph = plainText.slice(0, 300).toLowerCase();
243
- const inIntro = firstParagraph.includes(keyphrase);
308
+ const firstParagraph = plainText.slice(0, 300).toLowerCase()
309
+ const inIntro = firstParagraph.includes(keyphrase)
244
310
  checks.push({
245
311
  id: 'keyphrase-intro',
246
312
  label: 'Keyphrase in introduction',
247
313
  status: inIntro ? 'good' : 'ok',
248
314
  detail: inIntro ? 'Found in first paragraph' : 'Not found in first paragraph',
249
- });
315
+ })
250
316
 
251
317
  if (wordCount > 0) {
252
- const kpWords = keyphrase.split(/\s+/).length;
253
- const regex = new RegExp(keyphrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
254
- const matches = plainText.match(regex);
255
- const occurrences = matches ? matches.length : 0;
256
- const density = (occurrences * kpWords) / wordCount * 100;
257
- const densityOk = density >= 0.5 && density <= 3;
318
+ const kpWords = keyphrase.split(/\s+/).length
319
+ const regex = new RegExp(keyphrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi')
320
+ const matches = plainText.match(regex)
321
+ const occurrences = matches ? matches.length : 0
322
+ const density = ((occurrences * kpWords) / wordCount) * 100
323
+ const densityOk = density >= 0.5 && density <= 3
258
324
  checks.push({
259
325
  id: 'keyphrase-density',
260
326
  label: 'Keyphrase density',
261
327
  status: densityOk ? 'good' : density === 0 ? 'bad' : 'ok',
262
328
  detail: `${density.toFixed(1)}% (aim for 0.5-3%)`,
263
- });
329
+ })
264
330
  }
265
331
  }
266
332
 
267
333
  // Image alt text
268
- const imgTags = content.match(/<img[^>]*>/gi) ?? [];
334
+ const imgTags = content.match(/<img[^>]*>/gi) ?? []
269
335
  if (imgTags.length > 0) {
270
- const withoutAlt = imgTags.filter(tag => !tag.match(/alt\s*=\s*["'][^"']+["']/i));
336
+ const withoutAlt = imgTags.filter((tag) => !tag.match(/alt\s*=\s*["'][^"']+["']/i))
271
337
  if (withoutAlt.length === 0) {
272
- checks.push({ id: 'img-alt', label: 'Image alt text', status: 'good', detail: 'All images have alt text' });
338
+ checks.push({
339
+ id: 'img-alt',
340
+ label: 'Image alt text',
341
+ status: 'good',
342
+ detail: 'All images have alt text',
343
+ })
273
344
  } else {
274
- checks.push({ id: 'img-alt', label: 'Image alt text', status: 'ok', detail: `${withoutAlt.length} image(s) missing alt text` });
345
+ checks.push({
346
+ id: 'img-alt',
347
+ label: 'Image alt text',
348
+ status: 'ok',
349
+ detail: `${withoutAlt.length} image(s) missing alt text`,
350
+ })
275
351
  }
276
352
  }
277
353
 
278
354
  // OG checks
279
- const hasOgTitle = !!(seoData.ogTitle || metaTitle);
280
- const hasOgDesc = !!(seoData.ogDescription || metaDesc);
281
- const hasOgImage = !!seoData.ogImage;
355
+ const hasOgTitle = !!(seoData.ogTitle || metaTitle)
356
+ const hasOgDesc = !!(seoData.ogDescription || metaDesc)
357
+ const hasOgImage = !!seoData.ogImage
282
358
  checks.push({
283
359
  id: 'og-title',
284
360
  label: 'Social title',
285
361
  status: hasOgTitle ? 'good' : 'ok',
286
362
  detail: hasOgTitle ? 'Set' : 'Missing — will fall back to page title',
287
- });
363
+ })
288
364
  checks.push({
289
365
  id: 'og-desc',
290
366
  label: 'Social description',
291
367
  status: hasOgDesc ? 'good' : 'ok',
292
368
  detail: hasOgDesc ? 'Set' : 'Missing — will fall back to meta description',
293
- });
369
+ })
294
370
  checks.push({
295
371
  id: 'og-image',
296
372
  label: 'Social image',
297
373
  status: hasOgImage ? 'good' : 'bad',
298
374
  detail: hasOgImage ? 'Set' : 'No OG image set — strongly recommended',
299
- });
375
+ })
300
376
 
301
- return checks;
377
+ return checks
302
378
  }
303
379
 
304
380
  function computeOverallScore(checks: SEOCheck[]): number {
305
- if (checks.length === 0) return 0;
306
- const scoreMap = { good: 100, ok: 50, bad: 0 };
307
- const total = checks.reduce((sum, c) => sum + scoreMap[c.status], 0);
308
- return Math.round(total / checks.length);
381
+ if (checks.length === 0) return 0
382
+ const scoreMap = { good: 100, ok: 50, bad: 0 }
383
+ const total = checks.reduce((sum, c) => sum + scoreMap[c.status], 0)
384
+ return Math.round(total / checks.length)
309
385
  }
310
386
 
311
387
  function StatusDot({ status }: { status: 'good' | 'ok' | 'bad' }) {
312
- if (status === 'good') return <CheckCircle2 className="h-4 w-4 shrink-0 text-green-500" />;
313
- if (status === 'ok') return <AlertCircle className="h-4 w-4 shrink-0 text-amber-500" />;
314
- return <XCircle className="h-4 w-4 shrink-0 text-red-500" />;
388
+ if (status === 'good') return <CheckCircle2 className="h-4 w-4 shrink-0 text-green-500" />
389
+ if (status === 'ok') return <AlertCircle className="h-4 w-4 shrink-0 text-amber-500" />
390
+ return <XCircle className="h-4 w-4 shrink-0 text-red-500" />
315
391
  }
316
392
 
317
393
  function ScoreRing({ score }: { score: number }) {
318
- const radius = 28;
319
- const circumference = 2 * Math.PI * radius;
320
- const offset = circumference - (score / 100) * circumference;
321
- const color = score >= 70 ? '#22c55e' : score >= 40 ? '#f59e0b' : '#ef4444';
394
+ const radius = 28
395
+ const circumference = 2 * Math.PI * radius
396
+ const offset = circumference - (score / 100) * circumference
397
+ const color = score >= 70 ? '#22c55e' : score >= 40 ? '#f59e0b' : '#ef4444'
322
398
 
323
399
  return (
324
400
  <div className="relative inline-flex items-center justify-center">
@@ -337,9 +413,11 @@ function ScoreRing({ score }: { score: number }) {
337
413
  className="transition-all duration-500"
338
414
  />
339
415
  </svg>
340
- <span className="absolute text-sm font-bold" style={{ color }}>{score}</span>
416
+ <span className="absolute text-sm font-bold" style={{ color }}>
417
+ {score}
418
+ </span>
341
419
  </div>
342
- );
420
+ )
343
421
  }
344
422
 
345
423
  function Section({
@@ -351,13 +429,13 @@ function Section({
351
429
  children,
352
430
  badge,
353
431
  }: {
354
- id: string;
355
- title: string;
356
- icon: React.ReactNode;
357
- expanded: boolean;
358
- onToggle: (id: string) => void;
359
- children: React.ReactNode;
360
- badge?: React.ReactNode;
432
+ id: string
433
+ title: string
434
+ icon: React.ReactNode
435
+ expanded: boolean
436
+ onToggle: (id: string) => void
437
+ children: React.ReactNode
438
+ badge?: React.ReactNode
361
439
  }) {
362
440
  return (
363
441
  <div className="border border-[var(--border)] rounded-lg overflow-hidden">
@@ -370,11 +448,15 @@ function Section({
370
448
  {title}
371
449
  {badge}
372
450
  </span>
373
- {expanded ? <ChevronUp className="h-4 w-4 text-[var(--muted-foreground)]" /> : <ChevronDown className="h-4 w-4 text-[var(--muted-foreground)]" />}
451
+ {expanded ? (
452
+ <ChevronUp className="h-4 w-4 text-[var(--muted-foreground)]" />
453
+ ) : (
454
+ <ChevronDown className="h-4 w-4 text-[var(--muted-foreground)]" />
455
+ )}
374
456
  </button>
375
457
  {expanded && <div className="px-4 pb-4 pt-1">{children}</div>}
376
458
  </div>
377
- );
459
+ )
378
460
  }
379
461
 
380
462
  function ToggleSwitch({
@@ -383,10 +465,10 @@ function ToggleSwitch({
383
465
  checked,
384
466
  onChange,
385
467
  }: {
386
- label: string;
387
- description: string;
388
- checked: boolean;
389
- onChange: (v: boolean) => void;
468
+ label: string
469
+ description: string
470
+ checked: boolean
471
+ onChange: (v: boolean) => void
390
472
  }) {
391
473
  return (
392
474
  <div className="flex items-center justify-between gap-3">
@@ -401,10 +483,12 @@ function ToggleSwitch({
401
483
  onClick={() => onChange(!checked)}
402
484
  className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full transition-colors ${checked ? 'bg-[var(--primary)]' : 'bg-[var(--muted)]'}`}
403
485
  >
404
- <span className={`pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform mt-0.5 ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`} />
486
+ <span
487
+ className={`pointer-events-none block h-5 w-5 rounded-full bg-white shadow-sm transition-transform mt-0.5 ${checked ? 'translate-x-[22px]' : 'translate-x-0.5'}`}
488
+ />
405
489
  </button>
406
490
  </div>
407
- );
491
+ )
408
492
  }
409
493
 
410
494
  function InputField({
@@ -416,17 +500,19 @@ function InputField({
416
500
  charCount,
417
501
  charTarget,
418
502
  }: {
419
- label: string;
420
- value: string;
421
- onChange: (v: string) => void;
422
- placeholder?: string;
423
- type?: string;
424
- charCount?: number;
425
- charTarget?: string;
503
+ label: string
504
+ value: string
505
+ onChange: (v: string) => void
506
+ placeholder?: string
507
+ type?: string
508
+ charCount?: number
509
+ charTarget?: string
426
510
  }) {
427
511
  return (
428
512
  <div>
429
- <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">{label}</label>
513
+ <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
514
+ {label}
515
+ </label>
430
516
  <input
431
517
  type={type}
432
518
  value={value}
@@ -435,10 +521,12 @@ function InputField({
435
521
  placeholder={placeholder}
436
522
  />
437
523
  {charCount !== undefined && charTarget && (
438
- <p className="text-xs mt-1 text-[var(--muted-foreground)]">{charCount} chars {charTarget}</p>
524
+ <p className="text-xs mt-1 text-[var(--muted-foreground)]">
525
+ {charCount} chars {charTarget}
526
+ </p>
439
527
  )}
440
528
  </div>
441
- );
529
+ )
442
530
  }
443
531
 
444
532
  function TextareaField({
@@ -450,17 +538,19 @@ function TextareaField({
450
538
  charCount,
451
539
  charTarget,
452
540
  }: {
453
- label: string;
454
- value: string;
455
- onChange: (v: string) => void;
456
- placeholder?: string;
457
- rows?: number;
458
- charCount?: number;
459
- charTarget?: string;
541
+ label: string
542
+ value: string
543
+ onChange: (v: string) => void
544
+ placeholder?: string
545
+ rows?: number
546
+ charCount?: number
547
+ charTarget?: string
460
548
  }) {
461
549
  return (
462
550
  <div>
463
- <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">{label}</label>
551
+ <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
552
+ {label}
553
+ </label>
464
554
  <textarea
465
555
  value={value}
466
556
  onChange={(e) => onChange(e.target.value)}
@@ -469,42 +559,65 @@ function TextareaField({
469
559
  placeholder={placeholder}
470
560
  />
471
561
  {charCount !== undefined && charTarget && (
472
- <p className="text-xs mt-1 text-[var(--muted-foreground)]">{charCount} chars {charTarget}</p>
562
+ <p className="text-xs mt-1 text-[var(--muted-foreground)]">
563
+ {charCount} chars {charTarget}
564
+ </p>
473
565
  )}
474
566
  </div>
475
- );
567
+ )
476
568
  }
477
569
 
478
- export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl = 'https://example.com' }: SEOPanelProps) {
479
- const [expandedSections, setExpandedSections] = useState<string[]>(['analysis']);
570
+ export function SEOPanel({
571
+ title,
572
+ slug,
573
+ content = '',
574
+ seoData,
575
+ onChange,
576
+ siteUrl = 'https://example.com',
577
+ }: SEOPanelProps) {
578
+ const [expandedSections, setExpandedSections] = useState<string[]>(['analysis'])
480
579
 
481
580
  const update = (partial: Partial<SEOData>) => {
482
- onChange({ ...seoData, ...partial });
483
- };
581
+ onChange({ ...seoData, ...partial })
582
+ }
484
583
 
485
584
  const toggleSection = (id: string) => {
486
585
  setExpandedSections((prev) =>
487
- prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]
488
- );
489
- };
490
-
491
- const checks = useMemo(() => runSEOChecks(seoData, title, slug, content), [seoData, title, slug, content]);
492
- const readability = useMemo(() => analyzeReadability(content), [content]);
493
- const score = useMemo(() => computeOverallScore(checks), [checks]);
494
-
495
- const goodCount = checks.filter((c) => c.status === 'good').length;
496
- const okCount = checks.filter((c) => c.status === 'ok').length;
497
- const badCount = checks.filter((c) => c.status === 'bad').length;
498
-
499
- const metaTitle = seoData.metaTitle ?? '';
500
- const metaDesc = seoData.metaDescription ?? '';
501
- const robotsPolicy = getRobotsPolicy(seoData);
502
- const displayTitle = seoData.ogTitle || metaTitle || title || 'Page Title';
503
- const displayDesc = seoData.ogDescription || metaDesc || 'Add a meta description to see how this page will appear in search results.';
586
+ prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id],
587
+ )
588
+ }
589
+
590
+ const checks = useMemo(
591
+ () => runSEOChecks(seoData, title, slug, content),
592
+ [seoData, title, slug, content],
593
+ )
594
+ const readability = useMemo(() => analyzeReadability(content), [content])
595
+ const score = useMemo(() => computeOverallScore(checks), [checks])
596
+
597
+ const goodCount = checks.filter((c) => c.status === 'good').length
598
+ const okCount = checks.filter((c) => c.status === 'ok').length
599
+ const badCount = checks.filter((c) => c.status === 'bad').length
600
+
601
+ const metaTitle = seoData.metaTitle ?? ''
602
+ const metaDesc = seoData.metaDescription ?? ''
603
+ const robotsPolicy = getRobotsPolicy(seoData)
604
+ const displayTitle = seoData.ogTitle || metaTitle || title || 'Page Title'
605
+ const displayDesc =
606
+ seoData.ogDescription ||
607
+ metaDesc ||
608
+ 'Add a meta description to see how this page will appear in search results.'
504
609
  const fleschLabel =
505
- readability.fleschScore >= 60 ? 'Easy to read' : readability.fleschScore >= 30 ? 'Fairly difficult' : 'Very difficult';
610
+ readability.fleschScore >= 60
611
+ ? 'Easy to read'
612
+ : readability.fleschScore >= 30
613
+ ? 'Fairly difficult'
614
+ : 'Very difficult'
506
615
  const fleschColor =
507
- readability.fleschScore >= 60 ? 'text-green-500' : readability.fleschScore >= 30 ? 'text-amber-500' : 'text-red-500';
616
+ readability.fleschScore >= 60
617
+ ? 'text-green-500'
618
+ : readability.fleschScore >= 30
619
+ ? 'text-amber-500'
620
+ : 'text-red-500'
508
621
 
509
622
  return (
510
623
  <div className="rounded-lg border border-[var(--border)] bg-[var(--card)]">
@@ -527,9 +640,15 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
527
640
  <div className="flex items-center gap-4 px-4 py-3 border-b border-[var(--border)]">
528
641
  <ScoreRing score={score} />
529
642
  <div className="text-xs text-[var(--muted-foreground)] space-y-0.5">
530
- <div className="flex items-center gap-1.5"><CheckCircle2 className="h-3 w-3 text-green-500" /> {goodCount} passed</div>
531
- <div className="flex items-center gap-1.5"><AlertCircle className="h-3 w-3 text-amber-500" /> {okCount} improvements</div>
532
- <div className="flex items-center gap-1.5"><XCircle className="h-3 w-3 text-red-500" /> {badCount} issues</div>
643
+ <div className="flex items-center gap-1.5">
644
+ <CheckCircle2 className="h-3 w-3 text-green-500" /> {goodCount} passed
645
+ </div>
646
+ <div className="flex items-center gap-1.5">
647
+ <AlertCircle className="h-3 w-3 text-amber-500" /> {okCount} improvements
648
+ </div>
649
+ <div className="flex items-center gap-1.5">
650
+ <XCircle className="h-3 w-3 text-red-500" /> {badCount} issues
651
+ </div>
533
652
  </div>
534
653
  </div>
535
654
 
@@ -582,18 +701,28 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
582
701
  </div>
583
702
  <div className="rounded-lg bg-[var(--muted)] p-2.5">
584
703
  <p className="text-xs text-[var(--muted-foreground)]">Avg. Sentence</p>
585
- <p className="text-lg font-bold text-[var(--foreground)]">{readability.avgSentenceLength.toFixed(1)}</p>
704
+ <p className="text-lg font-bold text-[var(--foreground)]">
705
+ {readability.avgSentenceLength.toFixed(1)}
706
+ </p>
586
707
  <p className="text-[10px] text-[var(--muted-foreground)]">words/sentence</p>
587
708
  </div>
588
709
  <div className="rounded-lg bg-[var(--muted)] p-2.5">
589
710
  <p className="text-xs text-[var(--muted-foreground)]">Reading Time</p>
590
- <p className="text-lg font-bold text-[var(--foreground)]">{readability.readingTime}</p>
711
+ <p className="text-lg font-bold text-[var(--foreground)]">
712
+ {readability.readingTime}
713
+ </p>
591
714
  <p className="text-[10px] text-[var(--muted-foreground)]">min</p>
592
715
  </div>
593
716
  </div>
594
717
  <div className="mt-3 flex items-center gap-2 text-xs text-[var(--muted-foreground)]">
595
718
  <span>Passive voice est.:</span>
596
- <span className={readability.passiveEstimate > 15 ? 'text-amber-500 font-medium' : 'text-green-500 font-medium'}>
719
+ <span
720
+ className={
721
+ readability.passiveEstimate > 15
722
+ ? 'text-amber-500 font-medium'
723
+ : 'text-green-500 font-medium'
724
+ }
725
+ >
597
726
  {readability.passiveEstimate}%
598
727
  </span>
599
728
  </div>
@@ -650,14 +779,17 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
650
779
  >
651
780
  <div className="space-y-4">
652
781
  <div>
653
- <label id="robots-policy-label" className="mb-1 block text-xs font-medium text-muted-foreground">
782
+ <label
783
+ id="robots-policy-label"
784
+ className="mb-1 block text-xs font-medium text-muted-foreground"
785
+ >
654
786
  Robots Policy
655
787
  </label>
656
788
  <Select.Root
657
789
  value={robotsPolicy}
658
790
  onValueChange={(value) => {
659
- const policy = value as NonNullable<SEOData['robotsPolicy']>;
660
- update({ robotsPolicy: policy, ...robotsPolicyToBooleans(policy) });
791
+ const policy = value as NonNullable<SEOData['robotsPolicy']>
792
+ update({ robotsPolicy: policy, ...robotsPolicyToBooleans(policy) })
661
793
  }}
662
794
  >
663
795
  <Select.Trigger
@@ -689,7 +821,8 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
689
821
  </Select.Portal>
690
822
  </Select.Root>
691
823
  <p className="mt-1 text-xs text-muted-foreground">
692
- Use inheritance for most pages. Override only when a page needs different index/follow behavior.
824
+ Use inheritance for most pages. Override only when a page needs different
825
+ index/follow behavior.
693
826
  </p>
694
827
  </div>
695
828
  </div>
@@ -711,7 +844,8 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
711
844
  {siteUrl}/{slug}
712
845
  </div>
713
846
  <div className="text-sm text-[var(--muted-foreground)] mt-1 line-clamp-2">
714
- {metaDesc || 'Add a meta description to see how this page will appear in search results.'}
847
+ {metaDesc ||
848
+ 'Add a meta description to see how this page will appear in search results.'}
715
849
  </div>
716
850
  </div>
717
851
  </Section>
@@ -747,7 +881,9 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
747
881
  />
748
882
 
749
883
  <div className="border-t border-[var(--border)] pt-3 mt-3">
750
- <p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">Twitter / X Overrides</p>
884
+ <p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
885
+ Twitter / X Overrides
886
+ </p>
751
887
  <div className="space-y-3">
752
888
  <InputField
753
889
  label="Twitter Title"
@@ -774,11 +910,17 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
774
910
 
775
911
  {/* Social preview card */}
776
912
  <div className="mt-3">
777
- <p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">Social Preview</p>
913
+ <p className="text-xs font-medium text-[var(--muted-foreground)] mb-2">
914
+ Social Preview
915
+ </p>
778
916
  <div className="rounded-lg border border-[var(--border)] overflow-hidden bg-[var(--muted)]">
779
917
  {seoData.ogImage ? (
780
918
  <div className="aspect-video bg-[var(--muted)] flex items-center justify-center overflow-hidden">
781
- <img src={seoData.ogImage} alt="OG preview" className="w-full h-full object-cover" />
919
+ <img
920
+ src={seoData.ogImage}
921
+ alt="OG preview"
922
+ className="w-full h-full object-cover"
923
+ />
782
924
  </div>
783
925
  ) : (
784
926
  <div className="aspect-video bg-[var(--muted)] flex items-center justify-center text-[var(--muted-foreground)] text-sm">
@@ -786,11 +928,15 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
786
928
  </div>
787
929
  )}
788
930
  <div className="p-3">
789
- <div className="text-sm font-medium text-[var(--foreground)] line-clamp-1">{displayTitle}</div>
931
+ <div className="text-sm font-medium text-[var(--foreground)] line-clamp-1">
932
+ {displayTitle}
933
+ </div>
790
934
  <div className="text-xs text-[var(--muted-foreground)] mt-1 line-clamp-2">
791
935
  {displayDesc.slice(0, 100)}
792
936
  </div>
793
- <div className="text-xs text-[var(--muted-foreground)] mt-1 truncate">{siteUrl}</div>
937
+ <div className="text-xs text-[var(--muted-foreground)] mt-1 truncate">
938
+ {siteUrl}
939
+ </div>
794
940
  </div>
795
941
  </div>
796
942
  </div>
@@ -819,14 +965,18 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
819
965
  </div>
820
966
  )}
821
967
  <div>
822
- <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">Schema Type</label>
968
+ <label className="block text-xs font-medium text-[var(--muted-foreground)] mb-1">
969
+ Schema Type
970
+ </label>
823
971
  <select
824
972
  value={seoData.schemaType ?? 'Article'}
825
973
  onChange={(e) => update({ schemaType: e.target.value })}
826
974
  className="w-full px-3 py-1.5 text-sm border border-[var(--border)] rounded-lg bg-[var(--background)] text-[var(--foreground)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
827
975
  >
828
976
  {SCHEMA_TYPES.map((t) => (
829
- <option key={t} value={t}>{t}</option>
977
+ <option key={t} value={t}>
978
+ {t}
979
+ </option>
830
980
  ))}
831
981
  </select>
832
982
  </div>
@@ -834,5 +984,5 @@ export function SEOPanel({ title, slug, content = '', seoData, onChange, siteUrl
834
984
  </Section>
835
985
  </div>
836
986
  </div>
837
- );
987
+ )
838
988
  }