@morphika/andami 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/app/admin/layout.tsx +26 -14
- package/app/admin/pages/[slug]/page.tsx +39 -22
- package/app/admin/pages/page.tsx +13 -8
- package/app/admin/projects/page.tsx +17 -8
- package/app/api/admin/assets/register/route.ts +51 -14
- package/app/api/admin/assets/registry/route.ts +4 -1
- package/app/api/admin/assets/relink/confirm/route.ts +4 -1
- package/app/api/admin/assets/relink/route.ts +4 -1
- package/app/api/admin/assets/scan/route.ts +4 -1
- package/app/api/admin/backups/restore-data/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +4 -1
- package/app/api/admin/r2/delete/route.ts +4 -1
- package/app/api/admin/r2/rename/route.ts +4 -1
- package/app/api/admin/r2/upload-url/route.ts +4 -1
- package/app/api/admin/revalidate/route.ts +4 -1
- package/app/api/admin/storage/switch/route.ts +4 -1
- package/app/api/custom-sections/[id]/route.ts +5 -6
- package/components/admin/PublishToggle.tsx +2 -2
- package/components/admin/nav-builder/NavGridItem.tsx +4 -2
- package/components/admin/nav-builder/NavSettingsFields.tsx +10 -6
- package/components/admin/styles/ColorsEditor.tsx +7 -6
- package/components/admin/styles/FontsEditor.tsx +3 -1
- package/components/blocks/CoverSectionRenderer.tsx +7 -1
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/builder/BubbleIcons.tsx +14 -0
- package/components/builder/CanvasMinimap.tsx +66 -49
- package/components/builder/CanvasToolbar.tsx +31 -41
- package/components/builder/SectionEditorBar.tsx +4 -2
- package/components/builder/SectionTypePicker.tsx +4 -2
- package/components/builder/SectionV2Column.tsx +13 -1
- package/components/builder/SettingsPanel.tsx +21 -17
- package/components/builder/SortableBlock.tsx +2 -2
- package/components/builder/SortableRow.tsx +6 -9
- package/components/builder/VirtualAssetGrid.tsx +8 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +8 -4
- package/components/builder/color-picker/EyedropperButton.tsx +7 -6
- package/components/builder/color-picker/SwatchBar.tsx +11 -6
- package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -2
- package/components/builder/editors/MarqueeBlockEditor.tsx +3 -2
- package/components/builder/editors/ProjectGridEditor.tsx +12 -7
- package/components/builder/editors/SpacerBlockEditor.tsx +25 -23
- package/components/builder/editors/TextBlockEditor.tsx +19 -14
- package/components/builder/editors/shared.tsx +4 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +3 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -1
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
- package/components/builder/live-preview/shared.tsx +5 -2
- package/components/builder/settings-panel/BlockLayoutTab.tsx +4 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
- package/components/builder/settings-panel/CoverSectionSettings.tsx +4 -2
- package/components/builder/settings-panel/SectionV2Settings.tsx +13 -8
- package/components/builder/settings-panel/index.ts +1 -0
- package/lib/builder/serializer/normalizers.ts +14 -0
- package/lib/builder/serializer/serializers.ts +27 -0
- package/lib/builder/store-sections.ts +21 -0
- package/lib/builder/types-slices.ts +14 -0
- package/lib/sanity/queries.ts +48 -0
- package/lib/sanity/types.ts +14 -0
- package/lib/version.ts +1 -1
- package/package.json +7 -5
- package/sanity/schemas/objects/coverSection.ts +32 -0
- package/sanity/schemas/objects/parallaxSlide.ts +32 -0
- package/sanity/schemas/pageSectionV2.ts +32 -0
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useActiveViewport,
|
|
15
15
|
INPUT_CLASS,
|
|
16
16
|
} from "./shared";
|
|
17
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
17
18
|
|
|
18
19
|
interface Props {
|
|
19
20
|
block: SpacerBlock;
|
|
@@ -79,29 +80,30 @@ export default function SpacerBlockEditor({ block }: Props) {
|
|
|
79
80
|
onReset={() => resetOverride("height")}
|
|
80
81
|
>
|
|
81
82
|
<div className="flex gap-1">
|
|
82
|
-
{HEIGHT_PRESETS.map((preset) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
83
|
+
{HEIGHT_PRESETS.map((preset) => {
|
|
84
|
+
const tooltip =
|
|
85
|
+
preset.value === "custom" ? "Custom height" : `${preset.px}px`;
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
key={preset.value}
|
|
89
|
+
onClick={() => updateResponsive("height", preset.value)}
|
|
90
|
+
className={`group/bb relative flex-1 rounded border py-1.5 text-xs transition-colors flex flex-col items-center gap-0.5 ${
|
|
91
|
+
effectiveHeight === preset.value
|
|
92
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
93
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
94
|
+
}`}
|
|
95
|
+
aria-label={tooltip}
|
|
96
|
+
>
|
|
97
|
+
<span>{preset.label}</span>
|
|
98
|
+
{preset.value !== "custom" && (
|
|
99
|
+
<span className="text-[8px] text-neutral-600">
|
|
100
|
+
{preset.px}px
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
<BubbleTooltip>{tooltip}</BubbleTooltip>
|
|
104
|
+
</button>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
105
107
|
</div>
|
|
106
108
|
</ResponsiveField>
|
|
107
109
|
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
AlignRightIcon,
|
|
31
31
|
AlignJustifyIcon,
|
|
32
32
|
} from "./TextAlignmentIcons";
|
|
33
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
33
34
|
|
|
34
35
|
// ============================================
|
|
35
36
|
// Responsive style field — MUST be defined outside the editor component
|
|
@@ -292,20 +293,24 @@ export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
|
292
293
|
|
|
293
294
|
<ResponsiveStyleField label="Align" subProp="alignment" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("alignment")} onReset={resetStyleOverride}>
|
|
294
295
|
<div className="flex gap-0.5 bg-[#f5f5f5] rounded-lg p-0.5">
|
|
295
|
-
{alignments.map(({ value, icon }) =>
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
296
|
+
{alignments.map(({ value, icon }) => {
|
|
297
|
+
const label = value.charAt(0).toUpperCase() + value.slice(1);
|
|
298
|
+
return (
|
|
299
|
+
<button
|
|
300
|
+
key={value}
|
|
301
|
+
onClick={() => updateStyleResponsive("alignment", value)}
|
|
302
|
+
className={`group/bb relative flex-1 flex items-center justify-center py-[5px] rounded-md transition-all ${
|
|
303
|
+
currentAlignment === value
|
|
304
|
+
? "bg-white text-neutral-900 shadow-[0_1px_3px_rgba(0,0,0,0.08)]"
|
|
305
|
+
: "text-neutral-300 hover:text-neutral-500"
|
|
306
|
+
}`}
|
|
307
|
+
aria-label={label}
|
|
308
|
+
>
|
|
309
|
+
{icon}
|
|
310
|
+
<BubbleTooltip>{label}</BubbleTooltip>
|
|
311
|
+
</button>
|
|
312
|
+
);
|
|
313
|
+
})}
|
|
309
314
|
</div>
|
|
310
315
|
</ResponsiveStyleField>
|
|
311
316
|
|
|
@@ -6,6 +6,7 @@ import { hasOverride } from "../../../lib/builder/responsive";
|
|
|
6
6
|
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
7
7
|
import type { ContentBlock } from "../../../lib/sanity/types";
|
|
8
8
|
import AssetBrowser from "../AssetBrowser";
|
|
9
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
9
10
|
|
|
10
11
|
// ============================================
|
|
11
12
|
// Shared CSS classes — Framer-style design system
|
|
@@ -338,10 +339,11 @@ export function AssetPathInput({
|
|
|
338
339
|
<button
|
|
339
340
|
type="button"
|
|
340
341
|
onClick={() => setBrowserOpen(true)}
|
|
341
|
-
className="shrink-0 rounded-lg bg-[#f5f5f5] px-2.5 py-[7px] text-[11px] text-neutral-500 hover:text-neutral-900 hover:bg-[#efefef] transition-colors"
|
|
342
|
-
|
|
342
|
+
className="group/bb relative shrink-0 rounded-lg bg-[#f5f5f5] px-2.5 py-[7px] text-[11px] text-neutral-500 hover:text-neutral-900 hover:bg-[#efefef] transition-colors"
|
|
343
|
+
aria-label="Browse assets"
|
|
343
344
|
>
|
|
344
345
|
Browse
|
|
346
|
+
<BubbleTooltip>Browse assets</BubbleTooltip>
|
|
345
347
|
</button>
|
|
346
348
|
</div>
|
|
347
349
|
<AssetBrowser
|
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
|
|
5
5
|
import { ThumbBadge } from "./shared";
|
|
6
6
|
import type { ImageBlock } from "../../../lib/sanity/types";
|
|
7
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
7
8
|
|
|
8
9
|
export default function LiveImagePreview({ block }: { block: ImageBlock }) {
|
|
9
10
|
const [imgError, setImgError] = useState(false);
|
|
@@ -81,8 +82,9 @@ export default function LiveImagePreview({ block }: { block: ImageBlock }) {
|
|
|
81
82
|
>
|
|
82
83
|
<span className="text-red-400 text-lg">{"\u26A0"}</span>
|
|
83
84
|
<span className="text-red-400 text-xs">Image failed to load</span>
|
|
84
|
-
<span className="text-red-300 text-[10px] max-w-[200px] truncate"
|
|
85
|
+
<span className="group/bb relative text-red-300 text-[10px] max-w-[200px] truncate" aria-label={block.asset_path}>
|
|
85
86
|
{block.asset_path}
|
|
87
|
+
<BubbleTooltip>{block.asset_path}</BubbleTooltip>
|
|
86
88
|
</span>
|
|
87
89
|
</div>
|
|
88
90
|
) : (
|
|
@@ -13,6 +13,7 @@ import { ProjectGridCard } from "./shared";
|
|
|
13
13
|
import { useBuilderStore } from "../../../lib/builder/store";
|
|
14
14
|
import { ADMIN_BLUE, DROP_BLUE, CrossArrowIcon } from "./drag-utils";
|
|
15
15
|
import type { ProjectGridItem } from "../../../lib/sanity/types";
|
|
16
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
16
17
|
|
|
17
18
|
// ─── Props ───────────────────────────────────────────────────────────
|
|
18
19
|
|
|
@@ -221,6 +222,7 @@ export default function ProjectCardWrapper({
|
|
|
221
222
|
<div
|
|
222
223
|
onPointerDown={handleHandleDown}
|
|
223
224
|
onClick={(e) => e.stopPropagation()}
|
|
225
|
+
className="group/bb relative"
|
|
224
226
|
style={{
|
|
225
227
|
width: 48,
|
|
226
228
|
height: 48,
|
|
@@ -233,10 +235,10 @@ export default function ProjectCardWrapper({
|
|
|
233
235
|
cursor: "grab",
|
|
234
236
|
transform: `scale(${invZoom})`,
|
|
235
237
|
}}
|
|
236
|
-
title="Drag to reorder"
|
|
237
238
|
aria-label="Drag to reorder project"
|
|
238
239
|
>
|
|
239
240
|
<CrossArrowIcon size={22} color="#333" />
|
|
241
|
+
<BubbleTooltip>Drag to reorder</BubbleTooltip>
|
|
240
242
|
</div>
|
|
241
243
|
</div>
|
|
242
244
|
|
|
@@ -12,6 +12,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
|
|
|
12
12
|
import { BubbleMenu, type Editor } from "@tiptap/react";
|
|
13
13
|
import { UnifiedColorPicker } from "../color-picker";
|
|
14
14
|
import { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
15
|
+
import { BubbleTooltipAbove } from "../BubbleIcons";
|
|
15
16
|
import type { ColorField } from "../../../lib/sanity/types";
|
|
16
17
|
|
|
17
18
|
// ── Icon components (inline SVGs to avoid external deps) ────────────
|
|
@@ -83,9 +84,9 @@ function ToolbarButton({
|
|
|
83
84
|
onMouseDown={(e) => e.preventDefault()}
|
|
84
85
|
onClick={onClick}
|
|
85
86
|
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") onClick(); }}
|
|
86
|
-
|
|
87
|
+
aria-label={title}
|
|
87
88
|
className={`
|
|
88
|
-
flex items-center justify-center w-7 h-7 rounded cursor-pointer select-none
|
|
89
|
+
group/bb relative flex items-center justify-center w-7 h-7 rounded cursor-pointer select-none
|
|
89
90
|
transition-colors duration-100
|
|
90
91
|
${isActive
|
|
91
92
|
? "bg-white/20 text-white"
|
|
@@ -94,6 +95,7 @@ function ToolbarButton({
|
|
|
94
95
|
`}
|
|
95
96
|
>
|
|
96
97
|
{children}
|
|
98
|
+
<BubbleTooltipAbove>{title}</BubbleTooltipAbove>
|
|
97
99
|
</div>
|
|
98
100
|
);
|
|
99
101
|
}
|
|
@@ -190,11 +192,12 @@ function LinkPopover({
|
|
|
190
192
|
tabIndex={0}
|
|
191
193
|
onClick={handleRemove}
|
|
192
194
|
onKeyDown={(e) => { if (e.key === "Enter") handleRemove(); }}
|
|
193
|
-
|
|
194
|
-
className="p-1.5 text-red-400 hover:text-red-300 hover:bg-red-400/10
|
|
195
|
+
aria-label="Remove link"
|
|
196
|
+
className="group/bb relative p-1.5 text-red-400 hover:text-red-300 hover:bg-red-400/10
|
|
195
197
|
rounded transition-colors cursor-pointer select-none"
|
|
196
198
|
>
|
|
197
199
|
<UnlinkIcon />
|
|
200
|
+
<BubbleTooltipAbove>Remove link</BubbleTooltipAbove>
|
|
198
201
|
</div>
|
|
199
202
|
)}
|
|
200
203
|
</div>
|
|
@@ -401,12 +404,13 @@ export default function RichTextBubbleMenu({ editor }: { editor: Editor }) {
|
|
|
401
404
|
onMouseDown={(e) => e.preventDefault()}
|
|
402
405
|
onClick={handleRemoveColor}
|
|
403
406
|
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleRemoveColor(); }}
|
|
404
|
-
|
|
405
|
-
className="flex items-center justify-center w-5 h-5 rounded cursor-pointer select-none
|
|
407
|
+
aria-label="Remove color"
|
|
408
|
+
className="group/bb relative flex items-center justify-center w-5 h-5 rounded cursor-pointer select-none
|
|
406
409
|
text-neutral-400 hover:text-red-400 hover:bg-red-400/10
|
|
407
410
|
transition-colors duration-100 text-[10px]"
|
|
408
411
|
>
|
|
409
412
|
✕
|
|
413
|
+
<BubbleTooltipAbove>Remove color</BubbleTooltipAbove>
|
|
410
414
|
</div>
|
|
411
415
|
)}
|
|
412
416
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
import { useThumbStatus } from "../../../lib/contexts/ThumbStatusContext";
|
|
5
5
|
import { adminAssetUrl, adminThumbUrl } from "../../../lib/assets";
|
|
6
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
6
7
|
|
|
7
8
|
// ============================================
|
|
8
9
|
// Thumbnail status badge (builder-only)
|
|
@@ -14,10 +15,11 @@ export function ThumbBadge({ assetPath }: { assetPath: string }) {
|
|
|
14
15
|
const status = hasThumb(assetPath);
|
|
15
16
|
// undefined = not raster or unknown — don't show badge
|
|
16
17
|
if (status === undefined) return null;
|
|
18
|
+
const label = status ? "Thumbnail available" : "No thumbnail — full resolution";
|
|
17
19
|
return (
|
|
18
20
|
<span
|
|
19
|
-
|
|
20
|
-
className="absolute bottom-1.5 right-1.5 z-10 flex items-center justify-center"
|
|
21
|
+
aria-label={label}
|
|
22
|
+
className="group/bb absolute bottom-1.5 right-1.5 z-10 flex items-center justify-center"
|
|
21
23
|
style={{
|
|
22
24
|
width: 16,
|
|
23
25
|
height: 16,
|
|
@@ -38,6 +40,7 @@ export function ThumbBadge({ assetPath }: { assetPath: string }) {
|
|
|
38
40
|
<rect x="4.25" y="5" width="1.5" height="3" rx="0.5" fill="white" />
|
|
39
41
|
</svg>
|
|
40
42
|
)}
|
|
43
|
+
<BubbleTooltip>{label}</BubbleTooltip>
|
|
41
44
|
</span>
|
|
42
45
|
);
|
|
43
46
|
}
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
25
25
|
import { serializeColorField, parseColorField, isGradient } from "../../../lib/color-utils";
|
|
26
26
|
import { TRBLInputs } from "./TRBLInputs";
|
|
27
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
27
28
|
import {
|
|
28
29
|
getBlockLayoutValue,
|
|
29
30
|
hasBlockLayoutOverride,
|
|
@@ -129,15 +130,16 @@ function AlignmentButtons<T extends string>({
|
|
|
129
130
|
return (
|
|
130
131
|
<button
|
|
131
132
|
key={opt.value}
|
|
132
|
-
|
|
133
|
+
aria-label={opt.label}
|
|
133
134
|
onClick={() => onChange(opt.value)}
|
|
134
|
-
className={`flex items-center justify-center w-[34px] h-[30px] rounded-md border transition-all ${
|
|
135
|
+
className={`group/bb relative flex items-center justify-center w-[34px] h-[30px] rounded-md border transition-all ${
|
|
135
136
|
isActive
|
|
136
137
|
? "bg-[#3580f9]/10 border-[#3580f9]/30 text-[#3580f9]"
|
|
137
138
|
: "bg-[#f5f5f5] border-transparent text-neutral-400 hover:bg-[#efefef] hover:text-neutral-600"
|
|
138
139
|
}`}
|
|
139
140
|
>
|
|
140
141
|
{renderIcon(opt.value)}
|
|
142
|
+
<BubbleTooltip>{opt.label}</BubbleTooltip>
|
|
141
143
|
</button>
|
|
142
144
|
);
|
|
143
145
|
})}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ColumnV2LayoutTab — Layout tab for a selected V2/cover/parallax-slide column.
|
|
5
|
+
*
|
|
6
|
+
* Desktop-only for now. Viewport-aware column layout would require extending
|
|
7
|
+
* ColumnOverride (currently position-only) — not yet implemented.
|
|
8
|
+
*
|
|
9
|
+
* Sections: Background (color/opacity/image), Border (color/width/style/sides/radius).
|
|
10
|
+
* Columns intentionally have no Spacing or Offset — use the parent section's
|
|
11
|
+
* row_gap/col_gap, or block-level padding.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
15
|
+
import type { PageSectionV2, SectionColumn } from "../../../lib/sanity/types";
|
|
16
|
+
import {
|
|
17
|
+
SettingsField,
|
|
18
|
+
SettingsSection,
|
|
19
|
+
SELECT_CLASS,
|
|
20
|
+
AssetPathInput,
|
|
21
|
+
} from "../editors/shared";
|
|
22
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
23
|
+
import { serializeColorField, parseColorField, isGradient } from "../../../lib/color-utils";
|
|
24
|
+
import { BackgroundIcon, BorderIcon } from "../editors/section-icons";
|
|
25
|
+
|
|
26
|
+
type LayoutField =
|
|
27
|
+
| "background_color" | "background_opacity" | "background_image"
|
|
28
|
+
| "background_size" | "background_position" | "background_repeat"
|
|
29
|
+
| "border_color" | "border_width" | "border_style" | "border_sides" | "border_radius";
|
|
30
|
+
|
|
31
|
+
export function ColumnV2LayoutTab({
|
|
32
|
+
section,
|
|
33
|
+
column,
|
|
34
|
+
}: {
|
|
35
|
+
section: PageSectionV2;
|
|
36
|
+
column: SectionColumn;
|
|
37
|
+
}) {
|
|
38
|
+
const store = useBuilderStore();
|
|
39
|
+
const paletteSwatches = usePaletteSwatches();
|
|
40
|
+
const activeViewport = store.activeViewport;
|
|
41
|
+
|
|
42
|
+
const isResponsive = activeViewport !== "desktop";
|
|
43
|
+
|
|
44
|
+
const update = (field: LayoutField, value: unknown) => {
|
|
45
|
+
store._pushSnapshot();
|
|
46
|
+
store.updateColumnV2Layout(section._key, column._key, {
|
|
47
|
+
[field]: value,
|
|
48
|
+
} as Partial<SectionColumn>);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const getValue = <T,>(field: LayoutField, fallback: T): T => {
|
|
52
|
+
const val = (column as unknown as Record<string, unknown>)[field];
|
|
53
|
+
return (val !== undefined && val !== null ? val : fallback) as T;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleBgPreview = (val: import("../../../lib/sanity/types").ColorField) => {
|
|
57
|
+
store.setColorPickerPreview({ sectionKey: section._key, field: "column_background_color", value: val });
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const bgIsGradient = isGradient(parseColorField(getValue<string>("background_color", "")));
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<>
|
|
64
|
+
{isResponsive && (
|
|
65
|
+
<div className="px-4 pt-3">
|
|
66
|
+
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-100 border border-neutral-200">
|
|
67
|
+
<span className="text-[11px] font-medium text-neutral-500">
|
|
68
|
+
Column layout is desktop-only for now
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Background */}
|
|
75
|
+
<SettingsSection title="Background" defaultOpen icon={<BackgroundIcon />}>
|
|
76
|
+
<SettingsField label="Color">
|
|
77
|
+
<ColorSwatchPicker
|
|
78
|
+
value={parseColorField(getValue<string>("background_color", ""))}
|
|
79
|
+
onChange={(val) => {
|
|
80
|
+
store.clearColorPickerPreview();
|
|
81
|
+
update("background_color", serializeColorField(val));
|
|
82
|
+
}}
|
|
83
|
+
swatches={paletteSwatches}
|
|
84
|
+
allowGradients
|
|
85
|
+
onPreview={handleBgPreview}
|
|
86
|
+
/>
|
|
87
|
+
</SettingsField>
|
|
88
|
+
|
|
89
|
+
<SettingsField label="Opacity">
|
|
90
|
+
<div className="flex items-center gap-2">
|
|
91
|
+
<input
|
|
92
|
+
type="range"
|
|
93
|
+
min={0}
|
|
94
|
+
max={100}
|
|
95
|
+
value={getValue<number>("background_opacity", 100)}
|
|
96
|
+
onChange={(e) => update("background_opacity", parseInt(e.target.value))}
|
|
97
|
+
className={`flex-1 accent-[#3580f9] ${bgIsGradient ? "opacity-40 pointer-events-none" : ""}`}
|
|
98
|
+
disabled={bgIsGradient}
|
|
99
|
+
/>
|
|
100
|
+
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
101
|
+
{getValue<number>("background_opacity", 100)}%
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
{bgIsGradient && (
|
|
105
|
+
<p className="text-[9px] text-neutral-400 italic mt-1">
|
|
106
|
+
Opacity is controlled per stop in gradient mode
|
|
107
|
+
</p>
|
|
108
|
+
)}
|
|
109
|
+
</SettingsField>
|
|
110
|
+
|
|
111
|
+
<SettingsField label="Image">
|
|
112
|
+
<AssetPathInput
|
|
113
|
+
value={getValue<string>("background_image", "")}
|
|
114
|
+
onFocus={() => store._pushSnapshot()}
|
|
115
|
+
onChange={(v) => update("background_image", v)}
|
|
116
|
+
placeholder="path/to/image.jpg"
|
|
117
|
+
filterType="image"
|
|
118
|
+
/>
|
|
119
|
+
</SettingsField>
|
|
120
|
+
|
|
121
|
+
{getValue<string>("background_image", "") && (
|
|
122
|
+
<>
|
|
123
|
+
<SettingsField label="Size">
|
|
124
|
+
<select
|
|
125
|
+
value={getValue<string>("background_size", "cover")}
|
|
126
|
+
onChange={(e) => update("background_size", e.target.value)}
|
|
127
|
+
className={SELECT_CLASS}
|
|
128
|
+
>
|
|
129
|
+
<option value="cover">Cover</option>
|
|
130
|
+
<option value="contain">Contain</option>
|
|
131
|
+
<option value="auto">Auto</option>
|
|
132
|
+
</select>
|
|
133
|
+
</SettingsField>
|
|
134
|
+
|
|
135
|
+
<SettingsField label="Position">
|
|
136
|
+
<select
|
|
137
|
+
value={getValue<string>("background_position", "center center")}
|
|
138
|
+
onChange={(e) => update("background_position", e.target.value)}
|
|
139
|
+
className={SELECT_CLASS}
|
|
140
|
+
>
|
|
141
|
+
<option value="center center">Center</option>
|
|
142
|
+
<option value="top center">Top</option>
|
|
143
|
+
<option value="bottom center">Bottom</option>
|
|
144
|
+
<option value="left center">Left</option>
|
|
145
|
+
<option value="right center">Right</option>
|
|
146
|
+
</select>
|
|
147
|
+
</SettingsField>
|
|
148
|
+
|
|
149
|
+
<SettingsField label="Repeat">
|
|
150
|
+
<select
|
|
151
|
+
value={getValue<string>("background_repeat", "no-repeat")}
|
|
152
|
+
onChange={(e) => update("background_repeat", e.target.value)}
|
|
153
|
+
className={SELECT_CLASS}
|
|
154
|
+
>
|
|
155
|
+
<option value="no-repeat">No Repeat</option>
|
|
156
|
+
<option value="repeat">Repeat</option>
|
|
157
|
+
<option value="repeat-x">Repeat X</option>
|
|
158
|
+
<option value="repeat-y">Repeat Y</option>
|
|
159
|
+
</select>
|
|
160
|
+
</SettingsField>
|
|
161
|
+
</>
|
|
162
|
+
)}
|
|
163
|
+
</SettingsSection>
|
|
164
|
+
|
|
165
|
+
{/* Border */}
|
|
166
|
+
<SettingsSection title="Border" icon={<BorderIcon />}>
|
|
167
|
+
<SettingsField label="Color">
|
|
168
|
+
<ColorSwatchPicker
|
|
169
|
+
value={parseColorField(getValue<string>("border_color", ""))}
|
|
170
|
+
onChange={(val) => {
|
|
171
|
+
store.clearColorPickerPreview();
|
|
172
|
+
update("border_color", serializeColorField(val));
|
|
173
|
+
}}
|
|
174
|
+
swatches={paletteSwatches}
|
|
175
|
+
allowGradients
|
|
176
|
+
/>
|
|
177
|
+
</SettingsField>
|
|
178
|
+
|
|
179
|
+
<SettingsField label="Width">
|
|
180
|
+
<div className="flex items-center gap-2">
|
|
181
|
+
<input
|
|
182
|
+
type="range"
|
|
183
|
+
min={0}
|
|
184
|
+
max={20}
|
|
185
|
+
value={parseInt(getValue<string>("border_width", "0"))}
|
|
186
|
+
onChange={(e) => update("border_width", e.target.value)}
|
|
187
|
+
className="flex-1 accent-[#3580f9]"
|
|
188
|
+
/>
|
|
189
|
+
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
190
|
+
{getValue<string>("border_width", "0")}px
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
</SettingsField>
|
|
194
|
+
|
|
195
|
+
<SettingsField label="Style">
|
|
196
|
+
<select
|
|
197
|
+
value={getValue<string>("border_style", "none")}
|
|
198
|
+
onChange={(e) => update("border_style", e.target.value)}
|
|
199
|
+
className={SELECT_CLASS}
|
|
200
|
+
>
|
|
201
|
+
<option value="none">None</option>
|
|
202
|
+
<option value="solid">Solid</option>
|
|
203
|
+
<option value="dashed">Dashed</option>
|
|
204
|
+
<option value="dotted">Dotted</option>
|
|
205
|
+
</select>
|
|
206
|
+
</SettingsField>
|
|
207
|
+
|
|
208
|
+
<SettingsField label="Sides">
|
|
209
|
+
<select
|
|
210
|
+
value={getValue<string>("border_sides", "all")}
|
|
211
|
+
onChange={(e) => update("border_sides", e.target.value)}
|
|
212
|
+
className={SELECT_CLASS}
|
|
213
|
+
>
|
|
214
|
+
<option value="all">All</option>
|
|
215
|
+
<option value="top">Top</option>
|
|
216
|
+
<option value="right">Right</option>
|
|
217
|
+
<option value="bottom">Bottom</option>
|
|
218
|
+
<option value="left">Left</option>
|
|
219
|
+
<option value="top-bottom">Top & Bottom</option>
|
|
220
|
+
<option value="left-right">Left & Right</option>
|
|
221
|
+
</select>
|
|
222
|
+
</SettingsField>
|
|
223
|
+
|
|
224
|
+
<SettingsField label="Radius">
|
|
225
|
+
<div className="flex items-center gap-2">
|
|
226
|
+
<input
|
|
227
|
+
type="range"
|
|
228
|
+
min={0}
|
|
229
|
+
max={50}
|
|
230
|
+
value={parseInt(getValue<string>("border_radius", "0"))}
|
|
231
|
+
onChange={(e) => update("border_radius", e.target.value)}
|
|
232
|
+
className="flex-1 accent-[#3580f9]"
|
|
233
|
+
/>
|
|
234
|
+
<span className="text-xs text-neutral-900 w-10 text-right">
|
|
235
|
+
{getValue<string>("border_radius", "0")}px
|
|
236
|
+
</span>
|
|
237
|
+
</div>
|
|
238
|
+
</SettingsField>
|
|
239
|
+
</SettingsSection>
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { AssetPathInput } from "../editors/shared";
|
|
35
35
|
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
36
36
|
import StaggerSettings from "../editors/StaggerSettings";
|
|
37
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
37
38
|
|
|
38
39
|
const BG_POSITION_OPTIONS = [
|
|
39
40
|
{ value: "center center", label: "Center" },
|
|
@@ -264,10 +265,11 @@ export default function CoverSectionSettings({ section }: CoverSectionSettingsPr
|
|
|
264
265
|
{section.cover_rows.length > 1 && (
|
|
265
266
|
<button
|
|
266
267
|
onClick={() => store.removeCoverRow(section._key, row._key)}
|
|
267
|
-
className="text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
|
|
268
|
-
|
|
268
|
+
className="group/bb relative text-neutral-300 hover:text-red-500 transition-colors text-xs shrink-0"
|
|
269
|
+
aria-label="Remove row"
|
|
269
270
|
>
|
|
270
271
|
✕
|
|
272
|
+
<BubbleTooltip>Remove row</BubbleTooltip>
|
|
271
273
|
</button>
|
|
272
274
|
)}
|
|
273
275
|
</div>
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
buildStackOverride,
|
|
29
29
|
buildColumnV2Overrides,
|
|
30
30
|
} from "./responsive-helpers";
|
|
31
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
31
32
|
|
|
32
33
|
// ============================================
|
|
33
34
|
// Preset definitions for the picker grid
|
|
@@ -99,14 +100,14 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
99
100
|
<button
|
|
100
101
|
key={preset.id}
|
|
101
102
|
onClick={() => !isCustom && applyPresetV2(section._key, preset.id)}
|
|
102
|
-
className={`flex flex-col items-center gap-1 p-2 rounded-lg border transition-all ${
|
|
103
|
+
className={`group/bb relative flex flex-col items-center gap-1 p-2 rounded-lg border transition-all ${
|
|
103
104
|
isActive
|
|
104
105
|
? "border-[#3580f9] bg-[#3580f9]/5"
|
|
105
106
|
: isCustom
|
|
106
107
|
? "border-neutral-200 bg-neutral-50 opacity-60 cursor-default"
|
|
107
108
|
: "border-neutral-200 bg-white hover:border-neutral-300 hover:bg-neutral-50"
|
|
108
109
|
}`}
|
|
109
|
-
|
|
110
|
+
aria-label={isCustom ? "Layout doesn't match any preset" : preset.label}
|
|
110
111
|
>
|
|
111
112
|
{/* Visual representation */}
|
|
112
113
|
<div className="flex gap-0.5 w-full h-4">
|
|
@@ -133,6 +134,7 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
133
134
|
}`}>
|
|
134
135
|
{preset.label}
|
|
135
136
|
</span>
|
|
137
|
+
<BubbleTooltip>{isCustom ? "Layout doesn't match any preset" : preset.label}</BubbleTooltip>
|
|
136
138
|
</button>
|
|
137
139
|
);
|
|
138
140
|
})}
|
|
@@ -140,8 +142,8 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
140
142
|
{/* + Add Column button */}
|
|
141
143
|
<button
|
|
142
144
|
onClick={handleAddColumn}
|
|
143
|
-
className="flex flex-col items-center gap-1 p-2 rounded-lg border border-dashed border-neutral-300 transition-all hover:border-[#3580f9] hover:bg-[#3580f9]/5 group"
|
|
144
|
-
|
|
145
|
+
className="group/bb relative flex flex-col items-center gap-1 p-2 rounded-lg border border-dashed border-neutral-300 transition-all hover:border-[#3580f9] hover:bg-[#3580f9]/5 group"
|
|
146
|
+
aria-label="Add a column (fills first gap, or adds new row below)"
|
|
145
147
|
>
|
|
146
148
|
<div className="flex items-center justify-center w-full h-4">
|
|
147
149
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" className="text-neutral-400 group-hover:text-[#3580f9] transition-colors">
|
|
@@ -151,6 +153,7 @@ function PresetGrid({ section }: { section: PageSectionV2 }) {
|
|
|
151
153
|
<span className="text-[9px] font-medium text-neutral-400 group-hover:text-[#3580f9] transition-colors">
|
|
152
154
|
Add Col
|
|
153
155
|
</span>
|
|
156
|
+
<BubbleTooltip>Add a column (fills first gap, or adds new row below)</BubbleTooltip>
|
|
154
157
|
</button>
|
|
155
158
|
</div>
|
|
156
159
|
);
|
|
@@ -218,22 +221,24 @@ export default function SectionV2Settings({ section }: { section: PageSectionV2
|
|
|
218
221
|
<div className="flex gap-2">
|
|
219
222
|
<button
|
|
220
223
|
onClick={handleStack}
|
|
221
|
-
className="flex-1 rounded-lg bg-[#3580f9]/8 border border-[#3580f9]/20 py-2 text-xs font-medium text-[#3580f9] hover:bg-[#3580f9]/15 transition-colors"
|
|
222
|
-
|
|
224
|
+
className="group/bb relative flex-1 rounded-lg bg-[#3580f9]/8 border border-[#3580f9]/20 py-2 text-xs font-medium text-[#3580f9] hover:bg-[#3580f9]/15 transition-colors"
|
|
225
|
+
aria-label="Stack all columns vertically (full width, one per row)"
|
|
223
226
|
>
|
|
224
227
|
Stack Columns
|
|
228
|
+
<BubbleTooltip>Stack all columns vertically (full width, one per row)</BubbleTooltip>
|
|
225
229
|
</button>
|
|
226
230
|
<button
|
|
227
231
|
onClick={handleReset}
|
|
228
232
|
disabled={!hasAnyOverrides}
|
|
229
|
-
className={`flex-1 rounded-lg border py-2 text-xs font-medium transition-colors ${
|
|
233
|
+
className={`group/bb relative flex-1 rounded-lg border py-2 text-xs font-medium transition-colors ${
|
|
230
234
|
hasAnyOverrides
|
|
231
235
|
? "bg-neutral-100 border-neutral-200 text-neutral-600 hover:bg-neutral-200"
|
|
232
236
|
: "bg-neutral-50 border-neutral-100 text-neutral-300 cursor-not-allowed"
|
|
233
237
|
}`}
|
|
234
|
-
|
|
238
|
+
aria-label="Reset all responsive overrides for this viewport"
|
|
235
239
|
>
|
|
236
240
|
Reset Overrides
|
|
241
|
+
<BubbleTooltip>Reset all responsive overrides for this viewport</BubbleTooltip>
|
|
237
242
|
</button>
|
|
238
243
|
</div>
|
|
239
244
|
{hasAnyOverrides && (
|
|
@@ -21,5 +21,6 @@ export { useSettingsPanelSelection } from "./useSettingsPanelSelection";
|
|
|
21
21
|
export type { SelectedBlockInfo, SelectedParallaxSlideInfo } from "./useSettingsPanelSelection";
|
|
22
22
|
export { AnimationTab, getBlockHoverEffect } from "./AnimationTab";
|
|
23
23
|
export { ColumnV2AnimationTab } from "./ColumnV2AnimationTab";
|
|
24
|
+
export { ColumnV2LayoutTab } from "./ColumnV2LayoutTab";
|
|
24
25
|
export { CardEntranceSection, ENTRANCE_PRESETS, CARD_ENTRANCE_SELECT_CLASS, CARD_ENTRANCE_SLIDER_CLASS } from "./CardEntranceSection";
|
|
25
26
|
export { CustomSectionSettings } from "./CustomSectionSettings";
|