@morphika/andami 0.1.2

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 +50 -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 +212 -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,533 @@
1
+ /**
2
+ * Push Cascade Engine — V2 Grid System
3
+ *
4
+ * The central algorithm driving resize, drag & drop, and add column.
5
+ * A single, reusable pure function engine.
6
+ *
7
+ * Rules:
8
+ * 1. Horizontal push within a row: When column A grows or is inserted,
9
+ * everything to its right shifts rightward.
10
+ * 2. Overflow → drop to next row: If a column exceeds grid_columns,
11
+ * it moves to the next row at position 1, keeping its span.
12
+ * 3. Cascade continues: If the dropped column collides with columns in
13
+ * the destination row, horizontal push applies again.
14
+ * 4. Always terminates: In the worst case, each column occupies its own row.
15
+ * 5. Column constraints: Span 1–grid_columns (integer). Grid positions are integers.
16
+ * 6. Atomic undo: The entire cascade is a single history entry.
17
+ *
18
+ * Session 83: V2 Grid System — Phase 1.
19
+ */
20
+
21
+ import type { SectionColumn } from "../../lib/sanity/types";
22
+ import { logger } from "../../lib/logger";
23
+
24
+ // ============================================
25
+ // Types
26
+ // ============================================
27
+
28
+ /** Minimal column representation for cascade computation */
29
+ export interface CascadeColumn {
30
+ _key: string;
31
+ grid_column: number; // 1-based
32
+ grid_row: number; // 1-based
33
+ span: number; // 1–gridColumns
34
+ }
35
+
36
+ // ============================================
37
+ // Core Cascade
38
+ // ============================================
39
+
40
+ /**
41
+ * Compute the push cascade for a flat list of columns within a section.
42
+ *
43
+ * Given a set of columns (potentially with overlapping or overflowing positions),
44
+ * resolves all conflicts by pushing columns rightward and dropping to new rows
45
+ * when they overflow.
46
+ *
47
+ * @param columns - The columns to resolve (will NOT be mutated)
48
+ * @param gridColumns - Total columns in the grid (default 12)
49
+ * @returns A new array of columns with valid, non-overlapping positions
50
+ */
51
+ export function computeCascade(
52
+ columns: CascadeColumn[],
53
+ gridColumns: number = 12
54
+ ): CascadeColumn[] {
55
+ if (columns.length === 0) return [];
56
+
57
+ // Defensive validation — warn on invalid input but don't throw (self-healing)
58
+ if (gridColumns < 1 || !Number.isInteger(gridColumns)) {
59
+ logger.warn("[Cascade]", "Invalid gridColumns, defaulting to 12", { gridColumns });
60
+ gridColumns = 12;
61
+ }
62
+
63
+ // Deep clone to avoid mutation
64
+ const cols = columns.map((c) => {
65
+ // Clamp and warn on out-of-range values
66
+ const span = Math.max(1, Math.min(Math.round(c.span), gridColumns));
67
+ const grid_column = Math.max(1, Math.round(c.grid_column));
68
+ const grid_row = Math.max(1, Math.round(c.grid_row));
69
+
70
+ if (span !== c.span || grid_column !== c.grid_column || grid_row !== c.grid_row) {
71
+ logger.warn("[Cascade]", "Clamped invalid column values", {
72
+ key: c._key,
73
+ original: { span: c.span, grid_column: c.grid_column, grid_row: c.grid_row },
74
+ clamped: { span, grid_column, grid_row },
75
+ });
76
+ }
77
+
78
+ return { ...c, span, grid_column, grid_row };
79
+ });
80
+
81
+ // Convergence loop: resolve overlaps and overflows across all rows
82
+ // until no more changes occur. Handles cascading effects where
83
+ // pushing a column to a new row creates new conflicts.
84
+ const maxIterations = cols.length * gridColumns; // Safety limit
85
+ let stable = false;
86
+ let passes = 0;
87
+ while (!stable && passes < maxIterations) {
88
+ passes++;
89
+ stable = true;
90
+ const allRows = [...new Set(cols.map((c) => c.grid_row))].sort((a, b) => a - b);
91
+
92
+ for (const row of allRows) {
93
+ const rowCols = cols
94
+ .filter((c) => c.grid_row === row)
95
+ .sort((a, b) => a.grid_column - b.grid_column);
96
+
97
+ if (rowCols.length <= 1) continue;
98
+
99
+ // Resolve overlaps
100
+ const hadOverlap = resolveRowOverlaps(rowCols);
101
+ if (hadOverlap) stable = false;
102
+
103
+ // Check for overflows
104
+ for (const col of rowCols) {
105
+ if (col.grid_column + col.span - 1 > gridColumns) {
106
+ col.grid_row = row + 1;
107
+ col.grid_column = 1;
108
+ stable = false;
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ if (!stable) {
115
+ logger.warn("[Cascade]", "Convergence limit reached — layout may have unresolved overlaps", {
116
+ passes,
117
+ maxIterations,
118
+ columnCount: cols.length,
119
+ });
120
+ }
121
+
122
+ return cols;
123
+ }
124
+
125
+ /**
126
+ * Resolve overlaps within a single row by pushing columns rightward.
127
+ * Columns must be sorted by grid_column (ascending).
128
+ * Mutates the columns in place.
129
+ *
130
+ * @returns true if any columns were moved
131
+ */
132
+ function resolveRowOverlaps(rowCols: CascadeColumn[]): boolean {
133
+ let moved = false;
134
+
135
+ for (let i = 1; i < rowCols.length; i++) {
136
+ const prev = rowCols[i - 1];
137
+ const curr = rowCols[i];
138
+ const prevEnd = prev.grid_column + prev.span; // 1 past the end of prev
139
+
140
+ if (curr.grid_column < prevEnd) {
141
+ // Overlap — push current to just after prev
142
+ curr.grid_column = prevEnd;
143
+ moved = true;
144
+ }
145
+ }
146
+
147
+ return moved;
148
+ }
149
+
150
+ /**
151
+ * Get the maximum row number across all columns.
152
+ */
153
+ function getMaxRow(cols: CascadeColumn[]): number {
154
+ if (cols.length === 0) return 0;
155
+ return Math.max(...cols.map((c) => c.grid_row));
156
+ }
157
+
158
+ // ============================================
159
+ // Preset Detection
160
+ // ============================================
161
+
162
+ /**
163
+ * Detect which preset a set of columns matches, if any.
164
+ * Returns "custom" if no preset matches.
165
+ */
166
+ export function detectPreset(
167
+ columns: CascadeColumn[],
168
+ gridColumns: number = 12
169
+ ): "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3" | "custom" {
170
+ // All columns must be on row 1 for a preset match
171
+ if (columns.length === 0) return "full";
172
+ if (columns.some((c) => c.grid_row !== 1)) return "custom";
173
+
174
+ const sorted = [...columns].sort((a, b) => a.grid_column - b.grid_column);
175
+ const spans = sorted.map((c) => c.span);
176
+
177
+ // Check contiguous placement (no gaps)
178
+ let expectedCol = 1;
179
+ for (const col of sorted) {
180
+ if (col.grid_column !== expectedCol) return "custom";
181
+ expectedCol = col.grid_column + col.span;
182
+ }
183
+ // Must fill exactly gridColumns
184
+ if (expectedCol - 1 !== gridColumns) return "custom";
185
+
186
+ // Match against known presets
187
+ const key = spans.join(",");
188
+
189
+ // For 12-column grid, specific common presets
190
+ if (gridColumns === 12) {
191
+ if (key === "12") return "full";
192
+ if (key === "6,6") return "halves";
193
+ if (key === "4,4,4") return "thirds";
194
+ if (key === "3,3,3,3") return "quarters";
195
+ if (key === "4,8") return "1/3+2/3";
196
+ if (key === "8,4") return "2/3+1/3";
197
+ } else {
198
+ // Generic preset detection for non-12 grids
199
+ if (spans.length === 1 && spans[0] === gridColumns) return "full";
200
+ if (spans.length === 2 && spans[0] === spans[1] && spans[0] * 2 === gridColumns) return "halves";
201
+ if (spans.length === 3 && spans.every((s) => s === spans[0]) && spans[0] * 3 === gridColumns) return "thirds";
202
+ if (spans.length === 4 && spans.every((s) => s === spans[0]) && spans[0] * 4 === gridColumns) return "quarters";
203
+ }
204
+
205
+ return "custom";
206
+ }
207
+
208
+ // ============================================
209
+ // Column Operations (use cascade internally)
210
+ // ============================================
211
+
212
+ /**
213
+ * Resize a column to a new span, applying push cascade.
214
+ *
215
+ * @param columns - All columns in the section
216
+ * @param columnKey - The column to resize
217
+ * @param newSpan - The desired new span
218
+ * @param gridColumns - Total columns in the grid
219
+ * @returns Updated columns after cascade
220
+ */
221
+ export function resizeColumn(
222
+ columns: CascadeColumn[],
223
+ columnKey: string,
224
+ newSpan: number,
225
+ gridColumns: number = 12
226
+ ): CascadeColumn[] {
227
+ if (!columns.find((c) => c._key === columnKey)) {
228
+ logger.warn("[Cascade]", "resizeColumn: column not found", { columnKey });
229
+ return columns.map((c) => ({ ...c }));
230
+ }
231
+ const clampedSpan = Math.max(1, Math.min(newSpan, gridColumns));
232
+ const updated = columns.map((c) =>
233
+ c._key === columnKey ? { ...c, span: clampedSpan } : { ...c }
234
+ );
235
+ return computeCascade(updated, gridColumns);
236
+ }
237
+
238
+ /**
239
+ * Resize a column from the left edge (stretch left).
240
+ * Moves the column's start position leftward and increases span.
241
+ *
242
+ * When expanding left into a neighbor, the neighbor is compressed
243
+ * (its span shrinks to make room) down to a minimum of 1 column.
244
+ * Only the immediate left neighbor is affected — no recursive cascade.
245
+ *
246
+ * Returns null only when truly impossible (column not found, or the
247
+ * neighbor is already at span 1 and can't shrink further).
248
+ *
249
+ * The `neighborAtMinimum` flag in the result signals that the left
250
+ * neighbor has been compressed to its minimum span (1), so the UI
251
+ * can show a visual indicator that further expansion is blocked.
252
+ *
253
+ * @param columns - All columns in the section
254
+ * @param columnKey - The column to resize
255
+ * @param newGridColumn - The desired new start position
256
+ * @param gridColumns - Total columns in the grid
257
+ * @returns Updated columns + metadata, or null if completely blocked
258
+ */
259
+ export interface ResizeLeftResult {
260
+ columns: CascadeColumn[];
261
+ /** Key of the left neighbor that was compressed, if any */
262
+ compressedNeighborKey: string | null;
263
+ /** True when the left neighbor is at minimum span (1) — further expansion blocked */
264
+ neighborAtMinimum: boolean;
265
+ }
266
+
267
+ export function resizeColumnLeft(
268
+ columns: CascadeColumn[],
269
+ columnKey: string,
270
+ newGridColumn: number,
271
+ gridColumns: number = 12
272
+ ): ResizeLeftResult | null {
273
+ const col = columns.find((c) => c._key === columnKey);
274
+ if (!col) return null;
275
+
276
+ // The right edge stays fixed: rightEdge = col.grid_column + col.span - 1
277
+ const rightEdge = col.grid_column + col.span - 1;
278
+
279
+ // Clamp: can't go past position 1 on the left, and can't go past
280
+ // rightEdge on the right (minimum span of 1)
281
+ let clampedStart = Math.max(1, Math.min(newGridColumn, rightEdge));
282
+
283
+ // Find the column immediately to the left in the same row
284
+ const sameRowCols = columns
285
+ .filter((c) => c.grid_row === col.grid_row && c._key !== columnKey)
286
+ .sort((a, b) => a.grid_column - b.grid_column);
287
+
288
+ const leftNeighbor = sameRowCols
289
+ .filter((c) => c.grid_column + c.span - 1 < col.grid_column)
290
+ .pop(); // rightmost column that ends before us
291
+
292
+ let compressedNeighborKey: string | null = null;
293
+ let neighborAtMinimum = false;
294
+ let neighborNewSpan: number | null = null;
295
+
296
+ if (leftNeighbor) {
297
+ // Hard floor: neighbor can't shrink below span 1
298
+ const neighborAbsoluteMin = leftNeighbor.grid_column + 1; // neighbor at span 1 ends here
299
+ const freeStart = leftNeighbor.grid_column + leftNeighbor.span; // current free position
300
+
301
+ if (clampedStart < freeStart) {
302
+ // We're pushing into the neighbor — compress it
303
+ // The neighbor's new span = clampedStart - neighbor.grid_column
304
+ const desiredNeighborSpan = clampedStart - leftNeighbor.grid_column;
305
+
306
+ if (desiredNeighborSpan < 1) {
307
+ // Can't compress further — clamp our start to neighbor's absolute minimum
308
+ clampedStart = neighborAbsoluteMin;
309
+ neighborNewSpan = 1;
310
+ neighborAtMinimum = true;
311
+ } else {
312
+ neighborNewSpan = desiredNeighborSpan;
313
+ neighborAtMinimum = desiredNeighborSpan === 1;
314
+ }
315
+
316
+ compressedNeighborKey = leftNeighbor._key;
317
+ }
318
+ }
319
+
320
+ // Compute new span from the fixed right edge
321
+ const newSpan = rightEdge - clampedStart + 1;
322
+
323
+ if (newSpan < 1 || newSpan > gridColumns) return null;
324
+
325
+ // If nothing changed (same position as current), return null
326
+ if (clampedStart === col.grid_column && !compressedNeighborKey) return null;
327
+
328
+ const updated = columns.map((c) => {
329
+ if (c._key === columnKey) {
330
+ return { ...c, grid_column: clampedStart, span: newSpan };
331
+ }
332
+ if (c._key === compressedNeighborKey && neighborNewSpan !== null) {
333
+ return { ...c, span: neighborNewSpan };
334
+ }
335
+ return { ...c };
336
+ });
337
+
338
+ return { columns: updated, compressedNeighborKey, neighborAtMinimum };
339
+ }
340
+
341
+ /**
342
+ * Add a new column at a specific gap position.
343
+ * The column fills the available gap space.
344
+ *
345
+ * @param columns - All columns in the section
346
+ * @param gridRow - The row to add the column in
347
+ * @param gridColumn - The start position for the new column
348
+ * @param span - The span for the new column
349
+ * @param columnKey - The _key for the new column
350
+ * @param gridColumns - Total columns in the grid
351
+ * @returns Updated columns with the new column added, after cascade
352
+ */
353
+ export function addColumn(
354
+ columns: CascadeColumn[],
355
+ gridRow: number,
356
+ gridColumn: number,
357
+ span: number,
358
+ columnKey: string,
359
+ gridColumns: number = 12
360
+ ): CascadeColumn[] {
361
+ const newCol: CascadeColumn = {
362
+ _key: columnKey,
363
+ grid_column: gridColumn,
364
+ grid_row: gridRow,
365
+ span: Math.max(1, Math.min(span, gridColumns)),
366
+ };
367
+
368
+ const updated = [...columns.map((c) => ({ ...c })), newCol];
369
+ return computeCascade(updated, gridColumns);
370
+ }
371
+
372
+ /**
373
+ * Move a column to a new position (drag & drop).
374
+ *
375
+ * @param columns - All columns in the section
376
+ * @param columnKey - The column to move
377
+ * @param targetRow - The destination row
378
+ * @param targetColumn - The destination grid column
379
+ * @param gridColumns - Total columns in the grid
380
+ * @returns Updated columns after cascade
381
+ */
382
+ export function moveColumn(
383
+ columns: CascadeColumn[],
384
+ columnKey: string,
385
+ targetRow: number,
386
+ targetColumn: number,
387
+ gridColumns: number = 12
388
+ ): CascadeColumn[] {
389
+ if (!columns.find((c) => c._key === columnKey)) {
390
+ logger.warn("[Cascade]", "moveColumn: column not found", { columnKey });
391
+ return columns.map((c) => ({ ...c }));
392
+ }
393
+ const updated = columns.map((c) =>
394
+ c._key === columnKey
395
+ ? { ...c, grid_row: targetRow, grid_column: Math.max(1, Math.min(targetColumn, gridColumns)) }
396
+ : { ...c }
397
+ );
398
+ return computeCascade(updated, gridColumns);
399
+ }
400
+
401
+ /**
402
+ * Delete a column. Returns the remaining columns (no cascade needed
403
+ * since deletion only creates empty space, and per the spec,
404
+ * remaining columns do NOT collapse into the gap).
405
+ *
406
+ * @param columns - All columns in the section
407
+ * @param columnKey - The column to delete
408
+ * @returns Updated columns with the column removed
409
+ */
410
+ export function deleteColumn(
411
+ columns: CascadeColumn[],
412
+ columnKey: string
413
+ ): CascadeColumn[] {
414
+ return columns.filter((c) => c._key !== columnKey).map((c) => ({ ...c }));
415
+ }
416
+
417
+ // ============================================
418
+ // Gap Detection (for + Add Column buttons)
419
+ // ============================================
420
+
421
+ export interface GridGap {
422
+ grid_row: number;
423
+ grid_column: number; // start position of the gap
424
+ span: number; // width of the gap in grid columns
425
+ }
426
+
427
+ /**
428
+ * Find all empty gaps in the grid where "+ Add Column" buttons should appear.
429
+ *
430
+ * @param columns - All columns in the section
431
+ * @param gridColumns - Total columns in the grid
432
+ * @returns Array of gaps sorted by row then column
433
+ */
434
+ export function findGaps(
435
+ columns: CascadeColumn[],
436
+ gridColumns: number = 12
437
+ ): GridGap[] {
438
+ if (columns.length === 0) {
439
+ // Entire grid is one big gap
440
+ return [{ grid_row: 1, grid_column: 1, span: gridColumns }];
441
+ }
442
+
443
+ const gaps: GridGap[] = [];
444
+ const maxRow = getMaxRow(columns);
445
+
446
+ for (let row = 1; row <= maxRow; row++) {
447
+ const rowCols = columns
448
+ .filter((c) => c.grid_row === row)
449
+ .sort((a, b) => a.grid_column - b.grid_column);
450
+
451
+ if (rowCols.length === 0) {
452
+ // Empty row — entire row is a gap
453
+ gaps.push({ grid_row: row, grid_column: 1, span: gridColumns });
454
+ continue;
455
+ }
456
+
457
+ // Check gap before first column
458
+ if (rowCols[0].grid_column > 1) {
459
+ gaps.push({
460
+ grid_row: row,
461
+ grid_column: 1,
462
+ span: rowCols[0].grid_column - 1,
463
+ });
464
+ }
465
+
466
+ // Check gaps between columns
467
+ for (let i = 0; i < rowCols.length - 1; i++) {
468
+ const endOfCurrent = rowCols[i].grid_column + rowCols[i].span;
469
+ const startOfNext = rowCols[i + 1].grid_column;
470
+ if (startOfNext > endOfCurrent) {
471
+ gaps.push({
472
+ grid_row: row,
473
+ grid_column: endOfCurrent,
474
+ span: startOfNext - endOfCurrent,
475
+ });
476
+ }
477
+ }
478
+
479
+ // Check gap after last column
480
+ const lastCol = rowCols[rowCols.length - 1];
481
+ const endOfLast = lastCol.grid_column + lastCol.span;
482
+ if (endOfLast <= gridColumns) {
483
+ gaps.push({
484
+ grid_row: row,
485
+ grid_column: endOfLast,
486
+ span: gridColumns - endOfLast + 1,
487
+ });
488
+ }
489
+ }
490
+
491
+ return gaps;
492
+ }
493
+
494
+ // ============================================
495
+ // Preset → Columns factory
496
+ // ============================================
497
+
498
+ /**
499
+ * Create columns from a preset layout.
500
+ *
501
+ * @param preset - The layout preset
502
+ * @param gridColumns - Total columns in the grid (default 12)
503
+ * @param keyFactory - Function to generate unique keys
504
+ * @returns Array of columns matching the preset layout
505
+ */
506
+ export function columnsFromPreset(
507
+ preset: "full" | "halves" | "thirds" | "quarters" | "1/3+2/3" | "2/3+1/3",
508
+ gridColumns: number = 12,
509
+ keyFactory: () => string = () => Math.random().toString(36).substring(2, 14)
510
+ ): CascadeColumn[] {
511
+ const presetSpans: Record<string, number[]> = {
512
+ full: [gridColumns],
513
+ halves: [gridColumns / 2, gridColumns / 2],
514
+ thirds: [gridColumns / 3, gridColumns / 3, gridColumns / 3],
515
+ quarters: [gridColumns / 4, gridColumns / 4, gridColumns / 4, gridColumns / 4],
516
+ "1/3+2/3": [Math.round(gridColumns / 3), gridColumns - Math.round(gridColumns / 3)],
517
+ "2/3+1/3": [gridColumns - Math.round(gridColumns / 3), Math.round(gridColumns / 3)],
518
+ };
519
+
520
+ const spans = presetSpans[preset] || [gridColumns];
521
+ let currentCol = 1;
522
+
523
+ return spans.map((span) => {
524
+ const col: CascadeColumn = {
525
+ _key: keyFactory(),
526
+ grid_column: currentCol,
527
+ grid_row: 1,
528
+ span: Math.round(span),
529
+ };
530
+ currentCol += Math.round(span);
531
+ return col;
532
+ });
533
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared constants for the builder and public site renderers.
3
+ * Consolidates duplicated maps and utility functions.
4
+ */
5
+
6
+
7
+ // ============================================
8
+ // Responsive Breakpoints
9
+ // ============================================
10
+ // Single source of truth for all media query breakpoints.
11
+ // Import these instead of hardcoding numeric values.
12
+
13
+ export const BREAKPOINTS = {
14
+ /** Phone max-width breakpoint (640px) */
15
+ phone: 640,
16
+ /** Scroll animation disable breakpoint (768px) */
17
+ mobileAnimation: 768,
18
+ /** Tablet max-width breakpoint (1024px) */
19
+ tablet: 1024,
20
+ } as const;
21
+
22
+ // ============================================
23
+ // Default Grid Width
24
+ // ============================================
25
+ // The default page grid max-width in pixels.
26
+
27
+ export const DEFAULT_GRID_WIDTH = "1445";
28
+ export const DEFAULT_GRID_WIDTH_PX = `${DEFAULT_GRID_WIDTH}px`;
29
+
30
+ // ============================================
31
+ // Default Page Colors
32
+ // ============================================
33
+ // Default background and text colors for new pages.
34
+
35
+ export const DEFAULT_BG_COLOR = "#ffffff";
36
+ export const DEFAULT_TEXT_COLOR = "#000000";
37
+
38
+ // ============================================
39
+ // Admin UI Colors
40
+ // ============================================
41
+ // Accent colors used throughout the admin/builder interface.
42
+ // These are also available as CSS custom properties:
43
+ // --admin-accent, --admin-accent-dark
44
+
45
+ export const ADMIN_ACCENT = "#076bff";
46
+ export const ADMIN_ACCENT_DARK = "#0559d4";
47
+ export const ADMIN_ACCENT_SHADOW = "rgba(7,107,255,0.06)";
48
+ export const ADMIN_ERROR = "#ed3821";
49
+ export const ADMIN_ERROR_DARK = "#d42f1a";
50
+
51
+ // ============================================
52
+ // Builder Semantic Color System
53
+ // ============================================
54
+ // Each UI entity in the builder has a dedicated color to provide
55
+ // visual differentiation at a glance. This is a design rule that
56
+ // MUST be followed across all builder components.
57
+ //
58
+ // BLUE (#076bff) — Columns: outlines, resize handles, drag grip,
59
+ // span badge, column selection/hover chrome.
60
+ // ORANGE (#e28b00) — Blocks: "+ Add Block" buttons, block toolbar
61
+ // pill, block selection ring, block-level actions.
62
+ // GREEN (#22c55e) — Drop zones: gap drop targets during drag,
63
+ // insertion lines, "Drop Here" labels, swap target
64
+ // highlight (green border + tinted background).
65
+ // VIOLET (#8b5cf6) — Custom sections: saved section cards, custom
66
+ // section instance badges, section editor chrome,
67
+ // "Create New" custom section button.
68
+ //
69
+ // When adding new builder UI, pick the color that matches the entity
70
+ // being represented, not the action being performed. For example, a
71
+ // delete button on a column is BLUE (it belongs to the column chrome),
72
+ // while a delete button on a block toolbar is ORANGE.
73
+
74
+ export const BUILDER_BLUE = "#076bff"; // Columns
75
+ export const BUILDER_ORANGE = "#e28b00"; // Blocks
76
+ export const BUILDER_GREEN = "#22c55e"; // Drop zones
77
+ export const BUILDER_VIOLET = "#8b5cf6"; // Custom sections
78
+
79
+ /**
80
+ * Padding map for Row settings (in pixels)
81
+ * Used by builder (SortableRow, ReadOnlyFrame) and public site (RowRenderer)
82
+ */
83
+ export const PADDING_MAP: Record<string, string> = {
84
+ none: "0",
85
+ small: "16px",
86
+ medium: "32px",
87
+ large: "64px",
88
+ xlarge: "96px",
89
+ };
90
+
91
+ /**
92
+ * Padding map for Row settings (in Tailwind classes)
93
+ * Used only by public site (RowRenderer) when using class-based padding
94
+ */
95
+ export const PADDING_CLASSES_MAP: Record<string, string> = {
96
+ none: "py-0",
97
+ small: "py-4",
98
+ medium: "py-8",
99
+ large: "py-16",
100
+ xlarge: "py-24",
101
+ };
102
+
103
+