@actuate-media/cms-admin 0.65.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/AdminRoot.d.ts.map +1 -1
  3. package/dist/AdminRoot.js +7 -4
  4. package/dist/AdminRoot.js.map +1 -1
  5. package/dist/__tests__/components/inspector-pane.test.d.ts +2 -0
  6. package/dist/__tests__/components/inspector-pane.test.d.ts.map +1 -0
  7. package/dist/__tests__/components/inspector-pane.test.js +31 -0
  8. package/dist/__tests__/components/inspector-pane.test.js.map +1 -0
  9. package/dist/__tests__/lib/pages-tree.test.d.ts +2 -0
  10. package/dist/__tests__/lib/pages-tree.test.d.ts.map +1 -0
  11. package/dist/__tests__/lib/pages-tree.test.js +173 -0
  12. package/dist/__tests__/lib/pages-tree.test.js.map +1 -0
  13. package/dist/__tests__/views/dashboard.test.js +7 -3
  14. package/dist/__tests__/views/dashboard.test.js.map +1 -1
  15. package/dist/__tests__/views/media-seo-audit.render.test.d.ts +2 -0
  16. package/dist/__tests__/views/media-seo-audit.render.test.d.ts.map +1 -0
  17. package/dist/__tests__/views/media-seo-audit.render.test.js +102 -0
  18. package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -0
  19. package/dist/__tests__/views/onboarding-block.render.test.d.ts +2 -0
  20. package/dist/__tests__/views/onboarding-block.render.test.d.ts.map +1 -0
  21. package/dist/__tests__/views/onboarding-block.render.test.js +146 -0
  22. package/dist/__tests__/views/onboarding-block.render.test.js.map +1 -0
  23. package/dist/__tests__/views/pages-list-view.test.js +77 -1
  24. package/dist/__tests__/views/pages-list-view.test.js.map +1 -1
  25. package/dist/actuate-admin.css +1 -1
  26. package/dist/components/script-tags/ScriptTagEditorPane.d.ts +26 -0
  27. package/dist/components/script-tags/ScriptTagEditorPane.d.ts.map +1 -0
  28. package/dist/components/script-tags/ScriptTagEditorPane.js +160 -0
  29. package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -0
  30. package/dist/components/ui/InspectorPane.d.ts +51 -0
  31. package/dist/components/ui/InspectorPane.d.ts.map +1 -0
  32. package/dist/components/ui/InspectorPane.js +42 -0
  33. package/dist/components/ui/InspectorPane.js.map +1 -0
  34. package/dist/components/ui/RowActions.d.ts +19 -0
  35. package/dist/components/ui/RowActions.d.ts.map +1 -0
  36. package/dist/components/ui/RowActions.js +16 -0
  37. package/dist/components/ui/RowActions.js.map +1 -0
  38. package/dist/components/ui/index.d.ts +4 -0
  39. package/dist/components/ui/index.d.ts.map +1 -1
  40. package/dist/components/ui/index.js +2 -0
  41. package/dist/components/ui/index.js.map +1 -1
  42. package/dist/layout/Header.js +1 -1
  43. package/dist/layout/Header.js.map +1 -1
  44. package/dist/layout/Layout.js +8 -38
  45. package/dist/layout/Layout.js.map +1 -1
  46. package/dist/lib/page-editor-service.d.ts +11 -2
  47. package/dist/lib/page-editor-service.d.ts.map +1 -1
  48. package/dist/lib/page-editor-service.js +8 -2
  49. package/dist/lib/page-editor-service.js.map +1 -1
  50. package/dist/lib/pages-service.d.ts +9 -0
  51. package/dist/lib/pages-service.d.ts.map +1 -1
  52. package/dist/lib/pages-service.js +14 -0
  53. package/dist/lib/pages-service.js.map +1 -1
  54. package/dist/lib/pages-tree.d.ts +44 -0
  55. package/dist/lib/pages-tree.d.ts.map +1 -0
  56. package/dist/lib/pages-tree.js +106 -0
  57. package/dist/lib/pages-tree.js.map +1 -0
  58. package/dist/lib/posts-service.d.ts +11 -0
  59. package/dist/lib/posts-service.d.ts.map +1 -1
  60. package/dist/lib/posts-service.js +13 -0
  61. package/dist/lib/posts-service.js.map +1 -1
  62. package/dist/lib/seo-service.d.ts +3 -0
  63. package/dist/lib/seo-service.d.ts.map +1 -1
  64. package/dist/lib/seo-service.js.map +1 -1
  65. package/dist/views/Dashboard.d.ts.map +1 -1
  66. package/dist/views/Dashboard.js +40 -3
  67. package/dist/views/Dashboard.js.map +1 -1
  68. package/dist/views/MediaBrowser.d.ts.map +1 -1
  69. package/dist/views/MediaBrowser.js +55 -2
  70. package/dist/views/MediaBrowser.js.map +1 -1
  71. package/dist/views/MediaSeoAudit.d.ts +25 -0
  72. package/dist/views/MediaSeoAudit.d.ts.map +1 -0
  73. package/dist/views/MediaSeoAudit.js +97 -0
  74. package/dist/views/MediaSeoAudit.js.map +1 -0
  75. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  76. package/dist/views/Pages/PagesListView.js +145 -16
  77. package/dist/views/Pages/PagesListView.js.map +1 -1
  78. package/dist/views/Pages/PagesTreeTable.d.ts +35 -0
  79. package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -0
  80. package/dist/views/Pages/PagesTreeTable.js +28 -0
  81. package/dist/views/Pages/PagesTreeTable.js.map +1 -0
  82. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  83. package/dist/views/Posts/PostsListView.js +66 -9
  84. package/dist/views/Posts/PostsListView.js.map +1 -1
  85. package/dist/views/ScriptTags.d.ts +5 -1
  86. package/dist/views/ScriptTags.d.ts.map +1 -1
  87. package/dist/views/ScriptTags.js +32 -7
  88. package/dist/views/ScriptTags.js.map +1 -1
  89. package/dist/views/dashboard/OnboardingBlock.d.ts +29 -0
  90. package/dist/views/dashboard/OnboardingBlock.d.ts.map +1 -0
  91. package/dist/views/dashboard/OnboardingBlock.js +71 -0
  92. package/dist/views/dashboard/OnboardingBlock.js.map +1 -0
  93. package/dist/views/page-editor/PageSectionEditor.d.ts +7 -1
  94. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  95. package/dist/views/page-editor/PageSectionEditor.js +3 -3
  96. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  97. package/dist/views/page-editor/PageSettingsModal.d.ts +1 -1
  98. package/dist/views/page-editor/PageSettingsModal.d.ts.map +1 -1
  99. package/dist/views/page-editor/PageSettingsModal.js +42 -3
  100. package/dist/views/page-editor/PageSettingsModal.js.map +1 -1
  101. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  102. package/dist/views/seo/TechnicalTab.js +15 -0
  103. package/dist/views/seo/TechnicalTab.js.map +1 -1
  104. package/package.json +3 -3
  105. package/src/AdminRoot.tsx +7 -3
  106. package/src/__tests__/components/inspector-pane.test.tsx +64 -0
  107. package/src/__tests__/lib/pages-tree.test.ts +197 -0
  108. package/src/__tests__/views/dashboard.test.tsx +7 -3
  109. package/src/__tests__/views/media-seo-audit.render.test.tsx +117 -0
  110. package/src/__tests__/views/onboarding-block.render.test.tsx +193 -0
  111. package/src/__tests__/views/pages-list-view.test.tsx +93 -1
  112. package/src/components/script-tags/ScriptTagEditorPane.tsx +474 -0
  113. package/src/components/ui/InspectorPane.tsx +129 -0
  114. package/src/components/ui/RowActions.tsx +50 -0
  115. package/src/components/ui/index.ts +12 -0
  116. package/src/layout/Header.tsx +2 -2
  117. package/src/layout/Layout.tsx +8 -80
  118. package/src/lib/page-editor-service.ts +16 -2
  119. package/src/lib/pages-service.ts +18 -0
  120. package/src/lib/pages-tree.ts +145 -0
  121. package/src/lib/posts-service.ts +26 -0
  122. package/src/lib/seo-service.ts +3 -0
  123. package/src/views/Dashboard.tsx +286 -214
  124. package/src/views/MediaBrowser.tsx +673 -512
  125. package/src/views/MediaSeoAudit.tsx +320 -0
  126. package/src/views/Pages/PagesListView.tsx +459 -185
  127. package/src/views/Pages/PagesTreeTable.tsx +339 -0
  128. package/src/views/Posts/PostsListView.tsx +135 -10
  129. package/src/views/ScriptTags.tsx +204 -129
  130. package/src/views/dashboard/OnboardingBlock.tsx +233 -0
  131. package/src/views/page-editor/PageSectionEditor.tsx +9 -2
  132. package/src/views/page-editor/PageSettingsModal.tsx +75 -2
  133. package/src/views/seo/TechnicalTab.tsx +15 -0
  134. package/dist/views/ScriptTagEditor.d.ts +0 -6
  135. package/dist/views/ScriptTagEditor.d.ts.map +0 -1
  136. package/dist/views/ScriptTagEditor.js +0 -111
  137. package/dist/views/ScriptTagEditor.js.map +0 -1
  138. package/src/views/ScriptTagEditor.tsx +0 -353
