@morphika/andami 0.4.1 → 0.4.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/README.md +2 -1
- package/components/builder/ColumnDragOverlay.tsx +21 -6
- package/components/builder/hooks/useColumnDrag.ts +64 -11
- package/lib/builder/store-blocks.ts +2 -2
- package/lib/builder/store-canvas.ts +2 -2
- package/lib/builder/store-cover.ts +2 -2
- package/lib/builder/store-helpers.ts +43 -0
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +387 -0
- package/lib/builder/types.ts +77 -225
- package/lib/version.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ A reusable Visual Page Builder framework for Next.js. Build custom websites with
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- **Visual Page Builder** — Infinite canvas editor with device previews (desktop, tablet, phone)
|
|
10
|
-
- **
|
|
10
|
+
- **6 Content Blocks** — Text, Image, Image Grid, Video, Spacer, Button (added via "+ Add Block" inside columns)
|
|
11
|
+
- **2 Section-level Blocks** — Project Grid (masonry) and Project Carousel (horizontal "keep browsing" at end of project pages). Added via "+ Add Section"
|
|
11
12
|
- **Cover Sections** — Full-viewport hero sections with proportional rows, background media, and drag-to-resize
|
|
12
13
|
- **V2 Grid System** — 12-column CSS grid with push cascade engine and responsive overrides
|
|
13
14
|
- **Custom Sections** — Create reusable sections with per-instance setting overrides
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
6
|
-
import type { PageSectionV2 } from "../../lib/sanity/types";
|
|
7
|
-
import { isPageSectionV2 } from "../../lib/sanity/types";
|
|
6
|
+
import type { PageSectionV2, CoverSection, SectionColumn } from "../../lib/sanity/types";
|
|
7
|
+
import { isPageSectionV2, isCoverSection } from "../../lib/sanity/types";
|
|
8
8
|
import { BUILDER_BLUE } from "../../lib/builder/constants";
|
|
9
9
|
|
|
10
10
|
interface ColumnDragOverlayProps {
|
|
@@ -20,14 +20,29 @@ const ColumnDragOverlay = memo(function ColumnDragOverlay({
|
|
|
20
20
|
}: ColumnDragOverlayProps) {
|
|
21
21
|
const rows = useBuilderStore((s) => s.rows);
|
|
22
22
|
const item = rows.find((r) => r._key === sectionKey);
|
|
23
|
-
if (!item
|
|
23
|
+
if (!item) return null;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Accept both PageSectionV2 and CoverSection — both expose `columns:
|
|
26
|
+
// SectionColumn[]` and `settings.grid_columns`. Other section types
|
|
27
|
+
// (parallax, custom instance) are not column-draggable targets.
|
|
28
|
+
let columns: SectionColumn[] | undefined;
|
|
29
|
+
let gridColumns = 12;
|
|
30
|
+
if (isPageSectionV2(item)) {
|
|
31
|
+
const v2 = item as PageSectionV2;
|
|
32
|
+
columns = v2.columns;
|
|
33
|
+
gridColumns = v2.settings?.grid_columns || 12;
|
|
34
|
+
} else if (isCoverSection(item)) {
|
|
35
|
+
const cover = item as CoverSection;
|
|
36
|
+
columns = cover.columns;
|
|
37
|
+
gridColumns = cover.settings?.grid_columns || 12;
|
|
38
|
+
} else {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const col = columns?.find((c) => c._key === columnKey);
|
|
27
43
|
if (!col) return null;
|
|
28
44
|
|
|
29
45
|
const blockCount = (col.blocks || []).length;
|
|
30
|
-
const gridColumns = v2Section.settings.grid_columns || 12;
|
|
31
46
|
|
|
32
47
|
const overlay = (
|
|
33
48
|
<div
|
|
@@ -2,12 +2,68 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
4
4
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
-
import type { PageSectionV2 } from "../../../lib/sanity/types";
|
|
6
|
-
import { isPageSectionV2 } from "../../../lib/sanity/types";
|
|
5
|
+
import type { PageSectionV2, CoverSection, ContentItem } from "../../../lib/sanity/types";
|
|
6
|
+
import { isPageSectionV2, isCoverSection } from "../../../lib/sanity/types";
|
|
7
7
|
import { getEffectiveColumnsV2, buildColumnV2Overrides } from "../settings-panel/responsive-helpers";
|
|
8
8
|
import { moveColumn as cascadeMoveColumn } from "../../../lib/builder/cascade";
|
|
9
9
|
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* View a Cover section as a PageSectionV2 for DnD purposes.
|
|
13
|
+
*
|
|
14
|
+
* Cover and V2 share the same column shape, grid_columns, and
|
|
15
|
+
* `responsive[vp].columns` shape — the fields read/written by column
|
|
16
|
+
* DnD. This shim lets us reuse getEffectiveColumnsV2/buildColumnV2Overrides
|
|
17
|
+
* without duplicating logic. Only `columns`, `settings.grid_columns`, and
|
|
18
|
+
* `responsive[vp].columns` are exposed; Cover-specific fields
|
|
19
|
+
* (`cover_rows`, Cover-specific `settings`) are hidden from the V2 helpers.
|
|
20
|
+
*
|
|
21
|
+
* The write-back path (`updateSectionV2Responsive` → `updateSectionAtPath`)
|
|
22
|
+
* merges column overrides back into the cover, preserving Cover-specific
|
|
23
|
+
* fields. See `store-helpers.ts :: updateSectionAtPath` cover branch.
|
|
24
|
+
*/
|
|
25
|
+
function coverAsV2(cover: CoverSection): PageSectionV2 {
|
|
26
|
+
return {
|
|
27
|
+
_type: "pageSectionV2",
|
|
28
|
+
_key: cover._key,
|
|
29
|
+
section_type: "empty-v2",
|
|
30
|
+
columns: cover.columns,
|
|
31
|
+
settings: {
|
|
32
|
+
preset: "custom",
|
|
33
|
+
grid_columns: cover.settings.grid_columns,
|
|
34
|
+
col_gap: cover.settings.col_gap,
|
|
35
|
+
row_gap: cover.settings.row_gap,
|
|
36
|
+
},
|
|
37
|
+
responsive:
|
|
38
|
+
cover.responsive?.tablet?.columns || cover.responsive?.phone?.columns
|
|
39
|
+
? {
|
|
40
|
+
...(cover.responsive.tablet?.columns
|
|
41
|
+
? { tablet: { columns: cover.responsive.tablet.columns } }
|
|
42
|
+
: {}),
|
|
43
|
+
...(cover.responsive.phone?.columns
|
|
44
|
+
? { phone: { columns: cover.responsive.phone.columns } }
|
|
45
|
+
: {}),
|
|
46
|
+
}
|
|
47
|
+
: undefined,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Find the section by key and return a V2-shaped view of it (PageSectionV2
|
|
53
|
+
* directly, or Cover adapted via `coverAsV2`). Returns null if the section
|
|
54
|
+
* doesn't exist or is an unsupported type (parallax, custom instance).
|
|
55
|
+
*/
|
|
56
|
+
function findColumnarSection(
|
|
57
|
+
rows: ContentItem[],
|
|
58
|
+
sectionKey: string,
|
|
59
|
+
): PageSectionV2 | null {
|
|
60
|
+
const section = rows.find((r) => r._key === sectionKey);
|
|
61
|
+
if (!section) return null;
|
|
62
|
+
if (isPageSectionV2(section)) return section as PageSectionV2;
|
|
63
|
+
if (isCoverSection(section)) return coverAsV2(section as CoverSection);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
11
67
|
// ============================================
|
|
12
68
|
// Types
|
|
13
69
|
// ============================================
|
|
@@ -57,9 +113,8 @@ function executeResponsiveSwap(
|
|
|
57
113
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
58
114
|
): void {
|
|
59
115
|
const rows = useBuilderStore.getState().rows;
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
const v2Section = section as PageSectionV2;
|
|
116
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
117
|
+
if (!v2Section) return;
|
|
63
118
|
|
|
64
119
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
65
120
|
const draggedCol = effectiveCols.find((c) => c._key === draggedKey);
|
|
@@ -109,9 +164,8 @@ function executeResponsiveGapMove(
|
|
|
109
164
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
110
165
|
): void {
|
|
111
166
|
const rows = useBuilderStore.getState().rows;
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
-
const v2Section = section as PageSectionV2;
|
|
167
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
168
|
+
if (!v2Section) return;
|
|
115
169
|
|
|
116
170
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
117
171
|
const columnOverrides = effectiveCols.map((c) => {
|
|
@@ -147,9 +201,8 @@ function executeResponsiveInsert(
|
|
|
147
201
|
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void
|
|
148
202
|
): void {
|
|
149
203
|
const rows = useBuilderStore.getState().rows;
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
const v2Section = section as PageSectionV2;
|
|
204
|
+
const v2Section = findColumnarSection(rows, sectionKey);
|
|
205
|
+
if (!v2Section) return;
|
|
153
206
|
|
|
154
207
|
const effectiveCols = getEffectiveColumnsV2(v2Section, viewport);
|
|
155
208
|
const cascadeResult = cascadeMoveColumn(effectiveCols, columnKey, targetRow, targetCol, v2Section.settings.grid_columns);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderStore, BuilderState } from "./types";
|
|
1
|
+
import type { BuilderStore, BuilderState, BlockSliceActions } from "./types";
|
|
2
2
|
import type { ContentBlock, ContentItem, PageSectionV2, ParallaxGroup, CoverSection } from "../../lib/sanity/types";
|
|
3
3
|
import { isPageSectionV2, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
|
|
4
4
|
import { createDefaultBlock } from "./defaults";
|
|
@@ -47,7 +47,7 @@ function applyBlockUpdate(
|
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
export function createBlockActions(set: StoreSet, get: StoreGet) {
|
|
50
|
+
export function createBlockActions(set: StoreSet, get: StoreGet): BlockSliceActions {
|
|
51
51
|
return {
|
|
52
52
|
// ---- Block operations ----
|
|
53
53
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderStore, BuilderState, CanvasTool, DeviceViewport, PageSettings } from "./types";
|
|
1
|
+
import type { BuilderStore, BuilderState, CanvasTool, DeviceViewport, PageSettings, CanvasSliceActions } from "./types";
|
|
2
2
|
import { DEFAULT_PAGE_SETTINGS, DEFAULT_GRID_SETTINGS, DEVICE_WIDTHS } from "./types";
|
|
3
3
|
import type { PageSectionV2 } from "../../lib/sanity/types";
|
|
4
4
|
import { isPageSectionV2 } from "../../lib/sanity/types";
|
|
@@ -11,7 +11,7 @@ type StoreSet = (
|
|
|
11
11
|
) => void;
|
|
12
12
|
type StoreGet = () => BuilderStore;
|
|
13
13
|
|
|
14
|
-
export function createCanvasActions(set: StoreSet, get: StoreGet) {
|
|
14
|
+
export function createCanvasActions(set: StoreSet, get: StoreGet): CanvasSliceActions {
|
|
15
15
|
return {
|
|
16
16
|
// ---- Editor mode ----
|
|
17
17
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Session 176: Cover Sections — Phase 3 (Store).
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { BuilderState } from "./types";
|
|
12
|
+
import type { BuilderState, CoverSliceActions } from "./types";
|
|
13
13
|
import type {
|
|
14
14
|
ContentItem,
|
|
15
15
|
CoverSection,
|
|
@@ -79,7 +79,7 @@ function updateCoverInRows(
|
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
export function createCoverActions(set: StoreSet, get: StoreGet) {
|
|
82
|
+
export function createCoverActions(set: StoreSet, get: StoreGet): CoverSliceActions {
|
|
83
83
|
return {
|
|
84
84
|
addCoverSection: (afterRowKey?: string | null): void => {
|
|
85
85
|
get()._pushSnapshot();
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
SectionColumn,
|
|
17
17
|
SectionV2Preset,
|
|
18
18
|
CoverSection,
|
|
19
|
+
CoverSectionResponsiveOverride,
|
|
19
20
|
} from "../../lib/sanity/types";
|
|
20
21
|
import { isPageSectionV2, isParallaxGroup, isCoverSection } from "../../lib/sanity/types";
|
|
21
22
|
import { columnsFromPreset, detectPreset } from "./cascade";
|
|
@@ -480,6 +481,25 @@ export function updateSectionAtPath(
|
|
|
480
481
|
return rows.map((item, i) => {
|
|
481
482
|
if (i !== path.index || !isCoverSection(item)) return item;
|
|
482
483
|
const cover = item as CoverSection;
|
|
484
|
+
|
|
485
|
+
// Passthrough of `responsive.tablet/phone.columns` only — V2 operations
|
|
486
|
+
// (column DnD, resize) read and write this field, and its shape
|
|
487
|
+
// (`ColumnOverride[]`) is identical between V2 and Cover. We do NOT
|
|
488
|
+
// expose Cover-specific fields (`cover_rows`, Cover `settings`) to the
|
|
489
|
+
// updater via virtualSection.responsive — those stay on the cover
|
|
490
|
+
// unchanged. This keeps the virtualSection's shape a true V2.
|
|
491
|
+
const virtualResponsive: PageSectionV2["responsive"] =
|
|
492
|
+
cover.responsive?.tablet?.columns || cover.responsive?.phone?.columns
|
|
493
|
+
? {
|
|
494
|
+
...(cover.responsive.tablet?.columns
|
|
495
|
+
? { tablet: { columns: cover.responsive.tablet.columns } }
|
|
496
|
+
: {}),
|
|
497
|
+
...(cover.responsive.phone?.columns
|
|
498
|
+
? { phone: { columns: cover.responsive.phone.columns } }
|
|
499
|
+
: {}),
|
|
500
|
+
}
|
|
501
|
+
: undefined;
|
|
502
|
+
|
|
483
503
|
const virtualSection: PageSectionV2 = {
|
|
484
504
|
_type: "pageSectionV2",
|
|
485
505
|
_key: cover._key,
|
|
@@ -497,11 +517,34 @@ export function updateSectionAtPath(
|
|
|
497
517
|
enter_animation: cover.settings.enter_animation,
|
|
498
518
|
stagger: cover.settings.stagger,
|
|
499
519
|
},
|
|
520
|
+
responsive: virtualResponsive,
|
|
500
521
|
};
|
|
501
522
|
const updated = updater(virtualSection);
|
|
523
|
+
|
|
524
|
+
// Merge updated `responsive.columns` back into cover.responsive,
|
|
525
|
+
// preserving `cover_rows` and Cover-specific `settings` per viewport.
|
|
526
|
+
const mergedResponsive: CoverSection["responsive"] = {};
|
|
527
|
+
for (const vp of ["tablet", "phone"] as const) {
|
|
528
|
+
const existing: CoverSectionResponsiveOverride = {
|
|
529
|
+
...(cover.responsive?.[vp] || {}),
|
|
530
|
+
};
|
|
531
|
+
const updatedCols = updated.responsive?.[vp]?.columns;
|
|
532
|
+
if (updatedCols === undefined) {
|
|
533
|
+
delete existing.columns;
|
|
534
|
+
} else {
|
|
535
|
+
existing.columns = updatedCols;
|
|
536
|
+
}
|
|
537
|
+
if (existing.columns || existing.cover_rows || existing.settings) {
|
|
538
|
+
mergedResponsive[vp] = existing;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
502
542
|
return {
|
|
503
543
|
...cover,
|
|
504
544
|
columns: updated.columns,
|
|
545
|
+
responsive: Object.keys(mergedResponsive).length
|
|
546
|
+
? mergedResponsive
|
|
547
|
+
: undefined,
|
|
505
548
|
settings: {
|
|
506
549
|
...cover.settings,
|
|
507
550
|
grid_columns: updated.settings.grid_columns,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BuilderStore, BuilderState, BlockType } from "./types";
|
|
1
|
+
import type { BuilderStore, BuilderState, BlockType, SectionSliceActions } from "./types";
|
|
2
2
|
import type {
|
|
3
3
|
ContentBlock,
|
|
4
4
|
ContentItem,
|
|
@@ -45,7 +45,7 @@ type StoreSet = (
|
|
|
45
45
|
) => void;
|
|
46
46
|
type StoreGet = () => BuilderStore;
|
|
47
47
|
|
|
48
|
-
export function createSectionActions(set: StoreSet, get: StoreGet) {
|
|
48
|
+
export function createSectionActions(set: StoreSet, get: StoreGet): SectionSliceActions {
|
|
49
49
|
return {
|
|
50
50
|
// ---- Section operations ----
|
|
51
51
|
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// Builder Store — Slice Types
|
|
3
|
+
// ============================================
|
|
4
|
+
// The builder store is composed of 7 logical slices. Each slice owns a
|
|
5
|
+
// subset of the state and a subset of the actions. `BuilderState` and
|
|
6
|
+
// `BuilderActions` in `./types.ts` are declared as intersections of
|
|
7
|
+
// these slices, so the runtime shape stays flat — no `state.blocks.rows`
|
|
8
|
+
// indirection. The split is purely a type-level organization.
|
|
9
|
+
//
|
|
10
|
+
// Slice owners (who writes what):
|
|
11
|
+
// - MetaSlice : page identity, draft/publish, dirty/saving flags
|
|
12
|
+
// - SectionSlice : rows (ContentItem[]), section editor mode, custom section cache, parallax groups
|
|
13
|
+
// - BlockSlice : block CRUD + debounced update (writes rows, too)
|
|
14
|
+
// - CanvasSlice : canvas viewport, preview mode, page/grid settings
|
|
15
|
+
// - CoverSlice : cover-section-specific row + background + settings ops
|
|
16
|
+
// - SelectionSlice : selection keys, color picker preview
|
|
17
|
+
// - HistorySlice : undo/redo + snapshot push
|
|
18
|
+
//
|
|
19
|
+
// Cross-slice writes are legitimate (e.g. deleteBlock clears selectedBlockKey).
|
|
20
|
+
// Fase 1 does NOT narrow the `set` signature — that is Fase 2, when the
|
|
21
|
+
// runtime shape is actually split into nested slice objects.
|
|
22
|
+
//
|
|
23
|
+
// Session 183 (Fase 1 / Nivel A).
|
|
24
|
+
// ============================================
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
Page,
|
|
28
|
+
PageType,
|
|
29
|
+
ContentBlock,
|
|
30
|
+
ContentItem,
|
|
31
|
+
PageSectionV2,
|
|
32
|
+
SectionV2Settings,
|
|
33
|
+
SectionV2Preset,
|
|
34
|
+
PageMetadata,
|
|
35
|
+
ColorField,
|
|
36
|
+
CoverSection,
|
|
37
|
+
CoverSectionSettings,
|
|
38
|
+
CoverRow,
|
|
39
|
+
} from "../../lib/sanity/types";
|
|
40
|
+
import type { BlockType } from "./types";
|
|
41
|
+
import type { PageSettings, GridSettings, CanvasTool, DeviceViewport } from "./types";
|
|
42
|
+
|
|
43
|
+
// ============================================
|
|
44
|
+
// MetaSlice — page identity and persistence flags
|
|
45
|
+
// ============================================
|
|
46
|
+
|
|
47
|
+
export interface MetaSliceState {
|
|
48
|
+
pageId: string | null;
|
|
49
|
+
pageTitle: string;
|
|
50
|
+
pageSlug: string;
|
|
51
|
+
/** The slug as loaded from Sanity — used for API calls even if slug is edited. */
|
|
52
|
+
_originalSlug: string;
|
|
53
|
+
pageType: PageType;
|
|
54
|
+
metadata: PageMetadata;
|
|
55
|
+
publishedAt: string | null;
|
|
56
|
+
draftMode: boolean;
|
|
57
|
+
|
|
58
|
+
isDirty: boolean;
|
|
59
|
+
isSaving: boolean;
|
|
60
|
+
saveError: string | null;
|
|
61
|
+
lastSavedAt: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MetaSliceActions {
|
|
65
|
+
setPageTitle: (title: string) => void;
|
|
66
|
+
setPageSlug: (slug: string) => void;
|
|
67
|
+
setMetadata: (metadata: Partial<PageMetadata>) => void;
|
|
68
|
+
setDraftMode: (draft: boolean) => void;
|
|
69
|
+
publishPage: () => void;
|
|
70
|
+
unpublishPage: () => void;
|
|
71
|
+
|
|
72
|
+
loadFromDocument: (doc: Page) => void;
|
|
73
|
+
save: () => Promise<void>;
|
|
74
|
+
reset: () => void;
|
|
75
|
+
|
|
76
|
+
markDirty: () => void;
|
|
77
|
+
markClean: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type MetaSlice = MetaSliceState & MetaSliceActions;
|
|
81
|
+
|
|
82
|
+
// ============================================
|
|
83
|
+
// SectionSlice — content rows + section editor + custom sections + parallax
|
|
84
|
+
// ============================================
|
|
85
|
+
|
|
86
|
+
export interface SectionSliceState {
|
|
87
|
+
/** Content rows: V2 sections, custom section instances, parallax groups, cover sections. */
|
|
88
|
+
rows: ContentItem[];
|
|
89
|
+
|
|
90
|
+
/** Cache of fetched custom section base settings, keyed by custom_section_id. */
|
|
91
|
+
_customSectionCache: Record<string, SectionV2Settings>;
|
|
92
|
+
|
|
93
|
+
/** Counter incremented after a custom section is saved — triggers refetch in consumers. */
|
|
94
|
+
_customSectionRefetchTick: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SectionSliceActions {
|
|
98
|
+
// Section-level (top-level row) operations
|
|
99
|
+
addSection: (
|
|
100
|
+
blockType: "projectGridBlock" | "projectCarouselBlock",
|
|
101
|
+
afterRowKey?: string | null,
|
|
102
|
+
) => void;
|
|
103
|
+
reorderRows: (fromIndex: number, toIndex: number) => void;
|
|
104
|
+
deleteSection: (sectionKey: string) => void;
|
|
105
|
+
duplicateSection: (sectionKey: string) => void;
|
|
106
|
+
|
|
107
|
+
// V2 section operations
|
|
108
|
+
addSectionV2: (preset: SectionV2Preset, afterRowKey?: string | null) => void;
|
|
109
|
+
addColumnV2: (sectionKey: string, gridRow: number, gridColumn: number, span: number) => void;
|
|
110
|
+
deleteColumnV2: (sectionKey: string, columnKey: string) => void;
|
|
111
|
+
resizeColumnV2: (sectionKey: string, columnKey: string, newSpan: number) => void;
|
|
112
|
+
resizeColumnV2Left: (sectionKey: string, columnKey: string, newGridColumn: number) => void;
|
|
113
|
+
moveColumnV2: (
|
|
114
|
+
sectionKey: string,
|
|
115
|
+
columnKey: string,
|
|
116
|
+
targetRow: number,
|
|
117
|
+
targetColumn: number,
|
|
118
|
+
) => void;
|
|
119
|
+
swapColumnV2: (sectionKey: string, draggedKey: string, targetKey: string) => void;
|
|
120
|
+
moveColumnToGapV2: (
|
|
121
|
+
sectionKey: string,
|
|
122
|
+
columnKey: string,
|
|
123
|
+
targetRow: number,
|
|
124
|
+
targetColumn: number,
|
|
125
|
+
targetSpan: number,
|
|
126
|
+
) => void;
|
|
127
|
+
applyPresetV2: (sectionKey: string, preset: SectionV2Preset) => void;
|
|
128
|
+
updateSectionV2Settings: (sectionKey: string, settings: Partial<SectionV2Settings>) => void;
|
|
129
|
+
updateSectionV2Responsive: (
|
|
130
|
+
sectionKey: string,
|
|
131
|
+
responsive: PageSectionV2["responsive"],
|
|
132
|
+
) => void;
|
|
133
|
+
addBlockV2: (
|
|
134
|
+
sectionKey: string,
|
|
135
|
+
columnKey: string,
|
|
136
|
+
blockType: BlockType,
|
|
137
|
+
insertIndex?: number,
|
|
138
|
+
) => void;
|
|
139
|
+
updateColumnEnterAnimation: (
|
|
140
|
+
sectionKey: string,
|
|
141
|
+
colKey: string,
|
|
142
|
+
config: import("../../lib/animation/enter-types").EnterAnimationConfig | undefined,
|
|
143
|
+
) => void;
|
|
144
|
+
|
|
145
|
+
/** Select a V2 column — sets selectedRowKey + selectedColumnKey. */
|
|
146
|
+
selectColumnV2: (sectionKey: string | null, columnKey: string | null) => void;
|
|
147
|
+
|
|
148
|
+
// Custom section instance operations (reference lifecycle on the page)
|
|
149
|
+
addCustomSectionInstance: (
|
|
150
|
+
id: string,
|
|
151
|
+
slug: string,
|
|
152
|
+
title: string,
|
|
153
|
+
afterRowKey?: string | null,
|
|
154
|
+
) => void;
|
|
155
|
+
detachCustomSectionInstance: (instanceKey: string, sectionData: PageSectionV2) => void;
|
|
156
|
+
updateCustomSectionInstanceTitle: (instanceKey: string, newTitle: string) => void;
|
|
157
|
+
updateCustomSectionInstanceSettings: (
|
|
158
|
+
instanceKey: string,
|
|
159
|
+
updates: Partial<SectionV2Settings>,
|
|
160
|
+
) => void;
|
|
161
|
+
cacheCustomSectionSettings: (sectionId: string, settings: SectionV2Settings) => void;
|
|
162
|
+
|
|
163
|
+
// Parallax Group operations (Session 123)
|
|
164
|
+
addParallaxGroup: (afterRowKey?: string | null) => void;
|
|
165
|
+
addParallaxSlide: (groupKey: string) => void;
|
|
166
|
+
removeParallaxSlide: (groupKey: string, slideKey: string) => void;
|
|
167
|
+
moveParallaxSlide: (groupKey: string, slideKey: string, direction: "up" | "down") => void;
|
|
168
|
+
updateParallaxSlideBackground: (
|
|
169
|
+
groupKey: string,
|
|
170
|
+
slideKey: string,
|
|
171
|
+
fields: Partial<import("../../lib/sanity/types").ParallaxSlideV2>,
|
|
172
|
+
) => void;
|
|
173
|
+
updateParallaxGroupSettings: (
|
|
174
|
+
groupKey: string,
|
|
175
|
+
fields: Partial<
|
|
176
|
+
Pick<
|
|
177
|
+
import("../../lib/sanity/types").ParallaxGroup,
|
|
178
|
+
"transition_effect" | "snap_enabled" | "parallax_intensity"
|
|
179
|
+
>
|
|
180
|
+
>,
|
|
181
|
+
) => void;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type SectionSlice = SectionSliceState & SectionSliceActions;
|
|
185
|
+
|
|
186
|
+
// ============================================
|
|
187
|
+
// BlockSlice — block CRUD (no exclusive state; writes rows)
|
|
188
|
+
// ============================================
|
|
189
|
+
|
|
190
|
+
// Block slice has no exclusive state — all block data lives inside `rows`
|
|
191
|
+
// (owned by SectionSlice). Block operations legitimately write to rows and
|
|
192
|
+
// to selection keys (e.g. deleteBlock clears selectedBlockKey).
|
|
193
|
+
|
|
194
|
+
export interface BlockSliceActions {
|
|
195
|
+
updateBlock: (blockKey: string, updates: Partial<ContentBlock>) => void;
|
|
196
|
+
deleteBlock: (blockKey: string) => void;
|
|
197
|
+
duplicateBlock: (blockKey: string) => void;
|
|
198
|
+
moveBlock: (
|
|
199
|
+
blockKey: string,
|
|
200
|
+
targetSectionKey: string,
|
|
201
|
+
targetColumnKey: string,
|
|
202
|
+
toIndex: number,
|
|
203
|
+
) => void;
|
|
204
|
+
reorderBlocks: (
|
|
205
|
+
sectionKey: string,
|
|
206
|
+
columnKey: string,
|
|
207
|
+
fromIndex: number,
|
|
208
|
+
toIndex: number,
|
|
209
|
+
) => void;
|
|
210
|
+
/** Debounced block update — batches keystrokes, does not push history per call. */
|
|
211
|
+
updateBlockDebounced: (blockKey: string, updates: Partial<ContentBlock>) => void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export type BlockSlice = BlockSliceActions;
|
|
215
|
+
|
|
216
|
+
// ============================================
|
|
217
|
+
// CanvasSlice — viewport, preview mode, page + grid settings
|
|
218
|
+
// ============================================
|
|
219
|
+
|
|
220
|
+
export interface CanvasSliceState {
|
|
221
|
+
// Editor mode
|
|
222
|
+
previewMode: boolean;
|
|
223
|
+
|
|
224
|
+
// Custom section editor mode (Session 107) — the canvas swaps its
|
|
225
|
+
// rows between the current page and the custom section being edited,
|
|
226
|
+
// so this is modelled as a canvas concern.
|
|
227
|
+
editorMode: "page" | "customSection";
|
|
228
|
+
customSectionSlug: string | null;
|
|
229
|
+
customSectionTitle: string | null;
|
|
230
|
+
/** Stashed page state while editing a custom section */
|
|
231
|
+
savedPageState: { rows: ContentItem[]; selectedKey: string | null } | null;
|
|
232
|
+
|
|
233
|
+
// Page-level settings (global per page, persisted to Sanity)
|
|
234
|
+
pageSettings: PageSettings;
|
|
235
|
+
|
|
236
|
+
// Grid settings (from Customize — ephemeral, NOT saved per page)
|
|
237
|
+
gridSettings: GridSettings;
|
|
238
|
+
|
|
239
|
+
// Canvas viewport state (ephemeral)
|
|
240
|
+
canvasZoom: number;
|
|
241
|
+
canvasPanX: number;
|
|
242
|
+
canvasPanY: number;
|
|
243
|
+
canvasTool: CanvasTool;
|
|
244
|
+
activeViewport: DeviceViewport;
|
|
245
|
+
|
|
246
|
+
// BUG-014 fix: Track whether the Sanity document had page_settings.
|
|
247
|
+
// When true, applyGlobalStyles() won't overwrite user-chosen colors.
|
|
248
|
+
_hasDocumentPageSettings: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface CanvasSliceActions {
|
|
252
|
+
togglePreviewMode: () => void;
|
|
253
|
+
setPreviewMode: (preview: boolean) => void;
|
|
254
|
+
|
|
255
|
+
// Custom section editor mode (Session 107)
|
|
256
|
+
/** Enter section editor mode — stashes current page rows, replaces with a single V2 section */
|
|
257
|
+
enterSectionEditor: (
|
|
258
|
+
slug: string | null,
|
|
259
|
+
title: string | null,
|
|
260
|
+
sectionData: PageSectionV2 | null,
|
|
261
|
+
) => void;
|
|
262
|
+
/** Exit section editor mode — restores page rows from stash. Pass wasSaved=true to mark page dirty. */
|
|
263
|
+
exitSectionEditor: (wasSaved?: boolean) => void;
|
|
264
|
+
/** Save the custom section being edited, then exit editor mode.
|
|
265
|
+
* Returns { id, slug, title } on success (for instance insertion after creation). */
|
|
266
|
+
saveSectionEditor: (
|
|
267
|
+
title: string,
|
|
268
|
+
) => Promise<{ id: string; slug: string; title: string } | null>;
|
|
269
|
+
|
|
270
|
+
updatePageSettings: (settings: Partial<PageSettings>) => void;
|
|
271
|
+
applyGlobalStyles: () => Promise<void>;
|
|
272
|
+
|
|
273
|
+
setCanvasZoom: (zoom: number) => void;
|
|
274
|
+
setCanvasPan: (x: number, y: number) => void;
|
|
275
|
+
setCanvasTool: (tool: CanvasTool) => void;
|
|
276
|
+
setActiveViewport: (viewport: DeviceViewport) => void;
|
|
277
|
+
zoomToFit: (viewportWidth: number, viewportHeight: number) => void;
|
|
278
|
+
zoomToPoint: (newZoom: number, cursorX: number, cursorY: number) => void;
|
|
279
|
+
zoomToFrame: (
|
|
280
|
+
device: DeviceViewport,
|
|
281
|
+
viewportWidth: number,
|
|
282
|
+
viewportHeight: number,
|
|
283
|
+
) => void;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export type CanvasSlice = CanvasSliceState & CanvasSliceActions;
|
|
287
|
+
|
|
288
|
+
// ============================================
|
|
289
|
+
// CoverSlice — cover-section-specific row + background + settings ops
|
|
290
|
+
// ============================================
|
|
291
|
+
|
|
292
|
+
// Cover slice has no exclusive state — cover sections live inside `rows`
|
|
293
|
+
// (SectionSlice). Column/block ops within cover sections reuse V2 actions
|
|
294
|
+
// via findSectionPath() which supports cover sections.
|
|
295
|
+
|
|
296
|
+
export interface CoverSliceActions {
|
|
297
|
+
addCoverSection: (afterRowKey?: string | null) => void;
|
|
298
|
+
addCoverRow: (sectionKey: string) => void;
|
|
299
|
+
removeCoverRow: (sectionKey: string, rowKey: string) => void;
|
|
300
|
+
resizeCoverRow: (
|
|
301
|
+
sectionKey: string,
|
|
302
|
+
handleIndex: number,
|
|
303
|
+
deltaPercent: number,
|
|
304
|
+
startAbove: number,
|
|
305
|
+
startBelow: number,
|
|
306
|
+
) => void;
|
|
307
|
+
updateCoverRowAlign: (
|
|
308
|
+
sectionKey: string,
|
|
309
|
+
rowKey: string,
|
|
310
|
+
align: CoverRow["vertical_align"],
|
|
311
|
+
) => void;
|
|
312
|
+
updateCoverBackground: (
|
|
313
|
+
sectionKey: string,
|
|
314
|
+
fields: Partial<
|
|
315
|
+
Pick<
|
|
316
|
+
CoverSection,
|
|
317
|
+
| "background_type"
|
|
318
|
+
| "background_color"
|
|
319
|
+
| "background_image"
|
|
320
|
+
| "background_video"
|
|
321
|
+
| "background_position"
|
|
322
|
+
| "background_size"
|
|
323
|
+
| "background_overlay_color"
|
|
324
|
+
| "background_overlay_opacity"
|
|
325
|
+
| "nav_color"
|
|
326
|
+
>
|
|
327
|
+
>,
|
|
328
|
+
) => void;
|
|
329
|
+
updateCoverSettings: (sectionKey: string, settings: Partial<CoverSectionSettings>) => void;
|
|
330
|
+
updateCoverHeight: (sectionKey: string, height: CoverSection["height"]) => void;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export type CoverSlice = CoverSliceActions;
|
|
334
|
+
|
|
335
|
+
// ============================================
|
|
336
|
+
// SelectionSlice — selection keys + color picker preview
|
|
337
|
+
// ============================================
|
|
338
|
+
|
|
339
|
+
export interface SelectionSliceState {
|
|
340
|
+
selectedRowKey: string | null;
|
|
341
|
+
selectedColumnKey: string | null;
|
|
342
|
+
selectedBlockKey: string | null;
|
|
343
|
+
/** Sub-selection: which project card is selected within a ProjectGrid block */
|
|
344
|
+
selectedProjectCardKey: string | null;
|
|
345
|
+
|
|
346
|
+
/** Live preview overlay from color picker — shown on canvas without persisting to Sanity.
|
|
347
|
+
* Set while user is dragging in the color picker; cleared on close. */
|
|
348
|
+
colorPickerPreview: {
|
|
349
|
+
blockKey?: string;
|
|
350
|
+
sectionKey?: string;
|
|
351
|
+
field: string;
|
|
352
|
+
value: ColorField;
|
|
353
|
+
} | null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface SelectionSliceActions {
|
|
357
|
+
selectRow: (key: string | null) => void;
|
|
358
|
+
selectColumn: (rowKey: string | null, colKey: string | null) => void;
|
|
359
|
+
selectBlock: (key: string | null) => void;
|
|
360
|
+
selectProjectCard: (key: string | null) => void;
|
|
361
|
+
clearSelection: () => void;
|
|
362
|
+
|
|
363
|
+
setColorPickerPreview: (preview: SelectionSliceState["colorPickerPreview"]) => void;
|
|
364
|
+
clearColorPickerPreview: () => void;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export type SelectionSlice = SelectionSliceState & SelectionSliceActions;
|
|
368
|
+
|
|
369
|
+
// ============================================
|
|
370
|
+
// HistorySlice — undo/redo + snapshot push
|
|
371
|
+
// ============================================
|
|
372
|
+
|
|
373
|
+
export interface HistorySliceState {
|
|
374
|
+
_history: import("./history").HistorySnapshot[];
|
|
375
|
+
_future: import("./history").HistorySnapshot[];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export interface HistorySliceActions {
|
|
379
|
+
undo: () => void;
|
|
380
|
+
redo: () => void;
|
|
381
|
+
canUndo: () => boolean;
|
|
382
|
+
canRedo: () => boolean;
|
|
383
|
+
/** Push a snapshot before a mutation (called internally by block/section mutations). */
|
|
384
|
+
_pushSnapshot: () => void;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export type HistorySlice = HistorySliceState & HistorySliceActions;
|
package/lib/builder/types.ts
CHANGED
|
@@ -182,231 +182,83 @@ export const DEFAULT_GRID_SETTINGS: GridSettings = {
|
|
|
182
182
|
gutter_phone: "16",
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Content — V2 sections, custom sections, parallax groups
|
|
198
|
-
rows: ContentItem[];
|
|
199
|
-
|
|
200
|
-
// Selection
|
|
201
|
-
selectedRowKey: string | null;
|
|
202
|
-
selectedColumnKey: string | null;
|
|
203
|
-
selectedBlockKey: string | null;
|
|
204
|
-
/** Sub-selection: which project card is selected within a ProjectGrid block */
|
|
205
|
-
selectedProjectCardKey: string | null;
|
|
206
|
-
|
|
207
|
-
// UI state
|
|
208
|
-
isDirty: boolean;
|
|
209
|
-
isSaving: boolean;
|
|
210
|
-
saveError: string | null;
|
|
211
|
-
lastSavedAt: string | null;
|
|
212
|
-
|
|
213
|
-
// Editor mode
|
|
214
|
-
previewMode: boolean;
|
|
215
|
-
|
|
216
|
-
// Custom section editor mode (Session 107)
|
|
217
|
-
editorMode: "page" | "customSection";
|
|
218
|
-
customSectionSlug: string | null;
|
|
219
|
-
customSectionTitle: string | null;
|
|
220
|
-
/** Stashed page state while editing a custom section */
|
|
221
|
-
savedPageState: { rows: ContentItem[]; selectedKey: string | null } | null;
|
|
222
|
-
|
|
223
|
-
// Page-level settings
|
|
224
|
-
pageSettings: PageSettings;
|
|
225
|
-
|
|
226
|
-
// Grid settings (from Customize, ephemeral — NOT saved per page)
|
|
227
|
-
gridSettings: GridSettings;
|
|
228
|
-
|
|
229
|
-
// Canvas state (ephemeral — NOT saved to Sanity)
|
|
230
|
-
canvasZoom: number;
|
|
231
|
-
canvasPanX: number;
|
|
232
|
-
canvasPanY: number;
|
|
233
|
-
canvasTool: CanvasTool;
|
|
234
|
-
activeViewport: DeviceViewport;
|
|
235
|
-
|
|
236
|
-
// BUG-014 fix: Track whether the Sanity document had page_settings.
|
|
237
|
-
// When true, applyGlobalStyles() won't overwrite user-chosen colors.
|
|
238
|
-
_hasDocumentPageSettings: boolean;
|
|
239
|
-
|
|
240
|
-
// History (Undo/Redo) — BUG-010 fix: snapshots include pageSettings
|
|
241
|
-
_history: import("./history").HistorySnapshot[];
|
|
242
|
-
_future: import("./history").HistorySnapshot[];
|
|
243
|
-
|
|
244
|
-
/** Cache of fetched custom section base settings, keyed by custom_section_id.
|
|
245
|
-
* Populated by CustomSectionInstanceCard/ReadOnlyCustomSection on fetch.
|
|
246
|
-
* Used by SortableRow to merge base settings with per-instance overrides. */
|
|
247
|
-
_customSectionCache: Record<string, import("../../lib/sanity/types").SectionV2Settings>;
|
|
248
|
-
|
|
249
|
-
/** Counter incremented after a custom section is saved via the section editor.
|
|
250
|
-
* Used as a useEffect dependency in CustomSectionInstanceCard to trigger refetch. */
|
|
251
|
-
_customSectionRefetchTick: number;
|
|
252
|
-
|
|
253
|
-
/** Live preview overlay from color picker — shown on canvas without persisting to Sanity.
|
|
254
|
-
* Set while user is dragging in the color picker; cleared on close. */
|
|
255
|
-
colorPickerPreview: {
|
|
256
|
-
blockKey?: string;
|
|
257
|
-
sectionKey?: string;
|
|
258
|
-
field: string;
|
|
259
|
-
value: ColorField;
|
|
260
|
-
} | null;
|
|
261
|
-
}
|
|
185
|
+
// ============================================
|
|
186
|
+
// BuilderState & BuilderActions — composed from slices
|
|
187
|
+
// ============================================
|
|
188
|
+
// Session 183 (Fase 1 / Nivel A): state and actions are declared as
|
|
189
|
+
// intersections of per-slice types defined in `./types-slices.ts`.
|
|
190
|
+
// The runtime shape is still a flat object — no `state.blocks.rows`
|
|
191
|
+
// indirection. This split is purely a type-level organization that:
|
|
192
|
+
// 1. Documents which slice owns which state field / action
|
|
193
|
+
// 2. Provides a clear template for adding a new slice
|
|
194
|
+
// 3. Prepares for Fase 2 (actual shape split) without forcing it now
|
|
195
|
+
// ============================================
|
|
262
196
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
/** Move a column to an empty gap, adopting the gap's span */
|
|
330
|
-
moveColumnToGapV2: (sectionKey: string, columnKey: string, targetRow: number, targetColumn: number, targetSpan: number) => void;
|
|
331
|
-
/** Apply a layout preset to a V2 section (replaces columns + updates preset atomically) */
|
|
332
|
-
applyPresetV2: (sectionKey: string, preset: SectionV2Preset) => void;
|
|
333
|
-
/** Update settings for a V2 section */
|
|
334
|
-
updateSectionV2Settings: (sectionKey: string, settings: Partial<SectionV2Settings>) => void;
|
|
335
|
-
/** Update responsive overrides for a V2 section */
|
|
336
|
-
updateSectionV2Responsive: (sectionKey: string, responsive: PageSectionV2["responsive"]) => void;
|
|
337
|
-
/** Add a block to a V2 section column, optionally at a specific index */
|
|
338
|
-
addBlockV2: (sectionKey: string, columnKey: string, blockType: BlockType, insertIndex?: number) => void;
|
|
339
|
-
/** Select a V2 column */
|
|
340
|
-
selectColumnV2: (sectionKey: string | null, columnKey: string | null) => void;
|
|
341
|
-
/** Update enter animation for a V2 column (Session 117) */
|
|
342
|
-
updateColumnEnterAnimation: (sectionKey: string, colKey: string, config: import("../../lib/animation/enter-types").EnterAnimationConfig | undefined) => void;
|
|
343
|
-
|
|
344
|
-
// Debounced update (no snapshot per keystroke)
|
|
345
|
-
updateBlockDebounced: (blockKey: string, updates: Partial<ContentBlock>) => void;
|
|
346
|
-
|
|
347
|
-
// Editor mode
|
|
348
|
-
togglePreviewMode: () => void;
|
|
349
|
-
setPreviewMode: (preview: boolean) => void;
|
|
350
|
-
|
|
351
|
-
// Custom section editor mode (Session 107)
|
|
352
|
-
/** Enter section editor mode — stashes current page rows, replaces with a single V2 section */
|
|
353
|
-
enterSectionEditor: (slug: string | null, title: string | null, sectionData: PageSectionV2 | null) => void;
|
|
354
|
-
/** Exit section editor mode — restores page rows from stash. Pass wasSaved=true to mark page dirty. */
|
|
355
|
-
exitSectionEditor: (wasSaved?: boolean) => void;
|
|
356
|
-
/** Save the custom section being edited, then exit editor mode.
|
|
357
|
-
* Returns { id, slug, title } on success (for instance insertion after creation). */
|
|
358
|
-
saveSectionEditor: (title: string) => Promise<{ id: string; slug: string; title: string } | null>;
|
|
359
|
-
/** Insert a custom section instance reference into the page rows */
|
|
360
|
-
addCustomSectionInstance: (id: string, slug: string, title: string, afterRowKey?: string | null) => void;
|
|
361
|
-
/** Detach a custom section instance — replaces it with a copy of the section data (inline V2 section) */
|
|
362
|
-
detachCustomSectionInstance: (instanceKey: string, sectionData: PageSectionV2) => void;
|
|
363
|
-
/** Update the cached title on a custom section instance (cosmetic sync, Session 110) */
|
|
364
|
-
updateCustomSectionInstanceTitle: (instanceKey: string, newTitle: string) => void;
|
|
365
|
-
/** Update per-instance section settings overrides (Session 130) */
|
|
366
|
-
updateCustomSectionInstanceSettings: (instanceKey: string, updates: Partial<SectionV2Settings>) => void;
|
|
367
|
-
/** Cache base settings from a fetched custom section (for SortableRow merge) */
|
|
368
|
-
cacheCustomSectionSettings: (sectionId: string, settings: SectionV2Settings) => void;
|
|
369
|
-
|
|
370
|
-
// ---- Parallax Group operations (Session 123) ----
|
|
371
|
-
/** Add a new ParallaxGroup with 1 empty slide */
|
|
372
|
-
addParallaxGroup: (afterRowKey?: string | null) => void;
|
|
373
|
-
/** Add a new empty slide to a parallax group */
|
|
374
|
-
addParallaxSlide: (groupKey: string) => void;
|
|
375
|
-
/** Remove a slide from a parallax group (minimum 1 enforced) */
|
|
376
|
-
removeParallaxSlide: (groupKey: string, slideKey: string) => void;
|
|
377
|
-
/** Reorder a slide up or down within its group */
|
|
378
|
-
moveParallaxSlide: (groupKey: string, slideKey: string, direction: "up" | "down") => void;
|
|
379
|
-
/** Update background fields on a parallax slide */
|
|
380
|
-
updateParallaxSlideBackground: (groupKey: string, slideKey: string, fields: Partial<import("../../lib/sanity/types").ParallaxSlideV2>) => void;
|
|
381
|
-
/** Update group-level settings (transition effect, snap, etc.) */
|
|
382
|
-
updateParallaxGroupSettings: (groupKey: string, fields: Partial<Pick<import("../../lib/sanity/types").ParallaxGroup, "transition_effect" | "snap_enabled" | "parallax_intensity">>) => void;
|
|
383
|
-
|
|
384
|
-
// ---- Cover Section operations (Session 176) ----
|
|
385
|
-
addCoverSection: (afterRowKey?: string | null) => void;
|
|
386
|
-
addCoverRow: (sectionKey: string) => void;
|
|
387
|
-
removeCoverRow: (sectionKey: string, rowKey: string) => void;
|
|
388
|
-
resizeCoverRow: (sectionKey: string, handleIndex: number, deltaPercent: number, startAbove: number, startBelow: number) => void;
|
|
389
|
-
updateCoverRowAlign: (sectionKey: string, rowKey: string, align: CoverRow["vertical_align"]) => void;
|
|
390
|
-
updateCoverBackground: (sectionKey: string, fields: Partial<Pick<CoverSection, "background_type" | "background_color" | "background_image" | "background_video" | "background_position" | "background_size" | "background_overlay_color" | "background_overlay_opacity" | "nav_color">>) => void;
|
|
391
|
-
updateCoverSettings: (sectionKey: string, settings: Partial<CoverSectionSettings>) => void;
|
|
392
|
-
updateCoverHeight: (sectionKey: string, height: CoverSection["height"]) => void;
|
|
393
|
-
|
|
394
|
-
// Page-level settings
|
|
395
|
-
updatePageSettings: (settings: Partial<PageSettings>) => void;
|
|
396
|
-
applyGlobalStyles: () => Promise<void>;
|
|
397
|
-
|
|
398
|
-
// Canvas
|
|
399
|
-
setCanvasZoom: (zoom: number) => void;
|
|
400
|
-
setCanvasPan: (x: number, y: number) => void;
|
|
401
|
-
setCanvasTool: (tool: CanvasTool) => void;
|
|
402
|
-
setActiveViewport: (viewport: DeviceViewport) => void;
|
|
403
|
-
zoomToFit: (viewportWidth: number, viewportHeight: number) => void;
|
|
404
|
-
zoomToPoint: (newZoom: number, cursorX: number, cursorY: number) => void;
|
|
405
|
-
zoomToFrame: (device: DeviceViewport, viewportWidth: number, viewportHeight: number) => void;
|
|
406
|
-
|
|
407
|
-
// Color picker live preview (Phase 4)
|
|
408
|
-
setColorPickerPreview: (preview: BuilderState["colorPickerPreview"]) => void;
|
|
409
|
-
clearColorPickerPreview: () => void;
|
|
410
|
-
}
|
|
197
|
+
import type {
|
|
198
|
+
MetaSlice,
|
|
199
|
+
MetaSliceState,
|
|
200
|
+
MetaSliceActions,
|
|
201
|
+
SectionSlice,
|
|
202
|
+
SectionSliceState,
|
|
203
|
+
SectionSliceActions,
|
|
204
|
+
BlockSlice,
|
|
205
|
+
BlockSliceActions,
|
|
206
|
+
CanvasSlice,
|
|
207
|
+
CanvasSliceState,
|
|
208
|
+
CanvasSliceActions,
|
|
209
|
+
CoverSlice,
|
|
210
|
+
CoverSliceActions,
|
|
211
|
+
SelectionSlice,
|
|
212
|
+
SelectionSliceState,
|
|
213
|
+
SelectionSliceActions,
|
|
214
|
+
HistorySlice,
|
|
215
|
+
HistorySliceState,
|
|
216
|
+
HistorySliceActions,
|
|
217
|
+
} from "./types-slices";
|
|
218
|
+
|
|
219
|
+
export type {
|
|
220
|
+
MetaSlice,
|
|
221
|
+
MetaSliceState,
|
|
222
|
+
MetaSliceActions,
|
|
223
|
+
SectionSlice,
|
|
224
|
+
SectionSliceState,
|
|
225
|
+
SectionSliceActions,
|
|
226
|
+
BlockSlice,
|
|
227
|
+
BlockSliceActions,
|
|
228
|
+
CanvasSlice,
|
|
229
|
+
CanvasSliceState,
|
|
230
|
+
CanvasSliceActions,
|
|
231
|
+
CoverSlice,
|
|
232
|
+
CoverSliceActions,
|
|
233
|
+
SelectionSlice,
|
|
234
|
+
SelectionSliceState,
|
|
235
|
+
SelectionSliceActions,
|
|
236
|
+
HistorySlice,
|
|
237
|
+
HistorySliceState,
|
|
238
|
+
HistorySliceActions,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Flat state object composed of all slice states.
|
|
243
|
+
* Runtime shape is unchanged from pre-Session 183 — a single level
|
|
244
|
+
* of properties. See `./types-slices.ts` for slice boundaries.
|
|
245
|
+
*/
|
|
246
|
+
export type BuilderState = MetaSliceState &
|
|
247
|
+
SectionSliceState &
|
|
248
|
+
CanvasSliceState &
|
|
249
|
+
SelectionSliceState &
|
|
250
|
+
HistorySliceState;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* All store actions, composed from per-slice action types.
|
|
254
|
+
* Runtime shape is unchanged. See `./types-slices.ts` for slice owners.
|
|
255
|
+
*/
|
|
256
|
+
export type BuilderActions = MetaSliceActions &
|
|
257
|
+
SectionSliceActions &
|
|
258
|
+
BlockSliceActions &
|
|
259
|
+
CanvasSliceActions &
|
|
260
|
+
CoverSliceActions &
|
|
261
|
+
SelectionSliceActions &
|
|
262
|
+
HistorySliceActions;
|
|
411
263
|
|
|
412
264
|
export type BuilderStore = BuilderState & BuilderActions;
|
package/lib/version.ts
CHANGED
package/package.json
CHANGED