@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.
- package/README.md +2 -1
- package/app/admin/pages/[slug]/page.tsx +39 -2
- package/components/blocks/BlockRenderer.tsx +0 -7
- package/components/blocks/CoverSectionRenderer.tsx +295 -0
- package/components/blocks/PageRenderer.tsx +13 -9
- package/components/builder/BlockLivePreview.tsx +0 -5
- package/components/builder/BlockTypePicker.tsx +0 -1
- package/components/builder/ColorSwatchPicker.tsx +2 -2
- package/components/builder/CoverRowResizeHandle.tsx +180 -0
- package/components/builder/CoverSectionCanvas.tsx +260 -0
- package/components/builder/ReadOnlyFrame.tsx +127 -3
- package/components/builder/SectionTypePicker.tsx +29 -0
- package/components/builder/SectionV2Canvas.tsx +4 -1
- package/components/builder/SectionV2Column.tsx +15 -20
- package/components/builder/SettingsPanel.tsx +14 -0
- package/components/builder/SortableRow.tsx +7 -21
- package/components/builder/blockStyles.tsx +13 -14
- package/components/builder/editors/index.ts +0 -1
- package/components/builder/index.ts +1 -0
- package/components/builder/live-preview/RichTextEditor.tsx +23 -2
- package/components/builder/live-preview/index.ts +0 -1
- package/components/builder/settings-panel/BlockSettings.tsx +0 -7
- package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
- package/components/builder/settings-panel/index.ts +1 -0
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
- package/lib/animation/enter-types.ts +0 -1
- package/lib/animation/hover-effect-types.ts +0 -1
- package/lib/builder/defaults.ts +43 -22
- package/lib/builder/serializer/normalizers.ts +34 -1
- package/lib/builder/serializer/serializers.ts +39 -2
- package/lib/builder/store-blocks.ts +11 -3
- package/lib/builder/store-cover.ts +220 -0
- package/lib/builder/store-helpers.ts +81 -4
- package/lib/builder/store-sections.ts +12 -2
- package/lib/builder/store.ts +11 -2
- package/lib/builder/types.ts +15 -2
- package/lib/sanity/types.ts +79 -43
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/index.ts +1 -2
- package/sanity/schemas/index.ts +5 -3
- package/sanity/schemas/objects/coverSection.ts +317 -0
- package/sanity/schemas/objects/parallaxSlide.ts +0 -1
- package/sanity/schemas/page.ts +1 -1
- package/sanity/schemas/pageSectionV2.ts +0 -1
- package/components/blocks/CoverBlockRenderer.tsx +0 -261
- package/components/builder/editors/CoverBlockEditor.tsx +0 -550
- package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
- 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={`
|
|
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
|
-
: "
|
|
476
|
+
: "opacity-0 pointer-events-none"
|
|
477
477
|
}`}
|
|
478
478
|
style={{
|
|
479
|
+
padding: "5px 16px",
|
|
479
480
|
pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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={`
|
|
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={{
|
|
501
|
+
style={{ minHeight: 24 }}
|
|
507
502
|
>
|
|
508
503
|
<button
|
|
509
504
|
onClick={handleAddBlockBelow}
|
|
510
505
|
aria-label="Add block below existing blocks"
|
|
511
|
-
className="
|
|
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: "
|
|
515
|
-
color: "
|
|
516
|
-
|
|
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
|
|
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=
|
|
336
|
-
aria-label=
|
|
321
|
+
title={`Add ${addColumnLabel.toLowerCase()}`}
|
|
322
|
+
aria-label={`Add ${addColumnLabel.toLowerCase()}`}
|
|
337
323
|
>
|
|
338
|
-
<span className="text-white/30">+</span>
|
|
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={
|
|
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
|
-
|
|
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="
|
|
157
|
-
<stop offset="0%" stopColor="#
|
|
158
|
-
<stop offset="100%" stopColor="#
|
|
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
|
-
<
|
|
165
|
-
<
|
|
166
|
-
<
|
|
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
|
+
}
|