@morphika/andami 0.1.10 → 0.2.1

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 (42) hide show
  1. package/app/admin/pages/[slug]/page.tsx +3 -7
  2. package/app/api/admin/pages/[slug]/route.ts +2 -28
  3. package/app/api/admin/settings/route.ts +30 -0
  4. package/components/blocks/EnterAnimationWrapper.tsx +19 -4
  5. package/components/blocks/PageRenderer.tsx +2 -15
  6. package/components/blocks/ProjectGridBlockRenderer.tsx +34 -36
  7. package/components/blocks/TextBlockRenderer.tsx +1 -1
  8. package/components/builder/DndWrapper.tsx +2 -24
  9. package/components/builder/InsertionLines.tsx +5 -5
  10. package/components/builder/ReadOnlyFrame.tsx +5 -49
  11. package/components/builder/SectionV2Canvas.tsx +2 -2
  12. package/components/builder/SectionV2Column.tsx +5 -5
  13. package/components/builder/SettingsPanel.tsx +0 -12
  14. package/components/builder/SortableBlock.tsx +3 -3
  15. package/components/builder/SortableRow.tsx +6 -27
  16. package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -3
  17. package/components/builder/live-preview/drag-utils.tsx +2 -2
  18. package/components/builder/settings-panel/AnimationTab.tsx +2 -16
  19. package/components/builder/settings-panel/index.ts +0 -1
  20. package/components/builder/settings-panel/responsive-helpers.ts +2 -50
  21. package/components/builder/settings-panel/useSettingsPanelSelection.ts +1 -16
  22. package/lib/builder/constants.ts +5 -4
  23. package/lib/builder/serializer/normalizers.ts +2 -40
  24. package/lib/builder/serializer/serializers.ts +3 -74
  25. package/lib/builder/store-blocks.ts +3 -19
  26. package/lib/builder/store-helpers.ts +2 -2
  27. package/lib/builder/store-sections.ts +26 -64
  28. package/lib/builder/store.ts +3 -6
  29. package/lib/builder/templates.ts +9 -45
  30. package/lib/builder/types.ts +4 -11
  31. package/lib/sanity/queries.ts +6 -29
  32. package/lib/sanity/types.ts +2 -70
  33. package/package.json +2 -2
  34. package/sanity/schemas/index.ts +0 -5
  35. package/sanity/schemas/objects/parallaxGroup.ts +2 -2
  36. package/sanity/schemas/page.ts +1 -1
  37. package/sanity/schemas/pageSectionV2.ts +1 -0
  38. package/sanity/schemas/siteSettings.ts +42 -0
  39. package/styles/base.css +7 -5
  40. package/components/blocks/SectionRenderer.tsx +0 -171
  41. package/components/builder/settings-panel/LayoutTab.tsx +0 -346
  42. package/sanity/schemas/pageSection.ts +0 -157
@@ -18,8 +18,8 @@ import {
18
18
  SortableContext,
19
19
  verticalListSortingStrategy,
20
20
  } from "@dnd-kit/sortable";
