@jini-ai/admin 0.4.2 → 0.4.3

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 (360) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +7 -0
  3. package/dist/agent-plugins/react/components/AgentPluginListPanel.d.ts +1 -1
  4. package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +1 -1
  5. package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +1 -1
  6. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +1 -1
  7. package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +1 -1
  8. package/dist/agent-plugins/react/hooks/MarketplaceTab.hooks.d.ts +1 -1
  9. package/dist/agent-plugins/react/index.js +1 -1
  10. package/dist/agent-plugins/react/pages/AgentPluginsPage.d.ts +1 -1
  11. package/dist/agent-plugins/react/tabs/DownloadedTab.d.ts +1 -1
  12. package/dist/agent-plugins/react/tabs/InstalledTab.d.ts +1 -1
  13. package/dist/agent-plugins/react/tabs/MarketplaceTab.d.ts +1 -1
  14. package/dist/comments/adapters/http.d.ts +29 -0
  15. package/dist/comments/adapters/http.js +50 -0
  16. package/dist/comments/adapters/memory.d.ts +17 -0
  17. package/dist/comments/adapters/memory.js +61 -0
  18. package/dist/comments/comments.module.d.ts +73 -0
  19. package/dist/comments/comments.module.js +16 -0
  20. package/dist/comments/conformance/comments-api.conformance.d.ts +9 -0
  21. package/dist/comments/conformance/comments-api.conformance.js +63 -0
  22. package/dist/comments/controllers/comment-queue.controller.d.ts +48 -0
  23. package/dist/comments/controllers/comment-queue.controller.js +164 -0
  24. package/dist/comments/controllers/comment-settings.controller.d.ts +32 -0
  25. package/dist/comments/controllers/comment-settings.controller.js +77 -0
  26. package/dist/comments/controllers/comments.controller.d.ts +16 -0
  27. package/dist/comments/controllers/comments.controller.js +16 -0
  28. package/dist/comments/index.d.ts +13 -0
  29. package/dist/comments/index.js +8 -0
  30. package/dist/comments/messages.en.d.ts +54 -0
  31. package/dist/comments/messages.en.js +56 -0
  32. package/dist/comments/models.d.ts +22 -0
  33. package/dist/comments/models.js +2 -0
  34. package/dist/comments/ports.d.ts +14 -0
  35. package/dist/comments/ports.js +5 -0
  36. package/dist/comments/react/components/QueueSection.d.ts +30 -0
  37. package/dist/comments/react/components/QueueSection.js +57 -0
  38. package/dist/comments/react/components/SettingsSection.d.ts +14 -0
  39. package/dist/comments/react/components/SettingsSection.js +23 -0
  40. package/dist/comments/react/hooks/CommentsPage.hooks.d.ts +9 -0
  41. package/dist/comments/react/hooks/CommentsPage.hooks.js +10 -0
  42. package/dist/comments/react/hooks/CommentsPorts.hooks.d.ts +13 -0
  43. package/dist/comments/react/hooks/CommentsPorts.hooks.js +15 -0
  44. package/dist/comments/react/hooks/QueueSection.hooks.d.ts +62 -0
  45. package/dist/comments/react/hooks/QueueSection.hooks.js +57 -0
  46. package/dist/comments/react/hooks/SettingsSection.hooks.d.ts +12 -0
  47. package/dist/comments/react/hooks/SettingsSection.hooks.js +21 -0
  48. package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +34 -0
  49. package/dist/comments/react/hooks/use-comment-queue.hooks.js +47 -0
  50. package/dist/comments/react/hooks/use-comment-settings.hooks.d.ts +22 -0
  51. package/dist/comments/react/hooks/use-comment-settings.hooks.js +30 -0
  52. package/dist/comments/react/hooks/use-comments.hooks.d.ts +15 -0
  53. package/dist/comments/react/hooks/use-comments.hooks.js +20 -0
  54. package/dist/comments/react/index.d.ts +316 -0
  55. package/dist/comments/react/index.js +35 -0
  56. package/dist/comments/react/pages/CommentsPage.d.ts +12 -0
  57. package/dist/comments/react/pages/CommentsPage.js +15 -0
  58. package/dist/comments/rules.d.ts +140 -0
  59. package/dist/comments/rules.js +199 -0
  60. package/dist/core/index.d.ts +3 -1
  61. package/dist/core/index.js +1 -0
  62. package/dist/core/ledger-slice.d.ts +90 -0
  63. package/dist/core/ledger-slice.js +81 -0
  64. package/dist/core/ports/comments.d.ts +2 -0
  65. package/dist/core/ports/forms.d.ts +19 -7
  66. package/dist/core/ports/forms.js +4 -4
  67. package/dist/core/ports/index.d.ts +1 -1
  68. package/dist/core/ports/seo.d.ts +33 -6
  69. package/dist/core/react/bind-react.d.ts +46 -0
  70. package/dist/core/react/bind-react.js +28 -0
  71. package/dist/core/react/index.d.ts +3 -0
  72. package/dist/core/react/index.js +3 -0
  73. package/dist/core/react/use-controller.d.ts +16 -0
  74. package/dist/core/react/use-controller.js +18 -0
  75. package/dist/forms/adapters/http.d.ts +26 -0
  76. package/dist/forms/adapters/http.js +43 -0
  77. package/dist/forms/adapters/memory.d.ts +14 -0
  78. package/dist/forms/adapters/memory.js +77 -0
  79. package/dist/forms/conformance/forms-api.conformance.d.ts +17 -0
  80. package/dist/forms/conformance/forms-api.conformance.js +71 -0
  81. package/dist/forms/controllers/draft-store.d.ts +18 -0
  82. package/dist/forms/controllers/draft-store.js +17 -0
  83. package/dist/forms/controllers/field-attributes-dialog.controller.d.ts +37 -0
  84. package/dist/forms/controllers/field-attributes-dialog.controller.js +27 -0
  85. package/dist/forms/controllers/form-editor.controller.d.ts +36 -0
  86. package/dist/forms/controllers/form-editor.controller.js +22 -0
  87. package/dist/forms/controllers/form-fields-editor.controller.d.ts +20 -0
  88. package/dist/forms/controllers/form-fields-editor.controller.js +10 -0
  89. package/dist/forms/controllers/form-submission-detail.controller.d.ts +20 -0
  90. package/dist/forms/controllers/form-submission-detail.controller.js +10 -0
  91. package/dist/forms/controllers/form-submissions.controller.d.ts +25 -0
  92. package/dist/forms/controllers/form-submissions.controller.js +10 -0
  93. package/dist/forms/controllers/forms-list.controller.d.ts +22 -0
  94. package/dist/forms/controllers/forms-list.controller.js +10 -0
  95. package/dist/forms/forms.module.d.ts +137 -0
  96. package/dist/forms/forms.module.js +17 -0
  97. package/dist/forms/html-rules.d.ts +20 -0
  98. package/dist/forms/html-rules.js +24 -0
  99. package/dist/forms/index.d.ts +14 -0
  100. package/dist/forms/index.js +12 -0
  101. package/dist/forms/messages.en.d.ts +100 -0
  102. package/dist/forms/messages.en.js +104 -0
  103. package/dist/forms/models.d.ts +29 -0
  104. package/dist/forms/models.js +8 -0
  105. package/dist/forms/ports.d.ts +26 -0
  106. package/dist/forms/ports.js +6 -0
  107. package/dist/forms/react/components/FieldAttributesDialog.d.ts +14 -0
  108. package/dist/forms/react/components/FieldAttributesDialog.js +47 -0
  109. package/dist/forms/react/components/FormEditorFieldsBody.d.ts +28 -0
  110. package/dist/forms/react/components/FormEditorFieldsBody.js +24 -0
  111. package/dist/forms/react/components/FormEditorHeaderText.d.ts +7 -0
  112. package/dist/forms/react/components/FormEditorHeaderText.js +8 -0
  113. package/dist/forms/react/components/FormEditorMainPanel.d.ts +12 -0
  114. package/dist/forms/react/components/FormEditorMainPanel.js +13 -0
  115. package/dist/forms/react/components/FormEditorTabStrip.d.ts +13 -0
  116. package/dist/forms/react/components/FormEditorTabStrip.js +14 -0
  117. package/dist/forms/react/components/FormFieldsEditor.d.ts +13 -0
  118. package/dist/forms/react/components/FormFieldsEditor.js +39 -0
  119. package/dist/forms/react/components/FormSubmissionDate.d.ts +8 -0
  120. package/dist/forms/react/components/FormSubmissionDate.js +5 -0
  121. package/dist/forms/react/components/FormSubmissionDetail.d.ts +13 -0
  122. package/dist/forms/react/components/FormSubmissionDetail.js +29 -0
  123. package/dist/forms/react/components/FormSubmissions.d.ts +12 -0
  124. package/dist/forms/react/components/FormSubmissions.js +46 -0
  125. package/dist/forms/react/hooks/FormsPorts.hooks.d.ts +41 -0
  126. package/dist/forms/react/hooks/FormsPorts.hooks.js +16 -0
  127. package/dist/forms/react/hooks/forms-view.hooks.d.ts +122 -0
  128. package/dist/forms/react/hooks/forms-view.hooks.js +79 -0
  129. package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.d.ts +31 -0
  130. package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +22 -0
  131. package/dist/forms/react/hooks/use-form-editor.hooks.d.ts +118 -0
  132. package/dist/forms/react/hooks/use-form-editor.hooks.js +208 -0
  133. package/dist/forms/react/hooks/use-form-fields-editor.hooks.d.ts +34 -0
  134. package/dist/forms/react/hooks/use-form-fields-editor.hooks.js +36 -0
  135. package/dist/forms/react/hooks/use-form-submission-date.hooks.d.ts +12 -0
  136. package/dist/forms/react/hooks/use-form-submission-date.hooks.js +9 -0
  137. package/dist/forms/react/hooks/use-form-submission-detail.hooks.d.ts +68 -0
  138. package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +81 -0
  139. package/dist/forms/react/hooks/use-form-submissions.hooks.d.ts +67 -0
  140. package/dist/forms/react/hooks/use-form-submissions.hooks.js +110 -0
  141. package/dist/forms/react/hooks/use-forms-list.hooks.d.ts +78 -0
  142. package/dist/forms/react/hooks/use-forms-list.hooks.js +103 -0
  143. package/dist/forms/react/index.d.ts +449 -0
  144. package/dist/forms/react/index.js +35 -0
  145. package/dist/forms/react/pages/FormEditor.d.ts +19 -0
  146. package/dist/forms/react/pages/FormEditor.js +40 -0
  147. package/dist/forms/react/pages/FormsList.d.ts +39 -0
  148. package/dist/forms/react/pages/FormsList.js +55 -0
  149. package/dist/forms/rules.d.ts +291 -0
  150. package/dist/forms/rules.js +338 -0
  151. package/dist/media/react/hooks/EditMediaPanel.hooks.js +1 -1
  152. package/dist/media/react/hooks/LibraryTab.hooks.d.ts +1 -1
  153. package/dist/media/react/hooks/LibraryTab.hooks.js +1 -1
  154. package/dist/media/react/hooks/MediaCard.hooks.js +6 -20
  155. package/dist/media/react/hooks/MediaGrid.hooks.d.ts +2 -2
  156. package/dist/media/react/hooks/MediaPage.hooks.d.ts +1 -1
  157. package/dist/media/react/hooks/MediaPage.hooks.js +1 -1
  158. package/dist/media/react/hooks/MediaPicker.hooks.js +1 -1
  159. package/dist/media/react/index.js +1 -1
  160. package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
  161. package/dist/media/react/tabs/ProvidersTab.d.ts +1 -1
  162. package/dist/playground/react/components/Canvas.d.ts +1 -1
  163. package/dist/playground/react/hooks/Canvas.hooks.d.ts +1 -1
  164. package/dist/playground/react/hooks/Canvas.hooks.js +1 -1
  165. package/dist/playground/react/hooks/PlaygroundPage.hooks.d.ts +1 -1
  166. package/dist/playground/react/index.js +1 -1
  167. package/dist/playground/react/pages/PlaygroundPage.d.ts +1 -1
  168. package/dist/playground/react/tabs/CanvasTab.d.ts +1 -1
  169. package/dist/react/bind-react.d.ts +1 -45
  170. package/dist/react/bind-react.js +2 -27
  171. package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts +22 -5
  172. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts +2 -2
  173. package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js +17 -55
  174. package/dist/react/components/ConfirmDialog/ConfirmDialog.js +33 -70
  175. package/dist/react/use-controller.d.ts +1 -15
  176. package/dist/react/use-controller.js +2 -17
  177. package/dist/redirects/adapters/http.d.ts +20 -0
  178. package/dist/redirects/adapters/http.js +31 -0
  179. package/dist/redirects/adapters/memory.d.ts +13 -0
  180. package/dist/redirects/adapters/memory.js +83 -0
  181. package/dist/redirects/conformance/redirects-api.conformance.d.ts +12 -0
  182. package/dist/redirects/conformance/redirects-api.conformance.js +66 -0
  183. package/dist/redirects/controllers/hit-count.controller.d.ts +14 -0
  184. package/dist/redirects/controllers/hit-count.controller.js +10 -0
  185. package/dist/redirects/controllers/import-redirects.controller.d.ts +28 -0
  186. package/dist/redirects/controllers/import-redirects.controller.js +29 -0
  187. package/dist/redirects/controllers/redirects.controller.d.ts +37 -0
  188. package/dist/redirects/controllers/redirects.controller.js +38 -0
  189. package/dist/redirects/index.d.ts +11 -0
  190. package/dist/redirects/index.js +10 -0
  191. package/dist/redirects/messages.en.d.ts +56 -0
  192. package/dist/redirects/messages.en.js +55 -0
  193. package/dist/redirects/models.d.ts +14 -0
  194. package/dist/redirects/models.js +2 -0
  195. package/dist/redirects/ports.d.ts +9 -0
  196. package/dist/redirects/ports.js +4 -0
  197. package/dist/redirects/react/components/HitCountCell.d.ts +19 -0
  198. package/dist/redirects/react/components/HitCountCell.js +19 -0
  199. package/dist/redirects/react/components/ImportRedirectsForm.d.ts +17 -0
  200. package/dist/redirects/react/components/ImportRedirectsForm.js +12 -0
  201. package/dist/redirects/react/hooks/RedirectsOptions.hooks.d.ts +27 -0
  202. package/dist/redirects/react/hooks/RedirectsOptions.hooks.js +15 -0
  203. package/dist/redirects/react/hooks/RedirectsPage.hooks.d.ts +50 -0
  204. package/dist/redirects/react/hooks/RedirectsPage.hooks.js +30 -0
  205. package/dist/redirects/react/hooks/RedirectsPorts.hooks.d.ts +10 -0
  206. package/dist/redirects/react/hooks/RedirectsPorts.hooks.js +11 -0
  207. package/dist/redirects/react/hooks/use-hit-count-cell.hooks.d.ts +19 -0
  208. package/dist/redirects/react/hooks/use-hit-count-cell.hooks.js +15 -0
  209. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +21 -0
  210. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +25 -0
  211. package/dist/redirects/react/hooks/use-redirects.hooks.d.ts +34 -0
  212. package/dist/redirects/react/hooks/use-redirects.hooks.js +63 -0
  213. package/dist/redirects/react/hooks/wired.hooks.d.ts +55 -0
  214. package/dist/redirects/react/hooks/wired.hooks.js +31 -0
  215. package/dist/redirects/react/index.d.ts +316 -0
  216. package/dist/redirects/react/index.js +28 -0
  217. package/dist/redirects/react/pages/RedirectsPage.d.ts +15 -0
  218. package/dist/redirects/react/pages/RedirectsPage.js +20 -0
  219. package/dist/redirects/redirects.module.d.ts +75 -0
  220. package/dist/redirects/redirects.module.js +13 -0
  221. package/dist/redirects/rules.d.ts +131 -0
  222. package/dist/redirects/rules.js +134 -0
  223. package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +1 -1
  224. package/dist/security/react/hooks/AccessTokensTab.hooks.js +1 -1
  225. package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +1 -1
  226. package/dist/security/react/hooks/RootKeyTab.hooks.js +1 -1
  227. package/dist/security/react/hooks/SecurityPage.hooks.d.ts +1 -1
  228. package/dist/security/react/index.js +1 -1
  229. package/dist/security/react/pages/SecurityPage.d.ts +1 -1
  230. package/dist/security/react/tabs/AccessTokensTab.d.ts +1 -1
  231. package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
  232. package/dist/seo/adapters/http.d.ts +31 -0
  233. package/dist/seo/adapters/http.js +37 -0
  234. package/dist/seo/adapters/memory.d.ts +28 -0
  235. package/dist/seo/adapters/memory.js +129 -0
  236. package/dist/seo/conformance/seo-api.conformance.d.ts +8 -0
  237. package/dist/seo/conformance/seo-api.conformance.js +44 -0
  238. package/dist/seo/controllers/entry-picker.controller.d.ts +21 -0
  239. package/dist/seo/controllers/entry-picker.controller.js +17 -0
  240. package/dist/seo/controllers/entry-section.controller.d.ts +20 -0
  241. package/dist/seo/controllers/entry-section.controller.js +7 -0
  242. package/dist/seo/controllers/media-ref.controller.d.ts +26 -0
  243. package/dist/seo/controllers/media-ref.controller.js +14 -0
  244. package/dist/seo/controllers/seo-entry.controller.d.ts +31 -0
  245. package/dist/seo/controllers/seo-entry.controller.js +52 -0
  246. package/dist/seo/controllers/seo.controller.d.ts +29 -0
  247. package/dist/seo/controllers/seo.controller.js +50 -0
  248. package/dist/seo/controllers/sitemap.controller.d.ts +25 -0
  249. package/dist/seo/controllers/sitemap.controller.js +27 -0
  250. package/dist/seo/index.d.ts +14 -0
  251. package/dist/seo/index.js +12 -0
  252. package/dist/seo/messages.en.d.ts +78 -0
  253. package/dist/seo/messages.en.js +78 -0
  254. package/dist/seo/models.d.ts +179 -0
  255. package/dist/seo/models.js +8 -0
  256. package/dist/seo/ports.d.ts +11 -0
  257. package/dist/seo/ports.js +4 -0
  258. package/dist/seo/react/components/MediaPickerBridge.d.ts +4 -0
  259. package/dist/seo/react/components/MediaPickerBridge.js +8 -0
  260. package/dist/seo/react/components/MediaRefField.d.ts +36 -0
  261. package/dist/seo/react/components/MediaRefField.js +12 -0
  262. package/dist/seo/react/components/SeoIcons.d.ts +13 -0
  263. package/dist/seo/react/components/SeoIcons.js +32 -0
  264. package/dist/seo/react/components/SitemapModal.d.ts +48 -0
  265. package/dist/seo/react/components/SitemapModal.js +70 -0
  266. package/dist/seo/react/hooks/MediaPickerBridge.hooks.d.ts +6 -0
  267. package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +27 -0
  268. package/dist/seo/react/hooks/MediaRefField.hooks.d.ts +22 -0
  269. package/dist/seo/react/hooks/MediaRefField.hooks.js +30 -0
  270. package/dist/seo/react/hooks/SeoModuleTabs.d.ts +14 -0
  271. package/dist/seo/react/hooks/SeoModuleTabs.js +18 -0
  272. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +69 -0
  273. package/dist/seo/react/hooks/SeoPage.hooks.js +37 -0
  274. package/dist/seo/react/hooks/SeoPorts.hooks.d.ts +19 -0
  275. package/dist/seo/react/hooks/SeoPorts.hooks.js +22 -0
  276. package/dist/seo/react/hooks/SitemapModal.hooks.d.ts +26 -0
  277. package/dist/seo/react/hooks/SitemapModal.hooks.js +20 -0
  278. package/dist/seo/react/hooks/use-entry-picker.hooks.d.ts +11 -0
  279. package/dist/seo/react/hooks/use-entry-picker.hooks.js +20 -0
  280. package/dist/seo/react/hooks/use-seo-entry-panel.hooks.d.ts +14 -0
  281. package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +26 -0
  282. package/dist/seo/react/hooks/use-seo-entry-section.hooks.d.ts +4 -0
  283. package/dist/seo/react/hooks/use-seo-entry-section.hooks.js +7 -0
  284. package/dist/seo/react/hooks/use-seo.hooks.d.ts +11 -0
  285. package/dist/seo/react/hooks/use-seo.hooks.js +29 -0
  286. package/dist/seo/react/hooks/use-sitemap-modal.hooks.d.ts +10 -0
  287. package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +18 -0
  288. package/dist/seo/react/index.d.ts +366 -0
  289. package/dist/seo/react/index.js +30 -0
  290. package/dist/seo/react/options.d.ts +19 -0
  291. package/dist/seo/react/options.js +2 -0
  292. package/dist/seo/react/pages/SeoPage.d.ts +161 -0
  293. package/dist/seo/react/pages/SeoPage.js +215 -0
  294. package/dist/seo/rules.d.ts +136 -0
  295. package/dist/seo/rules.js +212 -0
  296. package/dist/seo/seo.module.d.ts +114 -0
  297. package/dist/seo/seo.module.js +14 -0
  298. package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +1 -1
  299. package/dist/source-control/react/hooks/ProvidersTab.hooks.js +1 -1
  300. package/dist/source-control/react/hooks/SourceControlPage.hooks.d.ts +1 -1
  301. package/dist/source-control/react/index.js +1 -1
  302. package/dist/source-control/react/pages/SourceControlPage.d.ts +1 -1
  303. package/dist/source-control/react/tabs/ProvidersTab.d.ts +1 -1
  304. package/dist/widgets/adapters/http.d.ts +19 -0
  305. package/dist/widgets/adapters/http.js +33 -0
  306. package/dist/widgets/adapters/memory.d.ts +22 -0
  307. package/dist/widgets/adapters/memory.js +93 -0
  308. package/dist/widgets/conformance/widgets-api.conformance.d.ts +6 -0
  309. package/dist/widgets/conformance/widgets-api.conformance.js +55 -0
  310. package/dist/widgets/controllers/instance-editor.controller.d.ts +49 -0
  311. package/dist/widgets/controllers/instance-editor.controller.js +87 -0
  312. package/dist/widgets/controllers/library.controller.d.ts +33 -0
  313. package/dist/widgets/controllers/library.controller.js +53 -0
  314. package/dist/widgets/controllers/region-editor.controller.d.ts +42 -0
  315. package/dist/widgets/controllers/region-editor.controller.js +69 -0
  316. package/dist/widgets/controllers/regions.controller.d.ts +27 -0
  317. package/dist/widgets/controllers/regions.controller.js +44 -0
  318. package/dist/widgets/index.d.ts +11 -0
  319. package/dist/widgets/index.js +9 -0
  320. package/dist/widgets/messages.en.d.ts +78 -0
  321. package/dist/widgets/messages.en.js +83 -0
  322. package/dist/widgets/models.d.ts +62 -0
  323. package/dist/widgets/models.js +2 -0
  324. package/dist/widgets/ports.d.ts +79 -0
  325. package/dist/widgets/ports.js +5 -0
  326. package/dist/widgets/react/components/WidgetInstanceNotices.d.ts +14 -0
  327. package/dist/widgets/react/components/WidgetInstanceNotices.js +25 -0
  328. package/dist/widgets/react/components/WidgetRegionEditorHeaderActions.d.ts +22 -0
  329. package/dist/widgets/react/components/WidgetRegionEditorHeaderActions.js +18 -0
  330. package/dist/widgets/react/components/WidgetsLibraryNotices.d.ts +13 -0
  331. package/dist/widgets/react/components/WidgetsLibraryNotices.js +10 -0
  332. package/dist/widgets/react/hooks/WidgetsPorts.hooks.d.ts +270 -0
  333. package/dist/widgets/react/hooks/WidgetsPorts.hooks.js +17 -0
  334. package/dist/widgets/react/hooks/navigation.hooks.d.ts +9 -0
  335. package/dist/widgets/react/hooks/navigation.hooks.js +6 -0
  336. package/dist/widgets/react/hooks/page-views.hooks.d.ts +87 -0
  337. package/dist/widgets/react/hooks/page-views.hooks.js +30 -0
  338. package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.d.ts +56 -0
  339. package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.js +38 -0
  340. package/dist/widgets/react/hooks/use-widget-region-editor.hooks.d.ts +32 -0
  341. package/dist/widgets/react/hooks/use-widget-region-editor.hooks.js +22 -0
  342. package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +23 -0
  343. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +22 -0
  344. package/dist/widgets/react/hooks/use-widgets-library.hooks.d.ts +37 -0
  345. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +23 -0
  346. package/dist/widgets/react/index.d.ts +391 -0
  347. package/dist/widgets/react/index.js +31 -0
  348. package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +23 -0
  349. package/dist/widgets/react/pages/WidgetInstanceEditor.js +37 -0
  350. package/dist/widgets/react/pages/WidgetRegionEditor.d.ts +22 -0
  351. package/dist/widgets/react/pages/WidgetRegionEditor.js +25 -0
  352. package/dist/widgets/react/pages/WidgetRegions.d.ts +21 -0
  353. package/dist/widgets/react/pages/WidgetRegions.js +31 -0
  354. package/dist/widgets/react/pages/WidgetsLibrary.d.ts +24 -0
  355. package/dist/widgets/react/pages/WidgetsLibrary.js +61 -0
  356. package/dist/widgets/rules.d.ts +94 -0
  357. package/dist/widgets/rules.js +83 -0
  358. package/dist/widgets/widgets.module.d.ts +122 -0
  359. package/dist/widgets/widgets.module.js +21 -0
  360. package/package.json +206 -9
