@morphika/andami 0.2.2 → 0.2.4

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.
@@ -226,8 +226,14 @@ export default function ProjectGridBlockRenderer({
226
226
  // animations at once (with stagger). No per-card observer needed.
227
227
  const [gridVisible, setGridVisible] = useState(false);
228
228
 
229
+ // Track whether we have projects loaded — needed as a dep so the
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;
234
+
229
235
  useEffect(() => {
230
- if (!entranceEnabled || gridVisible) return;
236
+ if (!entranceEnabled || gridVisible || !hasProjects) return;
231
237
  const el = containerRef.current;
232
238
  if (!el) return;
233
239
 
@@ -244,12 +250,18 @@ export default function ProjectGridBlockRenderer({
244
250
  observer.disconnect();
245
251
  }
246
252
  },
247
- { threshold: 0.05 },
253
+ {
254
+ threshold: 0.01,
255
+ // Trigger 200px before the grid scrolls into view so cards
256
+ // start animating just before the user sees them (especially
257
+ // on mobile where the grid may sit just below the fold).
258
+ rootMargin: "200px 0px",
259
+ },
248
260
  );
249
261
 
250
262
  observer.observe(el);
251
263
  return () => observer.disconnect();
252
- }, [entranceEnabled, gridVisible]);
264
+ }, [entranceEnabled, gridVisible, hasProjects]);
253
265
 
254
266
  // ─── Compute stagger indices sorted by vertical position ───
255
267
  // Cards at similar y positions (within half of gapV) share the same rank.
@@ -330,6 +342,10 @@ export default function ProjectGridBlockRenderer({
330
342
  top: item.y,
331
343
  width: item.width,
332
344
  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,
333
349
  }}
334
350
  >
335
351
  {entranceEnabled && entranceAnimConfig ? (
@@ -428,9 +444,12 @@ const ProjectCard = memo(function ProjectCard({
428
444
  const handlePlay = useCallback(() => setIsPlaying(true), []);
429
445
  const handlePause = useCallback(() => setIsPlaying(false), []);
430
446
 
431
- // Scale transform
432
- const scaleTransform =
433
- hoverEffect === "scale" && hovered ? "scale(1.03)" : "scale(1)";
447
+ // Scale hover: applied to an inner content wrapper so the zoom is
448
+ // visible inside the card's borderRadius + overflow:hidden boundary
449
+ // (the parent masonry cell also clips, so scaling the card wrapper
450
+ // itself would be invisible).
451
+ const contentScale =
452
+ hoverEffect === "scale" && hovered ? "scale(1.05)" : "scale(1)";
434
453
 
435
454
  return (
436
455
  <Link
@@ -449,15 +468,22 @@ const ProjectCard = memo(function ProjectCard({
449
468
  height: "100%",
450
469
  overflow: "hidden",
451
470
  borderRadius: radius,
471
+ // 3D tilt: applied on the card wrapper (set via handleMouseMove)
452
472
  transition:
453
473
  hoverEffect === "3d"
454
474
  ? "transform 100ms ease-out"
455
- : hoverEffect === "scale"
456
- ? "transform 300ms ease"
457
- : undefined,
458
- transform: hoverEffect === "scale" ? scaleTransform : undefined,
475
+ : undefined,
459
476
  }}
460
477
  >
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
+ >
461
487
  {/* Thumbnail image */}
462
488
  {imgSrc ? (
463
489
  <img
@@ -522,7 +548,9 @@ const ProjectCard = memo(function ProjectCard({
522
548
  />
523
549
  )}
524
550
 
525
- {/* Subtitle overlay */}
551
+ </div>{/* end inner content wrapper */}
552
+
553
+ {/* Subtitle overlay — outside the scale wrapper so text stays crisp */}
526
554
  {showSubtitle && (
527
555
  <div
528
556
  style={{
@@ -10,6 +10,7 @@ import { useBuilderStore } from "../../lib/builder/store";
10
10
  import { makeBlockId, makeColumnDroppableId } from "./DndWrapper";
11
11
  import type { SectionColumn, ContentBlock, PageSectionV2 } from "../../lib/sanity/types";
12
12
  import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
13
+ import { isSectionBlockType } from "../../lib/builder/types";
13
14
  import { BUILDER_BLUE } from "../../lib/builder/constants";
14
15
 
15
16
  // ============================================
@@ -174,10 +175,26 @@ export default function SectionV2Column({
174
175
  );
175
176
 
176
177
  // ---- Stable callbacks ----
178
+
179
+ // When the column contains a single section-level block (e.g. projectGridBlock),
180
+ // that block fills the entire column visually. Clicking anywhere in the column
181
+ // should select the block — not the column — because users expect to reach the
182
+ // block's settings panel (Settings / Layout / Animation tabs).
183
+ const selectBlock = useBuilderStore((s) => s.selectBlock);
184
+ const singleSectionBlock = (() => {
185
+ const blocks = column.blocks || [];
186
+ if (blocks.length === 1 && isSectionBlockType(blocks[0]._type)) return blocks[0];
187
+ return null;
188
+ })();
189
+
177
190
  const handleClick = useCallback((e: React.MouseEvent) => {
178
191
  e.stopPropagation();
179
- onSelect();
180
- }, [onSelect]);
192
+ if (singleSectionBlock) {
193
+ selectBlock(singleSectionBlock._key);
194
+ } else {
195
+ onSelect();
196
+ }
197
+ }, [onSelect, singleSectionBlock, selectBlock]);
181
198
 
182
199
  const handleMouseEnter = useCallback(() => setIsHovered(true), []);
183
200
  const handleMouseLeave = useCallback(() => setIsHovered(false), []);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",