@morphika/andami 0.4.0 → 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.
@@ -4,6 +4,12 @@ import { generateKey } from "./utils";
4
4
  import type { EnterAnimationConfig, TypewriterConfig } from "../../lib/animation/enter-types";
5
5
  import type { HoverEffectConfig } from "../../lib/animation/hover-effect-types";
6
6
 
7
+ // Side-effect import: populates the block registry with all built-in block
8
+ // types so `createDefaultBlock` can delegate to each registration's
9
+ // `defaultFactory`. See `./block-registry.ts` for the stability contract.
10
+ import "./block-registrations";
11
+ import { getBlockRegistration } from "./block-registry";
12
+
7
13
  // ============================================
8
14
  // Parallax slide defaults (Session 128)
9
15
  // ============================================
@@ -116,109 +122,23 @@ export const DEFAULT_TYPEWRITER_CONFIG: Required<TypewriterConfig> = {
116
122
 
117
123
  /**
118
124
  * Create a new block with sensible defaults for the given block type.
125
+ *
126
+ * Session 181 (C): switched from a giant switch-case to a registry lookup.
127
+ * Each block type now declares its default shape in
128
+ * `./block-registrations.ts` via the `defaultFactory` field. The
129
+ * registration module is loaded eagerly here (side-effect import) so the
130
+ * registry is populated before any `createDefaultBlock` call.
131
+ *
132
+ * Unknown block types fall back to the minimal `{ _type, _key }` shape —
133
+ * same behavior as the previous switch-case's `default` branch.
119
134
  */
120
135
  export function createDefaultBlock(blockType: BlockType): ContentBlock {
121
- const _key = generateKey();
122
-
123
- switch (blockType) {
124
- case "textBlock":
125
- return {
126
- _type: "textBlock",
127
- _key,
128
- text: [],
129
- style: { fontSize: 14, alignment: "left", fontWeight: "400" },
130
- };
131
- case "imageBlock":
132
- return {
133
- _type: "imageBlock",
134
- _key,
135
- asset_path: "",
136
- alt: "",
137
- width: "full",
138
- aspect_ratio: "auto",
139
- lazy: true,
140
- shadow: false,
141
- border_radius: "",
142
- };
143
- case "imageGridBlock":
144
- return {
145
- _type: "imageGridBlock",
146
- _key,
147
- images: [],
148
- h_gutter: 10,
149
- v_gutter: 10,
150
- images_per_row: 2,
151
- random_grid: "disabled",
152
- random_seed: 1,
153
- lightbox: false,
154
- object_fit: "cover",
155
- };
156
- case "videoBlock":
157
- return {
158
- _type: "videoBlock",
159
- _key,
160
- video_type: "vimeo",
161
- url_or_path: "",
162
- autoplay: false,
163
- loop: false,
164
- muted: true,
165
- controls: true,
166
- aspect_ratio: "16:9",
167
- };
168
- case "spacerBlock":
169
- return {
170
- _type: "spacerBlock",
171
- _key,
172
- height: "medium",
173
- };
174
- case "buttonBlock":
175
- return {
176
- _type: "buttonBlock",
177
- _key,
178
- text: "Button",
179
- url: "#",
180
- style: "primary",
181
- size: "medium",
182
- };
183
- case "projectGridBlock":
184
- return {
185
- _type: "projectGridBlock",
186
- _key,
187
- columns: 3,
188
- aspect_ratios: ["16/9"],
189
- gap_v: 16,
190
- gap_h: 16,
191
- hover_effect: "scale",
192
- show_subtitle: true,
193
- border_radius: 0,
194
- video_mode: "off",
195
- projects: [],
196
- };
197
- case "projectCarouselBlock":
198
- return {
199
- _type: "projectCarouselBlock",
200
- _key,
201
- source_mode: "auto_latest",
202
- max_projects: 8,
203
- exclude_current: true,
204
- cards_per_view_desktop: 3.5,
205
- cards_per_view_tablet: 2.2,
206
- cards_per_view_phone: 1.2,
207
- gap: 16,
208
- aspect_ratio: "4/3",
209
- show_title: true,
210
- show_subtitle: false,
211
- border_radius: 0,
212
- hover_effect: "scale",
213
- video_mode: "off",
214
- show_arrows: true,
215
- show_dots: false,
216
- snap_scroll: true,
217
- };
218
- default:
219
- return {
220
- _type: blockType,
221
- _key,
222
- } as ContentBlock;
136
+ const registration = getBlockRegistration(blockType);
137
+ if (registration) {
138
+ return registration.defaultFactory(generateKey());
223
139
  }
140
+ return {
141
+ _type: blockType,
142
+ _key: generateKey(),
143
+ } as ContentBlock;
224
144
  }
@@ -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;