@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.
Files changed (58) hide show
  1. package/README.md +2 -1
  2. package/app/admin/pages/[slug]/page.tsx +39 -2
  3. package/components/blocks/BlockRenderer.tsx +0 -7
  4. package/components/blocks/CoverSectionRenderer.tsx +295 -0
  5. package/components/blocks/ImageBlockRenderer.tsx +12 -10
  6. package/components/blocks/PageRenderer.tsx +13 -9
  7. package/components/blocks/VideoBlockRenderer.tsx +11 -6
  8. package/components/builder/BlockLivePreview.tsx +0 -5
  9. package/components/builder/BlockTypePicker.tsx +0 -1
  10. package/components/builder/ColorSwatchPicker.tsx +2 -2
  11. package/components/builder/CoverRowResizeHandle.tsx +180 -0
  12. package/components/builder/CoverSectionCanvas.tsx +260 -0
  13. package/components/builder/ReadOnlyFrame.tsx +127 -3
  14. package/components/builder/SectionTypePicker.tsx +29 -0
  15. package/components/builder/SectionV2Canvas.tsx +4 -1
  16. package/components/builder/SectionV2Column.tsx +15 -20
  17. package/components/builder/SettingsPanel.tsx +14 -0
  18. package/components/builder/SortableRow.tsx +7 -21
  19. package/components/builder/blockStyles.tsx +13 -14
  20. package/components/builder/editors/ImageBlockEditor.tsx +1 -0
  21. package/components/builder/editors/VideoBlockEditor.tsx +1 -0
  22. package/components/builder/editors/index.ts +0 -1
  23. package/components/builder/index.ts +1 -0
  24. package/components/builder/live-preview/LiveImagePreview.tsx +21 -2
  25. package/components/builder/live-preview/LiveVideoPreview.tsx +8 -3
  26. package/components/builder/live-preview/RichTextEditor.tsx +23 -2
  27. package/components/builder/live-preview/index.ts +0 -1
  28. package/components/builder/settings-panel/BlockSettings.tsx +0 -7
  29. package/components/builder/settings-panel/CoverSectionSettings.tsx +296 -0
  30. package/components/builder/settings-panel/index.ts +1 -0
  31. package/components/builder/settings-panel/useSettingsPanelSelection.ts +36 -2
  32. package/lib/animation/enter-types.ts +0 -1
  33. package/lib/animation/hover-effect-types.ts +0 -1
  34. package/lib/builder/defaults.ts +43 -22
  35. package/lib/builder/serializer/normalizers.ts +34 -1
  36. package/lib/builder/serializer/serializers.ts +39 -2
  37. package/lib/builder/store-blocks.ts +11 -3
  38. package/lib/builder/store-cover.ts +220 -0
  39. package/lib/builder/store-helpers.ts +81 -4
  40. package/lib/builder/store-sections.ts +12 -2
  41. package/lib/builder/store.ts +11 -2
  42. package/lib/builder/types.ts +15 -2
  43. package/lib/sanity/queries.ts +18 -4
  44. package/lib/sanity/types.ts +81 -45
  45. package/lib/version.ts +1 -1
  46. package/package.json +1 -1
  47. package/sanity/schemas/blocks/imageBlock.ts +1 -0
  48. package/sanity/schemas/blocks/index.ts +1 -2
  49. package/sanity/schemas/blocks/videoBlock.ts +1 -0
  50. package/sanity/schemas/index.ts +5 -3
  51. package/sanity/schemas/objects/coverSection.ts +317 -0
  52. package/sanity/schemas/objects/parallaxSlide.ts +0 -1
  53. package/sanity/schemas/page.ts +1 -1
  54. package/sanity/schemas/pageSectionV2.ts +0 -1
  55. package/components/blocks/CoverBlockRenderer.tsx +0 -261
  56. package/components/builder/editors/CoverBlockEditor.tsx +0 -550
  57. package/components/builder/live-preview/LiveCoverPreview.tsx +0 -146
  58. package/sanity/schemas/blocks/coverBlock.ts +0 -229
