@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,166 @@
1
+ // ============================================
2
+ // Masonry Engine — Pure JS, zero React deps
3
+ // ============================================
4
+ // Computes absolute card positions for the Project Grid v2.
5
+ // Used by both the builder preview and the public renderer
6
+ // to guarantee pixel-perfect parity.
7
+
8
+ // ─── Interfaces ───
9
+
10
+ export interface MasonryItem {
11
+ key: string;
12
+ aspectRatio: number; // width / height, e.g. 16/9 ≈ 1.778, 1/1 = 1, 9/16 ≈ 0.5625
13
+ }
14
+
15
+ export interface MasonryConfig {
16
+ columns: number; // 1–6
17
+ gapH: number; // horizontal gap in px
18
+ gapV: number; // vertical gap in px
19
+ containerWidth: number; // px (measured at runtime)
20
+ }
21
+
22
+ export interface MasonryResult {
23
+ key: string;
24
+ x: number; // px from left
25
+ y: number; // px from top
26
+ width: number; // px
27
+ height: number; // px
28
+ column: number; // 0-based column index
29
+ }
30
+
31
+ export interface MasonryOutput {
32
+ items: MasonryResult[];
33
+ totalHeight: number; // px — height of tallest column (for container sizing)
34
+ }
35
+
36
+ export interface RatioResolverConfig {
37
+ gridRatios: string[]; // e.g. ["16/9", "1/1"]
38
+ seed?: number; // optional deterministic seed
39
+ }
40
+
41
+ // ─── Helpers ───
42
+
43
+ /**
44
+ * Parse a ratio string like "16/9" into a numeric value (width / height).
45
+ * Returns 1 if the string is malformed.
46
+ */
47
+ export function parseRatio(ratio: string): number {
48
+ const parts = ratio.split("/");
49
+ if (parts.length !== 2) return 1;
50
+ const w = Number(parts[0]);
51
+ const h = Number(parts[1]);
52
+ if (!w || !h || !isFinite(w) || !isFinite(h) || h === 0) return 1;
53
+ return w / h;
54
+ }
55
+
56
+ /**
57
+ * Returns the index of the shortest column. Tie-break: leftmost (lowest index).
58
+ */
59
+ export function getShortestColumn(columnHeights: number[]): number {
60
+ if (columnHeights.length === 0) return 0;
61
+ let minIdx = 0;
62
+ let minVal = columnHeights[0];
63
+ for (let i = 1; i < columnHeights.length; i++) {
64
+ if (columnHeights[i] < minVal) {
65
+ minVal = columnHeights[i];
66
+ minIdx = i;
67
+ }
68
+ }
69
+ return minIdx;
70
+ }
71
+
72
+ /**
73
+ * Resolve the aspect ratio for a single item.
74
+ *
75
+ * Priority: override > grid single ratio > deterministic random pick.
76
+ *
77
+ * The deterministic pick uses a simple hash:
78
+ * gridRatios[(index * 7 + (seed || 0)) % gridRatios.length]
79
+ * The * 7 prime multiplier avoids adjacent repeats.
80
+ */
81
+ export function resolveItemRatio(
82
+ index: number,
83
+ override: string | null | undefined,
84
+ config: RatioResolverConfig,
85
+ ): number {
86
+ // Per-card override wins
87
+ if (override) {
88
+ return parseRatio(override);
89
+ }
90
+
91
+ const { gridRatios, seed } = config;
92
+
93
+ // Fallback if empty array
94
+ if (!gridRatios || gridRatios.length === 0) return 1;
95
+
96
+ // Single ratio — no randomness needed
97
+ if (gridRatios.length === 1) {
98
+ return parseRatio(gridRatios[0]);
99
+ }
100
+
101
+ // Deterministic pseudo-random pick
102
+ const pick = Math.abs((index * 7 + (seed || 0)) % gridRatios.length);
103
+ return parseRatio(gridRatios[pick]);
104
+ }
105
+
106
+ // ─── Core Algorithm ───
107
+
108
+ /**
109
+ * Shortest-column masonry layout.
110
+ *
111
+ * 1. colWidth = (containerWidth - gapH * (columns - 1)) / columns
112
+ * 2. For each item → place in shortest column → compute x, y, height
113
+ * 3. totalHeight = max(columnHeights) - trailing gapV
114
+ *
115
+ * Edge cases:
116
+ * - containerWidth <= 0 → returns empty output
117
+ * - columns <= 0 → clamped to 1
118
+ * - empty items → returns { items: [], totalHeight: 0 }
119
+ */
120
+ export function computeMasonry(
121
+ items: MasonryItem[],
122
+ config: MasonryConfig,
123
+ ): MasonryOutput {
124
+ const { gapH, gapV, containerWidth } = config;
125
+ const columns = Math.max(1, Math.round(config.columns));
126
+
127
+ // Graceful handling of invalid container
128
+ if (containerWidth <= 0 || items.length === 0) {
129
+ return { items: [], totalHeight: 0 };
130
+ }
131
+
132
+ const colWidth =
133
+ (containerWidth - gapH * (columns - 1)) / columns;
134
+
135
+ // If column width is non-positive (e.g. gaps exceed container), bail
136
+ if (colWidth <= 0) {
137
+ return { items: [], totalHeight: 0 };
138
+ }
139
+
140
+ const columnHeights = new Array<number>(columns).fill(0);
141
+ const results: MasonryResult[] = [];
142
+
143
+ for (const item of items) {
144
+ const col = getShortestColumn(columnHeights);
145
+ const x = col * (colWidth + gapH);
146
+ const y = columnHeights[col];
147
+ const height = colWidth / (item.aspectRatio || 1); // guard div-by-zero
148
+
149
+ results.push({
150
+ key: item.key,
151
+ x,
152
+ y,
153
+ width: colWidth,
154
+ height,
155
+ column: col,
156
+ });
157
+
158
+ columnHeights[col] += height + gapV;
159
+ }
160
+
161
+ // totalHeight = tallest column minus trailing gap
162
+ const maxHeight = Math.max(...columnHeights);
163
+ const totalHeight = Math.max(0, maxHeight - gapV);
164
+
165
+ return { items: results, totalHeight };
166
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Responsive block resolution.
3
+ *
4
+ * The responsive system works with a simple override model:
5
+ *
6
+ * 1. Each block stores its BASE properties (= desktop defaults).
7
+ * 2. Each block has an optional `responsive` field:
8
+ * { tablet?: Partial<Props>, phone?: Partial<Props> }
9
+ * 3. At render time, `resolveBlock(block, viewport)` returns a merged
10
+ * copy: { ...base, ...overrides[viewport] }.
11
+ *
12
+ * Desktop always returns the block as-is (no merge needed).
13
+ * Tablet/phone only store what differs from desktop — everything else
14
+ * inherits automatically.
15
+ *
16
+ */
17
+
18
+ import type { ContentBlock } from "../../lib/sanity/types";
19
+ import type { DeviceViewport } from "./types";
20
+
21
+ /**
22
+ * Type-safe accessor for the `responsive` field on any ContentBlock.
23
+ * All block types in the union have `responsive?: ResponsiveOverrides<T>`,
24
+ * but TypeScript can't see the common property without narrowing.
25
+ */
26
+ type ResponsiveData = Record<string, Record<string, unknown>> | undefined;
27
+
28
+ function getResponsive(block: ContentBlock): ResponsiveData {
29
+ // All ContentBlock variants have a `responsive` field — safe to access via index
30
+ return (block as { responsive?: ResponsiveData }).responsive;
31
+ }
32
+
33
+ // ============================================
34
+ // Block resolution
35
+ // ============================================
36
+
37
+ /**
38
+ * Resolve a block for a given viewport by merging responsive overrides.
39
+ *
40
+ * - `desktop` → returns the block unchanged.
41
+ * - `tablet` / `phone` → shallow-merges the viewport overrides on top of
42
+ * the base block properties. Nested objects (like `style` on TextBlock)
43
+ * are deep-merged one level.
44
+ *
45
+ * The returned block always keeps the original `_type`, `_key`, and
46
+ * `responsive` fields intact.
47
+ */
48
+ export function resolveBlock<T extends ContentBlock>(
49
+ block: T,
50
+ viewport: DeviceViewport
51
+ ): T {
52
+ if (viewport === "desktop") return block;
53
+
54
+ const responsive = getResponsive(block);
55
+ if (!responsive) return block;
56
+
57
+ const overrides = responsive[viewport];
58
+ if (!overrides || Object.keys(overrides).length === 0) return block;
59
+
60
+ // Shallow merge with special handling for nested `style` object
61
+ const merged = { ...block } as T;
62
+ for (const [key, value] of Object.entries(overrides)) {
63
+ if (key === "responsive" || key === "_type" || key === "_key") continue;
64
+
65
+ // Deep-merge one-level nested objects (e.g. TextBlock.style, CoverBlock.cta_button)
66
+ const blockRecord = block as unknown as Record<string, unknown>;
67
+ const mergedRecord = merged as unknown as Record<string, unknown>;
68
+ if (
69
+ value !== null &&
70
+ typeof value === "object" &&
71
+ !Array.isArray(value) &&
72
+ key in block &&
73
+ typeof blockRecord[key] === "object" &&
74
+ blockRecord[key] !== null
75
+ ) {
76
+ mergedRecord[key] = {
77
+ ...(blockRecord[key] as Record<string, unknown>),
78
+ ...(value as Record<string, unknown>),
79
+ };
80
+ } else {
81
+ mergedRecord[key] = value;
82
+ }
83
+ }
84
+
85
+ return merged;
86
+ }
87
+
88
+ // ============================================
89
+ // Helpers for the settings panel
90
+ // ============================================
91
+
92
+ /**
93
+ * Check if a block property has a responsive override for the given viewport.
94
+ * Used by the editor to show "inherited" vs "overridden" state.
95
+ */
96
+ export function hasOverride(
97
+ block: ContentBlock,
98
+ viewport: DeviceViewport,
99
+ property: string
100
+ ): boolean {
101
+ if (viewport === "desktop") return true; // desktop is always "the value"
102
+ const responsive = getResponsive(block);
103
+ if (!responsive?.[viewport]) return false;
104
+ return property in responsive[viewport];
105
+ }
106
+
107
+ /**
108
+ * Get the effective value of a block property for a viewport.
109
+ * If the viewport has an override, returns it. Otherwise returns the base value.
110
+ */
111
+ export function getEffectiveValue<T>(
112
+ block: ContentBlock,
113
+ viewport: DeviceViewport,
114
+ property: string,
115
+ baseValue: T
116
+ ): T {
117
+ if (viewport === "desktop") return baseValue;
118
+ const responsive = getResponsive(block);
119
+ if (!responsive?.[viewport] || !(property in responsive[viewport])) {
120
+ return baseValue;
121
+ }
122
+ return responsive[viewport][property] as T;
123
+ }
124
+
125
+ /**
126
+ * Set or clear a responsive override for a specific property.
127
+ * Returns the updated `responsive` object to pass to `updateBlock()`.
128
+ *
129
+ * If `value` matches the base value, the override is removed (inherit).
130
+ * If `value` is `undefined`, the override is explicitly removed.
131
+ */
132
+ export function setResponsiveOverride(
133
+ block: ContentBlock,
134
+ viewport: DeviceViewport,
135
+ property: string,
136
+ value: unknown
137
+ ): { responsive: Record<string, Record<string, unknown>> } {
138
+ const existing = getResponsive(block) || {};
139
+ const viewportOverrides = { ...(existing[viewport] || {}) };
140
+
141
+ if (value === undefined) {
142
+ delete viewportOverrides[property];
143
+ } else {
144
+ viewportOverrides[property] = value;
145
+ }
146
+
147
+ // Clean up empty viewport objects
148
+ const responsive = { ...existing } as Record<string, Record<string, unknown>>;
149
+ if (Object.keys(viewportOverrides).length === 0) {
150
+ delete responsive[viewport];
151
+ } else {
152
+ responsive[viewport] = viewportOverrides;
153
+ }
154
+
155
+ return { responsive };
156
+ }