@@ -5,30 +5,22 @@ import { Code2, Plus, Loader2, AlertTriangle } from 'lucide-react'
5
5
  import { toast } from 'sonner'
6
6
  import { EnabledSwitch } from '../components/EnabledSwitch.js'
7
7
  import { StatPill } from '../components/ui/StatPill.js'
8
+ import { InspectorPane, SplitPaneLayout, SplitPaneList } from '../components/ui/InspectorPane.js'
9
+ import {
10
+ CONSENT_LABELS,
11
+ PLACEMENT_LABELS,
12
+ ScriptTagEditorPane,
13
+ type ScriptTag,
14
+ } from '../components/script-tags/ScriptTagEditorPane.js'
8
15
  import { useApiData } from '../lib/useApiData.js'
9
16
  import { cmsApi } from '../lib/api.js'
10
17
 
11
- interface ScriptTag {
12
- id: string
13
- name: string
14
- code: string
15
- placement: string
16
- scope: string
17
- targetPaths: string[]
18
- priority: number
19
- enabled: boolean
20
- createdAt: string
21
- updatedAt: string
22
- }
23
-
24
18
  export interface ScriptTagsProps {
25
19
  onNavigate?: (path: string) => void
26
- }
27
-
28
- const PLACEMENT_LABELS: Record<string, string> = {
29
- head: 'Head',
30
- body_open: 'Body Open',
31
- body_close: 'Body Close',
20
+ /** Deep-link support: open this tag's editor pane on mount (/script-tags/:id). */
21
+ initialSelectedId?: string
22
+ /** Deep-link support: open the create pane on mount (/script-tags/new). */
23
+ initialNew?: boolean
32
24
  }
