@morphika/andami 0.2.1 → 0.2.3

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.
@@ -19,7 +19,7 @@ import {
19
19
  verticalListSortingStrategy,
20
20
  } from "@dnd-kit/sortable";
21
21
  import type { Page, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem } from "../../../../lib/sanity/types";
22
- import { isPageSectionV2, isCustomSectionInstance, isParallexGroup } from "../../../../lib/sanity/types";
22
+ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../../../lib/sanity/types";
23
23
  import SectionEditorBar from "../../../../components/builder/SectionEditorBar";
24
24
  import CustomSectionInstanceCard from "../../../../components/builder/CustomSectionInstanceCard";
25
25
  import { ColumnDragProvider } from "../../../../components/builder/ColumnDragContext";
@@ -666,7 +666,7 @@ export default function PageEditorPage() {
666
666
  {store.rows.map((item, rowIndex) => {
667
667
  const isV2Section = isPageSectionV2(item);
668
668
  const isInstance = isCustomSectionInstance(item);
669
- const isParallax = isParallexGroup(item);
669
+ const isParallax = isParallaxGroup(item);
670
670
  const v2Section = isV2Section ? (item as PageSectionV2) : null;
671
671
 
672
672
  // Custom Section Instance — rendered directly without SortableRow chrome
@@ -783,7 +783,13 @@ export default function PageEditorPage() {
783
783
  e.stopPropagation();
784
784
  setShowSectionPicker(true);
785
785
  }}
786
- className="w-full rounded-xl py-3 text-xs font-medium text-white bg-[#93278f] hover:bg-[#7a1f76] transition-colors shadow-sm"
786
+ className="w-full rounded-xl py-3 text-xs font-medium transition-colors"
787
+ style={{
788
+ background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
789
+ color: "rgba(200,160,220,0.8)",
790
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
791
+ border: "1px solid rgba(255,255,255,0.06)",
792
+ }}
787
793
  >
788
794
  + Add Section
789
795
  </button>
@@ -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
 
@@ -249,7 +255,7 @@ export default function ProjectGridBlockRenderer({
249
255
 
250
256
  observer.observe(el);
251
257
  return () => observer.disconnect();
252
- }, [entranceEnabled, gridVisible]);
258
+ }, [entranceEnabled, gridVisible, hasProjects]);
253
259
 
254
260
  // ─── Compute stagger indices sorted by vertical position ───
255
261
  // Cards at similar y positions (within half of gapV) share the same rank.
@@ -330,6 +336,10 @@ export default function ProjectGridBlockRenderer({
330
336
  top: item.y,
331
337
  width: item.width,
332
338
  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,
333
343
  }}
334
344
  >
335
345
  {entranceEnabled && entranceAnimConfig ? (
@@ -87,7 +87,7 @@ export default function TextBlockRenderer({ block }: { block: TextBlock }) {
87
87
  return (
88
88
  <div
89
89
  className={`${className} space-y-[0.75em]`}
90
- style={{ overflowWrap: "break-word", wordBreak: "normal", minWidth: 0, ...style }}
90
+ style={style}
91
91
  >
92
92
  <PortableText value={block.text} />
93
93
  </div>
@@ -20,11 +20,11 @@ import { memo, useMemo, useState, useEffect } from "react";
20
20
  import { useBuilderStore } from "../../lib/builder/store";
21
21
  import type { DeviceViewport } from "../../lib/builder/types";
22
22
  import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
23
- import { isPageSectionV2, isCustomSectionInstance, isParallexGroup } from "../../lib/sanity/types";
23
+ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
24
24
  import { DEVICE_HEIGHTS } from "../../lib/builder/types";
25
25
  import { getEffectiveColumnsV2, getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
26
26
  import BlockLivePreview from "./BlockLivePreview";
27
- import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
27
+ import { getColumnVerticalAlign, getRowLayoutStyles } from "../../lib/builder/layout-styles";
28
28
 
29
29
  // Layout keys that support responsive overrides for V2 sections
30
30
  const OVERRIDABLE_KEYS = [
@@ -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), []);
@@ -450,12 +467,23 @@ export default function SectionV2Column({
450
467
  aria-label="Add block to empty column"
451
468
  className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
452
469
  showChrome
453
- ? "bg-[#0d9668] text-white hover:bg-[#0a7d56] shadow-sm opacity-100"
470
+ ? "opacity-100"
454
471
  : showFaintOutline
455
- ? "bg-[#0d9668]/30 text-white/50 opacity-40"
472
+ ? "opacity-40"
456
473
  : "bg-transparent text-transparent opacity-0 pointer-events-none"
457
474
  }`}
458
- style={{ pointerEvents: showChrome || showFaintOutline ? "auto" : "none" }}
475
+ style={{
476
+ pointerEvents: showChrome || showFaintOutline ? "auto" : "none",
477
+ ...(showChrome ? {
478
+ background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
479
+ color: "rgba(100,220,170,0.8)",
480
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
481
+ border: "1px solid rgba(255,255,255,0.06)",
482
+ } : showFaintOutline ? {
483
+ background: "rgba(38,38,48,0.3)",
484
+ color: "rgba(100,220,170,0.4)",
485
+ } : {}),
486
+ }}
459
487
  >
460
488
  + Add Block
461
489
  </button>
@@ -476,8 +504,14 @@ export default function SectionV2Column({
476
504
  <button
477
505
  onClick={handleAddBlockBelow}
478
506
  aria-label="Add block below existing blocks"
479
- className="w-full py-1.5 text-[11px] font-medium rounded bg-[#0d9668] text-white hover:bg-[#0a7d56] transition-all shadow-sm"
480
- style={{ pointerEvents: showChrome ? "auto" : "none" }}
507
+ className="w-full py-1.5 text-[11px] font-medium rounded transition-all"
508
+ style={{
509
+ pointerEvents: showChrome ? "auto" : "none",
510
+ background: "linear-gradient(170deg, rgba(38,38,48,0.95) 0%, rgba(28,28,36,0.97) 100%)",
511
+ color: "rgba(100,220,170,0.8)",
512
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.04)",
513
+ border: "1px solid rgba(255,255,255,0.06)",
514
+ }}
481
515
  >
482
516
  + Add Block
483
517
  </button>
@@ -144,9 +144,6 @@ export default function SettingsPanel() {
144
144
  } else if (selectedSectionV2) {
145
145
  onDelete = () => store.deleteSection(selectedSectionV2._key);
146
146
  deleteTitle = "Delete Section";
147
- } else if (selectedSection) {
148
- onDelete = () => store.deleteSection(selectedSection._key);
149
- deleteTitle = "Delete Section";
150
147
  }
151
148
 
152
149
  if (!onDelete) return null;
@@ -149,14 +149,18 @@ export default function SortableBlock({
149
149
  className="flex items-center gap-1.5"
150
150
  style={{ transform: `scale(${Math.min(2, 1 / canvasZoom)})`, transformOrigin: "top center" }}
151
151
  >
152
- <div className="flex items-center bg-[#0d9668] rounded shadow-lg overflow-hidden">
152
+ <div className="flex items-center rounded-[5px] overflow-hidden" style={{
153
+ background: "linear-gradient(170deg, rgba(38,38,48,0.97) 0%, rgba(28,28,36,0.98) 100%)",
154
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06)",
155
+ border: "1px solid rgba(255,255,255,0.06)",
156
+ }}>
153
157
  {/* Move up arrow */}
154
158
  <button
155
159
  onClick={() => canMoveUp && reorderBlocks(rowKey, colKey, blockIndex, blockIndex - 1)}
156
160
  className={`transition-colors px-1 py-0.5 text-[11px] ${
157
161
  canMoveUp
158
- ? "text-white/80 hover:text-white hover:bg-white/10 cursor-pointer"
159
- : "text-white/25 cursor-default"
162
+ ? "text-white/45 hover:text-white/80 hover:bg-white/10 cursor-pointer"
163
+ : "text-white/20 cursor-default"
160
164
  }`}
161
165
  title="Move block up"
162
166
  aria-label="Move block up"
@@ -169,10 +173,10 @@ export default function SortableBlock({
169
173
  {/* Move down arrow */}
170
174
  <button
171
175
  onClick={() => canMoveDown && reorderBlocks(rowKey, colKey, blockIndex, blockIndex + 1)}
172
- className={`transition-colors px-1 py-0.5 text-[11px] border-l border-white/20 ${
176
+ className={`transition-colors px-1 py-0.5 text-[11px] border-l border-white/10 ${
173
177
  canMoveDown
174
- ? "text-white/80 hover:text-white hover:bg-white/10 cursor-pointer"
175
- : "text-white/25 cursor-default"
178
+ ? "text-white/45 hover:text-white/80 hover:bg-white/10 cursor-pointer"
179
+ : "text-white/20 cursor-default"
176
180
  }`}
177
181
  title="Move block down"
178
182
  aria-label="Move block down"
@@ -183,12 +187,12 @@ export default function SortableBlock({
183
187
  </svg>
184
188
  </button>
185
189
  {/* Block type label */}
186
- <span className="text-[11px] text-white/70 px-1.5 py-0.5 border-l border-white/20">
190
+ <span className="text-[11px] px-1.5 py-0.5 border-l border-white/10 font-medium" style={{ color: "rgba(100,220,170,0.9)" }}>
187
191
  {info?.icon || "▪"} {info?.label || block._type}
188
192
  </span>
189
193
  {/* Enter animation badge */}
190
194
  {block.enter_animation?.preset && block.enter_animation.preset !== "none" && (
191
- <span className="text-[10px] text-white/50 px-1 py-0.5 border-l border-white/15" title={`Animation: ${block.enter_animation.preset}`}>
195
+ <span className="text-[10px] text-white/35 px-1 py-0.5 border-l border-white/10" title={`Animation: ${block.enter_animation.preset}`}>
192
196
 
193
197
  </span>
194
198
  )}
@@ -196,7 +200,7 @@ export default function SortableBlock({
196
200
  {onDuplicate && (
197
201
  <button
198
202
  onClick={onDuplicate}
199
- className="text-white/70 hover:text-white transition-colors px-1.5 py-0.5 text-[11px] border-l border-white/20 hover:bg-white/10"
203
+ className="text-white/45 hover:text-white/80 transition-colors px-1.5 py-0.5 text-[11px] border-l border-white/10 hover:bg-white/10"
200
204
  title="Duplicate block (Ctrl+D)"
201
205
  aria-label="Duplicate block"
202
206
  >
@@ -253,12 +253,18 @@ export default function SortableRow({
253
253
  >
254
254
  {/* Main toolbar — drag + actions */}
255
255
  <div
256
- className="flex flex-col items-stretch bg-[#93278f]/90 rounded-l-lg shadow-lg py-2 px-2.5 gap-1 cursor-grab active:cursor-grabbing"
256
+ className="flex flex-col items-stretch rounded-l-lg py-2 px-2.5 gap-1 cursor-grab active:cursor-grabbing"
257
+ style={{
258
+ background: "linear-gradient(170deg, rgba(38,38,48,0.97) 0%, rgba(28,28,36,0.98) 100%)",
259
+ boxShadow: "0 4px 16px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06)",
260
+ border: "1px solid rgba(255,255,255,0.06)",
261
+ borderRight: "none",
262
+ }}
257
263
  {...attributes}
258
264
  {...listeners}
259
265
  >
260
266
  {/* Section label — shows specific type for page sections */}
261
- <span className="text-[11px] text-white select-none leading-tight pointer-events-none font-medium tracking-wide">
267
+ <span className="text-[11px] select-none leading-tight pointer-events-none font-medium tracking-wide" style={{ color: "rgba(200,160,220,0.9)" }}>
262
268
  {sectionLabel || "Section"}
263
269
  </span>
264
270
 
@@ -267,7 +273,7 @@ export default function SortableRow({
267
273
  <button
268
274
  onClick={(e) => { e.stopPropagation(); onDuplicate(); }}
269
275
  onPointerDown={(e) => e.stopPropagation()}
270
- className="flex items-center justify-center text-[12px] text-white/80 hover:text-white transition-colors"
276
+ className="flex items-center justify-center text-[12px] text-white/50 hover:text-white/85 transition-colors"
271
277
  title="Duplicate section"
272
278
  aria-label="Duplicate section"
273
279
  >
@@ -277,7 +283,7 @@ export default function SortableRow({
277
283
  onClick={(e) => { e.stopPropagation(); onMoveUp(); }}
278
284
  onPointerDown={(e) => e.stopPropagation()}
279
285
  disabled={isFirst}
280
- className="flex items-center justify-center text-[12px] text-white/80 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
286
+ className="flex items-center justify-center text-[12px] text-white/50 hover:text-white/85 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
281
287
  title="Move up"
282
288
  aria-label="Move section up"
283
289
  >
@@ -287,7 +293,7 @@ export default function SortableRow({
287
293
  onClick={(e) => { e.stopPropagation(); onMoveDown(); }}
288
294
  onPointerDown={(e) => e.stopPropagation()}
289
295
  disabled={isLast}
290
- className="flex items-center justify-center text-[12px] text-white/80 hover:text-white transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
296
+ className="flex items-center justify-center text-[12px] text-white/50 hover:text-white/85 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
291
297
  title="Move down"
292
298
  aria-label="Move section down"
293
299
  >
@@ -300,11 +306,11 @@ export default function SortableRow({
300
306
  <button
301
307
  onClick={(e) => { e.stopPropagation(); onAddColumn(); }}
302
308
  onPointerDown={(e) => e.stopPropagation()}
303
- className="flex items-center gap-1 text-[11px] text-white/80 hover:text-white transition-colors py-0.5"
309
+ className="flex items-center gap-1 text-[11px] text-white/50 hover:text-white/85 transition-colors py-0.5"
304
310
  title="Add column"
305
311
  aria-label="Add column"
306
312
  >
307
- <span className="text-white/50">+</span> Col
313
+ <span className="text-white/30">+</span> Col
308
314
  </button>
309
315
  )}
310
316
 
@@ -312,11 +318,11 @@ export default function SortableRow({
312
318
  <button
313
319
  onClick={(e) => { e.stopPropagation(); onDelete(); }}
314
320
  onPointerDown={(e) => e.stopPropagation()}
315
- className="flex items-center gap-1 text-[11px] text-white/80 hover:text-red-300 transition-colors py-0.5"
321
+ className="flex items-center gap-1 text-[11px] text-white/50 hover:text-red-300 transition-colors py-0.5"
316
322
  title="Delete section"
317
323
  aria-label="Delete section"
318
324
  >
319
- <span className="text-white/50">-</span> Delete
325
+ <span className="text-white/30">-</span> Delete
320
326
  </button>
321
327
  </div>
322
328
  </div>
@@ -333,7 +339,7 @@ export default function SortableRow({
333
339
  )}
334
340
 
335
341
  {/* Content — same layout as Preview */}
336
- <div style={coverRow ? undefined : { maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative z-[2]">
342
+ <div style={coverRow ? undefined : { maxWidth, margin: "0 auto", paddingLeft: maxWidth !== "100%" ? gridPadding : undefined, paddingRight: maxWidth !== "100%" ? gridPadding : undefined }} className="relative">
337
343
  {children}
338
344
  </div>
339
345
  </div>
@@ -149,7 +149,7 @@ export default function LiveTextEditor({ block, editable = false }: { block: Tex
149
149
  fontFamily: "inherit",
150
150
  outline: "none",
151
151
  whiteSpace: "pre-wrap",
152
- wordBreak: "normal",
152
+ wordBreak: "break-word",
153
153
  minHeight: "1em",
154
154
  // Multi-column layout: gap inherits global grid gutter
155
155
  ...(cols ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
package/styles/base.css CHANGED
@@ -57,21 +57,14 @@ body {
57
57
  overflow-x: clip;
58
58
  }
59
59
 
60
- /* Prevent text overflow in grid columns on narrow viewports.
61
- * overflow-wrap: break-word — breaks mid-word ONLY when the entire word
62
- * cannot fit on a fresh line. Does NOT affect min-content intrinsic sizing,
63
- * so CSS Grid columns keep their proper width (unlike 'anywhere' which
64
- * collapses min-content to a single character width).
65
- * word-break: normal — wraps at natural word boundaries first, preventing
66
- * ugly mid-word splits like "Collecti" + "on". */
60
+ /* Prevent text overflow in grid columns on narrow viewports */
67
61
  [data-site] p,
68
62
  [data-site] h1,
69
63
  [data-site] h2,
70
64
  [data-site] h3,
71
65
  [data-site] h4,
72
66
  [data-site] span {
73
- overflow-wrap: break-word;
74
- word-break: normal;
67
+ word-break: break-word;
75
68
  }
76
69
 
77
70
  [data-custom-cursor] {