@morphika/webframe 0.1.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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/admin/assets.ts +4 -0
  4. package/admin/database.ts +4 -0
  5. package/admin/index.ts +6 -0
  6. package/admin/login.ts +4 -0
  7. package/admin/navigation.ts +4 -0
  8. package/admin/pages-editor.ts +4 -0
  9. package/admin/pages.ts +4 -0
  10. package/admin/projects-editor.ts +4 -0
  11. package/admin/projects.ts +4 -0
  12. package/admin/settings.ts +4 -0
  13. package/admin/setup.ts +4 -0
  14. package/admin/storage.ts +4 -0
  15. package/admin/styles.ts +4 -0
  16. package/app/(site)/[slug]/loading.tsx +20 -0
  17. package/app/(site)/[slug]/page.tsx +83 -0
  18. package/app/(site)/error.tsx +32 -0
  19. package/app/(site)/layout.tsx +53 -0
  20. package/app/(site)/loading.tsx +20 -0
  21. package/app/(site)/not-found.tsx +41 -0
  22. package/app/(site)/page.tsx +43 -0
  23. package/app/(site)/preview/page.tsx +99 -0
  24. package/app/(site)/work/[slug]/loading.tsx +23 -0
  25. package/app/(site)/work/[slug]/page.tsx +84 -0
  26. package/app/admin/assets/page.tsx +573 -0
  27. package/app/admin/database/page.tsx +302 -0
  28. package/app/admin/error.tsx +53 -0
  29. package/app/admin/layout.tsx +273 -0
  30. package/app/admin/login/page.tsx +88 -0
  31. package/app/admin/navigation/page.tsx +157 -0
  32. package/app/admin/page.tsx +17 -0
  33. package/app/admin/pages/[slug]/page.tsx +849 -0
  34. package/app/admin/pages/page.tsx +588 -0
  35. package/app/admin/projects/[slug]/page.tsx +3 -0
  36. package/app/admin/projects/page.tsx +669 -0
  37. package/app/admin/settings/page.tsx +132 -0
  38. package/app/admin/setup/page.tsx +64 -0
  39. package/app/admin/storage/page.tsx +518 -0
  40. package/app/admin/styles/page.tsx +243 -0
  41. package/app/api/admin/assets/file/route.ts +81 -0
  42. package/app/api/admin/assets/health/route.ts +170 -0
  43. package/app/api/admin/assets/register/route.ts +163 -0
  44. package/app/api/admin/assets/registry/route.ts +98 -0
  45. package/app/api/admin/assets/relink/confirm/route.ts +242 -0
  46. package/app/api/admin/assets/relink/route.ts +202 -0
  47. package/app/api/admin/assets/scan/route.ts +271 -0
  48. package/app/api/admin/auth/route.ts +160 -0
  49. package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
  50. package/app/api/admin/custom-sections/route.ts +127 -0
  51. package/app/api/admin/database/route.ts +53 -0
  52. package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
  53. package/app/api/admin/pages/[slug]/route.ts +617 -0
  54. package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
  55. package/app/api/admin/pages/route.ts +129 -0
  56. package/app/api/admin/preview/route.ts +53 -0
  57. package/app/api/admin/r2/connect/route.ts +181 -0
  58. package/app/api/admin/r2/delete/route.ts +198 -0
  59. package/app/api/admin/r2/disconnect/route.ts +42 -0
  60. package/app/api/admin/r2/rename/route.ts +265 -0
  61. package/app/api/admin/r2/status/route.ts +106 -0
  62. package/app/api/admin/r2/upload-url/route.ts +148 -0
  63. package/app/api/admin/revalidate/route.ts +55 -0
  64. package/app/api/admin/settings/route.ts +279 -0
  65. package/app/api/admin/setup/complete/route.ts +51 -0
  66. package/app/api/admin/setup/route.ts +118 -0
  67. package/app/api/admin/storage/switch/route.ts +117 -0
  68. package/app/api/admin/styles/fonts/route.ts +97 -0
  69. package/app/api/admin/styles/route.ts +304 -0
  70. package/app/api/assets/[...path]/route.ts +98 -0
  71. package/app/api/custom-sections/[id]/route.ts +43 -0
  72. package/app/api/draft-mode/disable/route.ts +10 -0
  73. package/app/api/draft-mode/enable/route.ts +26 -0
  74. package/app/api/projects/route.ts +42 -0
  75. package/app/api/styles/route.ts +88 -0
  76. package/app/favicon.ico +0 -0
  77. package/app/globals.css +7 -0
  78. package/app/layout.tsx +53 -0
  79. package/app/robots.ts +17 -0
  80. package/app/sitemap.ts +48 -0
  81. package/app/studio/[[...index]]/page.tsx +8 -0
  82. package/components/admin/MetadataEditor.tsx +173 -0
  83. package/components/admin/PublishToggle.tsx +130 -0
  84. package/components/admin/icons.tsx +40 -0
  85. package/components/admin/nav-builder/NavBuilder.tsx +182 -0
  86. package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
  87. package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
  88. package/components/admin/nav-builder/NavGridCell.tsx +48 -0
  89. package/components/admin/nav-builder/NavGridItem.tsx +189 -0
  90. package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
  91. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
  92. package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
  93. package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
  94. package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
  95. package/components/admin/nav-builder/index.ts +10 -0
  96. package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
  97. package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
  98. package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
  99. package/components/admin/setup-wizard/DoneStep.tsx +187 -0
  100. package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
  101. package/components/admin/setup-wizard/StorageStep.tsx +308 -0
  102. package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
  103. package/components/admin/setup-wizard/index.ts +9 -0
  104. package/components/admin/styles/ColorsEditor.tsx +214 -0
  105. package/components/admin/styles/FontsEditor.tsx +258 -0
  106. package/components/admin/styles/GridLayoutEditor.tsx +292 -0
  107. package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
  108. package/components/admin/styles/TypographyEditor.tsx +266 -0
  109. package/components/admin/styles/index.ts +9 -0
  110. package/components/admin/styles/shared.tsx +68 -0
  111. package/components/blocks/BlockRenderer.tsx +404 -0
  112. package/components/blocks/ButtonBlockRenderer.tsx +52 -0
  113. package/components/blocks/CoverBlockRenderer.tsx +239 -0
  114. package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
  115. package/components/blocks/EnterAnimationWrapper.tsx +140 -0
  116. package/components/blocks/HoverAnimationWrapper.tsx +308 -0
  117. package/components/blocks/ImageBlockRenderer.tsx +61 -0
  118. package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
  119. package/components/blocks/PageBackground.tsx +28 -0
  120. package/components/blocks/PageNavAnimation.tsx +35 -0
  121. package/components/blocks/PageNavColor.tsx +24 -0
  122. package/components/blocks/PageRenderer.tsx +142 -0
  123. package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
  124. package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
  125. package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
  126. package/components/blocks/SectionRenderer.tsx +170 -0
  127. package/components/blocks/SectionV2Renderer.tsx +330 -0
  128. package/components/blocks/ShaderCanvas.tsx +392 -0
  129. package/components/blocks/SpacerBlockRenderer.tsx +17 -0
  130. package/components/blocks/TextBlockRenderer.tsx +87 -0
  131. package/components/blocks/TypewriterRichText.tsx +464 -0
  132. package/components/blocks/TypewriterWrapper.tsx +149 -0
  133. package/components/blocks/VideoBlockRenderer.tsx +304 -0
  134. package/components/blocks/index.ts +2 -0
  135. package/components/builder/AssetBrowser.tsx +2 -0
  136. package/components/builder/BlockLivePreview.tsx +101 -0
  137. package/components/builder/BlockTypePicker.tsx +178 -0
  138. package/components/builder/BuilderCanvas.tsx +354 -0
  139. package/components/builder/CanvasMinimap.tsx +200 -0
  140. package/components/builder/CanvasToolbar.tsx +202 -0
  141. package/components/builder/ColorPicker.tsx +243 -0
  142. package/components/builder/ColorSwatchPicker.tsx +274 -0
  143. package/components/builder/ColumnDragContext.tsx +51 -0
  144. package/components/builder/ColumnDragOverlay.tsx +110 -0
  145. package/components/builder/CustomSectionInstanceCard.tsx +97 -0
  146. package/components/builder/DeviceFrame.tsx +123 -0
  147. package/components/builder/DndWrapper.tsx +337 -0
  148. package/components/builder/InsertionLines.tsx +186 -0
  149. package/components/builder/ParallaxGroupCanvas.tsx +228 -0
  150. package/components/builder/ParallaxSlideHeader.tsx +113 -0
  151. package/components/builder/ReadOnlyFrame.tsx +417 -0
  152. package/components/builder/SectionEditorBar.tsx +288 -0
  153. package/components/builder/SectionTypePicker.tsx +422 -0
  154. package/components/builder/SectionV2Canvas.tsx +297 -0
  155. package/components/builder/SectionV2Column.tsx +488 -0
  156. package/components/builder/SettingsPanel.tsx +911 -0
  157. package/components/builder/SortableBlock.tsx +230 -0
  158. package/components/builder/SortableRow.tsx +362 -0
  159. package/components/builder/VirtualAssetGrid.tsx +397 -0
  160. package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
  161. package/components/builder/asset-browser/FileLightbox.tsx +116 -0
  162. package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
  163. package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
  164. package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
  165. package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
  166. package/components/builder/asset-browser/helpers.ts +88 -0
  167. package/components/builder/asset-browser/index.ts +1 -0
  168. package/components/builder/asset-browser/types.ts +49 -0
  169. package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
  170. package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
  171. package/components/builder/asset-browser/useR2Operations.ts +189 -0
  172. package/components/builder/blockStyles.tsx +295 -0
  173. package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
  174. package/components/builder/editors/CoverBlockEditor.tsx +488 -0
  175. package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
  176. package/components/builder/editors/HoverEffectPicker.tsx +209 -0
  177. package/components/builder/editors/ImageBlockEditor.tsx +206 -0
  178. package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
  179. package/components/builder/editors/ProjectGridEditor.tsx +648 -0
  180. package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
  181. package/components/builder/editors/StaggerSettings.tsx +108 -0
  182. package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
  183. package/components/builder/editors/TextBlockEditor.tsx +462 -0
  184. package/components/builder/editors/TextStylePicker.tsx +183 -0
  185. package/components/builder/editors/VideoBlockEditor.tsx +278 -0
  186. package/components/builder/editors/index.ts +10 -0
  187. package/components/builder/editors/shared.tsx +345 -0
  188. package/components/builder/hooks/useColumnDrag.ts +472 -0
  189. package/components/builder/hooks/useColumnResize.ts +221 -0
  190. package/components/builder/index.ts +12 -0
  191. package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
  192. package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
  193. package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
  194. package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
  195. package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
  196. package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
  197. package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
  198. package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
  199. package/components/builder/live-preview/index.ts +10 -0
  200. package/components/builder/live-preview/shared.tsx +153 -0
  201. package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
  202. package/components/builder/settings-panel/BlockSettings.tsx +94 -0
  203. package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
  204. package/components/builder/settings-panel/LayoutTab.tsx +310 -0
  205. package/components/builder/settings-panel/PageSettings.tsx +200 -0
  206. package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
  207. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
  208. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
  209. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
  210. package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
  211. package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
  212. package/components/builder/settings-panel/index.ts +19 -0
  213. package/components/builder/settings-panel/responsive-helpers.ts +524 -0
  214. package/components/ui/CustomCursor.tsx +118 -0
  215. package/components/ui/NavContentLightbox.tsx +152 -0
  216. package/components/ui/Navbar.tsx +582 -0
  217. package/components/ui/PortfolioTracker.tsx +87 -0
  218. package/components/ui/ScrollToTop.tsx +47 -0
  219. package/lib/animation/enter-presets.ts +147 -0
  220. package/lib/animation/enter-resolve.ts +90 -0
  221. package/lib/animation/enter-types.ts +128 -0
  222. package/lib/animation/hover-effect-presets.ts +210 -0
  223. package/lib/animation/hover-effect-types.ts +126 -0
  224. package/lib/asset-retry.ts +111 -0
  225. package/lib/assets.ts +92 -0
  226. package/lib/audit.ts +35 -0
  227. package/lib/auth-token.ts +94 -0
  228. package/lib/auth.ts +13 -0
  229. package/lib/builder/cascade-helpers.ts +51 -0
  230. package/lib/builder/cascade.ts +533 -0
  231. package/lib/builder/constants.ts +103 -0
  232. package/lib/builder/defaults.ts +182 -0
  233. package/lib/builder/history.ts +48 -0
  234. package/lib/builder/index.ts +21 -0
  235. package/lib/builder/layout-styles.ts +344 -0
  236. package/lib/builder/masonry.ts +166 -0
  237. package/lib/builder/responsive.ts +156 -0
  238. package/lib/builder/serializer.ts +845 -0
  239. package/lib/builder/store-blocks.ts +193 -0
  240. package/lib/builder/store-canvas.ts +319 -0
  241. package/lib/builder/store-helpers.ts +490 -0
  242. package/lib/builder/store-sections.ts +709 -0
  243. package/lib/builder/store.ts +333 -0
  244. package/lib/builder/templates.ts +297 -0
  245. package/lib/builder/types.ts +374 -0
  246. package/lib/builder/utils.ts +37 -0
  247. package/lib/color-utils.ts +116 -0
  248. package/lib/config/index.ts +57 -0
  249. package/lib/config/types.ts +122 -0
  250. package/lib/contexts/AssetContext.tsx +79 -0
  251. package/lib/contexts/NavAnimationContext.tsx +44 -0
  252. package/lib/contexts/NavColorContext.tsx +38 -0
  253. package/lib/contexts/PageExitContext.tsx +194 -0
  254. package/lib/contexts/ThumbStatusContext.tsx +83 -0
  255. package/lib/csrf-client.ts +34 -0
  256. package/lib/csrf.ts +68 -0
  257. package/lib/format-utils.ts +24 -0
  258. package/lib/hooks/useViewport.ts +42 -0
  259. package/lib/logger.ts +81 -0
  260. package/lib/revalidate.ts +23 -0
  261. package/lib/sanitize.ts +91 -0
  262. package/lib/sanity/client.ts +8 -0
  263. package/lib/sanity/queries.ts +486 -0
  264. package/lib/sanity/types.ts +869 -0
  265. package/lib/sanity/writeClient.ts +24 -0
  266. package/lib/security.ts +402 -0
  267. package/lib/setup/detect.ts +156 -0
  268. package/lib/shader/glsl/index.ts +27 -0
  269. package/lib/shader/glsl/pixelate.ts +51 -0
  270. package/lib/shader/glsl/rgb-shift.ts +45 -0
  271. package/lib/shader/glsl/ripple.ts +46 -0
  272. package/lib/shader/glsl/vertex.ts +14 -0
  273. package/lib/storage/index.ts +211 -0
  274. package/lib/storage/r2-adapter.ts +286 -0
  275. package/lib/storage/types.ts +125 -0
  276. package/lib/styles/provider.tsx +267 -0
  277. package/lib/thumbnails/generate.ts +151 -0
  278. package/lib/utils.ts +6 -0
  279. package/package.json +212 -0
  280. package/sanity/compose.ts +65 -0
  281. package/sanity/sanity.config.ts +126 -0
  282. package/sanity/schemas/assetRegistry.ts +301 -0
  283. package/sanity/schemas/blocks/blockLayout.ts +90 -0
  284. package/sanity/schemas/blocks/buttonBlock.ts +82 -0
  285. package/sanity/schemas/blocks/coverBlock.ts +229 -0
  286. package/sanity/schemas/blocks/imageBlock.ts +58 -0
  287. package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
  288. package/sanity/schemas/blocks/index.ts +9 -0
  289. package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
  290. package/sanity/schemas/blocks/spacerBlock.ts +41 -0
  291. package/sanity/schemas/blocks/textBlock.ts +139 -0
  292. package/sanity/schemas/blocks/videoBlock.ts +80 -0
  293. package/sanity/schemas/customSection.ts +69 -0
  294. package/sanity/schemas/customSectionInstance.ts +163 -0
  295. package/sanity/schemas/index.ts +111 -0
  296. package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
  297. package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
  298. package/sanity/schemas/objects/parallaxGroup.ts +66 -0
  299. package/sanity/schemas/objects/parallaxSlide.ts +217 -0
  300. package/sanity/schemas/objects/typewriterConfig.ts +38 -0
  301. package/sanity/schemas/page.ts +162 -0
  302. package/sanity/schemas/pageSection.ts +157 -0
  303. package/sanity/schemas/pageSectionV2.ts +269 -0
  304. package/sanity/schemas/siteSettings.ts +256 -0
  305. package/sanity/schemas/siteStyles.ts +210 -0
  306. package/site/error.ts +4 -0
  307. package/site/index.ts +8 -0
  308. package/site/not-found.ts +4 -0
  309. package/site/page.ts +4 -0
  310. package/site/preview.ts +4 -0
  311. package/site/robots.ts +4 -0
  312. package/site/sitemap.ts +4 -0
  313. package/site/work.ts +4 -0
  314. package/studio/index.ts +4 -0
  315. package/styles/admin.css +85 -0
  316. package/styles/animations.css +237 -0
  317. package/styles/base.css +148 -0
  318. package/styles/globals.css +10 -0
  319. package/tsconfig.json +25 -0
