@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.
Files changed (147) hide show
  1. package/README.md +27 -2
  2. package/app/admin/assets/page.tsx +6 -6
  3. package/app/admin/database/page.tsx +302 -302
  4. package/app/admin/error.tsx +53 -53
  5. package/app/admin/layout.tsx +332 -320
  6. package/app/admin/navigation/page.tsx +255 -255
  7. package/app/admin/pages/[slug]/page.tsx +44 -27
  8. package/app/admin/pages/page.tsx +24 -19
  9. package/app/admin/projects/page.tsx +30 -21
  10. package/app/admin/setup/page.tsx +1 -1
  11. package/app/admin/styles/page.tsx +1 -1
  12. package/app/api/admin/assets/register/route.ts +51 -14
  13. package/app/api/admin/assets/registry/route.ts +4 -1
  14. package/app/api/admin/assets/relink/confirm/route.ts +4 -1
  15. package/app/api/admin/assets/relink/route.ts +4 -1
  16. package/app/api/admin/assets/scan/route.ts +4 -1
  17. package/app/api/admin/backups/restore-data/route.ts +4 -1
  18. package/app/api/admin/r2/connect/route.ts +4 -1
  19. package/app/api/admin/r2/delete/route.ts +4 -1
  20. package/app/api/admin/r2/rename/route.ts +4 -1
  21. package/app/api/admin/r2/upload-url/route.ts +4 -1
  22. package/app/api/admin/revalidate/route.ts +4 -1
  23. package/app/api/admin/storage/switch/route.ts +4 -1
  24. package/app/api/custom-sections/[id]/route.ts +5 -6
  25. package/components/admin/MetadataEditor.tsx +6 -6
  26. package/components/admin/PublishToggle.tsx +2 -2
  27. package/components/admin/nav-builder/NavBuilder.tsx +1 -1
  28. package/components/admin/nav-builder/NavBuilderGrid.tsx +3 -3
  29. package/components/admin/nav-builder/NavGridCell.tsx +48 -48
  30. package/components/admin/nav-builder/NavGridItem.tsx +8 -6
  31. package/components/admin/nav-builder/NavItemSettings.tsx +331 -331
  32. package/components/admin/nav-builder/NavItemTypePicker.tsx +102 -102
  33. package/components/admin/nav-builder/NavLivePreview.tsx +1 -1
  34. package/components/admin/nav-builder/NavMobileLivePreview.tsx +226 -226
  35. package/components/admin/nav-builder/NavMobileSettings.tsx +242 -242
  36. package/components/admin/nav-builder/NavSettingsFields.tsx +518 -514
  37. package/components/admin/setup-wizard/BrandingStep.tsx +3 -3
  38. package/components/admin/setup-wizard/DatabaseStep.tsx +2 -2
  39. package/components/admin/setup-wizard/DoneStep.tsx +1 -1
  40. package/components/admin/setup-wizard/SetupWizard.tsx +4 -4
  41. package/components/admin/setup-wizard/StorageStep.tsx +2 -2
  42. package/components/admin/setup-wizard/WelcomeStep.tsx +2 -2
  43. package/components/admin/styles/ColorsEditor.tsx +9 -8
  44. package/components/admin/styles/FontsEditor.tsx +9 -7
  45. package/components/admin/styles/GridLayoutEditor.tsx +9 -9
  46. package/components/admin/styles/LinksButtonsEditor.tsx +5 -5
  47. package/components/admin/styles/TypographyEditor.tsx +6 -6
  48. package/components/admin/styles/shared.tsx +68 -68
  49. package/components/blocks/AudioBlockRenderer.tsx +286 -286
  50. package/components/blocks/CoverSectionRenderer.tsx +7 -1
  51. package/components/blocks/MarqueeBlockRenderer.tsx +316 -0
  52. package/components/blocks/ProjectCarouselBlockRenderer.tsx +1 -1
  53. package/components/blocks/SectionV2Renderer.tsx +8 -1
  54. package/components/builder/BlockCardIcons.tsx +316 -316
  55. package/components/builder/BlockTypePicker.tsx +1 -1
  56. package/components/builder/BubbleIcons.tsx +104 -0
  57. package/components/builder/BuilderCanvas.tsx +2 -0
  58. package/components/builder/CanvasMinimap.tsx +66 -49
  59. package/components/builder/CanvasToolbar.tsx +31 -41
  60. package/components/builder/CoverSectionCanvas.tsx +363 -363
  61. package/components/builder/DeviceFrame.tsx +1 -1
  62. package/components/builder/DndWrapper.tsx +3 -3
  63. package/components/builder/InsertionLines.tsx +1 -1
  64. package/components/builder/SectionCardIcons.tsx +421 -320
  65. package/components/builder/SectionEditorBar.tsx +5 -3
  66. package/components/builder/SectionTypePicker.tsx +7 -5
  67. package/components/builder/SectionV2Canvas.tsx +1 -1
  68. package/components/builder/SectionV2Column.tsx +82 -68
  69. package/components/builder/SettingsPanel.tsx +21 -17
  70. package/components/builder/SortableBlock.tsx +93 -73
  71. package/components/builder/SortableRow.tsx +33 -35
  72. package/components/builder/VirtualAssetGrid.tsx +10 -4
  73. package/components/builder/asset-browser/R2BrowserContent.tsx +18 -14
  74. package/components/builder/blockStyles.tsx +192 -185
  75. package/components/builder/color-picker/AlphaSlider.tsx +141 -141
  76. package/components/builder/color-picker/ColorInputs.tsx +105 -105
  77. package/components/builder/color-picker/EyedropperButton.tsx +75 -74
  78. package/components/builder/color-picker/HueSlider.tsx +124 -124
  79. package/components/builder/color-picker/SaturationCanvas.tsx +142 -142
  80. package/components/builder/color-picker/SwatchBar.tsx +98 -93
  81. package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
  82. package/components/builder/editors/AudioBlockEditor.tsx +242 -242
  83. package/components/builder/editors/BeforeAfterBlockEditor.tsx +360 -360
  84. package/components/builder/editors/ButtonBlockEditor.tsx +4 -4
  85. package/components/builder/editors/EnterAnimationPicker.tsx +2 -2
  86. package/components/builder/editors/HoverEffectPicker.tsx +2 -2
  87. package/components/builder/editors/ImageBlockEditor.tsx +2 -2
  88. package/components/builder/editors/ImageGridBlockEditor.tsx +8 -6
  89. package/components/builder/editors/MarqueeBlockEditor.tsx +622 -0
  90. package/components/builder/editors/ProjectCarouselBlockEditor.tsx +443 -443
  91. package/components/builder/editors/ProjectGridEditor.tsx +21 -16
  92. package/components/builder/editors/SpacerBlockEditor.tsx +29 -27
  93. package/components/builder/editors/StaggerSettings.tsx +109 -109
  94. package/components/builder/editors/TextBlockEditor.tsx +22 -17
  95. package/components/builder/editors/TextStylePicker.tsx +1 -1
  96. package/components/builder/editors/VideoBlockEditor.tsx +2 -2
  97. package/components/builder/editors/index.ts +11 -10
  98. package/components/builder/editors/shared.tsx +10 -8
  99. package/components/builder/live-preview/LiveAudioPreview.tsx +120 -120
  100. package/components/builder/live-preview/LiveBeforeAfterPreview.tsx +1 -1
  101. package/components/builder/live-preview/LiveImageGridPreview.tsx +10 -2
  102. package/components/builder/live-preview/LiveImagePreview.tsx +4 -2
  103. package/components/builder/live-preview/LiveMarqueePreview.tsx +39 -0
  104. package/components/builder/live-preview/LiveProjectCarouselPreview.tsx +1 -1
  105. package/components/builder/live-preview/LiveVideoPreview.tsx +1 -1
  106. package/components/builder/live-preview/ProjectCardWrapper.tsx +293 -291
  107. package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
  108. package/components/builder/live-preview/shared.tsx +5 -2
  109. package/components/builder/settings-panel/AnimationTab.tsx +138 -138
  110. package/components/builder/settings-panel/BlockLayoutTab.tsx +11 -9
  111. package/components/builder/settings-panel/CardEntranceSection.tsx +114 -114
  112. package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
  113. package/components/builder/settings-panel/ColumnV2Settings.tsx +5 -5
  114. package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +71 -71
  115. package/components/builder/settings-panel/CoverSectionSettings.tsx +337 -335
  116. package/components/builder/settings-panel/PageSettings.tsx +3 -3
  117. package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
  118. package/components/builder/settings-panel/SectionV2AnimationTab.tsx +4 -4
  119. package/components/builder/settings-panel/SectionV2LayoutTab.tsx +356 -356
  120. package/components/builder/settings-panel/SectionV2Settings.tsx +25 -20
  121. package/components/builder/settings-panel/TRBLInputs.tsx +1 -1
  122. package/components/builder/settings-panel/index.ts +1 -0
  123. package/lib/animation/enter-types.ts +1 -0
  124. package/lib/animation/hover-effect-presets.ts +210 -210
  125. package/lib/animation/hover-effect-types.ts +1 -0
  126. package/lib/builder/block-registrations.ts +468 -417
  127. package/lib/builder/constants.ts +111 -111
  128. package/lib/builder/serializer/normalizers.ts +14 -0
  129. package/lib/builder/serializer/serializers.ts +27 -0
  130. package/lib/builder/store-sections.ts +23 -2
  131. package/lib/builder/types-slices.ts +428 -414
  132. package/lib/builder/types.ts +4 -1
  133. package/lib/config/index.ts +27 -27
  134. package/lib/sanity/queries.ts +48 -0
  135. package/lib/sanity/types.ts +112 -1
  136. package/lib/version.ts +1 -1
  137. package/package.json +7 -5
  138. package/sanity/schemas/blocks/audioBlock.ts +69 -69
  139. package/sanity/schemas/blocks/index.ts +12 -11
  140. package/sanity/schemas/blocks/marqueeBlock.ts +292 -0
  141. package/sanity/schemas/index.ts +120 -117
  142. package/sanity/schemas/objects/coverSection.ts +32 -0
  143. package/sanity/schemas/objects/parallaxSlide.ts +32 -0
  144. package/sanity/schemas/pageSectionV2.ts +32 -0
  145. package/styles/admin.css +85 -85
  146. package/styles/animations.css +237 -237
  147. 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-[#076bff] bg-[#076bff]/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 || "#4794E2"}
164
- onChange={(value) => {
165
- const hex = resolveColorHex(value) || "#4794E2";
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-[#076bff]" : "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
- }
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
+ }