@@ -460,32 +460,27 @@ export default function SectionV2Column({
460
460
  {/* Blocks content */}
461
461
  <SortableContext items={blockIds} strategy={verticalListSortingStrategy}>
462
462
  {!hasBlocks ? (
463
- /* Empty column: show + Add Block */
463
+ /* Empty column: show + Add Block (flex-1 stretches in fillHeight cover sections) */
464
464
  <div
465
- className="relative flex items-center justify-center"
465
+ className="relative flex items-center justify-center flex-1"
466
466
  style={{ minHeight: 80, padding: "16px 12px" }}
467
467
  >
468
468
  <button
469
469
  onClick={handleAddBlockEmpty}
470
470
  aria-label="Add block to empty column"
471
- className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
471
+ className={`rounded-full text-[10px] font-medium transition-all hover:scale-105 ${
472
472
  showChrome
473
473
  ? "opacity-100"
474
474
  : showFaintOutline
475
475
  ? "opacity-40"
476
- : "bg-transparent text-transparent opacity-0 pointer-events-none"
476
+ : "opacity-0 pointer-events-none"
477
477
  }`}
478
478
  style={{
479
+ padding: "5px 16px",
479
480
  pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
480
- ...(showChrome ? {
481
- background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
482
- color: "rgba(100,220,170,0.8)",
483
- boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
484
- border: "1px solid rgba(255,255,255,0.06)",
485
- } : showFaintOutline ? {
486
- background: "rgba(38,38,48,0.3)",
487
- color: "rgba(100,220,170,0.4)",
488
- } : {}),
481
+ background: showChrome ? "rgba(13, 150, 104, 0.12)" : "rgba(13, 150, 104, 0.06)",
482
+ color: "#0d9668",
483
+ border: `1px dashed ${showChrome ? "rgba(13, 150, 104, 0.5)" : "rgba(13, 150, 104, 0.25)"}`,
489
484
  }}
490
485
  >
491
486
  + Add Block
@@ -500,21 +495,21 @@ export default function SectionV2Column({
500
495
  {/* Hidden for section-level blocks (e.g. projectGridBlock) that own the full column */}
501
496
  {hasBlocks && !singleSectionBlock && (
502
497
  <div
503
- className={`absolute left-0 right-0 z-[3] transition-all ${
498
+ className={`flex-1 min-h-0 flex items-center justify-center z-[3] transition-all ${
504
499
  showChrome ? "opacity-100" : showFaintOutline ? "opacity-30" : "opacity-0 pointer-events-none"
505
500
  }`}
506
- style={{ bottom: 0, transform: "translateY(100%)", padding: "4px 12px 0" }}
501
+ style={{ minHeight: 24 }}
507
502
  >
508
503
  <button
509
504
  onClick={handleAddBlockBelow}
510
505
  aria-label="Add block below existing blocks"
511
- className="w-full py-1.5 text-[11px] font-medium rounded transition-all"
506
+ className="rounded-full text-[10px] font-medium transition-all hover:scale-105"
512
507
  style={{
508
+ padding: "4px 14px",
513
509
  pointerEvents: showChrome ? "auto" : "none",
514
- background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
515
- color: "rgba(100,220,170,0.8)",
516
- boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
517
- border: "1px solid rgba(255,255,255,0.06)",
510
+ background: "rgba(13, 150, 104, 0.12)",
511
+ color: "#0d9668",
512
+ border: "1px dashed rgba(13, 150, 104, 0.5)",
518
513
  }}
519
514
  >
520
515
  + Add Block
@@ -37,6 +37,7 @@ import {
37
37
  ColumnV2Settings,
38
38
  ParallaxSlideSettings,
39
39
  ParallaxGroupSettings,
40
+ CoverSectionSettings,
40
41
  } from "./settings-panel";
41
42
 
42
43
  type SettingsTab = "settings" | "layout" | "seo" | "animation";
@@ -49,6 +50,7 @@ export default function SettingsPanel() {
49
50
  const {
50
51
  selectedSectionV2,
51
52
  selectedCustomSectionInstance,
53
+ selectedCoverSection,
52
54
  selectedParallaxGroup,
53
55
  selectedParallaxSlide,
54
56
  effectiveSectionV2,
@@ -59,6 +61,7 @@ export default function SettingsPanel() {
59
61
  HeaderIconComponent,
60
62
  isColumnOnly,
61
63
  isParallaxGroupOnly,
64
+ isCoverSectionOnly,
62
65
  isPageLevel,
63
66
  } = sel;
64
67
 
@@ -141,6 +144,9 @@ export default function SettingsPanel() {
141
144
  } else if (selectedParallaxGroup && !selectedParallaxSlide) {
142
145
  onDelete = () => store.deleteSection(selectedParallaxGroup._key);
143
146
  deleteTitle = "Delete Parallax Group";
147
+ } else if (selectedCoverSection) {
148
+ onDelete = () => store.deleteSection(selectedCoverSection._key);
149
+ deleteTitle = "Delete Cover Section";
144
150
  } else if (selectedSectionV2) {
145
151
  onDelete = () => store.deleteSection(selectedSectionV2._key);
146
152
  deleteTitle = "Delete Section";
@@ -327,6 +333,14 @@ export default function SettingsPanel() {
327
333
  <CustomSectionSettings instance={selectedCustomSectionInstance} />
328
334
  )
329
335
  ) :
336
+ /* ---- Cover Section routing ---- */
337
+ isCoverSectionOnly && selectedCoverSection ? (
338
+ activeTab === "animation" ? (
339
+ <SectionV2AnimationTab section={effectiveSectionV2!} />
340
+ ) : (
341
+ <CoverSectionSettings section={selectedCoverSection} />
342
+ )
343
+ ) :
330
344
  /* ---- V2 Section / Column / Block routing ---- */
331
345
  /* BUG-V2-003 fix: When a block inside a V2 column is selected, show BlockSettings
332
346
  instead of ColumnV2Settings. Block selection takes priority over column. */
@@ -60,6 +60,7 @@ interface SortableRowProps {
60
60
  onSelect: () => void;
61
61
  onDelete: () => void;
62
62
  onAddColumn: () => void;
63
+ addColumnLabel?: string;
63
64
  onDuplicate: () => void;
64
65
  onMoveUp: () => void;
65
66
  onMoveDown: () => void;
@@ -76,6 +77,7 @@ export default function SortableRow({
76
77
  onSelect,
77
78
  onDelete,
78
79
  onAddColumn,
80
+ addColumnLabel = "Col",
79
81
  onDuplicate,
80
82
  onMoveUp,
81
83
  onMoveDown,
@@ -87,7 +89,6 @@ export default function SortableRow({
87
89
  const selectBlock = useBuilderStore((s) => s.selectBlock);
88
90
  const canvasZoom = useBuilderStore((s) => s.canvasZoom);
89
91
  const activeViewport = useBuilderStore((s) => s.activeViewport);
90
- const gridSettings = useBuilderStore((s) => s.gridSettings);
91
92
  const customSectionCache = useBuilderStore((s) => s._customSectionCache);
92
93
  const [isHovered, setIsHovered] = useState(false);
93
94
  const {
@@ -176,24 +177,9 @@ export default function SortableRow({
176
177
  const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown> || {});
177
178
 
178
179
  const showToolbar = isSelected || isHovered;
179
- const coverRow = false;
180
180
 
181
181
  // ---- Preview Mode: clean rendering with row styles applied ----
182
182
  if (previewMode) {
183
- // Cover rows: full-width, no padding, no container (matches RowRenderer)
184
- if (coverRow) {
185
- return (
186
- <div
187
- ref={setNodeRef}
188
- style={{
189
- ...style,
190
- backgroundColor: bgColor !== "transparent" ? bgColor : undefined,
191
- }}
192
- >
193
- {children}
194
- </div>
195
- );
196
- }
197
183
  // Build merged styles for preview mode
198
184
  const previewRowStyle: React.CSSProperties = {
199
185
  ...style,
@@ -219,7 +205,7 @@ export default function SortableRow({
219
205
  const designRowStyle: React.CSSProperties = {
220
206
  ...style,
221
207
  ...layoutStyles,
222
- minHeight: coverRow ? undefined : minHeight,
208
+ minHeight,
223
209
  };
224
210
  // Legacy fallback: if no layout background set, use old bgColor
225
211
  if (!layoutStyles.backgroundColor && !layoutStyles.backgroundImage) {
@@ -332,10 +318,10 @@ export default function SortableRow({
332
318
  onClick={(e) => { e.stopPropagation(); onAddColumn(); }}
333
319
  onPointerDown={(e) => e.stopPropagation()}
334
320
  className="flex items-center gap-1 text-[11px] text-white/50 hover:text-white/85 transition-colors py-0.5"
335
- title="Add column"
336
- aria-label="Add column"
321
+ title={`Add ${addColumnLabel.toLowerCase()}`}
322
+ aria-label={`Add ${addColumnLabel.toLowerCase()}`}
337
323
  >
338
- <span className="text-white/30">+</span> Col
324
+ <span className="text-white/30">+</span> {addColumnLabel}
339
325
  </button>
340
326
  )}
341
327
 
@@ -364,7 +350,7 @@ export default function SortableRow({
364
350
  )}
365
351
 
366
352
  {/* Content — same layout as Preview */}
367
- <div style={coverRow ? undefined : { maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative">
353
+ <div style={{ maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative">
368
354
  {children}
369
355
  </div>
370
356
  </div>
@@ -14,8 +14,8 @@ export const BLOCK_GRADIENTS: Record<string, string> = {
14
14
  videoBlock: "linear-gradient(135deg, #ffb8d4 0%, #ffc8a8 50%, #ffe0b8 100%)",
15
15
  spacerBlock: "linear-gradient(135deg, #d8d8e8 0%, #e8e8f0 50%, #f0f0f8 100%)",
16
16
  buttonBlock: "linear-gradient(135deg, #ffb8e0 0%, #b8ffe8 50%, #a8ffd8 100%)",
17
- coverBlock: "linear-gradient(135deg, #ffd0a8 0%, #ffc090 50%, #ffb080 100%)",
18
17
  projectGridBlock: "linear-gradient(135deg, #ffd4a8 0%, #ffe8b8 50%, #fff0c8 100%)",
18
+ coverSection: "linear-gradient(135deg, #b2f5ea 0%, #81e6d9 50%, #5eead4 100%)",
19
19
  parallaxGroup: "linear-gradient(135deg, #c8a8ff 0%, #d8b8ff 50%, #e8d0ff 100%)",
20
20
  customSectionInstance: "linear-gradient(135deg, #d0b8ff 0%, #b8a8f8 50%, #c8b8ff 100%)",
21
21
  // Non-block contexts
@@ -149,27 +149,26 @@ export function ButtonBlockIcon({ size = 28 }: { size?: number }) {
149
149
  );
150
150
  }
151
151
 
152
- export function CoverBlockIcon({ size = 28 }: { size?: number }) {
152
+ // ── Non-block context icons ──
153
+
154
+ export function CoverSectionSettingsIcon({ size = 28 }: { size?: number }) {
153
155
  return (
154
156
  <svg width={size} height={size} viewBox="0 0 40 40" fill="none">
155
157
  <defs>
156
- <linearGradient id="starGrad" x1="10" y1="4" x2="30" y2="36">
157
- <stop offset="0%" stopColor="#f0a040" />
158
- <stop offset="100%" stopColor="#e07820" />
158
+ <linearGradient id="csSettingsGrad" x1="5" y1="5" x2="35" y2="35">
159
+ <stop offset="0%" stopColor="#0d9488" />
160
+ <stop offset="100%" stopColor="#0f766e" />
159
161
  </linearGradient>
160
- <filter id="starDrop">
161
- <feDropShadow dx="0" dy="1.5" stdDeviation="2" floodColor="rgba(200,100,20,0.3)" />
162
- </filter>
163
162
  </defs>
164
- <path d="M20,2 L24,15 L37,20 L24,25 L20,38 L16,25 L3,20 L16,15 Z" fill="url(#starGrad)" filter="url(#starDrop)" />
165
- <path d="M20,2 L24,15 L37,20 L16,15 Z" fill="white" opacity="0.2" />
166
- <circle cx="20" cy="20" r="2.5" fill="white" opacity="0.4" />
163
+ <rect x="3" y="3" width="34" height="34" rx="6" fill="url(#csSettingsGrad)" opacity="0.12" />
164
+ <rect x="3" y="3" width="34" height="34" rx="6" stroke="url(#csSettingsGrad)" strokeWidth="1.5" fill="none" opacity="0.4" />
165
+ <rect x="7" y="7" width="26" height="16" rx="2" fill="url(#csSettingsGrad)" opacity="0.2" />
166
+ <rect x="7" y="25" width="26" height="8" rx="2" fill="url(#csSettingsGrad)" opacity="0.35" />
167
+ <line x1="9" y1="24" x2="31" y2="24" stroke="#0d9488" strokeWidth="1" opacity="0.4" strokeDasharray="2 2" />
167
168
  </svg>
168
169
  );
169
170
  }
170
171
 
171
- // ── Non-block context icons ──
172
-
173
172
  export function RowIcon({ size = 28 }: { size?: number }) {
174
173
  return (
175
174
  <svg width={size} height={size} viewBox="0 0 40 40" fill="none">
@@ -285,9 +284,9 @@ export const BLOCK_ICON_COMPONENTS: Record<string, React.FC<{ size?: number }>>
285
284
  videoBlock: VideoBlockIcon,
286
285
  spacerBlock: SpacerBlockIcon,
287
286
  buttonBlock: ButtonBlockIcon,
288
- coverBlock: CoverBlockIcon,
289
287
  projectGridBlock: ProjectGridBlockIcon,
290
288
  parallaxGroup: ParallaxGroupIcon,
289
+ coverSection: CoverSectionSettingsIcon,
291
290
  customSectionInstance: CustomSectionInstanceIcon,
292
291
  row: RowIcon,
293
292
  column: ColumnIcon,
@@ -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
@@ -192,6 +192,7 @@ export default function VideoBlockEditor({ block }: Props) {
192
192
  [
193
193
  { value: "full", label: "Full" },
194
194
  { value: "contained", label: "Contained" },
195
+ { value: "fill", label: "Fill" },
195
196
  ] as const
196
197
  ).map((opt) => (
197
198
  <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 widthStyle =
25
- block.width === "contained"
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={{ width: widthStyle, margin: block.width === "contained" ? "0 auto" : undefined, minWidth: 0, borderRadius, overflow: borderRadius ? "hidden" : undefined }}>
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