@morphika/andami 0.5.4 → 0.5.5
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/app/admin/assets/page.tsx +3 -2
- package/app/admin/layout.tsx +4 -0
- package/components/admin/nav-builder/NavBuilder.tsx +2 -1
- package/components/admin/styles/FontsEditor.tsx +2 -1
- package/components/builder/CoverSectionCanvas.tsx +7 -6
- package/components/builder/SettingsPanel.tsx +14 -8
- package/components/builder/SortableBlock.tsx +4 -0
- package/components/builder/SortableRow.tsx +2 -0
- package/components/builder/asset-browser/useR2Operations.ts +5 -4
- package/components/builder/editors/AudioBlockEditor.tsx +10 -8
- package/components/builder/editors/BeforeAfterBlockEditor.tsx +10 -8
- package/components/builder/editors/ButtonBlockEditor.tsx +9 -7
- package/components/builder/editors/ImageBlockEditor.tsx +10 -8
- package/components/builder/editors/ImageGridBlockEditor.tsx +10 -8
- package/components/builder/editors/SpacerBlockEditor.tsx +4 -4
- package/components/builder/editors/TextBlockEditor.tsx +471 -468
- package/components/builder/editors/VideoBlockEditor.tsx +10 -8
- package/components/builder/settings-panel/AnimationTab.tsx +11 -8
- package/components/builder/settings-panel/BlockLayoutTab.tsx +514 -511
- package/components/builder/settings-panel/ColumnV2AnimationTab.tsx +2 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +11 -8
- package/components/builder/settings-panel/ColumnV2Settings.tsx +6 -5
- package/components/builder/settings-panel/CoverSectionLayoutTab.tsx +4 -3
- package/components/builder/settings-panel/CoverSectionSettings.tsx +14 -9
- package/components/builder/settings-panel/CustomSectionSettings.tsx +9 -7
- package/components/builder/settings-panel/PageSettings.tsx +39 -32
- package/components/builder/settings-panel/ParallaxGroupSettings.tsx +2 -2
- package/components/builder/settings-panel/ParallaxSlideSettings.tsx +2 -2
- package/components/builder/settings-panel/SectionV2AnimationTab.tsx +7 -5
- package/components/builder/settings-panel/SectionV2LayoutTab.tsx +13 -9
- package/components/builder/settings-panel/SectionV2Settings.tsx +7 -6
- package/components/builder/settings-panel/TRBLInputs.tsx +2 -2
- package/components/builder/settings-panel/useSettingsPanelSelection.ts +16 -13
- package/components/ui/ToastStack.tsx +142 -0
- package/lib/auth-token.ts +5 -1
- package/lib/bot-guard.ts +6 -0
- package/lib/builder/constants.ts +0 -7
- package/lib/toast/index.ts +56 -0
- package/lib/toast/store.ts +56 -0
- package/lib/version.ts +1 -1
- package/package.json +3 -1
|
@@ -1,468 +1,471 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, type ReactNode } from "react";
|
|
4
|
-
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
-
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
6
|
-
import type { TextBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
7
|
-
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
8
|
-
import {
|
|
9
|
-
TypographyIcon,
|
|
10
|
-
ColumnsIcon,
|
|
11
|
-
} from "./section-icons";
|
|
12
|
-
import {
|
|
13
|
-
SettingsSection,
|
|
14
|
-
SettingsField,
|
|
15
|
-
ViewportBadge,
|
|
16
|
-
ResponsiveField,
|
|
17
|
-
useActiveViewport,
|
|
18
|
-
INPUT_CLASS,
|
|
19
|
-
SELECT_CLASS,
|
|
20
|
-
} from "./shared";
|
|
21
|
-
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
22
|
-
import TextStylePicker, {
|
|
23
|
-
FALLBACK_PRESETS,
|
|
24
|
-
buildPresetsFromStyles,
|
|
25
|
-
type TextStylePreset,
|
|
26
|
-
} from "./TextStylePicker";
|
|
27
|
-
import {
|
|
28
|
-
AlignLeftIcon,
|
|
29
|
-
AlignCenterIcon,
|
|
30
|
-
AlignRightIcon,
|
|
31
|
-
AlignJustifyIcon,
|
|
32
|
-
} from "./TextAlignmentIcons";
|
|
33
|
-
import { BubbleTooltip } from "../BubbleIcons";
|
|
34
|
-
|
|
35
|
-
// ============================================
|
|
36
|
-
// Responsive style field — MUST be defined outside the editor component
|
|
37
|
-
// to avoid React treating it as a new component on every re-render,
|
|
38
|
-
// which causes input elements to lose focus.
|
|
39
|
-
// ============================================
|
|
40
|
-
|
|
41
|
-
function ResponsiveStyleField({
|
|
42
|
-
label,
|
|
43
|
-
subProp,
|
|
44
|
-
viewport,
|
|
45
|
-
isOverridden,
|
|
46
|
-
onReset,
|
|
47
|
-
children,
|
|
48
|
-
hint,
|
|
49
|
-
}: {
|
|
50
|
-
label: string;
|
|
51
|
-
subProp: string;
|
|
52
|
-
viewport: DeviceViewport;
|
|
53
|
-
isOverridden: boolean;
|
|
54
|
-
onReset: (subProp: string) => void;
|
|
55
|
-
children: ReactNode;
|
|
56
|
-
hint?: string;
|
|
57
|
-
}) {
|
|
58
|
-
return (
|
|
59
|
-
<div className="flex items-start gap-3 mb-2 last:mb-0">
|
|
60
|
-
<label className="text-[11px] text-neutral-400 w-[68px] min-w-[68px] shrink-0 pt-[7px] leading-tight">
|
|
61
|
-
{label}
|
|
62
|
-
{viewport !== "desktop" && !isOverridden && (
|
|
63
|
-
<span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
|
|
64
|
-
)}
|
|
65
|
-
{isOverridden && (
|
|
66
|
-
<span className="block text-[9px] text-[#3580f9] mt-0.5">overridden</span>
|
|
67
|
-
)}
|
|
68
|
-
</label>
|
|
69
|
-
<div className="flex-1 min-w-0">
|
|
70
|
-
{children}
|
|
71
|
-
{hint && <p className="text-[10px] text-neutral-400 mt-1">{hint}</p>}
|
|
72
|
-
{isOverridden && (
|
|
73
|
-
<button
|
|
74
|
-
onClick={() => onReset(subProp)}
|
|
75
|
-
className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors mt-0.5"
|
|
76
|
-
>
|
|
77
|
-
Reset
|
|
78
|
-
</button>
|
|
79
|
-
)}
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ============================================
|
|
86
|
-
// Main Editor
|
|
87
|
-
// ============================================
|
|
88
|
-
|
|
89
|
-
export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (fw
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
{ value: "
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
<option value="
|
|
355
|
-
<option value="
|
|
356
|
-
<option value="
|
|
357
|
-
<option value="
|
|
358
|
-
<option value="
|
|
359
|
-
<option value="
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
<option value="
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, type ReactNode } from "react";
|
|
4
|
+
import { useBuilderStore } from "../../../lib/builder/store";
|
|
5
|
+
import { getEffectiveValue, setResponsiveOverride } from "../../../lib/builder/responsive";
|
|
6
|
+
import type { TextBlock, ContentBlock } from "../../../lib/sanity/types";
|
|
7
|
+
import type { DeviceViewport } from "../../../lib/builder/types";
|
|
8
|
+
import {
|
|
9
|
+
TypographyIcon,
|
|
10
|
+
ColumnsIcon,
|
|
11
|
+
} from "./section-icons";
|
|
12
|
+
import {
|
|
13
|
+
SettingsSection,
|
|
14
|
+
SettingsField,
|
|
15
|
+
ViewportBadge,
|
|
16
|
+
ResponsiveField,
|
|
17
|
+
useActiveViewport,
|
|
18
|
+
INPUT_CLASS,
|
|
19
|
+
SELECT_CLASS,
|
|
20
|
+
} from "./shared";
|
|
21
|
+
import ColorSwatchPicker, { usePaletteSwatches } from "../ColorSwatchPicker";
|
|
22
|
+
import TextStylePicker, {
|
|
23
|
+
FALLBACK_PRESETS,
|
|
24
|
+
buildPresetsFromStyles,
|
|
25
|
+
type TextStylePreset,
|
|
26
|
+
} from "./TextStylePicker";
|
|
27
|
+
import {
|
|
28
|
+
AlignLeftIcon,
|
|
29
|
+
AlignCenterIcon,
|
|
30
|
+
AlignRightIcon,
|
|
31
|
+
AlignJustifyIcon,
|
|
32
|
+
} from "./TextAlignmentIcons";
|
|
33
|
+
import { BubbleTooltip } from "../BubbleIcons";
|
|
34
|
+
|
|
35
|
+
// ============================================
|
|
36
|
+
// Responsive style field — MUST be defined outside the editor component
|
|
37
|
+
// to avoid React treating it as a new component on every re-render,
|
|
38
|
+
// which causes input elements to lose focus.
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
function ResponsiveStyleField({
|
|
42
|
+
label,
|
|
43
|
+
subProp,
|
|
44
|
+
viewport,
|
|
45
|
+
isOverridden,
|
|
46
|
+
onReset,
|
|
47
|
+
children,
|
|
48
|
+
hint,
|
|
49
|
+
}: {
|
|
50
|
+
label: string;
|
|
51
|
+
subProp: string;
|
|
52
|
+
viewport: DeviceViewport;
|
|
53
|
+
isOverridden: boolean;
|
|
54
|
+
onReset: (subProp: string) => void;
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
hint?: string;
|
|
57
|
+
}) {
|
|
58
|
+
return (
|
|
59
|
+
<div className="flex items-start gap-3 mb-2 last:mb-0">
|
|
60
|
+
<label className="text-[11px] text-neutral-400 w-[68px] min-w-[68px] shrink-0 pt-[7px] leading-tight">
|
|
61
|
+
{label}
|
|
62
|
+
{viewport !== "desktop" && !isOverridden && (
|
|
63
|
+
<span className="block text-[9px] text-neutral-300 italic mt-0.5">inherited</span>
|
|
64
|
+
)}
|
|
65
|
+
{isOverridden && (
|
|
66
|
+
<span className="block text-[9px] text-[#3580f9] mt-0.5">overridden</span>
|
|
67
|
+
)}
|
|
68
|
+
</label>
|
|
69
|
+
<div className="flex-1 min-w-0">
|
|
70
|
+
{children}
|
|
71
|
+
{hint && <p className="text-[10px] text-neutral-400 mt-1">{hint}</p>}
|
|
72
|
+
{isOverridden && (
|
|
73
|
+
<button
|
|
74
|
+
onClick={() => onReset(subProp)}
|
|
75
|
+
className="text-[10px] text-neutral-400 hover:text-[var(--admin-error)] transition-colors mt-0.5"
|
|
76
|
+
>
|
|
77
|
+
Reset
|
|
78
|
+
</button>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================
|
|
86
|
+
// Main Editor
|
|
87
|
+
// ============================================
|
|
88
|
+
|
|
89
|
+
export default function TextBlockEditor({ block }: { block: TextBlock }) {
|
|
90
|
+
const updateBlock = useBuilderStore((s) => s.updateBlock);
|
|
91
|
+
const updateBlockDebounced = useBuilderStore((s) => s.updateBlockDebounced);
|
|
92
|
+
const _pushSnapshot = useBuilderStore((s) => s._pushSnapshot);
|
|
93
|
+
const pageSettings = useBuilderStore((s) => s.pageSettings);
|
|
94
|
+
const viewport = useActiveViewport();
|
|
95
|
+
const paletteSwatches = usePaletteSwatches();
|
|
96
|
+
const pageTextColor = pageSettings.text_color || "#0a0a0a";
|
|
97
|
+
const [presets, setPresets] = useState<TextStylePreset[]>(FALLBACK_PRESETS);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
fetch("/api/admin/styles", { credentials: "include" })
|
|
101
|
+
.then((r) => r.json())
|
|
102
|
+
.then((data) => {
|
|
103
|
+
if (data.styles) {
|
|
104
|
+
const built = buildPresetsFromStyles(data.styles);
|
|
105
|
+
if (built.length > 0) setPresets(built);
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
.catch(() => { /* Style presets unavailable — fallback presets used */ });
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const style = block.style || {};
|
|
112
|
+
// Undo snapshot strategy:
|
|
113
|
+
// - Continuous inputs (text fields, sliders): snapshot on focus (one snapshot per edit session)
|
|
114
|
+
// - Discrete actions (buttons, preset picks): snapshot immediately before mutation
|
|
115
|
+
const snapshotOnFocus = () => _pushSnapshot();
|
|
116
|
+
|
|
117
|
+
// === Responsive helpers for nested style sub-properties ===
|
|
118
|
+
// The responsive system deep-merges 1-level objects, so we store
|
|
119
|
+
// partial style overrides at responsive[viewport].style = { fontSize: 24 }
|
|
120
|
+
// and resolveBlock merges { ...block.style, ...responsive[viewport].style }.
|
|
121
|
+
|
|
122
|
+
/** Get the current responsive style overrides for the active viewport */
|
|
123
|
+
const getViewportStyleOverrides = (): Record<string, unknown> => {
|
|
124
|
+
const responsive = (block as unknown as Record<string, unknown>).responsive as
|
|
125
|
+
| Record<string, Record<string, unknown>>
|
|
126
|
+
| undefined;
|
|
127
|
+
if (!responsive?.[viewport]?.style) return {};
|
|
128
|
+
return responsive[viewport].style as Record<string, unknown>;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Check if a style sub-property has a responsive override */
|
|
132
|
+
const hasStyleOverride = (subProp: string): boolean => {
|
|
133
|
+
if (viewport === "desktop") return true;
|
|
134
|
+
const overrides = getViewportStyleOverrides();
|
|
135
|
+
return subProp in overrides;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** Get effective value of a style sub-property for the active viewport */
|
|
139
|
+
const getEffectiveStyleValue = <T,>(subProp: string, baseValue: T): T => {
|
|
140
|
+
if (viewport === "desktop") return baseValue;
|
|
141
|
+
const overrides = getViewportStyleOverrides();
|
|
142
|
+
return subProp in overrides ? (overrides[subProp] as T) : baseValue;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/** Update a style sub-property, responsive-aware */
|
|
146
|
+
const updateStyleResponsive = (subProp: string, value: unknown) => {
|
|
147
|
+
if (viewport === "desktop") {
|
|
148
|
+
updateBlock(block._key, {
|
|
149
|
+
style: { ...style, [subProp]: value },
|
|
150
|
+
} as Partial<ContentBlock>);
|
|
151
|
+
} else {
|
|
152
|
+
// Merge into responsive[viewport].style
|
|
153
|
+
const existing = (block as unknown as Record<string, unknown>).responsive as
|
|
154
|
+
| Record<string, Record<string, unknown>>
|
|
155
|
+
| undefined || {};
|
|
156
|
+
const vpOverrides = { ...(existing[viewport] || {}) };
|
|
157
|
+
const styleOverrides = { ...((vpOverrides.style as Record<string, unknown>) || {}), [subProp]: value };
|
|
158
|
+
vpOverrides.style = styleOverrides;
|
|
159
|
+
updateBlock(block._key, {
|
|
160
|
+
responsive: { ...existing, [viewport]: vpOverrides },
|
|
161
|
+
} as Partial<ContentBlock>);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Update a style sub-property with debounce, responsive-aware */
|
|
166
|
+
const updateStyleDebouncedResponsive = (subProp: string, value: unknown) => {
|
|
167
|
+
if (viewport === "desktop") {
|
|
168
|
+
updateBlockDebounced(block._key, {
|
|
169
|
+
style: { ...style, [subProp]: value },
|
|
170
|
+
} as Partial<ContentBlock>);
|
|
171
|
+
} else {
|
|
172
|
+
const existing = (block as unknown as Record<string, unknown>).responsive as
|
|
173
|
+
| Record<string, Record<string, unknown>>
|
|
174
|
+
| undefined || {};
|
|
175
|
+
const vpOverrides = { ...(existing[viewport] || {}) };
|
|
176
|
+
const styleOverrides = { ...((vpOverrides.style as Record<string, unknown>) || {}), [subProp]: value };
|
|
177
|
+
vpOverrides.style = styleOverrides;
|
|
178
|
+
updateBlockDebounced(block._key, {
|
|
179
|
+
responsive: { ...existing, [viewport]: vpOverrides },
|
|
180
|
+
} as Partial<ContentBlock>);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/** Reset a style sub-property override */
|
|
185
|
+
const resetStyleOverride = (subProp: string) => {
|
|
186
|
+
const existing = (block as unknown as Record<string, unknown>).responsive as
|
|
187
|
+
| Record<string, Record<string, unknown>>
|
|
188
|
+
| undefined || {};
|
|
189
|
+
const vpOverrides = { ...(existing[viewport] || {}) };
|
|
190
|
+
const styleOverrides = { ...((vpOverrides.style as Record<string, unknown>) || {}) };
|
|
191
|
+
delete styleOverrides[subProp];
|
|
192
|
+
if (Object.keys(styleOverrides).length === 0) {
|
|
193
|
+
delete vpOverrides.style;
|
|
194
|
+
} else {
|
|
195
|
+
vpOverrides.style = styleOverrides;
|
|
196
|
+
}
|
|
197
|
+
const responsive = { ...existing };
|
|
198
|
+
if (Object.keys(vpOverrides).length === 0) {
|
|
199
|
+
delete responsive[viewport];
|
|
200
|
+
} else {
|
|
201
|
+
responsive[viewport] = vpOverrides;
|
|
202
|
+
}
|
|
203
|
+
updateBlock(block._key, { responsive } as Partial<ContentBlock>);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// === Responsive helpers for top-level properties (e.g. columns) ===
|
|
207
|
+
const updateResponsive = (property: string, value: unknown) => {
|
|
208
|
+
if (viewport === "desktop") {
|
|
209
|
+
updateBlock(block._key, { [property]: value } as Partial<ContentBlock>);
|
|
210
|
+
} else {
|
|
211
|
+
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, value);
|
|
212
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const resetOverride = (property: string) => {
|
|
217
|
+
const overrides = setResponsiveOverride(block as ContentBlock, viewport, property, undefined);
|
|
218
|
+
updateBlock(block._key, overrides as Partial<ContentBlock>);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const baseFontSizePx = (() => {
|
|
222
|
+
const fs = style.fontSize;
|
|
223
|
+
if (typeof fs === "number") return fs;
|
|
224
|
+
const legacyMap: Record<string, number> = { small: 12, base: 14, large: 20, xl: 24, "2xl": 32, "3xl": 48 };
|
|
225
|
+
return legacyMap[fs || "base"] || 14;
|
|
226
|
+
})();
|
|
227
|
+
|
|
228
|
+
// Responsive-aware effective values
|
|
229
|
+
const currentFontSizePx = getEffectiveStyleValue<number>("fontSize", baseFontSizePx);
|
|
230
|
+
const currentAlignment = getEffectiveStyleValue<string>("alignment", style.alignment || "left");
|
|
231
|
+
const currentMaxWidth = getEffectiveStyleValue<string>("maxWidth", style.maxWidth || "");
|
|
232
|
+
const effectiveColumns = getEffectiveValue<number>(block as ContentBlock, viewport, "columns", block.columns || 1);
|
|
233
|
+
|
|
234
|
+
const baseFontWeight = (() => {
|
|
235
|
+
const fw = style.fontWeight;
|
|
236
|
+
if (!fw) return "400";
|
|
237
|
+
if (!isNaN(parseInt(fw, 10))) return fw;
|
|
238
|
+
if (fw === "bold") return "700";
|
|
239
|
+
if (fw === "medium") return "500";
|
|
240
|
+
return "400";
|
|
241
|
+
})();
|
|
242
|
+
const currentFontWeight = getEffectiveStyleValue<string>("fontWeight", baseFontWeight);
|
|
243
|
+
|
|
244
|
+
const handleStyleSelect = (preset: TextStylePreset) => {
|
|
245
|
+
_pushSnapshot();
|
|
246
|
+
updateBlock(block._key, {
|
|
247
|
+
textStyle: preset.key,
|
|
248
|
+
style: {
|
|
249
|
+
...style,
|
|
250
|
+
fontSize: preset.fontSize,
|
|
251
|
+
fontWeight: preset.fontWeight,
|
|
252
|
+
lineHeight: preset.lineHeight,
|
|
253
|
+
letterSpacing: preset.letterSpacing,
|
|
254
|
+
textTransform: preset.textTransform as TextBlock["style"] extends undefined ? never : NonNullable<TextBlock["style"]>["textTransform"],
|
|
255
|
+
},
|
|
256
|
+
} as Partial<ContentBlock>);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const handleStyleClear = () => {
|
|
260
|
+
_pushSnapshot();
|
|
261
|
+
updateBlock(block._key, {
|
|
262
|
+
textStyle: undefined,
|
|
263
|
+
} as Partial<ContentBlock>);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const alignments: { value: "left" | "center" | "right" | "justify"; icon: React.ReactNode }[] = [
|
|
267
|
+
{ value: "left", icon: <AlignLeftIcon /> },
|
|
268
|
+
{ value: "center", icon: <AlignCenterIcon /> },
|
|
269
|
+
{ value: "right", icon: <AlignRightIcon /> },
|
|
270
|
+
{ value: "justify", icon: <AlignJustifyIcon /> },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<>
|
|
275
|
+
<ViewportBadge />
|
|
276
|
+
|
|
277
|
+
{/* Typography section: Style, Color, Align, Size, Weight, Line height, Letter spacing, Transform */}
|
|
278
|
+
<SettingsSection title="Typography" defaultOpen icon={<TypographyIcon />}>
|
|
279
|
+
<SettingsField label="Style">
|
|
280
|
+
<TextStylePicker
|
|
281
|
+
presets={presets}
|
|
282
|
+
activeKey={block.textStyle}
|
|
283
|
+
onSelect={handleStyleSelect}
|
|
284
|
+
onClear={handleStyleClear}
|
|
285
|
+
/>
|
|
286
|
+
</SettingsField>
|
|
287
|
+
|
|
288
|
+
<ResponsiveStyleField label="Color" subProp="color" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("color")} onReset={resetStyleOverride}>
|
|
289
|
+
<ColorSwatchPicker
|
|
290
|
+
value={getEffectiveStyleValue<string>("color", style.color || "")}
|
|
291
|
+
onChange={(hex) => updateStyleResponsive("color", hex)}
|
|
292
|
+
swatches={paletteSwatches}
|
|
293
|
+
allowClear
|
|
294
|
+
/>
|
|
295
|
+
</ResponsiveStyleField>
|
|
296
|
+
|
|
297
|
+
<ResponsiveStyleField label="Align" subProp="alignment" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("alignment")} onReset={resetStyleOverride}>
|
|
298
|
+
<div className="flex gap-0.5 bg-[#f5f5f5] rounded-lg p-0.5">
|
|
299
|
+
{alignments.map(({ value, icon }) => {
|
|
300
|
+
const label = value.charAt(0).toUpperCase() + value.slice(1);
|
|
301
|
+
return (
|
|
302
|
+
<button
|
|
303
|
+
key={value}
|
|
304
|
+
onClick={() => updateStyleResponsive("alignment", value)}
|
|
305
|
+
className={`group/bb relative flex-1 flex items-center justify-center py-[5px] rounded-md transition-all ${
|
|
306
|
+
currentAlignment === value
|
|
307
|
+
? "bg-white text-neutral-900 shadow-[0_1px_3px_rgba(0,0,0,0.08)]"
|
|
308
|
+
: "text-neutral-300 hover:text-neutral-500"
|
|
309
|
+
}`}
|
|
310
|
+
aria-label={label}
|
|
311
|
+
>
|
|
312
|
+
{icon}
|
|
313
|
+
<BubbleTooltip>{label}</BubbleTooltip>
|
|
314
|
+
</button>
|
|
315
|
+
);
|
|
316
|
+
})}
|
|
317
|
+
</div>
|
|
318
|
+
</ResponsiveStyleField>
|
|
319
|
+
|
|
320
|
+
<ResponsiveStyleField label="Size" subProp="fontSize" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("fontSize")} onReset={resetStyleOverride}>
|
|
321
|
+
<div className="flex items-center gap-0 bg-[#f5f5f5] rounded-lg overflow-hidden transition-all border border-transparent focus-within:bg-white focus-within:border-[#3580f9] focus-within:shadow-[0_0_0_3px_rgba(53, 128, 249,0.06)]">
|
|
322
|
+
<input
|
|
323
|
+
type="number"
|
|
324
|
+
min={1}
|
|
325
|
+
max={999}
|
|
326
|
+
value={currentFontSizePx}
|
|
327
|
+
onFocus={snapshotOnFocus}
|
|
328
|
+
onChange={(e) => {
|
|
329
|
+
const val = parseInt(e.target.value, 10);
|
|
330
|
+
if (!isNaN(val) && val > 0) {
|
|
331
|
+
updateStyleDebouncedResponsive("fontSize", val);
|
|
332
|
+
if (viewport === "desktop" && block.textStyle) {
|
|
333
|
+
updateBlockDebounced(block._key, { textStyle: undefined } as Partial<ContentBlock>);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}}
|
|
337
|
+
className="flex-1 min-w-0 bg-transparent border-none px-2.5 py-[7px] text-xs text-neutral-900 outline-none"
|
|
338
|
+
/>
|
|
339
|
+
<span className="text-[10px] text-neutral-400 pr-2.5 shrink-0 select-none">px</span>
|
|
340
|
+
</div>
|
|
341
|
+
</ResponsiveStyleField>
|
|
342
|
+
|
|
343
|
+
<ResponsiveStyleField label="Weight" subProp="fontWeight" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("fontWeight")} onReset={resetStyleOverride}>
|
|
344
|
+
<select
|
|
345
|
+
value={currentFontWeight}
|
|
346
|
+
onChange={(e) => {
|
|
347
|
+
updateStyleResponsive("fontWeight", e.target.value);
|
|
348
|
+
if (viewport === "desktop" && block.textStyle) {
|
|
349
|
+
updateBlock(block._key, { textStyle: undefined } as Partial<ContentBlock>);
|
|
350
|
+
}
|
|
351
|
+
}}
|
|
352
|
+
className={SELECT_CLASS}
|
|
353
|
+
>
|
|
354
|
+
<option value="100">Thin (100)</option>
|
|
355
|
+
<option value="200">ExtraLight (200)</option>
|
|
356
|
+
<option value="300">Light (300)</option>
|
|
357
|
+
<option value="400">Regular (400)</option>
|
|
358
|
+
<option value="500">Medium (500)</option>
|
|
359
|
+
<option value="600">SemiBold (600)</option>
|
|
360
|
+
<option value="700">Bold (700)</option>
|
|
361
|
+
<option value="800">ExtraBold (800)</option>
|
|
362
|
+
<option value="900">Black (900)</option>
|
|
363
|
+
</select>
|
|
364
|
+
</ResponsiveStyleField>
|
|
365
|
+
|
|
366
|
+
<ResponsiveStyleField label="Line height" subProp="lineHeight" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("lineHeight")} onReset={resetStyleOverride}>
|
|
367
|
+
<input
|
|
368
|
+
type="text"
|
|
369
|
+
value={getEffectiveStyleValue<string>("lineHeight", style.lineHeight || "")}
|
|
370
|
+
onFocus={snapshotOnFocus}
|
|
371
|
+
onChange={(e) => {
|
|
372
|
+
updateStyleDebouncedResponsive("lineHeight", e.target.value);
|
|
373
|
+
if (viewport === "desktop" && block.textStyle) {
|
|
374
|
+
updateBlockDebounced(block._key, { textStyle: undefined } as Partial<ContentBlock>);
|
|
375
|
+
}
|
|
376
|
+
}}
|
|
377
|
+
placeholder="1.5"
|
|
378
|
+
className={INPUT_CLASS}
|
|
379
|
+
/>
|
|
380
|
+
</ResponsiveStyleField>
|
|
381
|
+
|
|
382
|
+
<ResponsiveStyleField label="Spacing" subProp="letterSpacing" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("letterSpacing")} onReset={resetStyleOverride}>
|
|
383
|
+
<input
|
|
384
|
+
type="text"
|
|
385
|
+
value={getEffectiveStyleValue<string>("letterSpacing", style.letterSpacing || "")}
|
|
386
|
+
onFocus={snapshotOnFocus}
|
|
387
|
+
onChange={(e) => {
|
|
388
|
+
updateStyleDebouncedResponsive("letterSpacing", e.target.value);
|
|
389
|
+
if (viewport === "desktop" && block.textStyle) {
|
|
390
|
+
updateBlockDebounced(block._key, { textStyle: undefined } as Partial<ContentBlock>);
|
|
391
|
+
}
|
|
392
|
+
}}
|
|
393
|
+
placeholder="0, -0.02em, 2px"
|
|
394
|
+
className={INPUT_CLASS}
|
|
395
|
+
/>
|
|
396
|
+
</ResponsiveStyleField>
|
|
397
|
+
|
|
398
|
+
<ResponsiveStyleField label="Transform" subProp="textTransform" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("textTransform")} onReset={resetStyleOverride}>
|
|
399
|
+
<select
|
|
400
|
+
value={getEffectiveStyleValue<string>("textTransform", style.textTransform || "none")}
|
|
401
|
+
onChange={(e) => updateStyleResponsive("textTransform", e.target.value)}
|
|
402
|
+
className={SELECT_CLASS}
|
|
403
|
+
>
|
|
404
|
+
<option value="none">None</option>
|
|
405
|
+
<option value="uppercase">UPPERCASE</option>
|
|
406
|
+
<option value="lowercase">lowercase</option>
|
|
407
|
+
<option value="capitalize">Capitalize</option>
|
|
408
|
+
</select>
|
|
409
|
+
</ResponsiveStyleField>
|
|
410
|
+
</SettingsSection>
|
|
411
|
+
|
|
412
|
+
{/* Columns section */}
|
|
413
|
+
<SettingsSection title="Columns" icon={<ColumnsIcon />}>
|
|
414
|
+
<ResponsiveField
|
|
415
|
+
label="Columns"
|
|
416
|
+
block={block as ContentBlock}
|
|
417
|
+
property="columns"
|
|
418
|
+
onReset={() => resetOverride("columns")}
|
|
419
|
+
>
|
|
420
|
+
<div className="flex gap-0.5 bg-[#f5f5f5] rounded-lg p-0.5">
|
|
421
|
+
{[1, 2, 3, 4].map((n) => (
|
|
422
|
+
<button
|
|
423
|
+
key={n}
|
|
424
|
+
onClick={() => {
|
|
425
|
+
_pushSnapshot();
|
|
426
|
+
updateResponsive("columns", n);
|
|
427
|
+
}}
|
|
428
|
+
className={`flex-1 flex items-center justify-center py-[5px] rounded-md text-xs transition-all ${
|
|
429
|
+
effectiveColumns === n
|
|
430
|
+
? "bg-white text-neutral-900 shadow-[0_1px_3px_rgba(0,0,0,0.08)] font-medium"
|
|
431
|
+
: "text-neutral-400 hover:text-neutral-600"
|
|
432
|
+
}`}
|
|
433
|
+
>
|
|
434
|
+
{n}
|
|
435
|
+
</button>
|
|
436
|
+
))}
|
|
437
|
+
</div>
|
|
438
|
+
</ResponsiveField>
|
|
439
|
+
|
|
440
|
+
<ResponsiveStyleField label="Max Width" subProp="maxWidth" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("maxWidth")} onReset={resetStyleOverride}>
|
|
441
|
+
<input
|
|
442
|
+
type="text"
|
|
443
|
+
value={currentMaxWidth}
|
|
444
|
+
onFocus={snapshotOnFocus}
|
|
445
|
+
onChange={(e) => updateStyleDebouncedResponsive("maxWidth", e.target.value)}
|
|
446
|
+
placeholder="none, 600px, 80%"
|
|
447
|
+
className={INPUT_CLASS}
|
|
448
|
+
/>
|
|
449
|
+
</ResponsiveStyleField>
|
|
450
|
+
|
|
451
|
+
<ResponsiveStyleField label="Opacity" subProp="opacity" viewport={viewport} isOverridden={viewport !== "desktop" && hasStyleOverride("opacity")} onReset={resetStyleOverride}>
|
|
452
|
+
<div className="flex items-center gap-2">
|
|
453
|
+
<input
|
|
454
|
+
type="range"
|
|
455
|
+
min={0}
|
|
456
|
+
max={100}
|
|
457
|
+
value={Math.round((getEffectiveStyleValue<number>("opacity", style.opacity ?? 1)) * 100)}
|
|
458
|
+
onChange={(e) =>
|
|
459
|
+
updateStyleResponsive("opacity", parseInt(e.target.value) / 100)
|
|
460
|
+
}
|
|
461
|
+
className="flex-1 accent-[#3580f9]"
|
|
462
|
+
/>
|
|
463
|
+
<span className="text-xs text-neutral-900 w-10 text-right tabular-nums">
|
|
464
|
+
{Math.round((getEffectiveStyleValue<number>("opacity", style.opacity ?? 1)) * 100)}%
|
|
465
|
+
</span>
|
|
466
|
+
</div>
|
|
467
|
+
</ResponsiveStyleField>
|
|
468
|
+
</SettingsSection>
|
|
469
|
+
</>
|
|
470
|
+
);
|
|
471
|
+
}
|