@@ -0,0 +1,215 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useAnalyzePanel, useSeoDefaults, useSeoTabs, seoTabPanel } from "../hooks/SeoPage.hooks.js";
3
+ import { agentHandle } from "@jini-ai/agentic";
4
+ import { TabBar } from "@jini-ai/ui/tab-strip";
5
+ import { useSeoCopy, useSeoOptions } from "../hooks/SeoPorts.hooks.js";
6
+ import { actionLabel, orEmpty } from "../../rules.js";
7
+ import { useWiredEntryPicker } from "../hooks/use-entry-picker.hooks.js";
8
+ import { useWiredSeoEntryPanel } from "../hooks/use-seo-entry-panel.hooks.js";
9
+ import { useSeoEntrySection } from "../hooks/use-seo-entry-section.hooks.js";
10
+ import { useWiredSeo } from "../hooks/use-seo.hooks.js";
11
+ import { useSeoPageActions, sitemapStateLabel, } from "../hooks/SeoPage.hooks.js";
12
+ import { MediaRefField } from "../components/MediaRefField.js";
13
+ import { SitemapModal } from "../components/SitemapModal.js";
14
+ /**
15
+ * One titled group on this screen — title in the left rail, controls in the right column. Markup
16
+ * only; every visual decision and the reasoning for the rail lives in `styles/seo.css`'s header.
17
+ *
18
+ * This is the 1:1 replacement for the `<div className="field-group"><h2 className="card-title">`
19
+ * pair `56e87ae0` introduced, and the swap is deliberately structure-preserving: the section body
20
+ * carries `.field-group`'s exact `flex-column` + `--space-4` gap, so the fields inside are laid
21
+ * out identically and NOTHING becomes conditional. That matters more here than anywhere else in
22
+ * this file — `SeoDefaultsTab` submits via `new FormData(e.currentTarget)`, so a field that stops
23
+ * being mounted is simply absent from the payload and silently saves as blank. A wrapper element
24
+ * cannot unmount anything, and `Seo.unit.test.tsx`'s "keeps every defaults field inside ONE form"
25
+ * pins all seven names, in order, against exactly that.
26
+ *
27
+ * Fixed `<h2>` rather than a `level` prop: both callers are top-level groups under the page `H1`,
28
+ * and the one heading on this screen that is genuinely subordinate — `SeoEntryPanel`'s "Per-entry
29
+ * overrides", which sits INSIDE the "Per-entry SEO" section — keeps its own `<h3>` and is not
30
+ * routed through here.
31
+ */
32
+ function SeoSection({ title, children }) {
33
+ return (_jsxs("section", { className: "seo-section", children: [_jsx("div", { className: "seo-section-head", children: _jsx("h2", { className: "seo-section-title", children: title }) }), _jsx("div", { className: "seo-section-body", children: children })] }));
34
+ }
35
+ /** Dropdown over every post + page, sourced from the already-existing `listPosts`/`listPages`
36
+ * routes — cheapest entry-selection UX available given what's already built (REQ-06). */
37
+ function EntryPicker({ locale, entryId, onChange, useEntryPickerHook = useWiredEntryPicker }) {
38
+ const t = useSeoCopy();
39
+ const { entries, error } = useEntryPickerHook();
40
+ if (error)
41
+ return _jsx("div", { className: "notice error", children: error });
42
+ if (!entries)
43
+ return _jsx("div", { className: "notice", children: t({ locale: locale, key: "Loading entries…" }) });
44
+ /* The "Entry" caption is no longer painted (owner, 2026-09-06: "you can actually just get rid of
45
+ 'Entry'") — the placeholder option already says "Choose an entry…", so the visible word was
46
+ saying it twice, and with no gap between the two it read as one smashed-together control.
47
+ It stays a REAL label rather than becoming a placeholder-only select: this is the exact
48
+ `<label class="a11y-label-wrap">` + `.visually-hidden` idiom `styles/editor.css` documents for
49
+ the four controls a previous accessibility pass found unnamed — including, by name, a
50
+ `<select>` with no accessible name at all. `display: contents` drops the wrapper's own box, so
51
+ the select keeps the section body's sizing exactly as if the label were not there.
52
+ The i18n key is unchanged and still passed through `t`, so nothing dangles. */
53
+ return (_jsxs("label", { className: "a11y-label-wrap", children: [_jsx("span", { className: "visually-hidden", children: t({ locale: locale, key: "Entry" }) }), _jsxs("select", { value: entryId, onChange: (e) => onChange(e.target.value), ...agentHandle({ handle: "seo-entry-picker" }, {
54
+ role: "field",
55
+ label: "Choose which page or post to edit or analyze SEO for",
56
+ }), children: [_jsx("option", { value: "", children: t({ locale: locale, key: "Choose an entry…" }) }), entries.map((entry) => (_jsxs("option", { value: entry.id, children: [entry.title, " (", entry.status, ")"] }, entry.id)))] })] }));
57
+ }
58
+ /** Read-only score + issues view (REQ-07) — exact field names read off `SeoAnalysis`/`SeoIssue`
59
+ * (`src/seo/types.ts`), not guessed. No state of its own — only the severity sort, which lives in
60
+ * `rules.ts` as `sortIssuesBySeverity`. */
61
+ function AnalyzePanel(props) {
62
+ const t = useSeoCopy();
63
+ const { locale } = props;
64
+ const { sortedIssues } = useAnalyzePanel({ analysis: props.analysis });
65
+ return (_jsxs("div", { className: "seo-analyze-panel", children: [_jsx("h3", { children: t({ locale: locale, key: "Analysis" }) }), _jsxs("p", { children: [t({ locale: locale, key: "Score:" }), " ", _jsx("strong", { children: props.analysis.score })] }), sortedIssues.length === 0 ? (_jsx("p", { className: "muted-cell", children: t({ locale: locale, key: "No issues." }) })) : (_jsx("ul", { children: sortedIssues.map((issue, i) => (_jsxs("li", { children: [_jsx("span", { className: `status status-${issue.severity === "error" ? "failure" : issue.severity === "warning" ? "unavailable" : "success"}`, children: issue.severity }), " ", _jsx("code", { children: issue.code }), issue.field ? _jsxs("span", { className: "muted-cell", children: [" (", issue.field, ")"] }) : null, " \u2014 ", issue.message] }, `${issue.code}-${i}`))) }))] }));
66
+ }
67
+ /** Per-entry overrides edit form (REQ-06). Pre-fills from `getSeoEntry`'s resolved meta, but
68
+ * tracks which fields the user actually touched so `putSeoEntry` only ever sends a genuine
69
+ * partial patch — matching the resolved-vs-override distinction `SeoExtFields` implies (an
70
+ * untouched field must not turn into a persisted override equal to today's resolved default). */
71
+ // EXEMPTION (complexity ceiling, 2026-08-06, updated for the ≤9/≤9 bar): ESLint scores this
72
+ // component's cyclomatic complexity at 25 against a 9 ceiling, but its cognitive complexity is 6
73
+ // (also under 9). That gap between the two is the signature of a
74
+ // measurement artifact, not real branching: eleven form fields each read as
75
+ // `fieldValue(key, resolved.X ?? default) ?? default`, and ESLint's cyclomatic rule counts every
76
+ // `??` as its own decision point — twenty of the twenty-five come from those fallback chains alone,
77
+ // none of which nest inside one another or inside each other's control flow (which is exactly what
78
+ // keeps cognitive complexity low). The other five points are ordinary flat conditionals (notice,
79
+ // saveError, the disabled expression, the Save button's label, the analysis panel) already under
80
+ // the ceiling on their own. There is nothing to extract: splitting the eleven fields into their own
81
+ // components would still evaluate the same fallback chains, just spread across more functions, for
82
+ // no complexity benefit and a real loss of "one form, one place to read its fields."
83
+ function SeoEntryPanel({ locale, entryId, useSeoEntryPanelHook = useWiredSeoEntryPanel }) {
84
+ const t = useSeoCopy();
85
+ const { resolved, analysis, loadError, saving, saveError, notice, fieldValue, setField, save, touched } = useSeoEntryPanelHook({ entryId });
86
+ if (loadError)
87
+ return _jsx("div", { className: "notice error", children: loadError });
88
+ if (!resolved)
89
+ return _jsx("div", { className: "notice", children: t({ locale: locale, key: "Loading entry SEO…" }) });
90
+ return (_jsxs("div", { className: "seo-entry-panel", children: [_jsx("h3", { children: t({ locale: locale, key: "Per-entry overrides" }) }), _jsxs("p", { className: "muted-cell", children: [t({ locale: locale, key: "Fields show the currently-effective value (author override, or site default, or derived from the entry). Only fields you change here are saved as overrides." }), " ", t({ locale: locale, key: "Empty a field and save to remove its override — the entry falls back to the site default." })] }), notice ? _jsx("div", { className: "notice", children: notice }) : null, saveError ? (_jsx("div", { className: "notice error", role: "alert", children: saveError })) : null, _jsxs("label", { children: [t({ locale: locale, key: "Title" }), _jsx("input", { value: fieldValue("title", resolved.title) ?? "", onChange: (e) => setField("title", e.target.value), ...agentHandle({ handle: "seo-entry-title" }, { role: "field", label: "This entry's SEO title override" }) })] }), _jsxs("label", { children: [t({ locale: locale, key: "Description" }), _jsx("textarea", { value: fieldValue("description", resolved.description ?? "") ?? "", onChange: (e) => setField("description", e.target.value), ...agentHandle({ handle: "seo-entry-description" }, { role: "field", label: "This entry's SEO description override" }) })] }), _jsxs("label", { children: [t({ locale: locale, key: "Canonical URL" }), _jsx("input", { value: fieldValue("canonical", resolved.canonical) ?? "", onChange: (e) => setField("canonical", e.target.value), ...agentHandle({ handle: "seo-entry-canonical" }, { role: "field", label: "This entry's canonical URL override" }) })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: fieldValue("noindex", resolved.robots.noindex) ?? false, onChange: (e) => setField("noindex", e.target.checked), ...agentHandle({ handle: "seo-entry-noindex" }, { role: "checkbox", label: "Override this entry's robots noindex directive" }) }), t({ locale: locale, key: "Noindex" })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: fieldValue("nofollow", resolved.robots.nofollow) ?? false, onChange: (e) => setField("nofollow", e.target.checked), ...agentHandle({ handle: "seo-entry-nofollow" }, { role: "checkbox", label: "Override this entry's robots nofollow directive" }) }), t({ locale: locale, key: "Nofollow" })] }), _jsxs("label", { children: [t({ locale: locale, key: "OG title" }), _jsx("input", { value: fieldValue("ogTitle", resolved.openGraph.title) ?? "", onChange: (e) => setField("ogTitle", e.target.value), ...agentHandle({ handle: "seo-entry-og-title" }, { role: "field", label: "This entry's Open Graph title override" }) })] }), _jsxs("label", { children: [t({ locale: locale, key: "OG description" }), _jsx("input", { value: fieldValue("ogDescription", resolved.openGraph.description ?? "") ?? "", onChange: (e) => setField("ogDescription", e.target.value), ...agentHandle({ handle: "seo-entry-og-description" }, { role: "field", label: "This entry's Open Graph description override" }) })] }), _jsx(MediaRefField, { locale: locale, id: "seo-entry-og-image", label: t({ locale: locale, key: "OG image (media ref or URL)" }), value: fieldValue("ogImage", resolved.openGraph.image ?? "") ?? "", onChange: (value) => setField("ogImage", value), agentHandle: "seo-entry-og-image" }), _jsxs("label", { children: [t({ locale: locale, key: "Twitter title" }), _jsx("input", { value: fieldValue("twitterTitle", resolved.twitter.title) ?? "", onChange: (e) => setField("twitterTitle", e.target.value), ...agentHandle({ handle: "seo-entry-twitter-title" }, { role: "field", label: "This entry's Twitter card title override" }) })] }), _jsxs("label", { children: [t({ locale: locale, key: "Twitter description" }), _jsx("input", { value: fieldValue("twitterDescription", resolved.twitter.description ?? "") ?? "", onChange: (e) => setField("twitterDescription", e.target.value), ...agentHandle({ handle: "seo-entry-twitter-description" }, { role: "field", label: "This entry's Twitter card description override" }) })] }), _jsx(MediaRefField, { locale: locale, id: "seo-entry-twitter-image", label: t({ locale: locale, key: "Twitter image (media ref or URL)" }), value: fieldValue("twitterImage", resolved.twitter.image ?? "") ?? "", onChange: (value) => setField("twitterImage", value), agentHandle: "seo-entry-twitter-image" }), _jsx("span", { className: "editor-actions", children: _jsx("button", { type: "button", className: "btn-secondary", onClick: save, disabled: saving || Object.keys(touched).length === 0, ...agentHandle({ handle: "seo-entry-save-overrides" }, { role: "button", label: "Save this entry's SEO overrides" }), children: saving ? t({ locale: locale, key: "Saving…" }) : t({ locale: locale, key: "Save overrides" }) }) }), analysis ? _jsx(AnalyzePanel, { locale: locale, analysis: analysis }) : null] }));
91
+ }
92
+ /** Section wrapper (REQ-06/07) — entry picker over the per-entry edit + analyze panels. This IS the
93
+ * "Pages & posts" tab panel; it kept its name and its markup, because nothing about it changed
94
+ * except that a tab now selects it.
95
+ *
96
+ * The `<h2>` stays, and deliberately so — dropping it as "redundant with the tab label" was tried
97
+ * and reverted. A tab label is not a heading: `TabBar` emits `role="tab"` buttons, which do not
98
+ * appear in a screen reader's heading list at all, so removing this left the whole panel with `H1`
99
+ * and nothing else — the exact regression `deployment/HistoryTab.tsx`'s own comment records
100
+ * measuring live on its tab. It is also not a duplicate: the tab reads "Pages & posts" (what you
101
+ * pick from), the heading reads "Per-entry SEO" (what you are editing). */
102
+ export function SeoEntrySection({ locale, useSeoEntrySectionHook = useSeoEntrySection }, _optional = {}) {
103
+ const t = useSeoCopy();
104
+ const { entryId, setEntryId } = useSeoEntrySectionHook();
105
+ return (_jsx("div", { className: "seo-panel seo-panel--full seo-entry-section", ...agentHandle({ handle: "seo-per-entry" }, {
106
+ role: "region",
107
+ label: "Per-entry SEO overrides — pick one entry and edit or analyze its metadata",
108
+ }), children: _jsxs(SeoSection, { title: t({ locale: locale, key: "Per-entry SEO" }), children: [_jsx(EntryPicker, { locale: locale, entryId: entryId, onChange: setEntryId }), entryId ? _jsx(SeoEntryPanel, { locale: locale, entryId: entryId }, entryId) : null] }) }));
109
+ }
110
+ /** What the two site-wide tab panels need from `useWiredSeo()`. Named as one object rather than
111
+ * passed as six loose props for the same reason `Roles.tsx`'s `RoleCreateFormController` documents:
112
+ * none of these is meaningful without the others, so naming the group is what lets a test hand a
113
+ * panel a controller instead of assembling one out of parts. */
114
+ /**
115
+ * The "Site defaults" tab — the site-wide `seo.*` settings form, moved here verbatim from `Seo`'s
116
+ * own body.
117
+ *
118
+ * ONE addition beyond relocation: the two `.field-group`s now carry headings ("Search appearance",
119
+ * "Crawling"). They are the grouping an operator already thinks in — every SEO tool they have used
120
+ * separates what search/social show from whether to be indexed at all — and the form had no
121
+ * internal hierarchy whatsoever before, just seven controls in a row. Expressing it as headings
122
+ * INSIDE one tab rather than as more tabs is deliberate and load-bearing: see `react/hooks/SeoPage.hooks.ts`'s
123
+ * header for why splitting this form across panels would silently blank fields on save.
124
+ *
125
+ * The submit handler is now one call to `buildSeoSettingsPatch` (`react/hooks/SeoPage.hooks.ts`) rather than seven
126
+ * inline `??`/`||` field reads — same fields, same fallbacks, same payload, just not written in the
127
+ * `.tsx`.
128
+ */
129
+ export function SeoDefaultsTab({ controller }, _optional = {}) {
130
+ const t = useSeoCopy();
131
+ const { locale, settings, saving, defaultOgImage, setDefaultOgImage } = controller;
132
+ const { submit } = useSeoDefaults({ save: controller.save });
133
+ return (_jsxs("form", { className: "seo-panel seo-panel--full", ...agentHandle({ handle: "seo-defaults-form" }, {
134
+ role: "form",
135
+ label: "Site-wide SEO defaults — title template, meta description, social image, robots",
136
+ }), onSubmit: submit, children: [_jsxs(SeoSection, { title: t({ locale: locale, key: "Search appearance" }), children: [_jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "seo-title-template", children: t({ locale: locale, key: "Title template (must contain %s)" }) }), _jsx("input", { id: "seo-title-template", name: "titleTemplate", defaultValue: settings.titleTemplate, ...agentHandle({ handle: "seo-title-template" }, {
137
+ role: "field",
138
+ label: "Site-wide title template; %s is replaced by the page's own title",
139
+ }) })] }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "seo-default-description", children: t({ locale: locale, key: "Default meta description" }) }), _jsx("textarea", { id: "seo-default-description", name: "defaultDescription", defaultValue: orEmpty({ value: settings.defaultDescription }), ...agentHandle({ handle: "seo-default-description" }, {
140
+ role: "field",
141
+ label: "Fallback meta description for pages that set none of their own",
142
+ }) })] })] }), _jsxs(SeoSection, { title: t({ locale: locale, key: "Social sharing" }), children: [_jsx(MediaRefField, { locale: locale, id: "seo-default-og-image", name: "defaultOgImage", label: t({ locale: locale, key: "Default Open Graph / Twitter image (media ref)" }), value: defaultOgImage, onChange: setDefaultOgImage, agentHandle: "seo-default-og-image" }), _jsxs("div", { className: "field", children: [_jsx("label", { className: "field-label", htmlFor: "seo-twitter-site", children: t({ locale: locale, key: "Twitter @site handle" }) }), _jsx("input", { id: "seo-twitter-site", name: "twitterSite", defaultValue: orEmpty({ value: settings.twitterSite }), ...agentHandle({ handle: "seo-twitter-site" }, {
143
+ role: "field",
144
+ label: "The site's Twitter @handle, used in Twitter card metadata",
145
+ }) })] })] }), _jsxs(SeoSection, { title: t({ locale: locale, key: "Crawling" }), children: [_jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", name: "noindex", defaultChecked: settings.defaultRobots.noindex, ...agentHandle({ handle: "seo-default-noindex" }, {
146
+ role: "checkbox",
147
+ label: "Ask search engines not to index pages by default",
148
+ }) }), t({ locale: locale, key: "Default noindex" })] }), _jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", name: "nofollow", defaultChecked: settings.defaultRobots.nofollow, ...agentHandle({ handle: "seo-default-nofollow" }, {
149
+ role: "checkbox",
150
+ label: "Ask search engines not to follow links by default",
151
+ }) }), t({ locale: locale, key: "Default nofollow" })] }), _jsxs("label", { className: "form-checkbox-field", children: [_jsx("input", { type: "checkbox", name: "sitemapEnabled", defaultChecked: settings.sitemapEnabled, ...agentHandle({ handle: "seo-sitemap-enabled" }, {
152
+ role: "checkbox",
153
+ label: "Whether this site publishes a sitemap at all",
154
+ }) }), t({ locale: locale, key: "Sitemap enabled" })] })] }), _jsx("div", { className: "editor-actions form-actions seo-actions", children: _jsx("button", { type: "submit", disabled: saving, ...agentHandle({ handle: "seo-save-settings" }, {
155
+ role: "button",
156
+ label: "Save the site-wide SEO defaults above",
157
+ }), children: actionLabel({ pending: saving, pendingLabel: t({ locale: locale, key: "Saving…" }), idleLabel: t({ locale: locale, key: "Save settings" }) }) }) })] }));
158
+ }
159
+ /**
160
+ * The "Sitemap" tab — the two sitemap actions, moved here verbatim, plus one line this screen never
161
+ * had: whether a sitemap is being published at all.
162
+ *
163
+ * That line is the reason this is a tab rather than a footnote on Site defaults. The switch
164
+ * ("Sitemap enabled") cannot move here — it belongs to the defaults form's `FormData` submit, see
165
+ * `react/hooks/SeoPage.hooks.ts`'s header — so without a state read, an operator who had sitemaps switched off
166
+ * would find a Regenerate button that appears live and reports success while publishing nothing.
167
+ * Reporting the state instead of duplicating the control keeps exactly one switch on the screen.
168
+ *
169
+ * Wrapped in a `.card` like the defaults form beside it, rather than left as loose text on the
170
+ * page — rendered side by side the uncontained version read as an unfinished panel next to a
171
+ * carded one. The heading is "Cached sitemap", not "Sitemap": it names what the card acts on
172
+ * without restating the tab label, the same way `deployment/OverviewTab.tsx`'s cards are titled.
173
+ * It also has to exist at all — a `role="tab"` button is not in a screen reader's heading list, so
174
+ * a panel with no `<h2>` leaves that reader nothing but the page `H1`.
175
+ */
176
+ export function SeoSitemapTab({ controller }, _optional = {}) {
177
+ const t = useSeoCopy();
178
+ const { locale, settings, saving, regenerateSitemap, openSitemapModal } = controller;
179
+ return (_jsx("div", { className: "seo-panel seo-panel--full", ...agentHandle({ handle: "seo-sitemap" }, {
180
+ role: "region",
181
+ label: "Sitemap — force a rebuild of the cached sitemap",
182
+ }), children: _jsxs(SeoSection, { title: t({ locale: locale, key: "Cached sitemap" }), children: [_jsx("p", { className: "card-lead", children: sitemapStateLabel({ sitemapEnabled: settings.sitemapEnabled }, { t: key => t({ locale, key }) }) }), _jsx("p", { children: t({ locale: locale, key: "Force-rebuild the cached sitemap now, bypassing the normal cache-hit path." }) }), _jsxs("span", { className: "editor-actions", children: [_jsx("button", { className: "btn-secondary", disabled: saving, onClick: regenerateSitemap, ...agentHandle({ handle: "seo-regenerate-sitemap" }, {
183
+ role: "button",
184
+ label: "Rebuild the cached sitemap now, bypassing the cache",
185
+ }), children: actionLabel({ pending: saving, pendingLabel: t({ locale: locale, key: "Working…" }), idleLabel: t({ locale: locale, key: "Regenerate sitemap" }) }) }), _jsx("button", { type: "button", className: "btn-secondary", onClick: openSitemapModal, ...agentHandle({ handle: "seo-view-sitemap" }, {
186
+ role: "button",
187
+ label: "Open a modal showing the sitemap's URLs, or its raw XML",
188
+ }), children: t({ locale: locale, key: "View sitemap" }) })] })] }) }));
189
+ }
190
+ export function Seo({ tabId, onTabChange, useSeoHook = useWiredSeo } = {}, _optional = {}) {
191
+ const t = useSeoCopy();
192
+ const { headerActions } = useSeoOptions();
193
+ const { changeTab } = useSeoPageActions({ onTabChange });
194
+ const { settings, error, saving, notice, save, defaultOgImage, setDefaultOgImage, regenerateSitemap, sitemapModalOpen, openSitemapModal, closeSitemapModal, locale, } = useSeoHook();
195
+ // Locale is a context-backed hook; it must run on the loading/error renders too.
196
+ const { activeTabId, tabs } = useSeoTabs({ tabId });
197
+ if (error && !settings)
198
+ return _jsx("div", { className: "notice error", children: error });
199
+ if (!settings)
200
+ return _jsx("div", { className: "notice", children: t({ locale: locale, key: "Loading SEO settings…" }) });
201
+ const controller = {
202
+ locale,
203
+ settings,
204
+ saving,
205
+ save,
206
+ defaultOgImage,
207
+ setDefaultOgImage,
208
+ regenerateSitemap,
209
+ openSitemapModal,
210
+ };
211
+ return (_jsxs("div", { className: "page seo-page", children: [_jsxs("div", { className: "page-header", children: [_jsxs("div", { className: "page-header-text", children: [_jsx("p", { className: "page-kicker", children: t({ locale: locale, key: "Marketing" }) }), _jsx("h1", { className: "page-title", children: "SEO" }), _jsx("p", { className: "page-description", children: t({ locale: locale, key: "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives." }) })] }), _jsx("div", { className: "page-actions", children: headerActions })] }), error ? _jsx("div", { className: "notice error", children: error }) : null, notice ? _jsx("div", { className: "notice", children: notice }) : null, _jsx(TabBar, { ariaLabel: "SEO", tabs: tabs, activeId: activeTabId, onChange: changeTab, containerHandle: "seo-tab-bar" }), seoTabPanel({ activeTabId, controller, components: { SeoDefaultsTab, SeoSitemapTab, SeoEntrySection } }), sitemapModalOpen ? (_jsx(SitemapModal, { locale: locale, sitemapEnabled: settings.sitemapEnabled, regenerating: saving, onRegenerate: regenerateSitemap, onClose: closeSitemapModal })) : null] }));
212
+ }
213
+ export { Seo as SeoPage };
214
+ export default Seo;
215
+ //# sourceMappingURL=SeoPage.js.map
@@ -0,0 +1,136 @@
1
+ import type { AdminSeoIssue as SeoIssue, AdminSeoSettingsPatch as SeoSettingsPatch } from "../core/ports/seo.js";
2
+ /**
3
+ * Orders issues error-first, then warning, then info, matching the severities' natural urgency.
4
+ * An unrecognized severity sorts last (falls back to `9`) rather than throwing, since this reads
5
+ * off a server-provided field this client does not fully control.
6
+ *
7
+ * @complexity Time: O(n log n) in issue count via the underlying sort; space: O(n) for the copy
8
+ * (never mutates the array passed in).
9
+ */
10
+ export declare function sortIssuesBySeverity({ issues }: {
11
+ issues: readonly SeoIssue[];
12
+ }, _optional?: Record<string, never>): SeoIssue[];
13
+ /**
14
+ * One edited per-entry override field's outgoing value. An emptied text/URL box means "remove this
15
+ * override so the entry falls back to the site default", which on the wire is `null` — the clear
16
+ * sentinel `SeoExtFieldsPatch` documents (`apps/website/src/features/seo/types.ts`) and
17
+ * `setEntrySeoOverrides` implements by `delete`-ing the key.
18
+ *
19
+ * It is NOT `""`. `write-service.ts` stores `""` as a genuine override and `seo.ts` resolves
20
+ * overrides with `??`, so a blank override beats the site default — the exact trap that left one
21
+ * post pinned at `{"description":""}` with no way back from the admin UI. It is not `undefined`
22
+ * either: an omitted key means "leave unchanged", and `JSON.stringify` would drop it from the
23
+ * request body entirely.
24
+ *
25
+ * Generic and value-shaped rather than key-shaped on purpose — the two robots checkboxes go
26
+ * through the same setter and pass straight through, since a checkbox has no "empty" gesture with
27
+ * which to express a clear. Only a genuinely emptied string can mean one.
28
+ *
29
+ * @complexity O(1) — one equality test.
30
+ */
31
+ export declare function overrideOrClear<V>({ value }: {
32
+ value: V;
33
+ }, _optional?: Record<string, never>): V | null;
34
+ /** An optional site-wide default's controlled-input value — `react/pages/SeoPage.tsx`'s three optional defaults
35
+ * (`defaultDescription`, `defaultOgImage`, `twitterSite`) all fall back to `""` the same way. */
36
+ export declare function orEmpty({ value }: {
37
+ value: string | undefined;
38
+ }, _optional?: Record<string, never>): string;
39
+ /** A pending-action button's label — `react/pages/SeoPage.tsx` uses this for both the save-settings button
40
+ * ("Saving…"/"Save settings") and the regenerate-sitemap button ("Working…"/"Regenerate
41
+ * sitemap"), same `pending ? … : …` shape, different copy. Also reused by `SitemapModal.tsx`'s
42
+ * own footer Regenerate button — same two states, same copy, different trigger. */
43
+ export declare function actionLabel({ pending, pendingLabel, idleLabel }: {
44
+ pending: boolean;
45
+ pendingLabel: string;
46
+ idleLabel: string;
47
+ }, _optional?: Record<string, never>): string;
48
+ /**
49
+ * One `<url>` entry from a parsed sitemap. Only the fields `apps/website`'s
50
+ * `registerSeoSitemapRoute` (`server/inbound/public-http/routes/site/sitemap.ts`) actually emits —
51
+ * `<loc>` always, `<lastmod>` when the entry has one — verified by reading that route rather than
52
+ * guessed: it never writes `changefreq`/`priority`, so `SitemapModal.tsx`'s table has no columns
53
+ * for those (SPEC intent: "omit a column entirely if the sitemap never emits that field").
54
+ */
55
+ export interface SitemapUrlEntry {
56
+ loc: string;
57
+ lastmod: string | null;
58
+ }
59
+ /**
60
+ * Parses a `<urlset>` sitemap document into its `<url>` entries via the browser's built-in
61
+ * `DOMParser` — no new dependency, and it reads the exact same bytes `GET /sitemap.xml` served
62
+ * (this never re-derives sitemap content of its own). Malformed input (a parser error, a document
63
+ * with no `<url>` elements, empty text) yields `[]` rather than throwing: `useSitemapModal` already
64
+ * guards the fetch itself via `SitemapPort`; this only guards the parse step.
65
+ *
66
+ * @complexity Time/space: O(n) in document size — one DOM parse, one linear pass over `<url>` nodes.
67
+ */
68
+ export declare function parseSitemapXml({ xmlText }: {
69
+ xmlText: string;
70
+ }, _optional?: Record<string, never>): SitemapUrlEntry[];
71
+ /** Case-insensitive substring filter over a parsed sitemap's URLs — `SitemapModal.tsx`'s filter
72
+ * box, for the same "narrow a long list by typing" shape every other filter in this admin uses.
73
+ * An empty/whitespace-only query returns every entry unchanged (a copy, not the same reference,
74
+ * matching `sortIssuesBySeverity`'s own never-mutate-the-input convention above).
75
+ *
76
+ * @complexity Time: O(n) in entry count; space: O(n) for the filtered copy. */
77
+ export declare function filterSitemapEntries({ entries, query }: {
78
+ entries: readonly SitemapUrlEntry[];
79
+ query: string;
80
+ }, _optional?: Record<string, never>): SitemapUrlEntry[];
81
+ /** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
82
+ * prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
83
+ * `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
84
+ * `apps/website/src/features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
85
+ * uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
86
+ * (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
87
+ * for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
88
+ * (S4's `resolveSeoImageRef` accepts either spelling) — this only changes what gets WRITTEN next. */
89
+ export declare function buildMediaRef({ item }: {
90
+ item: {
91
+ id: string;
92
+ slug?: string;
93
+ };
94
+ }, _optional?: Record<string, never>): string;
95
+ /** `MediaRefField`'s thumbnail `<img src>` for the field's current value. An absolute URL (the
96
+ * field's other legal shape — someone pasted a raw URL) renders directly; a `{assetId}:{transform}`
97
+ * ref resolves through the injected `mediaOriginalUrl` builder — the admin media library's own
98
+ * preview URL (`MediaPickerPort.mediaOriginalUrl`), same as `MediaPickerDialog`'s own grid
99
+ * thumbnails use, NOT the public `/m/...` rendition URL (that needs a live workspace/transform
100
+ * lookup this client-side preview has no reason to perform). `null` for an empty or unparseable
101
+ * value — the caller renders no preview then, rather than a broken `<img>`. `typeof value !==
102
+ * "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
103
+ * `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
104
+ * non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
105
+ * "never throws over one bad reference" contract (`apps/website/src/features/seo/media.ts`).
106
+ *
107
+ * @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
108
+ * performs the actual fetch only once it is used as an `<img src>`). */
109
+ export declare function resolveMediaRefPreviewUrl({ value, mediaOriginalUrl }: {
110
+ value: string;
111
+ mediaOriginalUrl: (id: string) => string;
112
+ }, _optional?: Record<string, never>): string | null;
113
+ /**
114
+ * The site-wide settings patch the defaults form submits — lifted out of the form's own inline
115
+ * `onSubmit` when this screen gained tabs. Same seven fields, same `checkbox === "on"` reading of
116
+ * the three toggles.
117
+ *
118
+ * ONE deliberate behavior change since that lift (2026-09-06): the three optional scalars now
119
+ * submit `null` rather than `undefined` when their box is empty, so emptying one CLEARS the site
120
+ * default instead of silently leaving it as it was. See {@link optionalFormString}. `titleTemplate`
121
+ * is unaffected — it is not nullable server-side and an emptied box still submits `""`, which the
122
+ * server rejects with its "must contain '%s'" validation error.
123
+ *
124
+ * @complexity O(1) — a fixed set of field reads, no iteration.
125
+ */
126
+ export declare function buildSeoSettingsPatch({ form }: {
127
+ form: FormData;
128
+ }, _optional?: Record<string, never>): SeoSettingsPatch;
129
+ /** Three stable query ids; defaults remains the first and fallback tab. */
130
+ export declare const SEO_TAB_IDS: readonly ["defaults", "sitemap", "entries"];
131
+ export type SeoTabId = (typeof SEO_TAB_IDS)[number];
132
+ /** Resolves an absent or stale bookmark to the defaults panel. */
133
+ export declare function resolveSeoTabId({ tabId }: {
134
+ tabId?: string | null | undefined;
135
+ }, _optional?: Record<string, never>): SeoTabId;
136
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1,212 @@
1
+ /**
2
+ * @file Pure logic for the `seo` feature — everything that computes a value rather than rendering
3
+ * one.
4
+ *
5
+ * Follows the `rules.ts` convention `features/posts/rules.ts` establishes. `sortIssuesBySeverity`
6
+ * was `AnalyzePanel`'s inline `.sort()` call plus its module-level `SEVERITY_ORDER` table; it
7
+ * computes an ordering, so per that convention it moves here rather than stay "presentation" —
8
+ * `AnalyzePanel` itself has no state and needs no hook, only this rule.
9
+ */
10
+ const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
11
+ /**
12
+ * Orders issues error-first, then warning, then info, matching the severities' natural urgency.
13
+ * An unrecognized severity sorts last (falls back to `9`) rather than throwing, since this reads
14
+ * off a server-provided field this client does not fully control.
15
+ *
16
+ * @complexity Time: O(n log n) in issue count via the underlying sort; space: O(n) for the copy
17
+ * (never mutates the array passed in).
18
+ */
19
+ export function sortIssuesBySeverity({ issues }, _optional = {}) {
20
+ return [...issues].sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 9) - (SEVERITY_ORDER[b.severity] ?? 9));
21
+ }
22
+ /**
23
+ * One edited per-entry override field's outgoing value. An emptied text/URL box means "remove this
24
+ * override so the entry falls back to the site default", which on the wire is `null` — the clear
25
+ * sentinel `SeoExtFieldsPatch` documents (`apps/website/src/features/seo/types.ts`) and
26
+ * `setEntrySeoOverrides` implements by `delete`-ing the key.
27
+ *
28
+ * It is NOT `""`. `write-service.ts` stores `""` as a genuine override and `seo.ts` resolves
29
+ * overrides with `??`, so a blank override beats the site default — the exact trap that left one
30
+ * post pinned at `{"description":""}` with no way back from the admin UI. It is not `undefined`
31
+ * either: an omitted key means "leave unchanged", and `JSON.stringify` would drop it from the
32
+ * request body entirely.
33
+ *
34
+ * Generic and value-shaped rather than key-shaped on purpose — the two robots checkboxes go
35
+ * through the same setter and pass straight through, since a checkbox has no "empty" gesture with
36
+ * which to express a clear. Only a genuinely emptied string can mean one.
37
+ *
38
+ * @complexity O(1) — one equality test.
39
+ */
40
+ export function overrideOrClear({ value }, _optional = {}) {
41
+ return value === "" ? null : value;
42
+ }
43
+ /** An optional site-wide default's controlled-input value — `react/pages/SeoPage.tsx`'s three optional defaults
44
+ * (`defaultDescription`, `defaultOgImage`, `twitterSite`) all fall back to `""` the same way. */
45
+ export function orEmpty({ value }, _optional = {}) {
46
+ return value ?? "";
47
+ }
48
+ /** A pending-action button's label — `react/pages/SeoPage.tsx` uses this for both the save-settings button
49
+ * ("Saving…"/"Save settings") and the regenerate-sitemap button ("Working…"/"Regenerate
50
+ * sitemap"), same `pending ? … : …` shape, different copy. Also reused by `SitemapModal.tsx`'s
51
+ * own footer Regenerate button — same two states, same copy, different trigger. */
52
+ export function actionLabel({ pending, pendingLabel, idleLabel }, _optional = {}) {
53
+ return pending ? pendingLabel : idleLabel;
54
+ }
55
+ /**
56
+ * Parses a `<urlset>` sitemap document into its `<url>` entries via the browser's built-in
57
+ * `DOMParser` — no new dependency, and it reads the exact same bytes `GET /sitemap.xml` served
58
+ * (this never re-derives sitemap content of its own). Malformed input (a parser error, a document
59
+ * with no `<url>` elements, empty text) yields `[]` rather than throwing: `useSitemapModal` already
60
+ * guards the fetch itself via `SitemapPort`; this only guards the parse step.
61
+ *
62
+ * @complexity Time/space: O(n) in document size — one DOM parse, one linear pass over `<url>` nodes.
63
+ */
64
+ export function parseSitemapXml({ xmlText }, _optional = {}) {
65
+ const doc = new DOMParser().parseFromString(xmlText, "text/xml");
66
+ if (doc.getElementsByTagName("parsererror").length > 0)
67
+ return [];
68
+ return Array.from(doc.getElementsByTagName("url"))
69
+ .map((urlEl) => ({
70
+ loc: urlEl.getElementsByTagName("loc")[0]?.textContent?.trim() ?? "",
71
+ lastmod: urlEl.getElementsByTagName("lastmod")[0]?.textContent?.trim() || null,
72
+ }))
73
+ .filter((entry) => entry.loc !== "");
74
+ }
75
+ /** Case-insensitive substring filter over a parsed sitemap's URLs — `SitemapModal.tsx`'s filter
76
+ * box, for the same "narrow a long list by typing" shape every other filter in this admin uses.
77
+ * An empty/whitespace-only query returns every entry unchanged (a copy, not the same reference,
78
+ * matching `sortIssuesBySeverity`'s own never-mutate-the-input convention above).
79
+ *
80
+ * @complexity Time: O(n) in entry count; space: O(n) for the filtered copy. */
81
+ export function filterSitemapEntries({ entries, query }, _optional = {}) {
82
+ const needle = query.trim().toLowerCase();
83
+ if (!needle)
84
+ return [...entries];
85
+ return entries.filter((entry) => entry.loc.toLowerCase().includes(needle));
86
+ }
87
+ /**
88
+ * `MediaRefField`'s ref-building/preview logic (`ogImage`/`twitterImage`/`defaultOgImage` all
89
+ * share this shape — SPEC intent: "make the OG image selectable, with a preview"). The reference
90
+ * format itself is NOT reinvented here — it is verified, not guessed, against the one function that
91
+ * actually parses it server-side: `resolveSeoImageRef` (`apps/website/src/features/seo/media.ts`)
92
+ * accepts either an already-absolute URL, passed through unchanged, or a bare `{assetId}:{transformName}`
93
+ * pair split on the FIRST `:`. `isAbsoluteMediaRef`/`parseMediaRefAssetId` below duplicate that
94
+ * parsing rather than import across the app boundary (apps/admin has no dependency on apps/website)
95
+ * — kept in lockstep by this doc comment naming the source of truth; a change to that split MUST
96
+ * update both.
97
+ */
98
+ /** The transform every upload receives (the server's default rendition) — the one
99
+ * `buildMediaRef` always targets, matching `EmbedInsertControl.tsx`'s own
100
+ * `transformName: "public"` for the identical `{assetId}:{transformName}` shape it writes into a
101
+ * post body's image node. */
102
+ const SEO_IMAGE_TRANSFORM = "public";
103
+ /** Mirrors `resolveSeoImageRef`'s own `isAbsoluteUrl` (`apps/website/src/features/seo/media.ts`) —
104
+ * see this section's file-header doc for why this is a duplicate, not an import. */
105
+ const ABSOLUTE_URL_PATTERN = /^[a-z][a-z0-9+.-]*:\/\//i;
106
+ function isAbsoluteMediaRef(value) {
107
+ return ABSOLUTE_URL_PATTERN.test(value) || value.startsWith("//");
108
+ }
109
+ /** Mirrors `resolveSeoImageRef`'s own `parseMediaRefParts`, narrowed to just the `assetId` half —
110
+ * all `MediaRefField`'s preview needs. Returns `null` for an absolute URL (nothing to parse) or a
111
+ * malformed ref (no `:`, or nothing on one side of it). */
112
+ function parseMediaRefAssetId(ref) {
113
+ if (!ref || isAbsoluteMediaRef(ref))
114
+ return null;
115
+ const separatorIndex = ref.indexOf(":");
116
+ if (separatorIndex <= 0 || separatorIndex === ref.length - 1)
117
+ return null;
118
+ return ref.slice(0, separatorIndex);
119
+ }
120
+ /** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
121
+ * prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
122
+ * `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
123
+ * `apps/website/src/features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
124
+ * uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
125
+ * (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
126
+ * for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
127
+ * (S4's `resolveSeoImageRef` accepts either spelling) — this only changes what gets WRITTEN next. */
128
+ export function buildMediaRef({ item }, _optional = {}) {
129
+ const key = item.slug || item.id;
130
+ return `${key}:${SEO_IMAGE_TRANSFORM}`;
131
+ }
132
+ /** `MediaRefField`'s thumbnail `<img src>` for the field's current value. An absolute URL (the
133
+ * field's other legal shape — someone pasted a raw URL) renders directly; a `{assetId}:{transform}`
134
+ * ref resolves through the injected `mediaOriginalUrl` builder — the admin media library's own
135
+ * preview URL (`MediaPickerPort.mediaOriginalUrl`), same as `MediaPickerDialog`'s own grid
136
+ * thumbnails use, NOT the public `/m/...` rendition URL (that needs a live workspace/transform
137
+ * lookup this client-side preview has no reason to perform). `null` for an empty or unparseable
138
+ * value — the caller renders no preview then, rather than a broken `<img>`. `typeof value !==
139
+ * "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
140
+ * `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
141
+ * non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
142
+ * "never throws over one bad reference" contract (`apps/website/src/features/seo/media.ts`).
143
+ *
144
+ * @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
145
+ * performs the actual fetch only once it is used as an `<img src>`). */
146
+ export function resolveMediaRefPreviewUrl({ value, mediaOriginalUrl }, _optional = {}) {
147
+ const trimmed = typeof value === "string" ? value.trim() : "";
148
+ if (!trimmed)
149
+ return null;
150
+ if (isAbsoluteMediaRef(trimmed))
151
+ return trimmed;
152
+ const assetId = parseMediaRefAssetId(trimmed);
153
+ return assetId ? mediaOriginalUrl(assetId) : null;
154
+ }
155
+ /** One `FormData` field as a string, with the caller's fallback for an absent field. Its own named
156
+ * function purely so {@link buildSeoSettingsPatch} below stays readable at a glance: written
157
+ * inline, the seven fields' `?? `/`||` chains scored the patch builder at the ESLint ceiling
158
+ * (`apps/admin` enforces 9/9 as a hard error) for no gain in clarity. */
159
+ function formString(form, key, fallback = "") {
160
+ return String(form.get(key) ?? fallback);
161
+ }
162
+ /**
163
+ * A `FormData` field that is sent as `null` when blank, rather than persisted as an empty string.
164
+ *
165
+ * `null` is what CLEARS a site default: the server registers these three (`default_description`,
166
+ * `default_og_image`, `twitter_site`) as `nullable: true` and `buildScalarWrites`
167
+ * (`apps/website/src/features/seo/settings.ts`) normalizes `null` to the `""` absent-sentinel that
168
+ * `getSeoSettings` reads back as `undefined`.
169
+ *
170
+ * It used to return `undefined`, under a comment claiming "an empty box means 'no site default'".
171
+ * That was false and is the bug this replaces: `setSeoSettings` is a MERGE, and `buildScalarWrites`
172
+ * does `if (raw === undefined) continue;` — so an omitted key means UNCHANGED, and an operator who
173
+ * emptied "Default meta description" to remove it just saw it come back on the next load, with no
174
+ * way to remove it from this form at all. `undefined` also cannot survive the request: the client
175
+ * `JSON.stringify`s this patch, which drops `undefined` keys and keeps `null` ones.
176
+ */
177
+ function optionalFormString(form, key) {
178
+ return formString(form, key) || null;
179
+ }
180
+ /**
181
+ * The site-wide settings patch the defaults form submits — lifted out of the form's own inline
182
+ * `onSubmit` when this screen gained tabs. Same seven fields, same `checkbox === "on"` reading of
183
+ * the three toggles.
184
+ *
185
+ * ONE deliberate behavior change since that lift (2026-09-06): the three optional scalars now
186
+ * submit `null` rather than `undefined` when their box is empty, so emptying one CLEARS the site
187
+ * default instead of silently leaving it as it was. See {@link optionalFormString}. `titleTemplate`
188
+ * is unaffected — it is not nullable server-side and an emptied box still submits `""`, which the
189
+ * server rejects with its "must contain '%s'" validation error.
190
+ *
191
+ * @complexity O(1) — a fixed set of field reads, no iteration.
192
+ */
193
+ export function buildSeoSettingsPatch({ form }, _optional = {}) {
194
+ return {
195
+ titleTemplate: formString(form, "titleTemplate", "%s"),
196
+ defaultDescription: optionalFormString(form, "defaultDescription"),
197
+ defaultOgImage: optionalFormString(form, "defaultOgImage"),
198
+ twitterSite: optionalFormString(form, "twitterSite"),
199
+ defaultRobots: {
200
+ noindex: form.get("noindex") === "on",
201
+ nofollow: form.get("nofollow") === "on",
202
+ },
203
+ sitemapEnabled: form.get("sitemapEnabled") === "on",
204
+ };
205
+ }
206
+ /** Three stable query ids; defaults remains the first and fallback tab. */
207
+ export const SEO_TAB_IDS = ['defaults', 'sitemap', 'entries'];
208
+ /** Resolves an absent or stale bookmark to the defaults panel. */
209
+ export function resolveSeoTabId({ tabId }, _optional = {}) {
210
+ return SEO_TAB_IDS.find(id => id === tabId) ?? 'defaults';
211
+ }
212
+ //# sourceMappingURL=rules.js.map