@morphika/andami 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. package/styles/base.css +114 -114
@@ -1,414 +1,428 @@
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
- /**
128
- * Move a column from one columnar section (V2 or Cover) to another.
129
- * Blocks travel with the column. Responsive overrides for the moved
130
- * column are stripped from the source section. Span/grid_column are
131
- * clamped to the target's grid_columns. Returns the final landed span
132
- * so the caller can adjust UI feedback (e.g. show clamped value).
133
- */
134
- moveColumnBetweenSections: (
135
- sourceSectionKey: string,
136
- columnKey: string,
137
- targetSectionKey: string,
138
- targetRow: number,
139
- targetColumn: number,
140
- targetSpan: number,
141
- ) => void;
142
- /**
143
- * Swap two columns across different columnar sections (V2 ↔ Cover).
144
- * Each column adopts the other's position/row/span. Blocks travel
145
- * with the column. Responsive overrides for both swapped columns
146
- * are stripped (overrides are per-section by `_key`).
147
- */
148
- swapColumnsBetweenSections: (
149
- sourceSectionKey: string,
150
- sourceColumnKey: string,
151
- targetSectionKey: string,
152
- targetColumnKey: string,
153
- ) => void;
154
- applyPresetV2: (sectionKey: string, preset: SectionV2Preset) => void;
155
- updateSectionV2Settings: (sectionKey: string, settings: Partial<SectionV2Settings>) => void;
156
- updateSectionV2Responsive: (
157
- sectionKey: string,
158
- responsive: PageSectionV2["responsive"],
159
- ) => void;
160
- addBlockV2: (
161
- sectionKey: string,
162
- columnKey: string,
163
- blockType: BlockType,
164
- insertIndex?: number,
165
- ) => void;
166
- updateColumnEnterAnimation: (
167
- sectionKey: string,
168
- colKey: string,
169
- config: import("../../lib/animation/enter-types").EnterAnimationConfig | undefined,
170
- ) => void;
171
-
172
- /** Select a V2 column sets selectedRowKey + selectedColumnKey. */
173
- selectColumnV2: (sectionKey: string | null, columnKey: string | null) => void;
174
-
175
- // Custom section instance operations (reference lifecycle on the page)
176
- addCustomSectionInstance: (
177
- id: string,
178
- slug: string,
179
- title: string,
180
- afterRowKey?: string | null,
181
- ) => void;
182
- detachCustomSectionInstance: (instanceKey: string, sectionData: PageSectionV2) => void;
183
- updateCustomSectionInstanceTitle: (instanceKey: string, newTitle: string) => void;
184
- updateCustomSectionInstanceSettings: (
185
- instanceKey: string,
186
- updates: Partial<SectionV2Settings>,
187
- ) => void;
188
- cacheCustomSectionSettings: (sectionId: string, settings: SectionV2Settings) => void;
189
-
190
- // Parallax Group operations (Session 123)
191
- addParallaxGroup: (afterRowKey?: string | null) => void;
192
- addParallaxSlide: (groupKey: string) => void;
193
- removeParallaxSlide: (groupKey: string, slideKey: string) => void;
194
- moveParallaxSlide: (groupKey: string, slideKey: string, direction: "up" | "down") => void;
195
- updateParallaxSlideBackground: (
196
- groupKey: string,
197
- slideKey: string,
198
- fields: Partial<import("../../lib/sanity/types").ParallaxSlideV2>,
199
- ) => void;
200
- updateParallaxGroupSettings: (
201
- groupKey: string,
202
- fields: Partial<
203
- Pick<
204
- import("../../lib/sanity/types").ParallaxGroup,
205
- "transition_effect" | "snap_enabled" | "parallax_intensity"
206
- >
207
- >,
208
- ) => void;
209
- }
210
-
211
- export type SectionSlice = SectionSliceState & SectionSliceActions;
212
-
213
- // ============================================
214
- // BlockSlice — block CRUD (no exclusive state; writes rows)
215
- // ============================================
216
-
217
- // Block slice has no exclusive state — all block data lives inside `rows`
218
- // (owned by SectionSlice). Block operations legitimately write to rows and
219
- // to selection keys (e.g. deleteBlock clears selectedBlockKey).
220
-
221
- export interface BlockSliceActions {
222
- updateBlock: (blockKey: string, updates: Partial<ContentBlock>) => void;
223
- deleteBlock: (blockKey: string) => void;
224
- duplicateBlock: (blockKey: string) => void;
225
- moveBlock: (
226
- blockKey: string,
227
- targetSectionKey: string,
228
- targetColumnKey: string,
229
- toIndex: number,
230
- ) => void;
231
- reorderBlocks: (
232
- sectionKey: string,
233
- columnKey: string,
234
- fromIndex: number,
235
- toIndex: number,
236
- ) => void;
237
- /** Debounced block update — batches keystrokes, does not push history per call. */
238
- updateBlockDebounced: (blockKey: string, updates: Partial<ContentBlock>) => void;
239
- }
240
-
241
- export type BlockSlice = BlockSliceActions;
242
-
243
- // ============================================
244
- // CanvasSlice — viewport, preview mode, page + grid settings
245
- // ============================================
246
-
247
- export interface CanvasSliceState {
248
- // Editor mode
249
- previewMode: boolean;
250
-
251
- // Custom section editor mode (Session 107) the canvas swaps its
252
- // rows between the current page and the custom section being edited,
253
- // so this is modelled as a canvas concern.
254
- editorMode: "page" | "customSection";
255
- customSectionSlug: string | null;
256
- customSectionTitle: string | null;
257
- /** Stashed page state while editing a custom section */
258
- savedPageState: { rows: ContentItem[]; selectedKey: string | null } | null;
259
-
260
- // Page-level settings (global per page, persisted to Sanity)
261
- pageSettings: PageSettings;
262
-
263
- // Grid settings (from Customize — ephemeral, NOT saved per page)
264
- gridSettings: GridSettings;
265
-
266
- // Canvas viewport state (ephemeral)
267
- canvasZoom: number;
268
- canvasPanX: number;
269
- canvasPanY: number;
270
- canvasTool: CanvasTool;
271
- activeViewport: DeviceViewport;
272
-
273
- // BUG-014 fix: Track whether the Sanity document had page_settings.
274
- // When true, applyGlobalStyles() won't overwrite user-chosen colors.
275
- _hasDocumentPageSettings: boolean;
276
- }
277
-
278
- export interface CanvasSliceActions {
279
- togglePreviewMode: () => void;
280
- setPreviewMode: (preview: boolean) => void;
281
-
282
- // Custom section editor mode (Session 107)
283
- /** Enter section editor mode — stashes current page rows, replaces with a single V2 section */
284
- enterSectionEditor: (
285
- slug: string | null,
286
- title: string | null,
287
- sectionData: PageSectionV2 | null,
288
- ) => void;
289
- /** Exit section editor mode — restores page rows from stash. Pass wasSaved=true to mark page dirty. */
290
- exitSectionEditor: (wasSaved?: boolean) => void;
291
- /** Save the custom section being edited, then exit editor mode.
292
- * Returns { id, slug, title } on success (for instance insertion after creation). */
293
- saveSectionEditor: (
294
- title: string,
295
- ) => Promise<{ id: string; slug: string; title: string } | null>;
296
-
297
- updatePageSettings: (settings: Partial<PageSettings>) => void;
298
- applyGlobalStyles: () => Promise<void>;
299
-
300
- setCanvasZoom: (zoom: number) => void;
301
- setCanvasPan: (x: number, y: number) => void;
302
- setCanvasTool: (tool: CanvasTool) => void;
303
- setActiveViewport: (viewport: DeviceViewport) => void;
304
- zoomToFit: (viewportWidth: number, viewportHeight: number) => void;
305
- zoomToPoint: (newZoom: number, cursorX: number, cursorY: number) => void;
306
- zoomToFrame: (
307
- device: DeviceViewport,
308
- viewportWidth: number,
309
- viewportHeight: number,
310
- ) => void;
311
- }
312
-
313
- export type CanvasSlice = CanvasSliceState & CanvasSliceActions;
314
-
315
- // ============================================
316
- // CoverSlice cover-section-specific row + background + settings ops
317
- // ============================================
318
-
319
- // Cover slice has no exclusive state cover sections live inside `rows`
320
- // (SectionSlice). Column/block ops within cover sections reuse V2 actions
321
- // via findSectionPath() which supports cover sections.
322
-
323
- export interface CoverSliceActions {
324
- addCoverSection: (afterRowKey?: string | null) => void;
325
- addCoverRow: (sectionKey: string) => void;
326
- removeCoverRow: (sectionKey: string, rowKey: string) => void;
327
- resizeCoverRow: (
328
- sectionKey: string,
329
- handleIndex: number,
330
- deltaPercent: number,
331
- startAbove: number,
332
- startBelow: number,
333
- ) => void;
334
- updateCoverRowAlign: (
335
- sectionKey: string,
336
- rowKey: string,
337
- align: CoverRow["vertical_align"],
338
- ) => void;
339
- updateCoverBackground: (
340
- sectionKey: string,
341
- fields: Partial<
342
- Pick<
343
- CoverSection,
344
- | "background_type"
345
- | "background_color"
346
- | "background_image"
347
- | "background_video"
348
- | "background_position"
349
- | "background_size"
350
- | "background_overlay_color"
351
- | "background_overlay_opacity"
352
- | "nav_color"
353
- >
354
- >,
355
- ) => void;
356
- updateCoverSettings: (sectionKey: string, settings: Partial<CoverSectionSettings>) => void;
357
- updateCoverHeight: (sectionKey: string, height: CoverSection["height"]) => void;
358
- }
359
-
360
- export type CoverSlice = CoverSliceActions;
361
-
362
- // ============================================
363
- // SelectionSlice — selection keys + color picker preview
364
- // ============================================
365
-
366
- export interface SelectionSliceState {
367
- selectedRowKey: string | null;
368
- selectedColumnKey: string | null;
369
- selectedBlockKey: string | null;
370
- /** Sub-selection: which project card is selected within a ProjectGrid block */
371
- selectedProjectCardKey: string | null;
372
-
373
- /** Live preview overlay from color picker — shown on canvas without persisting to Sanity.
374
- * Set while user is dragging in the color picker; cleared on close. */
375
- colorPickerPreview: {
376
- blockKey?: string;
377
- sectionKey?: string;
378
- field: string;
379
- value: ColorField;
380
- } | null;
381
- }
382
-
383
- export interface SelectionSliceActions {
384
- selectRow: (key: string | null) => void;
385
- selectColumn: (rowKey: string | null, colKey: string | null) => void;
386
- selectBlock: (key: string | null) => void;
387
- selectProjectCard: (key: string | null) => void;
388
- clearSelection: () => void;
389
-
390
- setColorPickerPreview: (preview: SelectionSliceState["colorPickerPreview"]) => void;
391
- clearColorPickerPreview: () => void;
392
- }
393
-
394
- export type SelectionSlice = SelectionSliceState & SelectionSliceActions;
395
-
396
- // ============================================
397
- // HistorySlice undo/redo + snapshot push
398
- // ============================================
399
-
400
- export interface HistorySliceState {
401
- _history: import("./history").HistorySnapshot[];
402
- _future: import("./history").HistorySnapshot[];
403
- }
404
-
405
- export interface HistorySliceActions {
406
- undo: () => void;
407
- redo: () => void;
408
- canUndo: () => boolean;
409
- canRedo: () => boolean;
410
- /** Push a snapshot before a mutation (called internally by block/section mutations). */
411
- _pushSnapshot: () => void;
412
- }
413
-
414
- export type HistorySlice = HistorySliceState & HistorySliceActions;
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
+ SectionColumn,
35
+ PageMetadata,
36
+ ColorField,
37
+ CoverSection,
38
+ CoverSectionSettings,
39
+ CoverRow,
40
+ } from "../../lib/sanity/types";
41
+ import type { BlockType, SectionBlockType } from "./types";
42
+ import type { PageSettings, GridSettings, CanvasTool, DeviceViewport } from "./types";
43
+
44
+ // ============================================
45
+ // MetaSlice — page identity and persistence flags
46
+ // ============================================
47
+
48
+ export interface MetaSliceState {
49
+ pageId: string | null;
50
+ pageTitle: string;
51
+ pageSlug: string;
52
+ /** The slug as loaded from Sanity — used for API calls even if slug is edited. */
53
+ _originalSlug: string;
54
+ pageType: PageType;
55
+ metadata: PageMetadata;
56
+ publishedAt: string | null;
57
+ draftMode: boolean;
58
+
59
+ isDirty: boolean;
60
+ isSaving: boolean;
61
+ saveError: string | null;
62
+ lastSavedAt: string | null;
63
+ }
64
+
65
+ export interface MetaSliceActions {
66
+ setPageTitle: (title: string) => void;
67
+ setPageSlug: (slug: string) => void;
68
+ setMetadata: (metadata: Partial<PageMetadata>) => void;
69
+ setDraftMode: (draft: boolean) => void;
70
+ publishPage: () => void;
71
+ unpublishPage: () => void;
72
+
73
+ loadFromDocument: (doc: Page) => void;
74
+ save: () => Promise<void>;
75
+ reset: () => void;
76
+
77
+ markDirty: () => void;
78
+ markClean: () => void;
79
+ }
80
+
81
+ export type MetaSlice = MetaSliceState & MetaSliceActions;
82
+
83
+ // ============================================
84
+ // SectionSlice — content rows + section editor + custom sections + parallax
85
+ // ============================================
86
+
87
+ export interface SectionSliceState {
88
+ /** Content rows: V2 sections, custom section instances, parallax groups, cover sections. */
89
+ rows: ContentItem[];
90
+
91
+ /** Cache of fetched custom section base settings, keyed by custom_section_id. */
92
+ _customSectionCache: Record<string, SectionV2Settings>;
93
+
94
+ /** Counter incremented after a custom section is saved — triggers refetch in consumers. */
95
+ _customSectionRefetchTick: number;
96
+ }
97
+
98
+ export interface SectionSliceActions {
99
+ // Section-level (top-level row) operations
100
+ addSection: (
101
+ blockType: SectionBlockType,
102
+ afterRowKey?: string | null,
103
+ ) => void;
104
+ reorderRows: (fromIndex: number, toIndex: number) => void;
105
+ deleteSection: (sectionKey: string) => void;
106
+ duplicateSection: (sectionKey: string) => void;
107
+
108
+ // V2 section operations
109
+ addSectionV2: (preset: SectionV2Preset, afterRowKey?: string | null) => void;
110
+ addColumnV2: (sectionKey: string, gridRow: number, gridColumn: number, span: number) => void;
111
+ deleteColumnV2: (sectionKey: string, columnKey: string) => void;
112
+ resizeColumnV2: (sectionKey: string, columnKey: string, newSpan: number) => void;
113
+ resizeColumnV2Left: (sectionKey: string, columnKey: string, newGridColumn: number) => void;
114
+ moveColumnV2: (
115
+ sectionKey: string,
116
+ columnKey: string,
117
+ targetRow: number,
118
+ targetColumn: number,
119
+ ) => void;
120
+ swapColumnV2: (sectionKey: string, draggedKey: string, targetKey: string) => void;
121
+ moveColumnToGapV2: (
122
+ sectionKey: string,
123
+ columnKey: string,
124
+ targetRow: number,
125
+ targetColumn: number,
126
+ targetSpan: number,
127
+ ) => void;
128
+ /**
129
+ * Move a column from one columnar section (V2 or Cover) to another.
130
+ * Blocks travel with the column. Responsive overrides for the moved
131
+ * column are stripped from the source section. Span/grid_column are
132
+ * clamped to the target's grid_columns. Returns the final landed span
133
+ * so the caller can adjust UI feedback (e.g. show clamped value).
134
+ */
135
+ moveColumnBetweenSections: (
136
+ sourceSectionKey: string,
137
+ columnKey: string,
138
+ targetSectionKey: string,
139
+ targetRow: number,
140
+ targetColumn: number,
141
+ targetSpan: number,
142
+ ) => void;
143
+ /**
144
+ * Swap two columns across different columnar sections (V2 ↔ Cover).
145
+ * Each column adopts the other's position/row/span. Blocks travel
146
+ * with the column. Responsive overrides for both swapped columns
147
+ * are stripped (overrides are per-section by `_key`).
148
+ */
149
+ swapColumnsBetweenSections: (
150
+ sourceSectionKey: string,
151
+ sourceColumnKey: string,
152
+ targetSectionKey: string,
153
+ targetColumnKey: string,
154
+ ) => void;
155
+ applyPresetV2: (sectionKey: string, preset: SectionV2Preset) => void;
156
+ updateSectionV2Settings: (sectionKey: string, settings: Partial<SectionV2Settings>) => void;
157
+ updateSectionV2Responsive: (
158
+ sectionKey: string,
159
+ responsive: PageSectionV2["responsive"],
160
+ ) => void;
161
+ addBlockV2: (
162
+ sectionKey: string,
163
+ columnKey: string,
164
+ blockType: BlockType,
165
+ insertIndex?: number,
166
+ ) => void;
167
+ updateColumnEnterAnimation: (
168
+ sectionKey: string,
169
+ colKey: string,
170
+ config: import("../../lib/animation/enter-types").EnterAnimationConfig | undefined,
171
+ ) => void;
172
+ /** Update desktop-level layout fields on a V2/cover/parallax-slide column.
173
+ * Columns only support background + border spacing belongs to the
174
+ * section (row_gap/col_gap) or to block-level padding. */
175
+ updateColumnV2Layout: (
176
+ sectionKey: string,
177
+ colKey: string,
178
+ updates: Partial<Pick<
179
+ SectionColumn,
180
+ | "background_color" | "background_opacity" | "background_image"
181
+ | "background_size" | "background_position" | "background_repeat"
182
+ | "border_color" | "border_width" | "border_style" | "border_sides" | "border_radius"
183
+ >>,
184
+ ) => void;
185
+
186
+ /** Select a V2 column — sets selectedRowKey + selectedColumnKey. */
187
+ selectColumnV2: (sectionKey: string | null, columnKey: string | null) => void;
188
+
189
+ // Custom section instance operations (reference lifecycle on the page)
190
+ addCustomSectionInstance: (
191
+ id: string,
192
+ slug: string,
193
+ title: string,
194
+ afterRowKey?: string | null,
195
+ ) => void;
196
+ detachCustomSectionInstance: (instanceKey: string, sectionData: PageSectionV2) => void;
197
+ updateCustomSectionInstanceTitle: (instanceKey: string, newTitle: string) => void;
198
+ updateCustomSectionInstanceSettings: (
199
+ instanceKey: string,
200
+ updates: Partial<SectionV2Settings>,
201
+ ) => void;
202
+ cacheCustomSectionSettings: (sectionId: string, settings: SectionV2Settings) => void;
203
+
204
+ // Parallax Group operations (Session 123)
205
+ addParallaxGroup: (afterRowKey?: string | null) => void;
206
+ addParallaxSlide: (groupKey: string) => void;
207
+ removeParallaxSlide: (groupKey: string, slideKey: string) => void;
208
+ moveParallaxSlide: (groupKey: string, slideKey: string, direction: "up" | "down") => void;
209
+ updateParallaxSlideBackground: (
210
+ groupKey: string,
211
+ slideKey: string,
212
+ fields: Partial<import("../../lib/sanity/types").ParallaxSlideV2>,
213
+ ) => void;
214
+ updateParallaxGroupSettings: (
215
+ groupKey: string,
216
+ fields: Partial<
217
+ Pick<
218
+ import("../../lib/sanity/types").ParallaxGroup,
219
+ "transition_effect" | "snap_enabled" | "parallax_intensity"
220
+ >
221
+ >,
222
+ ) => void;
223
+ }
224
+
225
+ export type SectionSlice = SectionSliceState & SectionSliceActions;
226
+
227
+ // ============================================
228
+ // BlockSlice — block CRUD (no exclusive state; writes rows)
229
+ // ============================================
230
+
231
+ // Block slice has no exclusive state — all block data lives inside `rows`
232
+ // (owned by SectionSlice). Block operations legitimately write to rows and
233
+ // to selection keys (e.g. deleteBlock clears selectedBlockKey).
234
+
235
+ export interface BlockSliceActions {
236
+ updateBlock: (blockKey: string, updates: Partial<ContentBlock>) => void;
237
+ deleteBlock: (blockKey: string) => void;
238
+ duplicateBlock: (blockKey: string) => void;
239
+ moveBlock: (
240
+ blockKey: string,
241
+ targetSectionKey: string,
242
+ targetColumnKey: string,
243
+ toIndex: number,
244
+ ) => void;
245
+ reorderBlocks: (
246
+ sectionKey: string,
247
+ columnKey: string,
248
+ fromIndex: number,
249
+ toIndex: number,
250
+ ) => void;
251
+ /** Debounced block update batches keystrokes, does not push history per call. */
252
+ updateBlockDebounced: (blockKey: string, updates: Partial<ContentBlock>) => void;
253
+ }
254
+
255
+ export type BlockSlice = BlockSliceActions;
256
+
257
+ // ============================================
258
+ // CanvasSlice viewport, preview mode, page + grid settings
259
+ // ============================================
260
+
261
+ export interface CanvasSliceState {
262
+ // Editor mode
263
+ previewMode: boolean;
264
+
265
+ // Custom section editor mode (Session 107) — the canvas swaps its
266
+ // rows between the current page and the custom section being edited,
267
+ // so this is modelled as a canvas concern.
268
+ editorMode: "page" | "customSection";
269
+ customSectionSlug: string | null;
270
+ customSectionTitle: string | null;
271
+ /** Stashed page state while editing a custom section */
272
+ savedPageState: { rows: ContentItem[]; selectedKey: string | null } | null;
273
+
274
+ // Page-level settings (global per page, persisted to Sanity)
275
+ pageSettings: PageSettings;
276
+
277
+ // Grid settings (from Customize — ephemeral, NOT saved per page)
278
+ gridSettings: GridSettings;
279
+
280
+ // Canvas viewport state (ephemeral)
281
+ canvasZoom: number;
282
+ canvasPanX: number;
283
+ canvasPanY: number;
284
+ canvasTool: CanvasTool;
285
+ activeViewport: DeviceViewport;
286
+
287
+ // BUG-014 fix: Track whether the Sanity document had page_settings.
288
+ // When true, applyGlobalStyles() won't overwrite user-chosen colors.
289
+ _hasDocumentPageSettings: boolean;
290
+ }
291
+
292
+ export interface CanvasSliceActions {
293
+ togglePreviewMode: () => void;
294
+ setPreviewMode: (preview: boolean) => void;
295
+
296
+ // Custom section editor mode (Session 107)
297
+ /** Enter section editor mode — stashes current page rows, replaces with a single V2 section */
298
+ enterSectionEditor: (
299
+ slug: string | null,
300
+ title: string | null,
301
+ sectionData: PageSectionV2 | null,
302
+ ) => void;
303
+ /** Exit section editor mode — restores page rows from stash. Pass wasSaved=true to mark page dirty. */
304
+ exitSectionEditor: (wasSaved?: boolean) => void;
305
+ /** Save the custom section being edited, then exit editor mode.
306
+ * Returns { id, slug, title } on success (for instance insertion after creation). */
307
+ saveSectionEditor: (
308
+ title: string,
309
+ ) => Promise<{ id: string; slug: string; title: string } | null>;
310
+
311
+ updatePageSettings: (settings: Partial<PageSettings>) => void;
312
+ applyGlobalStyles: () => Promise<void>;
313
+
314
+ setCanvasZoom: (zoom: number) => void;
315
+ setCanvasPan: (x: number, y: number) => void;
316
+ setCanvasTool: (tool: CanvasTool) => void;
317
+ setActiveViewport: (viewport: DeviceViewport) => void;
318
+ zoomToFit: (viewportWidth: number, viewportHeight: number) => void;
319
+ zoomToPoint: (newZoom: number, cursorX: number, cursorY: number) => void;
320
+ zoomToFrame: (
321
+ device: DeviceViewport,
322
+ viewportWidth: number,
323
+ viewportHeight: number,
324
+ ) => void;
325
+ }
326
+
327
+ export type CanvasSlice = CanvasSliceState & CanvasSliceActions;
328
+
329
+ // ============================================
330
+ // CoverSlice — cover-section-specific row + background + settings ops
331
+ // ============================================
332
+
333
+ // Cover slice has no exclusive state — cover sections live inside `rows`
334
+ // (SectionSlice). Column/block ops within cover sections reuse V2 actions
335
+ // via findSectionPath() which supports cover sections.
336
+
337
+ export interface CoverSliceActions {
338
+ addCoverSection: (afterRowKey?: string | null) => void;
339
+ addCoverRow: (sectionKey: string) => void;
340
+ removeCoverRow: (sectionKey: string, rowKey: string) => void;
341
+ resizeCoverRow: (
342
+ sectionKey: string,
343
+ handleIndex: number,
344
+ deltaPercent: number,
345
+ startAbove: number,
346
+ startBelow: number,
347
+ ) => void;
348
+ updateCoverRowAlign: (
349
+ sectionKey: string,
350
+ rowKey: string,
351
+ align: CoverRow["vertical_align"],
352
+ ) => void;
353
+ updateCoverBackground: (
354
+ sectionKey: string,
355
+ fields: Partial<
356
+ Pick<
357
+ CoverSection,
358
+ | "background_type"
359
+ | "background_color"
360
+ | "background_image"
361
+ | "background_video"
362
+ | "background_position"
363
+ | "background_size"
364
+ | "background_overlay_color"
365
+ | "background_overlay_opacity"
366
+ | "nav_color"
367
+ >
368
+ >,
369
+ ) => void;
370
+ updateCoverSettings: (sectionKey: string, settings: Partial<CoverSectionSettings>) => void;
371
+ updateCoverHeight: (sectionKey: string, height: CoverSection["height"]) => void;
372
+ }
373
+
374
+ export type CoverSlice = CoverSliceActions;
375
+
376
+ // ============================================
377
+ // SelectionSlice — selection keys + color picker preview
378
+ // ============================================
379
+
380
+ export interface SelectionSliceState {
381
+ selectedRowKey: string | null;
382
+ selectedColumnKey: string | null;
383
+ selectedBlockKey: string | null;
384
+ /** Sub-selection: which project card is selected within a ProjectGrid block */
385
+ selectedProjectCardKey: string | null;
386
+
387
+ /** Live preview overlay from color picker — shown on canvas without persisting to Sanity.
388
+ * Set while user is dragging in the color picker; cleared on close. */
389
+ colorPickerPreview: {
390
+ blockKey?: string;
391
+ sectionKey?: string;
392
+ field: string;
393
+ value: ColorField;
394
+ } | null;
395
+ }
396
+
397
+ export interface SelectionSliceActions {
398
+ selectRow: (key: string | null) => void;
399
+ selectColumn: (rowKey: string | null, colKey: string | null) => void;
400
+ selectBlock: (key: string | null) => void;
401
+ selectProjectCard: (key: string | null) => void;
402
+ clearSelection: () => void;
403
+
404
+ setColorPickerPreview: (preview: SelectionSliceState["colorPickerPreview"]) => void;
405
+ clearColorPickerPreview: () => void;
406
+ }
407
+
408
+ export type SelectionSlice = SelectionSliceState & SelectionSliceActions;
409
+
410
+ // ============================================
411
+ // HistorySlice undo/redo + snapshot push
412
+ // ============================================
413
+
414
+ export interface HistorySliceState {
415
+ _history: import("./history").HistorySnapshot[];
416
+ _future: import("./history").HistorySnapshot[];
417
+ }
418
+
419
+ export interface HistorySliceActions {
420
+ undo: () => void;
421
+ redo: () => void;
422
+ canUndo: () => boolean;
423
+ canRedo: () => boolean;
424
+ /** Push a snapshot before a mutation (called internally by block/section mutations). */
425
+ _pushSnapshot: () => void;
426
+ }
427
+
428
+ export type HistorySlice = HistorySliceState & HistorySliceActions;