33
25
 
34
26
  const PLACEMENT_COLORS: Record<string, string> = {
@@ -59,10 +51,25 @@ function scopeLabel(tag: ScriptTag): string {
59
51
  return scope || 'Custom'
60
52
  }
61
53
 
62
- export function ScriptTags({ onNavigate }: ScriptTagsProps) {
54
+ function formatUpdated(value: string): string {
55
+ const date = new Date(value)
56
+ if (Number.isNaN(date.getTime())) return '—'
57
+ return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
58
+ }
59
+
60
+ type Selection = { mode: 'edit'; id: string } | { mode: 'new' } | null
61
+
62
+ export function ScriptTags({ initialSelectedId, initialNew }: ScriptTagsProps) {
63
63
  const { data, loading, error, refetch } = useApiData<ScriptTag[]>('/script-tags')
64
64
  const [togglingId, setTogglingId] = useState<string | null>(null)
65
65
  const [enabledOverrides, setEnabledOverrides] = useState<Record<string, boolean>>({})
66
+ const [selection, setSelection] = useState<Selection>(
67
+ initialNew
68
+ ? { mode: 'new' }
69
+ : initialSelectedId
70
+ ? { mode: 'edit', id: initialSelectedId }
71
+ : null,
72
+ )
66
73
 
67
74
  const tags = useMemo(
68
75
  () =>
@@ -73,6 +80,16 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
73
80
  [data, enabledOverrides],
74
81
  )
75
82
 
83
+ const selectedTag =
84
+ selection?.mode === 'edit' ? (tags.find((t) => t.id === selection.id) ?? null) : null
85
+ const paneOpen = selection?.mode === 'new' || selectedTag !== null
86
+
87
+ const toggleRow = (tag: ScriptTag) => {
88
+ setSelection((prev) =>
89
+ prev?.mode === 'edit' && prev.id === tag.id ? null : { mode: 'edit', id: tag.id },
90
+ )
91
+ }
92
+
76
93
  const toggleEnabled = async (tag: ScriptTag, nextEnabled: boolean) => {
77
94
  setEnabledOverrides((prev) => ({ ...prev, [tag.id]: nextEnabled }))
78
95
  setTogglingId(tag.id)
@@ -99,6 +116,12 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
99
116
  toast.success(saved ? 'Tag enabled' : 'Tag disabled')
100
117
  }
101
118
 
119
+ const handlePaneChanged = (savedId?: string) => {
120
+ setEnabledOverrides({})
121
+ refetch()
122
+ if (savedId) setSelection({ mode: 'edit', id: savedId })
123
+ }
124
+
102
125
  if (loading) {
103
126
  return (
104
127
  <div className="flex h-64 items-center justify-center p-3 pr-6 sm:p-4 sm:pr-8">
@@ -108,122 +131,174 @@ export function ScriptTags({ onNavigate }: ScriptTagsProps) {
108
131
  }
109
132
 
110
133
  return (
111
- <div className="p-3 pr-6 sm:p-4 sm:pr-8">
112
- {error && (
113
- <div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-3">
114
- <AlertTriangle className="text-destructive h-5 w-5 shrink-0" />
115
- <span className="text-destructive flex-1 text-sm">{error}</span>
116
- <button
117
- onClick={refetch}
118
- className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors"
119
- >
120
- Retry
121
- </button>
122
- </div>
123
- )}
124
-
125
- <div className="mb-4 flex items-center justify-between">
126
- <div>
127
- <h1 className="text-foreground mb-1">Script Tags</h1>
128
- <p className="text-muted-foreground text-sm">
129
- Manage tracking codes, analytics, and custom scripts injected into your site
130
- </p>
131
- {tags.length > 0 && (
132
- <div className="mt-3 flex flex-wrap gap-1.5">
133
- <StatPill value={tags.length} label="Total" />
134
- <StatPill
135
- value={tags.filter((t) => t.enabled).length}
136
- label="Active"
137
- valueClassName="text-[var(--suc)]"
138
- />
139
- <StatPill
140
- value={tags.filter((t) => !t.enabled).length}
141
- label="Inactive"
142
- valueClassName="text-[var(--muted)]"
143
- />
144
- </div>
145
- )}
146
- </div>
147
- <button
148
- type="button"
149
- onClick={() => onNavigate?.('/script-tags/new')}
150
- className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
151
- >
152
- <Plus className="h-4 w-4" />
153
- New Tag
154
- </button>
155
- </div>
134
+ <div className="flex h-full min-h-0 flex-col">
135
+ <div className="p-3 pr-6 pb-0 sm:p-4 sm:pr-8 sm:pb-0">
136
+ {error && (
137
+ <div className="border-destructive/30 bg-destructive/10 mb-4 flex items-center gap-3 rounded-lg border p-3">
138
+ <AlertTriangle className="text-destructive h-5 w-5 shrink-0" />
139
+ <span className="text-destructive flex-1 text-sm">{error}</span>
140
+ <button
141
+ onClick={refetch}
142
+ className="border-destructive/40 text-destructive hover:bg-destructive/15 rounded-lg border px-3 py-1 text-sm transition-colors"
143
+ >
144
+ Retry
145
+ </button>
146
+ </div>
147
+ )}
156
148
 
157
- {tags.length === 0 && !error ? (
158
- <div className="border-border bg-card rounded-lg border p-12 text-center">
159
- <Code2 className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
160
- <h3 className="text-foreground text-sm font-medium">No script tags yet</h3>
161
- <p className="text-muted-foreground mt-1 text-sm">
162
- Add tracking codes like Google Analytics, Tag Manager, or Facebook Pixel.
163
- </p>
149
+ <div className="mb-4 flex items-center justify-between">
150
+ <div>
151
+ <h1 className="text-foreground mb-1">Script Tags</h1>
152
+ <p className="text-muted-foreground text-sm">
153
+ Manage tracking codes, analytics, and custom scripts injected into your site
154
+ </p>
155
+ {tags.length > 0 && (
156
+ <div className="mt-3 flex flex-wrap gap-1.5">
157
+ <StatPill value={tags.length} label="Total" />
158
+ <StatPill
159
+ value={tags.filter((t) => t.enabled).length}
160
+ label="Active"
161
+ valueClassName="text-[var(--suc)]"
162
+ />
163
+ <StatPill
164
+ value={tags.filter((t) => !t.enabled).length}
165
+ label="Inactive"
166
+ valueClassName="text-[var(--muted)]"
167
+ />
168
+ </div>
169
+ )}
170
+ </div>
164
171
  <button
165
172
  type="button"
166
- onClick={() => onNavigate?.('/script-tags/new')}
167
- className="bg-primary text-primary-foreground hover:bg-primary/90 mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
173
+ onClick={() => setSelection({ mode: 'new' })}
174
+ className="bg-primary text-primary-foreground hover:bg-primary/90 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
168
175
  >
169
176
  <Plus className="h-4 w-4" />
170
- Add Your First Tag
177
+ New Script
171
178
  </button>
172
179
  </div>
173
- ) : (
174
- <div className="border-border bg-card overflow-hidden rounded-lg border">
175
- <table className="w-full text-sm">
176
- <thead>
177
- <tr className="border-border bg-muted border-b">
178
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">Name</th>
179
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">Placement</th>
180
- <th className="text-muted-foreground px-4 py-3 text-left font-medium">Scope</th>
181
- <th className="text-muted-foreground px-4 py-3 text-center font-medium">
182
- Priority
183
- </th>
184
- <th className="text-muted-foreground px-4 py-3 text-center font-medium">Enabled</th>
185
- </tr>
186
- </thead>
187
- <tbody>
188
- {tags.map((tag) => (
189
- <tr
190
- key={tag.id}
191
- className="border-border hover:bg-muted border-b transition-colors last:border-0"
192
- >
193
- <td className="px-4 py-3">
194
- <button
195
- type="button"
196
- onClick={() => onNavigate?.(`/script-tags/${tag.id}`)}
197
- className="text-primary hover:text-primary/80 font-medium hover:underline"
198
- >
199
- {tag.name}
200
- </button>
201
- </td>
202
- <td className="px-4 py-3">
203
- <span
204
- className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-foreground'}`}
205
- >
206
- {PLACEMENT_LABELS[tag.placement] ?? tag.placement}
207
- </span>
208
- </td>
209
- <td className="text-muted-foreground px-4 py-3">{scopeLabel(tag)}</td>
210
- <td className="text-muted-foreground px-4 py-3 text-center font-mono">
211
- {tag.priority}
212
- </td>
213
- <td className="px-4 py-3 text-center">
214
- <EnabledSwitch
215
- checked={tag.enabled}
216
- disabled={togglingId === tag.id}
217
- onCheckedChange={(next) => void toggleEnabled(tag, next)}
218
- aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
219
- />
220
- </td>
221
- </tr>
222
- ))}
223
- </tbody>
224
- </table>
225
- </div>
226
- )}
180
+ </div>
181
+
182
+ <SplitPaneLayout>
183
+ <SplitPaneList className="p-3 pt-0 pr-6 sm:p-4 sm:pt-0 sm:pr-8">
184
+ {tags.length === 0 && !error ? (
185
+ <div className="border-border bg-card rounded-lg border p-12 text-center">
186
+ <Code2 className="text-muted-foreground mx-auto mb-3 h-10 w-10" />
187
+ <h3 className="text-foreground text-sm font-medium">No script tags yet</h3>
188
+ <p className="text-muted-foreground mt-1 text-sm">
189
+ Add tracking codes like Google Analytics, Tag Manager, or Facebook Pixel.
190
+ </p>
191
+ <button
192
+ type="button"
193
+ onClick={() => setSelection({ mode: 'new' })}
194
+ className="bg-primary text-primary-foreground hover:bg-primary/90 mt-4 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
195
+ >
196
+ <Plus className="h-4 w-4" />
197
+ Add Your First Script
198
+ </button>
199
+ </div>
200
+ ) : (
201
+ <div className="border-border bg-card overflow-hidden rounded-lg border">
202
+ <table className="w-full text-sm" aria-label="Script tags">
203
+ <thead>
204
+ <tr className="border-border bg-muted border-b">
205
+ <th className="text-muted-foreground px-4 py-3 text-left font-medium">
206
+ Script
207
+ </th>
208
+ <th className="text-muted-foreground px-4 py-3 text-left font-medium">
209
+ Location
210
+ </th>
211
+ <th className="text-muted-foreground px-4 py-3 text-center font-medium">
212
+ Priority
213
+ </th>
214
+ <th className="text-muted-foreground px-4 py-3 text-left font-medium">Scope</th>
215
+ <th className="text-muted-foreground px-4 py-3 text-left font-medium">
216
+ Consent
217
+ </th>
218
+ <th className="text-muted-foreground px-4 py-3 text-center font-medium">
219
+ Active
220
+ </th>
221
+ <th className="text-muted-foreground px-4 py-3 text-left font-medium">
222
+ Updated
223
+ </th>
224
+ </tr>
225
+ </thead>
226
+ <tbody>
227
+ {tags.map((tag) => {
228
+ const isSelected = selection?.mode === 'edit' && selection.id === tag.id
229
+ return (
230
+ <tr
231
+ key={tag.id}
232
+ className={`border-border cursor-pointer border-b transition-colors last:border-0 ${
233
+ isSelected ? 'bg-[var(--acc-l)]' : 'hover:bg-muted'
234
+ }`}
235
+ onClick={() => toggleRow(tag)}
236
+ >
237
+ <td className="px-4 py-[11px]">
238
+ <button
239
+ type="button"
240
+ onClick={(e) => {
241
+ e.stopPropagation()
242
+ toggleRow(tag)
243
+ }}
244
+ aria-expanded={isSelected}
245
+ className="text-foreground hover:text-primary text-left font-medium"
246
+ >
247
+ {tag.name}
248
+ </button>
249
+ </td>
250
+ <td className="px-4 py-[11px]">
251
+ <span
252
+ className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${PLACEMENT_COLORS[tag.placement] ?? 'bg-muted text-foreground'}`}
253
+ >
254
+ {PLACEMENT_LABELS[tag.placement] ?? tag.placement}
255
+ </span>
256
+ </td>
257
+ <td className="text-muted-foreground px-4 py-[11px] text-center font-mono">
258
+ {tag.priority}
259
+ </td>
260
+ <td className="text-muted-foreground px-4 py-[11px]">{scopeLabel(tag)}</td>
261
+ <td className="px-4 py-[11px]">
262
+ {tag.consentCategory && tag.consentCategory !== 'none' ? (
263
+ <span className="rounded-[5px] border border-[var(--bdr)] bg-[var(--bg)] px-1.5 py-0.5 text-[11px] text-[var(--sub)]">
264
+ {CONSENT_LABELS[tag.consentCategory] ?? tag.consentCategory}
265
+ </span>
266
+ ) : (
267
+ <span className="text-muted-foreground text-xs">—</span>
268
+ )}
269
+ </td>
270
+ <td
271
+ className="px-4 py-[11px] text-center"
272
+ onClick={(e) => e.stopPropagation()}
273
+ >
274
+ <EnabledSwitch
275
+ checked={tag.enabled}
276
+ disabled={togglingId === tag.id}
277
+ onCheckedChange={(next) => void toggleEnabled(tag, next)}
278
+ aria-label={`${tag.enabled ? 'Disable' : 'Enable'} ${tag.name}`}
279
+ />
280
+ </td>
281
+ <td className="text-muted-foreground px-4 py-[11px] text-xs">
282
+ {formatUpdated(tag.updatedAt)}
283
+ </td>
284
+ </tr>
285
+ )
286
+ })}
287
+ </tbody>
288
+ </table>
289
+ </div>
290
+ )}
291
+ </SplitPaneList>
292
+
293
+ <InspectorPane open={paneOpen} width={420} aria-label="Script editor">
294
+ <ScriptTagEditorPane
295
+ key={selection?.mode === 'edit' ? selection.id : 'new'}
296
+ tag={selectedTag}
297
+ onClose={() => setSelection(null)}
298
+ onChanged={handlePaneChanged}
299
+ />
300
+ </InspectorPane>
301
+ </SplitPaneLayout>
227
302
  </div>
228
303
  )
229
304
  }
@@ -0,0 +1,233 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Dashboard onboarding / getting-started block (design: Dashboard.html
5
+ * `OnboardingSection`). Shown only while the site is effectively empty —
6
+ * it REPLACES the Recent Activity / Publishing Queue / Content Health grid
7
+ * (see `shouldShowOnboarding`), sitting between the stats row and the
8
+ * Delivery API card.
9
+ *
10
+ * Steps are DERIVED from data the dashboard already fetches — the
11
+ * prototype's click-to-toggle checklist was mock behavior. No new API
12
+ * endpoints; every signal below comes from `/stats` or
13
+ * `/stats/api-delivery`:
14
+ *
15
+ * 1. Create your workspace — always complete (they're logged in).
16
+ * 2. Create your first Post/Page — post/page collection counts > 0.
17
+ * 3. Upload media assets — `totalMedia > 0`.
18
+ * 4. Configure SEO defaults — `avgSeoScore > 0`. The average SEO
19
+ * score is the only SEO signal in already-fetched data; it is 0 until
20
+ * content is scored, so the step reads incomplete on an empty site
21
+ * (honest, if conservative).
22
+ * 5. Get your API key — no API-key count exists in dashboard
23
+ * data, so we use recorded delivery-API traffic (`hasData` from
24
+ * /stats/api-delivery) as the proxy: traffic means the API is being
25
+ * consumed. Incomplete by default.
26
+ * 6. Invite team members — `totalUsers > 1`.
27
+ *
28
+ * Quick links deviate from the prototype in one place: "Webhooks guide"
29
+ * has no in-admin destination or docs URL (webhooks UI only exists inside
30
+ * individual form settings), so it is replaced with an internal SEO
31
+ * defaults link — no dead links.
32
+ */
33
+
34
+ import { Check, ChevronRight, Code, Key, Search, Users, type LucideIcon } from 'lucide-react'
35
+
36
+ export interface OnboardingSignals {
37
+ /** Combined document count across post/page-type collections. */
38
+ postsAndPagesCount: number
39
+ /** `totalMedia` from /stats. */
40
+ mediaCount: number
41
+ /** `avgSeoScore` from /stats (0 when nothing is scored yet). */
42
+ seoScore: number
43
+ /** `totalUsers` from /stats. */
44
+ userCount: number
45
+ /** `hasData` from /stats/api-delivery — any delivery traffic recorded. */
46
+ hasApiTraffic: boolean
47
+ }
48
+
49
+ export interface OnboardingStep {
50
+ id: string
51
+ label: string
52
+ done: boolean
53
+ }
54
+
55
+ export function deriveOnboardingSteps(signals: OnboardingSignals): OnboardingStep[] {
56
+ return [
57
+ { id: 'workspace', label: 'Create your workspace', done: true },
58
+ {
59
+ id: 'first-content',
60
+ label: 'Create your first Post or Page',
61
+ done: signals.postsAndPagesCount > 0,
62
+ },
63
+ { id: 'media', label: 'Upload media assets', done: signals.mediaCount > 0 },
64
+ { id: 'seo', label: 'Configure SEO defaults', done: signals.seoScore > 0 },
65
+ { id: 'api-key', label: 'Get your API key', done: signals.hasApiTraffic },
66
+ { id: 'invite', label: 'Invite team members', done: signals.userCount > 1 },
67
+ ]
68
+ }
69
+
70
+ /**
71
+ * The block appears only when content is essentially empty: zero posts +
72
+ * pages AND zero media (steps 2 and 3 both incomplete).
73
+ */
74
+ export function shouldShowOnboarding(
75
+ signals: Pick<OnboardingSignals, 'postsAndPagesCount' | 'mediaCount'>,
76
+ ): boolean {
77
+ return signals.postsAndPagesCount === 0 && signals.mediaCount === 0
78
+ }
79
+
80
+ interface QuickLink {
81
+ icon: LucideIcon
82
+ label: string
83
+ sub: string
84
+ path: string
85
+ }
86
+
87
+ const QUICK_LINKS: QuickLink[] = [
88
+ {
89
+ icon: Code,
90
+ label: 'API Reference',
91
+ sub: 'REST & GraphQL docs',
92
+ path: '/settings?tab=api-keys',
93
+ },
94
+ { icon: Search, label: 'SEO defaults', sub: 'Set up site metadata', path: '/settings?tab=seo' },
95
+ { icon: Users, label: 'Invite team', sub: 'Add editors & admins', path: '/settings?tab=users' },
96
+ { icon: Key, label: 'API Keys', sub: 'Manage access tokens', path: '/settings?tab=api-keys' },
97
+ ]
98
+
99
+ export interface OnboardingBlockProps {
100
+ signals: OnboardingSignals
101
+ onNavigate?: (path: string) => void
102
+ }
103
+
104
+ export function OnboardingBlock({ signals, onNavigate }: OnboardingBlockProps) {
105
+ const steps = deriveOnboardingSteps(signals)
106
+ const done = steps.filter((s) => s.done).length
107
+
108
+ return (
109
+ <div className="space-y-3.5">
110
+ {/* Welcome banner ─────────────────────────────────────────────── */}
111
+ <section
112
+ aria-labelledby="ob-welcome-title"
113
+ className="rounded-[var(--r)] border border-[var(--bdr)] bg-[linear-gradient(135deg,var(--acc-l)_0%,var(--card)_60%)] px-7 py-6 shadow-[var(--shd)]"
114
+ >
115
+ <h2
116
+ id="ob-welcome-title"
117
+ className="text-[17px] font-medium tracking-[-0.3px] text-[var(--txt)]"
118
+ >
119
+ Welcome to ActuateCMS <span aria-hidden>👋</span>
120
+ </h2>
121
+ <p className="mt-1 max-w-[480px] text-[length:var(--fs-body)] text-[var(--sub)]">
122
+ Your headless CMS is ready. Complete these steps to start delivering content to your
123
+ frontend. {done}/{steps.length} steps complete.
124
+ </p>
125
+ <div
126
+ role="progressbar"
127
+ aria-label="Onboarding progress"
128
+ aria-valuemin={0}
129
+ aria-valuemax={steps.length}
130
+ aria-valuenow={done}
131
+ aria-valuetext={`${done} of ${steps.length} steps complete`}
132
+ className="mt-3 h-[6px] max-w-[400px] overflow-hidden rounded-full bg-[var(--bdr)]"
133
+ >
134
+ <div
135
+ className="h-full rounded-full bg-[var(--acc)] motion-safe:transition-[width] motion-safe:duration-[var(--motion-base)]"
136
+ style={{ width: `${(done / steps.length) * 100}%` }}
137
+ />
138
+ </div>
139
+ </section>
140
+
141
+ {/* Two-card grid ──────────────────────────────────────────────── */}
142
+ <div className="grid grid-cols-1 gap-3.5 lg:grid-cols-2">
143
+ {/* Getting started checklist */}
144
+ <section
145
+ aria-labelledby="ob-checklist-title"
146
+ className="rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] p-6 shadow-[var(--shd)]"
147
+ >
148
+ <h3
149
+ id="ob-checklist-title"
150
+ className="text-[length:var(--fs-card)] font-medium text-[var(--txt)]"
151
+ >
152
+ Getting started
153
+ </h3>
154
+ <p className="mt-1 mb-4 text-[length:var(--fs-sub)] leading-[1.55] text-[var(--sub)]">
155
+ Complete each step to get the most out of ActuateCMS.
156
+ </p>
157
+ <ul role="list" className="space-y-0.5">
158
+ {steps.map((step) => (
159
+ <li key={step.id} className="flex items-center gap-[11px] rounded-lg px-2.5 py-2">
160
+ <span
161
+ aria-hidden
162
+ className={`flex h-[19px] w-[19px] shrink-0 items-center justify-center rounded-full motion-safe:transition-colors motion-safe:duration-[var(--motion-base)] ${
163
+ step.done ? 'bg-[var(--suc)]' : 'border-2 border-[var(--bdr)]'
164
+ }`}
165
+ >
166
+ {step.done && (
167
+ <Check
168
+ size={11}
169
+ strokeWidth={2.5}
170
+ className="text-[var(--success-foreground)]"
171
+ />
172
+ )}
173
+ </span>
174
+ <span
175
+ className={`text-[length:var(--fs-body)] ${
176
+ step.done ? 'text-[var(--muted)] line-through' : 'text-[var(--txt)]'
177
+ }`}
178
+ >
179
+ {step.label}
180
+ </span>
181
+ {/* Completion state must not rely on color/strikethrough alone. */}
182
+ <span className="sr-only">{step.done ? '(completed)' : '(not completed)'}</span>
183
+ </li>
184
+ ))}
185
+ </ul>
186
+ </section>
187
+
188
+ {/* Quick links */}
189
+ <section
190
+ aria-labelledby="ob-links-title"
191
+ className="rounded-[var(--r)] border border-[var(--bdr)] bg-[var(--card)] p-6 shadow-[var(--shd)]"
192
+ >
193
+ <h3
194
+ id="ob-links-title"
195
+ className="text-[length:var(--fs-card)] font-medium text-[var(--txt)]"
196
+ >
197
+ Quick links
198
+ </h3>
199
+ <p className="mt-1 mb-4 text-[length:var(--fs-sub)] leading-[1.55] text-[var(--sub)]">
200
+ Helpful resources for building with ActuateCMS.
201
+ </p>
202
+ <ul role="list" className="space-y-0.5">
203
+ {QUICK_LINKS.map((link) => (
204
+ <li key={link.label}>
205
+ <button
206
+ type="button"
207
+ onClick={() => onNavigate?.(link.path)}
208
+ className="focus-visible:ring-ring flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left transition-colors hover:bg-[var(--acc-l)] focus:outline-none focus-visible:ring-2"
209
+ >
210
+ <span
211
+ aria-hidden
212
+ className="flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-[7px] bg-[var(--acc-l)] text-[var(--acc)]"
213
+ >
214
+ <link.icon size={14} />
215
+ </span>
216
+ <span className="min-w-0 flex-1">
217
+ <span className="block truncate text-[length:var(--fs-body)] font-medium text-[var(--txt)]">
218
+ {link.label}
219
+ </span>
220
+ <span className="block truncate text-[length:var(--fs-hint)] text-[var(--muted)]">
221
+ {link.sub}
222
+ </span>
223
+ </span>
224
+ <ChevronRight size={14} className="shrink-0 text-[var(--muted)]" aria-hidden />
225
+ </button>
226
+ </li>
227
+ ))}
228
+ </ul>
229
+ </section>
230
+ </div>
231
+ </div>
232
+ )
233
+ }
@@ -48,6 +48,12 @@ import { resolveDefaultSitePreview } from '../../lib/editor-preview.js'
48
48
  interface PageSectionEditorProps {
49
49
  /** undefined when creating a new page (`/pages/new`) */
50
50
  documentId?: string
51
+ /**
52
+ * Parent page to preselect on a new page — set when arriving via
53
+ * "Add sub-page" in the Pages tree view (`/pages/new?parent=<id>`).
54
+ * Ignored when `documentId` is present.
55
+ */
56
+ initialParentId?: string | null
51
57
  config?: any
52
58
  session?: any
53
59
  onNavigate: (path: string) => void
@@ -73,6 +79,7 @@ function deriveCan(session: any): { canEdit: boolean; canPublish: boolean } {
73
79
 
74
80
  export function PageSectionEditor({
75
81
  documentId,
82
+ initialParentId,
76
83
  config,
77
84
  session,
78
85
  onNavigate,
@@ -136,7 +143,7 @@ export function PageSectionEditor({
136
143
  }
137
144
 
138
145
  if (!documentId) {
139
- setPage(emptyPage())
146
+ setPage(emptyPage(initialParentId ?? null))
140
147
  setLoading(false)
141
148
  return
142
149
  }
@@ -160,7 +167,7 @@ export function PageSectionEditor({
160
167
  return () => {
161
168
  cancelled = true
162
169
  }
163
- }, [documentId, schemaError])
170
+ }, [documentId, initialParentId, schemaError])
164
171
 
165
172
  // ─── Unsaved-changes guard (browser unload) ──────────────────────────
166
173
  useEffect(() => {