@@ -0,0 +1,308 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useEffect } from "react";
4
+ import { csrfHeaders } from "../../../lib/csrf-client";
5
+ import type { WizardStepProps } from "./SetupWizard";
6
+
7
+ // ── Icons ──
8
+
9
+ function CheckCircle() {
10
+ return (
11
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
12
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
13
+ <polyline points="22 4 12 14.01 9 11.01" />
14
+ </svg>
15
+ );
16
+ }
17
+
18
+ function Spinner() {
19
+ return (
20
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin">
21
+ <path d="M21 12a9 9 0 1 1-6.219-8.56" />
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ function R2Icon() {
27
+ return (
28
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#F6821F" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
29
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
30
+ <polyline points="3.27 6.96 12 12.01 20.73 6.96" />
31
+ <line x1="12" y1="22.08" x2="12" y2="12" />
32
+ </svg>
33
+ );
34
+ }
35
+
36
+ // ── Types ──
37
+
38
+ interface R2Status {
39
+ connected: boolean;
40
+ bucket_name: string | null;
41
+ public_url: string | null;
42
+ }
43
+
44
+ type StepState = "loading" | "disconnected" | "connected";
45
+
46
+ // ── Component ──
47
+
48
+ /**
49
+ * Step 3 — Storage (Cloudflare R2 Connection)
50
+ *
51
+ * Reuses the same R2 connect API as /admin/storage.
52
+ * Shows a simplified 5-field form or "Connected" status.
53
+ */
54
+ export function StorageStep({ onNext, onBack }: WizardStepProps) {
55
+ const [state, setState] = useState<StepState>("loading");
56
+ const [r2Status, setR2Status] = useState<R2Status | null>(null);
57
+
58
+ // Form fields
59
+ const [endpoint, setEndpoint] = useState("");
60
+ const [bucketName, setBucketName] = useState("");
61
+ const [accessKeyId, setAccessKeyId] = useState("");
62
+ const [secretAccessKey, setSecretAccessKey] = useState("");
63
+ const [publicUrl, setPublicUrl] = useState("");
64
+
65
+ const [testing, setTesting] = useState(false);
66
+ const [error, setError] = useState<string | null>(null);
67
+
68
+ // Check R2 status on mount
69
+ useEffect(() => {
70
+ fetch("/api/admin/r2/status")
71
+ .then((res) => (res.ok ? res.json() : null))
72
+ .then((data: R2Status | null) => {
73
+ if (data?.connected) {
74
+ setR2Status(data);
75
+ setState("connected");
76
+ } else {
77
+ setState("disconnected");
78
+ }
79
+ })
80
+ .catch(() => setState("disconnected"));
81
+ }, []);
82
+
83
+ const handleConnect = useCallback(
84
+ async (e: React.FormEvent) => {
85
+ e.preventDefault();
86
+ setTesting(true);
87
+ setError(null);
88
+
89
+ try {
90
+ const res = await fetch("/api/admin/r2/connect", {
91
+ method: "POST",
92
+ headers: {
93
+ "Content-Type": "application/json",
94
+ ...csrfHeaders(),
95
+ },
96
+ body: JSON.stringify({
97
+ endpoint: endpoint.trim(),
98
+ bucketName: bucketName.trim(),
99
+ accessKeyId: accessKeyId.trim(),
100
+ secretAccessKey: secretAccessKey.trim(),
101
+ publicUrl: publicUrl.trim(),
102
+ }),
103
+ });
104
+
105
+ const data = await res.json();
106
+ if (!res.ok) {
107
+ setError(data.error || "Connection failed");
108
+ return;
109
+ }
110
+
111
+ // Success — update state and auto-advance
112
+ setR2Status({
113
+ connected: true,
114
+ bucket_name: bucketName.trim(),
115
+ public_url: publicUrl.trim(),
116
+ });
117
+ setState("connected");
118
+
119
+ // Small delay so user sees the success state
120
+ setTimeout(() => onNext(), 800);
121
+ } catch (err) {
122
+ setError(err instanceof Error ? err.message : "Connection failed");
123
+ } finally {
124
+ setTesting(false);
125
+ }
126
+ },
127
+ [endpoint, bucketName, accessKeyId, secretAccessKey, publicUrl, onNext]
128
+ );
129
+
130
+ const inputClass =
131
+ "w-full rounded-lg border border-black/[0.08] bg-white px-3 py-2 text-sm text-[#333] placeholder:text-[#bbb] focus:outline-none focus:ring-2 focus:ring-[#076bff]/20 focus:border-[#076bff]/40";
132
+ const labelClass = "block text-xs font-medium text-[#666] mb-1";
133
+
134
+ if (state === "loading") {
135
+ return (
136
+ <div className="flex flex-col items-center justify-center py-16 text-center">
137
+ <Spinner />
138
+ <p className="text-[#999] text-xs mt-3">Checking storage status...</p>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <div className="pt-8">
145
+ <div className="flex items-center gap-2.5 mb-1">
146
+ <R2Icon />
147
+ <h2 className="text-lg font-semibold text-[#111]">Storage</h2>
148
+ </div>
149
+ <p className="text-sm text-[#666] mb-8">
150
+ Connect Cloudflare R2 to store images, videos, and other assets.
151
+ </p>
152
+
153
+ {/* Already connected */}
154
+ {state === "connected" && r2Status && (
155
+ <div className="bg-white rounded-xl border border-green-200 p-5 mb-6">
156
+ <div className="flex items-center gap-3 mb-3">
157
+ <CheckCircle />
158
+ <span className="text-sm font-medium text-[#333]">
159
+ Cloudflare R2 Connected
160
+ </span>
161
+ </div>
162
+ {r2Status.bucket_name && (
163
+ <p className="text-xs text-[#999]">
164
+ Bucket: <span className="font-mono">{r2Status.bucket_name}</span>
165
+ </p>
166
+ )}
167
+ {r2Status.public_url && (
168
+ <p className="text-xs text-[#999] mt-0.5">
169
+ URL: <span className="font-mono">{r2Status.public_url}</span>
170
+ </p>
171
+ )}
172
+ </div>
173
+ )}
174
+
175
+ {/* Connection form */}
176
+ {state === "disconnected" && (
177
+ <form onSubmit={handleConnect} className="space-y-3 mb-6">
178
+ <div className="bg-white rounded-xl border border-black/[0.06] p-5 space-y-3">
179
+ <div>
180
+ <label className={labelClass}>S3 Endpoint</label>
181
+ <input
182
+ type="url"
183
+ value={endpoint}
184
+ onChange={(e) => setEndpoint(e.target.value)}
185
+ placeholder="https://<account-id>.r2.cloudflarestorage.com"
186
+ className={inputClass}
187
+ required
188
+ />
189
+ <p className="text-[11px] text-[#bbb] mt-0.5">
190
+ Found in Cloudflare R2 dashboard → Account Details
191
+ </p>
192
+ </div>
193
+
194
+ <div>
195
+ <label className={labelClass}>Bucket Name</label>
196
+ <input
197
+ type="text"
198
+ value={bucketName}
199
+ onChange={(e) => setBucketName(e.target.value)}
200
+ placeholder="my-assets"
201
+ className={inputClass}
202
+ required
203
+ />
204
+ </div>
205
+
206
+ <div>
207
+ <label className={labelClass}>Access Key ID</label>
208
+ <input
209
+ type="text"
210
+ value={accessKeyId}
211
+ onChange={(e) => setAccessKeyId(e.target.value)}
212
+ placeholder="R2 API token Access Key ID"
213
+ className={inputClass}
214
+ required
215
+ autoComplete="off"
216
+ />
217
+ </div>
218
+
219
+ <div>
220
+ <label className={labelClass}>Secret Access Key</label>
221
+ <input
222
+ type="password"
223
+ value={secretAccessKey}
224
+ onChange={(e) => setSecretAccessKey(e.target.value)}
225
+ placeholder="R2 API token Secret Access Key"
226
+ className={inputClass}
227
+ required
228
+ autoComplete="off"
229
+ />
230
+ </div>
231
+
232
+ <div>
233
+ <label className={labelClass}>Public Bucket URL</label>
234
+ <input
235
+ type="url"
236
+ value={publicUrl}
237
+ onChange={(e) => setPublicUrl(e.target.value)}
238
+ placeholder="https://pub-xxx.r2.dev or https://assets.example.com"
239
+ className={inputClass}
240
+ required
241
+ />
242
+ <p className="text-[11px] text-[#bbb] mt-0.5">
243
+ R2.dev subdomain or custom domain with public access enabled
244
+ </p>
245
+ </div>
246
+ </div>
247
+
248
+ {error && (
249
+ <div className="p-3 rounded-lg bg-red-50 border border-red-200">
250
+ <p className="text-xs text-red-700">{error}</p>
251
+ </div>
252
+ )}
253
+
254
+ <button
255
+ type="submit"
256
+ disabled={testing}
257
+ className="inline-flex items-center gap-2 rounded-lg bg-[#F6821F] text-white text-sm font-medium px-4 py-2.5 hover:bg-[#F6821F]/90 transition-colors disabled:opacity-50"
258
+ >
259
+ {testing ? (
260
+ <>
261
+ <Spinner />
262
+ Testing Connection...
263
+ </>
264
+ ) : (
265
+ "Test & Connect"
266
+ )}
267
+ </button>
268
+ </form>
269
+ )}
270
+
271
+ {/* Actions */}
272
+ <div className="flex items-center justify-between">
273
+ <div>
274
+ {onBack && (
275
+ <button
276
+ onClick={onBack}
277
+ className="px-4 py-2 text-sm text-[#666] hover:text-[#333] transition-colors"
278
+ >
279
+ Back
280
+ </button>
281
+ )}
282
+ </div>
283
+
284
+ <div className="flex items-center gap-3">
285
+ {/* Skip — storage can be configured later */}
286
+ {state === "disconnected" && (
287
+ <button
288
+ onClick={onNext}
289
+ className="px-4 py-2 text-sm text-[#999] hover:text-[#333] transition-colors"
290
+ >
291
+ Skip for now
292
+ </button>
293
+ )}
294
+
295
+ {/* Next (when already connected) */}
296
+ {state === "connected" && (
297
+ <button
298
+ onClick={onNext}
299
+ className="px-5 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors"
300
+ >
301
+ Next
302
+ </button>
303
+ )}
304
+ </div>
305
+ </div>
306
+ </div>
307
+ );
308
+ }
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import { getSiteConfig } from "../../../lib/config";
4
+ import type { WizardStepProps } from "./SetupWizard";
5
+
6
+ /**
7
+ * Step 1 — Welcome
8
+ *
9
+ * Displays the site name, a welcome message, and a brief overview
10
+ * of what the wizard will configure.
11
+ */
12
+ export function WelcomeStep({ onNext }: WizardStepProps) {
13
+ const config = getSiteConfig();
14
+ const primaryColor = config.palette.primary;
15
+
16
+ return (
17
+ <div className="pt-12 text-center">
18
+ {/* Color accent bar */}
19
+ <div
20
+ className="w-12 h-1 rounded-full mx-auto mb-8"
21
+ style={{ backgroundColor: primaryColor }}
22
+ />
23
+
24
+ <h1 className="text-2xl font-semibold text-[#111] mb-2">
25
+ Welcome to {config.name}
26
+ </h1>
27
+
28
+ <p className="text-sm text-[#666] mb-10 max-w-sm mx-auto leading-relaxed">
29
+ Let&apos;s set up your site in a few quick steps. We&apos;ll connect your
30
+ database, configure storage, and set up your brand.
31
+ </p>
32
+
33
+ {/* What we'll configure */}
34
+ <div className="text-left bg-white rounded-xl border border-black/[0.06] p-6 mb-10">
35
+ <h2 className="text-xs font-semibold uppercase tracking-wider text-[#999] mb-4">
36
+ What we&apos;ll configure
37
+ </h2>
38
+ <ul className="space-y-3">
39
+ {[
40
+ {
41
+ icon: (
42
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
43
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
44
+ <path d="M21 12c0 1.66-4.03 3-9 3s-9-1.34-9-3" />
45
+ <path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" />
46
+ </svg>
47
+ ),
48
+ label: "Database",
49
+ desc: "Connect to Sanity CMS for content storage",
50
+ },
51
+ {
52
+ icon: (
53
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
54
+ <path d="M22 12H2" />
55
+ <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11Z" />
56
+ <line x1="6" y1="16" x2="6.01" y2="16" strokeWidth="2" />
57
+ <line x1="10" y1="16" x2="10.01" y2="16" strokeWidth="2" />
58
+ </svg>
59
+ ),
60
+ label: "Storage",
61
+ desc: "Set up Cloudflare R2 for images and videos",
62
+ },
63
+ {
64
+ icon: (
65
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
66
+ <circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" />
67
+ <circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" />
68
+ <circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" />
69
+ <circle cx="6.5" cy="12" r="0.5" fill="currentColor" />
70
+ <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2Z" />
71
+ </svg>
72
+ ),
73
+ label: "Branding",
74
+ desc: "Upload fonts, set colors, and configure your identity",
75
+ },
76
+ ].map((item) => (
77
+ <li key={item.label} className="flex items-start gap-3">
78
+ <span className="text-[#076bff] mt-0.5 shrink-0">{item.icon}</span>
79
+ <div>
80
+ <span className="text-sm font-medium text-[#333]">{item.label}</span>
81
+ <p className="text-xs text-[#999] mt-0.5">{item.desc}</p>
82
+ </div>
83
+ </li>
84
+ ))}
85
+ </ul>
86
+ </div>
87
+
88
+ <button
89
+ onClick={onNext}
90
+ className="px-6 py-2.5 bg-[#076bff] text-white text-sm font-medium rounded-lg hover:bg-[#0559d4] transition-colors"
91
+ >
92
+ Get Started
93
+ </button>
94
+ </div>
95
+ );
96
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Setup Wizard — onboarding flow for new Visual Builder instances.
3
+ */
4
+ export { SetupWizard } from "./SetupWizard";
5
+ export { WelcomeStep } from "./WelcomeStep";
6
+ export { DatabaseStep } from "./DatabaseStep";
7
+ export { StorageStep } from "./StorageStep";
8
+ export { BrandingStep } from "./BrandingStep";
9
+ export { DoneStep } from "./DoneStep";
@@ -0,0 +1,214 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import ColorPicker, { isValidHex } from "../../../components/builder/ColorPicker";
5
+ import { invalidatePaletteCache } from "../../../components/builder/ColorSwatchPicker";
6
+ import type { SiteStyles, ColorSwatch } from "../../../lib/sanity/types";
7
+ import { Section, SaveButton } from "./shared";
8
+
9
+ function getContrastColor(hex: string): string {
10
+ if (!isValidHex(hex)) return "#000";
11
+ const r = parseInt(hex.slice(1, 3), 16);
12
+ const g = parseInt(hex.slice(3, 5), 16);
13
+ const b = parseInt(hex.slice(5, 7), 16);
14
+ return (r * 0.299 + g * 0.587 + b * 0.114) > 160 ? "#000000" : "#ffffff";
15
+ }
16
+
17
+ export function ColorsEditor({
18
+ colors,
19
+ onSave,
20
+ saving,
21
+ }: {
22
+ colors?: SiteStyles["colors"];
23
+ onSave: (data: Record<string, unknown>) => void;
24
+ saving: boolean;
25
+ }) {
26
+ const [swatches, setSwatches] = useState<ColorSwatch[]>(colors?.swatches || []);
27
+ const [pickerOpen, setPickerOpen] = useState(false);
28
+ const [editingIndex, setEditingIndex] = useState<number | null>(null);
29
+ const [renamingIndex, setRenamingIndex] = useState<number | null>(null);
30
+ const [renameValue, setRenameValue] = useState("");
31
+ const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
32
+
33
+ useEffect(() => {
34
+ setSwatches(colors?.swatches || []);
35
+ }, [colors]);
36
+
37
+ const addSwatch = (hex: string) => {
38
+ if (editingIndex !== null) {
39
+ setSwatches((prev) => prev.map((s, i) => i === editingIndex ? { ...s, hex } : s));
40
+ setEditingIndex(null);
41
+ } else {
42
+ const key = crypto.randomUUID().slice(0, 8);
43
+ setSwatches((prev) => [...prev, { _key: key, name: `Color ${prev.length + 1}`, hex }]);
44
+ }
45
+ setPickerOpen(false);
46
+ };
47
+
48
+ const removeSwatch = (index: number) => {
49
+ setSwatches((prev) => prev.filter((_, i) => i !== index));
50
+ };
51
+
52
+ const startRename = (index: number) => {
53
+ setRenamingIndex(index);
54
+ setRenameValue(swatches[index].name);
55
+ };
56
+
57
+ const finishRename = () => {
58
+ if (renamingIndex !== null && renameValue.trim()) {
59
+ setSwatches((prev) => prev.map((s, i) => i === renamingIndex ? { ...s, name: renameValue.trim() } : s));
60
+ }
61
+ setRenamingIndex(null);
62
+ };
63
+
64
+ const handleSave = () => {
65
+ onSave({
66
+ color_palette: swatches.map((s) => ({
67
+ _key: s._key || crypto.randomUUID().slice(0, 8),
68
+ _type: "object",
69
+ name: s.name,
70
+ hex: s.hex,
71
+ })),
72
+ });
73
+ invalidatePaletteCache();
74
+ };
75
+
76
+ return (
77
+ <Section title="Color Palette" description="Build your project's color palette. These colors will appear as presets in every block editor.">
78
+ {swatches.length === 0 && !pickerOpen ? (
79
+ /* ─── Empty state ─── */
80
+ <div className="border-2 border-dashed border-neutral-200 rounded-2xl py-12 px-6 flex flex-col items-center text-center bg-neutral-50/50">
81
+ {/* 4-color icon */}
82
+ <div className="grid grid-cols-2 gap-1 w-12 h-12 mb-4">
83
+ <div className="rounded-tl-lg bg-[var(--admin-accent)] opacity-40" />
84
+ <div className="rounded-tr-lg bg-[var(--admin-error)] opacity-40" />
85
+ <div className="rounded-bl-lg bg-brand-accent opacity-40" />
86
+ <div className="rounded-br-lg bg-brand-accent-2 opacity-40" />
87
+ </div>
88
+ <p className="text-sm font-medium text-neutral-700 mb-1">No colors yet</p>
89
+ <p className="text-xs text-neutral-500 mb-5 max-w-xs leading-relaxed">
90
+ Start building your palette. Colors you add here will be available as quick presets in every block editor — text, backgrounds, borders, and more.
91
+ </p>
92
+ <button
93
+ onClick={() => { setEditingIndex(null); setPickerOpen(true); }}
94
+ className="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-neutral-900 text-white text-sm font-medium hover:bg-neutral-700 transition-colors"
95
+ >
96
+ <span className="text-lg leading-none">+</span>
97
+ Add your first color
98
+ </button>
99
+ </div>
100
+ ) : (
101
+ <>
102
+ {/* ─── Swatches grid ─── */}
103
+ {swatches.length > 0 && (
104
+ <div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-3 mb-4">
105
+ {swatches.map((s, i) => (
106
+ <div
107
+ key={s._key || i}
108
+ className="rounded-xl border border-neutral-200 overflow-hidden transition-all hover:shadow-md hover:-translate-y-0.5"
109
+ onMouseEnter={() => setHoveredIndex(i)}
110
+ onMouseLeave={() => setHoveredIndex(null)}
111
+ >
112
+ {/* Color area */}
113
+ <div
114
+ className="h-16 relative cursor-pointer"
115
+ style={{ background: s.hex }}
116
+ onClick={() => { setEditingIndex(i); setPickerOpen(true); }}
117
+ >
118
+ {hoveredIndex === i && (
119
+ <div className="absolute inset-0 bg-black/15 flex items-center justify-center gap-1.5">
120
+ <button
121
+ onClick={(e) => { e.stopPropagation(); setEditingIndex(i); setPickerOpen(true); }}
122
+ className="w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[11px] cursor-pointer hover:bg-white transition-colors border-none"
123
+ title="Edit color"
124
+ >✎</button>
125
+ <button
126
+ onClick={(e) => { e.stopPropagation(); removeSwatch(i); }}
127
+ className="w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[13px] text-red-500 cursor-pointer hover:bg-white transition-colors border-none"
128
+ title="Remove"
129
+ >×</button>
130
+ </div>
131
+ )}
132
+ </div>
133
+ {/* Info */}
134
+ <div className="px-2.5 py-2 bg-white">
135
+ {renamingIndex === i ? (
136
+ <input
137
+ autoFocus
138
+ value={renameValue}
139
+ onChange={(e) => setRenameValue(e.target.value)}
140
+ onBlur={finishRename}
141
+ onKeyDown={(e) => e.key === "Enter" && finishRename()}
142
+ className="w-full border-none border-b border-neutral-300 text-[11px] font-semibold text-neutral-800 outline-none p-0 bg-transparent"
143
+ />
144
+ ) : (
145
+ <div
146
+ onClick={() => startRename(i)}
147
+ className="text-[11px] font-semibold text-neutral-700 cursor-text truncate"
148
+ >{s.name}</div>
149
+ )}
150
+ <div className="text-[10px] text-neutral-400 mt-0.5 uppercase font-mono">{s.hex}</div>
151
+ </div>
152
+ </div>
153
+ ))}
154
+
155
+ {/* Add swatch card */}
156
+ {!pickerOpen && (
157
+ <button
158
+ onClick={() => { setEditingIndex(null); setPickerOpen(true); }}
159
+ className="rounded-xl border-2 border-dashed border-neutral-200 h-[100px] flex flex-col items-center justify-center gap-1 cursor-pointer hover:border-[#076bff] hover:text-[#076bff] text-neutral-400 transition-colors bg-transparent"
160
+ >
161
+ <span className="text-xl leading-none">+</span>
162
+ <span className="text-[10px] uppercase tracking-wider">Add</span>
163
+ </button>
164
+ )}
165
+ </div>
166
+ )}
167
+
168
+ {/* Palette strip preview */}
169
+ {swatches.length >= 2 && !pickerOpen && (
170
+ <div className="px-4 py-3 bg-neutral-50 rounded-xl border border-neutral-100 mb-4">
171
+ <div className="text-[10px] text-neutral-400 uppercase tracking-widest mb-2">Palette preview</div>
172
+ <div className="flex rounded-lg overflow-hidden h-8">
173
+ {swatches.map((s, i) => (
174
+ <div
175
+ key={s._key || i}
176
+ className="flex-1 flex items-center justify-center"
177
+ style={{ background: s.hex }}
178
+ >
179
+ <span
180
+ className="text-[7px] uppercase tracking-wider font-mono opacity-70"
181
+ style={{ color: getContrastColor(s.hex) }}
182
+ >{s.name}</span>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </div>
187
+ )}
188
+
189
+ {/* Picker */}
190
+ {pickerOpen && (
191
+ <div className="flex justify-center mb-4">
192
+ <ColorPicker
193
+ color={editingIndex !== null ? swatches[editingIndex].hex : "#076bff"}
194
+ onChange={addSwatch}
195
+ onClose={() => { setPickerOpen(false); setEditingIndex(null); }}
196
+ confirmLabel={editingIndex !== null ? "Update color" : "Add to palette"}
197
+ />
198
+ </div>
199
+ )}
200
+ </>
201
+ )}
202
+
203
+ {/* Save footer */}
204
+ {(swatches.length > 0 || (colors?.swatches?.length || 0) > 0) && (
205
+ <div className="flex items-center justify-between mt-4 pt-4 border-t border-neutral-100">
206
+ <span className="text-[11px] text-neutral-400">
207
+ {swatches.length} color{swatches.length !== 1 ? "s" : ""} in palette
208
+ </span>
209
+ <SaveButton onClick={handleSave} saving={saving} />
210
+ </div>
211
+ )}
212
+ </Section>
213
+ );
214
+ }