@actuate-media/cms-admin 0.4.0 → 0.7.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 (212) hide show
  1. package/dist/AdminRoot.d.ts.map +1 -1
  2. package/dist/AdminRoot.js +35 -0
  3. package/dist/AdminRoot.js.map +1 -1
  4. package/dist/actuate-admin.css +1 -1
  5. package/dist/components/Breadcrumbs.d.ts.map +1 -1
  6. package/dist/components/Breadcrumbs.js +1 -0
  7. package/dist/components/Breadcrumbs.js.map +1 -1
  8. package/dist/components/ErrorBoundary.js +1 -1
  9. package/dist/components/ErrorBoundary.js.map +1 -1
  10. package/dist/hooks/useBuilderState.d.ts +49 -0
  11. package/dist/hooks/useBuilderState.d.ts.map +1 -0
  12. package/dist/hooks/useBuilderState.js +238 -0
  13. package/dist/hooks/useBuilderState.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/layout/Sidebar.d.ts.map +1 -1
  19. package/dist/layout/Sidebar.js +2 -2
  20. package/dist/layout/Sidebar.js.map +1 -1
  21. package/dist/views/ForgotPassword.d.ts +5 -0
  22. package/dist/views/ForgotPassword.d.ts.map +1 -0
  23. package/dist/views/ForgotPassword.js +41 -0
  24. package/dist/views/ForgotPassword.js.map +1 -0
  25. package/dist/views/ResetPassword.d.ts +6 -0
  26. package/dist/views/ResetPassword.d.ts.map +1 -0
  27. package/dist/views/ResetPassword.js +46 -0
  28. package/dist/views/ResetPassword.js.map +1 -0
  29. package/dist/views/ScriptTagEditor.d.ts +6 -0
  30. package/dist/views/ScriptTagEditor.d.ts.map +1 -0
  31. package/dist/views/ScriptTagEditor.js +109 -0
  32. package/dist/views/ScriptTagEditor.js.map +1 -0
  33. package/dist/views/ScriptTags.d.ts +5 -0
  34. package/dist/views/ScriptTags.d.ts.map +1 -0
  35. package/dist/views/ScriptTags.js +54 -0
  36. package/dist/views/ScriptTags.js.map +1 -0
  37. package/dist/views/page-builder/AIBlockAssist.d.ts +9 -0
  38. package/dist/views/page-builder/AIBlockAssist.d.ts.map +1 -0
  39. package/dist/views/page-builder/AIBlockAssist.js +40 -0
  40. package/dist/views/page-builder/AIBlockAssist.js.map +1 -0
  41. package/dist/views/page-builder/AIGenerateDialog.d.ts +8 -0
  42. package/dist/views/page-builder/AIGenerateDialog.d.ts.map +1 -0
  43. package/dist/views/page-builder/AIGenerateDialog.js +170 -0
  44. package/dist/views/page-builder/AIGenerateDialog.js.map +1 -0
  45. package/dist/views/page-builder/BlockEditor.d.ts +11 -0
  46. package/dist/views/page-builder/BlockEditor.d.ts.map +1 -0
  47. package/dist/views/page-builder/BlockEditor.js +67 -0
  48. package/dist/views/page-builder/BlockEditor.js.map +1 -0
  49. package/dist/views/page-builder/BlockPicker.d.ts +7 -0
  50. package/dist/views/page-builder/BlockPicker.d.ts.map +1 -0
  51. package/dist/views/page-builder/BlockPicker.js +102 -0
  52. package/dist/views/page-builder/BlockPicker.js.map +1 -0
  53. package/dist/views/page-builder/BottomBar.d.ts +9 -0
  54. package/dist/views/page-builder/BottomBar.d.ts.map +1 -0
  55. package/dist/views/page-builder/BottomBar.js +13 -0
  56. package/dist/views/page-builder/BottomBar.js.map +1 -0
  57. package/dist/views/page-builder/BuilderToolbar.d.ts +21 -0
  58. package/dist/views/page-builder/BuilderToolbar.d.ts.map +1 -0
  59. package/dist/views/page-builder/BuilderToolbar.js +18 -0
  60. package/dist/views/page-builder/BuilderToolbar.js.map +1 -0
  61. package/dist/views/page-builder/ContextPanel.d.ts +20 -0
  62. package/dist/views/page-builder/ContextPanel.d.ts.map +1 -0
  63. package/dist/views/page-builder/ContextPanel.js +40 -0
  64. package/dist/views/page-builder/ContextPanel.js.map +1 -0
  65. package/dist/views/page-builder/DesignScore.d.ts +6 -0
  66. package/dist/views/page-builder/DesignScore.d.ts.map +1 -0
  67. package/dist/views/page-builder/DesignScore.js +93 -0
  68. package/dist/views/page-builder/DesignScore.js.map +1 -0
  69. package/dist/views/page-builder/NodeSettings.d.ts +12 -0
  70. package/dist/views/page-builder/NodeSettings.d.ts.map +1 -0
  71. package/dist/views/page-builder/NodeSettings.js +80 -0
  72. package/dist/views/page-builder/NodeSettings.js.map +1 -0
  73. package/dist/views/page-builder/PageBuilder.d.ts +8 -0
  74. package/dist/views/page-builder/PageBuilder.d.ts.map +1 -0
  75. package/dist/views/page-builder/PageBuilder.js +126 -0
  76. package/dist/views/page-builder/PageBuilder.js.map +1 -0
  77. package/dist/views/page-builder/PageSettings.d.ts +7 -0
  78. package/dist/views/page-builder/PageSettings.d.ts.map +1 -0
  79. package/dist/views/page-builder/PageSettings.js +27 -0
  80. package/dist/views/page-builder/PageSettings.js.map +1 -0
  81. package/dist/views/page-builder/SEOPanel.d.ts +10 -0
  82. package/dist/views/page-builder/SEOPanel.d.ts.map +1 -0
  83. package/dist/views/page-builder/SEOPanel.js +105 -0
  84. package/dist/views/page-builder/SEOPanel.js.map +1 -0
  85. package/dist/views/page-builder/SavedSections.d.ts +6 -0
  86. package/dist/views/page-builder/SavedSections.d.ts.map +1 -0
  87. package/dist/views/page-builder/SavedSections.js +145 -0
  88. package/dist/views/page-builder/SavedSections.js.map +1 -0
  89. package/dist/views/page-builder/TemplatePicker.d.ts +7 -0
  90. package/dist/views/page-builder/TemplatePicker.d.ts.map +1 -0
  91. package/dist/views/page-builder/TemplatePicker.js +68 -0
  92. package/dist/views/page-builder/TemplatePicker.js.map +1 -0
  93. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts +3 -0
  94. package/dist/views/page-builder/block-renderers/CTAPreview.d.ts.map +1 -0
  95. package/dist/views/page-builder/block-renderers/CTAPreview.js +19 -0
  96. package/dist/views/page-builder/block-renderers/CTAPreview.js.map +1 -0
  97. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts +3 -0
  98. package/dist/views/page-builder/block-renderers/CardsPreview.d.ts.map +1 -0
  99. package/dist/views/page-builder/block-renderers/CardsPreview.js +22 -0
  100. package/dist/views/page-builder/block-renderers/CardsPreview.js.map +1 -0
  101. package/dist/views/page-builder/block-renderers/CodePreview.d.ts +3 -0
  102. package/dist/views/page-builder/block-renderers/CodePreview.d.ts.map +1 -0
  103. package/dist/views/page-builder/block-renderers/CodePreview.js +16 -0
  104. package/dist/views/page-builder/block-renderers/CodePreview.js.map +1 -0
  105. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts +3 -0
  106. package/dist/views/page-builder/block-renderers/FAQPreview.d.ts.map +1 -0
  107. package/dist/views/page-builder/block-renderers/FAQPreview.js +24 -0
  108. package/dist/views/page-builder/block-renderers/FAQPreview.js.map +1 -0
  109. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts +6 -0
  110. package/dist/views/page-builder/block-renderers/FallbackPreview.d.ts.map +1 -0
  111. package/dist/views/page-builder/block-renderers/FallbackPreview.js +7 -0
  112. package/dist/views/page-builder/block-renderers/FallbackPreview.js.map +1 -0
  113. package/dist/views/page-builder/block-renderers/FormPreview.d.ts +3 -0
  114. package/dist/views/page-builder/block-renderers/FormPreview.d.ts.map +1 -0
  115. package/dist/views/page-builder/block-renderers/FormPreview.js +14 -0
  116. package/dist/views/page-builder/block-renderers/FormPreview.js.map +1 -0
  117. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts +3 -0
  118. package/dist/views/page-builder/block-renderers/GalleryPreview.d.ts.map +1 -0
  119. package/dist/views/page-builder/block-renderers/GalleryPreview.js +21 -0
  120. package/dist/views/page-builder/block-renderers/GalleryPreview.js.map +1 -0
  121. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts +3 -0
  122. package/dist/views/page-builder/block-renderers/HeroPreview.d.ts.map +1 -0
  123. package/dist/views/page-builder/block-renderers/HeroPreview.js +19 -0
  124. package/dist/views/page-builder/block-renderers/HeroPreview.js.map +1 -0
  125. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts +3 -0
  126. package/dist/views/page-builder/block-renderers/ImagePreview.d.ts.map +1 -0
  127. package/dist/views/page-builder/block-renderers/ImagePreview.js +17 -0
  128. package/dist/views/page-builder/block-renderers/ImagePreview.js.map +1 -0
  129. package/dist/views/page-builder/block-renderers/TextPreview.d.ts +3 -0
  130. package/dist/views/page-builder/block-renderers/TextPreview.d.ts.map +1 -0
  131. package/dist/views/page-builder/block-renderers/TextPreview.js +26 -0
  132. package/dist/views/page-builder/block-renderers/TextPreview.js.map +1 -0
  133. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts +3 -0
  134. package/dist/views/page-builder/block-renderers/VideoPreview.d.ts.map +1 -0
  135. package/dist/views/page-builder/block-renderers/VideoPreview.js +21 -0
  136. package/dist/views/page-builder/block-renderers/VideoPreview.js.map +1 -0
  137. package/dist/views/page-builder/block-renderers/index.d.ts +9 -0
  138. package/dist/views/page-builder/block-renderers/index.d.ts.map +1 -0
  139. package/dist/views/page-builder/block-renderers/index.js +25 -0
  140. package/dist/views/page-builder/block-renderers/index.js.map +1 -0
  141. package/dist/views/page-builder/canvas/BlockRenderer.d.ts +8 -0
  142. package/dist/views/page-builder/canvas/BlockRenderer.d.ts.map +1 -0
  143. package/dist/views/page-builder/canvas/BlockRenderer.js +30 -0
  144. package/dist/views/page-builder/canvas/BlockRenderer.js.map +1 -0
  145. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts +10 -0
  146. package/dist/views/page-builder/canvas/BuilderCanvas.d.ts.map +1 -0
  147. package/dist/views/page-builder/canvas/BuilderCanvas.js +26 -0
  148. package/dist/views/page-builder/canvas/BuilderCanvas.js.map +1 -0
  149. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts +8 -0
  150. package/dist/views/page-builder/canvas/ColumnRenderer.d.ts.map +1 -0
  151. package/dist/views/page-builder/canvas/ColumnRenderer.js +36 -0
  152. package/dist/views/page-builder/canvas/ColumnRenderer.js.map +1 -0
  153. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts +8 -0
  154. package/dist/views/page-builder/canvas/ContainerRenderer.d.ts.map +1 -0
  155. package/dist/views/page-builder/canvas/ContainerRenderer.js +33 -0
  156. package/dist/views/page-builder/canvas/ContainerRenderer.js.map +1 -0
  157. package/dist/views/page-builder/canvas/RowRenderer.d.ts +8 -0
  158. package/dist/views/page-builder/canvas/RowRenderer.d.ts.map +1 -0
  159. package/dist/views/page-builder/canvas/RowRenderer.js +32 -0
  160. package/dist/views/page-builder/canvas/RowRenderer.js.map +1 -0
  161. package/dist/views/page-builder/canvas/SectionRenderer.d.ts +8 -0
  162. package/dist/views/page-builder/canvas/SectionRenderer.d.ts.map +1 -0
  163. package/dist/views/page-builder/canvas/SectionRenderer.js +54 -0
  164. package/dist/views/page-builder/canvas/SectionRenderer.js.map +1 -0
  165. package/dist/views/page-builder/canvas/index.d.ts +3 -0
  166. package/dist/views/page-builder/canvas/index.d.ts.map +1 -0
  167. package/dist/views/page-builder/canvas/index.js +2 -0
  168. package/dist/views/page-builder/canvas/index.js.map +1 -0
  169. package/package.json +7 -4
  170. package/src/AdminRoot.tsx +41 -0
  171. package/src/components/Breadcrumbs.tsx +1 -0
  172. package/src/components/ErrorBoundary.tsx +3 -3
  173. package/src/hooks/useBuilderState.ts +328 -0
  174. package/src/index.ts +8 -0
  175. package/src/layout/Sidebar.tsx +7 -0
  176. package/src/views/ForgotPassword.tsx +136 -0
  177. package/src/views/ResetPassword.tsx +192 -0
  178. package/src/views/ScriptTagEditor.tsx +361 -0
  179. package/src/views/ScriptTags.tsx +174 -0
  180. package/src/views/page-builder/AIBlockAssist.tsx +68 -0
  181. package/src/views/page-builder/AIGenerateDialog.tsx +574 -0
  182. package/src/views/page-builder/BlockEditor.tsx +352 -0
  183. package/src/views/page-builder/BlockPicker.tsx +338 -0
  184. package/src/views/page-builder/BottomBar.tsx +64 -0
  185. package/src/views/page-builder/BuilderToolbar.tsx +218 -0
  186. package/src/views/page-builder/ContextPanel.tsx +145 -0
  187. package/src/views/page-builder/DesignScore.tsx +258 -0
  188. package/src/views/page-builder/NodeSettings.tsx +515 -0
  189. package/src/views/page-builder/PageBuilder.tsx +288 -0
  190. package/src/views/page-builder/PageSettings.tsx +161 -0
  191. package/src/views/page-builder/SEOPanel.tsx +485 -0
  192. package/src/views/page-builder/SavedSections.tsx +486 -0
  193. package/src/views/page-builder/TemplatePicker.tsx +201 -0
  194. package/src/views/page-builder/block-renderers/CTAPreview.tsx +81 -0
  195. package/src/views/page-builder/block-renderers/CardsPreview.tsx +71 -0
  196. package/src/views/page-builder/block-renderers/CodePreview.tsx +46 -0
  197. package/src/views/page-builder/block-renderers/FAQPreview.tsx +90 -0
  198. package/src/views/page-builder/block-renderers/FallbackPreview.tsx +18 -0
  199. package/src/views/page-builder/block-renderers/FormPreview.tsx +69 -0
  200. package/src/views/page-builder/block-renderers/GalleryPreview.tsx +93 -0
  201. package/src/views/page-builder/block-renderers/HeroPreview.tsx +103 -0
  202. package/src/views/page-builder/block-renderers/ImagePreview.tsx +54 -0
  203. package/src/views/page-builder/block-renderers/TextPreview.tsx +81 -0
  204. package/src/views/page-builder/block-renderers/VideoPreview.tsx +78 -0
  205. package/src/views/page-builder/block-renderers/index.ts +34 -0
  206. package/src/views/page-builder/canvas/BlockRenderer.tsx +62 -0
  207. package/src/views/page-builder/canvas/BuilderCanvas.tsx +90 -0
  208. package/src/views/page-builder/canvas/ColumnRenderer.tsx +86 -0
  209. package/src/views/page-builder/canvas/ContainerRenderer.tsx +71 -0
  210. package/src/views/page-builder/canvas/RowRenderer.tsx +72 -0
  211. package/src/views/page-builder/canvas/SectionRenderer.tsx +97 -0
  212. package/src/views/page-builder/canvas/index.ts +2 -0
