@prenta/admin 1.17.1 → 1.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/__tests__/lib/posts-service.test.js +35 -0
- package/dist/__tests__/lib/posts-service.test.js.map +1 -1
- package/dist/__tests__/lib/public-path.test.d.ts +2 -0
- package/dist/__tests__/lib/public-path.test.d.ts.map +1 -0
- package/dist/__tests__/lib/public-path.test.js +25 -0
- package/dist/__tests__/lib/public-path.test.js.map +1 -0
- package/dist/__tests__/views/ai-settings.render.test.js +67 -1
- package/dist/__tests__/views/ai-settings.render.test.js.map +1 -1
- package/dist/__tests__/views/posts-list-view.render.test.js +11 -0
- package/dist/__tests__/views/posts-list-view.render.test.js.map +1 -1
- package/dist/__tests__/views/seo-autopilot-card.render.test.js +4 -2
- package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -1
- package/dist/components/CoauthorResultPopover.d.ts.map +1 -1
- package/dist/components/CoauthorResultPopover.js +4 -1
- package/dist/components/CoauthorResultPopover.js.map +1 -1
- package/dist/components/CommentSidePanel.d.ts +0 -7
- package/dist/components/CommentSidePanel.d.ts.map +1 -1
- package/dist/components/CommentSidePanel.js +0 -2
- package/dist/components/CommentSidePanel.js.map +1 -1
- package/dist/components/EditorRail.d.ts +2 -1
- package/dist/components/EditorRail.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.d.ts +2 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/TipTapEditor.d.ts +3 -2
- package/dist/components/TipTapEditor.d.ts.map +1 -1
- package/dist/components/forms/EntriesTable.d.ts +0 -1
- package/dist/components/forms/EntriesTable.d.ts.map +1 -1
- package/dist/components/forms/EntriesTable.js +1 -1
- package/dist/components/forms/EntriesTable.js.map +1 -1
- package/dist/components/pages/tag-badges.d.ts +2 -2
- package/dist/components/pages/tag-badges.d.ts.map +1 -1
- package/dist/components/pages/tag-badges.js.map +1 -1
- package/dist/components/seo/FaqProposalPreview.d.ts +2 -3
- package/dist/components/seo/FaqProposalPreview.d.ts.map +1 -1
- package/dist/components/seo/FaqProposalPreview.js +1 -1
- package/dist/components/seo/FaqProposalPreview.js.map +1 -1
- package/dist/components/seo/SeoCopilotDrawer.js +1 -1
- package/dist/components/seo/SeoCopilotDrawer.js.map +1 -1
- package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
- package/dist/components/seo/SeoEditorPane.js +4 -1
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/ui/Badge.d.ts +3 -9
- package/dist/components/ui/Badge.d.ts.map +1 -1
- package/dist/components/ui/Badge.js +2 -3
- package/dist/components/ui/Badge.js.map +1 -1
- package/dist/components/ui/Button.d.ts +2 -2
- package/dist/components/ui/Button.d.ts.map +1 -1
- package/dist/components/ui/Button.js +0 -1
- package/dist/components/ui/Button.js.map +1 -1
- package/dist/components/ui/Card.d.ts +2 -3
- package/dist/components/ui/Card.d.ts.map +1 -1
- package/dist/components/ui/Card.js +0 -4
- package/dist/components/ui/Card.js.map +1 -1
- package/dist/components/ui/Input.d.ts +6 -8
- package/dist/components/ui/Input.d.ts.map +1 -1
- package/dist/components/ui/Input.js +6 -6
- package/dist/components/ui/Input.js.map +1 -1
- package/dist/components/ui/Modal.d.ts +1 -1
- package/dist/components/ui/Modal.d.ts.map +1 -1
- package/dist/components/ui/ResizableAside.d.ts +0 -13
- package/dist/components/ui/ResizableAside.d.ts.map +1 -1
- package/dist/components/ui/ResizableAside.js +5 -5
- package/dist/components/ui/ResizableAside.js.map +1 -1
- package/dist/components/ui/StatusBadge.d.ts +2 -2
- package/dist/components/ui/StatusBadge.d.ts.map +1 -1
- package/dist/components/ui/StatusBadge.js +0 -1
- package/dist/components/ui/StatusBadge.js.map +1 -1
- package/dist/components/ui/Toggle.d.ts +2 -8
- package/dist/components/ui/Toggle.d.ts.map +1 -1
- package/dist/components/ui/Toggle.js +0 -1
- package/dist/components/ui/Toggle.js.map +1 -1
- package/dist/fields/FieldRenderer.d.ts +2 -1
- package/dist/fields/FieldRenderer.d.ts.map +1 -1
- package/dist/fields/index.d.ts +0 -17
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +0 -15
- package/dist/fields/index.js.map +1 -1
- package/dist/layout/primitives/tokens.d.ts +0 -2
- package/dist/layout/primitives/tokens.d.ts.map +1 -1
- package/dist/layout/primitives/tokens.js +0 -6
- package/dist/layout/primitives/tokens.js.map +1 -1
- package/dist/lib/brand-voice-client.d.ts +14 -1
- package/dist/lib/brand-voice-client.d.ts.map +1 -1
- package/dist/lib/brand-voice-client.js +7 -0
- package/dist/lib/brand-voice-client.js.map +1 -1
- package/dist/lib/coauthor-client.d.ts +2 -1
- package/dist/lib/coauthor-client.d.ts.map +1 -1
- package/dist/lib/collection-schema.d.ts +0 -9
- package/dist/lib/collection-schema.d.ts.map +1 -1
- package/dist/lib/collection-schema.js +2 -2
- package/dist/lib/collection-schema.js.map +1 -1
- package/dist/lib/editor-seo-content.d.ts +0 -10
- package/dist/lib/editor-seo-content.d.ts.map +1 -1
- package/dist/lib/editor-seo-content.js +0 -13
- package/dist/lib/editor-seo-content.js.map +1 -1
- package/dist/lib/forms-service.d.ts +3 -21
- package/dist/lib/forms-service.d.ts.map +1 -1
- package/dist/lib/forms-service.js +0 -25
- package/dist/lib/forms-service.js.map +1 -1
- package/dist/lib/notifications-client.d.ts +4 -3
- package/dist/lib/notifications-client.d.ts.map +1 -1
- package/dist/lib/page-audit-input.d.ts +0 -10
- package/dist/lib/page-audit-input.d.ts.map +1 -1
- package/dist/lib/page-audit-input.js +1 -1
- package/dist/lib/page-audit-input.js.map +1 -1
- package/dist/lib/page-editor-service.d.ts +2 -4
- package/dist/lib/page-editor-service.d.ts.map +1 -1
- package/dist/lib/page-editor-service.js +3 -3
- package/dist/lib/page-editor-service.js.map +1 -1
- package/dist/lib/pages-service.d.ts +2 -13
- package/dist/lib/pages-service.d.ts.map +1 -1
- package/dist/lib/pages-service.js +3 -24
- package/dist/lib/pages-service.js.map +1 -1
- package/dist/lib/plan.d.ts +2 -1
- package/dist/lib/plan.d.ts.map +1 -1
- package/dist/lib/post-editor-service.d.ts +3 -13
- package/dist/lib/post-editor-service.d.ts.map +1 -1
- package/dist/lib/post-editor-service.js +5 -6
- package/dist/lib/post-editor-service.js.map +1 -1
- package/dist/lib/post-types-service.d.ts +2 -3
- package/dist/lib/post-types-service.d.ts.map +1 -1
- package/dist/lib/post-types-service.js +1 -1
- package/dist/lib/post-types-service.js.map +1 -1
- package/dist/lib/posts-service.d.ts +8 -1
- package/dist/lib/posts-service.d.ts.map +1 -1
- package/dist/lib/posts-service.js +6 -1
- package/dist/lib/posts-service.js.map +1 -1
- package/dist/lib/preview-link.d.ts +0 -9
- package/dist/lib/preview-link.d.ts.map +1 -1
- package/dist/lib/preview-link.js +0 -15
- package/dist/lib/preview-link.js.map +1 -1
- package/dist/lib/public-path.d.ts +21 -0
- package/dist/lib/public-path.d.ts.map +1 -0
- package/dist/lib/public-path.js +28 -0
- package/dist/lib/public-path.js.map +1 -0
- package/dist/lib/rail-width.d.ts +0 -1
- package/dist/lib/rail-width.d.ts.map +1 -1
- package/dist/lib/rail-width.js +1 -1
- package/dist/lib/rail-width.js.map +1 -1
- package/dist/lib/reflow-probe.d.ts +0 -67
- package/dist/lib/reflow-probe.d.ts.map +1 -1
- package/dist/lib/reflow-probe.js +4 -4
- package/dist/lib/reflow-probe.js.map +1 -1
- package/dist/lib/seo-service.d.ts +14 -32
- package/dist/lib/seo-service.d.ts.map +1 -1
- package/dist/lib/seo-service.js +7 -26
- package/dist/lib/seo-service.js.map +1 -1
- package/dist/lib/theme-chrome.d.ts +0 -1
- package/dist/lib/theme-chrome.d.ts.map +1 -1
- package/dist/lib/theme-chrome.js +1 -1
- package/dist/lib/theme-chrome.js.map +1 -1
- package/dist/lib/version-history.d.ts +2 -2
- package/dist/lib/version-history.d.ts.map +1 -1
- package/dist/lib/workflow-chrome.d.ts +0 -3
- package/dist/lib/workflow-chrome.d.ts.map +1 -1
- package/dist/lib/workflow-chrome.js +3 -3
- package/dist/lib/workflow-chrome.js.map +1 -1
- package/dist/views/Login.d.ts +2 -1
- package/dist/views/Login.d.ts.map +1 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +3 -2
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/PageEditor.d.ts +2 -1
- package/dist/views/PageEditor.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts +0 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +1 -1
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages.d.ts +2 -1
- package/dist/views/Pages.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +1 -1
- package/dist/views/Posts/PostsListView.js.map +1 -1
- package/dist/views/Redirects.d.ts +2 -1
- package/dist/views/Redirects.d.ts.map +1 -1
- package/dist/views/page-editor/section-types.d.ts +2 -2
- package/dist/views/page-editor/section-types.d.ts.map +1 -1
- package/dist/views/page-editor/section-types.js +1 -1
- package/dist/views/page-editor/section-types.js.map +1 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts +2 -1
- package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
- package/dist/views/page-editor/theme-chrome/index.d.ts +1 -6
- package/dist/views/page-editor/theme-chrome/index.d.ts.map +1 -1
- package/dist/views/page-editor/theme-chrome/index.js +1 -3
- package/dist/views/page-editor/theme-chrome/index.js.map +1 -1
- package/dist/views/page-editor/viewports.d.ts +1 -1
- package/dist/views/page-editor/viewports.d.ts.map +1 -1
- package/dist/views/page-editor/viewports.js +1 -1
- package/dist/views/page-editor/viewports.js.map +1 -1
- package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
- package/dist/views/settings/AISettingsTab.js +13 -3
- package/dist/views/settings/AISettingsTab.js.map +1 -1
- package/dist/views/settings/BrandVoiceCard.d.ts +3 -0
- package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
- package/dist/views/settings/BrandVoiceCard.js +2 -2
- package/dist/views/settings/BrandVoiceCard.js.map +1 -1
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.d.ts.map +1 -1
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js +4 -1
- package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js.map +1 -1
- package/dist/views/settings/featureModel.d.ts +0 -3
- package/dist/views/settings/featureModel.d.ts.map +1 -1
- package/dist/views/settings/featureModel.js +2 -2
- package/dist/views/settings/featureModel.js.map +1 -1
- package/dist/views/settings/useAiSettings.d.ts +2 -2
- package/dist/views/settings/useAiSettings.d.ts.map +1 -1
- package/dist/views/settings/useAiSettings.js +6 -2
- package/dist/views/settings/useAiSettings.js.map +1 -1
- package/dist/views/settings/useAppearanceSettings.d.ts +5 -6
- package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
- package/dist/views/settings/useAppearanceSettings.js +1 -1
- package/dist/views/settings/useAppearanceSettings.js.map +1 -1
- package/dist/views/settings/useEmailSettings.d.ts +3 -2
- package/dist/views/settings/useEmailSettings.d.ts.map +1 -1
- package/dist/views/settings/useGeneralSettings.d.ts +2 -11
- package/dist/views/settings/useGeneralSettings.d.ts.map +1 -1
- package/dist/views/settings/useGeneralSettings.js +3 -3
- package/dist/views/settings/useGeneralSettings.js.map +1 -1
- package/dist/views/settings/useSeoSettings.d.ts +1 -1
- package/dist/views/settings/useSeoSettings.d.ts.map +1 -1
- package/dist/views/settings/useUpdates.d.ts +1 -3
- package/dist/views/settings/useUpdates.d.ts.map +1 -1
- package/dist/views/settings/useUpdates.js +1 -1
- package/dist/views/settings/useUpdates.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/lib/posts-service.test.ts +35 -0
- package/src/__tests__/lib/public-path.test.ts +37 -0
- package/src/__tests__/views/ai-settings.render.test.tsx +86 -2
- package/src/__tests__/views/posts-list-view.render.test.tsx +12 -0
- package/src/__tests__/views/seo-autopilot-card.render.test.tsx +4 -4
- package/src/components/CoauthorResultPopover.tsx +3 -1
- package/src/components/CommentSidePanel.tsx +0 -1
- package/src/components/EditorRail.tsx +1 -1
- package/src/components/MediaPickerModal.tsx +1 -1
- package/src/components/TipTapEditor.tsx +2 -2
- package/src/components/forms/EntriesTable.tsx +1 -1
- package/src/components/pages/tag-badges.tsx +1 -1
- package/src/components/seo/FaqProposalPreview.tsx +2 -2
- package/src/components/seo/SeoCopilotDrawer.tsx +1 -1
- package/src/components/seo/SeoEditorPane.tsx +3 -1
- package/src/components/ui/Badge.tsx +4 -6
- package/src/components/ui/Button.tsx +1 -3
- package/src/components/ui/Card.tsx +1 -14
- package/src/components/ui/Input.tsx +7 -15
- package/src/components/ui/Modal.tsx +1 -1
- package/src/components/ui/ResizableAside.tsx +5 -5
- package/src/components/ui/StatusBadge.tsx +1 -3
- package/src/components/ui/Toggle.tsx +1 -3
- package/src/fields/FieldRenderer.tsx +1 -1
- package/src/fields/index.ts +0 -17
- package/src/layout/primitives/tokens.ts +0 -7
- package/src/lib/brand-voice-client.ts +18 -1
- package/src/lib/coauthor-client.ts +1 -1
- package/src/lib/collection-schema.ts +2 -2
- package/src/lib/editor-seo-content.ts +0 -22
- package/src/lib/forms-service.ts +2 -42
- package/src/lib/notifications-client.ts +3 -3
- package/src/lib/page-audit-input.ts +1 -1
- package/src/lib/page-editor-service.ts +1 -3
- package/src/lib/pages-service.ts +4 -29
- package/src/lib/plan.ts +1 -1
- package/src/lib/post-editor-service.ts +4 -10
- package/src/lib/post-types-service.ts +2 -2
- package/src/lib/posts-service.ts +13 -2
- package/src/lib/preview-link.ts +0 -14
- package/src/lib/public-path.ts +33 -0
- package/src/lib/rail-width.ts +1 -1
- package/src/lib/reflow-probe.ts +4 -4
- package/src/lib/seo-service.ts +20 -60
- package/src/lib/theme-chrome.ts +1 -1
- package/src/lib/version-history.ts +2 -2
- package/src/lib/workflow-chrome.ts +3 -3
- package/src/views/Login.tsx +1 -1
- package/src/views/MediaBrowser.tsx +3 -2
- package/src/views/PageEditor.tsx +1 -1
- package/src/views/Pages/PagesListView.tsx +1 -1
- package/src/views/Pages.tsx +1 -1
- package/src/views/Posts/PostsListView.tsx +2 -1
- package/src/views/Redirects.tsx +1 -1
- package/src/views/page-editor/section-types.ts +0 -9
- package/src/views/page-editor/sections/admin-renderers.tsx +1 -1
- package/src/views/page-editor/theme-chrome/index.ts +2 -10
- package/src/views/page-editor/viewports.ts +0 -3
- package/src/views/settings/AISettingsTab.tsx +15 -3
- package/src/views/settings/BrandVoiceCard.tsx +11 -1
- package/src/views/settings/brand-voice/BrandVoiceAlignmentPanel.tsx +3 -1
- package/src/views/settings/featureModel.ts +2 -2
- package/src/views/settings/useAiSettings.ts +8 -3
- package/src/views/settings/useAppearanceSettings.ts +4 -6
- package/src/views/settings/useEmailSettings.ts +2 -2
- package/src/views/settings/useGeneralSettings.ts +5 -10
- package/src/views/settings/useSeoSettings.ts +1 -1
- package/src/views/settings/useUpdates.ts +2 -2
package/src/lib/reflow-probe.ts
CHANGED
|
@@ -66,7 +66,7 @@ export function linesFromMetrics(height: number, lineHeight: number, fontSize: n
|
|
|
66
66
|
* whose content is a single replaced node, or a layout engine that gives no
|
|
67
67
|
* rects for empty text.
|
|
68
68
|
*/
|
|
69
|
-
|
|
69
|
+
function countLines(el: HTMLElement): number {
|
|
70
70
|
const doc = el.ownerDocument
|
|
71
71
|
try {
|
|
72
72
|
const range = doc.createRange()
|
|
@@ -106,7 +106,7 @@ export function countLines(el: HTMLElement): number {
|
|
|
106
106
|
*
|
|
107
107
|
* Use `mountProbeHost` rather than appending by hand.
|
|
108
108
|
*/
|
|
109
|
-
|
|
109
|
+
function createProbeHost(doc: Document = document): HTMLElement {
|
|
110
110
|
const host = doc.createElement('div')
|
|
111
111
|
host.setAttribute('aria-hidden', 'true')
|
|
112
112
|
host.dataset.reflowProbe = 'true'
|
|
@@ -128,7 +128,7 @@ export function createProbeHost(doc: Document = document): HTMLElement {
|
|
|
128
128
|
* root exists (it always does in the app) — and callers should treat a
|
|
129
129
|
* fallback mount as untrustworthy rather than silently measuring.
|
|
130
130
|
*/
|
|
131
|
-
|
|
131
|
+
function mountProbeHost(
|
|
132
132
|
host: HTMLElement,
|
|
133
133
|
doc: Document = document,
|
|
134
134
|
): { detach: () => void; scoped: boolean } {
|
|
@@ -156,7 +156,7 @@ export function mountProbeHost(
|
|
|
156
156
|
* Reporting a line count from the wrong element would be worse than reporting
|
|
157
157
|
* none: an editor reads a number as a fact.
|
|
158
158
|
*/
|
|
159
|
-
|
|
159
|
+
function measureFieldAcrossBreakpoints(
|
|
160
160
|
container: HTMLElement,
|
|
161
161
|
fieldPath: string,
|
|
162
162
|
scope?: string,
|
package/src/lib/seo-service.ts
CHANGED
|
@@ -31,7 +31,7 @@ function throwIfError(res: { error?: string }): void {
|
|
|
31
31
|
export type SeoGrade = 'good' | 'fair' | 'poor' | 'critical'
|
|
32
32
|
export type SeoSeverity = 'critical' | 'warning' | 'info'
|
|
33
33
|
export type SeoIssueStatus = 'open' | 'ignored' | 'resolved' | 'recurring'
|
|
34
|
-
|
|
34
|
+
type SeoIssueCategory =
|
|
35
35
|
| 'content'
|
|
36
36
|
| 'technical'
|
|
37
37
|
| 'redirects'
|
|
@@ -44,7 +44,7 @@ export type RedirectStatusCode = 301 | 302 | 307 | 308 | 410
|
|
|
44
44
|
|
|
45
45
|
// ─── Overview ────────────────────────────────────────────────────────
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
interface SeoSiteScore {
|
|
48
48
|
score: number
|
|
49
49
|
grade: SeoGrade
|
|
50
50
|
/** Change vs the previous audit run, e.g. +6. */
|
|
@@ -93,7 +93,7 @@ export interface QuickWin {
|
|
|
93
93
|
fixActionType: string | null
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
interface SearchPerformanceRow {
|
|
97
97
|
url: string
|
|
98
98
|
title: string
|
|
99
99
|
impressions: number
|
|
@@ -135,20 +135,9 @@ export async function fetchSeoOverview(): Promise<SeoOverview> {
|
|
|
135
135
|
return res.data ?? EMPTY_OVERVIEW
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
export async function fetchSearchPerformance(
|
|
139
|
-
params: {
|
|
140
|
-
limit?: number
|
|
141
|
-
} = {},
|
|
142
|
-
): Promise<SearchPerformanceRow[]> {
|
|
143
|
-
const qs = params.limit ? `?limit=${params.limit}` : ''
|
|
144
|
-
const res = await cmsApi<{ rows: SearchPerformanceRow[] }>(`/seo/search-performance${qs}`)
|
|
145
|
-
throwIfError(res)
|
|
146
|
-
return res.data?.rows ?? []
|
|
147
|
-
}
|
|
148
|
-
|
|
149
138
|
// ─── Per-document search insight (synced GSC + CrUX) ─────────────────
|
|
150
139
|
|
|
151
|
-
|
|
140
|
+
interface SearchInsightTopQuery {
|
|
152
141
|
query: string
|
|
153
142
|
clicks: number
|
|
154
143
|
impressions: number
|
|
@@ -201,8 +190,6 @@ export const SEO_SCHEMA_TYPES = [
|
|
|
201
190
|
'Recipe',
|
|
202
191
|
] as const
|
|
203
192
|
|
|
204
|
-
export type SeoSchemaType = (typeof SEO_SCHEMA_TYPES)[number]
|
|
205
|
-
|
|
206
193
|
export interface SeoContentRecord {
|
|
207
194
|
id: string
|
|
208
195
|
entityType: 'page' | 'post'
|
|
@@ -423,12 +410,6 @@ export async function fetchSeoAuditRuns(): Promise<SeoAuditRun[]> {
|
|
|
423
410
|
return res.data?.runs ?? []
|
|
424
411
|
}
|
|
425
412
|
|
|
426
|
-
export async function fetchSeoAuditRun(auditRunId: string): Promise<SeoAuditRun | null> {
|
|
427
|
-
const res = await cmsApi<SeoAuditRun>(`/seo/audits/${encodeURIComponent(auditRunId)}`)
|
|
428
|
-
throwIfError(res)
|
|
429
|
-
return res.data ?? null
|
|
430
|
-
}
|
|
431
|
-
|
|
432
413
|
export interface SeoIssueQuery {
|
|
433
414
|
severity?: SeoSeverity | 'all'
|
|
434
415
|
category?: SeoIssueCategory | 'all'
|
|
@@ -566,7 +547,7 @@ export async function exportSeoFixBrief(
|
|
|
566
547
|
}
|
|
567
548
|
|
|
568
549
|
/** Issue types with inline Approve fix in the Audit tab (keep in sync with cms-core). */
|
|
569
|
-
|
|
550
|
+
const SEO_INLINE_FIX_ISSUE_TYPES = new Set([
|
|
570
551
|
'meta-title-length',
|
|
571
552
|
'meta-description-length',
|
|
572
553
|
'accidental-noindex',
|
|
@@ -585,7 +566,7 @@ export function isInlineSeoFixIssue(type: string): boolean {
|
|
|
585
566
|
return SEO_INLINE_FIX_ISSUE_TYPES.has(type)
|
|
586
567
|
}
|
|
587
568
|
|
|
588
|
-
|
|
569
|
+
const SEO_CONTENT_BRIEF_ISSUE_TYPES = [
|
|
589
570
|
'thin-content',
|
|
590
571
|
'missing-h1',
|
|
591
572
|
'multiple-h1',
|
|
@@ -596,7 +577,7 @@ export function supportsSeoContentBrief(type: string): boolean {
|
|
|
596
577
|
return (SEO_CONTENT_BRIEF_ISSUE_TYPES as readonly string[]).includes(type)
|
|
597
578
|
}
|
|
598
579
|
|
|
599
|
-
|
|
580
|
+
interface SeoContentBriefSection {
|
|
600
581
|
heading: string
|
|
601
582
|
bullets: string[]
|
|
602
583
|
}
|
|
@@ -643,13 +624,13 @@ export async function generateSeoContentBrief(
|
|
|
643
624
|
return { brief: res.data?.brief }
|
|
644
625
|
}
|
|
645
626
|
|
|
646
|
-
|
|
627
|
+
const SEO_RESEARCH_ISSUE_TYPES = ['thin-content'] as const
|
|
647
628
|
|
|
648
629
|
export function supportsSeoContentResearch(type: string): boolean {
|
|
649
630
|
return (SEO_RESEARCH_ISSUE_TYPES as readonly string[]).includes(type)
|
|
650
631
|
}
|
|
651
632
|
|
|
652
|
-
|
|
633
|
+
interface SeoContentResearchCompetitor {
|
|
653
634
|
url: string
|
|
654
635
|
title: string
|
|
655
636
|
note: string
|
|
@@ -688,21 +669,6 @@ export async function fetchSeoContentResearch(issueId: string): Promise<{
|
|
|
688
669
|
}
|
|
689
670
|
}
|
|
690
671
|
|
|
691
|
-
export async function generateSeoContentResearch(
|
|
692
|
-
issueId: string,
|
|
693
|
-
competitorUrls?: string[],
|
|
694
|
-
): Promise<{ research?: SeoContentResearch; error?: string }> {
|
|
695
|
-
const res = await cmsApi<{ research: SeoContentResearch }>(
|
|
696
|
-
`/seo/issues/${encodeURIComponent(issueId)}/content-research`,
|
|
697
|
-
{
|
|
698
|
-
method: 'POST',
|
|
699
|
-
body: JSON.stringify({ competitorUrls: competitorUrls?.filter(Boolean) }),
|
|
700
|
-
},
|
|
701
|
-
)
|
|
702
|
-
if (res.error) return { error: res.error }
|
|
703
|
-
return { research: res.data?.research }
|
|
704
|
-
}
|
|
705
|
-
|
|
706
672
|
export async function researchAndBriefSeoIssue(
|
|
707
673
|
issueId: string,
|
|
708
674
|
competitorUrls?: string[],
|
|
@@ -916,14 +882,14 @@ export interface SeoAutopilotRunSummary {
|
|
|
916
882
|
haltedReason: string | null
|
|
917
883
|
}
|
|
918
884
|
|
|
919
|
-
|
|
885
|
+
interface SeoAutopilotOutcomeWatch {
|
|
920
886
|
enabled: boolean
|
|
921
887
|
days: number
|
|
922
888
|
dropRatio: number
|
|
923
889
|
minPriorClicks: number
|
|
924
890
|
}
|
|
925
891
|
|
|
926
|
-
|
|
892
|
+
interface SeoAutopilotGovernance {
|
|
927
893
|
/** Settings → AI "Requires approval" on the issue-fix feature vetoes every metadata type. */
|
|
928
894
|
issueFixApprovalRequired: boolean
|
|
929
895
|
/** Same veto for 404-recovery redirects. */
|
|
@@ -1137,7 +1103,7 @@ export async function revertAutopilotChange(
|
|
|
1137
1103
|
|
|
1138
1104
|
// ─── AI crawler activity ─────────────────────────────────────────────
|
|
1139
1105
|
|
|
1140
|
-
|
|
1106
|
+
interface AiCrawlerRow {
|
|
1141
1107
|
bot: string
|
|
1142
1108
|
surface: string
|
|
1143
1109
|
category: string
|
|
@@ -1182,7 +1148,7 @@ export interface SeoGeoEntityRef {
|
|
|
1182
1148
|
* Why `enabled` is `false`: the kill switch in Settings → SEO (`settings`) or
|
|
1183
1149
|
* the GEO tables are not migrated on the CMS database (`unavailable`).
|
|
1184
1150
|
*/
|
|
1185
|
-
|
|
1151
|
+
type SeoGeoDisabledReason = 'settings' | 'unavailable'
|
|
1186
1152
|
|
|
1187
1153
|
export interface SeoGeoReport {
|
|
1188
1154
|
enabled: boolean
|
|
@@ -1354,13 +1320,6 @@ export interface RedirectOverview {
|
|
|
1354
1320
|
trafficRecovered: number
|
|
1355
1321
|
}
|
|
1356
1322
|
|
|
1357
|
-
export interface Redirect404 {
|
|
1358
|
-
id: string
|
|
1359
|
-
path: string
|
|
1360
|
-
hits: number
|
|
1361
|
-
lastSeenAt: string
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
1323
|
export interface RedirectSuggestion {
|
|
1365
1324
|
id: string
|
|
1366
1325
|
fromPath: string
|
|
@@ -1581,9 +1540,10 @@ export interface SeoAiResult {
|
|
|
1581
1540
|
}
|
|
1582
1541
|
|
|
1583
1542
|
/**
|
|
1584
|
-
* AI SEO endpoints report unavailability
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1543
|
+
* AI SEO endpoints report unavailability as 503 (no provider connection or
|
|
1544
|
+
* model configured in Settings → AI; the body carries `code` + `hint`). 501 is
|
|
1545
|
+
* kept for CMS builds older than @prenta/core 7. Both map to the same graceful
|
|
1546
|
+
* "AI unavailable" UX.
|
|
1587
1547
|
*/
|
|
1588
1548
|
function isAiUnavailable(status?: number): boolean {
|
|
1589
1549
|
return status === 501 || status === 503
|
|
@@ -1755,7 +1715,7 @@ export async function fetchLinkHealthIssues(): Promise<{
|
|
|
1755
1715
|
// `seo/faq-signals.ts` (`FaqSignals`). The server is the contract.
|
|
1756
1716
|
|
|
1757
1717
|
/** Same union as core's `SeoProposalFixStrategy`; `null` for plain metadata writes. */
|
|
1758
|
-
|
|
1718
|
+
type SeoProposalFixStrategy = 'redirect' | 'fix-link' | 'insert-link' | 'add-section'
|
|
1759
1719
|
|
|
1760
1720
|
/** Why a page got a `missing-faq` proposal (28-day window). */
|
|
1761
1721
|
export interface SeoFaqSignals {
|
|
@@ -1769,7 +1729,7 @@ export interface SeoFaqSignals {
|
|
|
1769
1729
|
queries: Array<{ query: string; impressions: number; clicks: number }>
|
|
1770
1730
|
}
|
|
1771
1731
|
|
|
1772
|
-
|
|
1732
|
+
interface SeoFaqSectionDraft {
|
|
1773
1733
|
sectionType: 'faq'
|
|
1774
1734
|
content: { heading: string; items: Array<{ question: string; answer: string }> }
|
|
1775
1735
|
}
|
|
@@ -1845,7 +1805,7 @@ export interface SeoProposalBulkApplyItem {
|
|
|
1845
1805
|
}
|
|
1846
1806
|
|
|
1847
1807
|
/** Mirrors core's `BulkApplyItemStatus` (`seo/proposals-bulk-apply.ts`). */
|
|
1848
|
-
|
|
1808
|
+
type SeoProposalBulkApplyItemStatus =
|
|
1849
1809
|
| 'applied'
|
|
1850
1810
|
| 'stale'
|
|
1851
1811
|
| 'verify-failed'
|
package/src/lib/theme-chrome.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface AdminThemeChrome {
|
|
|
55
55
|
diagnostics?: { navLinkCount?: number; brandPresent?: boolean }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
const THEME_CHROME_MODE_KEY = 'prenta-theme-chrome-mode'
|
|
59
59
|
export const THEME_CHROME_MAIN_ID = 'prenta-canvas-main'
|
|
60
60
|
|
|
61
61
|
export function isThemeChromeEnabled(
|
|
@@ -21,9 +21,9 @@ import { coerceSections, type PageSection } from '@prenta/core/page-sections'
|
|
|
21
21
|
* `UPDATE` as "Saved" rather than "Draft" — the neutral reading is the only
|
|
22
22
|
* one that is true for both old and new rows.
|
|
23
23
|
*/
|
|
24
|
-
|
|
24
|
+
type VersionChangeType = 'CREATE' | 'UPDATE' | 'PUBLISH' | 'RESTORE' | 'DELETE'
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
interface VersionAuthor {
|
|
27
27
|
id: string
|
|
28
28
|
/** Display name, falling back to email and then to a non-blank placeholder. */
|
|
29
29
|
name: string
|
|
@@ -5,11 +5,11 @@ export type WorkflowChromeAction = 'submit' | 'approve' | 'waiting'
|
|
|
5
5
|
export const SUBMIT_FOR_REVIEW_LABEL = 'Submit for review'
|
|
6
6
|
export const APPROVE_LABEL = 'Approve'
|
|
7
7
|
export const REQUEST_CHANGES_LABEL = 'Request changes'
|
|
8
|
-
|
|
8
|
+
const WAITING_FOR_REVIEW_LABEL = 'Waiting for review.'
|
|
9
9
|
export const REVIEWER_NOTE_PREFIX = 'Reviewer note:'
|
|
10
10
|
export const SUBMIT_BEFORE_PUBLISH_ERROR = 'Submit for review before publishing.'
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const SAVE_DRAFT_LABEL = 'Save Draft'
|
|
12
|
+
const UPDATE_LIVE_LABEL = 'Update live'
|
|
13
13
|
|
|
14
14
|
const REVIEWER_ROLES = new Set(['ADMIN', 'EDITOR'])
|
|
15
15
|
const SUBMIT_ROLES = new Set(['ADMIN', 'EDITOR', 'AUTHOR'])
|
package/src/views/Login.tsx
CHANGED
|
@@ -18,7 +18,7 @@ export interface CaptchaConfig {
|
|
|
18
18
|
* sidebar so integrators only need to set it once. `logo` should be a URL or
|
|
19
19
|
* data URL; pass `null` to opt out of any logo entirely.
|
|
20
20
|
*/
|
|
21
|
-
|
|
21
|
+
interface LoginBrandingConfig {
|
|
22
22
|
logo?: string | null
|
|
23
23
|
name?: string
|
|
24
24
|
tagline?: string
|
|
@@ -342,8 +342,9 @@ export function MediaBrowser({ onNavigate, currentPath }: MediaBrowserProps) {
|
|
|
342
342
|
}>('/ai/media-metadata', { method: 'POST', body: JSON.stringify({ mediaId: activeItem.id }) })
|
|
343
343
|
|
|
344
344
|
if (res.error) {
|
|
345
|
-
// AI
|
|
346
|
-
// basic filename-derived suggestion so the action still
|
|
345
|
+
// No AI provider / model configured in Settings → AI (e.g. local dev):
|
|
346
|
+
// fall back to a basic filename-derived suggestion so the action still
|
|
347
|
+
// helps.
|
|
347
348
|
const base = (activeItem.name ?? '')
|
|
348
349
|
.replace(/\.\w+$/, '')
|
|
349
350
|
.replace(/[-_]/g, ' ')
|
package/src/views/PageEditor.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { SEOPanel, type SEOData } from '../components/SEOPanel.js'
|
|
|
7
7
|
import { useApiData } from '../lib/useApiData.js'
|
|
8
8
|
import { cmsApi } from '../lib/api.js'
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface PageEditorProps {
|
|
11
11
|
id?: string
|
|
12
12
|
onNavigate?: (path: string) => void
|
|
13
13
|
}
|
|
@@ -1451,7 +1451,7 @@ function formatDateShort(input: string): string {
|
|
|
1451
1451
|
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
1452
1452
|
}
|
|
1453
1453
|
|
|
1454
|
-
|
|
1454
|
+
function pageNumbers(current: number, total: number): Array<number | '…'> {
|
|
1455
1455
|
if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1)
|
|
1456
1456
|
const out: Array<number | '…'> = [1]
|
|
1457
1457
|
const start = Math.max(2, current - 1)
|
package/src/views/Pages.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import { FolderTree, type FolderSelection } from '../components/FolderTree.js'
|
|
|
24
24
|
|
|
25
25
|
type PageSortKey = 'title' | 'author' | 'template' | 'status' | 'date'
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
interface PagesProps {
|
|
28
28
|
onNavigate?: (path: string) => void
|
|
29
29
|
}
|
|
30
30
|
|
package/src/views/Redirects.tsx
CHANGED
|
@@ -10,27 +10,18 @@ export {
|
|
|
10
10
|
SECTION_TYPES,
|
|
11
11
|
SECTION_TYPE_LIST,
|
|
12
12
|
getSectionType,
|
|
13
|
-
getSectionTypesForScope,
|
|
14
13
|
getAddableSectionTypesForScope,
|
|
15
|
-
isSectionTypeAddable,
|
|
16
14
|
isSectionTypeHideable,
|
|
17
15
|
isSectionEffectivelyVisible,
|
|
18
|
-
sectionTypeLabel,
|
|
19
|
-
isKnownSectionType,
|
|
20
16
|
resolveRepeaterField,
|
|
21
|
-
isRepeaterField,
|
|
22
17
|
blankRepeaterRow,
|
|
23
18
|
} from '@prenta/core/page-sections'
|
|
24
19
|
|
|
25
20
|
export type {
|
|
26
21
|
SectionTypeId,
|
|
27
22
|
SectionScope,
|
|
28
|
-
SectionFieldType,
|
|
29
23
|
SectionFieldDef,
|
|
30
24
|
SectionSettings,
|
|
31
|
-
SectionTypeDef,
|
|
32
25
|
ResolvedRepeater,
|
|
33
|
-
RepeaterRow,
|
|
34
26
|
PostHeaderConfig,
|
|
35
|
-
PostTemplate,
|
|
36
27
|
} from '@prenta/core/page-sections'
|
|
@@ -62,7 +62,7 @@ export interface AdminSectionPostContext {
|
|
|
62
62
|
* Paint budget to protect, but sections-react renderers call `claim()` during
|
|
63
63
|
* render, so the shape must exist or a real renderer throws on its first image.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
interface AdminLcpController {
|
|
66
66
|
claim(): boolean
|
|
67
67
|
readonly claimed: boolean
|
|
68
68
|
}
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
export {
|
|
2
|
-
ThemeChromeProvider,
|
|
3
|
-
useThemeChromeSpec,
|
|
4
|
-
useThemeChromeContext,
|
|
5
|
-
} from './ThemeChromeProvider.js'
|
|
1
|
+
export { ThemeChromeProvider, useThemeChromeContext } from './ThemeChromeProvider.js'
|
|
6
2
|
export { ThemeChromeFrame } from './ThemeChromeFrame.js'
|
|
7
|
-
|
|
3
|
+
|
|
8
4
|
export { ThemeChromeHealth } from './ThemeChromeHealth.js'
|
|
9
|
-
export type { ThemeChromeHealthProps } from './ThemeChromeHealth.js'
|
|
10
|
-
export { ThemeChromeModeToggle } from './ThemeChromeModeToggle.js'
|
|
11
|
-
export type { ThemeChromeModeToggleProps } from './ThemeChromeModeToggle.js'
|
|
12
|
-
export { useThemeChromeMode } from './useThemeChromeMode.js'
|
|
@@ -5,9 +5,6 @@
|
|
|
5
5
|
* mid-migration; import from `../../lib/breakpoints.js` in new code.
|
|
6
6
|
*/
|
|
7
7
|
export {
|
|
8
|
-
BREAKPOINTS as VIEWPORTS,
|
|
9
8
|
DEFAULT_BREAKPOINT as DEFAULT_VIEWPORT,
|
|
10
|
-
getBreakpoint as getViewport,
|
|
11
|
-
type Breakpoint as Viewport,
|
|
12
9
|
type BreakpointId as ViewportId,
|
|
13
10
|
} from '../../lib/breakpoints.js'
|
|
@@ -54,7 +54,14 @@ export function AISettingsTab({ role, config }: AISettingsTabProps) {
|
|
|
54
54
|
'Connect an AI provider under Provider connections before generating a brand voice profile.',
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
|
|
57
|
+
// The runtime resolves the provider's curated default when no default
|
|
58
|
+
// model is set. Only built-in providers have a curated catalog — a
|
|
59
|
+
// `custom` endpoint cannot supply that default, so a custom-only site (or
|
|
60
|
+
// one with no keyed built-in connection) still needs an explicit model.
|
|
61
|
+
const hasKeyedProvider = ai.providers.some(
|
|
62
|
+
(p) => p.enabled && p.hasKey && p.provider !== 'custom',
|
|
63
|
+
)
|
|
64
|
+
if (!ai.form.defaultModelId && !hasKeyedProvider) {
|
|
58
65
|
return {
|
|
59
66
|
ready: false,
|
|
60
67
|
message:
|
|
@@ -67,8 +74,13 @@ export function AISettingsTab({ role, config }: AISettingsTabProps) {
|
|
|
67
74
|
message: 'Save your AI settings before generating a brand voice profile.',
|
|
68
75
|
}
|
|
69
76
|
}
|
|
70
|
-
return {
|
|
71
|
-
|
|
77
|
+
return {
|
|
78
|
+
ready: true,
|
|
79
|
+
...(ai.form.defaultModelId
|
|
80
|
+
? {}
|
|
81
|
+
: { note: 'No default model selected — using the provider’s recommended default.' }),
|
|
82
|
+
}
|
|
83
|
+
}, [ai.dirty, ai.form.defaultModelId, ai.form.features, ai.providers])
|
|
72
84
|
|
|
73
85
|
if (ai.loading) {
|
|
74
86
|
return (
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
type BrandAlignmentScoreResult,
|
|
10
10
|
type BrandVoiceStructuredProfile,
|
|
11
11
|
} from '@prenta/core/brand-voice'
|
|
12
|
-
import { AlertTriangle, Download, FileUp, Link2, Loader2, Sparkles } from 'lucide-react'
|
|
12
|
+
import { AlertTriangle, Download, FileUp, Info, Link2, Loader2, Sparkles } from 'lucide-react'
|
|
13
13
|
import { useCallback, useId, useMemo, useRef, useState } from 'react'
|
|
14
14
|
import { toast } from 'sonner'
|
|
15
15
|
import { BrandVoiceAlignmentPanel } from './brand-voice/BrandVoiceAlignmentPanel.js'
|
|
@@ -95,7 +95,10 @@ function downloadTextFile(filename: string, contents: string) {
|
|
|
95
95
|
*/
|
|
96
96
|
export interface BrandVoiceAiReadiness {
|
|
97
97
|
ready: boolean
|
|
98
|
+
/** Why AI generation is blocked (rendered as a warning when `ready` is false). */
|
|
98
99
|
message?: string
|
|
100
|
+
/** Informational context when ready — e.g. running on the provider's default model. */
|
|
101
|
+
note?: string
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
export function BrandVoiceCard({
|
|
@@ -348,6 +351,13 @@ export function BrandVoiceCard({
|
|
|
348
351
|
</div>
|
|
349
352
|
)}
|
|
350
353
|
|
|
354
|
+
{aiReadiness?.ready && aiReadiness.note && (
|
|
355
|
+
<p className="text-muted-foreground flex items-start gap-2 text-sm">
|
|
356
|
+
<Info size={16} className="mt-0.5 shrink-0" aria-hidden="true" />
|
|
357
|
+
<span>{aiReadiness.note}</span>
|
|
358
|
+
</p>
|
|
359
|
+
)}
|
|
360
|
+
|
|
351
361
|
<div className="flex flex-wrap items-center gap-2">
|
|
352
362
|
<button
|
|
353
363
|
type="button"
|
|
@@ -5,7 +5,7 @@ import { BRAND_VOICE_CHANNEL_PRESETS } from '@prenta/core/brand-voice'
|
|
|
5
5
|
import { Loader2, Wand2 } from 'lucide-react'
|
|
6
6
|
import { useId, useState } from 'react'
|
|
7
7
|
import { toast } from 'sonner'
|
|
8
|
-
import { fixBrandVoiceContent } from '../../../lib/brand-voice-client.js'
|
|
8
|
+
import { brandVoiceFixAiNotice, fixBrandVoiceContent } from '../../../lib/brand-voice-client.js'
|
|
9
9
|
|
|
10
10
|
const INPUT_CLASS =
|
|
11
11
|
'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-60'
|
|
@@ -78,6 +78,8 @@ export function BrandVoiceAlignmentPanel({
|
|
|
78
78
|
? 'Copy revised to match brand voice.'
|
|
79
79
|
: 'Applied brand voice rule fixes.',
|
|
80
80
|
)
|
|
81
|
+
const notice = brandVoiceFixAiNotice(res.result)
|
|
82
|
+
if (notice) toast.info(notice)
|
|
81
83
|
} finally {
|
|
82
84
|
setFixing(false)
|
|
83
85
|
}
|
|
@@ -21,13 +21,13 @@ const CAPABILITY_LABELS: Record<string, string> = {
|
|
|
21
21
|
longContext: 'long context',
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
function findModel(models: AiModelView[], id: string | undefined): AiModelView | undefined {
|
|
25
25
|
if (!id) return undefined
|
|
26
26
|
return models.find((m) => m.id === id || m.providerModelId === id)
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/** Required capabilities a model is missing. Unknown capability strings are ignored. */
|
|
30
|
-
|
|
30
|
+
function missingCapabilities(model: AiModelView | undefined, required: string[]): string[] {
|
|
31
31
|
const wanted = required.filter((c) => c in CAPABILITY_LABELS)
|
|
32
32
|
if (!model) return wanted
|
|
33
33
|
return wanted.filter((c) => !model.capabilities?.[c])
|
|
@@ -31,7 +31,7 @@ export type AiConnectionStatus =
|
|
|
31
31
|
| 'provider_down'
|
|
32
32
|
| 'needs_setup'
|
|
33
33
|
| 'unknown'
|
|
34
|
-
|
|
34
|
+
type AiModelStatus = 'available' | 'unavailable' | 'deprecated' | 'unknown'
|
|
35
35
|
|
|
36
36
|
export interface AiModelView {
|
|
37
37
|
id: string
|
|
@@ -147,7 +147,7 @@ const EMPTY_BRAND_VOICE: BrandVoiceForm = {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/** The buffered (Save-bar) portion of AI settings. */
|
|
150
|
-
|
|
150
|
+
interface AiSettingsForm {
|
|
151
151
|
defaultProviderId: string
|
|
152
152
|
defaultModelId: string
|
|
153
153
|
features: AiFeatureView[]
|
|
@@ -453,7 +453,12 @@ export function useAiSettings(canEdit: boolean): UseAiSettings {
|
|
|
453
453
|
method: 'POST',
|
|
454
454
|
body: JSON.stringify(input),
|
|
455
455
|
})
|
|
456
|
-
if (res.error)
|
|
456
|
+
if (res.error) {
|
|
457
|
+
// 503 "not configured" bodies carry the runtime's setup `hint`
|
|
458
|
+
// separately from `error` (same contract as every other AI route).
|
|
459
|
+
const hint = (res.errorBody as { hint?: unknown } | undefined)?.hint
|
|
460
|
+
return { ok: false, error: typeof hint === 'string' ? `${res.error} ${hint}` : res.error }
|
|
461
|
+
}
|
|
457
462
|
if (!res.data?.draft) return { ok: false, error: 'No draft profile was returned.' }
|
|
458
463
|
const structured = res.data.draft.document?.trim()
|
|
459
464
|
? parseStructuredProfile(res.data.draft.document)
|
|
@@ -30,10 +30,10 @@ import { cmsApi } from '../../lib/api.js'
|
|
|
30
30
|
import { BRAND_UPDATED_EVENT } from '../../lib/favicon.js'
|
|
31
31
|
import type { SettingsSourceMetadata, ValidationIssue } from './useGeneralSettings.js'
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
type SaveState = 'idle' | 'saving' | 'success' | 'error'
|
|
34
34
|
|
|
35
35
|
/** Theme + typography + motion form (the `appearance` settings blob). */
|
|
36
|
-
|
|
36
|
+
interface AppearanceForm {
|
|
37
37
|
colorMode: ColorMode
|
|
38
38
|
accent: string
|
|
39
39
|
density: SidebarDensity
|
|
@@ -42,7 +42,7 @@ export interface AppearanceForm {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/** Brand asset references + admin branding mode (the `brand` settings blob). */
|
|
45
|
-
|
|
45
|
+
interface BrandForm {
|
|
46
46
|
mode: AdminBrandingMode
|
|
47
47
|
publicPrimaryLogoAssetId?: string
|
|
48
48
|
publicLogoMarkAssetId?: string
|
|
@@ -64,7 +64,7 @@ export type BrandAssetField =
|
|
|
64
64
|
| 'adminDarkLogoAssetId'
|
|
65
65
|
| 'faviconSourceAssetId'
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
const FIELD_ROLE: Record<BrandAssetField, BrandAssetRole> = {
|
|
68
68
|
publicPrimaryLogoAssetId: 'publicPrimaryLogo',
|
|
69
69
|
publicLogoMarkAssetId: 'publicLogoMark',
|
|
70
70
|
publicDarkLogoAssetId: 'publicDarkLogo',
|
|
@@ -74,8 +74,6 @@ export const FIELD_ROLE: Record<BrandAssetField, BrandAssetRole> = {
|
|
|
74
74
|
faviconSourceAssetId: 'faviconSource',
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export type { AssetPreview } from '../../lib/appearance-api.js'
|
|
78
|
-
|
|
79
77
|
/** An existing media-library asset chosen for a brand field via the picker. */
|
|
80
78
|
export interface SelectedBrandAsset {
|
|
81
79
|
id: string
|
|
@@ -5,7 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
5
5
|
import { cmsApi } from '../../lib/api.js'
|
|
6
6
|
|
|
7
7
|
export type EmailProviderKey = 'resend' | 'smtpcom' | 'ses' | 'smtp'
|
|
8
|
-
|
|
8
|
+
type SaveState = 'idle' | 'saving' | 'success' | 'error'
|
|
9
9
|
|
|
10
10
|
export const EMAIL_PROVIDER_OPTIONS: Array<{ value: EmailProviderKey; label: string }> = [
|
|
11
11
|
{ value: 'resend', label: 'Resend' },
|
|
@@ -15,7 +15,7 @@ export const EMAIL_PROVIDER_OPTIONS: Array<{ value: EmailProviderKey; label: str
|
|
|
15
15
|
]
|
|
16
16
|
|
|
17
17
|
/** Form state. Secrets are write-only: inputs start empty and never echo stored values. */
|
|
18
|
-
|
|
18
|
+
interface EmailSettingsForm {
|
|
19
19
|
/** Empty string = email delivery not configured here. */
|
|
20
20
|
provider: EmailProviderKey | ''
|
|
21
21
|
from: string
|