@morphika/andami 0.2.4 → 0.2.6
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/components/blocks/ProjectGridBlockRenderer.tsx +50 -29
- package/components/blocks/TextBlockRenderer.tsx +27 -2
- package/components/blocks/TypewriterRichText.tsx +7 -1
- package/components/builder/CanvasToolbar.tsx +11 -17
- package/components/builder/SectionV2Canvas.tsx +3 -2
- package/components/builder/SectionV2Column.tsx +7 -3
- package/components/builder/SortableBlock.tsx +7 -6
- package/components/builder/SortableRow.tsx +36 -11
- package/components/builder/editors/TextBlockEditor.tsx +2 -6
- package/components/builder/live-preview/LiveTextEditor.tsx +117 -198
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +451 -0
- package/components/builder/live-preview/RichTextEditor.tsx +223 -0
- package/components/builder/live-preview/index.ts +2 -0
- package/components/builder/settings-panel/SectionV2Settings.tsx +4 -0
- package/lib/builder/types.ts +10 -0
- package/lib/editor/index.ts +2 -0
- package/lib/editor/portableToTiptap.ts +156 -0
- package/lib/editor/tiptapToPortable.ts +238 -0
- package/package.json +223 -212
- package/sanity/schemas/blocks/textBlock.ts +13 -0
|
@@ -165,6 +165,32 @@ export default function ProjectGridBlockRenderer({
|
|
|
165
165
|
const borderRadius = block.border_radius || 0;
|
|
166
166
|
const videoMode = block.video_mode || "off";
|
|
167
167
|
|
|
168
|
+
// ─── Allow scale hover to overflow the V2 column ───
|
|
169
|
+
// V2 columns have overflow:hidden (CSS Grid img protection). The project
|
|
170
|
+
// grid uses absolute positioning so it doesn't need that protection.
|
|
171
|
+
// When scale hover is active, we relax the parent column's overflow so
|
|
172
|
+
// the scaled card isn't clipped at the column boundary.
|
|
173
|
+
const hasProjects = resolvedProjects.length > 0;
|
|
174
|
+
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (hoverEffect !== "scale" && hoverEffect !== "3d") return;
|
|
177
|
+
const el = containerRef.current;
|
|
178
|
+
if (!el) return;
|
|
179
|
+
|
|
180
|
+
// Walk up to find the V2 column (class starts with "sv2-col-")
|
|
181
|
+
let col: HTMLElement | null = el.parentElement;
|
|
182
|
+
while (col && !col.className?.includes("sv2-col-")) {
|
|
183
|
+
col = col.parentElement;
|
|
184
|
+
}
|
|
185
|
+
if (!col) return;
|
|
186
|
+
|
|
187
|
+
const prev = col.style.overflow;
|
|
188
|
+
col.style.overflow = "visible";
|
|
189
|
+
return () => {
|
|
190
|
+
col.style.overflow = prev;
|
|
191
|
+
};
|
|
192
|
+
}, [hoverEffect, hasProjects]);
|
|
193
|
+
|
|
168
194
|
// ─── Build masonry items (viewport-aware per-card overrides) ───
|
|
169
195
|
const masonryItems: MasonryItem[] = useMemo(() => {
|
|
170
196
|
return resolvedProjects.map((proj, i) => {
|
|
@@ -226,11 +252,7 @@ export default function ProjectGridBlockRenderer({
|
|
|
226
252
|
// animations at once (with stagger). No per-card observer needed.
|
|
227
253
|
const [gridVisible, setGridVisible] = useState(false);
|
|
228
254
|
|
|
229
|
-
//
|
|
230
|
-
// IntersectionObserver effect re-runs after the async fetch populates
|
|
231
|
-
// resolvedProjects (on the first render the grid div isn't mounted yet
|
|
232
|
-
// because the component returns null when resolvedProjects is empty).
|
|
233
|
-
const hasProjects = resolvedProjects.length > 0;
|
|
255
|
+
// hasProjects is declared above (used by overflow + entrance effects)
|
|
234
256
|
|
|
235
257
|
useEffect(() => {
|
|
236
258
|
if (!entranceEnabled || gridVisible || !hasProjects) return;
|
|
@@ -342,10 +364,6 @@ export default function ProjectGridBlockRenderer({
|
|
|
342
364
|
top: item.y,
|
|
343
365
|
width: item.width,
|
|
344
366
|
height: item.height,
|
|
345
|
-
// Clip the card when hover scale-up overflows the cell bounds
|
|
346
|
-
// so border-radius is preserved visually.
|
|
347
|
-
overflow: "hidden",
|
|
348
|
-
borderRadius: borderRadius > 0 ? borderRadius : undefined,
|
|
349
367
|
}}
|
|
350
368
|
>
|
|
351
369
|
{entranceEnabled && entranceAnimConfig ? (
|
|
@@ -444,18 +462,32 @@ const ProjectCard = memo(function ProjectCard({
|
|
|
444
462
|
const handlePlay = useCallback(() => setIsPlaying(true), []);
|
|
445
463
|
const handlePause = useCallback(() => setIsPlaying(false), []);
|
|
446
464
|
|
|
447
|
-
// Scale
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
465
|
+
// Scale transform — applied to the Link wrapper so the entire card
|
|
466
|
+
// (including its border-radius clipping shape) scales uniformly.
|
|
467
|
+
// The inner div keeps overflow:hidden + borderRadius for content clipping.
|
|
468
|
+
// Applying scale on the same element as overflow:hidden + borderRadius
|
|
469
|
+
// causes GPU compositing artifacts where the border-radius clips at
|
|
470
|
+
// pre-transform bounds, making rounded corners disappear during scale.
|
|
471
|
+
const scaleTransform =
|
|
472
|
+
hoverEffect === "scale" && hovered ? "scale(1.03)" : "scale(1)";
|
|
453
473
|
|
|
454
474
|
return (
|
|
455
475
|
<Link
|
|
456
476
|
href={`/work/${project.slug}`}
|
|
457
477
|
className="block"
|
|
458
|
-
style={{
|
|
478
|
+
style={{
|
|
479
|
+
display: "block",
|
|
480
|
+
width: "100%",
|
|
481
|
+
height: "100%",
|
|
482
|
+
// Lift hovered card above neighbours so scale doesn't clip behind them
|
|
483
|
+
position: "relative",
|
|
484
|
+
zIndex: hovered && hoverEffect !== "none" ? 2 : undefined,
|
|
485
|
+
// Scale applied here (outside overflow:hidden + borderRadius) so the
|
|
486
|
+
// entire clipping shape scales with the content.
|
|
487
|
+
transition:
|
|
488
|
+
hoverEffect === "scale" ? "transform 300ms ease" : undefined,
|
|
489
|
+
transform: hoverEffect === "scale" ? scaleTransform : undefined,
|
|
490
|
+
}}
|
|
459
491
|
onMouseEnter={handleMouseEnter}
|
|
460
492
|
onMouseLeave={handleMouseLeave}
|
|
461
493
|
onMouseMove={hoverEffect === "3d" ? handleMouseMove : undefined}
|
|
@@ -468,22 +500,13 @@ const ProjectCard = memo(function ProjectCard({
|
|
|
468
500
|
height: "100%",
|
|
469
501
|
overflow: "hidden",
|
|
470
502
|
borderRadius: radius,
|
|
471
|
-
// 3D tilt
|
|
503
|
+
// 3D tilt still uses cardRef for per-pixel mouse tracking
|
|
472
504
|
transition:
|
|
473
505
|
hoverEffect === "3d"
|
|
474
506
|
? "transform 100ms ease-out"
|
|
475
507
|
: undefined,
|
|
476
508
|
}}
|
|
477
509
|
>
|
|
478
|
-
{/* Inner content wrapper — receives scale hover transform */}
|
|
479
|
-
<div
|
|
480
|
-
style={{
|
|
481
|
-
position: "absolute",
|
|
482
|
-
inset: 0,
|
|
483
|
-
transition: hoverEffect === "scale" ? "transform 300ms ease" : undefined,
|
|
484
|
-
transform: hoverEffect === "scale" ? contentScale : undefined,
|
|
485
|
-
}}
|
|
486
|
-
>
|
|
487
510
|
{/* Thumbnail image */}
|
|
488
511
|
{imgSrc ? (
|
|
489
512
|
<img
|
|
@@ -548,9 +571,7 @@ const ProjectCard = memo(function ProjectCard({
|
|
|
548
571
|
/>
|
|
549
572
|
)}
|
|
550
573
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
{/* Subtitle overlay — outside the scale wrapper so text stays crisp */}
|
|
574
|
+
{/* Subtitle overlay */}
|
|
554
575
|
{showSubtitle && (
|
|
555
576
|
<div
|
|
556
577
|
style={{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TextBlock } from "../../lib/sanity/types";
|
|
2
|
-
import { PortableText } from "next-sanity";
|
|
2
|
+
import { PortableText, type PortableTextComponents } from "next-sanity";
|
|
3
3
|
|
|
4
4
|
/** Resolve fontSize: supports numeric px and legacy string enum */
|
|
5
5
|
function resolvePublicFontSize(fontSize?: number | string): string | undefined {
|
|
@@ -79,6 +79,31 @@ export function getTextBlockStyles(block: TextBlock): { className: string; style
|
|
|
79
79
|
return { className: classes, style: inlineStyle };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Custom Portable Text components for rich text rendering. */
|
|
83
|
+
const richTextComponents: PortableTextComponents = {
|
|
84
|
+
marks: {
|
|
85
|
+
link: ({ value, children }) => {
|
|
86
|
+
const href = value?.href || "#";
|
|
87
|
+
const blank = value?.blank;
|
|
88
|
+
return (
|
|
89
|
+
<a
|
|
90
|
+
href={href}
|
|
91
|
+
target={blank ? "_blank" : undefined}
|
|
92
|
+
rel={blank ? "noopener noreferrer" : undefined}
|
|
93
|
+
className="underline decoration-current underline-offset-2 hover:opacity-75 transition-opacity"
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</a>
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
color: ({ value, children }) => {
|
|
100
|
+
const hex = value?.hex;
|
|
101
|
+
if (!hex) return <>{children}</>;
|
|
102
|
+
return <span style={{ color: hex }}>{children}</span>;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
82
107
|
export default function TextBlockRenderer({ block }: { block: TextBlock }) {
|
|
83
108
|
if (!block.text?.length) return null;
|
|
84
109
|
|
|
@@ -89,7 +114,7 @@ export default function TextBlockRenderer({ block }: { block: TextBlock }) {
|
|
|
89
114
|
className={`${className} space-y-[0.75em]`}
|
|
90
115
|
style={style}
|
|
91
116
|
>
|
|
92
|
-
<PortableText value={block.text} />
|
|
117
|
+
<PortableText value={block.text} components={richTextComponents} />
|
|
93
118
|
</div>
|
|
94
119
|
);
|
|
95
120
|
}
|
|
@@ -80,7 +80,7 @@ function wrapInMarks(
|
|
|
80
80
|
result = <code>{result}</code>;
|
|
81
81
|
break;
|
|
82
82
|
default: {
|
|
83
|
-
// Check if it's a
|
|
83
|
+
// Check if it's a markDef reference (link or color annotation)
|
|
84
84
|
const def = getMarkDef(block, mark);
|
|
85
85
|
if (def && def._type === "link" && def.href) {
|
|
86
86
|
result = (
|
|
@@ -92,6 +92,12 @@ function wrapInMarks(
|
|
|
92
92
|
{result}
|
|
93
93
|
</a>
|
|
94
94
|
);
|
|
95
|
+
} else if (def && def._type === "color" && def.hex) {
|
|
96
|
+
result = (
|
|
97
|
+
<span style={{ color: def.hex as string }}>
|
|
98
|
+
{result}
|
|
99
|
+
</span>
|
|
100
|
+
);
|
|
95
101
|
}
|
|
96
102
|
break;
|
|
97
103
|
}
|
|
@@ -75,10 +75,10 @@ export default function CanvasToolbar({ viewportRef, onAnimatedAction }: CanvasT
|
|
|
75
75
|
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-px rounded-full bg-[#1a1a1a] shadow-xl px-1 py-1"
|
|
76
76
|
style={{ userSelect: "none" }}
|
|
77
77
|
>
|
|
78
|
-
{/* Select tool */}
|
|
78
|
+
{/* Select tool — Figma-style filled cursor */}
|
|
79
79
|
<button
|
|
80
80
|
onClick={() => setCanvasTool("select")}
|
|
81
|
-
className={`flex items-center justify-center w-8 h-8 rounded-full
|
|
81
|
+
className={`flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
|
|
82
82
|
tool === "select"
|
|
83
83
|
? "bg-white/15 text-white"
|
|
84
84
|
: "text-neutral-400 hover:text-white hover:bg-white/10"
|
|
@@ -86,20 +86,15 @@ export default function CanvasToolbar({ viewportRef, onAnimatedAction }: CanvasT
|
|
|
86
86
|
title="Select tool (V)"
|
|
87
87
|
aria-label="Select tool"
|
|
88
88
|
>
|
|
89
|
-
<svg width="
|
|
90
|
-
<path
|
|
91
|
-
d="M2 1L12 7L7 8L5 13L2 1Z"
|
|
92
|
-
stroke="currentColor"
|
|
93
|
-
strokeWidth="1.5"
|
|
94
|
-
strokeLinejoin="round"
|
|
95
|
-
/>
|
|
89
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round">
|
|
90
|
+
<path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z" />
|
|
96
91
|
</svg>
|
|
97
92
|
</button>
|
|
98
93
|
|
|
99
|
-
{/* Hand tool */}
|
|
94
|
+
{/* Hand tool — Figma-style */}
|
|
100
95
|
<button
|
|
101
96
|
onClick={() => setCanvasTool("hand")}
|
|
102
|
-
className={`flex items-center justify-center w-8 h-8 rounded-full
|
|
97
|
+
className={`flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
|
|
103
98
|
tool === "hand"
|
|
104
99
|
? "bg-white/15 text-white"
|
|
105
100
|
: "text-neutral-400 hover:text-white hover:bg-white/10"
|
|
@@ -107,12 +102,11 @@ export default function CanvasToolbar({ viewportRef, onAnimatedAction }: CanvasT
|
|
|
107
102
|
title="Hand tool (H)"
|
|
108
103
|
aria-label="Hand tool"
|
|
109
104
|
>
|
|
110
|
-
<svg width="
|
|
111
|
-
|
|
112
|
-
<path d="
|
|
113
|
-
<path d="
|
|
114
|
-
<path d="
|
|
115
|
-
<path d="M9 11V9a1.5 1.5 0 0 0-3 0v3c0 4.42 2.69 8 6 8h1c3.31 0 6-3.58 6-8V11" />
|
|
105
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
106
|
+
<path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" />
|
|
107
|
+
<path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" />
|
|
108
|
+
<path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" />
|
|
109
|
+
<path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" />
|
|
116
110
|
</svg>
|
|
117
111
|
</button>
|
|
118
112
|
|
|
@@ -10,6 +10,7 @@ import SortableBlock from "./SortableBlock";
|
|
|
10
10
|
import { useColumnDragContext } from "./ColumnDragContext";
|
|
11
11
|
import { useColumnResize } from "./hooks/useColumnResize";
|
|
12
12
|
import { InsertionLines } from "./InsertionLines";
|
|
13
|
+
import { isSectionBlockSection } from "../../lib/builder/types";
|
|
13
14
|
|
|
14
15
|
// ============================================
|
|
15
16
|
// SectionV2Canvas — Renders a V2 section in the builder
|
|
@@ -248,8 +249,8 @@ export default function SectionV2Canvas({
|
|
|
248
249
|
);
|
|
249
250
|
})}
|
|
250
251
|
|
|
251
|
-
{/* Gap buttons: "+ Add Column" —
|
|
252
|
-
{!previewMode &&
|
|
252
|
+
{/* Gap buttons: "+ Add Column" — hidden for section-level blocks (locked layout) */}
|
|
253
|
+
{!previewMode && !isSectionBlockSection(section) &&
|
|
253
254
|
gaps.map((gap) => {
|
|
254
255
|
const isGapTarget =
|
|
255
256
|
dropTarget?.type === "gap" &&
|
|
@@ -215,9 +215,12 @@ export default function SectionV2Column({
|
|
|
215
215
|
}, [onAddBlock, column.blocks]);
|
|
216
216
|
|
|
217
217
|
const hasBlocks = (column.blocks || []).length > 0;
|
|
218
|
-
|
|
218
|
+
// Section-level blocks (e.g. projectGridBlock) own the full section —
|
|
219
|
+
// hide all column management chrome (resize, delete, drag, span badge).
|
|
220
|
+
const isLockedColumn = !!singleSectionBlock;
|
|
221
|
+
const showChrome = (isSelected || isHovered) && !isDraggedColumn && !isLockedColumn;
|
|
219
222
|
// Show faint outlines when section is hovered but not this specific column
|
|
220
|
-
const showFaintOutline = isSectionHovered && !isHovered && !isSelected && !isDraggedColumn;
|
|
223
|
+
const showFaintOutline = isSectionHovered && !isHovered && !isSelected && !isDraggedColumn && !isLockedColumn;
|
|
221
224
|
|
|
222
225
|
// Column-level vertical alignment from blocks' align_v settings
|
|
223
226
|
const colJustify = getColumnVerticalAlign(column.blocks || []);
|
|
@@ -494,7 +497,8 @@ export default function SectionV2Column({
|
|
|
494
497
|
</SortableContext>
|
|
495
498
|
|
|
496
499
|
{/* "+" add block button below blocks — absolutely positioned to avoid disrupting flex alignment */}
|
|
497
|
-
{
|
|
500
|
+
{/* Hidden for section-level blocks (e.g. projectGridBlock) that own the full column */}
|
|
501
|
+
{hasBlocks && !singleSectionBlock && (
|
|
498
502
|
<div
|
|
499
503
|
className={`absolute left-0 right-0 z-[3] transition-all ${
|
|
500
504
|
showChrome ? "opacity-100" : showFaintOutline ? "opacity-30" : "opacity-0 pointer-events-none"
|
|
@@ -122,16 +122,17 @@ export default function SortableBlock({
|
|
|
122
122
|
onMouseEnter={() => setIsHovered(true)}
|
|
123
123
|
onMouseLeave={() => setIsHovered(false)}
|
|
124
124
|
>
|
|
125
|
-
{/* Hover/selection outline overlay — scales with zoom */}
|
|
125
|
+
{/* Hover/selection outline overlay — scales with zoom, offset inward so it doesn't overlap column outlines */}
|
|
126
126
|
<div
|
|
127
|
-
className="pointer-events-none absolute
|
|
128
|
-
style={
|
|
129
|
-
|
|
127
|
+
className="pointer-events-none absolute z-[3] rounded transition-[box-shadow]"
|
|
128
|
+
style={{
|
|
129
|
+
inset: `${Math.max(2, Math.min(5, 3 / canvasZoom))}px`,
|
|
130
|
+
...(isSelected
|
|
130
131
|
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px ${BUILDER_ORANGE}` }
|
|
131
132
|
: isHovered
|
|
132
133
|
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(13, 150, 104, 0.4)` }
|
|
133
|
-
:
|
|
134
|
-
}
|
|
134
|
+
: {}),
|
|
135
|
+
}}
|
|
135
136
|
/>
|
|
136
137
|
|
|
137
138
|
{/* Floating toolbar — centered INSIDE top of block, appears on hover or when selected.
|
|
@@ -6,7 +6,7 @@ import { CSS } from "@dnd-kit/utilities";
|
|
|
6
6
|
import { makeRowId } from "./DndWrapper";
|
|
7
7
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
8
8
|
import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
|
|
9
|
-
import { DEVICE_HEIGHTS } from "../../lib/builder/types";
|
|
9
|
+
import { DEVICE_HEIGHTS, isSectionBlockSection } from "../../lib/builder/types";
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
11
|
import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
|
|
12
12
|
import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
|
|
@@ -41,6 +41,12 @@ function getSectionLabel(item: ContentItem): string | null {
|
|
|
41
41
|
return "Parallax Showcase";
|
|
42
42
|
}
|
|
43
43
|
if (isPageSectionV2(item)) {
|
|
44
|
+
const section = item as PageSectionV2;
|
|
45
|
+
if (isSectionBlockSection(section)) {
|
|
46
|
+
// Show the block type name instead of generic "Section"
|
|
47
|
+
const blockType = section.columns?.[0]?.blocks?.[0]?._type;
|
|
48
|
+
if (blockType === "projectGridBlock") return "Project Grid";
|
|
49
|
+
}
|
|
44
50
|
return "Section";
|
|
45
51
|
}
|
|
46
52
|
return null;
|
|
@@ -78,6 +84,7 @@ export default function SortableRow({
|
|
|
78
84
|
children,
|
|
79
85
|
}: SortableRowProps) {
|
|
80
86
|
const previewMode = useBuilderStore((s) => s.previewMode);
|
|
87
|
+
const selectBlock = useBuilderStore((s) => s.selectBlock);
|
|
81
88
|
const canvasZoom = useBuilderStore((s) => s.canvasZoom);
|
|
82
89
|
const activeViewport = useBuilderStore((s) => s.activeViewport);
|
|
83
90
|
const gridSettings = useBuilderStore((s) => s.gridSettings);
|
|
@@ -102,6 +109,9 @@ export default function SortableRow({
|
|
|
102
109
|
// Determine if this is a PageSectionV2
|
|
103
110
|
const isV2Section = isPageSectionV2(row);
|
|
104
111
|
const sectionLabel = getSectionLabel(row);
|
|
112
|
+
// Section-level blocks (e.g. projectGridBlock) own the full section —
|
|
113
|
+
// no column management UI, selecting section auto-selects the block.
|
|
114
|
+
const isLockedSection = isV2Section && isSectionBlockSection(row as PageSectionV2);
|
|
105
115
|
|
|
106
116
|
// For sections: use section settings — viewport-aware for both V1 and V2 sections
|
|
107
117
|
const resolvedSettings = useMemo(() => {
|
|
@@ -223,20 +233,28 @@ export default function SortableRow({
|
|
|
223
233
|
className={`relative transition-[opacity,box-shadow] ${
|
|
224
234
|
isDragging ? "ring-2 ring-[#93278f] ring-offset-2 ring-offset-[#0a0a0a]" : ""
|
|
225
235
|
}`}
|
|
226
|
-
onClick={(e) => {
|
|
236
|
+
onClick={(e) => {
|
|
237
|
+
e.stopPropagation();
|
|
238
|
+
if (isLockedSection) {
|
|
239
|
+
const blockKey = (row as PageSectionV2).columns?.[0]?.blocks?.[0]?._key;
|
|
240
|
+
if (blockKey) { selectBlock(blockKey); return; }
|
|
241
|
+
}
|
|
242
|
+
onSelect();
|
|
243
|
+
}}
|
|
227
244
|
onMouseEnter={() => setIsHovered(true)}
|
|
228
245
|
onMouseLeave={() => setIsHovered(false)}
|
|
229
246
|
>
|
|
230
|
-
{/* Selection/hover outline — scales with zoom */}
|
|
247
|
+
{/* Selection/hover outline — scales with zoom, offset outward so it doesn't overlap column/block outlines */}
|
|
231
248
|
<div
|
|
232
|
-
className="pointer-events-none absolute
|
|
233
|
-
style={
|
|
234
|
-
|
|
249
|
+
className="pointer-events-none absolute z-[1] transition-[box-shadow]"
|
|
250
|
+
style={{
|
|
251
|
+
inset: `${-Math.max(2, Math.min(5, 3 / canvasZoom))}px`,
|
|
252
|
+
...(isSelected
|
|
235
253
|
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px #93278f` }
|
|
236
254
|
: isHovered
|
|
237
255
|
? { boxShadow: `inset 0 0 0 ${Math.max(2, Math.min(5, 3 / canvasZoom))}px rgba(147, 39, 143, 0.4)` }
|
|
238
|
-
:
|
|
239
|
-
}
|
|
256
|
+
: {}),
|
|
257
|
+
}}
|
|
240
258
|
/>
|
|
241
259
|
|
|
242
260
|
{/* Section toolbar — wide pill aligned top-left outside the row */}
|
|
@@ -249,7 +267,14 @@ export default function SortableRow({
|
|
|
249
267
|
transformOrigin: "top right",
|
|
250
268
|
width: "90px",
|
|
251
269
|
}}
|
|
252
|
-
onClick={(e) => {
|
|
270
|
+
onClick={(e) => {
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
if (isLockedSection) {
|
|
273
|
+
const blockKey = (row as PageSectionV2).columns?.[0]?.blocks?.[0]?._key;
|
|
274
|
+
if (blockKey) { selectBlock(blockKey); return; }
|
|
275
|
+
}
|
|
276
|
+
onSelect();
|
|
277
|
+
}}
|
|
253
278
|
>
|
|
254
279
|
{/* Main toolbar — drag + actions */}
|
|
255
280
|
<div
|
|
@@ -301,8 +326,8 @@ export default function SortableRow({
|
|
|
301
326
|
</button>
|
|
302
327
|
</div>
|
|
303
328
|
|
|
304
|
-
{/* Add column — shown for V2 sections and regular rows, hidden for closed sections
|
|
305
|
-
{(!sectionLabel || isV2Section) && (
|
|
329
|
+
{/* Add column — shown for V2 sections and regular rows, hidden for section blocks and closed sections */}
|
|
330
|
+
{(!sectionLabel || isV2Section) && !isLockedSection && (
|
|
306
331
|
<button
|
|
307
332
|
onClick={(e) => { e.stopPropagation(); onAddColumn(); }}
|
|
308
333
|
onPointerDown={(e) => e.stopPropagation()}
|
|
@@ -6,7 +6,6 @@ import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/r
|
|
|
6
6
|
import type { TextBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
7
7
|
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
8
8
|
import {
|
|
9
|
-
TextIcon,
|
|
10
9
|
TypographyIcon,
|
|
11
10
|
ColumnsIcon,
|
|
12
11
|
} from "./section-icons";
|
|
@@ -271,8 +270,8 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
|
271
270
|
<>
|
|
272
271
|
<ViewportBadge />
|
|
273
272
|
|
|
274
|
-
{/*
|
|
275
|
-
<SettingsSection title="
|
|
273
|
+
{/* Typography section: Style, Color, Align, Size, Weight, Line height, Letter spacing, Transform */}
|
|
274
|
+
<SettingsSection title="Typography" defaultOpen icon={<TypographyIcon />}>
|
|
276
275
|
<SettingsField label="Style">
|
|
277
276
|
<TextStylePicker
|
|
278
277
|
presets={presets}
|
|
@@ -309,10 +308,7 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
|
309
308
|
))}
|
|
310
309
|
</div>
|
|
311
310
|
</ResponsiveStyleField>
|
|
312
|
-
</SettingsSection>
|
|
313
311
|
|
|
314
|
-
{/* Typography section: Size, Weight, Line height, Letter spacing */}
|
|
315
|
-
<SettingsSection title="Typography" defaultOpen icon={<TypographyIcon />}>
|
|
316
312
|
<ResponsiveStyleField label="Size" subProp="fontSize" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("fontSize")} onReset={resetStyleOverride}>
|
|
317
313
|
<div className="flex items-center gap-0 bg-[#f5f5f5] rounded-lg overflow-hidden transition-all border border-transparent focus-within:bg-white focus-within:border-[#076bff] focus-within:shadow-[0_0_0_3px_rgba(7,107,255,0.06)]">
|
|
318
314
|
<input
|