@morphika/andami 0.2.11 → 0.2.13

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 (49) hide show
  1. package/README.md +2 -1
  2. package/app/admin/pages/[slug]/page.tsx +39 -2
  3. package/components/blocks/BlockRenderer.tsx +0 -7
  4. package/components/blocks/CoverSectionRenderer.tsx +295 -0
  5. package/components/blocks/PageRenderer.tsx +13 -9
  6. package/components/builder/BlockLivePreview.tsx +0 -5
  7. package/components/builder/BlockTypePicker.tsx +0 -1
  8. package/components/builder/ColorSwatchPicker.tsx +2 -2
  9. package/components/builder/CoverRowResizeHandle.tsx +180 -0
  10. package/components/builder/CoverSectionCanvas.tsx +260 -0
  11. package/components/builder/ReadOnlyFrame.tsx +127 -3
  12. package/components/builder/SectionTypePicker.tsx +29 -0
  13. package/components/builder/SectionV2Canvas.tsx +4 -1
  14. package/components/builder/SectionV2Column.tsx +15 -20
  15. package/components/builder/SettingsPanel.tsx +14 -0
  16. package/components/builder/SortableRow.tsx +7 -21
  17. package/components/builder/blockStyles.tsx +13 -14
  18. package/components/builder/editors/index.ts +0 -1
  19. package/components/builder/index.ts +1 -0
  20. package/components/builder/live-preview/RichTextEditor.tsx +23 -2
  21. package/components/builder/live-preview/index.ts +0 -1
  22. package/components/builder/settings-panel/BlockSettings.tsx +0 -7
  23. package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
  24. package/components/builder/settings-panel/index.ts +1 -0
  25. package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
  26. package/lib/animation/enter-types.ts +0 -1
  27. package/lib/animation/hover-effect-types.ts +0 -1
  28. package/lib/builder/defaults.ts +43 -22
  29. package/lib/builder/serializer/normalizers.ts +34 -1
  30. package/lib/builder/serializer/serializers.ts +39 -2
  31. package/lib/builder/store-blocks.ts +11 -3
  32. package/lib/builder/store-cover.ts +220 -0
  33. package/lib/builder/store-helpers.ts +81 -4
  34. package/lib/builder/store-sections.ts +12 -2
  35. package/lib/builder/store.ts +11 -2
  36. package/lib/builder/types.ts +15 -2
  37. package/lib/sanity/types.ts +79 -43
  38. package/lib/version.ts +1 -1
  39. package/package.json +1 -1
  40. package/sanity/schemas/blocks/index.ts +1 -2
  41. package/sanity/schemas/index.ts +5 -3
  42. package/sanity/schemas/objects/coverSection.ts +317 -0
  43. package/sanity/schemas/objects/parallaxSlide.ts +0 -1
  44. package/sanity/schemas/page.ts +1 -1
  45. package/sanity/schemas/pageSectionV2.ts +0 -1
  46. package/components/blocks/CoverBlockRenderer.tsx +0 -261
  47. package/components/builder/editors/CoverBlockEditor.tsx +0 -550
  48. package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
  49. package/sanity/schemas/blocks/coverBlock.ts +0 -229
