@morphika/andami 0.2.12 → 0.2.14
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/ImageBlockRenderer.tsx +12 -10
- package/components/blocks/PageRenderer.tsx +13 -9
- package/components/blocks/VideoBlockRenderer.tsx +11 -6
- 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/ImageBlockEditor.tsx +1 -0
- package/components/builder/editors/VideoBlockEditor.tsx +1 -0
- package/components/builder/editors/index.ts +0 -1
- package/components/builder/index.ts +1 -0
- package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
- package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
- 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/queries.ts +18 -4
- package/lib/sanity/types.ts +81 -45
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/imageBlock.ts +1 -0
- package/sanity/schemas/blocks/index.ts +1 -2
- package/sanity/schemas/blocks/videoBlock.ts +1 -0
- 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,
|
|
@@ -113,6 +113,7 @@ export default function ImageBlockEditor({ block }: Props) {
|
|
|
113
113
|
{ value: "full", label: "100%" },
|
|
114
114
|
{ value: "contained", label: "75%" },
|
|
115
115
|
{ value: "small", label: "50%" },
|
|
116
|
+
{ value: "fill", label: "Fill" },
|
|
116
117
|
] as const
|
|
117
118
|
).map((opt) => (
|
|
118
119
|
<button
|
|
@@ -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";
|
|
@@ -21,8 +21,10 @@ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
|
|
|
21
21
|
const thumbSrc = adminThumbUrl(block.asset_path);
|
|
22
22
|
const fullSrc = adminAssetUrl(block.asset_path);
|
|
23
23
|
const src = useFallback ? fullSrc : thumbSrc;
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
const isFill = block.width === "fill";
|
|
25
|
+
const widthStyle = isFill
|
|
26
|
+
? "100%"
|
|
27
|
+
: block.width === "contained"
|
|
26
28
|
? "75%"
|
|
27
29
|
: block.width === "small"
|
|
28
30
|
? "50%"
|
|
@@ -35,6 +37,23 @@ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
|
|
|
35
37
|
"21:9": "21/9",
|
|
36
38
|
};
|
|
37
39
|
|
|
40
|
+
if (isFill) {
|
|
41
|
+
return (
|
|
42
|
+
<div style={{ position: "absolute", inset: 0, overflow: "hidden", borderRadius: block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined }}>
|
|
43
|
+
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
44
|
+
<img
|
|
45
|
+
src={src}
|
|
46
|
+
alt={block.alt || ""}
|
|
47
|
+
onLoad={() => setImgLoaded(true)}
|
|
48
|
+
onError={() => {
|
|
49
|
+
if (!useFallback) { setUseFallback(true); } else { setImgError(true); }
|
|
50
|
+
}}
|
|
51
|
+
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
return (
|
|
39
58
|
<div style={{ width: widthStyle, margin: block.width !== "full" ? "0 auto" : undefined }}>
|
|
40
59
|
{imgError ? (
|
|
@@ -30,8 +30,9 @@ export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
|
|
|
30
30
|
"4:3": "75%",
|
|
31
31
|
auto: "56.25%",
|
|
32
32
|
};
|
|
33
|
+
const isFill = block.width === "fill";
|
|
33
34
|
const paddingBottom = aspectMap[block.aspect_ratio || "16:9"] || "56.25%";
|
|
34
|
-
const widthStyle = block.width === "contained" ? "75%" : "100%";
|
|
35
|
+
const widthStyle = isFill ? "100%" : (block.width === "contained" ? "75%" : "100%");
|
|
35
36
|
|
|
36
37
|
// Resolve thumbnail URL based on video type (no iframes, no streaming)
|
|
37
38
|
let thumbnailUrl: string | null = null;
|
|
@@ -59,9 +60,13 @@ export default function LiveVideoPreview({ block }: { block: VideoBlock }) {
|
|
|
59
60
|
|
|
60
61
|
const borderRadius = block.border_radius ? `${String(block.border_radius).replace(/px$/i, "")}px` : undefined;
|
|
61
62
|
|
|
63
|
+
const outerStyle: React.CSSProperties = isFill
|
|
64
|
+
? { position: "absolute", inset: 0, minWidth: 0, borderRadius, overflow: "hidden" }
|
|
65
|
+
: { width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined };
|
|
66
|
+
|
|
62
67
|
return (
|
|
63
|
-
<div style={
|
|
64
|
-
<div style={{ position: "relative", paddingBottom, overflow: "hidden", background: "#000", lineHeight: 0, fontSize: 0, borderRadius: "inherit" }}>
|
|
68
|
+
<div style={outerStyle}>
|
|
69
|
+
<div style={{ position: "relative", paddingBottom: isFill ? undefined : paddingBottom, height: isFill ? "100%" : undefined, overflow: "hidden", background: "#000", lineHeight: 0, fontSize: 0, borderRadius: "inherit" }}>
|
|
65
70
|
{thumbnailUrl ? (
|
|
66
71
|
<>
|
|
67
72
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
@@ -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
|