@@ -0,0 +1,485 @@
1
+ 'use client';
2
+
3
+ import { useState, useMemo } from 'react';
4
+ import { analyzeSEO } from '@actuate-media/cms-core';
5
+ import type {
6
+ PageNode,
7
+ BuilderSEOAnalysis,
8
+ BuilderSEOCheck,
9
+ BuilderReadabilityResult,
10
+ } from '@actuate-media/cms-core';
11
+ import type { PageSettings } from '../../hooks/useBuilderState.js';
12
+ import {
13
+ CheckCircle2,
14
+ AlertCircle,
15
+ XCircle,
16
+ ChevronDown,
17
+ ChevronUp,
18
+ Search,
19
+ Globe,
20
+ Type,
21
+ FileText,
22
+ Clock,
23
+ BarChart3,
24
+ Target,
25
+ Eye,
26
+ } from 'lucide-react';
27
+
28
+ export interface BuilderSEOPanelProps {
29
+ tree: PageNode;
30
+ pageSettings: PageSettings;
31
+ onPageSettingsChange: (settings: Partial<PageSettings>) => void;
32
+ selectedNodeId: string | null;
33
+ }
34
+
35
+ function getScoreColor(score: number): string {
36
+ if (score >= 70) return 'text-green-500';
37
+ if (score >= 40) return 'text-amber-500';
38
+ return 'text-red-500';
39
+ }
40
+
41
+ function getRingStrokeColor(score: number): string {
42
+ if (score >= 70) return 'rgb(34, 197, 94)';
43
+ if (score >= 40) return 'rgb(245, 158, 11)';
44
+ return 'rgb(239, 68, 68)';
45
+ }
46
+
47
+ function getScoreLabel(score: number): string {
48
+ if (score >= 80) return 'Excellent';
49
+ if (score >= 70) return 'Good';
50
+ if (score >= 50) return 'Needs Work';
51
+ return 'Poor';
52
+ }
53
+
54
+ function StatusIcon({ status }: { status: BuilderSEOCheck['status'] }) {
55
+ switch (status) {
56
+ case 'good':
57
+ return <CheckCircle2 size={14} className="text-green-500 shrink-0" />;
58
+ case 'warning':
59
+ return <AlertCircle size={14} className="text-amber-500 shrink-0" />;
60
+ case 'error':
61
+ return <XCircle size={14} className="text-red-500 shrink-0" />;
62
+ }
63
+ }
64
+
65
+ function ScoreRing({ score }: { score: number }) {
66
+ const radius = 32;
67
+ const strokeWidth = 6;
68
+ const circumference = 2 * Math.PI * radius;
69
+ const progress = Math.min(score, 100) / 100;
70
+ const dashOffset = circumference * (1 - progress);
71
+ const size = (radius + strokeWidth) * 2;
72
+
73
+ return (
74
+ <div className="relative">
75
+ <svg
76
+ width={size}
77
+ height={size}
78
+ viewBox={`0 0 ${size} ${size}`}
79
+ className="-rotate-90"
80
+ >
81
+ <circle
82
+ cx={size / 2}
83
+ cy={size / 2}
84
+ r={radius}
85
+ fill="none"
86
+ stroke="var(--border)"
87
+ strokeWidth={strokeWidth}
88
+ />
89
+ <circle
90
+ cx={size / 2}
91
+ cy={size / 2}
92
+ r={radius}
93
+ fill="none"
94
+ stroke={getRingStrokeColor(score)}
95
+ strokeWidth={strokeWidth}
96
+ strokeDasharray={circumference}
97
+ strokeDashoffset={dashOffset}
98
+ strokeLinecap="round"
99
+ className="transition-all duration-700 ease-out"
100
+ />
101
+ </svg>
102
+ <div className="absolute inset-0 flex items-center justify-center">
103
+ <span className={`text-lg font-medium ${getScoreColor(score)}`}>
104
+ {score}
105
+ </span>
106
+ </div>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ function ScoreSummary({ checks }: { checks: BuilderSEOCheck[] }) {
112
+ const passed = checks.filter((c) => c.status === 'good').length;
113
+ const warnings = checks.filter((c) => c.status === 'warning').length;
114
+ const errors = checks.filter((c) => c.status === 'error').length;
115
+
116
+ return (
117
+ <div className="flex flex-col gap-1">
118
+ <div className="flex items-center gap-1.5">
119
+ <CheckCircle2 size={12} className="text-green-500" />
120
+ <span className="text-xs text-muted-foreground">{passed} passed</span>
121
+ </div>
122
+ <div className="flex items-center gap-1.5">
123
+ <AlertCircle size={12} className="text-amber-500" />
124
+ <span className="text-xs text-muted-foreground">{warnings} warnings</span>
125
+ </div>
126
+ <div className="flex items-center gap-1.5">
127
+ <XCircle size={12} className="text-red-500" />
128
+ <span className="text-xs text-muted-foreground">{errors} issues</span>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ function CollapsibleSection({
135
+ title,
136
+ icon: Icon,
137
+ expanded,
138
+ onToggle,
139
+ children,
140
+ }: {
141
+ title: string;
142
+ icon: typeof Search;
143
+ expanded: boolean;
144
+ onToggle: () => void;
145
+ children: React.ReactNode;
146
+ }) {
147
+ return (
148
+ <div className="border-b border-border last:border-b-0">
149
+ <button
150
+ type="button"
151
+ className="w-full text-left px-4 py-3 flex items-center justify-between hover:bg-muted/50 transition-colors"
152
+ onClick={onToggle}
153
+ aria-expanded={expanded}
154
+ >
155
+ <div className="flex items-center gap-2">
156
+ <Icon size={14} className="text-muted-foreground" />
157
+ <span className="text-xs font-medium text-foreground">{title}</span>
158
+ </div>
159
+ {expanded ? (
160
+ <ChevronUp size={14} className="text-muted-foreground" />
161
+ ) : (
162
+ <ChevronDown size={14} className="text-muted-foreground" />
163
+ )}
164
+ </button>
165
+ {expanded && <div className="px-4 pb-4">{children}</div>}
166
+ </div>
167
+ );
168
+ }
169
+
170
+ function SEOChecksList({ checks }: { checks: BuilderSEOCheck[] }) {
171
+ return (
172
+ <div className="flex flex-col gap-2">
173
+ {checks.map((check) => (
174
+ <div key={check.id} className="flex items-start gap-2">
175
+ <StatusIcon status={check.status} />
176
+ <div className="flex flex-col">
177
+ <span className="text-xs font-medium text-foreground">{check.label}</span>
178
+ <span className="text-xs text-muted-foreground">{check.detail}</span>
179
+ </div>
180
+ </div>
181
+ ))}
182
+ </div>
183
+ );
184
+ }
185
+
186
+ function ReadabilityGrid({ readability }: { readability: BuilderReadabilityResult }) {
187
+ const fleschLabel = readability.fleschScore >= 60
188
+ ? 'Easy'
189
+ : readability.fleschScore >= 30
190
+ ? 'Moderate'
191
+ : 'Difficult';
192
+
193
+ return (
194
+ <div className="grid grid-cols-2 gap-3">
195
+ <div className="bg-muted/50 rounded-md p-3 flex flex-col gap-1">
196
+ <div className="flex items-center gap-1.5">
197
+ <BarChart3 size={12} className="text-muted-foreground" />
198
+ <span className="text-xs text-muted-foreground">Flesch Score</span>
199
+ </div>
200
+ <span className={`text-sm font-medium ${getScoreColor(readability.fleschScore)}`}>
201
+ {readability.fleschScore}
202
+ </span>
203
+ <span className="text-xs text-muted-foreground">{fleschLabel}</span>
204
+ </div>
205
+
206
+ <div className="bg-muted/50 rounded-md p-3 flex flex-col gap-1">
207
+ <div className="flex items-center gap-1.5">
208
+ <Type size={12} className="text-muted-foreground" />
209
+ <span className="text-xs text-muted-foreground">Word Count</span>
210
+ </div>
211
+ <span className="text-sm font-medium text-foreground">
212
+ {readability.wordCount}
213
+ </span>
214
+ <span className="text-xs text-muted-foreground">
215
+ {readability.wordCount >= 300 ? 'Good length' : 'Short'}
216
+ </span>
217
+ </div>
218
+
219
+ <div className="bg-muted/50 rounded-md p-3 flex flex-col gap-1">
220
+ <div className="flex items-center gap-1.5">
221
+ <FileText size={12} className="text-muted-foreground" />
222
+ <span className="text-xs text-muted-foreground">Avg Sentence</span>
223
+ </div>
224
+ <span className="text-sm font-medium text-foreground">
225
+ {readability.avgSentenceLength} words
226
+ </span>
227
+ <span className="text-xs text-muted-foreground">
228
+ {readability.avgSentenceLength <= 20 ? 'Good' : 'Long'}
229
+ </span>
230
+ </div>
231
+
232
+ <div className="bg-muted/50 rounded-md p-3 flex flex-col gap-1">
233
+ <div className="flex items-center gap-1.5">
234
+ <Clock size={12} className="text-muted-foreground" />
235
+ <span className="text-xs text-muted-foreground">Reading Time</span>
236
+ </div>
237
+ <span className="text-sm font-medium text-foreground">
238
+ {readability.readingTime} min
239
+ </span>
240
+ <span className="text-xs text-muted-foreground">
241
+ ~{readability.wordCount} words
242
+ </span>
243
+ </div>
244
+ </div>
245
+ );
246
+ }
247
+
248
+ function PerBlockHints({ hints }: { hints: BuilderSEOCheck[] }) {
249
+ return (
250
+ <div className="flex flex-col gap-2">
251
+ {hints.map((hint, index) => (
252
+ <div key={`${hint.id}-${index}`} className="flex items-start gap-2">
253
+ <StatusIcon status={hint.status} />
254
+ <div className="flex flex-col">
255
+ <span className="text-xs font-medium text-foreground">{hint.label}</span>
256
+ <span className="text-xs text-muted-foreground">{hint.detail}</span>
257
+ </div>
258
+ </div>
259
+ ))}
260
+ </div>
261
+ );
262
+ }
263
+
264
+ function BasicSEOFields({
265
+ pageSettings,
266
+ onPageSettingsChange,
267
+ }: {
268
+ pageSettings: PageSettings;
269
+ onPageSettingsChange: (settings: Partial<PageSettings>) => void;
270
+ }) {
271
+ const metaTitleLength = (pageSettings.metaTitle || '').length;
272
+ const metaDescLength = (pageSettings.metaDescription || '').length;
273
+
274
+ return (
275
+ <div className="flex flex-col gap-4">
276
+ <div className="flex flex-col gap-1.5">
277
+ <div className="flex items-center justify-between">
278
+ <label className="text-xs font-medium text-foreground">Meta Title</label>
279
+ <span className={`text-xs ${metaTitleLength > 60 ? 'text-red-500' : metaTitleLength > 0 ? 'text-muted-foreground' : 'text-muted-foreground'}`}>
280
+ {metaTitleLength}/60
281
+ </span>
282
+ </div>
283
+ <input
284
+ type="text"
285
+ value={pageSettings.metaTitle || ''}
286
+ onChange={(e) => onPageSettingsChange({ metaTitle: e.target.value })}
287
+ placeholder="Page title for search engines"
288
+ className="w-full px-3 py-2 text-sm bg-input-background border border-border rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
289
+ />
290
+ </div>
291
+
292
+ <div className="flex flex-col gap-1.5">
293
+ <div className="flex items-center justify-between">
294
+ <label className="text-xs font-medium text-foreground">Meta Description</label>
295
+ <span className={`text-xs ${metaDescLength > 160 ? 'text-red-500' : metaDescLength > 0 ? 'text-muted-foreground' : 'text-muted-foreground'}`}>
296
+ {metaDescLength}/160
297
+ </span>
298
+ </div>
299
+ <textarea
300
+ value={pageSettings.metaDescription || ''}
301
+ onChange={(e) => onPageSettingsChange({ metaDescription: e.target.value })}
302
+ placeholder="Brief description of the page content"
303
+ rows={3}
304
+ className="w-full px-3 py-2 text-sm bg-input-background border border-border rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary resize-none"
305
+ />
306
+ </div>
307
+
308
+ <div className="flex flex-col gap-1.5">
309
+ <label className="text-xs font-medium text-foreground">Focus Keyphrase</label>
310
+ <input
311
+ type="text"
312
+ value={pageSettings.focusKeyphrase || ''}
313
+ onChange={(e) => onPageSettingsChange({ focusKeyphrase: e.target.value })}
314
+ placeholder="Primary keyword or phrase"
315
+ className="w-full px-3 py-2 text-sm bg-input-background border border-border rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
316
+ />
317
+ </div>
318
+ </div>
319
+ );
320
+ }
321
+
322
+ function SearchPreview({ pageSettings }: { pageSettings: PageSettings }) {
323
+ const title = pageSettings.metaTitle || pageSettings.title || 'Untitled Page';
324
+ const description = pageSettings.metaDescription || 'No description set';
325
+ const slug = pageSettings.slug || 'page';
326
+
327
+ return (
328
+ <div className="bg-background rounded-md border border-border p-4">
329
+ <p className="text-xs text-muted-foreground mb-2">Google Search Preview</p>
330
+ <div className="flex flex-col gap-1">
331
+ <p className="text-sm text-primary truncate">
332
+ {title}
333
+ </p>
334
+ <p className="text-xs text-green-600 truncate">
335
+ example.com/{slug}
336
+ </p>
337
+ <p className="text-xs text-muted-foreground line-clamp-2">
338
+ {description}
339
+ </p>
340
+ </div>
341
+ </div>
342
+ );
343
+ }
344
+
345
+ function SocialPreview({ pageSettings }: { pageSettings: PageSettings }) {
346
+ const title = pageSettings.metaTitle || pageSettings.title || 'Untitled Page';
347
+ const description = pageSettings.metaDescription || 'No description set';
348
+
349
+ return (
350
+ <div className="bg-background rounded-md border border-border overflow-hidden">
351
+ <p className="text-xs text-muted-foreground px-4 pt-3 pb-2">Open Graph Preview</p>
352
+ {pageSettings.ogImage && (
353
+ <div className="w-full h-32 bg-muted flex items-center justify-center overflow-hidden">
354
+ <img
355
+ src={pageSettings.ogImage}
356
+ alt="OG preview"
357
+ className="w-full h-full object-cover"
358
+ />
359
+ </div>
360
+ )}
361
+ {!pageSettings.ogImage && (
362
+ <div className="w-full h-32 bg-muted flex items-center justify-center">
363
+ <Globe size={24} className="text-muted-foreground" />
364
+ </div>
365
+ )}
366
+ <div className="p-3 flex flex-col gap-1">
367
+ <p className="text-xs text-muted-foreground uppercase">example.com</p>
368
+ <p className="text-sm font-medium text-foreground truncate">{title}</p>
369
+ <p className="text-xs text-muted-foreground line-clamp-2">{description}</p>
370
+ </div>
371
+ </div>
372
+ );
373
+ }
374
+
375
+ export function BuilderSEOPanel({
376
+ tree,
377
+ pageSettings,
378
+ onPageSettingsChange,
379
+ selectedNodeId,
380
+ }: BuilderSEOPanelProps) {
381
+ const analysis = useMemo<BuilderSEOAnalysis>(
382
+ () => analyzeSEO(tree, pageSettings as any),
383
+ [tree, pageSettings],
384
+ );
385
+
386
+ const [expandedSections, setExpandedSections] = useState<string[]>(['checks']);
387
+
388
+ const toggleSection = (section: string) => {
389
+ setExpandedSections((prev) =>
390
+ prev.includes(section)
391
+ ? prev.filter((s) => s !== section)
392
+ : [...prev, section]
393
+ );
394
+ };
395
+
396
+ const blockHints = useMemo(() => {
397
+ if (!selectedNodeId) return [];
398
+ return analysis.perBlockHints.get(selectedNodeId) || [];
399
+ }, [analysis.perBlockHints, selectedNodeId]);
400
+
401
+ if (!tree.children || tree.children.length === 0) {
402
+ return (
403
+ <div className="p-6 flex flex-col items-center justify-center text-center min-h-[200px]">
404
+ <Search size={32} className="text-muted-foreground mb-3" />
405
+ <p className="text-sm font-medium text-foreground mb-1">SEO Analysis</p>
406
+ <p className="text-xs text-muted-foreground">
407
+ Add content to your page to see SEO analysis
408
+ </p>
409
+ </div>
410
+ );
411
+ }
412
+
413
+ return (
414
+ <div className="flex flex-col">
415
+ <div className="flex items-center gap-4 px-4 py-5 border-b border-border">
416
+ <ScoreRing score={analysis.score} />
417
+ <div className="flex flex-col gap-1">
418
+ <span className="text-xs font-medium text-foreground">{getScoreLabel(analysis.score)}</span>
419
+ <ScoreSummary checks={analysis.checks} />
420
+ </div>
421
+ </div>
422
+
423
+ <div className="flex flex-col">
424
+ <CollapsibleSection
425
+ title="SEO Checks"
426
+ icon={Search}
427
+ expanded={expandedSections.includes('checks')}
428
+ onToggle={() => toggleSection('checks')}
429
+ >
430
+ <SEOChecksList checks={analysis.checks} />
431
+ </CollapsibleSection>
432
+
433
+ <CollapsibleSection
434
+ title="Readability"
435
+ icon={BarChart3}
436
+ expanded={expandedSections.includes('readability')}
437
+ onToggle={() => toggleSection('readability')}
438
+ >
439
+ <ReadabilityGrid readability={analysis.readability} />
440
+ </CollapsibleSection>
441
+
442
+ {blockHints.length > 0 && (
443
+ <CollapsibleSection
444
+ title="Block Hints"
445
+ icon={Target}
446
+ expanded={expandedSections.includes('blockHints')}
447
+ onToggle={() => toggleSection('blockHints')}
448
+ >
449
+ <PerBlockHints hints={blockHints} />
450
+ </CollapsibleSection>
451
+ )}
452
+
453
+ <CollapsibleSection
454
+ title="Basic SEO"
455
+ icon={FileText}
456
+ expanded={expandedSections.includes('basicSeo')}
457
+ onToggle={() => toggleSection('basicSeo')}
458
+ >
459
+ <BasicSEOFields
460
+ pageSettings={pageSettings}
461
+ onPageSettingsChange={onPageSettingsChange}
462
+ />
463
+ </CollapsibleSection>
464
+
465
+ <CollapsibleSection
466
+ title="Search Preview"
467
+ icon={Eye}
468
+ expanded={expandedSections.includes('searchPreview')}
469
+ onToggle={() => toggleSection('searchPreview')}
470
+ >
471
+ <SearchPreview pageSettings={pageSettings} />
472
+ </CollapsibleSection>
473
+
474
+ <CollapsibleSection
475
+ title="Social Preview"
476
+ icon={Globe}
477
+ expanded={expandedSections.includes('socialPreview')}
478
+ onToggle={() => toggleSection('socialPreview')}
479
+ >
480
+ <SocialPreview pageSettings={pageSettings} />
481
+ </CollapsibleSection>
482
+ </div>
483
+ </div>
484
+ );
485
+ }