@morphika/andami 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -182,231 +182,83 @@ export const DEFAULT_GRID_SETTINGS: GridSettings = {
182
182
  gutter_phone: "16",
183
183
  };
184
184
 
185
- export interface BuilderState {
186
- // Page metadata
187
- pageId: string | null;
188
- pageTitle: string;
189
- pageSlug: string;
190
- /** The slug as loaded from Sanity used for API calls even if slug is edited. */
191
- _originalSlug: string;
192
- pageType: PageType;
193
- metadata: PageMetadata;
194
- publishedAt: string | null;
195
- draftMode: boolean;
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
- export interface BuilderActions {
264
- // Page metadata
265
- setPageTitle: (title: string) => void;
266
- setPageSlug: (slug: string) => void;
267
- setMetadata: (metadata: Partial<PageMetadata>) => void;
268
- setDraftMode: (draft: boolean) => void;
269
- publishPage: () => void;
270
- unpublishPage: () => void;
271
-
272
- // Section operations
273
- /** Add a Project Grid section as a V2 section with a full-width column (Session 164) */
274
- addSection: (blockType: "projectGridBlock" | "projectCarouselBlock", afterRowKey?: string | null) => void;
275
- reorderRows: (fromIndex: number, toIndex: number) => void;
276
- /** Delete a section by key */
277
- deleteSection: (sectionKey: string) => void;
278
- /** Duplicate a section, inserting copy after the original */
279
- duplicateSection: (sectionKey: string) => void;
280
-
281
- // Block operations
282
- updateBlock: (blockKey: string, updates: Partial<ContentBlock>) => void;
283
- deleteBlock: (blockKey: string) => void;
284
- /** Duplicate a content block (inserts copy immediately after the original) */
285
- duplicateBlock: (blockKey: string) => void;
286
- moveBlock: (blockKey: string, targetSectionKey: string, targetColumnKey: string, toIndex: number) => void;
287
- reorderBlocks: (sectionKey: string, columnKey: string, fromIndex: number, toIndex: number) => void;
288
-
289
- // Selection
290
- selectRow: (key: string | null) => void;
291
- selectColumn: (rowKey: string | null, colKey: string | null) => void;
292
- selectBlock: (key: string | null) => void;
293
- /** Select a specific project card within a ProjectGrid block */
294
- selectProjectCard: (key: string | null) => void;
295
- clearSelection: () => void;
296
-
297
- // Persistence
298
- loadFromDocument: (doc: Page) => void;
299
- save: () => Promise<void>;
300
- reset: () => void;
301
-
302
- // Dirty tracking
303
- markDirty: () => void;
304
- markClean: () => void;
305
-
306
- // History (Undo/Redo)
307
- undo: () => void;
308
- redo: () => void;
309
- canUndo: () => boolean;
310
- canRedo: () => boolean;
311
- /** Push a snapshot before a mutation (called internally). */
312
- _pushSnapshot: () => void;
313
-
314
- // ---- V2 Section operations ----
315
- /** Add a new V2 grid section with a preset layout */
316
- addSectionV2: (preset: import("../../lib/sanity/types").SectionV2Preset, afterRowKey?: string | null) => void;
317
- /** Add a column to a V2 section at a specific position */
318
- addColumnV2: (sectionKey: string, gridRow: number, gridColumn: number, span: number) => void;
319
- /** Delete a column from a V2 section */
320
- deleteColumnV2: (sectionKey: string, columnKey: string) => void;
321
- /** Resize a column in a V2 section (right edge) */
322
- resizeColumnV2: (sectionKey: string, columnKey: string, newSpan: number) => void;
323
- /** Resize a column in a V2 section from the left edge */
324
- resizeColumnV2Left: (sectionKey: string, columnKey: string, newGridColumn: number) => void;
325
- /** Move a column in a V2 section (drag & drop) */
326
- moveColumnV2: (sectionKey: string, columnKey: string, targetRow: number, targetColumn: number) => void;
327
- /** Swap two columns in a V2 section (exchange positions, blocks stay) */
328
- swapColumnV2: (sectionKey: string, draggedKey: string, targetKey: string) => void;
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;
@@ -596,6 +596,23 @@ export function isCoverSection(item: ContentItem): item is CoverSection {
596
596
  return (item as CoverSection)._type === "coverSection";
597
597
  }
598
598
 
599
+ /**
600
+ * Type guard: check if a content item supports column drag-and-drop as a
601
+ * source or target — i.e. it has a top-level `columns` array with the same
602
+ * V2 grid semantics. Used by cross-section column DnD (Session 183).
603
+ *
604
+ * Eligible: PageSectionV2, CoverSection
605
+ * NOT eligible:
606
+ * - ParallaxGroup (columns live per-slide, cross-slide DnD not supported)
607
+ * - CustomSectionInstance (references an external document, mutating it
608
+ * from the page would break the reference contract)
609
+ */
610
+ export function isColumnarSection(
611
+ item: ContentItem,
612
+ ): item is PageSectionV2 | CoverSection {
613
+ return isPageSectionV2(item) || isCoverSection(item);
614
+ }
615
+
599
616
  // ============================================
600
617
  // Shared references
601
618
  // ============================================
package/lib/version.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * Exposed as a plain constant so it can be imported without reading
7
7
  * package.json at runtime.
8
8
  */
9
- export const ANDAMI_VERSION = "0.4.1";
9
+ export const ANDAMI_VERSION = "0.5.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",