21
- import type { Page, PageSection, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem } from "../../../../lib/sanity/types";
22
- import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../../../lib/sanity/types";
21
+ import type { Page, PageSectionV2, ParallaxGroup, SectionColumn, CustomSectionInstance, CustomSectionListItem } from "../../../../lib/sanity/types";
22
+ import { isPageSectionV2, isCustomSectionInstance, isParallexGroup } 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";
@@ -665,10 +665,8 @@ export default function PageEditorPage() {
665
665
  >
666
666
  {store.rows.map((item, rowIndex) => {
667
667
  const isV2Section = isPageSectionV2(item);
668
- const isSection = isPageSection(item);
669
668
  const isInstance = isCustomSectionInstance(item);
670
- const isParallax = isParallaxGroup(item);
671
- const section = isSection ? (item as PageSection) : null;
669
+ const isParallax = isParallexGroup(item);
672
670
  const v2Section = isV2Section ? (item as PageSectionV2) : null;
673
671
 
674
672
  // Custom Section Instance — rendered directly without SortableRow chrome
@@ -770,8 +768,6 @@ export default function PageEditorPage() {
770
768
  section={v2Section}
771
769
  onAddBlockTarget={handleAddBlockTargetV2}
772
770
  />
773
- ) : isSection && section?.block?.[0] ? (
774
- <BlockLivePreview block={section.block[0]} viewport={store.activeViewport} />
775
771
  ) : null}
776
772
  </SortableRow>
777
773
  );
@@ -18,7 +18,7 @@ interface RawNavItem {
18
18
  // ─── Helpers ──────────────────────────────────────────────────────────────
19
19
 
20
20
  /** Validate basic row→column→block structure before deeper sanitization.
21
- * Supports both Row (columns→blocks) and PageSection (_type: "pageSection", block array).
21
+ * Supports PageSectionV2, ParallaxGroup, and CustomSectionInstance.
22
22
  */
23
23
  function validateBlockStructure(rows: unknown[]): { valid: boolean; error?: string } {
24
24
  if (!Array.isArray(rows)) return { valid: false, error: "content_rows must be an array" };
@@ -43,20 +43,6 @@ function validateBlockStructure(rows: unknown[]): { valid: boolean; error?: stri
43
43
  continue;
44
44
  }
45
45
 
46
- // PageSection: validate block array instead of columns
47
- if (r._type === "pageSection") {
48
- if (!Array.isArray(r.block)) return { valid: false, error: `Section ${i}: block must be an array` };
49
- const blocks = r.block as unknown[];
50
- for (let k = 0; k < blocks.length; k++) {
51
- const block = blocks[k];
52
- if (!block || typeof block !== "object") return { valid: false, error: `Section ${i}, block ${k}: must be an object` };
53
- const b = block as Record<string, unknown>;
54
- if (typeof b._key !== "string" || !b._key) return { valid: false, error: `Section ${i}, block ${k}: missing _key` };
55
- if (typeof b._type !== "string" || !b._type) return { valid: false, error: `Section ${i}, block ${k}: missing _type` };
56
- }
57
- continue;
58
- }
59
-
60
46
  // ParallaxGroup: validate slides array with columns→blocks inside each slide (Session 127)
61
47
  if (r._type === "parallaxGroup") {
62
48
  if (!Array.isArray(r.slides)) return { valid: false, error: `ParallaxGroup ${i}: slides must be an array` };
@@ -167,7 +153,7 @@ function sanitizeColumnsBlocks(columns: unknown[]): { valid: boolean; error?: st
167
153
  }
168
154
 
169
155
  /** Recursively sanitize URLs and asset paths in block content.
170
- * Supports Row (columns→blocks), PageSection (block array), and ParallaxGroup (slides→columns→blocks).
156
+ * Supports PageSectionV2 (columns→blocks) and ParallaxGroup (slides→columns→blocks).
171
157
  */
172
158
  function sanitizeBlockContent(rows: unknown[]): { valid: boolean; error?: string } {
173
159
  if (!Array.isArray(rows)) return { valid: true };
@@ -176,18 +162,6 @@ function sanitizeBlockContent(rows: unknown[]): { valid: boolean; error?: string
176
162
  if (!row || typeof row !== "object") continue;
177
163
  const rowRecord = row as Record<string, unknown>;
178
164
 
179
- // PageSection: sanitize block array directly
180
- if (rowRecord._type === "pageSection") {
181
- const sectionBlocks = rowRecord.block;
182
- if (!Array.isArray(sectionBlocks)) continue;
183
- for (const block of sectionBlocks) {
184
- if (!block || typeof block !== "object") continue;
185
- const result = sanitizeSingleBlock(block as Record<string, unknown>);
186
- if (!result.valid) return result;
187
- }
188
- continue;
189
- }
190
-
191
165
  // ParallaxGroup: sanitize blocks inside each slide's columns (Session 127)
192
166
  if (rowRecord._type === "parallaxGroup") {
193
167
  const slides = rowRecord.slides;
@@ -16,6 +16,23 @@ import { logger } from "../../../../lib/logger";
16
16
 
17
17
  const SETTINGS_ID = "siteSettings";
18
18
 
19
+ /** Sanitize a per-viewport responsive override object for nav_design.
20
+ * Only allows the known overridable fields with proper type/range checks. */
21
+ function sanitizeNavResponsiveOverride(o: Record<string, unknown>) {
22
+ const result: Record<string, unknown> = {};
23
+ if (typeof o.font_size === "number") result.font_size = Math.max(8, Math.min(48, o.font_size));
24
+ if (typeof o.font_weight === "string") result.font_weight = o.font_weight.slice(0, 10);
25
+ if (typeof o.text_align === "string" && ["left", "center", "right"].includes(o.text_align)) result.text_align = o.text_align;
26
+ if (typeof o.vertical_align === "string" && ["top", "middle", "bottom"].includes(o.vertical_align)) result.vertical_align = o.vertical_align;
27
+ if (typeof o.text_transform === "string" && ["none", "uppercase", "lowercase", "capitalize"].includes(o.text_transform)) result.text_transform = o.text_transform;
28
+ if (typeof o.padding_h === "number") result.padding_h = Math.max(0, Math.min(200, o.padding_h));
29
+ if (typeof o.padding_v === "number") result.padding_v = Math.max(0, Math.min(200, o.padding_v));
30
+ if (typeof o.margin_h === "number") result.margin_h = Math.max(0, Math.min(200, o.margin_h));
31
+ if (typeof o.margin_v === "number") result.margin_v = Math.max(0, Math.min(200, o.margin_v));
32
+ if (typeof o.items_gap === "number") result.items_gap = Math.max(0, Math.min(200, o.items_gap));
33
+ return result;
34
+ }
35
+
19
36
  export async function GET() {
20
37
  const authenticated = await isAdminAuthenticated();
21
38
  if (!authenticated) {
@@ -221,6 +238,19 @@ export async function POST(request: NextRequest) {
221
238
  entrance_delay: typeof nd.entrance_delay === "number" ? Math.max(0, Math.min(5000, nd.entrance_delay)) : 0,
222
239
  entrance_stagger: !!nd.entrance_stagger,
223
240
  entrance_stagger_delay: typeof nd.entrance_stagger_delay === "number" ? Math.max(20, Math.min(500, nd.entrance_stagger_delay)) : 80,
241
+ // ── Responsive overrides (tablet / phone) ──
242
+ ...(nd.responsive && typeof nd.responsive === "object"
243
+ ? {
244
+ responsive: {
245
+ ...(nd.responsive.tablet && typeof nd.responsive.tablet === "object"
246
+ ? { tablet: sanitizeNavResponsiveOverride(nd.responsive.tablet) }
247
+ : {}),
248
+ ...(nd.responsive.phone && typeof nd.responsive.phone === "object"
249
+ ? { phone: sanitizeNavResponsiveOverride(nd.responsive.phone) }
250
+ : {}),
251
+ },
252
+ }
253
+ : {}),
224
254
  },
225
255
  };
226
256
  break;
@@ -31,6 +31,12 @@ interface EnterAnimationWrapperProps {
31
31
  staggerIndex?: number;
32
32
  /** Stagger delay in ms (multiplied by staggerIndex) */
33
33
  staggerDelay?: number;
34
+ /**
35
+ * When true, the animation is triggered immediately by the parent
36
+ * instead of using a per-element IntersectionObserver.
37
+ * Used by ProjectGridBlock to fire all card animations at once.
38
+ */
39
+ forceEnter?: boolean;
34
40
  }
35
41
 
36
42
  /**
@@ -54,6 +60,7 @@ export default function EnterAnimationWrapper({
54
60
  style,
55
61
  staggerIndex,
56
62
  staggerDelay,
63
+ forceEnter,
57
64
  }: EnterAnimationWrapperProps) {
58
65
  const { preset, duration, delay, easing } = config;
59
66
 
@@ -66,8 +73,18 @@ export default function EnterAnimationWrapper({
66
73
  // Get keyframe data for initial hidden state
67
74
  const keyframes = getEnterKeyframes(preset);
68
75
 
69
- // ── IntersectionObserver: above-the-fold detection + scroll reveal ──
76
+ // ── forceEnter: parent controls when animation fires ──
70
77
  useEffect(() => {
78
+ if (forceEnter && !hasEntered) {
79
+ setHasEntered(true);
80
+ }
81
+ }, [forceEnter, hasEntered]);
82
+
83
+ // ── IntersectionObserver: self-managed trigger (used when forceEnter is not set) ──
84
+ useEffect(() => {
85
+ // Skip per-element observer when parent controls the trigger
86
+ if (forceEnter !== undefined) return;
87
+
71
88
  const el = ref.current;
72
89
  if (!el || hasEntered) return;
73
90
 
@@ -80,8 +97,6 @@ export default function EnterAnimationWrapper({
80
97
  const observer = new IntersectionObserver(
81
98
  ([entry]) => {
82
99
  if (entry.isIntersecting) {
83
- // Whether above-the-fold (already visible on mount) or below-the-fold
84
- // (scrolled into view), behavior is the same: play animation once.
85
100
  setHasEntered(true);
86
101
  observer.disconnect();
87
102
  }
@@ -91,7 +106,7 @@ export default function EnterAnimationWrapper({
91
106
 
92
107
  observer.observe(el);
93
108
  return () => observer.disconnect();
94
- }, [hasEntered]);
109
+ }, [hasEntered, forceEnter]);
95
110
 
96
111
  // If preset is "none" or has no keyframes (typewriter), just render children
97
112
  if (preset === "none" || !keyframes) {
@@ -1,6 +1,5 @@
1
- import type { Page, ContentBlock, ContentItem, PageSection, PageSectionV2, SectionColumn, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
2
- import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
3
- import SectionRenderer from "./SectionRenderer";
1
+ import type { Page, ContentBlock, ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup } from "../../lib/sanity/types";
2
+ import { isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
4
3
  import SectionV2Renderer from "./SectionV2Renderer";
5
4
  import CustomSectionInstanceRenderer from "./CustomSectionInstanceRenderer";
6
5
  import ParallaxGroupRenderer from "./ParallaxGroupRenderer";
@@ -34,16 +33,6 @@ function getBlockImagePath(block: ContentBlock): string | null {
34
33
 
35
34
  function findFirstImagePath(items: ContentItem[]): string | null {
36
35
  for (const item of items) {
37
- if (isPageSection(item)) {
38
- // Check inside PageSection blocks for image/cover blocks
39
- const section = item as PageSection;
40
- const sectionBlock = Array.isArray(section.block) ? section.block[0] : undefined;
41
- if (sectionBlock) {
42
- const path = getBlockImagePath(sectionBlock as unknown as ContentBlock);
43
- if (path) return path;
44
- }
45
- continue;
46
- }
47
36
  if (isParallaxGroup(item)) {
48
37
  // Check first slide's background image for preloading
49
38
  const group = item as ParallaxGroup;
@@ -134,8 +123,6 @@ export default function PageRenderer({ page }: { page: Page }) {
134
123
  ? <ParallaxGroupRenderer key={item._key} group={item as ParallaxGroup} pageEnterAnimation={pageEnterAnimation} />
135
124
  : isPageSectionV2(item)
136
125
  ? <SectionV2Renderer key={item._key} section={item as PageSectionV2} pageEnterAnimation={pageEnterAnimation} />
137
- : isPageSection(item)
138
- ? <SectionRenderer key={item._key} section={item as PageSection} pageEnterAnimation={pageEnterAnimation} />
139
126
  : null
140
127
  )}
141
128
  </article>
@@ -62,8 +62,6 @@ export default function ProjectGridBlockRenderer({
62
62
  const viewport = useViewport();
63
63
  const containerRef = useRef<HTMLDivElement>(null);
64
64
  const roRef = useRef<ResizeObserver | null>(null);
65
- /** Container top offset at first measure — for above-the-fold detection */
66
- const containerTopRef = useRef<number | null>(null);
67
65
  const [containerWidth, setContainerWidth] = useState(0);
68
66
  const [resolvedProjects, setResolvedProjects] = useState<ResolvedProject[]>([]);
69
67
  // BLK-005: Track fetch errors so we can provide user feedback
@@ -85,10 +83,6 @@ export default function ProjectGridBlockRenderer({
85
83
  const w = entry.contentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
86
84
  if (w > 0) setContainerWidth(w);
87
85
  }
88
- // Capture container top on first observation (for above-the-fold detection)
89
- if (containerTopRef.current === null) {
90
- containerTopRef.current = node.getBoundingClientRect().top;
91
- }
92
86
  });
93
87
  roRef.current = ro;
94
88
  ro.observe(node);
@@ -227,39 +221,44 @@ export default function ProjectGridBlockRenderer({
227
221
  }
228
222
  : undefined;
229
223
 
230
- // ─── Determine which cards are above the fold (visible without scrolling) ───
231
- // Only these cards get the entrance animation. Cards below the fold
232
- // render immediately visible to avoid empty gaps while scrolling.
233
- const aboveFoldKeys = useMemo(() => {
234
- if (!entranceEnabled || masonry.items.length === 0) return new Set<string>();
235
-
236
- const containerTop = containerTopRef.current ?? 0;
237
- const viewportHeight = typeof window !== "undefined" ? window.innerHeight : 900;
238
- // How much vertical space is available below the container's top edge
239
- const cutoff = viewportHeight - containerTop;
240
-
241
- const keys = new Set<string>();
242
- for (const item of masonry.items) {
243
- // Card is above the fold if its top edge starts within the viewport
244
- if (item.y < cutoff) {
245
- keys.add(item.key);
246
- }
224
+ // ─── Grid-level entrance trigger ───
225
+ // A single IntersectionObserver on the grid container fires all card
226
+ // animations at once (with stagger). No per-card observer needed.
227
+ const [gridVisible, setGridVisible] = useState(false);
228
+
229
+ useEffect(() => {
230
+ if (!entranceEnabled || gridVisible) return;
231
+ const el = containerRef.current;
232
+ if (!el) return;
233
+
234
+ // Respect prefers-reduced-motion
235
+ if (typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
236
+ setGridVisible(true);
237
+ return;
247
238
  }
248
- return keys;
249
- }, [entranceEnabled, masonry.items]);
239
+
240
+ const observer = new IntersectionObserver(
241
+ ([entry]) => {
242
+ if (entry.isIntersecting) {
243
+ setGridVisible(true);
244
+ observer.disconnect();
245
+ }
246
+ },
247
+ { threshold: 0.05 },
248
+ );
249
+
250
+ observer.observe(el);
251
+ return () => observer.disconnect();
252
+ }, [entranceEnabled, gridVisible]);
250
253
 
251
254
  // ─── Compute stagger indices sorted by vertical position ───
252
255
  // Cards at similar y positions (within half of gapV) share the same rank.
253
- // Only computed for above-the-fold cards.
256
+ // Computed for ALL cards so the entire grid animates in together.
254
257
  const staggerIndices = useMemo(() => {
255
258
  if (!entranceEnabled || masonry.items.length === 0) return new Map<string, number>();
256
259
 
257
- // Only stagger above-the-fold cards
258
- const aboveFold = masonry.items.filter((item) => aboveFoldKeys.has(item.key));
259
- if (aboveFold.length === 0) return new Map<string, number>();
260
-
261
260
  // Sort items by y, then by x for tie-breaking
262
- const sorted = [...aboveFold].sort((a, b) => a.y - b.y || a.x - b.x);
261
+ const sorted = [...masonry.items].sort((a, b) => a.y - b.y || a.x - b.x);
263
262
  const indices = new Map<string, number>();
264
263
  let rank = 0;
265
264
  let prevY = -Infinity;
@@ -273,7 +272,7 @@ export default function ProjectGridBlockRenderer({
273
272
  indices.set(item.key, rank);
274
273
  }
275
274
  return indices;
276
- }, [entranceEnabled, masonry.items, aboveFoldKeys, gapV]);
275
+ }, [entranceEnabled, masonry.items, gapV]);
277
276
 
278
277
  if (resolvedProjects.length === 0) {
279
278
  // BLK-005: Show subtle error message if fetch failed (not just empty data)
@@ -321,12 +320,10 @@ export default function ProjectGridBlockRenderer({
321
320
  />
322
321
  );
323
322
 
324
- const isAboveFold = aboveFoldKeys.has(item.key);
325
-
326
323
  return (
327
324
  <div
328
325
  key={item.key}
329
- data-card-entrance={entranceEnabled && isAboveFold ? "" : undefined}
326
+ data-card-entrance={entranceEnabled ? "" : undefined}
330
327
  style={{
331
328
  position: "absolute",
332
329
  left: item.x,
@@ -335,11 +332,12 @@ export default function ProjectGridBlockRenderer({
335
332
  height: item.height,
336
333
  }}
337
334
  >
338
- {entranceEnabled && entranceAnimConfig && isAboveFold ? (
335
+ {entranceEnabled && entranceAnimConfig ? (
339
336
  <EnterAnimationWrapper
340
337
  config={entranceAnimConfig}
341
338
  staggerIndex={staggerIndices.get(item.key) ?? 0}
342
339
  staggerDelay={entranceStaggerDelay}
340
+ forceEnter={gridVisible}
343
341
  style={{ width: "100%", height: "100%" }}
344
342
  >
345
343
  {card}
@@ -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: "anywhere", wordBreak: "normal", minWidth: 0, ...style }}
90
+ style={{ overflowWrap: "break-word", wordBreak: "normal", minWidth: 0, ...style }}
91
91
  >
92
92
  <PortableText value={block.text} />
93
93
  </div>
@@ -100,22 +100,6 @@ const RowDragOverlay = memo(function RowDragOverlay({ rowKey }: { rowKey: string
100
100
  );
101
101
  }
102
102
 
103
- // PageSection
104
- if ((item as { _type?: string })._type === "pageSection") {
105
- const section = item as import("../../lib/sanity/types").PageSection;
106
- const label = section.section_type === "projectGrid"
107
- ? "Project Grid"
108
- : "Section";
109
- return (
110
- <div className="rounded border border-[#93278f] bg-[#1a1a1a]/90 px-4 py-3 shadow-lg shadow-[#93278f]/20 backdrop-blur-sm">
111
- <div className="flex items-center gap-2">
112
- <span className="text-[#93278f]">⠿</span>
113
- <span className="text-xs text-white">{label}</span>
114
- </div>
115
- </div>
116
- );
117
- }
118
-
119
103
  return null;
120
104
  });
121
105
 
@@ -140,7 +124,7 @@ const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: {
140
124
  }
141
125
  if (!item) return null;
142
126
 
143
- // Handle V2 sections, PageSections, and virtual sections from parallax slides
127
+ // Handle V2 sections and virtual sections from parallax slides
144
128
  let block: import("../../lib/sanity/types").ContentBlock | undefined;
145
129
  if (isPageSectionV2(item)) {
146
130
  const v2Section = item as PageSectionV2;
@@ -148,18 +132,12 @@ const BlockDragOverlay = memo(function BlockDragOverlay({ blockKey, rowKey }: {
148
132
  const found = (col.blocks || []).find((b) => b._key === blockKey);
149
133
  if (found) { block = found; break; }
150
134
  }
151
- } else if ((item as { _type?: string })._type === "pageSection") {
152
- const section = item as import("../../lib/sanity/types").PageSection;
153
- const sBlock = Array.isArray(section.block) ? section.block[0] : undefined;
154
- if (sBlock && sBlock._key === blockKey) {
155
- block = sBlock as import("../../lib/sanity/types").ContentBlock;
156
- }
157
135
  }
158
136
 
159
137
  if (!block) return null;
160
138
  const info = ALL_BLOCK_INFO.find((b) => b.type === block!._type);
161
139
  return (
162
- <div className="rounded border border-[#e28b00] bg-[#e28b00]/10 px-3 py-2 shadow-lg shadow-[#e28b00]/20 backdrop-blur-sm">
140
+ <div className="rounded border border-[#0d9668] bg-[#0d9668]/10 px-3 py-2 shadow-lg shadow-[#0d9668]/20 backdrop-blur-sm">
163
141
  <div className="flex items-center gap-2">
164
142
  <span className="text-xs">{info?.icon || "▪"}</span>
165
143
  <span className="text-xs text-white">{info?.label || block._type}</span>
@@ -3,7 +3,7 @@
3
3
  import { useMemo, memo } from "react";
4
4
  import type { CascadeColumn } from "../../lib/builder/cascade-helpers";
5
5
  import type { DropTarget } from "./hooks/useColumnDrag";
6
- import { BUILDER_GREEN } from "../../lib/builder/constants";
6
+ import { BUILDER_BLUE } from "../../lib/builder/constants";
7
7
 
8
8
  // ============================================
9
9
  // InsertionLines — Visual insertion indicators between adjacent columns
@@ -137,7 +137,7 @@ export const InsertionLines = memo(function InsertionLines({
137
137
  pointerEvents: "auto",
138
138
  }}
139
139
  >
140
- {/* Visual insertion line — green when active */}
140
+ {/* Visual insertion line — blue when active */}
141
141
  <div
142
142
  style={{
143
143
  position: "absolute",
@@ -146,7 +146,7 @@ export const InsertionLines = memo(function InsertionLines({
146
146
  bottom: 4,
147
147
  width: isActive ? 4 : 0,
148
148
  transform: "translateX(-50%)",
149
- background: BUILDER_GREEN,
149
+ background: BUILDER_BLUE,
150
150
  borderRadius: 2,
151
151
  transition: "width 100ms ease-out, opacity 100ms ease-out",
152
152
  opacity: isActive ? 1 : 0,
@@ -163,9 +163,9 @@ export const InsertionLines = memo(function InsertionLines({
163
163
  width: 14,
164
164
  height: 14,
165
165
  borderRadius: "50%",
166
- background: BUILDER_GREEN,
166
+ background: BUILDER_BLUE,
167
167
  border: "2px solid white",
168
- boxShadow: "0 1px 4px rgba(34, 197, 94, 0.4)",
168
+ boxShadow: "0 1px 4px rgba(7, 107, 255, 0.4)",
169
169
  display: "flex",
170
170
  alignItems: "center",
171
171
  justifyContent: "center",
@@ -18,16 +18,15 @@
18
18
 
19
19
  import { memo, useMemo, useState, useEffect } from "react";
20
20
  import { useBuilderStore } from "../../lib/builder/store";
21
- import { DEFAULT_GRID_WIDTH } from "../../lib/builder/constants";
22
21
  import type { DeviceViewport } from "../../lib/builder/types";
23
- import type { ContentItem, PageSection, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
24
- import { isPageSection, isPageSectionV2, isCustomSectionInstance, isParallaxGroup } from "../../lib/sanity/types";
22
+ import type { ContentItem, PageSectionV2, CustomSectionInstance, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
23
+ import { isPageSectionV2, isCustomSectionInstance, isParallexGroup } from "../../lib/sanity/types";
25
24
  import { DEVICE_HEIGHTS } from "../../lib/builder/types";
26
- import { getEffectiveColumnsV2, getSectionV2SettingValue, getRowSettingValue } from "./settings-panel/responsive-helpers";
25
+ import { getEffectiveColumnsV2, getSectionV2SettingValue } from "./settings-panel/responsive-helpers";
27
26
  import BlockLivePreview from "./BlockLivePreview";
28
- import { getRowLayoutStyles, getColumnVerticalAlign } from "../../lib/builder/layout-styles";
27
+ import { getColumnVerticalAlign } from "../../lib/builder/layout-styles";
29
28
 
30
- // Shared list of layout keys that support responsive overrides (V1 + V2)
29
+ // Layout keys that support responsive overrides for V2 sections
31
30
  const OVERRIDABLE_KEYS = [
32
31
  "spacing_top", "spacing_right", "spacing_bottom", "spacing_left",
33
32
  "offset_top", "offset_right", "offset_bottom", "offset_left",
@@ -35,43 +34,6 @@ const OVERRIDABLE_KEYS = [
35
34
  "border_color", "border_width", "border_style", "border_sides", "border_radius",
36
35
  ] as const;
37
36
 
38
- // ============================================
39
- // Memoized per-section renderer — renders PageSection block directly
40
- // ============================================
41
-
42
- interface ReadOnlySectionProps {
43
- section: PageSection;
44
- viewport: DeviceViewport;
45
- }
46
-
47
- const ReadOnlySection = memo(function ReadOnlySection({ section, viewport }: ReadOnlySectionProps) {
48
- const block = section.block?.[0];
49
- if (!block) return null;
50
-
51
- const base = section.settings || {};
52
- // Merge responsive overrides for V1 PageSection (same approach as V2)
53
- const resolvedSettings = useMemo(() => {
54
- const merged: Record<string, unknown> = { ...base };
55
- for (const key of OVERRIDABLE_KEYS) {
56
- merged[key] = getRowSettingValue(section, viewport, key, (base as Record<string, unknown>)[key]);
57
- }
58
- return merged;
59
- }, [section, viewport, base]);
60
- const layoutStyles = getRowLayoutStyles(resolvedSettings as Record<string, unknown>);
61
-
62
- const sectionStyle: React.CSSProperties = {
63
- ...layoutStyles,
64
- contentVisibility: "auto",
65
- containIntrinsicSize: "auto 200px",
66
- };
67
-
68
- return (
69
- <div style={sectionStyle}>
70
- <BlockLivePreview block={block} viewport={viewport} />
71
- </div>
72
- );
73
- });
74
-
75
37
  // ============================================
76
38
  // Memoized per-section V2 renderer — renders PageSectionV2 grid
77
39
  // BUG-V2-005 fix: V2 sections were not rendered in inactive device frames.
@@ -404,12 +366,6 @@ export default function ReadOnlyFrame({ viewport }: ReadOnlyFrameProps) {
404
366
  section={item as PageSectionV2}
405
367
  viewport={viewport}
406
368
  />
407
- ) : isPageSection(item) ? (
408
- <ReadOnlySection
409
- key={item._key}
410
- section={item as PageSection}
411
- viewport={viewport}
412
- />
413
369
  ) : null
414
370
  )}
415
371
  </div>
@@ -277,9 +277,9 @@ export default function SectionV2Canvas({
277
277
  }}
278
278
  className={`rounded-lg border-2 border-dashed text-xs font-medium transition-all flex items-center justify-center cursor-pointer ${
279
279
  isGapTarget
280
- ? "border-green-500 text-green-500 bg-green-500/10 opacity-100"
280
+ ? "border-blue-500 text-blue-500 bg-blue-500/10 opacity-100"
281
281
  : showAsDropTarget
282
- ? "border-green-500/40 text-green-500/60 bg-green-500/5 opacity-100"
282
+ ? "border-blue-500/40 text-blue-500/60 bg-blue-500/5 opacity-100"
283
283
  : isSectionHovered
284
284
  ? "border-[#076bff]/25 text-[#076bff]/50 hover:text-[#076bff] hover:border-[#076bff]/60 hover:bg-[#076bff]/5 opacity-100"
285
285
  : "border-transparent text-transparent opacity-0 pointer-events-none"
@@ -10,7 +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 { BUILDER_BLUE, BUILDER_GREEN } from "../../lib/builder/constants";
13
+ import { BUILDER_BLUE } from "../../lib/builder/constants";
14
14
 
15
15
  // ============================================
16
16
  // SectionV2Column — Individual column in a V2 section grid
@@ -266,7 +266,7 @@ export default function SectionV2Column({
266
266
  style={{
267
267
  transition: "box-shadow 150ms, border 150ms",
268
268
  ...(isSwapTarget
269
- ? { boxShadow: `inset 0 0 0 2px ${BUILDER_GREEN}`, background: "rgba(34, 197, 94, 0.08)" }
269
+ ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}`, background: "rgba(7, 107, 255, 0.08)" }
270
270
  : isBlockOver
271
271
  ? { boxShadow: `inset 0 0 0 2px ${BUILDER_BLUE}` }
272
272
  : isSelected
@@ -450,9 +450,9 @@ export default function SectionV2Column({
450
450
  aria-label="Add block to empty column"
451
451
  className={`w-full py-2 rounded-lg text-xs font-medium transition-all flex items-center justify-center ${
452
452
  showChrome
453
- ? "bg-[#e28b00] text-white hover:bg-[#c67a00] shadow-sm opacity-100"
453
+ ? "bg-[#0d9668] text-white hover:bg-[#0a7d56] shadow-sm opacity-100"
454
454
  : showFaintOutline
455
- ? "bg-[#e28b00]/30 text-white/50 opacity-40"
455
+ ? "bg-[#0d9668]/30 text-white/50 opacity-40"
456
456
  : "bg-transparent text-transparent opacity-0 pointer-events-none"
457
457
  }`}
458
458
  style={{ pointerEvents: showChrome || showFaintOutline ? "auto" : "none" }}
@@ -476,7 +476,7 @@ export default function SectionV2Column({
476
476
  <button
477
477
  onClick={handleAddBlockBelow}
478
478
  aria-label="Add block below existing blocks"
479
- className="w-full py-1.5 text-[11px] font-medium rounded bg-[#e28b00] text-white hover:bg-[#c67a00] transition-all shadow-sm"
479
+ className="w-full py-1.5 text-[11px] font-medium rounded bg-[#0d9668] text-white hover:bg-[#0a7d56] transition-all shadow-sm"
480
480
  style={{ pointerEvents: showChrome ? "auto" : "none" }}
481
481
  >
482
482
  + Add Block
@@ -27,7 +27,6 @@ import { AnimationTab } from "./settings-panel/AnimationTab";
27
27
  import { ColumnV2AnimationTab } from "./settings-panel/ColumnV2AnimationTab";
28
28
  import { CustomSectionSettings } from "./settings-panel/CustomSectionSettings";
29
29
  import {
30
- LayoutTab,
31
30
  BlockLayoutTab,
32
31
  PageSettings,
33
32
  PageSeoSettings,
@@ -48,7 +47,6 @@ export default function SettingsPanel() {
48
47
 
49
48
  const sel = useSettingsPanelSelection();
50
49
  const {
51
- selectedSection,
52
50
  selectedSectionV2,
53
51
  selectedCustomSectionInstance,
54
52
  selectedParallaxGroup,
@@ -354,14 +352,9 @@ export default function SettingsPanel() {
354
352
  ) : activeTab === "animation" ? (
355
353
  <AnimationTab
356
354
  selectedBlock={selectedBlock}
357
- selectedSection={selectedSection}
358
355
  />
359
356
  ) : activeTab === "layout" ? (
360
357
  (() => {
361
- // PageSection: show section layout settings (spacing, background, border)
362
- if (selectedSection) {
363
- return <LayoutTab section={selectedSection} sectionKey={selectedSection._key} />;
364
- }
365
358
  if (selectedBlock && !selectedBlock.isSection) {
366
359
  return <BlockLayoutTab block={selectedBlock.block} />;
367
360
  }
@@ -377,11 +370,6 @@ export default function SettingsPanel() {
377
370
  })()
378
371
  ) : activeTab === "seo" ? (
379
372
  <PageSeoSettings />
380
- ) : selectedSection ? (
381
- // PageSection selected → show the section block's settings directly
382
- <BlockSettings
383
- block={selectedSection.block[0]}
384
- />
385
373
  ) : selectedBlock ? (
386
374
  <BlockSettings
387
375
  block={selectedBlock.block}