@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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/admin/assets.ts +4 -0
- package/admin/database.ts +4 -0
- package/admin/index.ts +6 -0
- package/admin/login.ts +4 -0
- package/admin/navigation.ts +4 -0
- package/admin/pages-editor.ts +4 -0
- package/admin/pages.ts +4 -0
- package/admin/projects-editor.ts +4 -0
- package/admin/projects.ts +4 -0
- package/admin/settings.ts +4 -0
- package/admin/setup.ts +4 -0
- package/admin/storage.ts +4 -0
- package/admin/styles.ts +4 -0
- package/app/(site)/[slug]/loading.tsx +20 -0
- package/app/(site)/[slug]/page.tsx +83 -0
- package/app/(site)/error.tsx +32 -0
- package/app/(site)/layout.tsx +53 -0
- package/app/(site)/loading.tsx +20 -0
- package/app/(site)/not-found.tsx +41 -0
- package/app/(site)/page.tsx +43 -0
- package/app/(site)/preview/page.tsx +99 -0
- package/app/(site)/work/[slug]/loading.tsx +23 -0
- package/app/(site)/work/[slug]/page.tsx +84 -0
- package/app/admin/assets/page.tsx +573 -0
- package/app/admin/database/page.tsx +302 -0
- package/app/admin/error.tsx +53 -0
- package/app/admin/layout.tsx +273 -0
- package/app/admin/login/page.tsx +88 -0
- package/app/admin/navigation/page.tsx +157 -0
- package/app/admin/page.tsx +17 -0
- package/app/admin/pages/[slug]/page.tsx +849 -0
- package/app/admin/pages/page.tsx +588 -0
- package/app/admin/projects/[slug]/page.tsx +3 -0
- package/app/admin/projects/page.tsx +669 -0
- package/app/admin/settings/page.tsx +132 -0
- package/app/admin/setup/page.tsx +64 -0
- package/app/admin/storage/page.tsx +518 -0
- package/app/admin/styles/page.tsx +243 -0
- package/app/api/admin/assets/file/route.ts +81 -0
- package/app/api/admin/assets/health/route.ts +170 -0
- package/app/api/admin/assets/register/route.ts +163 -0
- package/app/api/admin/assets/registry/route.ts +98 -0
- package/app/api/admin/assets/relink/confirm/route.ts +242 -0
- package/app/api/admin/assets/relink/route.ts +202 -0
- package/app/api/admin/assets/scan/route.ts +271 -0
- package/app/api/admin/auth/route.ts +160 -0
- package/app/api/admin/custom-sections/[slug]/route.ts +159 -0
- package/app/api/admin/custom-sections/route.ts +127 -0
- package/app/api/admin/database/route.ts +53 -0
- package/app/api/admin/pages/[slug]/duplicate/route.ts +91 -0
- package/app/api/admin/pages/[slug]/route.ts +617 -0
- package/app/api/admin/pages/[slug]/set-home/route.ts +76 -0
- package/app/api/admin/pages/route.ts +129 -0
- package/app/api/admin/preview/route.ts +53 -0
- package/app/api/admin/r2/connect/route.ts +181 -0
- package/app/api/admin/r2/delete/route.ts +198 -0
- package/app/api/admin/r2/disconnect/route.ts +42 -0
- package/app/api/admin/r2/rename/route.ts +265 -0
- package/app/api/admin/r2/status/route.ts +106 -0
- package/app/api/admin/r2/upload-url/route.ts +148 -0
- package/app/api/admin/revalidate/route.ts +55 -0
- package/app/api/admin/settings/route.ts +279 -0
- package/app/api/admin/setup/complete/route.ts +51 -0
- package/app/api/admin/setup/route.ts +118 -0
- package/app/api/admin/storage/switch/route.ts +117 -0
- package/app/api/admin/styles/fonts/route.ts +97 -0
- package/app/api/admin/styles/route.ts +304 -0
- package/app/api/assets/[...path]/route.ts +98 -0
- package/app/api/custom-sections/[id]/route.ts +43 -0
- package/app/api/draft-mode/disable/route.ts +10 -0
- package/app/api/draft-mode/enable/route.ts +26 -0
- package/app/api/projects/route.ts +42 -0
- package/app/api/styles/route.ts +88 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +7 -0
- package/app/layout.tsx +53 -0
- package/app/robots.ts +17 -0
- package/app/sitemap.ts +48 -0
- package/app/studio/[[...index]]/page.tsx +8 -0
- package/components/admin/MetadataEditor.tsx +173 -0
- package/components/admin/PublishToggle.tsx +130 -0
- package/components/admin/icons.tsx +40 -0
- package/components/admin/nav-builder/NavBuilder.tsx +182 -0
- package/components/admin/nav-builder/NavBuilderGrid.tsx +326 -0
- package/components/admin/nav-builder/NavGeneralSettings.tsx +275 -0
- package/components/admin/nav-builder/NavGridCell.tsx +48 -0
- package/components/admin/nav-builder/NavGridItem.tsx +189 -0
- package/components/admin/nav-builder/NavItemSettings.tsx +288 -0
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -0
- package/components/admin/nav-builder/NavLivePreview.tsx +125 -0
- package/components/admin/nav-builder/NavSettingsFields.tsx +248 -0
- package/components/admin/nav-builder/NavSettingsPanel.tsx +127 -0
- package/components/admin/nav-builder/index.ts +10 -0
- package/components/admin/nav-builder/nav-builder-utils.ts +238 -0
- package/components/admin/setup-wizard/BrandingStep.tsx +218 -0
- package/components/admin/setup-wizard/DatabaseStep.tsx +331 -0
- package/components/admin/setup-wizard/DoneStep.tsx +187 -0
- package/components/admin/setup-wizard/SetupWizard.tsx +166 -0
- package/components/admin/setup-wizard/StorageStep.tsx +308 -0
- package/components/admin/setup-wizard/WelcomeStep.tsx +96 -0
- package/components/admin/setup-wizard/index.ts +9 -0
- package/components/admin/styles/ColorsEditor.tsx +214 -0
- package/components/admin/styles/FontsEditor.tsx +258 -0
- package/components/admin/styles/GridLayoutEditor.tsx +292 -0
- package/components/admin/styles/LinksButtonsEditor.tsx +120 -0
- package/components/admin/styles/TypographyEditor.tsx +266 -0
- package/components/admin/styles/index.ts +9 -0
- package/components/admin/styles/shared.tsx +68 -0
- package/components/blocks/BlockRenderer.tsx +404 -0
- package/components/blocks/ButtonBlockRenderer.tsx +52 -0
- package/components/blocks/CoverBlockRenderer.tsx +239 -0
- package/components/blocks/CustomSectionInstanceRenderer.tsx +82 -0
- package/components/blocks/EnterAnimationWrapper.tsx +140 -0
- package/components/blocks/HoverAnimationWrapper.tsx +308 -0
- package/components/blocks/ImageBlockRenderer.tsx +61 -0
- package/components/blocks/ImageGridBlockRenderer.tsx +545 -0
- package/components/blocks/PageBackground.tsx +28 -0
- package/components/blocks/PageNavAnimation.tsx +35 -0
- package/components/blocks/PageNavColor.tsx +24 -0
- package/components/blocks/PageRenderer.tsx +142 -0
- package/components/blocks/ParallaxGroupRenderer.tsx +448 -0
- package/components/blocks/ParallaxSlideRenderer.tsx +175 -0
- package/components/blocks/ProjectGridBlockRenderer.tsx +556 -0
- package/components/blocks/SectionRenderer.tsx +170 -0
- package/components/blocks/SectionV2Renderer.tsx +330 -0
- package/components/blocks/ShaderCanvas.tsx +392 -0
- package/components/blocks/SpacerBlockRenderer.tsx +17 -0
- package/components/blocks/TextBlockRenderer.tsx +87 -0
- package/components/blocks/TypewriterRichText.tsx +464 -0
- package/components/blocks/TypewriterWrapper.tsx +149 -0
- package/components/blocks/VideoBlockRenderer.tsx +304 -0
- package/components/blocks/index.ts +2 -0
- package/components/builder/AssetBrowser.tsx +2 -0
- package/components/builder/BlockLivePreview.tsx +101 -0
- package/components/builder/BlockTypePicker.tsx +178 -0
- package/components/builder/BuilderCanvas.tsx +354 -0
- package/components/builder/CanvasMinimap.tsx +200 -0
- package/components/builder/CanvasToolbar.tsx +202 -0
- package/components/builder/ColorPicker.tsx +243 -0
- package/components/builder/ColorSwatchPicker.tsx +274 -0
- package/components/builder/ColumnDragContext.tsx +51 -0
- package/components/builder/ColumnDragOverlay.tsx +110 -0
- package/components/builder/CustomSectionInstanceCard.tsx +97 -0
- package/components/builder/DeviceFrame.tsx +123 -0
- package/components/builder/DndWrapper.tsx +337 -0
- package/components/builder/InsertionLines.tsx +186 -0
- package/components/builder/ParallaxGroupCanvas.tsx +228 -0
- package/components/builder/ParallaxSlideHeader.tsx +113 -0
- package/components/builder/ReadOnlyFrame.tsx +417 -0
- package/components/builder/SectionEditorBar.tsx +288 -0
- package/components/builder/SectionTypePicker.tsx +422 -0
- package/components/builder/SectionV2Canvas.tsx +297 -0
- package/components/builder/SectionV2Column.tsx +488 -0
- package/components/builder/SettingsPanel.tsx +911 -0
- package/components/builder/SortableBlock.tsx +230 -0
- package/components/builder/SortableRow.tsx +362 -0
- package/components/builder/VirtualAssetGrid.tsx +397 -0
- package/components/builder/asset-browser/AssetBrowser.tsx +178 -0
- package/components/builder/asset-browser/FileLightbox.tsx +116 -0
- package/components/builder/asset-browser/FolderTreeItem.tsx +55 -0
- package/components/builder/asset-browser/R2BrowserContent.tsx +436 -0
- package/components/builder/asset-browser/R2ContextMenu.tsx +98 -0
- package/components/builder/asset-browser/VideoThumbnail.tsx +63 -0
- package/components/builder/asset-browser/helpers.ts +88 -0
- package/components/builder/asset-browser/index.ts +1 -0
- package/components/builder/asset-browser/types.ts +49 -0
- package/components/builder/asset-browser/useAssetBrowser.ts +344 -0
- package/components/builder/asset-browser/useR2DragDrop.ts +116 -0
- package/components/builder/asset-browser/useR2Operations.ts +189 -0
- package/components/builder/blockStyles.tsx +295 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +184 -0
- package/components/builder/editors/CoverBlockEditor.tsx +488 -0
- package/components/builder/editors/EnterAnimationPicker.tsx +297 -0
- package/components/builder/editors/HoverEffectPicker.tsx +209 -0
- package/components/builder/editors/ImageBlockEditor.tsx +206 -0
- package/components/builder/editors/ImageGridBlockEditor.tsx +386 -0
- package/components/builder/editors/ProjectGridEditor.tsx +648 -0
- package/components/builder/editors/SpacerBlockEditor.tsx +167 -0
- package/components/builder/editors/StaggerSettings.tsx +108 -0
- package/components/builder/editors/TextAlignmentIcons.tsx +39 -0
- package/components/builder/editors/TextBlockEditor.tsx +462 -0
- package/components/builder/editors/TextStylePicker.tsx +183 -0
- package/components/builder/editors/VideoBlockEditor.tsx +278 -0
- package/components/builder/editors/index.ts +10 -0
- package/components/builder/editors/shared.tsx +345 -0
- package/components/builder/hooks/useColumnDrag.ts +472 -0
- package/components/builder/hooks/useColumnResize.ts +221 -0
- package/components/builder/index.ts +12 -0
- package/components/builder/live-preview/LiveButtonPreview.tsx +38 -0
- package/components/builder/live-preview/LiveCoverPreview.tsx +146 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +123 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +107 -0
- package/components/builder/live-preview/LiveProjectGridPreview.tsx +1010 -0
- package/components/builder/live-preview/LiveSpacerPreview.tsx +9 -0
- package/components/builder/live-preview/LiveTextEditor.tsx +198 -0
- package/components/builder/live-preview/LiveVideoPreview.tsx +98 -0
- package/components/builder/live-preview/index.ts +10 -0
- package/components/builder/live-preview/shared.tsx +153 -0
- package/components/builder/settings-panel/BlockLayoutTab.tsx +532 -0
- package/components/builder/settings-panel/BlockSettings.tsx +94 -0
- package/components/builder/settings-panel/ColumnV2Settings.tsx +160 -0
- package/components/builder/settings-panel/LayoutTab.tsx +310 -0
- package/components/builder/settings-panel/PageSettings.tsx +200 -0
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +118 -0
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +178 -0
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +103 -0
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +312 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +323 -0
- package/components/builder/settings-panel/TRBLInputs.tsx +51 -0
- package/components/builder/settings-panel/index.ts +19 -0
- package/components/builder/settings-panel/responsive-helpers.ts +524 -0
- package/components/ui/CustomCursor.tsx +118 -0
- package/components/ui/NavContentLightbox.tsx +152 -0
- package/components/ui/Navbar.tsx +582 -0
- package/components/ui/PortfolioTracker.tsx +87 -0
- package/components/ui/ScrollToTop.tsx +47 -0
- package/lib/animation/enter-presets.ts +147 -0
- package/lib/animation/enter-resolve.ts +90 -0
- package/lib/animation/enter-types.ts +128 -0
- package/lib/animation/hover-effect-presets.ts +210 -0
- package/lib/animation/hover-effect-types.ts +126 -0
- package/lib/asset-retry.ts +111 -0
- package/lib/assets.ts +92 -0
- package/lib/audit.ts +35 -0
- package/lib/auth-token.ts +94 -0
- package/lib/auth.ts +13 -0
- package/lib/builder/cascade-helpers.ts +51 -0
- package/lib/builder/cascade.ts +533 -0
- package/lib/builder/constants.ts +103 -0
- package/lib/builder/defaults.ts +182 -0
- package/lib/builder/history.ts +48 -0
- package/lib/builder/index.ts +21 -0
- package/lib/builder/layout-styles.ts +344 -0
- package/lib/builder/masonry.ts +166 -0
- package/lib/builder/responsive.ts +156 -0
- package/lib/builder/serializer.ts +845 -0
- package/lib/builder/store-blocks.ts +193 -0
- package/lib/builder/store-canvas.ts +319 -0
- package/lib/builder/store-helpers.ts +490 -0
- package/lib/builder/store-sections.ts +709 -0
- package/lib/builder/store.ts +333 -0
- package/lib/builder/templates.ts +297 -0
- package/lib/builder/types.ts +374 -0
- package/lib/builder/utils.ts +37 -0
- package/lib/color-utils.ts +116 -0
- package/lib/config/index.ts +57 -0
- package/lib/config/types.ts +122 -0
- package/lib/contexts/AssetContext.tsx +79 -0
- package/lib/contexts/NavAnimationContext.tsx +44 -0
- package/lib/contexts/NavColorContext.tsx +38 -0
- package/lib/contexts/PageExitContext.tsx +194 -0
- package/lib/contexts/ThumbStatusContext.tsx +83 -0
- package/lib/csrf-client.ts +34 -0
- package/lib/csrf.ts +68 -0
- package/lib/format-utils.ts +24 -0
- package/lib/hooks/useViewport.ts +42 -0
- package/lib/logger.ts +81 -0
- package/lib/revalidate.ts +23 -0
- package/lib/sanitize.ts +91 -0
- package/lib/sanity/client.ts +8 -0
- package/lib/sanity/queries.ts +486 -0
- package/lib/sanity/types.ts +869 -0
- package/lib/sanity/writeClient.ts +24 -0
- package/lib/security.ts +402 -0
- package/lib/setup/detect.ts +156 -0
- package/lib/shader/glsl/index.ts +27 -0
- package/lib/shader/glsl/pixelate.ts +51 -0
- package/lib/shader/glsl/rgb-shift.ts +45 -0
- package/lib/shader/glsl/ripple.ts +46 -0
- package/lib/shader/glsl/vertex.ts +14 -0
- package/lib/storage/index.ts +211 -0
- package/lib/storage/r2-adapter.ts +286 -0
- package/lib/storage/types.ts +125 -0
- package/lib/styles/provider.tsx +267 -0
- package/lib/thumbnails/generate.ts +151 -0
- package/lib/utils.ts +6 -0
- package/package.json +212 -0
- package/sanity/compose.ts +65 -0
- package/sanity/sanity.config.ts +126 -0
- package/sanity/schemas/assetRegistry.ts +301 -0
- package/sanity/schemas/blocks/blockLayout.ts +90 -0
- package/sanity/schemas/blocks/buttonBlock.ts +82 -0
- package/sanity/schemas/blocks/coverBlock.ts +229 -0
- package/sanity/schemas/blocks/imageBlock.ts +58 -0
- package/sanity/schemas/blocks/imageGridBlock.ts +112 -0
- package/sanity/schemas/blocks/index.ts +9 -0
- package/sanity/schemas/blocks/projectGridBlock.ts +251 -0
- package/sanity/schemas/blocks/spacerBlock.ts +41 -0
- package/sanity/schemas/blocks/textBlock.ts +139 -0
- package/sanity/schemas/blocks/videoBlock.ts +80 -0
- package/sanity/schemas/customSection.ts +69 -0
- package/sanity/schemas/customSectionInstance.ts +163 -0
- package/sanity/schemas/index.ts +111 -0
- package/sanity/schemas/objects/enterAnimationConfig.ts +72 -0
- package/sanity/schemas/objects/hoverEffectConfig.ts +90 -0
- package/sanity/schemas/objects/parallaxGroup.ts +66 -0
- package/sanity/schemas/objects/parallaxSlide.ts +217 -0
- package/sanity/schemas/objects/typewriterConfig.ts +38 -0
- package/sanity/schemas/page.ts +162 -0
- package/sanity/schemas/pageSection.ts +157 -0
- package/sanity/schemas/pageSectionV2.ts +269 -0
- package/sanity/schemas/siteSettings.ts +256 -0
- package/sanity/schemas/siteStyles.ts +212 -0
- package/site/error.ts +4 -0
- package/site/index.ts +8 -0
- package/site/not-found.ts +4 -0
- package/site/page.ts +4 -0
- package/site/preview.ts +4 -0
- package/site/robots.ts +4 -0
- package/site/sitemap.ts +4 -0
- package/site/work.ts +4 -0
- package/studio/index.ts +4 -0
- package/styles/admin.css +85 -0
- package/styles/animations.css +237 -0
- package/styles/base.css +148 -0
- package/styles/globals.css +10 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper functions extracted from Zustand store actions.
|
|
3
|
+
*
|
|
4
|
+
* Each function takes immutable state and returns new state —
|
|
5
|
+
* no side effects, no store references, fully testable.
|
|
6
|
+
*
|
|
7
|
+
* Session 96: Store Action Extraction
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ContentBlock,
|
|
12
|
+
ContentItem,
|
|
13
|
+
PageSectionV2,
|
|
14
|
+
ParallaxGroup,
|
|
15
|
+
ParallaxSlideV2,
|
|
16
|
+
SectionColumn,
|
|
17
|
+
SectionV2Preset,
|
|
18
|
+
} from "../../lib/sanity/types";
|
|
19
|
+
import { isPageSection, isPageSectionV2, isParallaxGroup } from "../../lib/sanity/types";
|
|
20
|
+
import { columnsFromPreset, detectPreset } from "./cascade";
|
|
21
|
+
import { resizeColumnLeft as cascadeResizeLeft, moveColumn as cascadeMoveColumn, type ResizeLeftResult } from "./cascade";
|
|
22
|
+
import { applyBlocksToColumns, toCascadeColumns, type CascadeColumn } from "./cascade-helpers";
|
|
23
|
+
import { generateKey } from "./utils";
|
|
24
|
+
import { createDefaultParallaxSlide } from "./defaults";
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// moveBlockInState
|
|
28
|
+
// ============================================
|
|
29
|
+
|
|
30
|
+
export interface MoveBlockResult {
|
|
31
|
+
rows: ContentItem[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Move a block from its current position to a target column in a V2 section.
|
|
36
|
+
* Returns null if the move is invalid (block not found, target not V2).
|
|
37
|
+
*
|
|
38
|
+
* Pure function — no store access.
|
|
39
|
+
*/
|
|
40
|
+
export function moveBlockInState(
|
|
41
|
+
rows: ContentItem[],
|
|
42
|
+
blockKey: string,
|
|
43
|
+
targetSectionKey: string,
|
|
44
|
+
targetColumnKey: string,
|
|
45
|
+
toIndex: number
|
|
46
|
+
): MoveBlockResult | null {
|
|
47
|
+
// Validate target exists as a V2 section or parallax slide
|
|
48
|
+
const targetPath = findSectionPath(rows, targetSectionKey);
|
|
49
|
+
if (!targetPath) return null;
|
|
50
|
+
|
|
51
|
+
let movedBlock: ContentBlock | null = null;
|
|
52
|
+
|
|
53
|
+
const removeFromColumns = (columns: SectionColumn[]) =>
|
|
54
|
+
columns.map((c) => {
|
|
55
|
+
const blocks = [...c.blocks];
|
|
56
|
+
const idx = blocks.findIndex((b) => b._key === blockKey);
|
|
57
|
+
if (idx !== -1) {
|
|
58
|
+
[movedBlock] = blocks.splice(idx, 1);
|
|
59
|
+
}
|
|
60
|
+
return { ...c, blocks };
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Pass 1: find and remove block from all sections (V2 + parallax slides)
|
|
64
|
+
const rowsAfterRemove = rows.map((item) => {
|
|
65
|
+
if (isPageSectionV2(item)) {
|
|
66
|
+
return { ...item, columns: removeFromColumns(item.columns) } as ContentItem;
|
|
67
|
+
}
|
|
68
|
+
if (isParallaxGroup(item)) {
|
|
69
|
+
const group = item as ParallaxGroup;
|
|
70
|
+
return {
|
|
71
|
+
...group,
|
|
72
|
+
slides: group.slides.map((slide) => ({
|
|
73
|
+
...slide,
|
|
74
|
+
columns: removeFromColumns(slide.columns),
|
|
75
|
+
})),
|
|
76
|
+
} as ContentItem;
|
|
77
|
+
}
|
|
78
|
+
return item;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (!movedBlock) return null;
|
|
82
|
+
|
|
83
|
+
// Pass 2: insert into target column using the path-based approach
|
|
84
|
+
const insertIntoColumns = (columns: SectionColumn[]) =>
|
|
85
|
+
columns.map((c) => {
|
|
86
|
+
if (c._key !== targetColumnKey) return c;
|
|
87
|
+
const blocks = [...c.blocks];
|
|
88
|
+
blocks.splice(toIndex, 0, movedBlock!);
|
|
89
|
+
return { ...c, blocks };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const finalRows = updateSectionAtPath(rowsAfterRemove, targetPath, (section) => ({
|
|
93
|
+
...section,
|
|
94
|
+
columns: insertIntoColumns(section.columns),
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return { rows: finalRows };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================
|
|
101
|
+
// resizeColumnV2LeftInState
|
|
102
|
+
// ============================================
|
|
103
|
+
|
|
104
|
+
export interface ResizeColumnLeftResult {
|
|
105
|
+
rows: ContentItem[];
|
|
106
|
+
/** Key of the left neighbor that was compressed, if any */
|
|
107
|
+
compressedNeighborKey: string | null;
|
|
108
|
+
/** True when the left neighbor is at minimum span (1) — further expansion blocked */
|
|
109
|
+
neighborAtMinimum: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resize a V2 column by moving its left edge.
|
|
114
|
+
* When expanding into a left neighbor, the neighbor is compressed (down to span 1).
|
|
115
|
+
* Returns null only if the operation is completely impossible.
|
|
116
|
+
*
|
|
117
|
+
* Pure function — no store access.
|
|
118
|
+
*/
|
|
119
|
+
export function resizeColumnV2LeftInState(
|
|
120
|
+
rows: ContentItem[],
|
|
121
|
+
sectionKey: string,
|
|
122
|
+
columnKey: string,
|
|
123
|
+
newGridColumn: number
|
|
124
|
+
): ResizeColumnLeftResult | null {
|
|
125
|
+
const path = findSectionPath(rows, sectionKey);
|
|
126
|
+
if (!path) return null;
|
|
127
|
+
const section = getSectionFromPath(rows, path);
|
|
128
|
+
if (!section) return null;
|
|
129
|
+
|
|
130
|
+
const cascadeCols = toCascadeColumns(section.columns);
|
|
131
|
+
const result = cascadeResizeLeft(cascadeCols, columnKey, newGridColumn, section.settings.grid_columns);
|
|
132
|
+
if (!result) return null;
|
|
133
|
+
|
|
134
|
+
const updatedColumns = applyBlocksToColumns(result.columns, section.columns);
|
|
135
|
+
const newPreset = detectPreset(result.columns, section.settings.grid_columns);
|
|
136
|
+
|
|
137
|
+
const newRows = updateSectionAtPath(rows, path, (sec) => ({
|
|
138
|
+
...sec,
|
|
139
|
+
columns: updatedColumns,
|
|
140
|
+
settings: { ...sec.settings, preset: newPreset },
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
rows: newRows,
|
|
145
|
+
compressedNeighborKey: result.compressedNeighborKey,
|
|
146
|
+
neighborAtMinimum: result.neighborAtMinimum,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================
|
|
151
|
+
// addSectionV2InState
|
|
152
|
+
// ============================================
|
|
153
|
+
|
|
154
|
+
export interface AddSectionV2Result {
|
|
155
|
+
rows: ContentItem[];
|
|
156
|
+
newSectionKey: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Create a new V2 section with the given preset and insert it into the rows.
|
|
161
|
+
* Returns the new rows array and the key of the created section.
|
|
162
|
+
*
|
|
163
|
+
* Pure function — uses generateKey() for side effects (unique IDs), but
|
|
164
|
+
* is otherwise deterministic given the same key generation.
|
|
165
|
+
*/
|
|
166
|
+
export function addSectionV2InState(
|
|
167
|
+
rows: ContentItem[],
|
|
168
|
+
preset: SectionV2Preset,
|
|
169
|
+
afterRowKey?: string | null
|
|
170
|
+
): AddSectionV2Result {
|
|
171
|
+
const gridColumns = 12;
|
|
172
|
+
let columns: SectionColumn[];
|
|
173
|
+
|
|
174
|
+
if (preset === "custom") {
|
|
175
|
+
columns = [{
|
|
176
|
+
_key: generateKey(),
|
|
177
|
+
grid_column: 1,
|
|
178
|
+
grid_row: 1,
|
|
179
|
+
span: gridColumns,
|
|
180
|
+
blocks: [],
|
|
181
|
+
}];
|
|
182
|
+
} else {
|
|
183
|
+
const keyFactory = () => generateKey();
|
|
184
|
+
const cascadeCols = columnsFromPreset(preset as Exclude<SectionV2Preset, "custom">, gridColumns, keyFactory);
|
|
185
|
+
columns = cascadeCols.map((c: CascadeColumn) => ({
|
|
186
|
+
...c,
|
|
187
|
+
blocks: [] as ContentBlock[],
|
|
188
|
+
}));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const detectedPreset = detectPreset(columns, gridColumns);
|
|
192
|
+
|
|
193
|
+
const newSection: PageSectionV2 = {
|
|
194
|
+
_type: "pageSectionV2",
|
|
195
|
+
_key: generateKey(),
|
|
196
|
+
section_type: "empty-v2",
|
|
197
|
+
columns,
|
|
198
|
+
settings: {
|
|
199
|
+
preset: detectedPreset,
|
|
200
|
+
grid_columns: gridColumns,
|
|
201
|
+
col_gap: 20,
|
|
202
|
+
row_gap: 20,
|
|
203
|
+
spacing_top: "32",
|
|
204
|
+
spacing_bottom: "32",
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const newRows = [...rows];
|
|
209
|
+
if (afterRowKey) {
|
|
210
|
+
const idx = newRows.findIndex((r) => r._key === afterRowKey);
|
|
211
|
+
if (idx !== -1) {
|
|
212
|
+
newRows.splice(idx + 1, 0, newSection);
|
|
213
|
+
} else {
|
|
214
|
+
newRows.push(newSection);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
newRows.push(newSection);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { rows: newRows, newSectionKey: newSection._key };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================
|
|
224
|
+
// moveColumnV2InState
|
|
225
|
+
// ============================================
|
|
226
|
+
|
|
227
|
+
export interface MoveColumnV2Result {
|
|
228
|
+
rows: ContentItem[];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Move a V2 column to a new grid position within its section.
|
|
233
|
+
* Returns null if the section is not found.
|
|
234
|
+
*
|
|
235
|
+
* Pure function — no store access.
|
|
236
|
+
*/
|
|
237
|
+
export function moveColumnV2InState(
|
|
238
|
+
rows: ContentItem[],
|
|
239
|
+
sectionKey: string,
|
|
240
|
+
columnKey: string,
|
|
241
|
+
targetRow: number,
|
|
242
|
+
targetColumn: number
|
|
243
|
+
): MoveColumnV2Result | null {
|
|
244
|
+
const path = findSectionPath(rows, sectionKey);
|
|
245
|
+
if (!path) return null;
|
|
246
|
+
|
|
247
|
+
const newRows = updateSectionAtPath(rows, path, (section) => {
|
|
248
|
+
const updatedCascade = cascadeMoveColumn(
|
|
249
|
+
toCascadeColumns(section.columns),
|
|
250
|
+
columnKey,
|
|
251
|
+
targetRow,
|
|
252
|
+
targetColumn,
|
|
253
|
+
section.settings.grid_columns
|
|
254
|
+
);
|
|
255
|
+
const updatedColumns = applyBlocksToColumns(updatedCascade, section.columns);
|
|
256
|
+
const newPreset = detectPreset(updatedCascade, section.settings.grid_columns);
|
|
257
|
+
return {
|
|
258
|
+
...section,
|
|
259
|
+
columns: updatedColumns,
|
|
260
|
+
settings: { ...section.settings, preset: newPreset },
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return { rows: newRows };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// -------------------------------------------------------
|
|
268
|
+
// Swap two columns in a V2 section (exchange positions)
|
|
269
|
+
// -------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
export interface SwapColumnV2Result {
|
|
272
|
+
rows: ContentItem[];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function swapColumnV2InState(
|
|
276
|
+
rows: ContentItem[],
|
|
277
|
+
sectionKey: string,
|
|
278
|
+
draggedKey: string,
|
|
279
|
+
targetKey: string
|
|
280
|
+
): SwapColumnV2Result | null {
|
|
281
|
+
const path = findSectionPath(rows, sectionKey);
|
|
282
|
+
if (!path) return null;
|
|
283
|
+
|
|
284
|
+
const newRows = updateSectionAtPath(rows, path, (section) => {
|
|
285
|
+
const draggedCol = section.columns.find((c) => c._key === draggedKey);
|
|
286
|
+
const targetCol = section.columns.find((c) => c._key === targetKey);
|
|
287
|
+
if (!draggedCol || !targetCol) return section;
|
|
288
|
+
|
|
289
|
+
const updatedColumns = section.columns.map((c) => {
|
|
290
|
+
if (c._key === draggedKey) {
|
|
291
|
+
return { ...c, grid_column: targetCol.grid_column, grid_row: targetCol.grid_row, span: targetCol.span };
|
|
292
|
+
}
|
|
293
|
+
if (c._key === targetKey) {
|
|
294
|
+
return { ...c, grid_column: draggedCol.grid_column, grid_row: draggedCol.grid_row, span: draggedCol.span };
|
|
295
|
+
}
|
|
296
|
+
return c;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const cascadeCols = updatedColumns.map((c) => ({
|
|
300
|
+
_key: c._key, grid_column: c.grid_column, grid_row: c.grid_row, span: c.span,
|
|
301
|
+
}));
|
|
302
|
+
const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
|
|
303
|
+
|
|
304
|
+
return { ...section, columns: updatedColumns, settings: { ...section.settings, preset: newPreset } };
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return { rows: newRows };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// -------------------------------------------------------
|
|
311
|
+
// Move a column to an empty gap, adopting the gap's span
|
|
312
|
+
// -------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
export interface MoveColumnToGapV2Result {
|
|
315
|
+
rows: ContentItem[];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function moveColumnToGapV2InState(
|
|
319
|
+
rows: ContentItem[],
|
|
320
|
+
sectionKey: string,
|
|
321
|
+
columnKey: string,
|
|
322
|
+
targetRow: number,
|
|
323
|
+
targetColumn: number,
|
|
324
|
+
targetSpan: number
|
|
325
|
+
): MoveColumnToGapV2Result | null {
|
|
326
|
+
const path = findSectionPath(rows, sectionKey);
|
|
327
|
+
if (!path) return null;
|
|
328
|
+
|
|
329
|
+
const newRows = updateSectionAtPath(rows, path, (section) => {
|
|
330
|
+
const col = section.columns.find((c) => c._key === columnKey);
|
|
331
|
+
if (!col) return section;
|
|
332
|
+
|
|
333
|
+
const updatedColumns = section.columns.map((c) => {
|
|
334
|
+
if (c._key !== columnKey) return c;
|
|
335
|
+
return { ...c, grid_column: targetColumn, grid_row: targetRow, span: targetSpan };
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const cascadeCols = updatedColumns.map((c) => ({
|
|
339
|
+
_key: c._key, grid_column: c.grid_column, grid_row: c.grid_row, span: c.span,
|
|
340
|
+
}));
|
|
341
|
+
const newPreset = detectPreset(cascadeCols, section.settings.grid_columns);
|
|
342
|
+
|
|
343
|
+
return { ...section, columns: updatedColumns, settings: { ...section.settings, preset: newPreset } };
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return { rows: newRows };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ============================================
|
|
350
|
+
// findSectionPath — unified section lookup (V2 + ParallaxSlide)
|
|
351
|
+
// ============================================
|
|
352
|
+
|
|
353
|
+
export type SectionPath =
|
|
354
|
+
| { type: "v2"; index: number }
|
|
355
|
+
| { type: "parallaxSlide"; groupIndex: number; slideIndex: number };
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Find a V2 section by key — searches both top-level PageSectionV2
|
|
359
|
+
* and inside ParallaxGroup slides. Returns the path or null.
|
|
360
|
+
*
|
|
361
|
+
* This enables existing V2 actions (addColumnV2, addBlockV2, etc.)
|
|
362
|
+
* to work on parallax slides without signature changes — slides
|
|
363
|
+
* have unique _key values just like sections.
|
|
364
|
+
*/
|
|
365
|
+
export function findSectionPath(
|
|
366
|
+
rows: ContentItem[],
|
|
367
|
+
sectionKey: string
|
|
368
|
+
): SectionPath | null {
|
|
369
|
+
for (let i = 0; i < rows.length; i++) {
|
|
370
|
+
const item = rows[i];
|
|
371
|
+
if (item._key === sectionKey && isPageSectionV2(item)) {
|
|
372
|
+
return { type: "v2", index: i };
|
|
373
|
+
}
|
|
374
|
+
if (isParallaxGroup(item)) {
|
|
375
|
+
const group = item as ParallaxGroup;
|
|
376
|
+
for (let j = 0; j < group.slides.length; j++) {
|
|
377
|
+
if (group.slides[j]._key === sectionKey) {
|
|
378
|
+
return { type: "parallaxSlide", groupIndex: i, slideIndex: j };
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Get a PageSectionV2 from any section path.
|
|
388
|
+
* For parallax slides, returns a virtual PageSectionV2 view of the slide.
|
|
389
|
+
*/
|
|
390
|
+
export function getSectionFromPath(
|
|
391
|
+
rows: ContentItem[],
|
|
392
|
+
path: SectionPath
|
|
393
|
+
): PageSectionV2 | null {
|
|
394
|
+
if (path.type === "v2") {
|
|
395
|
+
const item = rows[path.index];
|
|
396
|
+
return isPageSectionV2(item) ? (item as PageSectionV2) : null;
|
|
397
|
+
}
|
|
398
|
+
const group = rows[path.groupIndex];
|
|
399
|
+
if (!isParallaxGroup(group)) return null;
|
|
400
|
+
const slide = (group as ParallaxGroup).slides[path.slideIndex];
|
|
401
|
+
if (!slide) return null;
|
|
402
|
+
return {
|
|
403
|
+
_type: "pageSectionV2",
|
|
404
|
+
_key: slide._key,
|
|
405
|
+
section_type: "empty-v2",
|
|
406
|
+
columns: slide.columns,
|
|
407
|
+
settings: slide.section_settings,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Apply an updater function to a section at a given path.
|
|
413
|
+
* For parallax slides, updates the slide in place within the group.
|
|
414
|
+
*/
|
|
415
|
+
export function updateSectionAtPath(
|
|
416
|
+
rows: ContentItem[],
|
|
417
|
+
path: SectionPath,
|
|
418
|
+
updater: (section: PageSectionV2) => PageSectionV2
|
|
419
|
+
): ContentItem[] {
|
|
420
|
+
if (path.type === "v2") {
|
|
421
|
+
return rows.map((item, i) => {
|
|
422
|
+
if (i !== path.index || !isPageSectionV2(item)) return item;
|
|
423
|
+
return updater(item as PageSectionV2) as ContentItem;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
// parallaxSlide path
|
|
427
|
+
return rows.map((item, i) => {
|
|
428
|
+
if (i !== path.groupIndex || !isParallaxGroup(item)) return item;
|
|
429
|
+
const group = item as ParallaxGroup;
|
|
430
|
+
const newSlides = group.slides.map((slide, j) => {
|
|
431
|
+
if (j !== path.slideIndex) return slide;
|
|
432
|
+
const virtualSection: PageSectionV2 = {
|
|
433
|
+
_type: "pageSectionV2",
|
|
434
|
+
_key: slide._key,
|
|
435
|
+
section_type: "empty-v2",
|
|
436
|
+
columns: slide.columns,
|
|
437
|
+
settings: slide.section_settings,
|
|
438
|
+
};
|
|
439
|
+
const updated = updater(virtualSection);
|
|
440
|
+
return {
|
|
441
|
+
...slide,
|
|
442
|
+
columns: updated.columns,
|
|
443
|
+
section_settings: updated.settings,
|
|
444
|
+
};
|
|
445
|
+
});
|
|
446
|
+
return { ...group, slides: newSlides } as ContentItem;
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================
|
|
451
|
+
// addParallaxGroupInState
|
|
452
|
+
// ============================================
|
|
453
|
+
|
|
454
|
+
export interface AddParallaxGroupResult {
|
|
455
|
+
rows: ContentItem[];
|
|
456
|
+
newGroupKey: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Create a new ParallaxGroup with 1 empty slide and insert it into the rows.
|
|
461
|
+
*/
|
|
462
|
+
export function addParallaxGroupInState(
|
|
463
|
+
rows: ContentItem[],
|
|
464
|
+
afterRowKey?: string | null
|
|
465
|
+
): AddParallaxGroupResult {
|
|
466
|
+
const defaultSlide: ParallaxSlideV2 = createDefaultParallaxSlide();
|
|
467
|
+
|
|
468
|
+
const newGroup: ParallaxGroup = {
|
|
469
|
+
_type: "parallaxGroup",
|
|
470
|
+
_key: generateKey(),
|
|
471
|
+
slides: [defaultSlide],
|
|
472
|
+
transition_effect: "parallax",
|
|
473
|
+
snap_enabled: true,
|
|
474
|
+
parallax_intensity: 0.4,
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const newRows = [...rows];
|
|
478
|
+
if (afterRowKey) {
|
|
479
|
+
const idx = newRows.findIndex((r) => r._key === afterRowKey);
|
|
480
|
+
if (idx !== -1) {
|
|
481
|
+
newRows.splice(idx + 1, 0, newGroup);
|
|
482
|
+
} else {
|
|
483
|
+
newRows.push(newGroup);
|
|
484
|
+
}
|
|
485
|
+
} else {
|
|
486
|
+
newRows.push(newGroup);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { rows: newRows, newGroupKey: newGroup._key };
|
|
490
|
+
}
|