@morphika/andami 0.2.3 → 0.2.5
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 +55 -16
- 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/settings-panel/SectionV2Settings.tsx +4 -0
- package/lib/builder/types.ts +10 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -250,7 +272,13 @@ export default function ProjectGridBlockRenderer({
|
|
|
250
272
|
observer.disconnect();
|
|
251
273
|
}
|
|
252
274
|
},
|
|
253
|
-
{
|
|
275
|
+
{
|
|
276
|
+
threshold: 0.01,
|
|
277
|
+
// Trigger 200px before the grid scrolls into view so cards
|
|
278
|
+
// start animating just before the user sees them (especially
|
|
279
|
+
// on mobile where the grid may sit just below the fold).
|
|
280
|
+
rootMargin: "200px 0px",
|
|
281
|
+
},
|
|
254
282
|
);
|
|
255
283
|
|
|
256
284
|
observer.observe(el);
|
|
@@ -336,10 +364,6 @@ export default function ProjectGridBlockRenderer({
|
|
|
336
364
|
top: item.y,
|
|
337
365
|
width: item.width,
|
|
338
366
|
height: item.height,
|
|
339
|
-
// Clip the card when hover scale-up overflows the cell bounds
|
|
340
|
-
// so border-radius is preserved visually.
|
|
341
|
-
overflow: "hidden",
|
|
342
|
-
borderRadius: borderRadius > 0 ? borderRadius : undefined,
|
|
343
367
|
}}
|
|
344
368
|
>
|
|
345
369
|
{entranceEnabled && entranceAnimConfig ? (
|
|
@@ -438,7 +462,12 @@ const ProjectCard = memo(function ProjectCard({
|
|
|
438
462
|
const handlePlay = useCallback(() => setIsPlaying(true), []);
|
|
439
463
|
const handlePause = useCallback(() => setIsPlaying(false), []);
|
|
440
464
|
|
|
441
|
-
// Scale transform
|
|
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.
|
|
442
471
|
const scaleTransform =
|
|
443
472
|
hoverEffect === "scale" && hovered ? "scale(1.03)" : "scale(1)";
|
|
444
473
|
|
|
@@ -446,7 +475,19 @@ const ProjectCard = memo(function ProjectCard({
|
|
|
446
475
|
<Link
|
|
447
476
|
href={`/work/${project.slug}`}
|
|
448
477
|
className="block"
|
|
449
|
-
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
|
+
}}
|
|
450
491
|
onMouseEnter={handleMouseEnter}
|
|
451
492
|
onMouseLeave={handleMouseLeave}
|
|
452
493
|
onMouseMove={hoverEffect === "3d" ? handleMouseMove : undefined}
|
|
@@ -459,13 +500,11 @@ const ProjectCard = memo(function ProjectCard({
|
|
|
459
500
|
height: "100%",
|
|
460
501
|
overflow: "hidden",
|
|
461
502
|
borderRadius: radius,
|
|
503
|
+
// 3D tilt still uses cardRef for per-pixel mouse tracking
|
|
462
504
|
transition:
|
|
463
505
|
hoverEffect === "3d"
|
|
464
506
|
? "transform 100ms ease-out"
|
|
465
|
-
:
|
|
466
|
-
? "transform 300ms ease"
|
|
467
|
-
: undefined,
|
|
468
|
-
transform: hoverEffect === "scale" ? scaleTransform : undefined,
|
|
507
|
+
: undefined,
|
|
469
508
|
}}
|
|
470
509
|
>
|
|
471
510
|
{/* Thumbnail image */}
|
|
@@ -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
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
SettingsSection,
|
|
19
19
|
} from "../editors/shared";
|
|
20
20
|
import { findGaps } from "../../../lib/builder/cascade";
|
|
21
|
+
import { isSectionBlockSection } from "../../../lib/builder/types";
|
|
21
22
|
import {
|
|
22
23
|
getSectionV2SettingValue,
|
|
23
24
|
hasSectionV2SettingOverride,
|
|
@@ -65,6 +66,9 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
65
66
|
const addColumnV2 = useBuilderStore((s) => s.addColumnV2);
|
|
66
67
|
const currentPreset = section.settings.preset;
|
|
67
68
|
|
|
69
|
+
// Section-level blocks own the full column — hide layout presets and add column
|
|
70
|
+
if (isSectionBlockSection(section)) return null;
|
|
71
|
+
|
|
68
72
|
const allPresets = currentPreset === "custom"
|
|
69
73
|
? [...PRESETS, CUSTOM_PRESET]
|
|
70
74
|
: PRESETS;
|
package/lib/builder/types.ts
CHANGED
|
@@ -80,6 +80,16 @@ export function isSectionBlockType(type: string): boolean {
|
|
|
80
80
|
return SECTION_BLOCK_TYPES.has(type);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** Check if a V2 section contains a single section-level block (e.g. projectGridBlock).
|
|
84
|
+
* These sections behave like top-level content items: no column management,
|
|
85
|
+
* selecting the section auto-selects the block. */
|
|
86
|
+
export function isSectionBlockSection(section: { columns?: Array<{ blocks?: Array<{ _type: string }> }> }): boolean {
|
|
87
|
+
const cols = section.columns || [];
|
|
88
|
+
if (cols.length !== 1) return false;
|
|
89
|
+
const blocks = cols[0].blocks || [];
|
|
90
|
+
return blocks.length === 1 && SECTION_BLOCK_TYPES.has(blocks[0]._type);
|
|
91
|
+
}
|
|
92
|
+
|
|
83
93
|
export type SectionType = "empty-v2" | "parallaxGroup" | SectionBlockType;
|
|
84
94
|
|
|
85
95
|
export interface SectionTypeInfo {
|
package/package.json
CHANGED