@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.
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
@@ -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
- });