@morphika/andami 0.5.0 → 0.5.2
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 +151 -36
- package/app/admin/assets/page.tsx +6 -6
- package/app/admin/database/page.tsx +302 -302
- package/app/admin/error.tsx +53 -53
- package/app/admin/layout.tsx +320 -327
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +6 -6
- package/app/admin/pages/page.tsx +11 -11
- package/app/admin/projects/page.tsx +14 -14
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- package/components/admin/MetadataEditor.tsx +6 -6
- package/components/admin/nav-builder/NavBuilder.tsx +1 -1
- package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
- package/components/admin/nav-builder/NavGridCell.tsx +48 -48
- package/components/admin/nav-builder/NavGridItem.tsx +4 -4
- package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
- package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
- package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
- package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
- package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
- package/components/admin/nav-builder/NavSettingsFields.tsx +514 -514
- package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
- package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
- package/components/admin/setup-wizard/DoneStep.tsx +1 -1
- package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
- package/components/admin/setup-wizard/StorageStep.tsx +2 -2
- package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
- package/components/admin/styles/ColorsEditor.tsx +2 -2
- package/components/admin/styles/FontsEditor.tsx +6 -6
- package/components/admin/styles/GridLayoutEditor.tsx +9 -9
- package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
- package/components/admin/styles/TypographyEditor.tsx +6 -6
- package/components/admin/styles/shared.tsx +68 -68
- package/components/blocks/AudioBlockRenderer.tsx +286 -0
- package/components/blocks/BeforeAfterBlockRenderer.tsx +274 -0
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/builder/BlockCardIcons.tsx +316 -227
- package/components/builder/BlockTypePicker.tsx +3 -1
- package/components/builder/BubbleIcons.tsx +90 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +2 -2
- package/components/builder/CoverSectionCanvas.tsx +363 -275
- package/components/builder/DeviceFrame.tsx +1 -1
- package/components/builder/DndWrapper.tsx +3 -3
- package/components/builder/InsertionLines.tsx +1 -1
- package/components/builder/SectionCardIcons.tsx +421 -320
- package/components/builder/SectionEditorBar.tsx +1 -1
- package/components/builder/SectionTypePicker.tsx +4 -4
- package/components/builder/SectionV2Canvas.tsx +20 -4
- package/components/builder/SectionV2Column.tsx +74 -68
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +27 -26
- package/components/builder/VirtualAssetGrid.tsx +2 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +34 -17
- package/components/builder/asset-browser/helpers.ts +4 -0
- package/components/builder/asset-browser/types.ts +2 -1
- package/components/builder/blockStyles.tsx +192 -173
- package/components/builder/color-picker/AlphaSlider.tsx +141 -141
- package/components/builder/color-picker/ColorInputs.tsx +105 -105
- package/components/builder/color-picker/EyedropperButton.tsx +74 -74
- package/components/builder/color-picker/HueSlider.tsx +124 -124
- package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
- package/components/builder/color-picker/SwatchBar.tsx +93 -93
- package/components/builder/editors/AudioBlockEditor.tsx +242 -0
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -0
- package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
- package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
- package/components/builder/editors/HoverEffectPicker.tsx +2 -2
- package/components/builder/editors/ImageBlockEditor.tsx +2 -2
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -4
- package/components/builder/editors/MarqueeBlockEditor.tsx +621 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +9 -9
- package/components/builder/editors/SpacerBlockEditor.tsx +5 -5
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +3 -3
- package/components/builder/editors/TextStylePicker.tsx +1 -1
- package/components/builder/editors/VideoBlockEditor.tsx +2 -2
- package/components/builder/editors/index.ts +11 -10
- package/components/builder/editors/shared.tsx +7 -7
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -0
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +176 -0
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +1 -1
- package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
- package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
- package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +291 -291
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +7 -7
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
- package/components/builder/settings-panel/CoverSectionSettings.tsx +335 -335
- package/components/builder/settings-panel/PageSettings.tsx +3 -3
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
- package/components/builder/settings-panel/SectionV2Settings.tsx +14 -14
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/lib/animation/enter-types.ts +3 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +3 -0
- package/lib/builder/block-registrations.ts +468 -335
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/store-sections.ts +2 -2
- package/lib/builder/types-slices.ts +414 -414
- package/lib/builder/types.ts +6 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/types.ts +156 -1
- package/lib/version.ts +1 -1
- package/package.json +1 -1
- package/sanity/schemas/blocks/audioBlock.ts +69 -0
- package/sanity/schemas/blocks/beforeAfterBlock.ts +121 -0
- package/sanity/schemas/blocks/index.ts +12 -9
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -111
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
4
|
+
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
5
|
+
import type { BeforeAfterBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
6
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
7
|
+
import { resolveColorHex } from "../../../lib/color-utils";
|
|
8
|
+
import {
|
|
9
|
+
SourceIcon,
|
|
10
|
+
LayoutIcon,
|
|
11
|
+
AppearanceIcon,
|
|
12
|
+
PlaybackIcon,
|
|
13
|
+
OptionsIcon,
|
|
14
|
+
} from "./section-icons";
|
|
15
|
+
import {
|
|
16
|
+
SettingsField,
|
|
17
|
+
SettingsSection,
|
|
18
|
+
StyledCheckbox,
|
|
19
|
+
AssetPathInput,
|
|
20
|
+
ViewportBadge,
|
|
21
|
+
ResponsiveField,
|
|
22
|
+
useActiveViewport,
|
|
23
|
+
INPUT_CLASS,
|
|
24
|
+
SELECT_CLASS,
|
|
25
|
+
} from "./shared";
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
block: BeforeAfterBlock;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default function BeforeAfterBlockEditor({ block }: Props) {
|
|
32
|
+
const store = useBuilderStore();
|
|
33
|
+
const viewport = useActiveViewport();
|
|
34
|
+
const paletteSwatches = usePaletteSwatches();
|
|
35
|
+
|
|
36
|
+
const snapshotOnFocus = () => store._pushSnapshot();
|
|
37
|
+
|
|
38
|
+
const updateResponsive = (property: string, value: unknown) => {
|
|
39
|
+
if (viewport === "desktop") {
|
|
40
|
+
store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
41
|
+
} else {
|
|
42
|
+
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
43
|
+
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const resetOverride = (property: string) => {
|
|
48
|
+
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
49
|
+
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const update = (updates: Partial<BeforeAfterBlock>) => {
|
|
53
|
+
store.updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const updateDebounced = (updates: Partial<BeforeAfterBlock>) => {
|
|
57
|
+
store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const beforeType = block.before_media_type || "image";
|
|
61
|
+
const afterType = block.after_media_type || "image";
|
|
62
|
+
|
|
63
|
+
const effectiveWidth = getEffectiveValue<string>(
|
|
64
|
+
block as ContentBlock, viewport, "width", block.width || "full"
|
|
65
|
+
);
|
|
66
|
+
const effectiveAspect = getEffectiveValue<string>(
|
|
67
|
+
block as ContentBlock, viewport, "aspect_ratio", block.aspect_ratio || "16:9"
|
|
68
|
+
);
|
|
69
|
+
const effectiveOrientation = getEffectiveValue<string>(
|
|
70
|
+
block as ContentBlock, viewport, "orientation", block.orientation || "horizontal"
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const eitherIsVideo = beforeType === "video" || afterType === "video";
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<>
|
|
77
|
+
<ViewportBadge />
|
|
78
|
+
|
|
79
|
+
{/* ── Before source ── */}
|
|
80
|
+
<SettingsSection title="Before" defaultOpen icon={<SourceIcon />}>
|
|
81
|
+
<SettingsField label="Media Type">
|
|
82
|
+
<div className="flex gap-1">
|
|
83
|
+
{(
|
|
84
|
+
[
|
|
85
|
+
{ value: "image", label: "Image" },
|
|
86
|
+
{ value: "video", label: "Video" },
|
|
87
|
+
] as const
|
|
88
|
+
).map((opt) => (
|
|
89
|
+
<button
|
|
90
|
+
key={opt.value}
|
|
91
|
+
onClick={() => update({ before_media_type: opt.value })}
|
|
92
|
+
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
93
|
+
beforeType === opt.value
|
|
94
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
95
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
96
|
+
}`}
|
|
97
|
+
>
|
|
98
|
+
{opt.label}
|
|
99
|
+
</button>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</SettingsField>
|
|
103
|
+
|
|
104
|
+
<SettingsField label="Asset Path" hint="Relative path from seed URL">
|
|
105
|
+
<AssetPathInput
|
|
106
|
+
value={block.before_asset_path || ""}
|
|
107
|
+
onFocus={snapshotOnFocus}
|
|
108
|
+
onChange={(v) => updateDebounced({ before_asset_path: v })}
|
|
109
|
+
placeholder={beforeType === "video" ? "projects/slug/before.mp4" : "projects/slug/before.jpg"}
|
|
110
|
+
filterType={beforeType}
|
|
111
|
+
/>
|
|
112
|
+
</SettingsField>
|
|
113
|
+
|
|
114
|
+
<SettingsField label="Alt Text">
|
|
115
|
+
<input
|
|
116
|
+
type="text"
|
|
117
|
+
value={block.before_alt || ""}
|
|
118
|
+
onFocus={snapshotOnFocus}
|
|
119
|
+
onChange={(e) => updateDebounced({ before_alt: e.target.value })}
|
|
120
|
+
className={INPUT_CLASS}
|
|
121
|
+
placeholder="Describe the before media"
|
|
122
|
+
/>
|
|
123
|
+
</SettingsField>
|
|
124
|
+
</SettingsSection>
|
|
125
|
+
|
|
126
|
+
{/* ── After source ── */}
|
|
127
|
+
<SettingsSection title="After" icon={<SourceIcon />}>
|
|
128
|
+
<SettingsField label="Media Type">
|
|
129
|
+
<div className="flex gap-1">
|
|
130
|
+
{(
|
|
131
|
+
[
|
|
132
|
+
{ value: "image", label: "Image" },
|
|
133
|
+
{ value: "video", label: "Video" },
|
|
134
|
+
] as const
|
|
135
|
+
).map((opt) => (
|
|
136
|
+
<button
|
|
137
|
+
key={opt.value}
|
|
138
|
+
onClick={() => update({ after_media_type: opt.value })}
|
|
139
|
+
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
140
|
+
afterType === opt.value
|
|
141
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
142
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
143
|
+
}`}
|
|
144
|
+
>
|
|
145
|
+
{opt.label}
|
|
146
|
+
</button>
|
|
147
|
+
))}
|
|
148
|
+
</div>
|
|
149
|
+
</SettingsField>
|
|
150
|
+
|
|
151
|
+
<SettingsField label="Asset Path" hint="Relative path from seed URL">
|
|
152
|
+
<AssetPathInput
|
|
153
|
+
value={block.after_asset_path || ""}
|
|
154
|
+
onFocus={snapshotOnFocus}
|
|
155
|
+
onChange={(v) => updateDebounced({ after_asset_path: v })}
|
|
156
|
+
placeholder={afterType === "video" ? "projects/slug/after.mp4" : "projects/slug/after.jpg"}
|
|
157
|
+
filterType={afterType}
|
|
158
|
+
/>
|
|
159
|
+
</SettingsField>
|
|
160
|
+
|
|
161
|
+
<SettingsField label="Alt Text">
|
|
162
|
+
<input
|
|
163
|
+
type="text"
|
|
164
|
+
value={block.after_alt || ""}
|
|
165
|
+
onFocus={snapshotOnFocus}
|
|
166
|
+
onChange={(e) => updateDebounced({ after_alt: e.target.value })}
|
|
167
|
+
className={INPUT_CLASS}
|
|
168
|
+
placeholder="Describe the after media"
|
|
169
|
+
/>
|
|
170
|
+
</SettingsField>
|
|
171
|
+
</SettingsSection>
|
|
172
|
+
|
|
173
|
+
{/* ── Slider options ── */}
|
|
174
|
+
<SettingsSection title="Slider" icon={<OptionsIcon />}>
|
|
175
|
+
<ResponsiveField
|
|
176
|
+
label="Orientation"
|
|
177
|
+
block={block as ContentBlock}
|
|
178
|
+
property="orientation"
|
|
179
|
+
onReset={() => resetOverride("orientation")}
|
|
180
|
+
>
|
|
181
|
+
<div className="flex gap-1">
|
|
182
|
+
{(
|
|
183
|
+
[
|
|
184
|
+
{ value: "horizontal", label: "Horizontal" },
|
|
185
|
+
{ value: "vertical", label: "Vertical" },
|
|
186
|
+
] as const
|
|
187
|
+
).map((opt) => (
|
|
188
|
+
<button
|
|
189
|
+
key={opt.value}
|
|
190
|
+
onClick={() => updateResponsive("orientation", opt.value)}
|
|
191
|
+
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
192
|
+
effectiveOrientation === opt.value
|
|
193
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
194
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
195
|
+
}`}
|
|
196
|
+
>
|
|
197
|
+
{opt.label}
|
|
198
|
+
</button>
|
|
199
|
+
))}
|
|
200
|
+
</div>
|
|
201
|
+
</ResponsiveField>
|
|
202
|
+
|
|
203
|
+
<SettingsField label="Initial Position" hint="0–100%">
|
|
204
|
+
<div className="flex items-center gap-1.5">
|
|
205
|
+
<input
|
|
206
|
+
type="number"
|
|
207
|
+
min={0}
|
|
208
|
+
max={100}
|
|
209
|
+
value={block.initial_position ?? 50}
|
|
210
|
+
onFocus={snapshotOnFocus}
|
|
211
|
+
onChange={(e) => {
|
|
212
|
+
const n = Math.max(0, Math.min(100, Number(e.target.value) || 0));
|
|
213
|
+
updateDebounced({ initial_position: n });
|
|
214
|
+
}}
|
|
215
|
+
className={INPUT_CLASS}
|
|
216
|
+
placeholder="50"
|
|
217
|
+
/>
|
|
218
|
+
<span className="text-[10px] text-neutral-400 shrink-0">%</span>
|
|
219
|
+
</div>
|
|
220
|
+
</SettingsField>
|
|
221
|
+
|
|
222
|
+
<SettingsField label="Handle Color">
|
|
223
|
+
<ColorSwatchPicker
|
|
224
|
+
value={block.handle_color || "#FFFFFF"}
|
|
225
|
+
onChange={(value) => {
|
|
226
|
+
const hex = resolveColorHex(value) || "#FFFFFF";
|
|
227
|
+
update({ handle_color: hex });
|
|
228
|
+
}}
|
|
229
|
+
swatches={paletteSwatches}
|
|
230
|
+
/>
|
|
231
|
+
</SettingsField>
|
|
232
|
+
</SettingsSection>
|
|
233
|
+
|
|
234
|
+
{/* ── Layout ── */}
|
|
235
|
+
<SettingsSection title="Layout" icon={<LayoutIcon />}>
|
|
236
|
+
<ResponsiveField
|
|
237
|
+
label="Width"
|
|
238
|
+
block={block as ContentBlock}
|
|
239
|
+
property="width"
|
|
240
|
+
onReset={() => resetOverride("width")}
|
|
241
|
+
>
|
|
242
|
+
<div className="flex gap-1">
|
|
243
|
+
{(
|
|
244
|
+
[
|
|
245
|
+
{ value: "full", label: "Full" },
|
|
246
|
+
{ value: "contained", label: "Contained" },
|
|
247
|
+
{ value: "small", label: "Small" },
|
|
248
|
+
{ value: "fill", label: "Fill" },
|
|
249
|
+
] as const
|
|
250
|
+
).map((opt) => (
|
|
251
|
+
<button
|
|
252
|
+
key={opt.value}
|
|
253
|
+
onClick={() => updateResponsive("width", opt.value)}
|
|
254
|
+
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
255
|
+
effectiveWidth === opt.value
|
|
256
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
257
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
258
|
+
}`}
|
|
259
|
+
>
|
|
260
|
+
{opt.label}
|
|
261
|
+
</button>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</ResponsiveField>
|
|
265
|
+
|
|
266
|
+
<ResponsiveField
|
|
267
|
+
label="Aspect Ratio"
|
|
268
|
+
block={block as ContentBlock}
|
|
269
|
+
property="aspect_ratio"
|
|
270
|
+
onReset={() => resetOverride("aspect_ratio")}
|
|
271
|
+
>
|
|
272
|
+
<select
|
|
273
|
+
value={effectiveAspect}
|
|
274
|
+
onChange={(e) => updateResponsive("aspect_ratio", e.target.value)}
|
|
275
|
+
className={SELECT_CLASS}
|
|
276
|
+
>
|
|
277
|
+
<option value="auto">Auto</option>
|
|
278
|
+
<option value="16:9">16:9</option>
|
|
279
|
+
<option value="4:3">4:3</option>
|
|
280
|
+
<option value="1:1">1:1</option>
|
|
281
|
+
<option value="21:9">21:9</option>
|
|
282
|
+
</select>
|
|
283
|
+
</ResponsiveField>
|
|
284
|
+
</SettingsSection>
|
|
285
|
+
|
|
286
|
+
{/* ── Appearance ── */}
|
|
287
|
+
<SettingsSection title="Appearance" icon={<AppearanceIcon />}>
|
|
288
|
+
<ResponsiveField
|
|
289
|
+
label="Border Radius"
|
|
290
|
+
block={block as ContentBlock}
|
|
291
|
+
property="border_radius"
|
|
292
|
+
onReset={() => resetOverride("border_radius")}
|
|
293
|
+
>
|
|
294
|
+
<div className="flex items-center gap-1.5">
|
|
295
|
+
<input
|
|
296
|
+
type="number"
|
|
297
|
+
value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
298
|
+
onFocus={snapshotOnFocus}
|
|
299
|
+
onChange={(e) => {
|
|
300
|
+
store._pushSnapshot();
|
|
301
|
+
updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
|
|
302
|
+
}}
|
|
303
|
+
className={INPUT_CLASS}
|
|
304
|
+
placeholder="0"
|
|
305
|
+
min={0}
|
|
306
|
+
/>
|
|
307
|
+
<span className="text-[10px] text-neutral-400 shrink-0">px</span>
|
|
308
|
+
</div>
|
|
309
|
+
</ResponsiveField>
|
|
310
|
+
|
|
311
|
+
<ResponsiveField
|
|
312
|
+
label="Shadow"
|
|
313
|
+
block={block as ContentBlock}
|
|
314
|
+
property="shadow"
|
|
315
|
+
onReset={() => resetOverride("shadow")}
|
|
316
|
+
>
|
|
317
|
+
<button
|
|
318
|
+
type="button"
|
|
319
|
+
onClick={() => {
|
|
320
|
+
const effectiveShadow = getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false);
|
|
321
|
+
updateResponsive("shadow", !effectiveShadow);
|
|
322
|
+
}}
|
|
323
|
+
className={`relative w-8 h-[18px] rounded-full transition-colors ${
|
|
324
|
+
getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false) ? "bg-[#3580f9]" : "bg-neutral-200 hover:bg-neutral-300"
|
|
325
|
+
}`}
|
|
326
|
+
>
|
|
327
|
+
<span
|
|
328
|
+
className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow-sm transition-transform ${
|
|
329
|
+
getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false) ? "left-[16px]" : "left-[2px]"
|
|
330
|
+
}`}
|
|
331
|
+
/>
|
|
332
|
+
</button>
|
|
333
|
+
</ResponsiveField>
|
|
334
|
+
</SettingsSection>
|
|
335
|
+
|
|
336
|
+
{/* ── Video playback (only shown when at least one side is video) ── */}
|
|
337
|
+
{eitherIsVideo && (
|
|
338
|
+
<SettingsSection title="Playback" icon={<PlaybackIcon />}>
|
|
339
|
+
<div className="space-y-1.5">
|
|
340
|
+
<StyledCheckbox
|
|
341
|
+
label="Autoplay"
|
|
342
|
+
checked={block.video_autoplay !== false}
|
|
343
|
+
onChange={(checked) => update({ video_autoplay: checked })}
|
|
344
|
+
/>
|
|
345
|
+
<StyledCheckbox
|
|
346
|
+
label="Loop"
|
|
347
|
+
checked={block.video_loop !== false}
|
|
348
|
+
onChange={(checked) => update({ video_loop: checked })}
|
|
349
|
+
/>
|
|
350
|
+
<StyledCheckbox
|
|
351
|
+
label="Muted"
|
|
352
|
+
checked={block.video_muted !== false}
|
|
353
|
+
onChange={(checked) => update({ video_muted: checked })}
|
|
354
|
+
/>
|
|
355
|
+
</div>
|
|
356
|
+
</SettingsSection>
|
|
357
|
+
)}
|
|
358
|
+
</>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
@@ -99,7 +99,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
99
99
|
onClick={() => update({ style: s })}
|
|
100
100
|
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
101
101
|
(block.style || "primary") === s
|
|
102
|
-
? "border-[#
|
|
102
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
103
103
|
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
104
104
|
}`}
|
|
105
105
|
>
|
|
@@ -123,7 +123,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
123
123
|
onClick={() => updateResponsive("size", s)}
|
|
124
124
|
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
125
125
|
effectiveSize === s
|
|
126
|
-
? "border-[#
|
|
126
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
127
127
|
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
128
128
|
}`}
|
|
129
129
|
>
|
|
@@ -146,7 +146,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
146
146
|
onClick={() => updateResponsive("alignment", a)}
|
|
147
147
|
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
148
148
|
effectiveAlignment === a
|
|
149
|
-
? "border-[#
|
|
149
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
150
150
|
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
151
151
|
}`}
|
|
152
152
|
>
|
|
@@ -173,7 +173,7 @@ export default function ButtonBlockEditor({ block }: Props) {
|
|
|
173
173
|
type="button"
|
|
174
174
|
onClick={() => updateResponsive("full_width", !effectiveFullWidth)}
|
|
175
175
|
className={`relative w-8 h-[18px] rounded-full transition-colors ${
|
|
176
|
-
effectiveFullWidth ? "bg-[#
|
|
176
|
+
effectiveFullWidth ? "bg-[#3580f9]" : "bg-neutral-200 hover:bg-neutral-300"
|
|
177
177
|
}`}
|
|
178
178
|
>
|
|
179
179
|
<span
|
|
@@ -27,10 +27,10 @@ import type { ContentBlock } from "../../../lib/sanity/types";
|
|
|
27
27
|
// ── CSS constants ─────────────────────────────────────────────────
|
|
28
28
|
|
|
29
29
|
const SELECT_CLASS =
|
|
30
|
-
"w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#
|
|
30
|
+
"w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]";
|
|
31
31
|
|
|
32
32
|
const SLIDER_CLASS =
|
|
33
|
-
"w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#
|
|
33
|
+
"w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#3580f9] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#3580f9] [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm";
|
|
34
34
|
|
|
35
35
|
// ── Types ─────────────────────────────────────────────────────────
|
|
36
36
|
|
|
@@ -35,10 +35,10 @@ import type { ContentBlock } from "../../../lib/sanity/types";
|
|
|
35
35
|
// ── CSS constants ─────────────────────────────────────────────────
|
|
36
36
|
|
|
37
37
|
const SELECT_CLASS =
|
|
38
|
-
"w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#
|
|
38
|
+
"w-full rounded-lg border border-transparent bg-[#f5f5f5] px-2.5 py-[7px] text-xs text-neutral-900 font-normal outline-none transition-all hover:bg-[#efefef] focus:bg-white focus:border-[#3580f9] focus:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]";
|
|
39
39
|
|
|
40
40
|
const SLIDER_CLASS =
|
|
41
|
-
"w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#
|
|
41
|
+
"w-full h-1.5 rounded-full bg-[#e5e5e5] appearance-none cursor-pointer accent-[#3580f9] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-[#3580f9] [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:shadow-sm";
|
|
42
42
|
|
|
43
43
|
// ── Types ─────────────────────────────────────────────────────────
|
|
44
44
|
|
|
@@ -121,7 +121,7 @@ export default function ImageBlockEditor({ block }: Props) {
|
|
|
121
121
|
onClick={() => updateResponsive("width", opt.value)}
|
|
122
122
|
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
123
123
|
effectiveWidth === opt.value
|
|
124
|
-
? "border-[#
|
|
124
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
125
125
|
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
126
126
|
}`}
|
|
127
127
|
>
|
|
@@ -190,7 +190,7 @@ export default function ImageBlockEditor({ block }: Props) {
|
|
|
190
190
|
updateResponsive("shadow", !effectiveShadow);
|
|
191
191
|
}}
|
|
192
192
|
className={`relative w-8 h-[18px] rounded-full transition-colors ${
|
|
193
|
-
getEffectiveValue<boolean>(block, viewport, "shadow", block.shadow || false) ? "bg-[#
|
|
193
|
+
getEffectiveValue<boolean>(block, viewport, "shadow", block.shadow || false) ? "bg-[#3580f9]" : "bg-neutral-200 hover:bg-neutral-300"
|
|
194
194
|
}`}
|
|
195
195
|
>
|
|
196
196
|
<span
|
|
@@ -187,7 +187,7 @@ export default function ImageGridBlockEditor({ block }: Props) {
|
|
|
187
187
|
{/* Add Images button — opens browser in multi-select mode */}
|
|
188
188
|
<button
|
|
189
189
|
onClick={() => setBrowserOpen(true)}
|
|
190
|
-
className="w-full rounded-lg border border-dashed border-neutral-300 py-3 text-xs text-neutral-500 hover:border-[#
|
|
190
|
+
className="w-full rounded-lg border border-dashed border-neutral-300 py-3 text-xs text-neutral-500 hover:border-[#3580f9] hover:text-neutral-900 transition-colors flex items-center justify-center gap-2"
|
|
191
191
|
>
|
|
192
192
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-neutral-400">
|
|
193
193
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
@@ -317,7 +317,7 @@ export default function ImageGridBlockEditor({ block }: Props) {
|
|
|
317
317
|
}}
|
|
318
318
|
className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
|
|
319
319
|
effectiveLightbox
|
|
320
|
-
? "bg-[#
|
|
320
|
+
? "bg-[#3580f9] text-white"
|
|
321
321
|
: "bg-white text-neutral-500 hover:bg-neutral-50"
|
|
322
322
|
}`}
|
|
323
323
|
>
|
|
@@ -330,7 +330,7 @@ export default function ImageGridBlockEditor({ block }: Props) {
|
|
|
330
330
|
}}
|
|
331
331
|
className={`flex-1 py-1.5 text-xs font-medium transition-colors ${
|
|
332
332
|
!effectiveLightbox
|
|
333
|
-
? "bg-[#
|
|
333
|
+
? "bg-[#3580f9] text-white"
|
|
334
334
|
: "bg-white text-neutral-500 hover:bg-neutral-50"
|
|
335
335
|
}`}
|
|
336
336
|
>
|
|
@@ -355,7 +355,7 @@ export default function ImageGridBlockEditor({ block }: Props) {
|
|
|
355
355
|
onClick={() => updateResponsive("object_fit", f)}
|
|
356
356
|
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
357
357
|
effectiveObjectFit === f
|
|
358
|
-
? "border-[#
|
|
358
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
359
359
|
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
360
360
|
}`}
|
|
361
361
|
>
|