@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.
- package/CHANGELOG.md +15 -0
- package/README.md +7 -0
- package/dist/agent-plugins/react/components/AgentPluginListPanel.d.ts +1 -1
- package/dist/agent-plugins/react/hooks/AgentPluginInspector.hooks.js +1 -1
- package/dist/agent-plugins/react/hooks/AgentPluginPanel.hooks.d.ts +1 -1
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.d.ts +1 -1
- package/dist/agent-plugins/react/hooks/AgentPluginsPage.hooks.js +1 -1
- package/dist/agent-plugins/react/hooks/MarketplaceTab.hooks.d.ts +1 -1
- package/dist/agent-plugins/react/index.js +1 -1
- package/dist/agent-plugins/react/pages/AgentPluginsPage.d.ts +1 -1
- package/dist/agent-plugins/react/tabs/DownloadedTab.d.ts +1 -1
- package/dist/agent-plugins/react/tabs/InstalledTab.d.ts +1 -1
- package/dist/agent-plugins/react/tabs/MarketplaceTab.d.ts +1 -1
- package/dist/comments/adapters/http.d.ts +29 -0
- package/dist/comments/adapters/http.js +50 -0
- package/dist/comments/adapters/memory.d.ts +17 -0
- package/dist/comments/adapters/memory.js +61 -0
- package/dist/comments/comments.module.d.ts +73 -0
- package/dist/comments/comments.module.js +16 -0
- package/dist/comments/conformance/comments-api.conformance.d.ts +9 -0
- package/dist/comments/conformance/comments-api.conformance.js +63 -0
- package/dist/comments/controllers/comment-queue.controller.d.ts +48 -0
- package/dist/comments/controllers/comment-queue.controller.js +164 -0
- package/dist/comments/controllers/comment-settings.controller.d.ts +32 -0
- package/dist/comments/controllers/comment-settings.controller.js +77 -0
- package/dist/comments/controllers/comments.controller.d.ts +16 -0
- package/dist/comments/controllers/comments.controller.js +16 -0
- package/dist/comments/index.d.ts +13 -0
- package/dist/comments/index.js +8 -0
- package/dist/comments/messages.en.d.ts +54 -0
- package/dist/comments/messages.en.js +56 -0
- package/dist/comments/models.d.ts +22 -0
- package/dist/comments/models.js +2 -0
- package/dist/comments/ports.d.ts +14 -0
- package/dist/comments/ports.js +5 -0
- package/dist/comments/react/components/QueueSection.d.ts +30 -0
- package/dist/comments/react/components/QueueSection.js +57 -0
- package/dist/comments/react/components/SettingsSection.d.ts +14 -0
- package/dist/comments/react/components/SettingsSection.js +23 -0
- package/dist/comments/react/hooks/CommentsPage.hooks.d.ts +9 -0
- package/dist/comments/react/hooks/CommentsPage.hooks.js +10 -0
- package/dist/comments/react/hooks/CommentsPorts.hooks.d.ts +13 -0
- package/dist/comments/react/hooks/CommentsPorts.hooks.js +15 -0
- package/dist/comments/react/hooks/QueueSection.hooks.d.ts +62 -0
- package/dist/comments/react/hooks/QueueSection.hooks.js +57 -0
- package/dist/comments/react/hooks/SettingsSection.hooks.d.ts +12 -0
- package/dist/comments/react/hooks/SettingsSection.hooks.js +21 -0
- package/dist/comments/react/hooks/use-comment-queue.hooks.d.ts +34 -0
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +47 -0
- package/dist/comments/react/hooks/use-comment-settings.hooks.d.ts +22 -0
- package/dist/comments/react/hooks/use-comment-settings.hooks.js +30 -0
- package/dist/comments/react/hooks/use-comments.hooks.d.ts +15 -0
- package/dist/comments/react/hooks/use-comments.hooks.js +20 -0
- package/dist/comments/react/index.d.ts +316 -0
- package/dist/comments/react/index.js +35 -0
- package/dist/comments/react/pages/CommentsPage.d.ts +12 -0
- package/dist/comments/react/pages/CommentsPage.js +15 -0
- package/dist/comments/rules.d.ts +140 -0
- package/dist/comments/rules.js +199 -0
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.js +1 -0
- package/dist/core/ledger-slice.d.ts +90 -0
- package/dist/core/ledger-slice.js +81 -0
- package/dist/core/ports/comments.d.ts +2 -0
- package/dist/core/ports/forms.d.ts +19 -7
- package/dist/core/ports/forms.js +4 -4
- package/dist/core/ports/index.d.ts +1 -1
- package/dist/core/ports/seo.d.ts +33 -6
- package/dist/core/react/bind-react.d.ts +46 -0
- package/dist/core/react/bind-react.js +28 -0
- package/dist/core/react/index.d.ts +3 -0
- package/dist/core/react/index.js +3 -0
- package/dist/core/react/use-controller.d.ts +16 -0
- package/dist/core/react/use-controller.js +18 -0
- package/dist/forms/adapters/http.d.ts +26 -0
- package/dist/forms/adapters/http.js +43 -0
- package/dist/forms/adapters/memory.d.ts +14 -0
- package/dist/forms/adapters/memory.js +77 -0
- package/dist/forms/conformance/forms-api.conformance.d.ts +17 -0
- package/dist/forms/conformance/forms-api.conformance.js +71 -0
- package/dist/forms/controllers/draft-store.d.ts +18 -0
- package/dist/forms/controllers/draft-store.js +17 -0
- package/dist/forms/controllers/field-attributes-dialog.controller.d.ts +37 -0
- package/dist/forms/controllers/field-attributes-dialog.controller.js +27 -0
- package/dist/forms/controllers/form-editor.controller.d.ts +36 -0
- package/dist/forms/controllers/form-editor.controller.js +22 -0
- package/dist/forms/controllers/form-fields-editor.controller.d.ts +20 -0
- package/dist/forms/controllers/form-fields-editor.controller.js +10 -0
- package/dist/forms/controllers/form-submission-detail.controller.d.ts +20 -0
- package/dist/forms/controllers/form-submission-detail.controller.js +10 -0
- package/dist/forms/controllers/form-submissions.controller.d.ts +25 -0
- package/dist/forms/controllers/form-submissions.controller.js +10 -0
- package/dist/forms/controllers/forms-list.controller.d.ts +22 -0
- package/dist/forms/controllers/forms-list.controller.js +10 -0
- package/dist/forms/forms.module.d.ts +138 -0
- package/dist/forms/forms.module.js +17 -0
- package/dist/forms/html-rules.d.ts +34 -0
- package/dist/forms/html-rules.js +34 -0
- package/dist/forms/index.d.ts +14 -0
- package/dist/forms/index.js +12 -0
- package/dist/forms/messages.en.d.ts +101 -0
- package/dist/forms/messages.en.js +105 -0
- package/dist/forms/models.d.ts +29 -0
- package/dist/forms/models.js +8 -0
- package/dist/forms/ports.d.ts +26 -0
- package/dist/forms/ports.js +6 -0
- package/dist/forms/react/components/FieldAttributesDialog.d.ts +14 -0
- package/dist/forms/react/components/FieldAttributesDialog.js +47 -0
- package/dist/forms/react/components/FormEditorFieldsBody.d.ts +28 -0
- package/dist/forms/react/components/FormEditorFieldsBody.js +24 -0
- package/dist/forms/react/components/FormEditorHeaderText.d.ts +7 -0
- package/dist/forms/react/components/FormEditorHeaderText.js +8 -0
- package/dist/forms/react/components/FormEditorMainPanel.d.ts +12 -0
- package/dist/forms/react/components/FormEditorMainPanel.js +13 -0
- package/dist/forms/react/components/FormEditorTabStrip.d.ts +13 -0
- package/dist/forms/react/components/FormEditorTabStrip.js +14 -0
- package/dist/forms/react/components/FormFieldsEditor.d.ts +13 -0
- package/dist/forms/react/components/FormFieldsEditor.js +39 -0
- package/dist/forms/react/components/FormSubmissionDate.d.ts +8 -0
- package/dist/forms/react/components/FormSubmissionDate.js +5 -0
- package/dist/forms/react/components/FormSubmissionDetail.d.ts +15 -0
- package/dist/forms/react/components/FormSubmissionDetail.js +30 -0
- package/dist/forms/react/components/FormSubmissions.d.ts +12 -0
- package/dist/forms/react/components/FormSubmissions.js +46 -0
- package/dist/forms/react/hooks/FormsPorts.hooks.d.ts +41 -0
- package/dist/forms/react/hooks/FormsPorts.hooks.js +16 -0
- package/dist/forms/react/hooks/forms-view.hooks.d.ts +117 -0
- package/dist/forms/react/hooks/forms-view.hooks.js +78 -0
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.d.ts +31 -0
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +22 -0
- package/dist/forms/react/hooks/use-form-editor.hooks.d.ts +118 -0
- package/dist/forms/react/hooks/use-form-editor.hooks.js +208 -0
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.d.ts +34 -0
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.js +36 -0
- package/dist/forms/react/hooks/use-form-submission-date.hooks.d.ts +12 -0
- package/dist/forms/react/hooks/use-form-submission-date.hooks.js +9 -0
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.d.ts +68 -0
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +81 -0
- package/dist/forms/react/hooks/use-form-submissions.hooks.d.ts +67 -0
- package/dist/forms/react/hooks/use-form-submissions.hooks.js +110 -0
- package/dist/forms/react/hooks/use-forms-list.hooks.d.ts +78 -0
- package/dist/forms/react/hooks/use-forms-list.hooks.js +103 -0
- package/dist/forms/react/index.d.ts +452 -0
- package/dist/forms/react/index.js +35 -0
- package/dist/forms/react/pages/FormEditor.d.ts +19 -0
- package/dist/forms/react/pages/FormEditor.js +40 -0
- package/dist/forms/react/pages/FormsList.d.ts +39 -0
- package/dist/forms/react/pages/FormsList.js +50 -0
- package/dist/forms/rules.d.ts +291 -0
- package/dist/forms/rules.js +338 -0
- package/dist/media/react/hooks/EditMediaPanel.hooks.js +1 -1
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +1 -1
- package/dist/media/react/hooks/LibraryTab.hooks.js +1 -1
- package/dist/media/react/hooks/MediaCard.hooks.js +6 -20
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +1 -1
- package/dist/media/react/hooks/MediaPage.hooks.js +1 -1
- package/dist/media/react/hooks/MediaPicker.hooks.js +1 -1
- package/dist/media/react/index.js +1 -1
- package/dist/media/react/tabs/LibraryTab.d.ts +1 -1
- package/dist/media/react/tabs/ProvidersTab.d.ts +1 -1
- package/dist/playground/react/components/Canvas.d.ts +1 -1
- package/dist/playground/react/hooks/Canvas.hooks.d.ts +1 -1
- package/dist/playground/react/hooks/Canvas.hooks.js +1 -1
- package/dist/playground/react/hooks/PlaygroundPage.hooks.d.ts +1 -1
- package/dist/playground/react/index.js +1 -1
- package/dist/playground/react/pages/PlaygroundPage.d.ts +1 -1
- package/dist/playground/react/tabs/CanvasTab.d.ts +1 -1
- package/dist/react/bind-react.d.ts +1 -45
- package/dist/react/bind-react.js +2 -27
- package/dist/react/components/ConfirmDialog/ConfirmDialog.d.ts +22 -5
- package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.d.ts +2 -2
- package/dist/react/components/ConfirmDialog/ConfirmDialog.hooks.js +17 -55
- package/dist/react/components/ConfirmDialog/ConfirmDialog.js +33 -70
- package/dist/react/components/RowMenu/RowMenu.hooks.js +14 -3
- package/dist/react/use-controller.d.ts +1 -15
- package/dist/react/use-controller.js +2 -17
- package/dist/redirects/adapters/http.d.ts +20 -0
- package/dist/redirects/adapters/http.js +31 -0
- package/dist/redirects/adapters/memory.d.ts +13 -0
- package/dist/redirects/adapters/memory.js +83 -0
- package/dist/redirects/conformance/redirects-api.conformance.d.ts +12 -0
- package/dist/redirects/conformance/redirects-api.conformance.js +66 -0
- package/dist/redirects/controllers/hit-count.controller.d.ts +14 -0
- package/dist/redirects/controllers/hit-count.controller.js +10 -0
- package/dist/redirects/controllers/import-redirects.controller.d.ts +28 -0
- package/dist/redirects/controllers/import-redirects.controller.js +29 -0
- package/dist/redirects/controllers/redirects.controller.d.ts +37 -0
- package/dist/redirects/controllers/redirects.controller.js +38 -0
- package/dist/redirects/index.d.ts +11 -0
- package/dist/redirects/index.js +10 -0
- package/dist/redirects/messages.en.d.ts +58 -0
- package/dist/redirects/messages.en.js +60 -0
- package/dist/redirects/models.d.ts +14 -0
- package/dist/redirects/models.js +2 -0
- package/dist/redirects/ports.d.ts +9 -0
- package/dist/redirects/ports.js +4 -0
- package/dist/redirects/react/components/HitCountCell.d.ts +19 -0
- package/dist/redirects/react/components/HitCountCell.js +19 -0
- package/dist/redirects/react/components/ImportRedirectsForm.d.ts +17 -0
- package/dist/redirects/react/components/ImportRedirectsForm.js +12 -0
- package/dist/redirects/react/hooks/RedirectsOptions.hooks.d.ts +27 -0
- package/dist/redirects/react/hooks/RedirectsOptions.hooks.js +15 -0
- package/dist/redirects/react/hooks/RedirectsPage.hooks.d.ts +50 -0
- package/dist/redirects/react/hooks/RedirectsPage.hooks.js +30 -0
- package/dist/redirects/react/hooks/RedirectsPorts.hooks.d.ts +10 -0
- package/dist/redirects/react/hooks/RedirectsPorts.hooks.js +11 -0
- package/dist/redirects/react/hooks/use-hit-count-cell.hooks.d.ts +19 -0
- package/dist/redirects/react/hooks/use-hit-count-cell.hooks.js +15 -0
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.d.ts +21 -0
- package/dist/redirects/react/hooks/use-import-redirects-form.hooks.js +25 -0
- package/dist/redirects/react/hooks/use-redirects.hooks.d.ts +34 -0
- package/dist/redirects/react/hooks/use-redirects.hooks.js +63 -0
- package/dist/redirects/react/hooks/wired.hooks.d.ts +55 -0
- package/dist/redirects/react/hooks/wired.hooks.js +31 -0
- package/dist/redirects/react/index.d.ts +324 -0
- package/dist/redirects/react/index.js +28 -0
- package/dist/redirects/react/pages/RedirectsPage.d.ts +15 -0
- package/dist/redirects/react/pages/RedirectsPage.js +20 -0
- package/dist/redirects/redirects.module.d.ts +77 -0
- package/dist/redirects/redirects.module.js +13 -0
- package/dist/redirects/rules.d.ts +131 -0
- package/dist/redirects/rules.js +134 -0
- package/dist/security/react/hooks/AccessTokensTab.hooks.d.ts +1 -1
- package/dist/security/react/hooks/AccessTokensTab.hooks.js +1 -1
- package/dist/security/react/hooks/RootKeyTab.hooks.d.ts +1 -1
- package/dist/security/react/hooks/RootKeyTab.hooks.js +1 -1
- package/dist/security/react/hooks/SecurityPage.hooks.d.ts +1 -1
- package/dist/security/react/index.js +1 -1
- package/dist/security/react/pages/SecurityPage.d.ts +1 -1
- package/dist/security/react/tabs/AccessTokensTab.d.ts +1 -1
- package/dist/security/react/tabs/RootKeyTab.d.ts +1 -1
- package/dist/seo/adapters/http.d.ts +31 -0
- package/dist/seo/adapters/http.js +37 -0
- package/dist/seo/adapters/memory.d.ts +28 -0
- package/dist/seo/adapters/memory.js +129 -0
- package/dist/seo/conformance/seo-api.conformance.d.ts +8 -0
- package/dist/seo/conformance/seo-api.conformance.js +44 -0
- package/dist/seo/controllers/entry-picker.controller.d.ts +21 -0
- package/dist/seo/controllers/entry-picker.controller.js +17 -0
- package/dist/seo/controllers/entry-section.controller.d.ts +20 -0
- package/dist/seo/controllers/entry-section.controller.js +7 -0
- package/dist/seo/controllers/media-ref.controller.d.ts +26 -0
- package/dist/seo/controllers/media-ref.controller.js +14 -0
- package/dist/seo/controllers/seo-entry.controller.d.ts +31 -0
- package/dist/seo/controllers/seo-entry.controller.js +52 -0
- package/dist/seo/controllers/seo.controller.d.ts +29 -0
- package/dist/seo/controllers/seo.controller.js +50 -0
- package/dist/seo/controllers/sitemap.controller.d.ts +25 -0
- package/dist/seo/controllers/sitemap.controller.js +27 -0
- package/dist/seo/index.d.ts +14 -0
- package/dist/seo/index.js +12 -0
- package/dist/seo/messages.en.d.ts +79 -0
- package/dist/seo/messages.en.js +79 -0
- package/dist/seo/models.d.ts +179 -0
- package/dist/seo/models.js +8 -0
- package/dist/seo/ports.d.ts +11 -0
- package/dist/seo/ports.js +4 -0
- package/dist/seo/react/components/MediaPickerBridge.d.ts +4 -0
- package/dist/seo/react/components/MediaPickerBridge.js +8 -0
- package/dist/seo/react/components/MediaRefField.d.ts +40 -0
- package/dist/seo/react/components/MediaRefField.js +12 -0
- package/dist/seo/react/components/SeoIcons.d.ts +13 -0
- package/dist/seo/react/components/SeoIcons.js +32 -0
- package/dist/seo/react/components/SitemapModal.d.ts +48 -0
- package/dist/seo/react/components/SitemapModal.js +72 -0
- package/dist/seo/react/hooks/MediaPickerBridge.hooks.d.ts +6 -0
- package/dist/seo/react/hooks/MediaPickerBridge.hooks.js +27 -0
- package/dist/seo/react/hooks/MediaRefField.hooks.d.ts +25 -0
- package/dist/seo/react/hooks/MediaRefField.hooks.js +30 -0
- package/dist/seo/react/hooks/SeoModuleTabs.d.ts +14 -0
- package/dist/seo/react/hooks/SeoModuleTabs.js +18 -0
- package/dist/seo/react/hooks/SeoPage.hooks.d.ts +69 -0
- package/dist/seo/react/hooks/SeoPage.hooks.js +37 -0
- package/dist/seo/react/hooks/SeoPorts.hooks.d.ts +19 -0
- package/dist/seo/react/hooks/SeoPorts.hooks.js +22 -0
- package/dist/seo/react/hooks/SitemapModal.hooks.d.ts +26 -0
- package/dist/seo/react/hooks/SitemapModal.hooks.js +20 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.d.ts +11 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +20 -0
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.d.ts +14 -0
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +26 -0
- package/dist/seo/react/hooks/use-seo-entry-section.hooks.d.ts +4 -0
- package/dist/seo/react/hooks/use-seo-entry-section.hooks.js +7 -0
- package/dist/seo/react/hooks/use-seo.hooks.d.ts +11 -0
- package/dist/seo/react/hooks/use-seo.hooks.js +29 -0
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.d.ts +10 -0
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +18 -0
- package/dist/seo/react/index.d.ts +369 -0
- package/dist/seo/react/index.js +30 -0
- package/dist/seo/react/options.d.ts +26 -0
- package/dist/seo/react/options.js +2 -0
- package/dist/seo/react/pages/SeoPage.d.ts +161 -0
- package/dist/seo/react/pages/SeoPage.js +215 -0
- package/dist/seo/rules.d.ts +142 -0
- package/dist/seo/rules.js +220 -0
- package/dist/seo/seo.module.d.ts +115 -0
- package/dist/seo/seo.module.js +14 -0
- package/dist/source-control/react/hooks/ProvidersTab.hooks.d.ts +1 -1
- package/dist/source-control/react/hooks/ProvidersTab.hooks.js +1 -1
- package/dist/source-control/react/hooks/SourceControlPage.hooks.d.ts +1 -1
- package/dist/source-control/react/index.js +1 -1
- package/dist/source-control/react/pages/SourceControlPage.d.ts +1 -1
- package/dist/source-control/react/tabs/ProvidersTab.d.ts +1 -1
- package/dist/widgets/adapters/http.d.ts +19 -0
- package/dist/widgets/adapters/http.js +33 -0
- package/dist/widgets/adapters/memory.d.ts +22 -0
- package/dist/widgets/adapters/memory.js +93 -0
- package/dist/widgets/conformance/widgets-api.conformance.d.ts +6 -0
- package/dist/widgets/conformance/widgets-api.conformance.js +55 -0
- package/dist/widgets/controllers/instance-editor.controller.d.ts +49 -0
- package/dist/widgets/controllers/instance-editor.controller.js +87 -0
- package/dist/widgets/controllers/library.controller.d.ts +33 -0
- package/dist/widgets/controllers/library.controller.js +53 -0
- package/dist/widgets/controllers/region-editor.controller.d.ts +42 -0
- package/dist/widgets/controllers/region-editor.controller.js +69 -0
- package/dist/widgets/controllers/regions.controller.d.ts +27 -0
- package/dist/widgets/controllers/regions.controller.js +44 -0
- package/dist/widgets/index.d.ts +11 -0
- package/dist/widgets/index.js +9 -0
- package/dist/widgets/messages.en.d.ts +78 -0
- package/dist/widgets/messages.en.js +83 -0
- package/dist/widgets/models.d.ts +62 -0
- package/dist/widgets/models.js +2 -0
- package/dist/widgets/ports.d.ts +79 -0
- package/dist/widgets/ports.js +5 -0
- package/dist/widgets/react/components/WidgetInstanceNotices.d.ts +14 -0
- package/dist/widgets/react/components/WidgetInstanceNotices.js +25 -0
- package/dist/widgets/react/components/WidgetRegionEditorHeaderActions.d.ts +22 -0
- package/dist/widgets/react/components/WidgetRegionEditorHeaderActions.js +18 -0
- package/dist/widgets/react/components/WidgetsLibraryNotices.d.ts +13 -0
- package/dist/widgets/react/components/WidgetsLibraryNotices.js +10 -0
- package/dist/widgets/react/hooks/WidgetsPorts.hooks.d.ts +270 -0
- package/dist/widgets/react/hooks/WidgetsPorts.hooks.js +17 -0
- package/dist/widgets/react/hooks/navigation.hooks.d.ts +9 -0
- package/dist/widgets/react/hooks/navigation.hooks.js +6 -0
- package/dist/widgets/react/hooks/page-views.hooks.d.ts +87 -0
- package/dist/widgets/react/hooks/page-views.hooks.js +30 -0
- package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.d.ts +56 -0
- package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.js +38 -0
- package/dist/widgets/react/hooks/use-widget-region-editor.hooks.d.ts +32 -0
- package/dist/widgets/react/hooks/use-widget-region-editor.hooks.js +22 -0
- package/dist/widgets/react/hooks/use-widget-regions.hooks.d.ts +23 -0
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +22 -0
- package/dist/widgets/react/hooks/use-widgets-library.hooks.d.ts +37 -0
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +23 -0
- package/dist/widgets/react/index.d.ts +391 -0
- package/dist/widgets/react/index.js +31 -0
- package/dist/widgets/react/pages/WidgetInstanceEditor.d.ts +23 -0
- package/dist/widgets/react/pages/WidgetInstanceEditor.js +37 -0
- package/dist/widgets/react/pages/WidgetRegionEditor.d.ts +22 -0
- package/dist/widgets/react/pages/WidgetRegionEditor.js +25 -0
- package/dist/widgets/react/pages/WidgetRegions.d.ts +21 -0
- package/dist/widgets/react/pages/WidgetRegions.js +31 -0
- package/dist/widgets/react/pages/WidgetsLibrary.d.ts +24 -0
- package/dist/widgets/react/pages/WidgetsLibrary.js +61 -0
- package/dist/widgets/rules.d.ts +94 -0
- package/dist/widgets/rules.js +83 -0
- package/dist/widgets/widgets.module.d.ts +122 -0
- package/dist/widgets/widgets.module.js +21 -0
- 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
|