@jini-ai/admin 0.4.2 → 0.4.4

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 (361) hide show
  1. package/CHANGELOG.md +15 -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 +138 -0
  96. package/dist/forms/forms.module.js +17 -0
  97. package/dist/forms/html-rules.d.ts +34 -0
  98. package/dist/forms/html-rules.js +34 -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 +101 -0
  102. package/dist/forms/messages.en.js +105 -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 +15 -0
  122. package/dist/forms/react/components/FormSubmissionDetail.js +30 -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 +117 -0
  128. package/dist/forms/react/hooks/forms-view.hooks.js +78 -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 +452 -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 +50 -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/components/RowMenu/RowMenu.hooks.js +14 -3
  176. package/dist/react/use-controller.d.ts +1 -15
  177. package/dist/react/use-controller.js +2 -17
  178. package/dist/redirects/adapters/http.d.ts +20 -0
  179. package/dist/redirects/adapters/http.js +31 -0
  180. package/dist/redirects/adapters/memory.d.ts +13 -0
  181. package/dist/redirects/adapters/memory.js +83 -0
  182. package/dist/redirects/conformance/redirects-api.conformance.d.ts +12 -0
  183. package/dist/redirects/conformance/redirects-api.conformance.js +66 -0
  184. package/dist/redirects/controllers/hit-count.controller.d.ts +14 -0
  185. package/dist/redirects/controllers/hit-count.controller.js +10 -0
  186. package/dist/redirects/controllers/import-redirects.controller.d.ts +28 -0
  187. package/dist/redirects/controllers/import-redirects.controller.js +29 -0
  188. package/dist/redirects/controllers/redirects.controller.d.ts +37 -0
  189. package/dist/redirects/controllers/redirects.controller.js +38 -0
  190. package/dist/redirects/index.d.ts +11 -0
  191. package/dist/redirects/index.js +10 -0
  192. package/dist/redirects/messages.en.d.ts +58 -0
  193. package/dist/redirects/messages.en.js +60 -0
  194. package/dist/redirects/models.d.ts +14 -0
  195. package/dist/redirects/models.js +2 -0
  196. package/dist/redirects/ports.d.ts +9 -0
  197. package/dist/redirects/ports.js +4 -0
  198. package/dist/redirects/react/components/HitCountCell.d.ts +19 -0
  199. package/dist/redirects/react/components/HitCountCell.js +19 -0
  200. package/dist/redirects/react/components/ImportRedirectsForm.d.ts +17 -0
  201. package/dist/redirects/react/components/ImportRedirectsForm.js +12 -0
  202. package/dist/redirects/react/hooks/RedirectsOptions.hooks.d.ts +27 -0
  203. package/dist/redirects/react/hooks/RedirectsOptions.hooks.js +15 -0
  204. package/dist/redirects/react/hooks/RedirectsPage.hooks.d.ts +50 -0
  205. package/dist/redirects/react/hooks/RedirectsPage.hooks.js +30 -0
  206. package/dist/redirects/react/hooks/RedirectsPorts.hooks.d.ts +10 -0
  207. package/dist/redirects/react/hooks/RedirectsPorts.hooks.js +11 -0
  208. package/dist/redirects/react/hooks/use-hit-count-cell.hooks.d.ts +19 -0
  209. package/dist/redirects/react/hooks/use-hit-count-cell.hooks.js +15 -0
  210. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +21 -0
  211. package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +25 -0
  212. package/dist/redirects/react/hooks/use-redirects.hooks.d.ts +34 -0
  213. package/dist/redirects/react/hooks/use-redirects.hooks.js +63 -0
  214. package/dist/redirects/react/hooks/wired.hooks.d.ts +55 -0
  215. package/dist/redirects/react/hooks/wired.hooks.js +31 -0
  216. package/dist/redirects/react/index.d.ts +324 -0
  217. package/dist/redirects/react/index.js +28 -0
  218. package/dist/redirects/react/pages/RedirectsPage.d.ts +15 -0
  219. package/dist/redirects/react/pages/RedirectsPage.js +20 -0
  220. package/dist/redirects/redirects.module.d.ts +77 -0
  221. package/dist/redirects/redirects.module.js +13 -0
  222. package/dist/redirects/rules.d.ts +131 -0
  223. package/dist/redirects/rules.js +134 -0
  224. package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +1 -1
  225. package/dist/security/react/hooks/AccessTokensTab.hooks.js +1 -1
  226. package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +1 -1
  227. package/dist/security/react/hooks/RootKeyTab.hooks.js +1 -1
  228. package/dist/security/react/hooks/SecurityPage.hooks.d.ts +1 -1
  229. package/dist/security/react/index.js +1 -1
  230. package/dist/security/react/pages/SecurityPage.d.ts +1 -1
  231. package/dist/security/react/tabs/AccessTokensTab.d.ts +1 -1
  232. package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
  233. package/dist/seo/adapters/http.d.ts +31 -0
  234. package/dist/seo/adapters/http.js +37 -0
  235. package/dist/seo/adapters/memory.d.ts +28 -0
  236. package/dist/seo/adapters/memory.js +129 -0
  237. package/dist/seo/conformance/seo-api.conformance.d.ts +8 -0
  238. package/dist/seo/conformance/seo-api.conformance.js +44 -0
  239. package/dist/seo/controllers/entry-picker.controller.d.ts +21 -0
  240. package/dist/seo/controllers/entry-picker.controller.js +17 -0
  241. package/dist/seo/controllers/entry-section.controller.d.ts +20 -0
  242. package/dist/seo/controllers/entry-section.controller.js +7 -0
  243. package/dist/seo/controllers/media-ref.controller.d.ts +26 -0
  244. package/dist/seo/controllers/media-ref.controller.js +14 -0
  245. package/dist/seo/controllers/seo-entry.controller.d.ts +31 -0
  246. package/dist/seo/controllers/seo-entry.controller.js +52 -0
  247. package/dist/seo/controllers/seo.controller.d.ts +29 -0
  248. package/dist/seo/controllers/seo.controller.js +50 -0
  249. package/dist/seo/controllers/sitemap.controller.d.ts +25 -0
  250. package/dist/seo/controllers/sitemap.controller.js +27 -0
  251. package/dist/seo/index.d.ts +14 -0
  252. package/dist/seo/index.js +12 -0
  253. package/dist/seo/messages.en.d.ts +79 -0
  254. package/dist/seo/messages.en.js +79 -0
  255. package/dist/seo/models.d.ts +179 -0
  256. package/dist/seo/models.js +8 -0
  257. package/dist/seo/ports.d.ts +11 -0
  258. package/dist/seo/ports.js +4 -0
  259. package/dist/seo/react/components/MediaPickerBridge.d.ts +4 -0
  260. package/dist/seo/react/components/MediaPickerBridge.js +8 -0
  261. package/dist/seo/react/components/MediaRefField.d.ts +40 -0
  262. package/dist/seo/react/components/MediaRefField.js +12 -0
  263. package/dist/seo/react/components/SeoIcons.d.ts +13 -0
  264. package/dist/seo/react/components/SeoIcons.js +32 -0
  265. package/dist/seo/react/components/SitemapModal.d.ts +48 -0
  266. package/dist/seo/react/components/SitemapModal.js +72 -0
  267. package/dist/seo/react/hooks/MediaPickerBridge.hooks.d.ts +6 -0
  268. package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +27 -0
  269. package/dist/seo/react/hooks/MediaRefField.hooks.d.ts +25 -0
  270. package/dist/seo/react/hooks/MediaRefField.hooks.js +30 -0
  271. package/dist/seo/react/hooks/SeoModuleTabs.d.ts +14 -0
  272. package/dist/seo/react/hooks/SeoModuleTabs.js +18 -0
  273. package/dist/seo/react/hooks/SeoPage.hooks.d.ts +69 -0
  274. package/dist/seo/react/hooks/SeoPage.hooks.js +37 -0
  275. package/dist/seo/react/hooks/SeoPorts.hooks.d.ts +19 -0
  276. package/dist/seo/react/hooks/SeoPorts.hooks.js +22 -0
  277. package/dist/seo/react/hooks/SitemapModal.hooks.d.ts +26 -0
  278. package/dist/seo/react/hooks/SitemapModal.hooks.js +20 -0
  279. package/dist/seo/react/hooks/use-entry-picker.hooks.d.ts +11 -0
  280. package/dist/seo/react/hooks/use-entry-picker.hooks.js +20 -0
  281. package/dist/seo/react/hooks/use-seo-entry-panel.hooks.d.ts +14 -0
  282. package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +26 -0
  283. package/dist/seo/react/hooks/use-seo-entry-section.hooks.d.ts +4 -0
  284. package/dist/seo/react/hooks/use-seo-entry-section.hooks.js +7 -0
  285. package/dist/seo/react/hooks/use-seo.hooks.d.ts +11 -0
  286. package/dist/seo/react/hooks/use-seo.hooks.js +29 -0
  287. package/dist/seo/react/hooks/use-sitemap-modal.hooks.d.ts +10 -0
  288. package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +18 -0
  289. package/dist/seo/react/index.d.ts +369 -0
  290. package/dist/seo/react/index.js +30 -0
  291. package/dist/seo/react/options.d.ts +26 -0
  292. package/dist/seo/react/options.js +2 -0
  293. package/dist/seo/react/pages/SeoPage.d.ts +161 -0
  294. package/dist/seo/react/pages/SeoPage.js +215 -0
  295. package/dist/seo/rules.d.ts +142 -0
  296. package/dist/seo/rules.js +220 -0
  297. package/dist/seo/seo.module.d.ts +115 -0
  298. package/dist/seo/seo.module.js +14 -0
  299. package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +1 -1
  300. package/dist/source-control/react/hooks/ProvidersTab.hooks.js +1 -1
  301. package/dist/source-control/react/hooks/SourceControlPage.hooks.d.ts +1 -1
  302. package/dist/source-control/react/index.js +1 -1
  303. package/dist/source-control/react/pages/SourceControlPage.d.ts +1 -1
  304. package/dist/source-control/react/tabs/ProvidersTab.d.ts +1 -1
  305. package/dist/widgets/adapters/http.d.ts +19 -0
  306. package/dist/widgets/adapters/http.js +33 -0
  307. package/dist/widgets/adapters/memory.d.ts +22 -0
  308. package/dist/widgets/adapters/memory.js +93 -0
  309. package/dist/widgets/conformance/widgets-api.conformance.d.ts +6 -0
  310. package/dist/widgets/conformance/widgets-api.conformance.js +55 -0
  311. package/dist/widgets/controllers/instance-editor.controller.d.ts +49 -0
  312. package/dist/widgets/controllers/instance-editor.controller.js +87 -0
  313. package/dist/widgets/controllers/library.controller.d.ts +33 -0
  314. package/dist/widgets/controllers/library.controller.js +53 -0
  315. package/dist/widgets/controllers/region-editor.controller.d.ts +42 -0
  316. package/dist/widgets/controllers/region-editor.controller.js +69 -0
  317. package/dist/widgets/controllers/regions.controller.d.ts +27 -0
  318. package/dist/widgets/controllers/regions.controller.js +44 -0
  319. package/dist/widgets/index.d.ts +11 -0
  320. package/dist/widgets/index.js +9 -0
  321. package/dist/widgets/messages.en.d.ts +78 -0
  322. package/dist/widgets/messages.en.js +83 -0
  323. package/dist/widgets/models.d.ts +62 -0
  324. package/dist/widgets/models.js +2 -0
  325. package/dist/widgets/ports.d.ts +79 -0
  326. package/dist/widgets/ports.js +5 -0
  327. package/dist/widgets/react/components/WidgetInstanceNotices.d.ts +14 -0
  328. package/dist/widgets/react/components/WidgetInstanceNotices.js +25 -0
  329. package/dist/widgets/react/components/WidgetRegionEditorHeaderActions.d.ts +22 -0
  330. package/dist/widgets/react/components/WidgetRegionEditorHeaderActions.js +18 -0
  331. package/dist/widgets/react/components/WidgetsLibraryNotices.d.ts +13 -0
  332. package/dist/widgets/react/components/WidgetsLibraryNotices.js +10 -0
  333. package/dist/widgets/react/hooks/WidgetsPorts.hooks.d.ts +270 -0
  334. package/dist/widgets/react/hooks/WidgetsPorts.hooks.js +17 -0
  335. package/dist/widgets/react/hooks/navigation.hooks.d.ts +9 -0
  336. package/dist/widgets/react/hooks/navigation.hooks.js +6 -0
  337. package/dist/widgets/react/hooks/page-views.hooks.d.ts +87 -0
  338. package/dist/widgets/react/hooks/page-views.hooks.js +30 -0
  339. package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.d.ts +56 -0
  340. package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.js +38 -0
  341. package/dist/widgets/react/hooks/use-widget-region-editor.hooks.d.ts +32 -0
  342. package/dist/widgets/react/hooks/use-widget-region-editor.hooks.js +22 -0
  343. package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +23 -0
  344. package/dist/widgets/react/hooks/use-widget-regions.hooks.js +22 -0
  345. package/dist/widgets/react/hooks/use-widgets-library.hooks.d.ts +37 -0
  346. package/dist/widgets/react/hooks/use-widgets-library.hooks.js +23 -0
  347. package/dist/widgets/react/index.d.ts +391 -0
  348. package/dist/widgets/react/index.js +31 -0
  349. package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +23 -0
  350. package/dist/widgets/react/pages/WidgetInstanceEditor.js +37 -0
  351. package/dist/widgets/react/pages/WidgetRegionEditor.d.ts +22 -0
  352. package/dist/widgets/react/pages/WidgetRegionEditor.js +25 -0
  353. package/dist/widgets/react/pages/WidgetRegions.d.ts +21 -0
  354. package/dist/widgets/react/pages/WidgetRegions.js +31 -0
  355. package/dist/widgets/react/pages/WidgetsLibrary.d.ts +24 -0
  356. package/dist/widgets/react/pages/WidgetsLibrary.js +61 -0
  357. package/dist/widgets/rules.d.ts +94 -0
  358. package/dist/widgets/rules.js +83 -0
  359. package/dist/widgets/widgets.module.d.ts +122 -0
  360. package/dist/widgets/widgets.module.js +21 -0
  361. 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: "jini-seo-section", children: [_jsx("div", { className: "jini-seo-section-head", children: _jsx("h2", { className: "jini-seo-section-title", children: title }) }), _jsx("div", { className: "jini-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: "jini-notice jini-notice-error", children: error });
42
+ if (!entries)
43
+ return _jsx("div", { className: "jini-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: "jini-a11y-label-wrap", children: [_jsx("span", { className: "jini-visually-hidden", children: t({ locale: locale, key: "Entry" }) }), _jsxs("select", { className: "jini-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: "jini-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: "jini-muted-cell", children: t({ locale: locale, key: "No issues." }) })) : (_jsx("ul", { children: sortedIssues.map((issue, i) => (_jsxs("li", { children: [_jsx("span", { className: `jini-status jini-status-${issue.severity === "error" ? "failure" : issue.severity === "warning" ? "unavailable" : "success"}`, children: issue.severity }), " ", _jsx("code", { children: issue.code }), issue.field ? _jsxs("span", { className: "jini-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: "jini-notice jini-notice-error", children: loadError });
88
+ if (!resolved)
89
+ return _jsx("div", { className: "jini-notice", children: t({ locale: locale, key: "Loading entry SEO…" }) });
90
+ return (_jsxs("div", { className: "jini-seo-entry-panel", children: [_jsx("h3", { children: t({ locale: locale, key: "Per-entry overrides" }) }), _jsxs("p", { className: "jini-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: "jini-notice", children: notice }) : null, saveError ? (_jsx("div", { className: "jini-notice jini-notice-error", role: "alert", children: saveError })) : null, _jsxs("label", { children: [t({ locale: locale, key: "Title" }), _jsx("input", { className: "jini-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", { className: "jini-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", { className: "jini-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", { className: "jini-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", { className: "jini-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", { className: "jini-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", { className: "jini-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" }), 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", { className: "jini-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", { className: "jini-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" }), value: fieldValue("twitterImage", resolved.twitter.image ?? "") ?? "", onChange: (value) => setField("twitterImage", value), agentHandle: "seo-entry-twitter-image" }), _jsx("span", { className: "jini-editor-actions", children: _jsx("button", { type: "button", className: "jini-btn jini-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: "jini-seo-panel jini-seo-panel--full jini-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: "jini-seo-panel jini-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: "jini-field", children: [_jsx("label", { className: "jini-field-label", htmlFor: "seo-title-template", children: t({ locale: locale, key: "Title template (must contain %s)" }) }), _jsx("input", { className: "jini-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: "jini-field", children: [_jsx("label", { className: "jini-field-label", htmlFor: "seo-default-description", children: t({ locale: locale, key: "Default meta description" }) }), _jsx("textarea", { className: "jini-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" }), value: defaultOgImage, onChange: setDefaultOgImage, agentHandle: "seo-default-og-image" }), _jsxs("div", { className: "jini-field", children: [_jsx("label", { className: "jini-field-label", htmlFor: "seo-twitter-site", children: t({ locale: locale, key: "Twitter @site handle" }) }), _jsx("input", { className: "jini-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: "jini-form-checkbox-field", children: [_jsx("input", { className: "jini-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: "jini-form-checkbox-field", children: [_jsx("input", { className: "jini-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: "jini-form-checkbox-field", children: [_jsx("input", { className: "jini-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: "jini-editor-actions jini-form-actions jini-seo-actions", children: _jsx("button", { className: "jini-btn", 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: "jini-seo-panel jini-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: "jini-card-lead", children: sitemapStateLabel({ sitemapEnabled: settings.sitemapEnabled }, { t: key => t({ locale, key }) }) }), _jsx("p", { children: t({ locale: locale, key: "Rebuild the sitemap now to include the latest published content." }) }), _jsxs("span", { className: "jini-editor-actions", children: [_jsx("button", { className: "jini-btn jini-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: "jini-btn jini-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: "jini-page", children: _jsx("div", { className: "jini-notice jini-notice-error", children: error }) });
199
+ if (!settings)
200
+ return _jsx("div", { className: "jini-page", children: _jsx("div", { className: "jini-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: "jini-page jini-seo-page", children: [_jsxs("div", { className: "jini-page-header", children: [_jsxs("div", { className: "jini-page-header-text", children: [_jsx("p", { className: "jini-page-kicker", children: t({ locale: locale, key: "Marketing" }) }), _jsx("h1", { className: "jini-page-title", children: "SEO" }), _jsx("p", { className: "jini-page-description", children: t({ locale: locale, key: "Site-wide defaults for meta titles, descriptions, Open Graph/Twitter cards, and robots directives." }) })] }), _jsx("div", { className: "jini-page-actions", children: headerActions })] }), error ? _jsx("div", { className: "jini-notice jini-notice-error", children: error }) : null, notice ? _jsx("div", { className: "jini-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,142 @@
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
+ /** A library selection is displayed as a preview while its persisted reference stays intact.
82
+ * URLs and incomplete input remain editable. @complexity O(n) in value length; no I/O.
83
+ */
84
+ export declare function isStoredMediaRef({ value }: {
85
+ value: string;
86
+ }, _optional?: Record<string, never>): boolean;
87
+ /** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
88
+ * prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
89
+ * `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
90
+ * `apps/website/src/features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
91
+ * uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
92
+ * (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
93
+ * for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
94
+ * (S4's `resolveSeoImageRef` accepts either spelling) — this only changes what gets WRITTEN next. */
95
+ export declare function buildMediaRef({ item }: {
96
+ item: {
97
+ id: string;
98
+ slug?: string;
99
+ };
100
+ }, _optional?: Record<string, never>): string;
101
+ /** `MediaRefField`'s thumbnail `<img src>` for the field's current value. An absolute URL (the
102
+ * field's other legal shape — someone pasted a raw URL) renders directly; a `{assetId}:{transform}`
103
+ * ref resolves through the injected `mediaOriginalUrl` builder — the admin media library's own
104
+ * preview URL (`MediaPickerPort.mediaOriginalUrl`), same as `MediaPickerDialog`'s own grid
105
+ * thumbnails use, NOT the public `/m/...` rendition URL (that needs a live workspace/transform
106
+ * lookup this client-side preview has no reason to perform). `null` for an empty or unparseable
107
+ * value — the caller renders no preview then, rather than a broken `<img>`. `typeof value !==
108
+ * "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
109
+ * `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
110
+ * non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
111
+ * "never throws over one bad reference" contract (`apps/website/src/features/seo/media.ts`).
112
+ *
113
+ * @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
114
+ * performs the actual fetch only once it is used as an `<img src>`). */
115
+ export declare function resolveMediaRefPreviewUrl({ value, mediaOriginalUrl }: {
116
+ value: string;
117
+ mediaOriginalUrl: (id: string) => string;
118
+ }, _optional?: Record<string, never>): string | null;
119
+ /**
120
+ * The site-wide settings patch the defaults form submits — lifted out of the form's own inline
121
+ * `onSubmit` when this screen gained tabs. Same seven fields, same `checkbox === "on"` reading of
122
+ * the three toggles.
123
+ *
124
+ * ONE deliberate behavior change since that lift (2026-09-06): the three optional scalars now
125
+ * submit `null` rather than `undefined` when their box is empty, so emptying one CLEARS the site
126
+ * default instead of silently leaving it as it was. See {@link optionalFormString}. `titleTemplate`
127
+ * is unaffected — it is not nullable server-side and an emptied box still submits `""`, which the
128
+ * server rejects with its "must contain '%s'" validation error.
129
+ *
130
+ * @complexity O(1) — a fixed set of field reads, no iteration.
131
+ */
132
+ export declare function buildSeoSettingsPatch({ form }: {
133
+ form: FormData;
134
+ }, _optional?: Record<string, never>): SeoSettingsPatch;
135
+ /** Three stable query ids; defaults remains the first and fallback tab. */
136
+ export declare const SEO_TAB_IDS: readonly ["defaults", "sitemap", "entries"];
137
+ export type SeoTabId = (typeof SEO_TAB_IDS)[number];
138
+ /** Resolves an absent or stale bookmark to the defaults panel. */
139
+ export declare function resolveSeoTabId({ tabId }: {
140
+ tabId?: string | null | undefined;
141
+ }, _optional?: Record<string, never>): SeoTabId;
142
+ //# sourceMappingURL=rules.d.ts.map
@@ -0,0 +1,220 @@
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
+ /** A library selection is displayed as a preview while its persisted reference stays intact.
121
+ * URLs and incomplete input remain editable. @complexity O(n) in value length; no I/O.
122
+ */
123
+ export function isStoredMediaRef({ value }, _optional = {}) {
124
+ const trimmed = typeof value === "string" ? value.trim() : "";
125
+ // A URL typed one character at a time reaches "https:/" before it reaches "https://".
126
+ return !trimmed.includes(":/") && parseMediaRefAssetId(trimmed) !== null;
127
+ }
128
+ /** Builds the `{slug}:public` reference `MediaRefField` writes into the field on selection —
129
+ * prefers the asset's readable slug over its id (readable-slugs S5b, 2026-09-23), matching
130
+ * `resolveSeoImageRef`'s own id-or-slug lookup server-side (`findMediaByIdOrSlug`,
131
+ * `apps/website/src/features/seo/media.ts`, S4) so a saved override reads as a slug, not an opaque
132
+ * uuid. Falls back to `id` when `slug` is empty — defensive only; a real upload always derives one
133
+ * (`deriveUniqueMediaSlug`). Same `transformName: "public"` `EmbedInsertControl.tsx` already writes
134
+ * for an inserted image node. An old stored `{assetId}:public` value keeps resolving regardless
135
+ * (S4's `resolveSeoImageRef` accepts either spelling) — this only changes what gets WRITTEN next. */
136
+ export function buildMediaRef({ item }, _optional = {}) {
137
+ const key = item.slug || item.id;
138
+ return `${key}:${SEO_IMAGE_TRANSFORM}`;
139
+ }
140
+ /** `MediaRefField`'s thumbnail `<img src>` for the field's current value. An absolute URL (the
141
+ * field's other legal shape — someone pasted a raw URL) renders directly; a `{assetId}:{transform}`
142
+ * ref resolves through the injected `mediaOriginalUrl` builder — the admin media library's own
143
+ * preview URL (`MediaPickerPort.mediaOriginalUrl`), same as `MediaPickerDialog`'s own grid
144
+ * thumbnails use, NOT the public `/m/...` rendition URL (that needs a live workspace/transform
145
+ * lookup this client-side preview has no reason to perform). `null` for an empty or unparseable
146
+ * value — the caller renders no preview then, rather than a broken `<img>`. `typeof value !==
147
+ * "string"` also resolves to `null` rather than throwing — `MediaRefField`'s `value` prop is a
148
+ * `fieldValue(key, resolved) ?? ""` fallback chain (`react/pages/SeoPage.tsx`), where `??` does not replace a
149
+ * non-nullish-but-wrong-typed result from a misbehaving caller; matches `resolveSeoImageRef`'s own
150
+ * "never throws over one bad reference" contract (`apps/website/src/features/seo/media.ts`).
151
+ *
152
+ * @complexity O(1) — string parsing only, no I/O (the returned URL is a template; the browser
153
+ * performs the actual fetch only once it is used as an `<img src>`). */
154
+ export function resolveMediaRefPreviewUrl({ value, mediaOriginalUrl }, _optional = {}) {
155
+ const trimmed = typeof value === "string" ? value.trim() : "";
156
+ if (!trimmed)
157
+ return null;
158
+ if (isAbsoluteMediaRef(trimmed))
159
+ return trimmed;
160
+ const assetId = parseMediaRefAssetId(trimmed);
161
+ return assetId ? mediaOriginalUrl(assetId) : null;
162
+ }
163
+ /** One `FormData` field as a string, with the caller's fallback for an absent field. Its own named
164
+ * function purely so {@link buildSeoSettingsPatch} below stays readable at a glance: written
165
+ * inline, the seven fields' `?? `/`||` chains scored the patch builder at the ESLint ceiling
166
+ * (`apps/admin` enforces 9/9 as a hard error) for no gain in clarity. */
167
+ function formString(form, key, fallback = "") {
168
+ return String(form.get(key) ?? fallback);
169
+ }
170
+ /**
171
+ * A `FormData` field that is sent as `null` when blank, rather than persisted as an empty string.
172
+ *
173
+ * `null` is what CLEARS a site default: the server registers these three (`default_description`,
174
+ * `default_og_image`, `twitter_site`) as `nullable: true` and `buildScalarWrites`
175
+ * (`apps/website/src/features/seo/settings.ts`) normalizes `null` to the `""` absent-sentinel that
176
+ * `getSeoSettings` reads back as `undefined`.
177
+ *
178
+ * It used to return `undefined`, under a comment claiming "an empty box means 'no site default'".
179
+ * That was false and is the bug this replaces: `setSeoSettings` is a MERGE, and `buildScalarWrites`
180
+ * does `if (raw === undefined) continue;` — so an omitted key means UNCHANGED, and an operator who
181
+ * emptied "Default meta description" to remove it just saw it come back on the next load, with no
182
+ * way to remove it from this form at all. `undefined` also cannot survive the request: the client
183
+ * `JSON.stringify`s this patch, which drops `undefined` keys and keeps `null` ones.
184
+ */
185
+ function optionalFormString(form, key) {
186
+ return formString(form, key) || null;
187
+ }
188
+ /**
189
+ * The site-wide settings patch the defaults form submits — lifted out of the form's own inline
190
+ * `onSubmit` when this screen gained tabs. Same seven fields, same `checkbox === "on"` reading of
191
+ * the three toggles.
192
+ *
193
+ * ONE deliberate behavior change since that lift (2026-09-06): the three optional scalars now
194
+ * submit `null` rather than `undefined` when their box is empty, so emptying one CLEARS the site
195
+ * default instead of silently leaving it as it was. See {@link optionalFormString}. `titleTemplate`
196
+ * is unaffected — it is not nullable server-side and an emptied box still submits `""`, which the
197
+ * server rejects with its "must contain '%s'" validation error.
198
+ *
199
+ * @complexity O(1) — a fixed set of field reads, no iteration.
200
+ */
201
+ export function buildSeoSettingsPatch({ form }, _optional = {}) {
202
+ return {
203
+ titleTemplate: formString(form, "titleTemplate", "%s"),
204
+ defaultDescription: optionalFormString(form, "defaultDescription"),
205
+ defaultOgImage: optionalFormString(form, "defaultOgImage"),
206
+ twitterSite: optionalFormString(form, "twitterSite"),
207
+ defaultRobots: {
208
+ noindex: form.get("noindex") === "on",
209
+ nofollow: form.get("nofollow") === "on",
210
+ },
211
+ sitemapEnabled: form.get("sitemapEnabled") === "on",
212
+ };
213
+ }
214
+ /** Three stable query ids; defaults remains the first and fallback tab. */
215
+ export const SEO_TAB_IDS = ['defaults', 'sitemap', 'entries'];
216
+ /** Resolves an absent or stale bookmark to the defaults panel. */
217
+ export function resolveSeoTabId({ tabId }, _optional = {}) {
218
+ return SEO_TAB_IDS.find(id => id === tabId) ?? 'defaults';
219
+ }
220
+ //# sourceMappingURL=rules.js.map