@morphika/andami 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +151 -36
  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 +320 -327
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +6 -6
  8. package/app/admin/pages/page.tsx +11 -11
  9. package/app/admin/projects/page.tsx +14 -14
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/components/admin/MetadataEditor.tsx +6 -6
  13. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  14. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  15. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  16. package/components/admin/nav-builder/NavGridItem.tsx +4 -4
  17. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  18. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  19. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  20. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  21. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  22. package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
  23. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  24. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  25. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  26. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  27. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  28. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  29. package/components/admin/styles/ColorsEditor.tsx +2 -2
  30. package/components/admin/styles/FontsEditor.tsx +6 -6
  31. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  32. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  33. package/components/admin/styles/TypographyEditor.tsx +6 -6
  34. package/components/admin/styles/shared.tsx +68 -68
  35. package/components/blocks/AudioBlockRenderer.tsx +286 -0
  36. package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
  37. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  38. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  39. package/components/builder/BlockCardIcons.tsx +316 -227
  40. package/components/builder/BlockTypePicker.tsx +3 -1
  41. package/components/builder/BubbleIcons.tsx +90 -0
  42. package/components/builder/BuilderCanvas.tsx +2 -0
  43. package/components/builder/CanvasMinimap.tsx +2 -2
  44. package/components/builder/CoverSectionCanvas.tsx +363 -275
  45. package/components/builder/DeviceFrame.tsx +1 -1
  46. package/components/builder/DndWrapper.tsx +3 -3
  47. package/components/builder/InsertionLines.tsx +1 -1
  48. package/components/builder/SectionCardIcons.tsx +421 -320
  49. package/components/builder/SectionEditorBar.tsx +1 -1
  50. package/components/builder/SectionTypePicker.tsx +4 -4
  51. package/components/builder/SectionV2Canvas.tsx +20 -4
  52. package/components/builder/SectionV2Column.tsx +74 -68
  53. package/components/builder/SortableBlock.tsx +93 -73
  54. package/components/builder/SortableRow.tsx +27 -26
  55. package/components/builder/VirtualAssetGrid.tsx +2 -2
  56. package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
  57. package/components/builder/asset-browser/helpers.ts +4 -0
  58. package/components/builder/asset-browser/types.ts +2 -1
  59. package/components/builder/blockStyles.tsx +192 -173
  60. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  61. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  62. package/components/builder/color-picker/EyedropperButton.tsx +74 -74
  63. package/components/builder/color-picker/HueSlider.tsx +124 -124
  64. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  65. package/components/builder/color-picker/SwatchBar.tsx +93 -93
  66. package/components/builder/editors/AudioBlockEditor.tsx +242 -0
  67. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
  68. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  69. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  70. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  71. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  72. package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
  73. package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
  74. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  75. package/components/builder/editors/ProjectGridEditor.tsx +9 -9
  76. package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
  77. package/components/builder/editors/StaggerSettings.tsx +109 -109
  78. package/components/builder/editors/TextBlockEditor.tsx +3 -3
  79. package/components/builder/editors/TextStylePicker.tsx +1 -1
  80. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  81. package/components/builder/editors/index.ts +11 -10
  82. package/components/builder/editors/shared.tsx +7 -7
  83. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
  84. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
  85. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  86. package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
  87. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  88. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  89. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  90. package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
  91. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  92. package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
  93. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  94. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  95. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  96. package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
  97. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  98. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  99. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  100. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  101. package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
  102. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  103. package/lib/animation/enter-types.ts +3 -0
  104. package/lib/animation/hover-effect-presets.ts +210 -210
  105. package/lib/animation/hover-effect-types.ts +3 -0
  106. package/lib/builder/block-registrations.ts +468 -335
  107. package/lib/builder/constants.ts +111 -111
  108. package/lib/builder/store-sections.ts +2 -2
  109. package/lib/builder/types-slices.ts +414 -414
  110. package/lib/builder/types.ts +6 -1
  111. package/lib/config/index.ts +27 -27
  112. package/lib/sanity/types.ts +156 -1
  113. package/lib/version.ts +1 -1
  114. package/package.json +1 -1
  115. package/sanity/schemas/blocks/audioBlock.ts +69 -0
  116. package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
  117. package/sanity/schemas/blocks/index.ts +12 -9
  118. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  119. package/sanity/schemas/index.ts +120 -111
  120. package/styles/admin.css +85 -85
  121. package/styles/animations.css +237 -237
  122. package/styles/base.css +114 -114
@@ -1,335 +1,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
-
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
+
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-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,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-[#3580f9]"
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-[#3580f9]"
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-[#3580f9]"
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-[#3580f9]"
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
+ }