@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,193 @@
1
+ import type { BuilderStore, BuilderState } from "./types";
2
+ import type { ContentBlock, ContentItem, PageSection, PageSectionV2, ParallaxGroup, SectionBlock } from "../../lib/sanity/types";
3
+ import { isPageSection, isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
4
+ import { createDefaultBlock } from "./defaults";
5
+ import { generateKey } from "./utils";
6
+ import { findSectionPath, updateSectionAtPath, moveBlockInState } from "./store-helpers";
7
+
8
+ type StoreSet = (
9
+ partial: Partial<BuilderState> | ((state: BuilderState) => Partial<BuilderState>)
10
+ ) => void;
11
+ type StoreGet = () => BuilderStore;
12
+
13
+ export function createBlockActions(set: StoreSet, get: StoreGet) {
14
+ return {
15
+ // ---- Block operations ----
16
+
17
+ updateBlock: (blockKey: string, updates: Partial<ContentBlock>): void => {
18
+ // Snapshot is NOT pushed here to avoid polluting undo history on every keystroke.
19
+ const updateColumns = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
20
+ cols.map((col) => ({
21
+ ...col,
22
+ blocks: col.blocks.map((b) =>
23
+ b._key === blockKey ? ({ ...b, ...updates } as ContentBlock) : b
24
+ ),
25
+ }));
26
+
27
+ set((state) => ({
28
+ rows: state.rows.map((item) => {
29
+ if (isPageSection(item)) {
30
+ const block = item.block[0];
31
+ if (block && block._key === blockKey) {
32
+ return { ...item, block: [{ ...block, ...updates } as SectionBlock] };
33
+ }
34
+ return item;
35
+ }
36
+ if (isPageSectionV2(item)) {
37
+ return { ...item, columns: updateColumns(item.columns) };
38
+ }
39
+ if (isParallaxGroup(item)) {
40
+ return {
41
+ ...item,
42
+ slides: (item as ParallaxGroup).slides.map((slide) => ({
43
+ ...slide,
44
+ columns: updateColumns(slide.columns),
45
+ })),
46
+ } as ContentItem;
47
+ }
48
+ return item;
49
+ }),
50
+ isDirty: true,
51
+ }));
52
+ },
53
+
54
+ deleteBlock: (blockKey: string): void => {
55
+ get()._pushSnapshot();
56
+ const filterBlocks = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
57
+ cols.map((c) => ({ ...c, blocks: c.blocks.filter((b) => b._key !== blockKey) }));
58
+
59
+ set((state) => {
60
+ // Handle PageSection: delete the entire section if its block is being deleted
61
+ const updatedRows = state.rows.filter((item) => {
62
+ if (isPageSection(item)) {
63
+ const block = item.block[0];
64
+ return !(block && block._key === blockKey);
65
+ }
66
+ return true;
67
+ });
68
+
69
+ // Handle V2 Sections + ParallaxGroup slides
70
+ const finalRows = updatedRows.map((item) => {
71
+ if (isPageSectionV2(item)) {
72
+ return { ...item, columns: filterBlocks(item.columns) } as ContentItem;
73
+ }
74
+ if (isParallaxGroup(item)) {
75
+ return {
76
+ ...item,
77
+ slides: (item as ParallaxGroup).slides.map((slide) => ({
78
+ ...slide,
79
+ columns: filterBlocks(slide.columns),
80
+ })),
81
+ } as ContentItem;
82
+ }
83
+ return item;
84
+ });
85
+
86
+ return {
87
+ rows: finalRows,
88
+ selectedBlockKey: state.selectedBlockKey === blockKey ? null : state.selectedBlockKey,
89
+ isDirty: true,
90
+ };
91
+ });
92
+ },
93
+
94
+ duplicateBlock: (blockKey: string): void => {
95
+ get()._pushSnapshot();
96
+ set((state) => {
97
+ const newKey = generateKey();
98
+ const dupInColumns = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
99
+ cols.map((col) => {
100
+ const idx = col.blocks.findIndex((b) => b._key === blockKey);
101
+ if (idx === -1) return col;
102
+ const original = col.blocks[idx];
103
+ const clone = { ...JSON.parse(JSON.stringify(original)), _key: newKey } as ContentBlock;
104
+ const blocks = [...col.blocks];
105
+ blocks.splice(idx + 1, 0, clone);
106
+ return { ...col, blocks };
107
+ });
108
+
109
+ const rows = state.rows.map((item) => {
110
+ if (isPageSectionV2(item)) {
111
+ return { ...item, columns: dupInColumns(item.columns) } as ContentItem;
112
+ }
113
+ if (isParallaxGroup(item)) {
114
+ return {
115
+ ...item,
116
+ slides: (item as ParallaxGroup).slides.map((slide) => ({
117
+ ...slide,
118
+ columns: dupInColumns(slide.columns),
119
+ })),
120
+ } as ContentItem;
121
+ }
122
+ return item;
123
+ });
124
+ return { rows, selectedBlockKey: newKey, isDirty: true };
125
+ });
126
+ },
127
+
128
+ moveBlock: (blockKey: string, targetSectionKey: string, targetColumnKey: string, toIndex: number): void => {
129
+ const result = moveBlockInState(get().rows, blockKey, targetSectionKey, targetColumnKey, toIndex);
130
+ if (!result) return;
131
+
132
+ get()._pushSnapshot();
133
+ set({ rows: result.rows, isDirty: true });
134
+ },
135
+
136
+ reorderBlocks: (sectionKey: string, columnKey: string, fromIndex: number, toIndex: number): void => {
137
+ get()._pushSnapshot();
138
+ set((state) => {
139
+ const path = findSectionPath(state.rows, sectionKey);
140
+ if (!path) return state;
141
+ const rows = updateSectionAtPath(state.rows, path, (section) => ({
142
+ ...section,
143
+ columns: section.columns.map((c) => {
144
+ if (c._key !== columnKey) return c;
145
+ const blocks = [...c.blocks];
146
+ const [moved] = blocks.splice(fromIndex, 1);
147
+ blocks.splice(toIndex, 0, moved);
148
+ return { ...c, blocks };
149
+ }),
150
+ }));
151
+ return { rows, isDirty: true };
152
+ });
153
+ },
154
+
155
+ // ---- Debounced block update (no undo snapshot per keystroke) ----
156
+
157
+ updateBlockDebounced: (blockKey: string, updates: Partial<ContentBlock>): void => {
158
+ const updateColumns = (cols: import("../../lib/sanity/types").SectionColumn[]) =>
159
+ cols.map((col) => ({
160
+ ...col,
161
+ blocks: col.blocks.map((b) =>
162
+ b._key === blockKey ? ({ ...b, ...updates } as ContentBlock) : b
163
+ ),
164
+ }));
165
+
166
+ set((state) => ({
167
+ rows: state.rows.map((item) => {
168
+ if (isPageSection(item)) {
169
+ const block = item.block[0];
170
+ if (block && block._key === blockKey) {
171
+ return { ...item, block: [{ ...block, ...updates } as SectionBlock] };
172
+ }
173
+ return item;
174
+ }
175
+ if (isPageSectionV2(item)) {
176
+ return { ...item, columns: updateColumns(item.columns) };
177
+ }
178
+ if (isParallaxGroup(item)) {
179
+ return {
180
+ ...item,
181
+ slides: (item as ParallaxGroup).slides.map((slide) => ({
182
+ ...slide,
183
+ columns: updateColumns(slide.columns),
184
+ })),
185
+ } as ContentItem;
186
+ }
187
+ return item;
188
+ }),
189
+ isDirty: true,
190
+ }));
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,319 @@
1
+ import type { BuilderStore, BuilderState, CanvasTool, DeviceViewport, PageSettings } from "./types";
2
+ import { DEFAULT_PAGE_SETTINGS, DEFAULT_GRID_SETTINGS, DEVICE_WIDTHS } from "./types";
3
+ import type { PageSectionV2 } from "../../lib/sanity/types";
4
+ import { isPageSectionV2 } from "../../lib/sanity/types";
5
+ import { generateKey } from "./utils";
6
+ import { csrfHeaders } from "../../lib/csrf-client";
7
+ import { revalidateSite } from "../../lib/revalidate";
8
+
9
+ type StoreSet = (
10
+ partial: Partial<BuilderState> | ((state: BuilderState) => Partial<BuilderState>)
11
+ ) => void;
12
+ type StoreGet = () => BuilderStore;
13
+
14
+ export function createCanvasActions(set: StoreSet, get: StoreGet) {
15
+ return {
16
+ // ---- Editor mode ----
17
+
18
+ togglePreviewMode: () =>
19
+ set((state) => ({
20
+ previewMode: !state.previewMode,
21
+ // Clear selection when entering preview mode
22
+ // BUG-001 fix: state.previewMode is the value BEFORE toggle, so when it's false
23
+ // we're about to enter preview → clear selection
24
+ ...(!state.previewMode
25
+ ? {
26
+ selectedRowKey: null,
27
+ selectedColumnKey: null,
28
+ selectedBlockKey: null,
29
+ }
30
+ : {}),
31
+ })),
32
+
33
+ setPreviewMode: (preview: boolean) =>
34
+ set({
35
+ previewMode: preview,
36
+ ...(preview
37
+ ? { selectedRowKey: null, selectedColumnKey: null, selectedBlockKey: null }
38
+ : {}),
39
+ }),
40
+
41
+ // ---- Custom Section Editor Mode (Session 107) ----
42
+
43
+ enterSectionEditor: (slug: string | null, title: string | null, sectionData: PageSectionV2 | null): void => {
44
+ const state = get();
45
+ // Stash current page rows and selection
46
+ const savedPageState = {
47
+ rows: state.rows,
48
+ selectedKey: state.selectedRowKey,
49
+ };
50
+
51
+ // Build the single section for the editor
52
+ const editorSection: PageSectionV2 = sectionData || {
53
+ _type: "pageSectionV2",
54
+ _key: generateKey(),
55
+ section_type: "empty-v2",
56
+ columns: [],
57
+ settings: {
58
+ preset: "full",
59
+ grid_columns: 12,
60
+ col_gap: 20,
61
+ row_gap: 20,
62
+ },
63
+ };
64
+
65
+ set({
66
+ editorMode: "customSection",
67
+ customSectionSlug: slug,
68
+ customSectionTitle: title,
69
+ savedPageState,
70
+ rows: [editorSection],
71
+ selectedRowKey: null,
72
+ selectedColumnKey: null,
73
+ selectedBlockKey: null,
74
+ selectedProjectCardKey: null,
75
+ // Reset history for the editor session
76
+ _history: [],
77
+ _future: [],
78
+ });
79
+ },
80
+
81
+ exitSectionEditor: (wasSaved?: boolean): void => {
82
+ const state = get();
83
+ const savedPageState = state.savedPageState;
84
+ set({
85
+ editorMode: "page",
86
+ customSectionSlug: null,
87
+ customSectionTitle: null,
88
+ savedPageState: null,
89
+ rows: savedPageState?.rows || [],
90
+ selectedRowKey: savedPageState?.selectedKey || null,
91
+ selectedColumnKey: null,
92
+ selectedBlockKey: null,
93
+ selectedProjectCardKey: null,
94
+ // m1 fix: only mark page dirty if the section was actually saved (Session 110)
95
+ isDirty: wasSaved ? true : state.isDirty,
96
+ // Reset history back to page context
97
+ _history: [],
98
+ _future: [],
99
+ });
100
+ },
101
+
102
+ saveSectionEditor: async (title: string): Promise<{ id: string; slug: string; title: string } | null> => {
103
+ const state = get();
104
+ if (state.editorMode !== "customSection" || state.rows.length === 0) {
105
+ return null;
106
+ }
107
+
108
+ // Serialize the single V2 section from rows[0]
109
+ const sectionItem = state.rows[0];
110
+ if (!isPageSectionV2(sectionItem)) {
111
+ return null;
112
+ }
113
+
114
+ // Build the section data using the serializer's stateToDocument approach
115
+ // We need to strip the section from the full document format
116
+ const sectionData = structuredClone(sectionItem);
117
+
118
+ const slug = state.customSectionSlug;
119
+ const isCreating = !slug;
120
+
121
+ try {
122
+ set({ isSaving: true, saveError: null });
123
+
124
+ if (isCreating) {
125
+ // Generate slug from title
126
+ const newSlug = title
127
+ .toLowerCase()
128
+ .trim()
129
+ .replace(/[^\w\s-]/g, "")
130
+ .replace(/[\s_]+/g, "-")
131
+ .replace(/-+/g, "-")
132
+ .replace(/^-|-$/g, "");
133
+
134
+ const res = await fetch("/api/admin/custom-sections", {
135
+ method: "POST",
136
+ headers: {
137
+ "Content-Type": "application/json",
138
+ ...csrfHeaders(),
139
+ },
140
+ body: JSON.stringify({
141
+ title,
142
+ slug: newSlug,
143
+ section: sectionData,
144
+ }),
145
+ });
146
+
147
+ if (!res.ok) {
148
+ const data = await res.json().catch(() => ({ error: "Failed to create" }));
149
+ throw new Error(data.error || `Create failed (${res.status})`);
150
+ }
151
+
152
+ const data = await res.json();
153
+ set({ isSaving: false });
154
+
155
+ // Exit editor mode — wasSaved=true to mark page dirty
156
+ get().exitSectionEditor(true);
157
+
158
+ return {
159
+ id: data.section._id,
160
+ slug: newSlug,
161
+ title,
162
+ };
163
+ } else {
164
+ // Update existing
165
+ const res = await fetch(`/api/admin/custom-sections/${slug}`, {
166
+ method: "PATCH",
167
+ headers: {
168
+ "Content-Type": "application/json",
169
+ ...csrfHeaders(),
170
+ },
171
+ body: JSON.stringify({
172
+ title,
173
+ section: sectionData,
174
+ }),
175
+ });
176
+
177
+ if (!res.ok) {
178
+ const data = await res.json().catch(() => ({ error: "Failed to save" }));
179
+ throw new Error(data.error || `Save failed (${res.status})`);
180
+ }
181
+
182
+ set({ isSaving: false });
183
+
184
+ // Exit editor mode — wasSaved=true to mark page dirty
185
+ get().exitSectionEditor(true);
186
+
187
+ return {
188
+ id: "", // ID not needed for update flow
189
+ slug,
190
+ title,
191
+ };
192
+ }
193
+ } catch (err) {
194
+ set({
195
+ isSaving: false,
196
+ saveError: err instanceof Error ? err.message : "Save failed",
197
+ });
198
+ return null;
199
+ }
200
+ },
201
+
202
+ // ---- Page-level settings ----
203
+
204
+ updatePageSettings: (settings: Partial<PageSettings>) =>
205
+ set((state) => ({
206
+ pageSettings: { ...state.pageSettings, ...settings },
207
+ isDirty: true,
208
+ })),
209
+
210
+ /** Apply global styles as page defaults (called on page load, doesn't mark dirty).
211
+ * Only fills in colors when the page doesn't have its own saved values —
212
+ * page-specific colors always win over global defaults.
213
+ *
214
+ * BUG-014 fix: Uses _hasDocumentPageSettings flag (set by serializer) instead
215
+ * of comparing against magic color values. This correctly handles the case where
216
+ * a user explicitly chose white (#ffffff) or black (#000000) as their page color. */
217
+ applyGlobalStyles: async () => {
218
+ try {
219
+ const res = await fetch("/api/styles");
220
+ if (!res.ok) return;
221
+ const data = await res.json();
222
+ if (!data?.styles) return;
223
+ const { colors, grid } = data.styles;
224
+ set((state) => {
225
+ // BUG-014 fix: If the Sanity document had page_settings saved,
226
+ // the user intentionally set these colors — don't overwrite them.
227
+ const hasOwnColors = state._hasDocumentPageSettings;
228
+
229
+ return {
230
+ pageSettings: {
231
+ ...state.pageSettings,
232
+ ...(colors && !hasOwnColors
233
+ ? {
234
+ background_color: colors.background || state.pageSettings.background_color,
235
+ text_color: colors.text || state.pageSettings.text_color,
236
+ }
237
+ : {}),
238
+ },
239
+ ...(grid
240
+ ? {
241
+ gridSettings: {
242
+ width: grid.width || DEFAULT_GRID_SETTINGS.width,
243
+ outer_padding: grid.outer_padding || DEFAULT_GRID_SETTINGS.outer_padding,
244
+ gutter_desktop: grid.gutter_desktop || DEFAULT_GRID_SETTINGS.gutter_desktop,
245
+ gutter_responsive:
246
+ grid.gutter_responsive || DEFAULT_GRID_SETTINGS.gutter_responsive,
247
+ gutter_phone: grid.gutter_phone || DEFAULT_GRID_SETTINGS.gutter_phone,
248
+ },
249
+ }
250
+ : {}),
251
+ };
252
+ });
253
+ } catch {
254
+ // Silently fail — defaults work fine
255
+ }
256
+ },
257
+
258
+ // ---- Canvas ----
259
+
260
+ setCanvasZoom: (zoom: number) =>
261
+ set({ canvasZoom: Math.min(2, Math.max(0.1, zoom)) }),
262
+
263
+ setCanvasPan: (x: number, y: number) =>
264
+ set({ canvasPanX: x, canvasPanY: y }),
265
+
266
+ setCanvasTool: (tool: CanvasTool) =>
267
+ set({ canvasTool: tool }),
268
+
269
+ setActiveViewport: (viewport: DeviceViewport) =>
270
+ set({ activeViewport: viewport }),
271
+
272
+ zoomToFit: (viewportWidth: number, viewportHeight: number): void => {
273
+ // Phase 2: fit all 3 device frames (Desktop + Tablet + Phone)
274
+ const frameGap = 80;
275
+ const totalContentWidth =
276
+ DEVICE_WIDTHS.desktop + frameGap + DEVICE_WIDTHS.tablet + frameGap + DEVICE_WIDTHS.phone;
277
+ const padding = 120; // px padding around all frames
278
+ const fitZoom = Math.min(
279
+ (viewportWidth - padding) / totalContentWidth,
280
+ 1.0 // don't zoom above 100% for fit
281
+ );
282
+ const clampedZoom = Math.min(1.0, Math.max(0.1, fitZoom));
283
+ // Center all frames horizontally
284
+ const scaledWidth = totalContentWidth * clampedZoom;
285
+ const panX = (viewportWidth - scaledWidth) / 2;
286
+ const panY = 40; // small top padding
287
+ set({ canvasZoom: clampedZoom, canvasPanX: panX, canvasPanY: panY });
288
+ },
289
+
290
+ zoomToPoint: (newZoom: number, cursorX: number, cursorY: number): void => {
291
+ const { canvasZoom: oldZoom, canvasPanX: panX, canvasPanY: panY } = get();
292
+ const clampedZoom = Math.min(2, Math.max(0.1, newZoom));
293
+ // Point in canvas space before zoom
294
+ const canvasX = (cursorX - panX) / oldZoom;
295
+ const canvasY = (cursorY - panY) / oldZoom;
296
+ // New pan to keep same point under cursor
297
+ const newPanX = cursorX - canvasX * clampedZoom;
298
+ const newPanY = cursorY - canvasY * clampedZoom;
299
+ set({ canvasZoom: clampedZoom, canvasPanX: newPanX, canvasPanY: newPanY });
300
+ },
301
+
302
+ zoomToFrame: (device: DeviceViewport, viewportWidth: number, viewportHeight: number): void => {
303
+ // Calculate the X offset of the target frame in canvas space
304
+ const frameGap = 80;
305
+ const order: DeviceViewport[] = ["desktop", "tablet", "phone"];
306
+ let frameX = 0;
307
+ for (const d of order) {
308
+ if (d === device) break;
309
+ frameX += DEVICE_WIDTHS[d] + frameGap;
310
+ }
311
+ const frameWidth = DEVICE_WIDTHS[device];
312
+ // Zoom to 100% and center the frame in the viewport
313
+ const zoom = 1.0;
314
+ const panX = (viewportWidth - frameWidth * zoom) / 2 - frameX * zoom;
315
+ const panY = 40;
316
+ set({ canvasZoom: zoom, canvasPanX: panX, canvasPanY: panY });
317
+ },
318
+ };
319
+ }