@morphika/andami 0.1.10 → 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/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/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/index.ts +0 -1
- package/components/builder/settings-panel/responsive-helpers.ts +2 -50
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +1 -16
- 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 +2 -70
- package/package.json +2 -2
- 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 +7 -5
- package/components/blocks/SectionRenderer.tsx +0 -171
- package/components/builder/settings-panel/LayoutTab.tsx +0 -346
- package/sanity/schemas/pageSection.ts +0 -157
|
@@ -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
|
-
}
|
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* LayoutTab — Page Section styling (spacing, offset, background, border).
|
|
5
|
-
* Viewport-aware with responsive override support.
|
|
6
|
-
*
|
|
7
|
-
* Session 64: Extracted from SettingsPanel.tsx.
|
|
8
|
-
* Session 65: Split out RowLayoutPresetPicker, TRBLInputs, BlockLayoutTab
|
|
9
|
-
* into separate modules. This file now contains only LayoutTab.
|
|
10
|
-
* Session 158: Added section title icons matching BlockLayoutTab/SectionV2LayoutTab.
|
|
11
|
-
* Reordered sections: Spacing → Offset → Background → Border.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { useBuilderStore } from "../../../lib/builder/store";
|
|
15
|
-
import { resolveEffectiveSpacing } from "../../../lib/builder/layout-styles";
|
|
16
|
-
import type { PageSection } from "../../../lib/sanity/types";
|
|
17
|
-
import {
|
|
18
|
-
SettingsField,
|
|
19
|
-
SettingsSection,
|
|
20
|
-
SELECT_CLASS,
|
|
21
|
-
AssetPathInput,
|
|
22
|
-
} from "../editors/shared";
|
|
23
|
-
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
24
|
-
import { serializeColorField, parseColorField, isGradient } from "../../../lib/color-utils";
|
|
25
|
-
import {
|
|
26
|
-
getRowSettingValue,
|
|
27
|
-
hasRowSettingOverride,
|
|
28
|
-
setRowResponsiveOverride,
|
|
29
|
-
} from "./responsive-helpers";
|
|
30
|
-
import { TRBLInputs } from "./TRBLInputs";
|
|
31
|
-
|
|
32
|
-
// ── Section title icons (centralized colored icons — Session 163) ──
|
|
33
|
-
import {
|
|
34
|
-
SpacingIcon,
|
|
35
|
-
OffsetIcon,
|
|
36
|
-
BackgroundIcon,
|
|
37
|
-
BorderIcon,
|
|
38
|
-
} from "../editors/section-icons";
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* BUG-007 fix: LayoutTab now handles PageSection styling (spacing, background, border).
|
|
42
|
-
* BUG-013 fix: Sections support responsive overrides (tablet/phone).
|
|
43
|
-
*/
|
|
44
|
-
export function LayoutTab({ section, sectionKey }: { section: PageSection; sectionKey: string }) {
|
|
45
|
-
const store = useBuilderStore();
|
|
46
|
-
const paletteSwatches = usePaletteSwatches();
|
|
47
|
-
const settings = section.settings || {};
|
|
48
|
-
const activeViewport = store.activeViewport;
|
|
49
|
-
|
|
50
|
-
// Live preview callbacks (Phase 4)
|
|
51
|
-
const handleBgPreview = (val: import("../../../lib/sanity/types").ColorField) => {
|
|
52
|
-
store.setColorPickerPreview({ sectionKey, field: "background_color", value: val });
|
|
53
|
-
};
|
|
54
|
-
const handleBorderPreview = (val: import("../../../lib/sanity/types").ColorField) => {
|
|
55
|
-
store.setColorPickerPreview({ sectionKey, field: "border_color", value: val });
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const updateSetting = (updates: Partial<NonNullable<PageSection["settings"]>>) => {
|
|
59
|
-
store.updateSectionSettings(sectionKey, updates);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Update a setting, viewport-aware. Supports sections with responsive overrides. */
|
|
63
|
-
const updateSettingResponsive = (property: string, value: unknown) => {
|
|
64
|
-
// BUG-013 fix: Sections now support responsive overrides
|
|
65
|
-
if (activeViewport === "desktop") {
|
|
66
|
-
store.updateSectionSettings(sectionKey, { [property]: value });
|
|
67
|
-
} else {
|
|
68
|
-
// Build responsive override for section
|
|
69
|
-
const existing = section.responsive || {};
|
|
70
|
-
const vp = activeViewport as "tablet" | "phone";
|
|
71
|
-
const vpOverrides = { ...(existing[vp] || {}), [property]: value };
|
|
72
|
-
if (value === undefined) delete (vpOverrides as Record<string, unknown>)[property];
|
|
73
|
-
const responsive = { ...existing, [vp]: vpOverrides };
|
|
74
|
-
if (Object.keys(vpOverrides).length === 0) delete responsive[vp];
|
|
75
|
-
store.updateSectionResponsive(sectionKey, Object.keys(responsive).length ? responsive : undefined);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Resolve effective spacing — shows real values even when legacy enum is active
|
|
80
|
-
const effective = resolveEffectiveSpacing(settings);
|
|
81
|
-
|
|
82
|
-
// Viewport-aware spacing values
|
|
83
|
-
const effectiveSpacingTop = getRowSettingValue<string>(section, activeViewport, "spacing_top", effective.top);
|
|
84
|
-
const effectiveSpacingRight = getRowSettingValue<string>(section, activeViewport, "spacing_right", effective.right);
|
|
85
|
-
const effectiveSpacingBottom = getRowSettingValue<string>(section, activeViewport, "spacing_bottom", effective.bottom);
|
|
86
|
-
const effectiveSpacingLeft = getRowSettingValue<string>(section, activeViewport, "spacing_left", effective.left);
|
|
87
|
-
|
|
88
|
-
// Parse background color + opacity to display
|
|
89
|
-
const bgOpacity = getRowSettingValue<number>(section, activeViewport, "background_opacity", settings.background_opacity ?? 100);
|
|
90
|
-
|
|
91
|
-
const viewportLabel = activeViewport !== "desktop"
|
|
92
|
-
? activeViewport === "tablet" ? "Tablet" : "Phone"
|
|
93
|
-
: null;
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<>
|
|
97
|
-
{viewportLabel && (
|
|
98
|
-
<div className="px-4 pt-3">
|
|
99
|
-
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#076bff]/8 border border-[#076bff]/15">
|
|
100
|
-
<span className="text-[11px] font-medium text-[#076bff]">
|
|
101
|
-
Editing {viewportLabel} overrides
|
|
102
|
-
</span>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
|
|
107
|
-
{/* Spacing (Padding) */}
|
|
108
|
-
<SettingsSection title="Spacing" defaultOpen icon={<SpacingIcon />}>
|
|
109
|
-
<TRBLInputs
|
|
110
|
-
top={effectiveSpacingTop}
|
|
111
|
-
right={effectiveSpacingRight}
|
|
112
|
-
bottom={effectiveSpacingBottom}
|
|
113
|
-
left={effectiveSpacingLeft}
|
|
114
|
-
onChange={(field, value) => {
|
|
115
|
-
if (activeViewport === "desktop") {
|
|
116
|
-
// When user edits TRBL, set explicit TRBL values
|
|
117
|
-
const base = resolveEffectiveSpacing(settings);
|
|
118
|
-
updateSetting({
|
|
119
|
-
spacing_top: field === "top" ? value : (settings.spacing_top ?? base.top),
|
|
120
|
-
spacing_right: field === "right" ? value : (settings.spacing_right ?? base.right),
|
|
121
|
-
spacing_bottom: field === "bottom" ? value : (settings.spacing_bottom ?? base.bottom),
|
|
122
|
-
spacing_left: field === "left" ? value : (settings.spacing_left ?? base.left),
|
|
123
|
-
});
|
|
124
|
-
} else {
|
|
125
|
-
updateSettingResponsive(`spacing_${field}`, value);
|
|
126
|
-
}
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
{activeViewport !== "desktop" && (
|
|
130
|
-
["spacing_top", "spacing_right", "spacing_bottom", "spacing_left"].some(
|
|
131
|
-
(p) => hasRowSettingOverride(section, activeViewport, p)
|
|
132
|
-
) ? (
|
|
133
|
-
<div className="flex items-center gap-2 mt-1">
|
|
134
|
-
<span className="text-[9px] text-[#076bff]">overridden</span>
|
|
135
|
-
<button
|
|
136
|
-
onClick={() => {
|
|
137
|
-
// BUG-021 fix: use proper store action for responsive reset
|
|
138
|
-
let updated = section;
|
|
139
|
-
["spacing_top", "spacing_right", "spacing_bottom", "spacing_left"].forEach((p) => {
|
|
140
|
-
const updates = setRowResponsiveOverride(updated, activeViewport, p, undefined);
|
|
141
|
-
if (updates.responsive !== undefined) {
|
|
142
|
-
updated = { ...updated, responsive: updates.responsive };
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
store.updateSectionResponsive(sectionKey, (updated as PageSection).responsive);
|
|
146
|
-
}}
|
|
147
|
-
className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors"
|
|
148
|
-
>
|
|
149
|
-
Reset
|
|
150
|
-
</button>
|
|
151
|
-
</div>
|
|
152
|
-
) : (
|
|
153
|
-
<p className="text-[9px] text-neutral-300 italic mt-1">inherited</p>
|
|
154
|
-
)
|
|
155
|
-
)}
|
|
156
|
-
</SettingsSection>
|
|
157
|
-
|
|
158
|
-
{/* Offset (Margin) */}
|
|
159
|
-
<SettingsSection title="Offset" icon={<OffsetIcon />}>
|
|
160
|
-
<TRBLInputs
|
|
161
|
-
top={getRowSettingValue<string>(section, activeViewport, "offset_top", "0")}
|
|
162
|
-
right={getRowSettingValue<string>(section, activeViewport, "offset_right", "0")}
|
|
163
|
-
bottom={getRowSettingValue<string>(section, activeViewport, "offset_bottom", "0")}
|
|
164
|
-
left={getRowSettingValue<string>(section, activeViewport, "offset_left", "0")}
|
|
165
|
-
onChange={(field, value) => {
|
|
166
|
-
updateSettingResponsive(`offset_${field}`, value);
|
|
167
|
-
}}
|
|
168
|
-
/>
|
|
169
|
-
</SettingsSection>
|
|
170
|
-
|
|
171
|
-
{/* Background */}
|
|
172
|
-
<SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
|
|
173
|
-
<SettingsField label="Color">
|
|
174
|
-
<ColorSwatchPicker
|
|
175
|
-
value={parseColorField(getRowSettingValue<string>(section, activeViewport, "background_color", ""))}
|
|
176
|
-
onChange={(val) => { store.clearColorPickerPreview(); updateSettingResponsive("background_color", serializeColorField(val)); }}
|
|
177
|
-
swatches={paletteSwatches}
|
|
178
|
-
allowGradients
|
|
179
|
-
onPreview={handleBgPreview}
|
|
180
|
-
/>
|
|
181
|
-
</SettingsField>
|
|
182
|
-
|
|
183
|
-
<SettingsField label="Opacity">
|
|
184
|
-
{(() => {
|
|
185
|
-
const bgIsGrad = isGradient(parseColorField(getRowSettingValue<string>(section, activeViewport, "background_color", "")));
|
|
186
|
-
return (
|
|
187
|
-
<>
|
|
188
|
-
<div className="flex items-center gap-2">
|
|
189
|
-
<input
|
|
190
|
-
type="range"
|
|
191
|
-
min={0}
|
|
192
|
-
max={100}
|
|
193
|
-
value={bgOpacity}
|
|
194
|
-
onChange={(e) => updateSettingResponsive("background_opacity", parseInt(e.target.value))}
|
|
195
|
-
className={`flex-1 accent-[#076bff] ${bgIsGrad ? "opacity-40 pointer-events-none" : ""}`}
|
|
196
|
-
disabled={bgIsGrad}
|
|
197
|
-
/>
|
|
198
|
-
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
199
|
-
{bgOpacity}%
|
|
200
|
-
</span>
|
|
201
|
-
</div>
|
|
202
|
-
{bgIsGrad && (
|
|
203
|
-
<p className="text-[9px] text-neutral-400 italic mt-1">
|
|
204
|
-
Opacity is controlled per stop in gradient mode
|
|
205
|
-
</p>
|
|
206
|
-
)}
|
|
207
|
-
</>
|
|
208
|
-
);
|
|
209
|
-
})()}
|
|
210
|
-
</SettingsField>
|
|
211
|
-
|
|
212
|
-
<SettingsField label="Image">
|
|
213
|
-
<AssetPathInput
|
|
214
|
-
value={getRowSettingValue<string>(section, activeViewport, "background_image", "")}
|
|
215
|
-
onFocus={() => store._pushSnapshot()}
|
|
216
|
-
onChange={(v) => updateSettingResponsive("background_image", v)}
|
|
217
|
-
placeholder="path/to/image.jpg"
|
|
218
|
-
filterType="image"
|
|
219
|
-
/>
|
|
220
|
-
</SettingsField>
|
|
221
|
-
|
|
222
|
-
{getRowSettingValue<string>(section, activeViewport, "background_image", "") && (
|
|
223
|
-
<>
|
|
224
|
-
<SettingsField label="Size">
|
|
225
|
-
<select
|
|
226
|
-
value={getRowSettingValue<string>(section, activeViewport, "background_size", "cover")}
|
|
227
|
-
onChange={(e) => updateSettingResponsive("background_size", e.target.value)}
|
|
228
|
-
className={SELECT_CLASS}
|
|
229
|
-
>
|
|
230
|
-
<option value="cover">Cover</option>
|
|
231
|
-
<option value="contain">Contain</option>
|
|
232
|
-
<option value="auto">Auto</option>
|
|
233
|
-
</select>
|
|
234
|
-
</SettingsField>
|
|
235
|
-
|
|
236
|
-
<SettingsField label="Position">
|
|
237
|
-
<select
|
|
238
|
-
value={getRowSettingValue<string>(section, activeViewport, "background_position", "center center")}
|
|
239
|
-
onFocus={() => store._pushSnapshot()}
|
|
240
|
-
onChange={(e) => updateSettingResponsive("background_position", e.target.value)}
|
|
241
|
-
className={SELECT_CLASS}
|
|
242
|
-
>
|
|
243
|
-
<option value="center center">Center</option>
|
|
244
|
-
<option value="top center">Top</option>
|
|
245
|
-
<option value="bottom center">Bottom</option>
|
|
246
|
-
<option value="left center">Left</option>
|
|
247
|
-
<option value="right center">Right</option>
|
|
248
|
-
<option value="top left">Top Left</option>
|
|
249
|
-
<option value="top right">Top Right</option>
|
|
250
|
-
<option value="bottom left">Bottom Left</option>
|
|
251
|
-
<option value="bottom right">Bottom Right</option>
|
|
252
|
-
</select>
|
|
253
|
-
</SettingsField>
|
|
254
|
-
|
|
255
|
-
<SettingsField label="Repeat">
|
|
256
|
-
<select
|
|
257
|
-
value={getRowSettingValue<string>(section, activeViewport, "background_repeat", "no-repeat")}
|
|
258
|
-
onChange={(e) => updateSettingResponsive("background_repeat", e.target.value)}
|
|
259
|
-
className={SELECT_CLASS}
|
|
260
|
-
>
|
|
261
|
-
<option value="no-repeat">No Repeat</option>
|
|
262
|
-
<option value="repeat">Repeat</option>
|
|
263
|
-
<option value="repeat-x">Repeat X</option>
|
|
264
|
-
<option value="repeat-y">Repeat Y</option>
|
|
265
|
-
</select>
|
|
266
|
-
</SettingsField>
|
|
267
|
-
</>
|
|
268
|
-
)}
|
|
269
|
-
</SettingsSection>
|
|
270
|
-
|
|
271
|
-
{/* Border */}
|
|
272
|
-
<SettingsSection title="Border" icon={<BorderIcon />}>
|
|
273
|
-
<SettingsField label="Color">
|
|
274
|
-
<ColorSwatchPicker
|
|
275
|
-
value={parseColorField(getRowSettingValue<string>(section, activeViewport, "border_color", ""))}
|
|
276
|
-
onChange={(val) => { store.clearColorPickerPreview(); updateSettingResponsive("border_color", serializeColorField(val)); }}
|
|
277
|
-
swatches={paletteSwatches}
|
|
278
|
-
allowGradients
|
|
279
|
-
onPreview={handleBorderPreview}
|
|
280
|
-
/>
|
|
281
|
-
</SettingsField>
|
|
282
|
-
|
|
283
|
-
<SettingsField label="Width">
|
|
284
|
-
<div className="flex items-center gap-2">
|
|
285
|
-
<input
|
|
286
|
-
type="range"
|
|
287
|
-
min={0}
|
|
288
|
-
max={20}
|
|
289
|
-
value={parseInt(getRowSettingValue<string>(section, activeViewport, "border_width", "0"))}
|
|
290
|
-
onChange={(e) => updateSettingResponsive("border_width", e.target.value)}
|
|
291
|
-
className="flex-1 accent-[#076bff]"
|
|
292
|
-
/>
|
|
293
|
-
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
294
|
-
{getRowSettingValue<string>(section, activeViewport, "border_width", "0")}px
|
|
295
|
-
</span>
|
|
296
|
-
</div>
|
|
297
|
-
</SettingsField>
|
|
298
|
-
|
|
299
|
-
<SettingsField label="Style">
|
|
300
|
-
<select
|
|
301
|
-
value={getRowSettingValue<string>(section, activeViewport, "border_style", "none")}
|
|
302
|
-
onChange={(e) => updateSettingResponsive("border_style", e.target.value)}
|
|
303
|
-
className={SELECT_CLASS}
|
|
304
|
-
>
|
|
305
|
-
<option value="none">None</option>
|
|
306
|
-
<option value="solid">Solid</option>
|
|
307
|
-
<option value="dashed">Dashed</option>
|
|
308
|
-
<option value="dotted">Dotted</option>
|
|
309
|
-
</select>
|
|
310
|
-
</SettingsField>
|
|
311
|
-
|
|
312
|
-
<SettingsField label="Sides">
|
|
313
|
-
<select
|
|
314
|
-
value={getRowSettingValue<string>(section, activeViewport, "border_sides", "all")}
|
|
315
|
-
onChange={(e) => updateSettingResponsive("border_sides", e.target.value)}
|
|
316
|
-
className={SELECT_CLASS}
|
|
317
|
-
>
|
|
318
|
-
<option value="all">All</option>
|
|
319
|
-
<option value="top">Top</option>
|
|
320
|
-
<option value="right">Right</option>
|
|
321
|
-
<option value="bottom">Bottom</option>
|
|
322
|
-
<option value="left">Left</option>
|
|
323
|
-
<option value="top-bottom">Top & Bottom</option>
|
|
324
|
-
<option value="left-right">Left & Right</option>
|
|
325
|
-
</select>
|
|
326
|
-
</SettingsField>
|
|
327
|
-
|
|
328
|
-
<SettingsField label="Radius">
|
|
329
|
-
<div className="flex items-center gap-2">
|
|
330
|
-
<input
|
|
331
|
-
type="range"
|
|
332
|
-
min={0}
|
|
333
|
-
max={50}
|
|
334
|
-
value={parseInt(getRowSettingValue<string>(section, activeViewport, "border_radius", "0"))}
|
|
335
|
-
onChange={(e) => updateSettingResponsive("border_radius", e.target.value)}
|
|
336
|
-
className="flex-1 accent-[#076bff]"
|
|
337
|
-
/>
|
|
338
|
-
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
339
|
-
{getRowSettingValue<string>(section, activeViewport, "border_radius", "0")}px
|
|
340
|
-
</span>
|
|
341
|
-
</div>
|
|
342
|
-
</SettingsField>
|
|
343
|
-
</SettingsSection>
|
|
344
|
-
</>
|
|
345
|
-
);
|
|
346
|
-
}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import { defineField, defineType } from "sanity";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* pageSection — First-class page section type.
|
|
5
|
-
*
|
|
6
|
-
* Unlike regular rows (which contain columns → blocks), page sections are
|
|
7
|
-
* direct, flat entities in the content_rows array. Each section wraps a single
|
|
8
|
-
* section-level block (projectGridBlock) with its own
|
|
9
|
-
* layout settings — no row/column matryoshka.
|
|
10
|
-
*
|
|
11
|
-
* Session 76: Refactored from the old "section row" approach where section
|
|
12
|
-
* blocks were wrapped in row → column → block.
|
|
13
|
-
*/
|
|
14
|
-
export default defineType({
|
|
15
|
-
name: "pageSection",
|
|
16
|
-
title: "Page Section",
|
|
17
|
-
type: "object",
|
|
18
|
-
fields: [
|
|
19
|
-
defineField({
|
|
20
|
-
name: "section_type",
|
|
21
|
-
title: "Section Type",
|
|
22
|
-
type: "string",
|
|
23
|
-
options: {
|
|
24
|
-
list: [
|
|
25
|
-
{ title: "Project Grid", value: "projectGrid" },
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
validation: (Rule) => Rule.required(),
|
|
29
|
-
}),
|
|
30
|
-
defineField({
|
|
31
|
-
name: "block",
|
|
32
|
-
title: "Section Content",
|
|
33
|
-
type: "array",
|
|
34
|
-
of: [{ type: "projectGridBlock" }],
|
|
35
|
-
validation: (Rule) => Rule.max(1).required(),
|
|
36
|
-
description: "The section block content (one block per section)",
|
|
37
|
-
}),
|
|
38
|
-
defineField({
|
|
39
|
-
name: "settings",
|
|
40
|
-
title: "Section Settings",
|
|
41
|
-
type: "object",
|
|
42
|
-
fields: [
|
|
43
|
-
// Background
|
|
44
|
-
defineField({ name: "background_color", title: "Background Color", type: "string" }),
|
|
45
|
-
defineField({ name: "background_opacity", title: "Background Opacity", type: "number" }),
|
|
46
|
-
defineField({ name: "background_image", title: "Background Image", type: "string" }),
|
|
47
|
-
defineField({
|
|
48
|
-
name: "background_size",
|
|
49
|
-
title: "Background Size",
|
|
50
|
-
type: "string",
|
|
51
|
-
options: { list: ["cover", "contain", "auto"] },
|
|
52
|
-
}),
|
|
53
|
-
defineField({ name: "background_position", title: "Background Position", type: "string" }),
|
|
54
|
-
defineField({
|
|
55
|
-
name: "background_repeat",
|
|
56
|
-
title: "Background Repeat",
|
|
57
|
-
type: "string",
|
|
58
|
-
options: { list: ["no-repeat", "repeat", "repeat-x", "repeat-y"] },
|
|
59
|
-
}),
|
|
60
|
-
// Spacing (padding TRBL)
|
|
61
|
-
defineField({ name: "spacing_top", title: "Spacing Top", type: "string" }),
|
|
62
|
-
defineField({ name: "spacing_right", title: "Spacing Right", type: "string" }),
|
|
63
|
-
defineField({ name: "spacing_bottom", title: "Spacing Bottom", type: "string" }),
|
|
64
|
-
defineField({ name: "spacing_left", title: "Spacing Left", type: "string" }),
|
|
65
|
-
// Offset (margin TRBL)
|
|
66
|
-
defineField({ name: "offset_top", title: "Offset Top", type: "string" }),
|
|
67
|
-
defineField({ name: "offset_right", title: "Offset Right", type: "string" }),
|
|
68
|
-
defineField({ name: "offset_bottom", title: "Offset Bottom", type: "string" }),
|
|
69
|
-
defineField({ name: "offset_left", title: "Offset Left", type: "string" }),
|
|
70
|
-
// Border
|
|
71
|
-
defineField({ name: "border_color", title: "Border Color", type: "string" }),
|
|
72
|
-
defineField({ name: "border_width", title: "Border Width", type: "string" }),
|
|
73
|
-
defineField({
|
|
74
|
-
name: "border_style",
|
|
75
|
-
title: "Border Style",
|
|
76
|
-
type: "string",
|
|
77
|
-
options: { list: ["none", "solid", "dashed", "dotted"] },
|
|
78
|
-
}),
|
|
79
|
-
defineField({
|
|
80
|
-
name: "border_sides",
|
|
81
|
-
title: "Border Sides",
|
|
82
|
-
type: "string",
|
|
83
|
-
options: { list: ["all", "top", "right", "bottom", "left", "top-bottom", "left-right"] },
|
|
84
|
-
}),
|
|
85
|
-
defineField({ name: "border_radius", title: "Border Radius", type: "string" }),
|
|
86
|
-
// Animation
|
|
87
|
-
defineField({
|
|
88
|
-
name: "enter_animation",
|
|
89
|
-
title: "Enter Animation",
|
|
90
|
-
type: "enterAnimationConfig",
|
|
91
|
-
}),
|
|
92
|
-
],
|
|
93
|
-
}),
|
|
94
|
-
// BUG-013 fix: Per-viewport responsive overrides for section settings
|
|
95
|
-
defineField({
|
|
96
|
-
name: "responsive",
|
|
97
|
-
title: "Responsive Overrides",
|
|
98
|
-
type: "object",
|
|
99
|
-
hidden: true, // Managed by the visual builder
|
|
100
|
-
fields: [
|
|
101
|
-
defineField({
|
|
102
|
-
name: "tablet",
|
|
103
|
-
title: "Tablet",
|
|
104
|
-
type: "object",
|
|
105
|
-
fields: [
|
|
106
|
-
defineField({ name: "background_color", type: "string", title: "Background Color" }),
|
|
107
|
-
defineField({ name: "background_opacity", type: "number", title: "Background Opacity" }),
|
|
108
|
-
defineField({ name: "spacing_top", type: "string", title: "Spacing Top" }),
|
|
109
|
-
defineField({ name: "spacing_right", type: "string", title: "Spacing Right" }),
|
|
110
|
-
defineField({ name: "spacing_bottom", type: "string", title: "Spacing Bottom" }),
|
|
111
|
-
defineField({ name: "spacing_left", type: "string", title: "Spacing Left" }),
|
|
112
|
-
defineField({ name: "offset_top", type: "string", title: "Offset Top" }),
|
|
113
|
-
defineField({ name: "offset_right", type: "string", title: "Offset Right" }),
|
|
114
|
-
defineField({ name: "offset_bottom", type: "string", title: "Offset Bottom" }),
|
|
115
|
-
defineField({ name: "offset_left", type: "string", title: "Offset Left" }),
|
|
116
|
-
defineField({ name: "border_color", type: "string", title: "Border Color" }),
|
|
117
|
-
defineField({ name: "border_width", type: "string", title: "Border Width" }),
|
|
118
|
-
defineField({ name: "border_style", type: "string", title: "Border Style" }),
|
|
119
|
-
defineField({ name: "border_sides", type: "string", title: "Border Sides" }),
|
|
120
|
-
defineField({ name: "border_radius", type: "string", title: "Border Radius" }),
|
|
121
|
-
],
|
|
122
|
-
}),
|
|
123
|
-
defineField({
|
|
124
|
-
name: "phone",
|
|
125
|
-
title: "Phone",
|
|
126
|
-
type: "object",
|
|
127
|
-
fields: [
|
|
128
|
-
defineField({ name: "background_color", type: "string", title: "Background Color" }),
|
|
129
|
-
defineField({ name: "background_opacity", type: "number", title: "Background Opacity" }),
|
|
130
|
-
defineField({ name: "spacing_top", type: "string", title: "Spacing Top" }),
|
|
131
|
-
defineField({ name: "spacing_right", type: "string", title: "Spacing Right" }),
|
|
132
|
-
defineField({ name: "spacing_bottom", type: "string", title: "Spacing Bottom" }),
|
|
133
|
-
defineField({ name: "spacing_left", type: "string", title: "Spacing Left" }),
|
|
134
|
-
defineField({ name: "offset_top", type: "string", title: "Offset Top" }),
|
|
135
|
-
defineField({ name: "offset_right", type: "string", title: "Offset Right" }),
|
|
136
|
-
defineField({ name: "offset_bottom", type: "string", title: "Offset Bottom" }),
|
|
137
|
-
defineField({ name: "offset_left", type: "string", title: "Offset Left" }),
|
|
138
|
-
defineField({ name: "border_color", type: "string", title: "Border Color" }),
|
|
139
|
-
defineField({ name: "border_width", type: "string", title: "Border Width" }),
|
|
140
|
-
defineField({ name: "border_style", type: "string", title: "Border Style" }),
|
|
141
|
-
defineField({ name: "border_sides", type: "string", title: "Border Sides" }),
|
|
142
|
-
defineField({ name: "border_radius", type: "string", title: "Border Radius" }),
|
|
143
|
-
],
|
|
144
|
-
}),
|
|
145
|
-
],
|
|
146
|
-
}),
|
|
147
|
-
],
|
|
148
|
-
preview: {
|
|
149
|
-
select: { section_type: "section_type" },
|
|
150
|
-
prepare({ section_type }) {
|
|
151
|
-
const labels: Record<string, string> = {
|
|
152
|
-
projectGrid: "Project Grid",
|
|
153
|
-
};
|
|
154
|
-
return { title: labels[section_type] || "Section" };
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
});
|