@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,335 +1,337 @@
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
- NavbarColorIcon,
26
- OverlayIcon,
27
- SpacingIcon,
28
- GridGapsIcon,
29
- } from "../editors/section-icons";
30
- import {
31
- SettingsField,
32
- SettingsSection,
33
- } from "../editors/shared";
34
- import { AssetPathInput } from "../editors/shared";
35
- import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
36
- import StaggerSettings from "../editors/StaggerSettings";
37
-
38
- const BG_POSITION_OPTIONS = [
39
- { value: "center center", label: "Center" },
40
- { value: "top center", label: "Top" },
41
- { value: "bottom center", label: "Bottom" },
42
- { value: "center left", label: "Left" },
43
- { value: "center right", label: "Right" },
44
- { value: "top left", label: "Top Left" },
45
- { value: "top right", label: "Top Right" },
46
- { value: "bottom left", label: "Bottom Left" },
47
- { value: "bottom right", label: "Bottom Right" },
48
- ];
49
-
50
- const BG_SIZE_OPTIONS = [
51
- { value: "cover", label: "Cover" },
52
- { value: "contain", label: "Contain" },
53
- { value: "auto", label: "Auto" },
54
- ];
55
-
56
- const HEIGHT_OPTIONS = [
57
- { value: "100vh", label: "Full Viewport (100vh)" },
58
- { value: "80vh", label: "80% Viewport (80vh)" },
59
- { value: "50vh", label: "50% Viewport (50vh)" },
60
- { value: "20vh", label: "20% Viewport (20vh)" },
61
- ];
62
-
63
- const ALIGN_OPTIONS = [
64
- { value: "start", label: "Top" },
65
- { value: "center", label: "Center" },
66
- { value: "end", label: "Bottom" },
67
- ];
68
-
69
- 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)]";
70
-
71
- interface CoverSectionSettingsProps {
72
- section: CoverSection;
73
- }
74
-
75
- export default function CoverSectionSettings({ section }: CoverSectionSettingsProps) {
76
- const store = useBuilderStore();
77
- const paletteSwatches = usePaletteSwatches();
78
-
79
- const bgType = section.background_type || "image";
80
- const bgPosition = section.background_position || "center center";
81
- const bgSize = section.background_size || "cover";
82
- const overlayColor = section.background_overlay_color || "#000000";
83
- const overlayOpacity = section.background_overlay_opacity ?? 0;
84
-
85
- const updateBg = (fields: Partial<Pick<CoverSection,
86
- "background_type" | "background_color" | "background_image" | "background_video" |
87
- "background_position" | "background_size" |
88
- "background_overlay_color" | "background_overlay_opacity" |
89
- "nav_color"
90
- >>) => {
91
- store.updateCoverBackground(section._key, fields);
92
- };
93
-
94
- return (
95
- <>
96
- {/* Background */}
97
- <SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
98
- <SettingsField label="Type">
99
- <div className="flex rounded-lg bg-[#f0f0f0] p-[3px]">
100
- {(["image", "video", "color"] as const).map((type) => (
101
- <button
102
- key={type}
103
- onClick={() => updateBg({ background_type: type })}
104
- className={`flex-1 py-1.5 rounded-md text-[11px] font-medium transition-all ${
105
- bgType === type
106
- ? "bg-white text-neutral-900 shadow-sm border border-[#e5e5e5]"
107
- : "text-neutral-400 hover:text-neutral-500"
108
- }`}
109
- >
110
- {type === "image" ? "Image" : type === "video" ? "Video" : "Color"}
111
- </button>
112
- ))}
113
- </div>
114
- </SettingsField>
115
-
116
- {bgType === "color" ? (
117
- <SettingsField label="Color">
118
- <ColorSwatchPicker
119
- value={section.background_color || "#000000"}
120
- onChange={(val) => updateBg({ background_color: typeof val === "string" ? val : "" })}
121
- swatches={paletteSwatches}
122
- />
123
- </SettingsField>
124
- ) : (
125
- <>
126
- <SettingsField label={bgType === "image" ? "Image" : "Video"}>
127
- <AssetPathInput
128
- value={bgType === "image" ? (section.background_image || "") : (section.background_video || "")}
129
- onChange={(path) => {
130
- if (bgType === "image") {
131
- updateBg({ background_image: path });
132
- } else {
133
- updateBg({ background_video: path });
134
- }
135
- }}
136
- filterType={bgType === "image" ? "image" : "video"}
137
- placeholder={bgType === "image" ? "path/to/image.jpg" : "path/to/video.mp4"}
138
- />
139
- </SettingsField>
140
-
141
- <SettingsField label="Position">
142
- <select
143
- value={bgPosition}
144
- onChange={(e) => updateBg({ background_position: e.target.value })}
145
- className={SELECT_CLASS}
146
- >
147
- {BG_POSITION_OPTIONS.map((opt) => (
148
- <option key={opt.value} value={opt.value}>{opt.label}</option>
149
- ))}
150
- </select>
151
- </SettingsField>
152
-
153
- <SettingsField label="Size">
154
- <select
155
- value={bgSize}
156
- onChange={(e) => updateBg({ background_size: e.target.value as CoverSection["background_size"] })}
157
- className={SELECT_CLASS}
158
- >
159
- {BG_SIZE_OPTIONS.map((opt) => (
160
- <option key={opt.value} value={opt.value}>{opt.label}</option>
161
- ))}
162
- </select>
163
- </SettingsField>
164
- </>
165
- )}
166
- </SettingsSection>
167
-
168
- {/* Navbar Color Override */}
169
- <SettingsSection title="Navbar Color" defaultOpen={false} icon={<NavbarColorIcon />}>
170
- <SettingsField label="Color">
171
- <div className="flex items-center gap-2">
172
- <ColorSwatchPicker
173
- value={section.nav_color || ""}
174
- onChange={(val) => updateBg({ nav_color: typeof val === "string" ? val : undefined })}
175
- swatches={paletteSwatches}
176
- />
177
- {section.nav_color && (
178
- <button
179
- onClick={() => updateBg({ nav_color: undefined })}
180
- className="text-[10px] text-neutral-400 hover:text-neutral-600 transition-colors shrink-0"
181
- >
182
- Clear
183
- </button>
184
- )}
185
- </div>
186
- </SettingsField>
187
- <p className="text-[10px] text-neutral-400 leading-snug px-0.5">
188
- Override the navbar text color while this cover section is on screen. Clears when the next section takes over.
189
- </p>
190
- </SettingsSection>
191
-
192
- {/* Overlay */}
193
- <SettingsSection title="Overlay" defaultOpen icon={<OverlayIcon />}>
194
- <SettingsField label="Color">
195
- <ColorSwatchPicker
196
- value={overlayColor}
197
- onChange={(val) => updateBg({ background_overlay_color: typeof val === "string" ? val : "" })}
198
- swatches={paletteSwatches}
199
- />
200
- </SettingsField>
201
-
202
- <SettingsField label="Opacity">
203
- <div className="flex items-center gap-2">
204
- <input
205
- type="range"
206
- min={0}
207
- max={100}
208
- step={5}
209
- value={overlayOpacity}
210
- onChange={(e) => updateBg({ background_overlay_opacity: parseInt(e.target.value) })}
211
- className="flex-1 accent-[#076bff]"
212
- />
213
- <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
214
- {overlayOpacity}%
215
- </span>
216
- </div>
217
- </SettingsField>
218
- </SettingsSection>
219
-
220
- {/* Section Height */}
221
- <SettingsSection title="Height" defaultOpen icon={<SpacingIcon />}>
222
- <SettingsField label="Height">
223
- <select
224
- value={section.height}
225
- onChange={(e) => store.updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
226
- className={SELECT_CLASS}
227
- >
228
- {HEIGHT_OPTIONS.map((opt) => (
229
- <option key={opt.value} value={opt.value}>{opt.label}</option>
230
- ))}
231
- </select>
232
- </SettingsField>
233
- </SettingsSection>
234
-
235
- {/* Rows */}
236
- <SettingsSection title="Rows" defaultOpen icon={<GridGapsIcon />}>
237
- <div className="space-y-2">
238
- {section.cover_rows.map((row, i) => (
239
- <div
240
- key={row._key}
241
- className="flex items-center gap-2 rounded-lg bg-[#f5f5f5] px-2.5 py-2"
242
- >
243
- <span className="text-[10px] font-semibold text-neutral-400 w-6 shrink-0">
244
- R{i + 1}
245
- </span>
246
- <span className="text-xs text-neutral-700 font-medium tabular-nums w-10">
247
- {row.height_percent}%
248
- </span>
249
- <select
250
- value={row.vertical_align}
251
- onChange={(e) =>
252
- store.updateCoverRowAlign(
253
- section._key,
254
- row._key,
255
- e.target.value as "start" | "center" | "end"
256
- )
257
- }
258
- 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]"
259
- >
260
- {ALIGN_OPTIONS.map((opt) => (
261
- <option key={opt.value} value={opt.value}>{opt.label}</option>
262
- ))}
263
- </select>
264
- {section.cover_rows.length > 1 && (
265
- <button
266
- onClick={() => store.removeCoverRow(section._key, row._key)}
267
- className="text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
268
- title="Remove row"
269
- >
270
-
271
- </button>
272
- )}
273
- </div>
274
- ))}
275
- </div>
276
-
277
- {section.cover_rows.length < 5 && (
278
- <button
279
- onClick={() => store.addCoverRow(section._key)}
280
- 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"
281
- >
282
- + Add Row
283
- </button>
284
- )}
285
-
286
- <p className="text-[10px] text-neutral-400 leading-snug px-0.5 mt-2">
287
- Drag the handles between rows in the canvas to resize. Heights always sum to 100%.
288
- </p>
289
- </SettingsSection>
290
-
291
- {/* Grid Gaps */}
292
- <SettingsSection title="Grid" defaultOpen={false} icon={<GridGapsIcon />}>
293
- <SettingsField label="Col Gap">
294
- <div className="flex items-center gap-2">
295
- <input
296
- type="range"
297
- min={0}
298
- max={60}
299
- step={2}
300
- value={section.settings.col_gap ?? 20}
301
- onChange={(e) => store.updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
302
- className="flex-1 accent-[#076bff]"
303
- />
304
- <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
305
- {section.settings.col_gap ?? 20}px
306
- </span>
307
- </div>
308
- </SettingsField>
309
-
310
- <SettingsField label="Row Gap">
311
- <div className="flex items-center gap-2">
312
- <input
313
- type="range"
314
- min={0}
315
- max={60}
316
- step={2}
317
- value={section.settings.row_gap ?? 20}
318
- onChange={(e) => store.updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
319
- className="flex-1 accent-[#076bff]"
320
- />
321
- <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
322
- {section.settings.row_gap ?? 20}px
323
- </span>
324
- </div>
325
- </SettingsField>
326
- </SettingsSection>
327
-
328
- {/* Stagger */}
329
- <StaggerSettings
330
- stagger={section.settings.stagger}
331
- onChange={(s) => store.updateCoverSettings(section._key, { stagger: s })}
332
- />
333
- </>
334
- );
335
- }
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
+ NavbarColorIcon,
26
+ OverlayIcon,
27
+ SpacingIcon,
28
+ GridGapsIcon,
29
+ } from "../editors/section-icons";
30
+ import {
31
+ SettingsField,
32
+ SettingsSection,
33
+ } from "../editors/shared";
34
+ import { AssetPathInput } from "../editors/shared";
35
+ import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
36
+ import StaggerSettings from "../editors/StaggerSettings";
37
+ import { BubbleTooltip } from "../BubbleIcons";
38
+
39
+ const BG_POSITION_OPTIONS = [
40
+ { value: "center center", label: "Center" },
41
+ { value: "top center", label: "Top" },
42
+ { value: "bottom center", label: "Bottom" },
43
+ { value: "center left", label: "Left" },
44
+ { value: "center right", label: "Right" },
45
+ { value: "top left", label: "Top Left" },
46
+ { value: "top right", label: "Top Right" },
47
+ { value: "bottom left", label: "Bottom Left" },
48
+ { value: "bottom right", label: "Bottom Right" },
49
+ ];
50
+
51
+ const BG_SIZE_OPTIONS = [
52
+ { value: "cover", label: "Cover" },
53
+ { value: "contain", label: "Contain" },
54
+ { value: "auto", label: "Auto" },
55
+ ];
56
+
57
+ const HEIGHT_OPTIONS = [
58
+ { value: "100vh", label: "Full Viewport (100vh)" },
59
+ { value: "80vh", label: "80% Viewport (80vh)" },
60
+ { value: "50vh", label: "50% Viewport (50vh)" },
61
+ { value: "20vh", label: "20% Viewport (20vh)" },
62
+ ];
63
+
64
+ const ALIGN_OPTIONS = [
65
+ { value: "start", label: "Top" },
66
+ { value: "center", label: "Center" },
67
+ { value: "end", label: "Bottom" },
68
+ ];
69
+
70
+ 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-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]";
71
+
72
+ interface CoverSectionSettingsProps {
73
+ section: CoverSection;
74
+ }
75
+
76
+ export default function CoverSectionSettings({ section }: CoverSectionSettingsProps) {
77
+ const store = useBuilderStore();
78
+ const paletteSwatches = usePaletteSwatches();
79
+
80
+ const bgType = section.background_type || "image";
81
+ const bgPosition = section.background_position || "center center";
82
+ const bgSize = section.background_size || "cover";
83
+ const overlayColor = section.background_overlay_color || "#000000";
84
+ const overlayOpacity = section.background_overlay_opacity ?? 0;
85
+
86
+ const updateBg = (fields: Partial<Pick<CoverSection,
87
+ "background_type" | "background_color" | "background_image" | "background_video" |
88
+ "background_position" | "background_size" |
89
+ "background_overlay_color" | "background_overlay_opacity" |
90
+ "nav_color"
91
+ >>) => {
92
+ store.updateCoverBackground(section._key, fields);
93
+ };
94
+
95
+ return (
96
+ <>
97
+ {/* Background */}
98
+ <SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
99
+ <SettingsField label="Type">
100
+ <div className="flex rounded-lg bg-[#f0f0f0] p-[3px]">
101
+ {(["image", "video", "color"] as const).map((type) => (
102
+ <button
103
+ key={type}
104
+ onClick={() => updateBg({ background_type: type })}
105
+ className={`flex-1 py-1.5 rounded-md text-[11px] font-medium transition-all ${
106
+ bgType === type
107
+ ? "bg-white text-neutral-900 shadow-sm border border-[#e5e5e5]"
108
+ : "text-neutral-400 hover:text-neutral-500"
109
+ }`}
110
+ >
111
+ {type === "image" ? "Image" : type === "video" ? "Video" : "Color"}
112
+ </button>
113
+ ))}
114
+ </div>
115
+ </SettingsField>
116
+
117
+ {bgType === "color" ? (
118
+ <SettingsField label="Color">
119
+ <ColorSwatchPicker
120
+ value={section.background_color || "#000000"}
121
+ onChange={(val) => updateBg({ background_color: typeof val === "string" ? val : "" })}
122
+ swatches={paletteSwatches}
123
+ />
124
+ </SettingsField>
125
+ ) : (
126
+ <>
127
+ <SettingsField label={bgType === "image" ? "Image" : "Video"}>
128
+ <AssetPathInput
129
+ value={bgType === "image" ? (section.background_image || "") : (section.background_video || "")}
130
+ onChange={(path) => {
131
+ if (bgType === "image") {
132
+ updateBg({ background_image: path });
133
+ } else {
134
+ updateBg({ background_video: path });
135
+ }
136
+ }}
137
+ filterType={bgType === "image" ? "image" : "video"}
138
+ placeholder={bgType === "image" ? "path/to/image.jpg" : "path/to/video.mp4"}
139
+ />
140
+ </SettingsField>
141
+
142
+ <SettingsField label="Position">
143
+ <select
144
+ value={bgPosition}
145
+ onChange={(e) => updateBg({ background_position: e.target.value })}
146
+ className={SELECT_CLASS}
147
+ >
148
+ {BG_POSITION_OPTIONS.map((opt) => (
149
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
150
+ ))}
151
+ </select>
152
+ </SettingsField>
153
+
154
+ <SettingsField label="Size">
155
+ <select
156
+ value={bgSize}
157
+ onChange={(e) => updateBg({ background_size: e.target.value as CoverSection["background_size"] })}
158
+ className={SELECT_CLASS}
159
+ >
160
+ {BG_SIZE_OPTIONS.map((opt) => (
161
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
162
+ ))}
163
+ </select>
164
+ </SettingsField>
165
+ </>
166
+ )}
167
+ </SettingsSection>
168
+
169
+ {/* Navbar Color Override */}
170
+ <SettingsSection title="Navbar Color" defaultOpen={false} icon={<NavbarColorIcon />}>
171
+ <SettingsField label="Color">
172
+ <div className="flex items-center gap-2">
173
+ <ColorSwatchPicker
174
+ value={section.nav_color || ""}
175
+ onChange={(val) => updateBg({ nav_color: typeof val === "string" ? val : undefined })}
176
+ swatches={paletteSwatches}
177
+ />
178
+ {section.nav_color && (
179
+ <button
180
+ onClick={() => updateBg({ nav_color: undefined })}
181
+ className="text-[10px] text-neutral-400 hover:text-neutral-600 transition-colors shrink-0"
182
+ >
183
+ Clear
184
+ </button>
185
+ )}
186
+ </div>
187
+ </SettingsField>
188
+ <p className="text-[10px] text-neutral-400 leading-snug px-0.5">
189
+ Override the navbar text color while this cover section is on screen. Clears when the next section takes over.
190
+ </p>
191
+ </SettingsSection>
192
+
193
+ {/* Overlay */}
194
+ <SettingsSection title="Overlay" defaultOpen icon={<OverlayIcon />}>
195
+ <SettingsField label="Color">
196
+ <ColorSwatchPicker
197
+ value={overlayColor}
198
+ onChange={(val) => updateBg({ background_overlay_color: typeof val === "string" ? val : "" })}
199
+ swatches={paletteSwatches}
200
+ />
201
+ </SettingsField>
202
+
203
+ <SettingsField label="Opacity">
204
+ <div className="flex items-center gap-2">
205
+ <input
206
+ type="range"
207
+ min={0}
208
+ max={100}
209
+ step={5}
210
+ value={overlayOpacity}
211
+ onChange={(e) => updateBg({ background_overlay_opacity: parseInt(e.target.value) })}
212
+ className="flex-1 accent-[#3580f9]"
213
+ />
214
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
215
+ {overlayOpacity}%
216
+ </span>
217
+ </div>
218
+ </SettingsField>
219
+ </SettingsSection>
220
+
221
+ {/* Section Height */}
222
+ <SettingsSection title="Height" defaultOpen icon={<SpacingIcon />}>
223
+ <SettingsField label="Height">
224
+ <select
225
+ value={section.height}
226
+ onChange={(e) => store.updateCoverHeight(section._key, e.target.value as CoverSection["height"])}
227
+ className={SELECT_CLASS}
228
+ >
229
+ {HEIGHT_OPTIONS.map((opt) => (
230
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
231
+ ))}
232
+ </select>
233
+ </SettingsField>
234
+ </SettingsSection>
235
+
236
+ {/* Rows */}
237
+ <SettingsSection title="Rows" defaultOpen icon={<GridGapsIcon />}>
238
+ <div className="space-y-2">
239
+ {section.cover_rows.map((row, i) => (
240
+ <div
241
+ key={row._key}
242
+ className="flex items-center gap-2 rounded-lg bg-[#f5f5f5] px-2.5 py-2"
243
+ >
244
+ <span className="text-[10px] font-semibold text-neutral-400 w-6 shrink-0">
245
+ R{i + 1}
246
+ </span>
247
+ <span className="text-xs text-neutral-700 font-medium tabular-nums w-10">
248
+ {row.height_percent}%
249
+ </span>
250
+ <select
251
+ value={row.vertical_align}
252
+ onChange={(e) =>
253
+ store.updateCoverRowAlign(
254
+ section._key,
255
+ row._key,
256
+ e.target.value as "start" | "center" | "end"
257
+ )
258
+ }
259
+ 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-[#3580f9]"
260
+ >
261
+ {ALIGN_OPTIONS.map((opt) => (
262
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
263
+ ))}
264
+ </select>
265
+ {section.cover_rows.length > 1 && (
266
+ <button
267
+ onClick={() => store.removeCoverRow(section._key, row._key)}
268
+ className="group/bb relative text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
269
+ aria-label="Remove row"
270
+ >
271
+
272
+ <BubbleTooltip>Remove row</BubbleTooltip>
273
+ </button>
274
+ )}
275
+ </div>
276
+ ))}
277
+ </div>
278
+
279
+ {section.cover_rows.length < 5 && (
280
+ <button
281
+ onClick={() => store.addCoverRow(section._key)}
282
+ 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"
283
+ >
284
+ + Add Row
285
+ </button>
286
+ )}
287
+
288
+ <p className="text-[10px] text-neutral-400 leading-snug px-0.5 mt-2">
289
+ Drag the handles between rows in the canvas to resize. Heights always sum to 100%.
290
+ </p>
291
+ </SettingsSection>
292
+
293
+ {/* Grid Gaps */}
294
+ <SettingsSection title="Grid" defaultOpen={false} icon={<GridGapsIcon />}>
295
+ <SettingsField label="Col Gap">
296
+ <div className="flex items-center gap-2">
297
+ <input
298
+ type="range"
299
+ min={0}
300
+ max={60}
301
+ step={2}
302
+ value={section.settings.col_gap ?? 20}
303
+ onChange={(e) => store.updateCoverSettings(section._key, { col_gap: parseInt(e.target.value) })}
304
+ className="flex-1 accent-[#3580f9]"
305
+ />
306
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
307
+ {section.settings.col_gap ?? 20}px
308
+ </span>
309
+ </div>
310
+ </SettingsField>
311
+
312
+ <SettingsField label="Row Gap">
313
+ <div className="flex items-center gap-2">
314
+ <input
315
+ type="range"
316
+ min={0}
317
+ max={60}
318
+ step={2}
319
+ value={section.settings.row_gap ?? 20}
320
+ onChange={(e) => store.updateCoverSettings(section._key, { row_gap: parseInt(e.target.value) })}
321
+ className="flex-1 accent-[#3580f9]"
322
+ />
323
+ <span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
324
+ {section.settings.row_gap ?? 20}px
325
+ </span>
326
+ </div>
327
+ </SettingsField>
328
+ </SettingsSection>
329
+
330
+ {/* Stagger */}
331
+ <StaggerSettings
332
+ stagger={section.settings.stagger}
333
+ onChange={(s) => store.updateCoverSettings(section._key, { stagger: s })}
334
+ />
335
+ </>
336
+ );
337
+ }