@morphika/andami 0.1.9 → 0.2.0
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/app/admin/pages/[slug]/page.tsx +3 -7
- package/app/api/admin/pages/[slug]/route.ts +2 -28
- package/app/api/admin/settings/route.ts +30 -0
- package/components/admin/nav-builder/NavBuilder.tsx +90 -14
- package/components/admin/nav-builder/NavGeneralSettings.tsx +521 -271
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -312
- package/components/admin/nav-builder/NavMobileSettings.tsx +159 -140
- package/components/admin/nav-builder/NavSettingsFields.tsx +287 -21
- package/components/admin/nav-builder/NavSettingsPanel.tsx +137 -127
- package/components/blocks/EnterAnimationWrapper.tsx +19 -4
- package/components/blocks/PageRenderer.tsx +2 -15
- package/components/blocks/ProjectGridBlockRenderer.tsx +34 -36
- package/components/blocks/TextBlockRenderer.tsx +1 -1
- package/components/builder/DndWrapper.tsx +2 -24
- package/components/builder/InsertionLines.tsx +5 -5
- package/components/builder/ReadOnlyFrame.tsx +5 -49
- package/components/builder/SectionV2Canvas.tsx +2 -2
- package/components/builder/SectionV2Column.tsx +5 -5
- package/components/builder/SettingsPanel.tsx +0 -12
- package/components/builder/SortableBlock.tsx +3 -3
- package/components/builder/SortableRow.tsx +6 -27
- package/components/builder/editors/ButtonBlockEditor.tsx +8 -3
- package/components/builder/editors/CoverBlockEditor.tsx +14 -6
- package/components/builder/editors/ImageBlockEditor.tsx +8 -3
- package/components/builder/editors/ImageGridBlockEditor.tsx +8 -3
- package/components/builder/editors/ProjectGridEditor.tsx +7 -46
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -1
- package/components/builder/editors/StaggerSettings.tsx +2 -1
- package/components/builder/editors/TextBlockEditor.tsx +8 -3
- package/components/builder/editors/VideoBlockEditor.tsx +10 -4
- package/components/builder/editors/section-icons.tsx +492 -0
- package/components/builder/editors/shared.tsx +23 -4
- package/components/builder/live-preview/LiveTextEditor.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -3
- package/components/builder/live-preview/drag-utils.tsx +2 -2
- package/components/builder/settings-panel/AnimationTab.tsx +2 -16
- package/components/builder/settings-panel/BlockLayoutTab.tsx +13 -58
- package/components/builder/settings-panel/ColumnV2Settings.tsx +4 -1
- package/components/builder/settings-panel/PageSettings.tsx +10 -4
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +6 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +8 -3
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +11 -47
- package/components/builder/settings-panel/SectionV2Settings.tsx +6 -27
- package/components/builder/settings-panel/index.ts +0 -1
- package/components/builder/settings-panel/responsive-helpers.ts +2 -50
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +1 -16
- package/components/ui/Navbar.tsx +151 -30
- package/lib/builder/constants.ts +5 -4
- package/lib/builder/serializer/normalizers.ts +2 -40
- package/lib/builder/serializer/serializers.ts +3 -74
- package/lib/builder/store-blocks.ts +3 -19
- package/lib/builder/store-helpers.ts +2 -2
- package/lib/builder/store-sections.ts +26 -64
- package/lib/builder/store.ts +3 -6
- package/lib/builder/templates.ts +9 -45
- package/lib/builder/types.ts +4 -11
- package/lib/sanity/queries.ts +6 -29
- package/lib/sanity/types.ts +24 -70
- package/package.json +4 -1
- package/sanity/schemas/index.ts +0 -5
- package/sanity/schemas/objects/parallaxGroup.ts +2 -2
- package/sanity/schemas/page.ts +1 -1
- package/sanity/schemas/pageSectionV2.ts +1 -0
- package/sanity/schemas/siteSettings.ts +42 -0
- package/styles/base.css +8 -2
- package/components/blocks/SectionRenderer.tsx +0 -171
- package/components/builder/settings-panel/LayoutTab.tsx +0 -382
- package/sanity/schemas/pageSection.ts +0 -157
package/lib/builder/templates.ts
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
// Each template defines a set of pre-built rows/columns/blocks
|
|
5
5
|
// that get injected as content_rows when creating a new page.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// objects for section blocks instead of the old matryoshka format.
|
|
7
|
+
// All templates use PageSectionV2 (V1 PageSection removed in cleanup).
|
|
9
8
|
|
|
10
|
-
import type { ContentBlock, ContentItem,
|
|
9
|
+
import type { ContentBlock, ContentItem, PageSectionV2, SectionColumn, ParallaxGroup, ParallaxSlideV2 } from "../../lib/sanity/types";
|
|
11
10
|
import { generateKey } from "./utils";
|
|
12
11
|
|
|
13
12
|
// ============================================
|
|
@@ -84,43 +83,20 @@ function makeV2Section(blocks: ContentBlock[], settings?: Partial<PageSectionV2[
|
|
|
84
83
|
};
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
/**
|
|
88
|
-
* Create a first-class PageSection with a section block.
|
|
89
|
-
* BUG-004 fix: replaces the old pattern of wrapping section blocks in makeRow().
|
|
90
|
-
*/
|
|
91
|
-
function makeSection(
|
|
92
|
-
blockType: "projectGridBlock",
|
|
93
|
-
blockDefaults: Record<string, unknown>,
|
|
94
|
-
): PageSection {
|
|
95
|
-
const block = makeBlock({ _type: blockType, ...blockDefaults }) as SectionBlock;
|
|
96
|
-
return {
|
|
97
|
-
_type: "pageSection",
|
|
98
|
-
_key: generateKey(),
|
|
99
|
-
section_type: "projectGrid" as import("../../lib/sanity/types").PageSectionType,
|
|
100
|
-
block: [block] as [SectionBlock],
|
|
101
|
-
settings: {
|
|
102
|
-
spacing_top: "32",
|
|
103
|
-
spacing_bottom: "32",
|
|
104
|
-
},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
86
|
// ============================================
|
|
109
|
-
// Template:
|
|
87
|
+
// Template: Portfolio (content showcase)
|
|
110
88
|
// ============================================
|
|
111
89
|
|
|
112
|
-
const
|
|
113
|
-
id: "
|
|
114
|
-
label: "
|
|
115
|
-
description: "
|
|
90
|
+
const portfolioTemplate: PageTemplate = {
|
|
91
|
+
id: "portfolio",
|
|
92
|
+
label: "Portfolio",
|
|
93
|
+
description: "Portfolio showcase with flexible layout sections",
|
|
116
94
|
icon: "⬡",
|
|
117
95
|
preview: [
|
|
118
96
|
{ type: "spacer", height: 0.5 },
|
|
119
97
|
{ type: "text", height: 0.5 },
|
|
120
98
|
{ type: "spacer", height: 0.3 },
|
|
121
|
-
{ type: "cover", height: 2 },
|
|
122
99
|
{ type: "columns", height: 1.5 },
|
|
123
|
-
{ type: "cover", height: 2 },
|
|
124
100
|
{ type: "columns", height: 1.5 },
|
|
125
101
|
],
|
|
126
102
|
buildRows: () => [
|
|
@@ -136,19 +112,7 @@ const projectGridTemplate: PageTemplate = {
|
|
|
136
112
|
]),
|
|
137
113
|
// Section 3: Spacer
|
|
138
114
|
makeV2Section([makeBlock({ _type: "spacerBlock", height: "medium" })]),
|
|
139
|
-
// Section 4:
|
|
140
|
-
makeSection("projectGridBlock", {
|
|
141
|
-
columns: 3,
|
|
142
|
-
aspect_ratios: ["16/9"],
|
|
143
|
-
gap_v: 16,
|
|
144
|
-
gap_h: 16,
|
|
145
|
-
hover_effect: "scale",
|
|
146
|
-
show_subtitle: true,
|
|
147
|
-
border_radius: 0,
|
|
148
|
-
video_mode: "off",
|
|
149
|
-
projects: [],
|
|
150
|
-
}),
|
|
151
|
-
// Section 5: Bottom spacer
|
|
115
|
+
// Section 4: Bottom spacer
|
|
152
116
|
makeV2Section([makeBlock({ _type: "spacerBlock", height: "xlarge" })]),
|
|
153
117
|
],
|
|
154
118
|
};
|
|
@@ -285,7 +249,7 @@ const parallaxHomeTemplate: PageTemplate = {
|
|
|
285
249
|
|
|
286
250
|
export const PAGE_TEMPLATES: PageTemplate[] = [
|
|
287
251
|
blankTemplate,
|
|
288
|
-
|
|
252
|
+
portfolioTemplate,
|
|
289
253
|
parallaxHomeTemplate,
|
|
290
254
|
];
|
|
291
255
|
|
package/lib/builder/types.ts
CHANGED
|
@@ -9,9 +9,7 @@ import type {
|
|
|
9
9
|
PageType,
|
|
10
10
|
ContentBlock,
|
|
11
11
|
ContentItem,
|
|
12
|
-
PageSection,
|
|
13
12
|
PageSectionV2,
|
|
14
|
-
SectionSettings,
|
|
15
13
|
SectionV2Settings,
|
|
16
14
|
SectionV2Preset,
|
|
17
15
|
SectionColumn,
|
|
@@ -178,7 +176,7 @@ export interface BuilderState {
|
|
|
178
176
|
publishedAt: string | null;
|
|
179
177
|
draftMode: boolean;
|
|
180
178
|
|
|
181
|
-
// Content —
|
|
179
|
+
// Content — V2 sections, custom sections, parallax groups
|
|
182
180
|
rows: ContentItem[];
|
|
183
181
|
|
|
184
182
|
// Selection
|
|
@@ -251,17 +249,12 @@ export interface BuilderActions {
|
|
|
251
249
|
unpublishPage: () => void;
|
|
252
250
|
|
|
253
251
|
// Section operations
|
|
252
|
+
/** Add a Project Grid section as a V2 section with a full-width column (Session 164) */
|
|
254
253
|
addSection: (blockType: "projectGridBlock", afterRowKey?: string | null) => void;
|
|
255
254
|
reorderRows: (fromIndex: number, toIndex: number) => void;
|
|
256
|
-
/**
|
|
257
|
-
updateSectionSettings: (sectionKey: string, settings: Partial<SectionSettings>) => void;
|
|
258
|
-
/** BUG-013 fix: Update responsive overrides for a PageSection */
|
|
259
|
-
updateSectionResponsive: (sectionKey: string, responsive: PageSection["responsive"]) => void;
|
|
260
|
-
/** Update the block content inside a PageSection */
|
|
261
|
-
updateSectionBlock: (sectionKey: string, updates: Partial<import("../../lib/sanity/types").ContentBlock>) => void;
|
|
262
|
-
/** Delete a section (PageSection or PageSectionV2) by key */
|
|
255
|
+
/** Delete a section by key */
|
|
263
256
|
deleteSection: (sectionKey: string) => void;
|
|
264
|
-
/** Duplicate a section
|
|
257
|
+
/** Duplicate a section, inserting copy after the original */
|
|
265
258
|
duplicateSection: (sectionKey: string) => void;
|
|
266
259
|
|
|
267
260
|
// Block operations
|
package/lib/sanity/queries.ts
CHANGED
|
@@ -4,45 +4,22 @@ import { groq } from "next-sanity";
|
|
|
4
4
|
// BLOCK EXPANSION (shared fragment)
|
|
5
5
|
// ============================================
|
|
6
6
|
|
|
7
|
-
// Deep expansion of
|
|
8
|
-
// Handles
|
|
9
|
-
//
|
|
10
|
-
// ParallaxGroup objects (_type: "parallaxGroup") with nested slides.
|
|
11
|
-
// GROQ uses conditional projections to handle all types.
|
|
7
|
+
// Deep expansion of content_rows.
|
|
8
|
+
// Handles PageSectionV2, CustomSectionInstance, and ParallaxGroup.
|
|
9
|
+
// GROQ projections are additive — fields that don't exist on an object are omitted.
|
|
12
10
|
const blockExpansion = `
|
|
13
11
|
content_rows[] {
|
|
14
12
|
_key,
|
|
15
13
|
_type,
|
|
16
|
-
// ── PageSection fields (only present when _type == "pageSection") ──
|
|
17
14
|
section_type,
|
|
18
|
-
|
|
19
|
-
_type,
|
|
20
|
-
_key,
|
|
21
|
-
...,
|
|
22
|
-
"project_ref": project_ref-> {
|
|
23
|
-
_id,
|
|
24
|
-
title,
|
|
25
|
-
slug,
|
|
26
|
-
page_type
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
// ── Row fields (columns only present on row objects) ──
|
|
30
|
-
// Note: V2 sections also have "columns" but with different fields.
|
|
31
|
-
// GROQ projections are additive — fields that don't exist on an object are omitted.
|
|
32
|
-
// We include BOTH Row column fields (width, settings, responsive) and V2 column
|
|
33
|
-
// fields (grid_column, grid_row, span) so both types are fully hydrated.
|
|
15
|
+
// ── V2 section columns ──
|
|
34
16
|
columns[] {
|
|
35
17
|
_key,
|
|
36
18
|
_type,
|
|
37
|
-
// Row column fields
|
|
38
|
-
width,
|
|
39
|
-
settings,
|
|
40
|
-
responsive,
|
|
41
|
-
// V2 column fields (grid positions)
|
|
42
19
|
grid_column,
|
|
43
20
|
grid_row,
|
|
44
21
|
span,
|
|
45
|
-
|
|
22
|
+
enter_animation,
|
|
46
23
|
blocks[] {
|
|
47
24
|
_type,
|
|
48
25
|
_key,
|
|
@@ -96,7 +73,7 @@ const blockExpansion = `
|
|
|
96
73
|
}
|
|
97
74
|
}
|
|
98
75
|
},
|
|
99
|
-
// ── Settings (
|
|
76
|
+
// ── Settings (V2 section settings) ──
|
|
100
77
|
settings,
|
|
101
78
|
// ── Responsive overrides (V2 sections store responsive at section level) ──
|
|
102
79
|
responsive
|
package/lib/sanity/types.ts
CHANGED
|
@@ -346,68 +346,6 @@ export interface ParallaxGroup {
|
|
|
346
346
|
parallax_intensity: number; // 0.2–0.6, default 0.4 (hidden from UI, hardcoded)
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
-
// ============================================
|
|
350
|
-
// Page Section — first-class section type (Session 76)
|
|
351
|
-
// ============================================
|
|
352
|
-
// Replaces the old "matryoshka" approach where section blocks were
|
|
353
|
-
// wrapped in row → column → block. Sections are direct entries in
|
|
354
|
-
// content_rows alongside regular rows.
|
|
355
|
-
|
|
356
|
-
export type PageSectionType = "projectGrid";
|
|
357
|
-
|
|
358
|
-
export interface SectionSettings {
|
|
359
|
-
// Background
|
|
360
|
-
background_color?: string;
|
|
361
|
-
background_opacity?: number;
|
|
362
|
-
background_image?: string;
|
|
363
|
-
background_size?: "cover" | "contain" | "auto";
|
|
364
|
-
background_position?: string;
|
|
365
|
-
background_repeat?: "no-repeat" | "repeat" | "repeat-x" | "repeat-y";
|
|
366
|
-
// Spacing (padding TRBL)
|
|
367
|
-
spacing_top?: string;
|
|
368
|
-
spacing_right?: string;
|
|
369
|
-
spacing_bottom?: string;
|
|
370
|
-
spacing_left?: string;
|
|
371
|
-
// Offset (margin TRBL)
|
|
372
|
-
offset_top?: string;
|
|
373
|
-
offset_right?: string;
|
|
374
|
-
offset_bottom?: string;
|
|
375
|
-
offset_left?: string;
|
|
376
|
-
// Border
|
|
377
|
-
border_color?: string;
|
|
378
|
-
border_width?: string;
|
|
379
|
-
border_style?: "none" | "solid" | "dashed" | "dotted";
|
|
380
|
-
border_sides?: "all" | "top" | "right" | "bottom" | "left" | "top-bottom" | "left-right";
|
|
381
|
-
border_radius?: string;
|
|
382
|
-
// Animation
|
|
383
|
-
enter_animation?: import("../../lib/animation/enter-types").EnterAnimationConfig;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** Section block types that can appear inside a PageSection */
|
|
387
|
-
export type SectionBlock = ProjectGridBlock;
|
|
388
|
-
|
|
389
|
-
/** Section settings that can be overridden per-viewport */
|
|
390
|
-
export type SectionSettingsOverridable = Pick<SectionSettings,
|
|
391
|
-
"background_color" | "background_opacity" | "background_image" | "background_size" | "background_position" | "background_repeat" |
|
|
392
|
-
"spacing_top" | "spacing_right" | "spacing_bottom" | "spacing_left" |
|
|
393
|
-
"offset_top" | "offset_right" | "offset_bottom" | "offset_left" |
|
|
394
|
-
"border_color" | "border_width" | "border_style" | "border_sides" | "border_radius"
|
|
395
|
-
>;
|
|
396
|
-
|
|
397
|
-
export interface PageSection {
|
|
398
|
-
_type: "pageSection";
|
|
399
|
-
_key: string;
|
|
400
|
-
section_type: PageSectionType;
|
|
401
|
-
/** The actual section content — single block */
|
|
402
|
-
block: [SectionBlock];
|
|
403
|
-
settings?: SectionSettings;
|
|
404
|
-
/** BUG-013 fix: Per-viewport responsive overrides for section settings */
|
|
405
|
-
responsive?: {
|
|
406
|
-
tablet?: Partial<SectionSettingsOverridable>;
|
|
407
|
-
phone?: Partial<SectionSettingsOverridable>;
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
349
|
// ============================================
|
|
412
350
|
// Page Section V2 — V2 Grid System (Session 83)
|
|
413
351
|
// ============================================
|
|
@@ -540,16 +478,10 @@ export interface CustomSectionInstance {
|
|
|
540
478
|
}
|
|
541
479
|
|
|
542
480
|
// ============================================
|
|
543
|
-
// Content Item — union of
|
|
481
|
+
// Content Item — union of section types in content_rows
|
|
544
482
|
// ============================================
|
|
545
|
-
// Used in content_rows and the builder store.
|
|
546
483
|
|
|
547
|
-
export type ContentItem =
|
|
548
|
-
|
|
549
|
-
/** Type guard: check if a content item is a PageSection (V1) */
|
|
550
|
-
export function isPageSection(item: ContentItem): item is PageSection {
|
|
551
|
-
return (item as PageSection)._type === "pageSection";
|
|
552
|
-
}
|
|
484
|
+
export type ContentItem = PageSectionV2 | CustomSectionInstance | ParallaxGroup;
|
|
553
485
|
|
|
554
486
|
/** Type guard: check if a content item is a PageSectionV2 */
|
|
555
487
|
export function isPageSectionV2(item: ContentItem): item is PageSectionV2 {
|
|
@@ -704,6 +636,28 @@ export interface NavDesign {
|
|
|
704
636
|
entrance_delay?: number; // ms, default 0
|
|
705
637
|
entrance_stagger?: boolean; // stagger items, default false
|
|
706
638
|
entrance_stagger_delay?: number; // ms between items, default 80
|
|
639
|
+
|
|
640
|
+
/** Per-viewport overrides for responsive nav settings.
|
|
641
|
+
* Only typography + spacing fields are overridable.
|
|
642
|
+
* Missing fields = inherit from desktop. */
|
|
643
|
+
responsive?: {
|
|
644
|
+
tablet?: NavDesignResponsiveOverride;
|
|
645
|
+
phone?: NavDesignResponsiveOverride;
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Subset of NavDesign fields that can be overridden per viewport */
|
|
650
|
+
export interface NavDesignResponsiveOverride {
|
|
651
|
+
font_size?: number;
|
|
652
|
+
font_weight?: string;
|
|
653
|
+
text_align?: "left" | "center" | "right";
|
|
654
|
+
vertical_align?: "top" | "middle" | "bottom";
|
|
655
|
+
text_transform?: "none" | "uppercase" | "lowercase" | "capitalize";
|
|
656
|
+
padding_h?: number;
|
|
657
|
+
padding_v?: number;
|
|
658
|
+
margin_h?: number;
|
|
659
|
+
margin_v?: number;
|
|
660
|
+
items_gap?: number;
|
|
707
661
|
}
|
|
708
662
|
|
|
709
663
|
// ============================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphika/andami",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"page-builder",
|
|
18
18
|
"morphika"
|
|
19
19
|
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
20
23
|
"files": [
|
|
21
24
|
"admin/",
|
|
22
25
|
"app/",
|
package/sanity/schemas/index.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import page from "./page";
|
|
2
|
-
import pageSection from "./pageSection";
|
|
3
2
|
import pageSectionV2 from "./pageSectionV2";
|
|
4
3
|
import customSection from "./customSection";
|
|
5
4
|
import customSectionInstance from "./customSectionInstance";
|
|
@@ -35,9 +34,6 @@ export {
|
|
|
35
34
|
export {
|
|
36
35
|
default as assetRegistry,
|
|
37
36
|
} from "./assetRegistry";
|
|
38
|
-
export {
|
|
39
|
-
default as pageSection,
|
|
40
|
-
} from "./pageSection";
|
|
41
37
|
export {
|
|
42
38
|
default as pageSectionV2,
|
|
43
39
|
} from "./pageSectionV2";
|
|
@@ -81,7 +77,6 @@ export const schemaTypes = [
|
|
|
81
77
|
assetRegistry,
|
|
82
78
|
|
|
83
79
|
// Structural objects
|
|
84
|
-
pageSection,
|
|
85
80
|
pageSectionV2,
|
|
86
81
|
customSection,
|
|
87
82
|
customSectionInstance,
|
|
@@ -3,8 +3,8 @@ import { defineField, defineType } from "sanity";
|
|
|
3
3
|
/**
|
|
4
4
|
* parallaxGroup — A group of full-screen parallax slides.
|
|
5
5
|
*
|
|
6
|
-
* Lives inline in a page's content_rows alongside
|
|
7
|
-
*
|
|
6
|
+
* Lives inline in a page's content_rows alongside PageSectionV2
|
|
7
|
+
* and CustomSectionInstance. Each slide is a
|
|
8
8
|
* full V2 section with background image/video and 100vh height.
|
|
9
9
|
*
|
|
10
10
|
* Session 123: Parallax V2 rewrite.
|
package/sanity/schemas/page.ts
CHANGED
|
@@ -42,7 +42,7 @@ export default defineType({
|
|
|
42
42
|
name: "content_rows",
|
|
43
43
|
title: "Content Rows",
|
|
44
44
|
type: "array",
|
|
45
|
-
of: [{ type: "
|
|
45
|
+
of: [{ type: "pageSectionV2" }, { type: "parallaxGroup" }, { type: "customSectionInstance" }],
|
|
46
46
|
}),
|
|
47
47
|
defineField({
|
|
48
48
|
name: "metadata",
|
|
@@ -210,6 +210,48 @@ export default defineType({
|
|
|
210
210
|
defineField({ name: "entrance_delay", title: "Entrance Delay (ms)", type: "number", description: "Delay before animation starts. Default: 0" }),
|
|
211
211
|
defineField({ name: "entrance_stagger", title: "Stagger Items", type: "boolean", description: "Animate items one by one", initialValue: false }),
|
|
212
212
|
defineField({ name: "entrance_stagger_delay", title: "Stagger Delay (ms)", type: "number", description: "Delay between items. Default: 80" }),
|
|
213
|
+
// ── Responsive overrides (tablet / phone) ──
|
|
214
|
+
defineField({
|
|
215
|
+
name: "responsive",
|
|
216
|
+
title: "Responsive Overrides",
|
|
217
|
+
type: "object",
|
|
218
|
+
fields: [
|
|
219
|
+
defineField({
|
|
220
|
+
name: "tablet",
|
|
221
|
+
title: "Tablet Overrides",
|
|
222
|
+
type: "object",
|
|
223
|
+
fields: [
|
|
224
|
+
defineField({ name: "font_size", title: "Font Size", type: "number" }),
|
|
225
|
+
defineField({ name: "font_weight", title: "Font Weight", type: "string" }),
|
|
226
|
+
defineField({ name: "text_align", title: "Text Align", type: "string" }),
|
|
227
|
+
defineField({ name: "vertical_align", title: "Vertical Align", type: "string" }),
|
|
228
|
+
defineField({ name: "text_transform", title: "Text Transform", type: "string" }),
|
|
229
|
+
defineField({ name: "padding_h", title: "Padding H", type: "number" }),
|
|
230
|
+
defineField({ name: "padding_v", title: "Padding V", type: "number" }),
|
|
231
|
+
defineField({ name: "margin_h", title: "Margin H", type: "number" }),
|
|
232
|
+
defineField({ name: "margin_v", title: "Margin V", type: "number" }),
|
|
233
|
+
defineField({ name: "items_gap", title: "Items Gap", type: "number" }),
|
|
234
|
+
],
|
|
235
|
+
}),
|
|
236
|
+
defineField({
|
|
237
|
+
name: "phone",
|
|
238
|
+
title: "Phone Overrides",
|
|
239
|
+
type: "object",
|
|
240
|
+
fields: [
|
|
241
|
+
defineField({ name: "font_size", title: "Font Size", type: "number" }),
|
|
242
|
+
defineField({ name: "font_weight", title: "Font Weight", type: "string" }),
|
|
243
|
+
defineField({ name: "text_align", title: "Text Align", type: "string" }),
|
|
244
|
+
defineField({ name: "vertical_align", title: "Vertical Align", type: "string" }),
|
|
245
|
+
defineField({ name: "text_transform", title: "Text Transform", type: "string" }),
|
|
246
|
+
defineField({ name: "padding_h", title: "Padding H", type: "number" }),
|
|
247
|
+
defineField({ name: "padding_v", title: "Padding V", type: "number" }),
|
|
248
|
+
defineField({ name: "margin_h", title: "Margin H", type: "number" }),
|
|
249
|
+
defineField({ name: "margin_v", title: "Margin V", type: "number" }),
|
|
250
|
+
defineField({ name: "items_gap", title: "Items Gap", type: "number" }),
|
|
251
|
+
],
|
|
252
|
+
}),
|
|
253
|
+
],
|
|
254
|
+
}),
|
|
213
255
|
],
|
|
214
256
|
}),
|
|
215
257
|
|
package/styles/base.css
CHANGED
|
@@ -57,7 +57,13 @@ body {
|
|
|
57
57
|
overflow-x: clip;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/* Prevent text overflow in grid columns on narrow viewports
|
|
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". */
|
|
61
67
|
[data-site] p,
|
|
62
68
|
[data-site] h1,
|
|
63
69
|
[data-site] h2,
|
|
@@ -65,7 +71,7 @@ body {
|
|
|
65
71
|
[data-site] h4,
|
|
66
72
|
[data-site] span {
|
|
67
73
|
overflow-wrap: break-word;
|
|
68
|
-
word-break:
|
|
74
|
+
word-break: normal;
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
[data-custom-cursor] {
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* SectionRenderer — renders a first-class PageSection on the public site.
|
|
5
|
-
*
|
|
6
|
-
* Unlike V2 sections (which have columns → blocks), SectionRenderer
|
|
7
|
-
* renders the section block directly — no row/column wrapper needed.
|
|
8
|
-
*
|
|
9
|
-
* Session 76: Created as part of the matryoshka → first-class section refactor.
|
|
10
|
-
* Session 120: Updated for new enter animation system.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { PageSection, SectionBlock } from "../../lib/sanity/types";
|
|
14
|
-
import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
|
|
15
|
-
import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
|
|
16
|
-
import BlockRenderer from "./BlockRenderer";
|
|
17
|
-
import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
18
|
-
import { getRowLayoutStyles, hexToRgba } from "../../lib/builder/layout-styles";
|
|
19
|
-
import { colorToOverrideRule, borderColorToOverrideRule, parseColorField } from "../../lib/color-utils";
|
|
20
|
-
import type { ColorField } from "../../lib/sanity/types";
|
|
21
|
-
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* BUG-013 fix: Build responsive CSS overrides for section layout settings.
|
|
25
|
-
* Same approach as RowRenderer's buildRowResponsiveCss.
|
|
26
|
-
*/
|
|
27
|
-
function buildSectionResponsiveCss(section: PageSection): string | null {
|
|
28
|
-
const responsive = section.responsive;
|
|
29
|
-
if (!responsive) return null;
|
|
30
|
-
|
|
31
|
-
const key = section._key;
|
|
32
|
-
const cssRules: string[] = [];
|
|
33
|
-
|
|
34
|
-
for (const [vp, breakpoint] of [["tablet", BREAKPOINTS.tablet], ["phone", BREAKPOINTS.phone]] as const) {
|
|
35
|
-
const overrides = responsive[vp];
|
|
36
|
-
if (!overrides) continue;
|
|
37
|
-
const rules: string[] = [];
|
|
38
|
-
|
|
39
|
-
// px-value properties
|
|
40
|
-
const pxMap: Record<string, string> = {
|
|
41
|
-
spacing_top: "padding-top", spacing_right: "padding-right",
|
|
42
|
-
spacing_bottom: "padding-bottom", spacing_left: "padding-left",
|
|
43
|
-
offset_top: "margin-top", offset_right: "margin-right",
|
|
44
|
-
offset_bottom: "margin-bottom", offset_left: "margin-left",
|
|
45
|
-
border_radius: "border-radius",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
for (const [field, cssProp] of Object.entries(pxMap)) {
|
|
49
|
-
const val = overrides[field as keyof typeof overrides];
|
|
50
|
-
if (val !== undefined && val !== null && val !== "") {
|
|
51
|
-
rules.push(`${cssProp}:${val}px!important`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Border width — side-aware
|
|
56
|
-
if (overrides.border_width !== undefined && overrides.border_width !== null && overrides.border_width !== "") {
|
|
57
|
-
const bw = overrides.border_width;
|
|
58
|
-
const sides = (overrides.border_sides as string) || "all";
|
|
59
|
-
switch (sides) {
|
|
60
|
-
case "top":
|
|
61
|
-
rules.push(`border-top-width:${bw}px!important`);
|
|
62
|
-
break;
|
|
63
|
-
case "right":
|
|
64
|
-
rules.push(`border-right-width:${bw}px!important`);
|
|
65
|
-
break;
|
|
66
|
-
case "bottom":
|
|
67
|
-
rules.push(`border-bottom-width:${bw}px!important`);
|
|
68
|
-
break;
|
|
69
|
-
case "left":
|
|
70
|
-
rules.push(`border-left-width:${bw}px!important`);
|
|
71
|
-
break;
|
|
72
|
-
case "top-bottom":
|
|
73
|
-
rules.push(`border-top-width:${bw}px!important`);
|
|
74
|
-
rules.push(`border-bottom-width:${bw}px!important`);
|
|
75
|
-
break;
|
|
76
|
-
case "left-right":
|
|
77
|
-
rules.push(`border-left-width:${bw}px!important`);
|
|
78
|
-
rules.push(`border-right-width:${bw}px!important`);
|
|
79
|
-
break;
|
|
80
|
-
default:
|
|
81
|
-
rules.push(`border-width:${bw}px!important`);
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Border color (supports solid + gradients via ColorField bridge)
|
|
87
|
-
if (overrides.border_color) {
|
|
88
|
-
rules.push(borderColorToOverrideRule(parseColorField(overrides.border_color)));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Border style
|
|
92
|
-
if (overrides.border_style && overrides.border_style !== "none") {
|
|
93
|
-
rules.push(`border-style:${overrides.border_style}!important`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Background color + opacity (gradient-safe via ColorField bridge)
|
|
97
|
-
if (overrides.background_color) {
|
|
98
|
-
const opacity = overrides.background_opacity as number | undefined;
|
|
99
|
-
rules.push(colorToOverrideRule(
|
|
100
|
-
parseColorField(overrides.background_color),
|
|
101
|
-
opacity
|
|
102
|
-
));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Background image + sub-properties
|
|
106
|
-
if (overrides.background_image) {
|
|
107
|
-
const imgUrl = process.env.NEXT_PUBLIC_ASSET_BASE_URL
|
|
108
|
-
? `${(process.env.NEXT_PUBLIC_ASSET_BASE_URL as string).replace(/\/$/, "")}/${overrides.background_image}`
|
|
109
|
-
: overrides.background_image;
|
|
110
|
-
rules.push(`background-image:url(${imgUrl})!important`);
|
|
111
|
-
rules.push(`background-size:${overrides.background_size || "cover"}!important`);
|
|
112
|
-
rules.push(`background-position:${overrides.background_position || "center center"}!important`);
|
|
113
|
-
rules.push(`background-repeat:${overrides.background_repeat || "no-repeat"}!important`);
|
|
114
|
-
} else {
|
|
115
|
-
if (overrides.background_size) rules.push(`background-size:${overrides.background_size}!important`);
|
|
116
|
-
if (overrides.background_position) rules.push(`background-position:${overrides.background_position}!important`);
|
|
117
|
-
if (overrides.background_repeat) rules.push(`background-repeat:${overrides.background_repeat}!important`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (rules.length > 0) {
|
|
121
|
-
cssRules.push(`@media(max-width:${breakpoint}px){.section-${key}{${rules.join(";")}}}`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return cssRules.length > 0 ? cssRules.join("") : null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
interface SectionRendererProps {
|
|
129
|
-
section: PageSection;
|
|
130
|
-
/** Page-level enter animation config (from page_settings.enter_animation) */
|
|
131
|
-
pageEnterAnimation?: EnterAnimationConfig;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export default function SectionRenderer({ section, pageEnterAnimation }: SectionRendererProps) {
|
|
135
|
-
const s = section.settings ?? {};
|
|
136
|
-
|
|
137
|
-
// Get the section block
|
|
138
|
-
const block = Array.isArray(section.block) ? section.block[0] : undefined;
|
|
139
|
-
if (!block) return null;
|
|
140
|
-
|
|
141
|
-
// Resolve enter animation (section settings → page default → none)
|
|
142
|
-
const sectionEnterConfig = s.enter_animation;
|
|
143
|
-
const resolvedEnter = resolveEnterAnimation(undefined, undefined, sectionEnterConfig, pageEnterAnimation);
|
|
144
|
-
const hasAnimation = resolvedEnter !== null && resolvedEnter.preset !== "none";
|
|
145
|
-
|
|
146
|
-
// Section layout styles (background, spacing, border, etc.)
|
|
147
|
-
const layoutStyles = getRowLayoutStyles(s as Record<string, unknown>);
|
|
148
|
-
|
|
149
|
-
// BUG-013 fix: build responsive CSS overrides for section
|
|
150
|
-
const responsiveCss = buildSectionResponsiveCss(section);
|
|
151
|
-
|
|
152
|
-
// Render the section block directly — no columns, no grid
|
|
153
|
-
// V1 PageSections contain section blocks (ProjectGrid, Parallax) which
|
|
154
|
-
// don't use the block-level enter/hover cascade — they have their own systems.
|
|
155
|
-
let content: React.ReactNode = (
|
|
156
|
-
<section className={`section-${section._key}`} style={layoutStyles}>
|
|
157
|
-
{responsiveCss && <style dangerouslySetInnerHTML={{ __html: responsiveCss }} />}
|
|
158
|
-
<BlockRenderer block={block} pageEnterAnimation={pageEnterAnimation} sectionEnterAnimation={sectionEnterConfig} />
|
|
159
|
-
</section>
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
if (hasAnimation && resolvedEnter) {
|
|
163
|
-
content = (
|
|
164
|
-
<EnterAnimationWrapper config={resolvedEnter}>
|
|
165
|
-
{content}
|
|
166
|
-
</EnterAnimationWrapper>
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return content;
|
|
171
|
-
}
|