@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,490 @@
1
+ /**
2
+ * Pure helper functions extracted from Zustand store actions.
3
+ *
4
+ * Each function takes immutable state and returns new state —
5
+ * no side effects, no store references, fully testable.
6
+ *
7
+ * Session 96: Store Action Extraction
8
+ */
9
+
10
+ import type {
11
+ ContentBlock,
12
+ ContentItem,
13
+ PageSectionV2,
14
+ ParallaxGroup,
15
+ ParallaxSlideV2,
16
+ SectionColumn,
17
+ SectionV2Preset,
18
+ } from "../../lib/sanity/types";
19
+ import { isPageSection, isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
20
+ import { columnsFromPreset, detectPreset } from "./cascade";
21
+ import { resizeColumnLeft as cascadeResizeLeft, moveColumn as cascadeMoveColumn, type ResizeLeftResult } from "./cascade";
22
+ import { applyBlocksToColumns, toCascadeColumns, type CascadeColumn } from "./cascade-helpers";
23
+ import { generateKey } from "./utils";
24
+ import { createDefaultParallaxSlide } from "./defaults";
25
+
26
+ // ============================================
27
+ // moveBlockInState
28
+ // ============================================
29
+
30
+ export interface MoveBlockResult {
31
+ rows: ContentItem[];
32
+ }
33
+
34
+ /**
35
+ * Move a block from its current position to a target column in a V2 section.
36
+ * Returns null if the move is invalid (block not found, target not V2).
37
+ *
38
+ * Pure function — no store access.
39
+ */
40
+ export function moveBlockInState(
41
+ rows: ContentItem[],
42
+ blockKey: string,
43
+ targetSectionKey: string,
44
+ targetColumnKey: string,
45
+ toIndex: number
46
+ ): MoveBlockResult | null {
47
+ // Validate target exists as a V2 section or parallax slide
48
+ const targetPath = findSectionPath(rows, targetSectionKey);
49
+ if (!targetPath) return null;
50
+
51
+ let movedBlock: ContentBlock | null = null;
52
+
53
+ const removeFromColumns = (columns: SectionColumn[]) =>
54
+ columns.map((c) => {
55
+ const blocks = [...c.blocks];
56
+ const idx = blocks.findIndex((b) => b._key === blockKey);
57
+ if (idx !== -1) {
58
+ [movedBlock] = blocks.splice(idx, 1);
59
+ }
60
+ return { ...c, blocks };
61
+ });
62
+
63
+ // Pass 1: find and remove block from all sections (V2 + parallax slides)
64
+ const rowsAfterRemove = rows.map((item) => {
65
+ if (isPageSectionV2(item)) {
66
+ return { ...item, columns: removeFromColumns(item.columns) } as ContentItem;
67
+ }
68
+ if (isParallaxGroup(item)) {
69
+ const group = item as ParallaxGroup;
70
+ return {
71
+ ...group,
72
+ slides: group.slides.map((slide) => ({
73
+ ...slide,
74
+ columns: removeFromColumns(slide.columns),
75
+ })),
76
+ } as ContentItem;
77
+ }
78
+ return item;
79
+ });
80
+
81
+ if (!movedBlock) return null;
82
+
83
+ // Pass 2: insert into target column using the path-based approach
84
+ const insertIntoColumns = (columns: SectionColumn[]) =>
85
+ columns.map((c) => {
86
+ if (c._key !== targetColumnKey) return c;
87
+ const blocks = [...c.blocks];
88
+ blocks.splice(toIndex, 0, movedBlock!);
89
+ return { ...c, blocks };
90
+ });
91
+
92
+ const finalRows = updateSectionAtPath(rowsAfterRemove, targetPath, (section) => ({
93
+ ...section,
94
+ columns: insertIntoColumns(section.columns),
95
+ }));
96
+
97
+ return { rows: finalRows };
98
+ }
99
+
100
+ // ============================================
101
+ // resizeColumnV2LeftInState
102
+ // ============================================
103
+
104
+ export interface ResizeColumnLeftResult {
105
+ rows: ContentItem[];
106
+ /** Key of the left neighbor that was compressed, if any */
107
+ compressedNeighborKey: string | null;
108
+ /** True when the left neighbor is at minimum span (1) — further expansion blocked */
109
+ neighborAtMinimum: boolean;
110
+ }
111
+
112
+ /**
113
+ * Resize a V2 column by moving its left edge.
114
+ * When expanding into a left neighbor, the neighbor is compressed (down to span 1).
115
+ * Returns null only if the operation is completely impossible.
116
+ *
117
+ * Pure function — no store access.
118
+ */
119
+ export function resizeColumnV2LeftInState(
120
+ rows: ContentItem[],
121
+ sectionKey: string,
122
+ columnKey: string,
123
+ newGridColumn: number
124
+ ): ResizeColumnLeftResult | null {
125
+ const path = findSectionPath(rows, sectionKey);
126
+ if (!path) return null;
127
+ const section = getSectionFromPath(rows, path);
128
+ if (!section) return null;
129
+
130
+ const cascadeCols = toCascadeColumns(section.columns);
131
+ const result = cascadeResizeLeft(cascadeCols, columnKey, newGridColumn, section.settings.grid_columns);
132
+ if (!result) return null;
133
+
134
+ const updatedColumns = applyBlocksToColumns(result.columns, section.columns);
135
+ const newPreset = detectPreset(result.columns, section.settings.grid_columns);
136
+
137
+ const newRows = updateSectionAtPath(rows, path, (sec) => ({
138
+ ...sec,
139
+ columns: updatedColumns,
140
+ settings: { ...sec.settings, preset: newPreset },
141
+ }));
142
+
143
+ return {
144
+ rows: newRows,
145
+ compressedNeighborKey: result.compressedNeighborKey,
146
+ neighborAtMinimum: result.neighborAtMinimum,
147
+ };
148
+ }
149
+
150
+ // ============================================
151
+ // addSectionV2InState
152
+ // ============================================
153
+
154
+ export interface AddSectionV2Result {
155
+ rows: ContentItem[];
156
+ newSectionKey: string;
157
+ }
158
+
159
+ /**
160
+ * Create a new V2 section with the given preset and insert it into the rows.
161
+ * Returns the new rows array and the key of the created section.
162
+ *
163
+ * Pure function — uses generateKey() for side effects (unique IDs), but
164
+ * is otherwise deterministic given the same key generation.
165
+ */
166
+ export function addSectionV2InState(
167
+ rows: ContentItem[],
168
+ preset: SectionV2Preset,
169
+ afterRowKey?: string | null
170
+ ): AddSectionV2Result {
171
+ const gridColumns = 12;
172
+ let columns: SectionColumn[];
173
+
174
+ if (preset === "custom") {
175
+ columns = [{
176
+ _key: generateKey(),
177
+ grid_column: 1,
178
+ grid_row: 1,
179
+ span: gridColumns,
180
+ blocks: [],
181
+ }];
182
+ } else {
183
+ const keyFactory = () => generateKey();
184
+ const cascadeCols = columnsFromPreset(preset as Exclude<SectionV2Preset, "custom">, gridColumns, keyFactory);
185
+ columns = cascadeCols.map((c: CascadeColumn) => ({
186
+ ...c,
187
+ blocks: [] as ContentBlock[],
188
+ }));
189
+ }
190
+
191
+ const detectedPreset = detectPreset(columns, gridColumns);
192
+
193
+ const newSection: PageSectionV2 = {
194
+ _type: "pageSectionV2",
195
+ _key: generateKey(),
196
+ section_type: "empty-v2",
197
+ columns,
198
+ settings: {
199
+ preset: detectedPreset,
200
+ grid_columns: gridColumns,
201
+ col_gap: 20,
202
+ row_gap: 20,
203
+ spacing_top: "32",
204
+ spacing_bottom: "32",
205
+ },
206
+ };
207
+
208
+ const newRows = [...rows];
209
+ if (afterRowKey) {
210
+ const idx = newRows.findIndex((r) => r._key === afterRowKey);
211
+ if (idx !== -1) {
212
+ newRows.splice(idx + 1, 0, newSection);
213
+ } else {
214
+ newRows.push(newSection);
215
+ }
216
+ } else {
217
+ newRows.push(newSection);
218
+ }
219
+
220
+ return { rows: newRows, newSectionKey: newSection._key };
221
+ }
222
+
223
+ // ============================================
224
+ // moveColumnV2InState
225
+ // ============================================
226
+
227
+ export interface MoveColumnV2Result {
228
+ rows: ContentItem[];
229
+ }
230
+
231
+ /**
232
+ * Move a V2 column to a new grid position within its section.
233
+ * Returns null if the section is not found.
234
+ *
235
+ * Pure function — no store access.
236
+ */
237
+ export function moveColumnV2InState(
238
+ rows: ContentItem[],
239
+ sectionKey: string,
240
+ columnKey: string,
241
+ targetRow: number,
242
+ targetColumn: number
243
+ ): MoveColumnV2Result | null {
244
+ const path = findSectionPath(rows, sectionKey);
245
+ if (!path) return null;
246
+
247
+ const newRows = updateSectionAtPath(rows, path, (section) => {
248
+ const updatedCascade = cascadeMoveColumn(
249
+ toCascadeColumns(section.columns),
250
+ columnKey,
251
+ targetRow,
252
+ targetColumn,
253
+ section.settings.grid_columns
254
+ );
255
+ const updatedColumns = applyBlocksToColumns(updatedCascade, section.columns);
256
+ const newPreset = detectPreset(updatedCascade, section.settings.grid_columns);
257
+ return {
258
+ ...section,
259
+ columns: updatedColumns,
260
+ settings: { ...section.settings, preset: newPreset },
261
+ };
262
+ });
263
+
264
+ return { rows: newRows };
265
+ }
266
+
267
+ // -------------------------------------------------------
268
+ // Swap two columns in a V2 section (exchange positions)
269
+ // -------------------------------------------------------
270
+
271
+ export interface SwapColumnV2Result {
272
+ rows: ContentItem[];
273
+ }
274
+
275
+ export function swapColumnV2InState(
276
+ rows: ContentItem[],
277
+ sectionKey: string,
278
+ draggedKey: string,
279
+ targetKey: string
280
+ ): SwapColumnV2Result | null {
281
+ const path = findSectionPath(rows, sectionKey);
282
+ if (!path) return null;
283
+
284
+ const newRows = updateSectionAtPath(rows, path, (section) => {
285
+ const draggedCol = section.columns.find((c) => c._key === draggedKey);
286
+ const targetCol = section.columns.find((c) => c._key === targetKey);
287
+ if (!draggedCol || !targetCol) return section;
288
+
289
+ const updatedColumns = section.columns.map((c) => {
290
+ if (c._key === draggedKey) {
291
+ return { ...c, grid_column: targetCol.grid_column, grid_row: targetCol.grid_row, span: targetCol.span };
292
+ }
293
+ if (c._key === targetKey) {
294
+ return { ...c, grid_column: draggedCol.grid_column, grid_row: draggedCol.grid_row, span: draggedCol.span };
295
+ }
296
+ return c;
297
+ });
298
+
299
+ const cascadeCols = updatedColumns.map((c) => ({
300
+ _key: c._key, grid_column: c.grid_column, grid_row: c.grid_row, span: c.span,
301
+ }));
302
+ const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
303
+
304
+ return { ...section, columns: updatedColumns, settings: { ...section.settings, preset: newPreset } };
305
+ });
306
+
307
+ return { rows: newRows };
308
+ }
309
+
310
+ // -------------------------------------------------------
311
+ // Move a column to an empty gap, adopting the gap's span
312
+ // -------------------------------------------------------
313
+
314
+ export interface MoveColumnToGapV2Result {
315
+ rows: ContentItem[];
316
+ }
317
+
318
+ export function moveColumnToGapV2InState(
319
+ rows: ContentItem[],
320
+ sectionKey: string,
321
+ columnKey: string,
322
+ targetRow: number,
323
+ targetColumn: number,
324
+ targetSpan: number
325
+ ): MoveColumnToGapV2Result | null {
326
+ const path = findSectionPath(rows, sectionKey);
327
+ if (!path) return null;
328
+
329
+ const newRows = updateSectionAtPath(rows, path, (section) => {
330
+ const col = section.columns.find((c) => c._key === columnKey);
331
+ if (!col) return section;
332
+
333
+ const updatedColumns = section.columns.map((c) => {
334
+ if (c._key !== columnKey) return c;
335
+ return { ...c, grid_column: targetColumn, grid_row: targetRow, span: targetSpan };
336
+ });
337
+
338
+ const cascadeCols = updatedColumns.map((c) => ({
339
+ _key: c._key, grid_column: c.grid_column, grid_row: c.grid_row, span: c.span,
340
+ }));
341
+ const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
342
+
343
+ return { ...section, columns: updatedColumns, settings: { ...section.settings, preset: newPreset } };
344
+ });
345
+
346
+ return { rows: newRows };
347
+ }
348
+
349
+ // ============================================
350
+ // findSectionPath — unified section lookup (V2 + ParallaxSlide)
351
+ // ============================================
352
+
353
+ export type SectionPath =
354
+ | { type: "v2"; index: number }
355
+ | { type: "parallaxSlide"; groupIndex: number; slideIndex: number };
356
+
357
+ /**
358
+ * Find a V2 section by key — searches both top-level PageSectionV2
359
+ * and inside ParallaxGroup slides. Returns the path or null.
360
+ *
361
+ * This enables existing V2 actions (addColumnV2, addBlockV2, etc.)
362
+ * to work on parallax slides without signature changes — slides
363
+ * have unique _key values just like sections.
364
+ */
365
+ export function findSectionPath(
366
+ rows: ContentItem[],
367
+ sectionKey: string
368
+ ): SectionPath | null {
369
+ for (let i = 0; i < rows.length; i++) {
370
+ const item = rows[i];
371
+ if (item._key === sectionKey && isPageSectionV2(item)) {
372
+ return { type: "v2", index: i };
373
+ }
374
+ if (isParallaxGroup(item)) {
375
+ const group = item as ParallaxGroup;
376
+ for (let j = 0; j < group.slides.length; j++) {
377
+ if (group.slides[j]._key === sectionKey) {
378
+ return { type: "parallaxSlide", groupIndex: i, slideIndex: j };
379
+ }
380
+ }
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+
386
+ /**
387
+ * Get a PageSectionV2 from any section path.
388
+ * For parallax slides, returns a virtual PageSectionV2 view of the slide.
389
+ */
390
+ export function getSectionFromPath(
391
+ rows: ContentItem[],
392
+ path: SectionPath
393
+ ): PageSectionV2 | null {
394
+ if (path.type === "v2") {
395
+ const item = rows[path.index];
396
+ return isPageSectionV2(item) ? (item as PageSectionV2) : null;
397
+ }
398
+ const group = rows[path.groupIndex];
399
+ if (!isParallaxGroup(group)) return null;
400
+ const slide = (group as ParallaxGroup).slides[path.slideIndex];
401
+ if (!slide) return null;
402
+ return {
403
+ _type: "pageSectionV2",
404
+ _key: slide._key,
405
+ section_type: "empty-v2",
406
+ columns: slide.columns,
407
+ settings: slide.section_settings,
408
+ };
409
+ }
410
+
411
+ /**
412
+ * Apply an updater function to a section at a given path.
413
+ * For parallax slides, updates the slide in place within the group.
414
+ */
415
+ export function updateSectionAtPath(
416
+ rows: ContentItem[],
417
+ path: SectionPath,
418
+ updater: (section: PageSectionV2) => PageSectionV2
419
+ ): ContentItem[] {
420
+ if (path.type === "v2") {
421
+ return rows.map((item, i) => {
422
+ if (i !== path.index || !isPageSectionV2(item)) return item;
423
+ return updater(item as PageSectionV2) as ContentItem;
424
+ });
425
+ }
426
+ // parallaxSlide path
427
+ return rows.map((item, i) => {
428
+ if (i !== path.groupIndex || !isParallaxGroup(item)) return item;
429
+ const group = item as ParallaxGroup;
430
+ const newSlides = group.slides.map((slide, j) => {
431
+ if (j !== path.slideIndex) return slide;
432
+ const virtualSection: PageSectionV2 = {
433
+ _type: "pageSectionV2",
434
+ _key: slide._key,
435
+ section_type: "empty-v2",
436
+ columns: slide.columns,
437
+ settings: slide.section_settings,
438
+ };
439
+ const updated = updater(virtualSection);
440
+ return {
441
+ ...slide,
442
+ columns: updated.columns,
443
+ section_settings: updated.settings,
444
+ };
445
+ });
446
+ return { ...group, slides: newSlides } as ContentItem;
447
+ });
448
+ }
449
+
450
+ // ============================================
451
+ // addParallaxGroupInState
452
+ // ============================================
453
+
454
+ export interface AddParallaxGroupResult {
455
+ rows: ContentItem[];
456
+ newGroupKey: string;
457
+ }
458
+
459
+ /**
460
+ * Create a new ParallaxGroup with 1 empty slide and insert it into the rows.
461
+ */
462
+ export function addParallaxGroupInState(
463
+ rows: ContentItem[],
464
+ afterRowKey?: string | null
465
+ ): AddParallaxGroupResult {
466
+ const defaultSlide: ParallaxSlideV2 = createDefaultParallaxSlide();
467
+
468
+ const newGroup: ParallaxGroup = {
469
+ _type: "parallaxGroup",
470
+ _key: generateKey(),
471
+ slides: [defaultSlide],
472
+ transition_effect: "parallax",
473
+ snap_enabled: true,
474
+ parallax_intensity: 0.4,
475
+ };
476
+
477
+ const newRows = [...rows];
478
+ if (afterRowKey) {
479
+ const idx = newRows.findIndex((r) => r._key === afterRowKey);
480
+ if (idx !== -1) {
481
+ newRows.splice(idx + 1, 0, newGroup);
482
+ } else {
483
+ newRows.push(newGroup);
484
+ }
485
+ } else {
486
+ newRows.push(newGroup);
487
+ }
488
+
489
+ return { rows: newRows, newGroupKey: newGroup._key };
490
+ }