@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,333 @@
1
+ import { create } from "zustand";
2
+ import type { BuilderStore, BuilderState, BlockType, PageSettings, CanvasTool, DeviceViewport, GridSettings } from "./types";
3
+ import { DEFAULT_PAGE_SETTINGS, DEFAULT_GRID_SETTINGS, DEVICE_WIDTHS } from "./types";
4
+ import { stateToDocument, documentToState } from "./serializer";
5
+ import { generateKey } from "./utils";
6
+ import type { ContentBlock, ContentItem, PageSection, PageSectionV2, SectionV2Settings, SectionSettings, SectionBlock, PageMetadata, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
7
+ import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
8
+ import { createDefaultBlock, createDefaultParallaxSlide } from "./defaults";
9
+ import { MAX_HISTORY, pushSnapshot } from "./history";
10
+ import {
11
+ computeCascade,
12
+ detectPreset,
13
+ columnsFromPreset,
14
+ addColumn as cascadeAddColumn,
15
+ deleteColumn as cascadeDeleteColumn,
16
+ resizeColumn as cascadeResizeColumn,
17
+ } from "./cascade";
18
+ import { applyBlocksToColumns, toCascadeColumns } from "./cascade-helpers";
19
+ import {
20
+ moveBlockInState,
21
+ resizeColumnV2LeftInState,
22
+ addSectionV2InState,
23
+ moveColumnV2InState,
24
+ swapColumnV2InState,
25
+ moveColumnToGapV2InState,
26
+ addParallaxGroupInState,
27
+ findSectionPath,
28
+ getSectionFromPath,
29
+ updateSectionAtPath,
30
+ } from "./store-helpers";
31
+ import { csrfHeaders } from "../../lib/csrf-client";
32
+ import { revalidateSite } from "../../lib/revalidate";
33
+ import { createSectionActions } from "./store-sections";
34
+ import { createBlockActions } from "./store-blocks";
35
+ import { createCanvasActions } from "./store-canvas";
36
+
37
+ // ============================================
38
+ // Initial State
39
+ // ============================================
40
+
41
+ const initialState: BuilderState = {
42
+ pageId: null,
43
+ pageTitle: "",
44
+ pageSlug: "",
45
+ _originalSlug: "",
46
+ pageType: "page",
47
+ metadata: {},
48
+ publishedAt: null,
49
+ draftMode: true,
50
+
51
+ rows: [],
52
+ _customSectionCache: {},
53
+
54
+ selectedRowKey: null,
55
+ selectedColumnKey: null,
56
+ selectedBlockKey: null,
57
+ selectedProjectCardKey: null,
58
+
59
+ isDirty: false,
60
+ isSaving: false,
61
+ saveError: null,
62
+ lastSavedAt: null,
63
+
64
+ // Editor mode
65
+ previewMode: false,
66
+
67
+ // Custom section editor mode (Session 107)
68
+ editorMode: "page" as const,
69
+ customSectionSlug: null,
70
+ customSectionTitle: null,
71
+ savedPageState: null,
72
+
73
+ // Page-level settings
74
+ pageSettings: { ...DEFAULT_PAGE_SETTINGS },
75
+
76
+ // Grid settings (from Customize)
77
+ gridSettings: { ...DEFAULT_GRID_SETTINGS },
78
+
79
+ // Canvas (ephemeral)
80
+ canvasZoom: 1,
81
+ canvasPanX: 0,
82
+ canvasPanY: 0,
83
+ canvasTool: "select",
84
+ activeViewport: "desktop",
85
+
86
+ // BUG-014: track whether document had page_settings
87
+ _hasDocumentPageSettings: false,
88
+
89
+ // History
90
+ _history: [],
91
+ _future: [],
92
+ _isTimeTraveling: false,
93
+ };
94
+
95
+ // ============================================
96
+ // Store
97
+ // ============================================
98
+
99
+ export const useBuilderStore = create<BuilderStore>((set, get) => ({
100
+ ...initialState,
101
+
102
+ // ---- Internal: push snapshot before mutations ----
103
+
104
+ // BUG-010 fix: Snapshots now include pageSettings alongside rows
105
+ _pushSnapshot: () => {
106
+ const state = get();
107
+ set({
108
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
109
+ _future: [], // new mutation clears redo stack
110
+ });
111
+ },
112
+
113
+ // ---- History (Undo/Redo) ----
114
+
115
+ canUndo: () => get()._history.length > 0,
116
+ canRedo: () => get()._future.length > 0,
117
+
118
+ undo: () => {
119
+ const state = get();
120
+ if (state._history.length === 0) return;
121
+ const history = [...state._history];
122
+ const previous = history.pop()!;
123
+ set({
124
+ _history: history,
125
+ _future: [...state._future, structuredClone({ rows: state.rows, pageSettings: state.pageSettings })],
126
+ rows: previous.rows,
127
+ pageSettings: previous.pageSettings,
128
+ isDirty: true,
129
+ selectedRowKey: null,
130
+ selectedColumnKey: null,
131
+ selectedBlockKey: null,
132
+ });
133
+ },
134
+
135
+ redo: () => {
136
+ const state = get();
137
+ if (state._future.length === 0) return;
138
+ const future = [...state._future];
139
+ const next = future.pop()!;
140
+ set({
141
+ _history: pushSnapshot(state._history, { rows: state.rows, pageSettings: state.pageSettings }),
142
+ _future: future,
143
+ rows: next.rows,
144
+ pageSettings: next.pageSettings,
145
+ isDirty: true,
146
+ selectedRowKey: null,
147
+ selectedColumnKey: null,
148
+ selectedBlockKey: null,
149
+ });
150
+ },
151
+
152
+ // ---- Page metadata ----
153
+
154
+ setPageTitle: (title: string) =>
155
+ set({ pageTitle: title, isDirty: true }),
156
+
157
+ setPageSlug: (slug: string) =>
158
+ set({ pageSlug: slug, isDirty: true }),
159
+
160
+ setMetadata: (metadata: Partial<PageMetadata>) =>
161
+ set((state) => ({
162
+ metadata: { ...state.metadata, ...metadata },
163
+ isDirty: true,
164
+ })),
165
+
166
+ setDraftMode: (draft: boolean) =>
167
+ set({ draftMode: draft, isDirty: true }),
168
+
169
+ publishPage: () =>
170
+ set({ draftMode: false, publishedAt: new Date().toISOString(), isDirty: true }),
171
+
172
+ unpublishPage: () =>
173
+ set({ draftMode: true, publishedAt: null, isDirty: true }),
174
+
175
+ // ---- Selection ----
176
+
177
+ selectRow: (key) =>
178
+ set({ selectedRowKey: key, selectedColumnKey: null, selectedBlockKey: null }),
179
+
180
+ selectColumn: (rowKey, colKey) =>
181
+ set({ selectedRowKey: rowKey, selectedColumnKey: colKey, selectedBlockKey: null }),
182
+
183
+ selectBlock: (key) => {
184
+ if (!key) {
185
+ set({ selectedBlockKey: null });
186
+ return;
187
+ }
188
+ // BUG-002 fix: resolve parent row and column keys so SettingsPanel
189
+ // and keyboard shortcuts always have consistent selection state.
190
+ const state = get();
191
+ let parentRowKey: string | null = null;
192
+ let parentColKey: string | null = null;
193
+
194
+ const searchColumns = (columns: import("@/lib/sanity/types").SectionColumn[], rowKey: string): boolean => {
195
+ for (const col of columns) {
196
+ if (col.blocks.some((b) => b._key === key)) {
197
+ parentRowKey = rowKey;
198
+ parentColKey = col._key;
199
+ return true;
200
+ }
201
+ }
202
+ return false;
203
+ };
204
+
205
+ for (const item of state.rows) {
206
+ if (isPageSection(item)) {
207
+ const sBlock = Array.isArray(item.block) ? item.block[0] : undefined;
208
+ if (sBlock && sBlock._key === key) {
209
+ parentRowKey = item._key;
210
+ break;
211
+ }
212
+ } else if (isPageSectionV2(item)) {
213
+ if (searchColumns(item.columns, item._key)) break;
214
+ } else if (isParallaxGroup(item)) {
215
+ const group = item as ParallaxGroup;
216
+ let found = false;
217
+ for (const slide of group.slides) {
218
+ // For parallax slides, the "row key" is the slide key (used as sectionKey)
219
+ if (searchColumns(slide.columns, slide._key)) {
220
+ found = true;
221
+ break;
222
+ }
223
+ }
224
+ if (found) break;
225
+ }
226
+ }
227
+ set({
228
+ selectedBlockKey: key,
229
+ selectedRowKey: parentRowKey,
230
+ selectedColumnKey: parentColKey,
231
+ selectedProjectCardKey: null,
232
+ });
233
+ },
234
+
235
+ selectProjectCard: (key) => set({ selectedProjectCardKey: key }),
236
+
237
+ clearSelection: () =>
238
+ set({ selectedRowKey: null, selectedColumnKey: null, selectedBlockKey: null, selectedProjectCardKey: null }),
239
+
240
+ // ---- Persistence ----
241
+
242
+ loadFromDocument: (doc) => {
243
+ const state = documentToState(doc);
244
+ set({
245
+ ...state,
246
+ _originalSlug: state.pageSlug, // Remember the slug from Sanity for API calls
247
+ isDirty: false,
248
+ isSaving: false,
249
+ saveError: null,
250
+ selectedRowKey: null,
251
+ selectedColumnKey: null,
252
+ selectedBlockKey: null,
253
+ selectedProjectCardKey: null,
254
+ // Reset history on document load
255
+ _history: [],
256
+ _future: [],
257
+ _isTimeTraveling: false,
258
+ });
259
+ },
260
+
261
+ save: async () => {
262
+ let state = get();
263
+ if (!state.pageId) {
264
+ set({ saveError: "No page loaded" });
265
+ return;
266
+ }
267
+ // Prevent saving with empty slug — auto-generate from title as fallback
268
+ if (!state.pageSlug.trim()) {
269
+ const autoSlug = state.pageTitle
270
+ .toLowerCase().trim()
271
+ .replace(/[^\w\s-]/g, "")
272
+ .replace(/[\s_]+/g, "-")
273
+ .replace(/-+/g, "-")
274
+ .replace(/^-|-$/g, "");
275
+ if (autoSlug) {
276
+ set({ pageSlug: autoSlug });
277
+ } else {
278
+ set({ saveError: "Slug cannot be empty — please set a title or slug" });
279
+ return;
280
+ }
281
+ }
282
+ set({ isSaving: true, saveError: null });
283
+ // Re-read state after potential slug auto-generation to avoid stale data
284
+ state = get();
285
+ try {
286
+ const document = stateToDocument(state);
287
+ // Use the original slug (as loaded from Sanity) for the API endpoint,
288
+ // since that's how the API finds the document. If the user edited the
289
+ // slug, it will be updated in the document body.
290
+ const res = await fetch(`/api/admin/pages/${state._originalSlug}`, {
291
+ method: "POST",
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ ...csrfHeaders(),
295
+ },
296
+ body: JSON.stringify(document),
297
+ });
298
+ if (!res.ok) {
299
+ const data = await res.json().catch(() => ({ error: "Save failed" }));
300
+ throw new Error(data.error || `Save failed (${res.status})`);
301
+ }
302
+ set({
303
+ isSaving: false,
304
+ isDirty: false,
305
+ lastSavedAt: new Date().toISOString(),
306
+ _originalSlug: get().pageSlug, // Update original slug after successful save
307
+ });
308
+ // Revalidate public site so page changes appear immediately
309
+ const slug = get().pageSlug;
310
+ const pageType = get().pageType;
311
+ const publicPath = pageType === "project" ? `/work/${slug}` : `/${slug}`;
312
+ revalidateSite(slug ? [publicPath] : undefined);
313
+ } catch (err) {
314
+ set({
315
+ isSaving: false,
316
+ saveError: err instanceof Error ? err.message : "Save failed",
317
+ });
318
+ }
319
+ },
320
+
321
+ reset: () => set(initialState),
322
+
323
+ // ---- Dirty tracking ----
324
+
325
+ markDirty: () => set({ isDirty: true }),
326
+ markClean: () => set({ isDirty: false }),
327
+
328
+ // ---- Spread in action groups ----
329
+
330
+ ...createSectionActions(set, get),
331
+ ...createBlockActions(set, get),
332
+ ...createCanvasActions(set, get),
333
+ }));
@@ -0,0 +1,297 @@
1
+ // ============================================
2
+ // Page Templates — Predefined page structures
3
+ // ============================================
4
+ // Each template defines a set of pre-built rows/columns/blocks
5
+ // that get injected as content_rows when creating a new page.
6
+ //
7
+ // BUG-004 fix (Session 78): Templates now create first-class PageSection
8
+ // objects for section blocks instead of the old matryoshka format.
9
+
10
+ import type { ContentBlock, ContentItem, PageSection, PageSectionV2, SectionBlock, SectionColumn, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
11
+ import { generateKey } from "./utils";
12
+
13
+ // ============================================
14
+ // Template Types
15
+ // ============================================
16
+
17
+ export interface PageTemplate {
18
+ /** Unique identifier */
19
+ id: string;
20
+ /** Display name */
21
+ label: string;
22
+ /** Short description shown under the label */
23
+ description: string;
24
+ /** Icon character or emoji for the card */
25
+ icon: string;
26
+ /** Suggested page_type ("page" | "project") */
27
+ pageType?: "page" | "project";
28
+ /** SVG preview (optional, rendered as mini wireframe in the picker) */
29
+ preview?: TemplateSectionPreview[];
30
+ /** Factory function — returns fresh content items (sections) with unique keys each time */
31
+ buildRows: () => ContentItem[];
32
+ }
33
+
34
+ /** Lightweight wireframe section descriptor for visual preview */
35
+ export interface TemplateSectionPreview {
36
+ /** e.g. "cover", "text", "image", "grid", "spacer", "columns", "parallax" */
37
+ type: string;
38
+ /** Relative height hint (1 = standard row height) */
39
+ height?: number;
40
+ }
41
+
42
+ // ============================================
43
+ // Blank Template (always first)
44
+ // ============================================
45
+
46
+ const blankTemplate: PageTemplate = {
47
+ id: "blank",
48
+ label: "Blank Page",
49
+ description: "Start from scratch with an empty canvas",
50
+ icon: "◻",
51
+ preview: [],
52
+ buildRows: () => [],
53
+ };
54
+
55
+ // ============================================
56
+ // Helper: create a block with a fresh key
57
+ // ============================================
58
+
59
+ function makeBlock(partial: Record<string, unknown> & { _type: string; _key?: string }): ContentBlock {
60
+ return { ...partial, _key: partial._key || generateKey() } as ContentBlock;
61
+ }
62
+
63
+ /** Create a V2 section with a single full-width column containing the given blocks */
64
+ function makeV2Section(blocks: ContentBlock[], settings?: Partial<PageSectionV2["settings"]>): PageSectionV2 {
65
+ const col: SectionColumn = {
66
+ _key: generateKey(),
67
+ grid_column: 1,
68
+ grid_row: 1,
69
+ span: 12,
70
+ blocks,
71
+ };
72
+ return {
73
+ _type: "pageSectionV2",
74
+ _key: generateKey(),
75
+ section_type: "empty-v2" as const,
76
+ columns: [col],
77
+ settings: {
78
+ preset: "full" as const,
79
+ grid_columns: 12,
80
+ col_gap: 16,
81
+ row_gap: 16,
82
+ ...(settings || {}),
83
+ },
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Create a first-class PageSection with a section block.
89
+ * BUG-004 fix: replaces the old pattern of wrapping section blocks in makeRow().
90
+ */
91
+ function makeSection(
92
+ blockType: "projectGridBlock",
93
+ blockDefaults: Record<string, unknown>,
94
+ ): PageSection {
95
+ const block = makeBlock({ _type: blockType, ...blockDefaults }) as SectionBlock;
96
+ return {
97
+ _type: "pageSection",
98
+ _key: generateKey(),
99
+ section_type: "projectGrid" as import("@/lib/sanity/types").PageSectionType,
100
+ block: [block] as [SectionBlock],
101
+ settings: {
102
+ spacing_top: "32",
103
+ spacing_bottom: "32",
104
+ },
105
+ };
106
+ }
107
+
108
+ // ============================================
109
+ // Template: Project Grid (staggered project showcase)
110
+ // ============================================
111
+
112
+ const projectGridTemplate: PageTemplate = {
113
+ id: "project-grid",
114
+ label: "Project Grid",
115
+ description: "Staggered project showcase with configurable layouts",
116
+ icon: "⬡",
117
+ preview: [
118
+ { type: "spacer", height: 0.5 },
119
+ { type: "text", height: 0.5 },
120
+ { type: "spacer", height: 0.3 },
121
+ { type: "cover", height: 2 },
122
+ { type: "columns", height: 1.5 },
123
+ { type: "cover", height: 2 },
124
+ { type: "columns", height: 1.5 },
125
+ ],
126
+ buildRows: () => [
127
+ // Section 1: Top spacer
128
+ makeV2Section([makeBlock({ _type: "spacerBlock", height: "large" })]),
129
+ // Section 2: Optional intro text
130
+ makeV2Section([
131
+ makeBlock({
132
+ _type: "textBlock",
133
+ text: [],
134
+ style: { fontSize: 20, alignment: "left", fontWeight: "400" },
135
+ }),
136
+ ]),
137
+ // Section 3: Spacer
138
+ makeV2Section([makeBlock({ _type: "spacerBlock", height: "medium" })]),
139
+ // Section 4: Project Grid (first-class PageSection)
140
+ makeSection("projectGridBlock", {
141
+ columns: 3,
142
+ aspect_ratios: ["16/9"],
143
+ gap_v: 16,
144
+ gap_h: 16,
145
+ hover_effect: "scale",
146
+ show_subtitle: true,
147
+ border_radius: 0,
148
+ video_mode: "off",
149
+ projects: [],
150
+ }),
151
+ // Section 5: Bottom spacer
152
+ makeV2Section([makeBlock({ _type: "spacerBlock", height: "xlarge" })]),
153
+ ],
154
+ };
155
+
156
+ // ============================================
157
+ // Helper: create a ParallaxSlide with defaults
158
+ // ============================================
159
+
160
+ function makeParallaxSlide(overrides?: Partial<ParallaxSlideV2>): ParallaxSlideV2 {
161
+ return {
162
+ _key: generateKey(),
163
+ _type: "parallaxSlide",
164
+ background_type: "image",
165
+ background_position: "center center",
166
+ background_overlay_color: "#000000",
167
+ background_overlay_opacity: 0,
168
+ columns: [{
169
+ _key: generateKey(),
170
+ grid_column: 1,
171
+ grid_row: 1,
172
+ span: 12,
173
+ blocks: [],
174
+ }],
175
+ section_settings: {
176
+ preset: "full",
177
+ grid_columns: 12,
178
+ col_gap: 20,
179
+ row_gap: 20,
180
+ spacing_top: "0",
181
+ spacing_bottom: "0",
182
+ },
183
+ ...overrides,
184
+ };
185
+ }
186
+
187
+ // ============================================
188
+ // Template: Parallax Home (full-screen slide showcase)
189
+ // ============================================
190
+
191
+ const parallaxHomeTemplate: PageTemplate = {
192
+ id: "parallax-home",
193
+ label: "Parallax Home",
194
+ description: "Full-screen slides with parallax backgrounds",
195
+ icon: "▽",
196
+ preview: [
197
+ { type: "parallax", height: 3 },
198
+ { type: "spacer", height: 0.3 },
199
+ ],
200
+ buildRows: () => {
201
+ // Slide 1: hero — single centered column with a text block placeholder
202
+ const slide1 = makeParallaxSlide({
203
+ columns: [{
204
+ _key: generateKey(),
205
+ grid_column: 3,
206
+ grid_row: 1,
207
+ span: 8,
208
+ blocks: [
209
+ makeBlock({
210
+ _type: "textBlock",
211
+ text: [],
212
+ style: { fontSize: 48, alignment: "center", fontWeight: "700" },
213
+ }),
214
+ ],
215
+ }],
216
+ section_settings: {
217
+ preset: "full",
218
+ grid_columns: 12,
219
+ col_gap: 20,
220
+ row_gap: 20,
221
+ spacing_top: "0",
222
+ spacing_bottom: "0",
223
+ },
224
+ background_overlay_opacity: 30,
225
+ });
226
+
227
+ // Slide 2: content — two columns (6/6)
228
+ const slide2 = makeParallaxSlide({
229
+ columns: [
230
+ {
231
+ _key: generateKey(),
232
+ grid_column: 1,
233
+ grid_row: 1,
234
+ span: 6,
235
+ blocks: [
236
+ makeBlock({
237
+ _type: "textBlock",
238
+ text: [],
239
+ style: { fontSize: 24, alignment: "left", fontWeight: "500" },
240
+ }),
241
+ ],
242
+ },
243
+ {
244
+ _key: generateKey(),
245
+ grid_column: 7,
246
+ grid_row: 1,
247
+ span: 6,
248
+ blocks: [],
249
+ },
250
+ ],
251
+ section_settings: {
252
+ preset: "halves",
253
+ grid_columns: 12,
254
+ col_gap: 20,
255
+ row_gap: 20,
256
+ spacing_top: "0",
257
+ spacing_bottom: "0",
258
+ },
259
+ background_overlay_opacity: 20,
260
+ });
261
+
262
+ // Slide 3: minimal — single full-width column
263
+ const slide3 = makeParallaxSlide({
264
+ background_overlay_opacity: 15,
265
+ });
266
+
267
+ const parallaxGroup: ParallaxGroup = {
268
+ _type: "parallaxGroup",
269
+ _key: generateKey(),
270
+ slides: [slide1, slide2, slide3],
271
+ transition_effect: "parallax",
272
+ snap_enabled: true,
273
+ parallax_intensity: 0.4,
274
+ };
275
+
276
+ return [parallaxGroup];
277
+ },
278
+ };
279
+
280
+ // ============================================
281
+ // Template Registry
282
+ // ============================================
283
+ // Order matters — this is the display order in the picker.
284
+ // "blank" is always first.
285
+
286
+ export const PAGE_TEMPLATES: PageTemplate[] = [
287
+ blankTemplate,
288
+ projectGridTemplate,
289
+ parallaxHomeTemplate,
290
+ ];
291
+
292
+ /**
293
+ * Get a template by ID. Returns undefined if not found.
294
+ */
295
+ export function getTemplate(id: string): PageTemplate | undefined {
296
+ return PAGE_TEMPLATES.find((t) => t.id === id);
297
+ }