@morphika/andami 0.5.1 → 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/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 +332 -320
- package/app/admin/navigation/page.tsx +255 -255
- package/app/admin/pages/[slug]/page.tsx +44 -27
- package/app/admin/pages/page.tsx +24 -19
- package/app/admin/projects/page.tsx +30 -21
- package/app/admin/setup/page.tsx +1 -1
- package/app/admin/styles/page.tsx +1 -1
- 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/MetadataEditor.tsx +6 -6
- package/components/admin/PublishToggle.tsx +2 -2
- 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 +8 -6
- 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 +518 -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 +9 -8
- package/components/admin/styles/FontsEditor.tsx +9 -7
- 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 -286
- package/components/blocks/CoverSectionRenderer.tsx +7 -1
- package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
- package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/builder/BlockCardIcons.tsx +316 -316
- package/components/builder/BlockTypePicker.tsx +1 -1
- package/components/builder/BubbleIcons.tsx +104 -0
- package/components/builder/BuilderCanvas.tsx +2 -0
- package/components/builder/CanvasMinimap.tsx +66 -49
- package/components/builder/CanvasToolbar.tsx +31 -41
- package/components/builder/CoverSectionCanvas.tsx +363 -363
- 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 +5 -3
- package/components/builder/SectionTypePicker.tsx +7 -5
- package/components/builder/SectionV2Canvas.tsx +1 -1
- package/components/builder/SectionV2Column.tsx +82 -68
- package/components/builder/SettingsPanel.tsx +21 -17
- package/components/builder/SortableBlock.tsx +93 -73
- package/components/builder/SortableRow.tsx +33 -35
- package/components/builder/VirtualAssetGrid.tsx +10 -4
- package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
- package/components/builder/blockStyles.tsx +192 -185
- 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 +75 -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 +98 -93
- package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
- package/components/builder/editors/AudioBlockEditor.tsx +242 -242
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
- 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 +8 -6
- package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
- package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
- package/components/builder/editors/ProjectGridEditor.tsx +21 -16
- package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
- package/components/builder/editors/StaggerSettings.tsx +109 -109
- package/components/builder/editors/TextBlockEditor.tsx +22 -17
- 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 +10 -8
- package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
- package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
- package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
- 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 +293 -291
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
- package/components/builder/live-preview/shared.tsx +5 -2
- package/components/builder/settings-panel/AnimationTab.tsx +138 -138
- package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
- package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
- 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 +337 -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 +25 -20
- package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
- package/components/builder/settings-panel/index.ts +1 -0
- package/lib/animation/enter-types.ts +1 -0
- package/lib/animation/hover-effect-presets.ts +210 -210
- package/lib/animation/hover-effect-types.ts +1 -0
- package/lib/builder/block-registrations.ts +468 -417
- package/lib/builder/constants.ts +111 -111
- package/lib/builder/serializer/normalizers.ts +14 -0
- package/lib/builder/serializer/serializers.ts +27 -0
- package/lib/builder/store-sections.ts +23 -2
- package/lib/builder/types-slices.ts +428 -414
- package/lib/builder/types.ts +4 -1
- package/lib/config/index.ts +27 -27
- package/lib/sanity/queries.ts +48 -0
- package/lib/sanity/types.ts +112 -1
- package/lib/version.ts +1 -1
- package/package.json +7 -5
- package/sanity/schemas/blocks/audioBlock.ts +69 -69
- package/sanity/schemas/blocks/index.ts +12 -11
- package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
- package/sanity/schemas/index.ts +120 -117
- package/sanity/schemas/objects/coverSection.ts +32 -0
- package/sanity/schemas/objects/parallaxSlide.ts +32 -0
- package/sanity/schemas/pageSectionV2.ts +32 -0
- package/styles/admin.css +85 -85
- package/styles/animations.css +237 -237
- package/styles/base.css +114 -114
|
@@ -1,242 +1,242 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useBuilderStore } from "../../../lib/builder/store";
|
|
4
|
-
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
5
|
-
import type { AudioBlock, 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
|
-
} from "./shared";
|
|
25
|
-
|
|
26
|
-
interface Props {
|
|
27
|
-
block: AudioBlock;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export default function AudioBlockEditor({ block }: Props) {
|
|
31
|
-
const store = useBuilderStore();
|
|
32
|
-
const viewport = useActiveViewport();
|
|
33
|
-
const paletteSwatches = usePaletteSwatches();
|
|
34
|
-
|
|
35
|
-
const snapshotOnFocus = () => store._pushSnapshot();
|
|
36
|
-
|
|
37
|
-
const updateResponsive = (property: string, value: unknown) => {
|
|
38
|
-
if (viewport === "desktop") {
|
|
39
|
-
store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
40
|
-
} else {
|
|
41
|
-
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
42
|
-
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const resetOverride = (property: string) => {
|
|
47
|
-
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
48
|
-
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const update = (updates: Partial<AudioBlock>) => {
|
|
52
|
-
store.updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const updateDebounced = (updates: Partial<AudioBlock>) => {
|
|
56
|
-
store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const effectiveWidth = getEffectiveValue<string>(
|
|
60
|
-
block as ContentBlock, viewport, "width", block.width || "contained"
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
return (
|
|
64
|
-
<>
|
|
65
|
-
<ViewportBadge />
|
|
66
|
-
|
|
67
|
-
{/* ── Source ── */}
|
|
68
|
-
<SettingsSection title="Audio" defaultOpen icon={<SourceIcon />}>
|
|
69
|
-
<SettingsField label="Audio File" hint="mp3, wav, ogg, m4a, aac, flac">
|
|
70
|
-
<AssetPathInput
|
|
71
|
-
value={block.asset_path || ""}
|
|
72
|
-
onFocus={snapshotOnFocus}
|
|
73
|
-
onChange={(v) => updateDebounced({ asset_path: v })}
|
|
74
|
-
placeholder="projects/slug/track.mp3"
|
|
75
|
-
filterType="audio"
|
|
76
|
-
/>
|
|
77
|
-
</SettingsField>
|
|
78
|
-
|
|
79
|
-
<SettingsField label="Alt Text">
|
|
80
|
-
<input
|
|
81
|
-
type="text"
|
|
82
|
-
value={block.alt || ""}
|
|
83
|
-
onFocus={snapshotOnFocus}
|
|
84
|
-
onChange={(e) => updateDebounced({ alt: e.target.value })}
|
|
85
|
-
className={INPUT_CLASS}
|
|
86
|
-
placeholder="Describe the audio for accessibility"
|
|
87
|
-
/>
|
|
88
|
-
</SettingsField>
|
|
89
|
-
</SettingsSection>
|
|
90
|
-
|
|
91
|
-
{/* ── Metadata ── */}
|
|
92
|
-
<SettingsSection title="Metadata" icon={<OptionsIcon />}>
|
|
93
|
-
<SettingsField label="Title">
|
|
94
|
-
<input
|
|
95
|
-
type="text"
|
|
96
|
-
value={block.title || ""}
|
|
97
|
-
onFocus={snapshotOnFocus}
|
|
98
|
-
onChange={(e) => updateDebounced({ title: e.target.value })}
|
|
99
|
-
className={INPUT_CLASS}
|
|
100
|
-
placeholder="Track title"
|
|
101
|
-
/>
|
|
102
|
-
</SettingsField>
|
|
103
|
-
|
|
104
|
-
<SettingsField label="Artist">
|
|
105
|
-
<input
|
|
106
|
-
type="text"
|
|
107
|
-
value={block.artist || ""}
|
|
108
|
-
onFocus={snapshotOnFocus}
|
|
109
|
-
onChange={(e) => updateDebounced({ artist: e.target.value })}
|
|
110
|
-
className={INPUT_CLASS}
|
|
111
|
-
placeholder="Artist name"
|
|
112
|
-
/>
|
|
113
|
-
</SettingsField>
|
|
114
|
-
|
|
115
|
-
<SettingsField label="Cover Art" hint="Optional relative path to an image">
|
|
116
|
-
<AssetPathInput
|
|
117
|
-
value={block.cover_path || ""}
|
|
118
|
-
onFocus={snapshotOnFocus}
|
|
119
|
-
onChange={(v) => updateDebounced({ cover_path: v })}
|
|
120
|
-
placeholder="projects/slug/cover.jpg"
|
|
121
|
-
filterType="image"
|
|
122
|
-
/>
|
|
123
|
-
</SettingsField>
|
|
124
|
-
</SettingsSection>
|
|
125
|
-
|
|
126
|
-
{/* ── Layout ── */}
|
|
127
|
-
<SettingsSection title="Layout" icon={<LayoutIcon />}>
|
|
128
|
-
<ResponsiveField
|
|
129
|
-
label="Width"
|
|
130
|
-
block={block as ContentBlock}
|
|
131
|
-
property="width"
|
|
132
|
-
onReset={() => resetOverride("width")}
|
|
133
|
-
>
|
|
134
|
-
<div className="flex gap-1">
|
|
135
|
-
{(
|
|
136
|
-
[
|
|
137
|
-
{ value: "full", label: "Full" },
|
|
138
|
-
{ value: "contained", label: "Contained" },
|
|
139
|
-
{ value: "small", label: "Small" },
|
|
140
|
-
{ value: "fill", label: "Fill" },
|
|
141
|
-
] as const
|
|
142
|
-
).map((opt) => (
|
|
143
|
-
<button
|
|
144
|
-
key={opt.value}
|
|
145
|
-
onClick={() => updateResponsive("width", opt.value)}
|
|
146
|
-
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
147
|
-
effectiveWidth === opt.value
|
|
148
|
-
? "border-[#
|
|
149
|
-
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
150
|
-
}`}
|
|
151
|
-
>
|
|
152
|
-
{opt.label}
|
|
153
|
-
</button>
|
|
154
|
-
))}
|
|
155
|
-
</div>
|
|
156
|
-
</ResponsiveField>
|
|
157
|
-
</SettingsSection>
|
|
158
|
-
|
|
159
|
-
{/* ── Appearance ── */}
|
|
160
|
-
<SettingsSection title="Appearance" icon={<AppearanceIcon />}>
|
|
161
|
-
<SettingsField label="Accent Color" hint="Play button + progress fill">
|
|
162
|
-
<ColorSwatchPicker
|
|
163
|
-
value={block.accent_color || "#
|
|
164
|
-
onChange={(value) => {
|
|
165
|
-
const hex = resolveColorHex(value) || "#
|
|
166
|
-
update({ accent_color: hex });
|
|
167
|
-
}}
|
|
168
|
-
swatches={paletteSwatches}
|
|
169
|
-
/>
|
|
170
|
-
</SettingsField>
|
|
171
|
-
|
|
172
|
-
<ResponsiveField
|
|
173
|
-
label="Border Radius"
|
|
174
|
-
block={block as ContentBlock}
|
|
175
|
-
property="border_radius"
|
|
176
|
-
onReset={() => resetOverride("border_radius")}
|
|
177
|
-
>
|
|
178
|
-
<div className="flex items-center gap-1.5">
|
|
179
|
-
<input
|
|
180
|
-
type="number"
|
|
181
|
-
value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
182
|
-
onFocus={snapshotOnFocus}
|
|
183
|
-
onChange={(e) => {
|
|
184
|
-
store._pushSnapshot();
|
|
185
|
-
updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
|
|
186
|
-
}}
|
|
187
|
-
className={INPUT_CLASS}
|
|
188
|
-
placeholder="12"
|
|
189
|
-
min={0}
|
|
190
|
-
/>
|
|
191
|
-
<span className="text-[10px] text-neutral-400 shrink-0">px</span>
|
|
192
|
-
</div>
|
|
193
|
-
</ResponsiveField>
|
|
194
|
-
|
|
195
|
-
<ResponsiveField
|
|
196
|
-
label="Shadow"
|
|
197
|
-
block={block as ContentBlock}
|
|
198
|
-
property="shadow"
|
|
199
|
-
onReset={() => resetOverride("shadow")}
|
|
200
|
-
>
|
|
201
|
-
<button
|
|
202
|
-
type="button"
|
|
203
|
-
onClick={() => {
|
|
204
|
-
const effectiveShadow = getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false);
|
|
205
|
-
updateResponsive("shadow", !effectiveShadow);
|
|
206
|
-
}}
|
|
207
|
-
className={`relative w-8 h-[18px] rounded-full transition-colors ${
|
|
208
|
-
getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false) ? "bg-[#
|
|
209
|
-
}`}
|
|
210
|
-
>
|
|
211
|
-
<span
|
|
212
|
-
className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow-sm transition-transform ${
|
|
213
|
-
getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false) ? "left-[16px]" : "left-[2px]"
|
|
214
|
-
}`}
|
|
215
|
-
/>
|
|
216
|
-
</button>
|
|
217
|
-
</ResponsiveField>
|
|
218
|
-
</SettingsSection>
|
|
219
|
-
|
|
220
|
-
{/* ── Playback ── */}
|
|
221
|
-
<SettingsSection title="Playback" icon={<PlaybackIcon />}>
|
|
222
|
-
<div className="space-y-1.5">
|
|
223
|
-
<StyledCheckbox
|
|
224
|
-
label="Autoplay"
|
|
225
|
-
checked={block.autoplay === true}
|
|
226
|
-
onChange={(checked) => update({ autoplay: checked })}
|
|
227
|
-
/>
|
|
228
|
-
<StyledCheckbox
|
|
229
|
-
label="Loop"
|
|
230
|
-
checked={block.loop === true}
|
|
231
|
-
onChange={(checked) => update({ loop: checked })}
|
|
232
|
-
/>
|
|
233
|
-
<StyledCheckbox
|
|
234
|
-
label="Muted"
|
|
235
|
-
checked={block.muted === true}
|
|
236
|
-
onChange={(checked) => update({ muted: checked })}
|
|
237
|
-
/>
|
|
238
|
-
</div>
|
|
239
|
-
</SettingsSection>
|
|
240
|
-
</>
|
|
241
|
-
);
|
|
242
|
-
}
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
4
|
+
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
5
|
+
import type { AudioBlock, 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
|
+
} from "./shared";
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
block: AudioBlock;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function AudioBlockEditor({ block }: Props) {
|
|
31
|
+
const store = useBuilderStore();
|
|
32
|
+
const viewport = useActiveViewport();
|
|
33
|
+
const paletteSwatches = usePaletteSwatches();
|
|
34
|
+
|
|
35
|
+
const snapshotOnFocus = () => store._pushSnapshot();
|
|
36
|
+
|
|
37
|
+
const updateResponsive = (property: string, value: unknown) => {
|
|
38
|
+
if (viewport === "desktop") {
|
|
39
|
+
store.updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
40
|
+
} else {
|
|
41
|
+
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
42
|
+
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const resetOverride = (property: string) => {
|
|
47
|
+
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
48
|
+
store.updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const update = (updates: Partial<AudioBlock>) => {
|
|
52
|
+
store.updateBlock(block._key, updates as Partial<ContentBlock>);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const updateDebounced = (updates: Partial<AudioBlock>) => {
|
|
56
|
+
store.updateBlockDebounced(block._key, updates as Partial<ContentBlock>);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const effectiveWidth = getEffectiveValue<string>(
|
|
60
|
+
block as ContentBlock, viewport, "width", block.width || "contained"
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<ViewportBadge />
|
|
66
|
+
|
|
67
|
+
{/* ── Source ── */}
|
|
68
|
+
<SettingsSection title="Audio" defaultOpen icon={<SourceIcon />}>
|
|
69
|
+
<SettingsField label="Audio File" hint="mp3, wav, ogg, m4a, aac, flac">
|
|
70
|
+
<AssetPathInput
|
|
71
|
+
value={block.asset_path || ""}
|
|
72
|
+
onFocus={snapshotOnFocus}
|
|
73
|
+
onChange={(v) => updateDebounced({ asset_path: v })}
|
|
74
|
+
placeholder="projects/slug/track.mp3"
|
|
75
|
+
filterType="audio"
|
|
76
|
+
/>
|
|
77
|
+
</SettingsField>
|
|
78
|
+
|
|
79
|
+
<SettingsField label="Alt Text">
|
|
80
|
+
<input
|
|
81
|
+
type="text"
|
|
82
|
+
value={block.alt || ""}
|
|
83
|
+
onFocus={snapshotOnFocus}
|
|
84
|
+
onChange={(e) => updateDebounced({ alt: e.target.value })}
|
|
85
|
+
className={INPUT_CLASS}
|
|
86
|
+
placeholder="Describe the audio for accessibility"
|
|
87
|
+
/>
|
|
88
|
+
</SettingsField>
|
|
89
|
+
</SettingsSection>
|
|
90
|
+
|
|
91
|
+
{/* ── Metadata ── */}
|
|
92
|
+
<SettingsSection title="Metadata" icon={<OptionsIcon />}>
|
|
93
|
+
<SettingsField label="Title">
|
|
94
|
+
<input
|
|
95
|
+
type="text"
|
|
96
|
+
value={block.title || ""}
|
|
97
|
+
onFocus={snapshotOnFocus}
|
|
98
|
+
onChange={(e) => updateDebounced({ title: e.target.value })}
|
|
99
|
+
className={INPUT_CLASS}
|
|
100
|
+
placeholder="Track title"
|
|
101
|
+
/>
|
|
102
|
+
</SettingsField>
|
|
103
|
+
|
|
104
|
+
<SettingsField label="Artist">
|
|
105
|
+
<input
|
|
106
|
+
type="text"
|
|
107
|
+
value={block.artist || ""}
|
|
108
|
+
onFocus={snapshotOnFocus}
|
|
109
|
+
onChange={(e) => updateDebounced({ artist: e.target.value })}
|
|
110
|
+
className={INPUT_CLASS}
|
|
111
|
+
placeholder="Artist name"
|
|
112
|
+
/>
|
|
113
|
+
</SettingsField>
|
|
114
|
+
|
|
115
|
+
<SettingsField label="Cover Art" hint="Optional relative path to an image">
|
|
116
|
+
<AssetPathInput
|
|
117
|
+
value={block.cover_path || ""}
|
|
118
|
+
onFocus={snapshotOnFocus}
|
|
119
|
+
onChange={(v) => updateDebounced({ cover_path: v })}
|
|
120
|
+
placeholder="projects/slug/cover.jpg"
|
|
121
|
+
filterType="image"
|
|
122
|
+
/>
|
|
123
|
+
</SettingsField>
|
|
124
|
+
</SettingsSection>
|
|
125
|
+
|
|
126
|
+
{/* ── Layout ── */}
|
|
127
|
+
<SettingsSection title="Layout" icon={<LayoutIcon />}>
|
|
128
|
+
<ResponsiveField
|
|
129
|
+
label="Width"
|
|
130
|
+
block={block as ContentBlock}
|
|
131
|
+
property="width"
|
|
132
|
+
onReset={() => resetOverride("width")}
|
|
133
|
+
>
|
|
134
|
+
<div className="flex gap-1">
|
|
135
|
+
{(
|
|
136
|
+
[
|
|
137
|
+
{ value: "full", label: "Full" },
|
|
138
|
+
{ value: "contained", label: "Contained" },
|
|
139
|
+
{ value: "small", label: "Small" },
|
|
140
|
+
{ value: "fill", label: "Fill" },
|
|
141
|
+
] as const
|
|
142
|
+
).map((opt) => (
|
|
143
|
+
<button
|
|
144
|
+
key={opt.value}
|
|
145
|
+
onClick={() => updateResponsive("width", opt.value)}
|
|
146
|
+
className={`flex-1 rounded border py-1 text-xs transition-colors ${
|
|
147
|
+
effectiveWidth === opt.value
|
|
148
|
+
? "border-[#3580f9] bg-[#3580f9]/20 text-neutral-900"
|
|
149
|
+
: "border-neutral-200 bg-white text-neutral-500 hover:border-neutral-600"
|
|
150
|
+
}`}
|
|
151
|
+
>
|
|
152
|
+
{opt.label}
|
|
153
|
+
</button>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
</ResponsiveField>
|
|
157
|
+
</SettingsSection>
|
|
158
|
+
|
|
159
|
+
{/* ── Appearance ── */}
|
|
160
|
+
<SettingsSection title="Appearance" icon={<AppearanceIcon />}>
|
|
161
|
+
<SettingsField label="Accent Color" hint="Play button + progress fill">
|
|
162
|
+
<ColorSwatchPicker
|
|
163
|
+
value={block.accent_color || "#3580f9"}
|
|
164
|
+
onChange={(value) => {
|
|
165
|
+
const hex = resolveColorHex(value) || "#3580f9";
|
|
166
|
+
update({ accent_color: hex });
|
|
167
|
+
}}
|
|
168
|
+
swatches={paletteSwatches}
|
|
169
|
+
/>
|
|
170
|
+
</SettingsField>
|
|
171
|
+
|
|
172
|
+
<ResponsiveField
|
|
173
|
+
label="Border Radius"
|
|
174
|
+
block={block as ContentBlock}
|
|
175
|
+
property="border_radius"
|
|
176
|
+
onReset={() => resetOverride("border_radius")}
|
|
177
|
+
>
|
|
178
|
+
<div className="flex items-center gap-1.5">
|
|
179
|
+
<input
|
|
180
|
+
type="number"
|
|
181
|
+
value={String(getEffectiveValue<string>(block as ContentBlock, viewport, "border_radius", block.border_radius || "")).replace(/px$/i, "")}
|
|
182
|
+
onFocus={snapshotOnFocus}
|
|
183
|
+
onChange={(e) => {
|
|
184
|
+
store._pushSnapshot();
|
|
185
|
+
updateResponsive("border_radius", e.target.value.replace(/[^0-9]/g, ""));
|
|
186
|
+
}}
|
|
187
|
+
className={INPUT_CLASS}
|
|
188
|
+
placeholder="12"
|
|
189
|
+
min={0}
|
|
190
|
+
/>
|
|
191
|
+
<span className="text-[10px] text-neutral-400 shrink-0">px</span>
|
|
192
|
+
</div>
|
|
193
|
+
</ResponsiveField>
|
|
194
|
+
|
|
195
|
+
<ResponsiveField
|
|
196
|
+
label="Shadow"
|
|
197
|
+
block={block as ContentBlock}
|
|
198
|
+
property="shadow"
|
|
199
|
+
onReset={() => resetOverride("shadow")}
|
|
200
|
+
>
|
|
201
|
+
<button
|
|
202
|
+
type="button"
|
|
203
|
+
onClick={() => {
|
|
204
|
+
const effectiveShadow = getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false);
|
|
205
|
+
updateResponsive("shadow", !effectiveShadow);
|
|
206
|
+
}}
|
|
207
|
+
className={`relative w-8 h-[18px] rounded-full transition-colors ${
|
|
208
|
+
getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false) ? "bg-[#3580f9]" : "bg-neutral-200 hover:bg-neutral-300"
|
|
209
|
+
}`}
|
|
210
|
+
>
|
|
211
|
+
<span
|
|
212
|
+
className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow-sm transition-transform ${
|
|
213
|
+
getEffectiveValue<boolean>(block as ContentBlock, viewport, "shadow", block.shadow || false) ? "left-[16px]" : "left-[2px]"
|
|
214
|
+
}`}
|
|
215
|
+
/>
|
|
216
|
+
</button>
|
|
217
|
+
</ResponsiveField>
|
|
218
|
+
</SettingsSection>
|
|
219
|
+
|
|
220
|
+
{/* ── Playback ── */}
|
|
221
|
+
<SettingsSection title="Playback" icon={<PlaybackIcon />}>
|
|
222
|
+
<div className="space-y-1.5">
|
|
223
|
+
<StyledCheckbox
|
|
224
|
+
label="Autoplay"
|
|
225
|
+
checked={block.autoplay === true}
|
|
226
|
+
onChange={(checked) => update({ autoplay: checked })}
|
|
227
|
+
/>
|
|
228
|
+
<StyledCheckbox
|
|
229
|
+
label="Loop"
|
|
230
|
+
checked={block.loop === true}
|
|
231
|
+
onChange={(checked) => update({ loop: checked })}
|
|
232
|
+
/>
|
|
233
|
+
<StyledCheckbox
|
|
234
|
+
label="Muted"
|
|
235
|
+
checked={block.muted === true}
|
|
236
|
+
onChange={(checked) => update({ muted: checked })}
|
|
237
|
+
/>
|
|
238
|
+
</div>
|
|
239
|
+
</SettingsSection>
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|