@@ -460,32 +460,27 @@ export default function SectionV2Column({
460
460
  {/* Blocks content */}
461
461
  <SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
462
462
  {!hasBlocks ? (
463
- /* Empty column: show + Add Block */
463
+ /* Empty column: show + Add Block (flex-1 stretches in fillHeight cover sections) */
464
464
  <div
465
- className="relative flex items-center justify-center"
465
+ className="relative flex items-center justify-center flex-1"
466
466
  style={{ minHeight: 80, padding: "16px 12px" }}
467
467
  >
468
468
  <button
469
469
  onClick={handleAddBlockEmpty}
470
470
  aria-label="Add block to empty column"
471
- className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
471
+ className={`rounded-full text-[10px] font-medium transition-all hover:scale-105 ${
472
472
  showChrome
473
473
  ? "opacity-100"
474
474
  : showFaintOutline
475
475
  ? "opacity-40"
476
- : "bg-transparent text-transparent opacity-0 pointer-events-none"
476
+ : "opacity-0 pointer-events-none"
477
477
  }`}
478
478
  style={{
479
+ padding: "5px 16px",
479
480
  pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
480
- ...(showChrome ? {
481
- background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
482
- color: "rgba(100,220,170,0.8)",
483
- boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
484
- border: "1px solid rgba(255,255,255,0.06)",
485
- } : showFaintOutline ? {
486
- background: "rgba(38,38,48,0.3)",
487
- color: "rgba(100,220,170,0.4)",
488
- } : {}),
481
+ background: showChrome ? "rgba(13, 150, 104, 0.12)" : "rgba(13, 150, 104, 0.06)",
482
+ color: "#0d9668",
483
+ border: `1px dashed ${showChrome ? "rgba(13, 150, 104, 0.5)" : "rgba(13, 150, 104, 0.25)"}`,
489
484
  }}
490
485
  >
491
486
  + Add Block
@@ -500,21 +495,21 @@ export default function SectionV2Column({
500
495
  {/* Hidden for section-level blocks (e.g. projectGridBlock) that own the full column */}
501
496
  {hasBlocks && !singleSectionBlock && (
502
497
  <div
503
- className={`absolute left-0 right-0 z-[3] transition-all ${
498
+ className={`flex-1 min-h-0 flex items-center justify-center z-[3] transition-all ${
504
499
  showChrome ? "opacity-100" : showFaintOutline ? "opacity-30" : "opacity-0 pointer-events-none"
505
500
  }`}
506
- style={{ bottom: 0, transform: "translateY(100%)", padding: "4px 12px 0" }}
501
+ style={{ minHeight: 24 }}
507
502
  >
508
503
  <button
509
504
  onClick={handleAddBlockBelow}
510
505
  aria-label="Add block below existing blocks"
511
- className="w-full py-1.5 text-[11px] font-medium rounded transition-all"
506
+ className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
512
507
  style={{
508
+ padding: "4px 14px",
513
509
  pointerEvents: showChrome ? "auto" : "none",
514
- background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
515
- color: "rgba(100,220,170,0.8)",
516
- boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
517
- border: "1px solid rgba(255,255,255,0.06)",
510
+ background: "rgba(13, 150, 104, 0.12)",
511
+ color: "#0d9668",
512
+ border: "1px dashed rgba(13, 150, 104, 0.5)",
518
513
  }}
519
514
  >
520
515
  + Add Block
@@ -37,6 +37,7 @@ import {
37
37
  ColumnV2Settings,
38
38
  ParallaxSlideSettings,
39
39
  ParallaxGroupSettings,
40
+ CoverSectionSettings,
40
41
  } from "./settings-panel";
41
42
 
42
43
  type SettingsTab = "settings" | "layout" | "seo" | "animation";
@@ -49,6 +50,7 @@ export default function SettingsPanel() {
49
50
  const {
50
51
  selectedSectionV2,
51
52
  selectedCustomSectionInstance,
53
+ selectedCoverSection,
52
54
  selectedParallaxGroup,
53
55
  selectedParallaxSlide,
54
56
  effectiveSectionV2,
@@ -59,6 +61,7 @@ export default function SettingsPanel() {
59
61
  HeaderIconComponent,
60
62
  isColumnOnly,
61
63
  isParallaxGroupOnly,
64
+ isCoverSectionOnly,
62
65
  isPageLevel,
63
66
  } = sel;
64
67
 
@@ -141,6 +144,9 @@ export default function SettingsPanel() {
141
144
  } else if (selectedParallaxGroup && !selectedParallaxSlide) {
142
145
  onDelete = () => store.deleteSection(selectedParallaxGroup._key);
143
146
  deleteTitle = "Delete Parallax Group";
147
+ } else if (selectedCoverSection) {
148
+ onDelete = () => store.deleteSection(selectedCoverSection._key);
149
+ deleteTitle = "Delete Cover Section";
144
150
  } else if (selectedSectionV2) {
145
151
  onDelete = () => store.deleteSection(selectedSectionV2._key);
146
152
  deleteTitle = "Delete Section";
@@ -327,6 +333,14 @@ export default function SettingsPanel() {
327
333
  <CustomSectionSettings instance={selectedCustomSectionInstance} />
328
334
  )
329
335
  ) :
336
+ /* ---- Cover Section routing ---- */
337
+ isCoverSectionOnly && selectedCoverSection ? (
338
+ activeTab === "animation" ? (
339
+ <SectionV2AnimationTab section={effectiveSectionV2!} />
340
+ ) : (
341
+ <CoverSectionSettings section={selectedCoverSection} />
342
+ )
343
+ ) :
330
344
  /* ---- V2 Section / Column / Block routing ---- */
331
345
  /* BUG-V2-003 fix: When a block inside a V2 column is selected, show BlockSettings
332
346
  instead of ColumnV2Settings. Block selection takes priority over column. */
@@ -60,6 +60,7 @@ interface SortableRowProps {
60
60
  onSelect: () => void;
61
61
  onDelete: () => void;
62
62
  onAddColumn: () => void;
63
+ addColumnLabel?: string;
63
64
  onDuplicate: () => void;
64
65
  onMoveUp: () => void;
65
66
  onMoveDown: () => void;
@@ -76,6 +77,7 @@ export default function SortableRow({
76
77
  onSelect,
77
78
  onDelete,
78
79
  onAddColumn,
80
+ addColumnLabel = "Col",
79
81
  onDuplicate,
80
82
  onMoveUp,
81
83
  onMoveDown,
@@ -87,7 +89,6 @@ export default function SortableRow({
87
89
  const selectBlock = useBuilderStore((s) => s.selectBlock);
88
90
  const canvasZoom = useBuilderStore((s) => s.canvasZoom);
89
91
  const activeViewport = useBuilderStore((s) => s.activeViewport);
90
- const gridSettings = useBuilderStore((s) => s.gridSettings);
91
92
  const customSectionCache = useBuilderStore((s) => s._customSectionCache);
92
93
  const [isHovered, setIsHovered] = useState(false);
93
94
  const {
@@ -176,24 +177,9 @@ export default function SortableRow({
176
177
  const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown> || {});
177
178
 
178
179
  const showToolbar = isSelected || isHovered;
179
- const coverRow = false;
180
180
 
181
181
  // ---- Preview Mode: clean rendering with row styles applied ----
182
182
  if (previewMode) {
183
- // Cover rows: full-width, no padding, no container (matches RowRenderer)
184
- if (coverRow) {
185
- return (
186
- <div
187
- ref={setNodeRef}
188
- style={{
189
- ...style,
190
- backgroundColor: bgColor !== "transparent" ? bgColor : undefined,
191
- }}
192
- >
193
- {children}
194
- </div>
195
- );
196
- }
197
183
  // Build merged styles for preview mode
198
184
  const previewRowStyle: React.CSSProperties = {
199
185
  ...style,
@@ -219,7 +205,7 @@ export default function SortableRow({
219
205
  const designRowStyle: React.CSSProperties = {
220
206
  ...style,
221
207
  ...layoutStyles,
222
- minHeight: coverRow ? undefined : minHeight,
208
+ minHeight,
223
209
  };
224
210
  // Legacy fallback: if no layout background set, use old bgColor
225
211
  if (!layoutStyles.backgroundColor && !layoutStyles.backgroundImage) {
@@ -332,10 +318,10 @@ export default function SortableRow({
332
318
  onClick={(e) => { e.stopPropagation(); onAddColumn(); }}
333
319
  onPointerDown={(e) => e.stopPropagation()}
334
320
  className="flex items-center gap-1 text-[11px] text-white/50 hover:text-white/85 transition-colors py-0.5"
335
- title="Add column"
336
- aria-label="Add column"
321
+ title={`Add ${addColumnLabel.toLowerCase()}`}
322
+ aria-label={`Add ${addColumnLabel.toLowerCase()}`}
337
323
  >
338
- <span className="text-white/30">+</span> Col
324
+ <span className="text-white/30">+</span> {addColumnLabel}
339
325
  </button>
340
326
  )}
341
327
 
@@ -364,7 +350,7 @@ export default function SortableRow({
364
350
  )}
365
351
 
366
352
  {/* Content — same layout as Preview */}
367
- <div style={coverRow ? undefined : { maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative">
353
+ <div style={{ maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative">
368
354
  {children}
369
355
  </div>
370
356
  </div>
@@ -14,8 +14,8 @@ export const BLOCK_GRADIENTS: Record<string, string> = {
14
14
  videoBlock: "linear-gradient(135deg, #ffb8d4 0%, #ffc8a8 50%, #ffe0b8 100%)",
15
15
  spacerBlock: "linear-gradient(135deg, #d8d8e8 0%, #e8e8f0 50%, #f0f0f8 100%)",
16
16
  buttonBlock: "linear-gradient(135deg, #ffb8e0 0%, #b8ffe8 50%, #a8ffd8 100%)",
17
- coverBlock: "linear-gradient(135deg, #ffd0a8 0%, #ffc090 50%, #ffb080 100%)",
18
17
  projectGridBlock: "linear-gradient(135deg, #ffd4a8 0%, #ffe8b8 50%, #fff0c8 100%)",
18
+ coverSection: "linear-gradient(135deg, #b2f5ea 0%, #81e6d9 50%, #5eead4 100%)",
19
19
  parallaxGroup: "linear-gradient(135deg, #c8a8ff 0%, #d8b8ff 50%, #e8d0ff 100%)",
20
20
  customSectionInstance: "linear-gradient(135deg, #d0b8ff 0%, #b8a8f8 50%, #c8b8ff 100%)",
21
21
  // Non-block contexts
@@ -149,27 +149,26 @@ export function ButtonBlockIcon({ size = 28 }: { size?: number }) {
149
149
  );
150
150
  }
151
151
 
152
- export function CoverBlockIcon({ size = 28 }: { size?: number }) {
152
+ // ── Non-block context icons ──
153
+
154
+ export function CoverSectionSettingsIcon({ size = 28 }: { size?: number }) {
153
155
  return (
154
156
  <svg width={size} height={size} viewBox="0 0 40 40" fill="none">
155
157
  <defs>
156
- <linearGradient id="starGrad" x1="10" y1="4" x2="30" y2="36">
157
- <stop offset="0%" stopColor="#f0a040" />
158
- <stop offset="100%" stopColor="#e07820" />
158
+ <linearGradient id="csSettingsGrad" x1="5" y1="5" x2="35" y2="35">
159
+ <stop offset="0%" stopColor="#0d9488" />
160
+ <stop offset="100%" stopColor="#0f766e" />
159
161
  </linearGradient>
160
- <filter id="starDrop">
161
- <feDropShadow dx="0" dy="1.5" stdDeviation="2" floodColor="rgba(200,100,20,0.3)" />
162
- </filter>
163
162
  </defs>
164
- <path d="M20,2 L24,15 L37,20 L24,25 L20,38 L16,25 L3,20 L16,15 Z" fill="url(#starGrad)" filter="url(#starDrop)" />
165
- <path d="M20,2 L24,15 L37,20 L16,15 Z" fill="white" opacity="0.2" />
166
- <circle cx="20" cy="20" r="2.5" fill="white" opacity="0.4" />
163
+ <rect x="3" y="3" width="34" height="34" rx="6" fill="url(#csSettingsGrad)" opacity="0.12" />
164
+ <rect x="3" y="3" width="34" height="34" rx="6" stroke="url(#csSettingsGrad)" strokeWidth="1.5" fill="none" opacity="0.4" />
165
+ <rect x="7" y="7" width="26" height="16" rx="2" fill="url(#csSettingsGrad)" opacity="0.2" />
166
+ <rect x="7" y="25" width="26" height="8" rx="2" fill="url(#csSettingsGrad)" opacity="0.35" />
167
+ <line x1="9" y1="24" x2="31" y2="24" stroke="#0d9488" strokeWidth="1" opacity="0.4" strokeDasharray="2 2" />
167
168
  </svg>
168
169
  );
169
170
  }
170
171
 
171
- // ── Non-block context icons ──
172
-
173
172
  export function RowIcon({ size = 28 }: { size?: number }) {
174
173
  return (
175
174
  <svg width={size} height={size} viewBox="0 0 40 40" fill="none">
@@ -285,9 +284,9 @@ export const BLOCK_ICON_COMPONENTS: Record<string, React.FC<{ size?: number }>>
285
284
  videoBlock: VideoBlockIcon,
286
285
  spacerBlock: SpacerBlockIcon,
287
286
  buttonBlock: ButtonBlockIcon,
288
- coverBlock: CoverBlockIcon,
289
287
  projectGridBlock: ProjectGridBlockIcon,
290
288
  parallaxGroup: ParallaxGroupIcon,
289
+ coverSection: CoverSectionSettingsIcon,
291
290
  customSectionInstance: CustomSectionInstanceIcon,
292
291
  row: RowIcon,
293
292
  column: ColumnIcon,
@@ -4,7 +4,6 @@ export { default as ImageGridBlockEditor } from "./ImageGridBlockEditor";
4
4
  export { default as VideoBlockEditor } from "./VideoBlockEditor";
5
5
  export { default as SpacerBlockEditor } from "./SpacerBlockEditor";
6
6
  export { default as ButtonBlockEditor } from "./ButtonBlockEditor";
7
- export { default as CoverBlockEditor } from "./CoverBlockEditor";
8
7
  export { default as ProjectGridEditor } from "./ProjectGridEditor";
9
8
  export { SettingsField, SettingsSection, StyledSelect, StyledInput, StyledCheckbox } from "./shared";
10
9
  export { getSpacerPx } from "./SpacerBlockEditor";
@@ -7,6 +7,7 @@ export { default as SettingsPanel } from "./SettingsPanel";
7
7
  export { default as BuilderCanvas } from "./BuilderCanvas";
8
8
  export { default as SectionV2Canvas } from "./SectionV2Canvas";
9
9
  export { default as ParallaxGroupCanvas } from "./ParallaxGroupCanvas";
10
+ export { default as CoverSectionCanvas } from "./CoverSectionCanvas";
10
11
  export { default as SectionV2Column } from "./SectionV2Column";
11
12
  export { default as CanvasToolbar } from "./CanvasToolbar";
12
13
  export { makeRowId, makeBlockId, makeColumnDroppableId } from "./DndWrapper";
@@ -11,7 +11,7 @@
11
11
  * debounce + snapshot pattern as the original LiveTextEditor.
12
12
  */
13
13
 
14
- import { useRef, useCallback, useEffect, useMemo } from "react";
14
+ import { useState, useRef, useCallback, useEffect, useMemo } from "react";
15
15
  import { useEditor, EditorContent, type Editor } from "@tiptap/react";
16
16
  import StarterKit from "@tiptap/starter-kit";
17
17
  import Underline from "@tiptap/extension-underline";
@@ -63,6 +63,7 @@ export default function RichTextEditor({ block, editable = false }: RichTextEdit
63
63
  const snapshotPushedRef = useRef(false);
64
64
  // Track block key to detect when we switch to a different block
65
65
  const blockKeyRef = useRef(block._key);
66
+ const [isEmpty, setIsEmpty] = useState(true);
66
67
 
67
68
  const style = block.style || {};
68
69
  const cols = block.columns && block.columns > 1 ? block.columns : undefined;
@@ -115,8 +116,12 @@ export default function RichTextEditor({ block, editable = false }: RichTextEdit
115
116
  ],
116
117
  content: initialContent,
117
118
  editable,
119
+ onCreate: ({ editor }) => {
120
+ setIsEmpty(editor.isEmpty);
121
+ },
118
122
  // Debounced update on every content change
119
123
  onUpdate: ({ editor }) => {
124
+ setIsEmpty(editor.isEmpty);
120
125
  if (debounceRef.current) clearTimeout(debounceRef.current);
121
126
  debounceRef.current = setTimeout(() => {
122
127
  commitContent(editor);
@@ -203,7 +208,8 @@ export default function RichTextEditor({ block, editable = false }: RichTextEdit
203
208
  fontFamily: "inherit",
204
209
  whiteSpace: "pre-wrap",
205
210
  wordBreak: "break-word",
206
- minHeight: "1em",
211
+ minHeight: editable ? "48px" : "1em",
212
+ position: "relative" as const,
207
213
  ...(cols
208
214
  ? {
209
215
  columnCount: cols,
@@ -218,6 +224,21 @@ export default function RichTextEditor({ block, editable = false }: RichTextEdit
218
224
  <div style={computedStyle} className="rich-text-editor-root">
219
225
  {editable && <RichTextBubbleMenu editor={editor} />}
220
226
  <EditorContent editor={editor} />
227
+ {editable && isEmpty && (
228
+ <div
229
+ className="absolute inset-0 pointer-events-none select-none"
230
+ style={{
231
+ color: "#a3a3a3",
232
+ fontStyle: "italic",
233
+ fontSize: "13px",
234
+ fontWeight: 400,
235
+ display: "flex",
236
+ alignItems: "center",
237
+ }}
238
+ >
239
+ Click to edit...
240
+ </div>
241
+ )}
221
242
  </div>
222
243
  );
223
244
  }
@@ -7,6 +7,5 @@ export { default as LiveImageGridPreview } from "./LiveImageGridPreview";
7
7
  export { default as LiveVideoPreview } from "./LiveVideoPreview";
8
8
  export { default as LiveSpacerPreview } from "./LiveSpacerPreview";
9
9
  export { default as LiveButtonPreview } from "./LiveButtonPreview";
10
- export { default as LiveCoverPreview } from "./LiveCoverPreview";
11
10
  export { default as LiveProjectGridPreview } from "./LiveProjectGridPreview";
12
11
  export { ThumbBadge, LivePlaceholder, useProjectThumbnails, ProjectGridCard } from "./shared";
@@ -14,7 +14,6 @@ import {
14
14
  VideoBlockEditor,
15
15
  SpacerBlockEditor,
16
16
  ButtonBlockEditor,
17
- CoverBlockEditor,
18
17
  ProjectGridEditor,
19
18
  } from "../editors";
20
19
 
@@ -68,12 +67,6 @@ function BlockTypeEditor({ block }: { block: ContentBlock }) {
68
67
  block={block as import("../../../lib/sanity/types").ButtonBlock}
69
68
  />
70
69
  );
71
- case "coverBlock":
72
- return (
73
- <CoverBlockEditor
74
- block={block as import("../../../lib/sanity/types").CoverBlock}
75
- />
76
- );
77
70
  case "projectGridBlock":
78
71
  return (
79
72
  <ProjectGridEditor
@@ -0,0 +1,296 @@
1
+ "use client";
2
+
3
+ /**
4
+ * CoverSectionSettings — Settings panel for a Cover Section.
5
+ *
6
+ * Displays:
7
+ * - Background type segmented control (Image / Video)
8
+ * - AssetBrowser picker for image or video
9
+ * - Background position / size dropdowns
10
+ * - Overlay color + overlay opacity slider
11
+ * - Section height selector (100vh / 80vh / 50vh)
12
+ * - Row list with percentages and vertical align
13
+ * - Add/Remove row controls
14
+ *
15
+ * Layout and Animation tabs are delegated to SectionV2LayoutTab / SectionV2AnimationTab
16
+ * via a virtual PageSectionV2 (same pattern as parallax slides).
17
+ *
18
+ * Session 176: Cover Sections — Phase 6 (Settings Panel).
19
+ */
20
+
21
+ import { useBuilderStore } from "../../../lib/builder/store";
22
+ import type { CoverSection } from "../../../lib/sanity/types";
23
+ import {
24
+ BackgroundIcon,
25
+ OverlayIcon,
26
+ SpacingIcon,
27
+ GridGapsIcon,
28
+ } from "../editors/section-icons";
29
+ import {
30
+ SettingsField,
31
+ SettingsSection,
32
+ } from "../editors/shared";
33
+ import { AssetPathInput } from "../editors/shared";
34
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
35
+ import StaggerSettings from "../editors/StaggerSettings";
36
+
37
+ const BG_POSITION_OPTIONS = [
38
+ { value: "center center", label: "Center" },
39
+ { value: "top center", label: "Top" },
40
+ { value: "bottom center", label: "Bottom" },
41
+ { value: "center left", label: "Left" },
42
+ { value: "center right", label: "Right" },
43
+ { value: "top left", label: "Top Left" },
44
+ { value: "top right", label: "Top Right" },
45
+ { value: "bottom left", label: "Bottom Left" },
46
+ { value: "bottom right", label: "Bottom Right" },
47
+ ];
48
+
49
+ const BG_SIZE_OPTIONS = [
50
+ { value: "cover", label: "Cover" },
51
+ { value: "contain", label: "Contain" },
52
+ { value: "auto", label: "Auto" },
53
+ ];
54
+
55
+ const HEIGHT_OPTIONS = [
56
+ { value: "100vh", label: "Full Viewport (100vh)" },
57
+ { value: "80vh", label: "80% Viewport (80vh)" },
58
+ { value: "50vh", label: "50% Viewport (50vh)" },
59
+ ];
60
+
61
+ const ALIGN_OPTIONS = [
62
+ { value: "start", label: "Top" },
63
+ { value: "center", label: "Center" },
64
+ { value: "end", label: "Bottom" },
65
+ ];
66
+
67
+ const SELECT_CLASS = "w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#076bff] focus:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]";
68
+
69
+ interface CoverSectionSettingsProps {
70
+ section: CoverSection;
71
+ }
72
+
73
+ export default function CoverSectionSettings({ section }: CoverSectionSettingsProps) {
74
+ const store = useBuilderStore();
75
+ const paletteSwatches = usePaletteSwatches();
76
+
77
+ const bgType = section.background_type || "image";
78
+ const bgPosition = section.background_position || "center center";
79
+ const bgSize = section.background_size || "cover";
80
+ const overlayColor = section.background_overlay_color || "#000000";
81
+ const overlayOpacity = section.background_overlay_opacity ?? 0;
82
+
83
+ const updateBg = (fields: Partial<Pick<CoverSection,
84
+ "background_type" | "background_image" | "background_video" |
85
+ "background_position" | "background_size" |
86
+ "background_overlay_color" | "background_overlay_opacity"
87
+ >>) => {
88
+ store.updateCoverBackground(section._key, fields);
89
+ };
90
+
91
+ return (
92
+ <>
93
+ {/* Background */}
94
+ <SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
95
+ <SettingsField label="Type">
96
+ <div className="flex rounded-lg bg-[#f0f0f0] p-[3px]">
97
+ {(["image", "video"] as const).map((type) => (
98
+ <button
99
+ key={type}
100
+ onClick={() => updateBg({ background_type: type })}
101
+ className={`flex-1 py-1.5 rounded-md text-[11px] font-medium transition-all ${
102
+ bgType === type
103
+ ? "bg-white text-neutral-900 shadow-sm border border-[#e5e5e5]"
104
+ : "text-neutral-400 hover:text-neutral-500"
105
+ }`}
106
+ >
107
+ {type === "image" ? "Image" : "Video"}
108
+ </button>
109
+ ))}
110
+ </div>
111
+ </SettingsField>
112
+
113
+ <SettingsField label={bgType === "image" ? "Image" : "Video"}>
114
+ <AssetPathInput
115
+ value={bgType === "image" ? (section.background_image || "") : (section.background_video || "")}
116
+ onChange={(path) => {
117
+ if (bgType === "image") {
118
+ updateBg({ background_image: path });
119
+ } else {
120
+ updateBg({ background_video: path });
121
+ }
122
+ }}
123
+ filterType={bgType === "image" ? "image" : "video"}
124
+ placeholder={bgType === "image" ? "path/to/image.jpg" : "path/to/video.mp4"}
125
+ />
126
+ </SettingsField>
127
+
128
+ <SettingsField label="Position">
129
+ <select
130
+ value={bgPosition}
131
+ onChange={(e) => updateBg({ background_position: e.target.value })}
132
+ className={SELECT_CLASS}
133
+ >
134
+ {BG_POSITION_OPTIONS.map((opt) => (
135
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
136
+ ))}
137
+ </select>
138
+ </SettingsField>
139
+
140
+ <SettingsField label="Size">
141
+ <select
142
+ value={bgSize}
143
+ onChange={(e) => updateBg({ background_size: e.target.value as CoverSection["background_size"] })}
144
+ className={SELECT_CLASS}
145
+ >
146
+ {BG_SIZE_OPTIONS.map((opt) => (
147
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
148
+ ))}
149
+ </select>
150
+ </SettingsField>
151
+ </SettingsSection>
152
+
153
+ {/* Overlay */}
154
+ <SettingsSection title="Overlay" defaultOpen icon={<OverlayIcon />}>
155
+ <SettingsField label="Color">
156
+ <ColorSwatchPicker
157
+ value={overlayColor}
158
+ onChange={(val) => updateBg({ background_overlay_color: typeof val === "string" ? val : "" })}
159
+ swatches={paletteSwatches}
160
+ />
161
+ </SettingsField>
162
+
163
+ <SettingsField label="Opacity">
164
+ <div className="flex items-center gap-2">
165
+ <input
166
+ type="range"
167
+ min={0}
168
+ max={100}
169
+ step={5}
170
+ value={overlayOpacity}
171
+ onChange={(e) => updateBg({ background_overlay_opacity: parseInt(e.target.value) })}
172
+ className="flex-1 accent-[#076bff]"
173
+ />
174
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
175
+ {overlayOpacity}%
176
+ </span>
177
+ </div>
178
+ </SettingsField>
179
+ </SettingsSection>
180
+
181
+ {/* Section Height */}
182
+ <SettingsSection title="Height" defaultOpen icon={<SpacingIcon />}>
183
+ <SettingsField label="Height">
184
+ <select
185
+ value={section.height}
186
+ onChange={(e) => store.updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
187
+ className={SELECT_CLASS}
188
+ >
189
+ {HEIGHT_OPTIONS.map((opt) => (
190
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
191
+ ))}
192
+ </select>
193
+ </SettingsField>
194
+ </SettingsSection>
195
+
196
+ {/* Rows */}
197
+ <SettingsSection title="Rows" defaultOpen icon={<GridGapsIcon />}>
198
+ <div className="space-y-2">
199
+ {section.cover_rows.map((row, i) => (
200
+ <div
201
+ key={row._key}
202
+ className="flex items-center gap-2 rounded-lg bg-[#f5f5f5] px-2.5 py-2"
203
+ >
204
+ <span className="text-[10px] font-semibold text-neutral-400 w-6 shrink-0">
205
+ R{i + 1}
206
+ </span>
207
+ <span className="text-xs text-neutral-700 font-medium tabular-nums w-10">
208
+ {row.height_percent}%
209
+ </span>
210
+ <select
211
+ value={row.vertical_align}
212
+ onChange={(e) =>
213
+ store.updateCoverRowAlign(
214
+ section._key,
215
+ row._key,
216
+ e.target.value as "start" | "center" | "end"
217
+ )
218
+ }
219
+ className="flex-1 rounded-md border border-transparent bg-white px-2 py-1 text-[11px] text-neutral-700 outline-none hover:border-[#e5e5e5] focus:border-[#076bff]"
220
+ >
221
+ {ALIGN_OPTIONS.map((opt) => (
222
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
223
+ ))}
224
+ </select>
225
+ {section.cover_rows.length > 1 && (
226
+ <button
227
+ onClick={() => store.removeCoverRow(section._key, row._key)}
228
+ className="text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
229
+ title="Remove row"
230
+ >
231
+
232
+ </button>
233
+ )}
234
+ </div>
235
+ ))}
236
+ </div>
237
+
238
+ {section.cover_rows.length < 5 && (
239
+ <button
240
+ onClick={() => store.addCoverRow(section._key)}
241
+ className="w-full mt-2 rounded-lg border border-dashed border-neutral-300 py-2 text-[11px] font-medium text-neutral-400 hover:text-neutral-600 hover:border-neutral-400 transition-colors"
242
+ >
243
+ + Add Row
244
+ </button>
245
+ )}
246
+
247
+ <p className="text-[10px] text-neutral-400 leading-snug px-0.5 mt-2">
248
+ Drag the handles between rows in the canvas to resize. Heights always sum to 100%.
249
+ </p>
250
+ </SettingsSection>
251
+
252
+ {/* Grid Gaps */}
253
+ <SettingsSection title="Grid" defaultOpen={false} icon={<GridGapsIcon />}>
254
+ <SettingsField label="Col Gap">
255
+ <div className="flex items-center gap-2">
256
+ <input
257
+ type="range"
258
+ min={0}
259
+ max={60}
260
+ step={2}
261
+ value={section.settings.col_gap ?? 20}
262
+ onChange={(e) => store.updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
263
+ className="flex-1 accent-[#076bff]"
264
+ />
265
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
266
+ {section.settings.col_gap ?? 20}px
267
+ </span>
268
+ </div>
269
+ </SettingsField>
270
+
271
+ <SettingsField label="Row Gap">
272
+ <div className="flex items-center gap-2">
273
+ <input
274
+ type="range"
275
+ min={0}
276
+ max={60}
277
+ step={2}
278
+ value={section.settings.row_gap ?? 20}
279
+ onChange={(e) => store.updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
280
+ className="flex-1 accent-[#076bff]"
281
+ />
282
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
283
+ {section.settings.row_gap ?? 20}px
284
+ </span>
285
+ </div>
286
+ </SettingsField>
287
+ </SettingsSection>
288
+
289
+ {/* Stagger */}
290
+ <StaggerSettings
291
+ stagger={section.settings.stagger}
292
+ onChange={(s) => store.updateCoverSettings(section._key, { stagger: s })}
293
+ />
294
+ </>
295
+ );
296
+ }