@morphika/andami 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/app/admin/layout.tsx +26 -14
- package/app/admin/pages/[slug]/page.tsx +39 -22
- package/app/admin/pages/page.tsx +13 -8
- package/app/admin/projects/page.tsx +17 -8
- package/app/api/admin/assets/register/route.ts +51 -14
- package/app/api/admin/assets/registry/route.ts +4 -1
- package/app/api/admin/assets/relink/confirm/route.ts +4 -1
- package/app/api/admin/assets/relink/route.ts +4 -1
- package/app/api/admin/assets/scan/route.ts +4 -1
- package/app/api/admin/backups/restore-data/route.ts +4 -1
- package/app/api/admin/r2/connect/route.ts +4 -1
- package/app/api/admin/r2/delete/route.ts +4 -1
- package/app/api/admin/r2/rename/route.ts +4 -1
- package/app/api/admin/r2/upload-url/route.ts +4 -1
- package/app/api/admin/revalidate/route.ts +4 -1
- package/app/api/admin/storage/switch/route.ts +4 -1
- package/app/api/custom-sections/[id]/route.ts +5 -6
- package/components/admin/PublishToggle.tsx +2 -2
- package/components/admin/nav-builder/NavGridItem.tsx +4 -2
- package/components/admin/nav-builder/NavSettingsFields.tsx +10 -6
- package/components/admin/styles/ColorsEditor.tsx +7 -6
- package/components/admin/styles/FontsEditor.tsx +3 -1
- package/components/blocks/CoverSectionRenderer.tsx +7 -1
- package/components/blocks/SectionV2Renderer.tsx +8 -1
- package/components/builder/BubbleIcons.tsx +14 -0
- package/components/builder/CanvasMinimap.tsx +66 -49
- package/components/builder/CanvasToolbar.tsx +31 -41
- package/components/builder/SectionEditorBar.tsx +4 -2
- package/components/builder/SectionTypePicker.tsx +4 -2
- package/components/builder/SectionV2Column.tsx +13 -1
- package/components/builder/SettingsPanel.tsx +21 -17
- package/components/builder/SortableBlock.tsx +2 -2
- package/components/builder/SortableRow.tsx +6 -9
- package/components/builder/VirtualAssetGrid.tsx +8 -2
- package/components/builder/asset-browser/R2BrowserContent.tsx +8 -4
- package/components/builder/color-picker/EyedropperButton.tsx +7 -6
- package/components/builder/color-picker/SwatchBar.tsx +11 -6
- package/components/builder/color-picker/UnifiedColorPicker.tsx +11 -6
- package/components/builder/editors/ImageGridBlockEditor.tsx +4 -2
- package/components/builder/editors/MarqueeBlockEditor.tsx +3 -2
- package/components/builder/editors/ProjectGridEditor.tsx +12 -7
- package/components/builder/editors/SpacerBlockEditor.tsx +25 -23
- package/components/builder/editors/TextBlockEditor.tsx +19 -14
- package/components/builder/editors/shared.tsx +4 -2
- package/components/builder/live-preview/LiveImagePreview.tsx +3 -1
- package/components/builder/live-preview/ProjectCardWrapper.tsx +3 -1
- package/components/builder/live-preview/RichTextBubbleMenu.tsx +10 -6
- package/components/builder/live-preview/shared.tsx +5 -2
- package/components/builder/settings-panel/BlockLayoutTab.tsx +4 -2
- package/components/builder/settings-panel/ColumnV2LayoutTab.tsx +242 -0
- package/components/builder/settings-panel/CoverSectionSettings.tsx +4 -2
- package/components/builder/settings-panel/SectionV2Settings.tsx +13 -8
- package/components/builder/settings-panel/index.ts +1 -0
- package/components/ui/NavContentLightbox.tsx +41 -4
- package/lib/builder/serializer/normalizers.ts +14 -0
- package/lib/builder/serializer/serializers.ts +27 -0
- package/lib/builder/store-blocks.ts +15 -5
- package/lib/builder/store-cover.ts +16 -6
- package/lib/builder/store-sections.ts +151 -51
- package/lib/builder/types-slices.ts +14 -0
- package/lib/sanity/queries.ts +48 -0
- package/lib/sanity/types.ts +14 -0
- package/lib/version.ts +1 -1
- package/package.json +7 -5
- package/sanity/schemas/objects/coverSection.ts +32 -0
- package/sanity/schemas/objects/parallaxSlide.ts +32 -0
- package/sanity/schemas/pageSectionV2.ts +32 -0
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
3
3
|
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
|
4
4
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
5
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
5
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
6
6
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError, isValidAssetPath, isValidUploadKey, checkRateLimit } from "../../../../../lib/security";
|
|
7
7
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
8
8
|
import { decryptToken } from "../../../../../lib/security";
|
|
@@ -27,6 +27,9 @@ export async function POST(request: NextRequest) {
|
|
|
27
27
|
if (!validateCsrf(request)) {
|
|
28
28
|
return csrfErrorResponse();
|
|
29
29
|
}
|
|
30
|
+
if (!hasJsonContentType(request)) {
|
|
31
|
+
return contentTypeErrorResponse();
|
|
32
|
+
}
|
|
30
33
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
31
34
|
return jsonError("Request body too large", 413);
|
|
32
35
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { revalidatePath } from "next/cache";
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../lib/auth";
|
|
4
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../lib/csrf";
|
|
4
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../lib/csrf";
|
|
5
5
|
import { logger } from "../../../../lib/logger";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -23,6 +23,9 @@ export async function POST(request: NextRequest) {
|
|
|
23
23
|
if (!validateCsrf(request)) {
|
|
24
24
|
return csrfErrorResponse();
|
|
25
25
|
}
|
|
26
|
+
if (!hasJsonContentType(request)) {
|
|
27
|
+
return contentTypeErrorResponse();
|
|
28
|
+
}
|
|
26
29
|
|
|
27
30
|
try {
|
|
28
31
|
const body = await request.json().catch(() => ({}));
|
|
@@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache";
|
|
|
3
3
|
import { isAdminAuthenticated } from "../../../../../lib/auth";
|
|
4
4
|
import { writeClient } from "../../../../../lib/sanity/writeClient";
|
|
5
5
|
import { adminClient as client } from "../../../../../lib/sanity/client";
|
|
6
|
-
import { validateCsrf, csrfErrorResponse } from "../../../../../lib/csrf";
|
|
6
|
+
import { validateCsrf, csrfErrorResponse, hasJsonContentType, contentTypeErrorResponse } from "../../../../../lib/csrf";
|
|
7
7
|
import { isBodyTooLarge, MAX_JSON_BODY_SIZE, jsonError } from "../../../../../lib/security";
|
|
8
8
|
import { invalidateProviderConfigCache } from "../../../../../lib/storage";
|
|
9
9
|
import { auditLog } from "../../../../../lib/audit";
|
|
@@ -36,6 +36,9 @@ export async function POST(request: NextRequest) {
|
|
|
36
36
|
if (!validateCsrf(request)) {
|
|
37
37
|
return csrfErrorResponse();
|
|
38
38
|
}
|
|
39
|
+
if (!hasJsonContentType(request)) {
|
|
40
|
+
return contentTypeErrorResponse();
|
|
41
|
+
}
|
|
39
42
|
if (isBodyTooLarge(request, MAX_JSON_BODY_SIZE)) {
|
|
40
43
|
return jsonError("Request body too large", 413);
|
|
41
44
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
2
|
+
import { client } from "../../../../lib/sanity/client";
|
|
3
3
|
import { customSectionByIdQuery } from "../../../../lib/sanity/queries";
|
|
4
4
|
import { logger } from "../../../../lib/logger";
|
|
5
5
|
|
|
@@ -11,10 +11,9 @@ type RouteContext = { params: Promise<{ id: string }> };
|
|
|
11
11
|
* Returns only the `section` field (PageSectionV2 data) for rendering
|
|
12
12
|
* custom section instances on the public site.
|
|
13
13
|
*
|
|
14
|
-
* Uses
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* Cache-Control headers — this avoids stale CDN responses after edits.
|
|
14
|
+
* Uses the public `client` (useCdn: true) to hit Sanity's CDN (~2ms) instead
|
|
15
|
+
* of the API (~200ms). Freshness is handled by `revalidatePath()` in the
|
|
16
|
+
* admin PATCH endpoint, which purges this endpoint's ISR cache on save.
|
|
18
17
|
*/
|
|
19
18
|
export async function GET(_request: NextRequest, context: RouteContext) {
|
|
20
19
|
try {
|
|
@@ -24,7 +23,7 @@ export async function GET(_request: NextRequest, context: RouteContext) {
|
|
|
24
23
|
return NextResponse.json({ error: "Missing section ID" }, { status: 400 });
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const result = await
|
|
26
|
+
const result = await client.fetch(customSectionByIdQuery, { id });
|
|
28
27
|
if (!result?.section) {
|
|
29
28
|
return NextResponse.json({ error: "Custom section not found" }, { status: 404 });
|
|
30
29
|
}
|
|
@@ -96,7 +96,7 @@ export default function PublishToggle(props: PublishToggleProps) {
|
|
|
96
96
|
cursor: "pointer",
|
|
97
97
|
whiteSpace: "nowrap",
|
|
98
98
|
transition: "background-color 150ms, color 150ms",
|
|
99
|
-
backgroundColor: isDraft ? "#
|
|
99
|
+
backgroundColor: isDraft ? "#d97706" : "transparent",
|
|
100
100
|
color: isDraft ? "#fff" : "#a3a3a3",
|
|
101
101
|
...(busy ? { opacity: 0.5 } : {}),
|
|
102
102
|
}}
|
|
@@ -118,7 +118,7 @@ export default function PublishToggle(props: PublishToggleProps) {
|
|
|
118
118
|
cursor: "pointer",
|
|
119
119
|
whiteSpace: "nowrap",
|
|
120
120
|
transition: "background-color 150ms, color 150ms",
|
|
121
|
-
backgroundColor: !isDraft ? "#
|
|
121
|
+
backgroundColor: !isDraft ? "#059669" : "transparent",
|
|
122
122
|
color: !isDraft ? "#fff" : "#a3a3a3",
|
|
123
123
|
...(busy ? { opacity: 0.5 } : {}),
|
|
124
124
|
}}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState, useRef, useCallback, useEffect } from "react";
|
|
4
4
|
import type { NavItem } from "../../../lib/sanity/types";
|
|
5
5
|
import { TOTAL_COLUMNS, getMaxSpan } from "./nav-builder-utils";
|
|
6
|
+
import { BubbleTooltip } from "../../builder/BubbleIcons";
|
|
6
7
|
|
|
7
8
|
interface NavGridItemProps {
|
|
8
9
|
item: NavItem;
|
|
@@ -155,8 +156,8 @@ export default function NavGridItem({
|
|
|
155
156
|
e.stopPropagation();
|
|
156
157
|
onDelete(item._key);
|
|
157
158
|
}}
|
|
158
|
-
className="absolute -top-2 -left-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-sm"
|
|
159
|
-
|
|
159
|
+
className="group/bb absolute -top-2 -left-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 transition-colors shadow-sm"
|
|
160
|
+
aria-label="Remove item"
|
|
160
161
|
>
|
|
161
162
|
<svg width="10" height="10" viewBox="0 0 10 10">
|
|
162
163
|
<path
|
|
@@ -166,6 +167,7 @@ export default function NavGridItem({
|
|
|
166
167
|
strokeLinecap="round"
|
|
167
168
|
/>
|
|
168
169
|
</svg>
|
|
170
|
+
<BubbleTooltip>Remove item</BubbleTooltip>
|
|
169
171
|
</button>
|
|
170
172
|
)}
|
|
171
173
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
StyleIcon,
|
|
13
13
|
BackgroundIcon,
|
|
14
14
|
} from "../../builder/editors/section-icons";
|
|
15
|
+
import { BubbleTooltip } from "../../builder/BubbleIcons";
|
|
15
16
|
|
|
16
17
|
// ── Nav viewport type for responsive overrides ──
|
|
17
18
|
export type NavViewport = "desktop" | "tablet" | "phone";
|
|
@@ -329,12 +330,13 @@ export function ColorInput({
|
|
|
329
330
|
{value && (
|
|
330
331
|
<button
|
|
331
332
|
onClick={() => onChange("")}
|
|
332
|
-
className="text-neutral-400 hover:text-neutral-600 shrink-0"
|
|
333
|
-
|
|
333
|
+
className="group/bb relative text-neutral-400 hover:text-neutral-600 shrink-0"
|
|
334
|
+
aria-label="Clear"
|
|
334
335
|
>
|
|
335
336
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
336
337
|
<path d="M3 3l6 6M9 3l-6 6" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
337
338
|
</svg>
|
|
339
|
+
<BubbleTooltip>Clear</BubbleTooltip>
|
|
338
340
|
</button>
|
|
339
341
|
)}
|
|
340
342
|
</div>
|
|
@@ -407,8 +409,8 @@ export function ViewportSwitcher({
|
|
|
407
409
|
<button
|
|
408
410
|
key={opt.value}
|
|
409
411
|
onClick={() => onChange(opt.value)}
|
|
410
|
-
|
|
411
|
-
className={`flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
|
|
412
|
+
aria-label={opt.label}
|
|
413
|
+
className={`group/bb relative flex-1 px-2 py-[5px] text-[10px] font-medium rounded-md transition-all flex items-center justify-center gap-1 ${
|
|
412
414
|
isActive
|
|
413
415
|
? isNonDesktop
|
|
414
416
|
? "text-[#3580f9] bg-blue-50 shadow-[0_1px_2px_rgba(53, 128, 249,0.08)]"
|
|
@@ -417,6 +419,7 @@ export function ViewportSwitcher({
|
|
|
417
419
|
}`}
|
|
418
420
|
>
|
|
419
421
|
{opt.icon}
|
|
422
|
+
<BubbleTooltip>{opt.label}</BubbleTooltip>
|
|
420
423
|
</button>
|
|
421
424
|
);
|
|
422
425
|
})}
|
|
@@ -501,12 +504,13 @@ export function ResponsiveField({
|
|
|
501
504
|
{isOverridden && onReset && (
|
|
502
505
|
<button
|
|
503
506
|
onClick={onReset}
|
|
504
|
-
className="shrink-0 text-[#3580f9]/60 hover:text-[#3580f9] transition-colors"
|
|
505
|
-
|
|
507
|
+
className="group/bb relative shrink-0 text-[#3580f9]/60 hover:text-[#3580f9] transition-colors"
|
|
508
|
+
aria-label="Reset to desktop value"
|
|
506
509
|
>
|
|
507
510
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
508
511
|
<polyline points="1 4 1 10 7 10" /><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
509
512
|
</svg>
|
|
513
|
+
<BubbleTooltip>Reset to desktop value</BubbleTooltip>
|
|
510
514
|
</button>
|
|
511
515
|
)}
|
|
512
516
|
</div>
|
|
@@ -5,6 +5,7 @@ import ColorPicker, { isValidHex } from "../../../components/builder/ColorPicker
|
|
|
5
5
|
import { invalidatePaletteCache } from "../../../components/builder/ColorSwatchPicker";
|
|
6
6
|
import type { SiteStyles, ColorSwatch } from "../../../lib/sanity/types";
|
|
7
7
|
import { Section, SaveButton } from "./shared";
|
|
8
|
+
import { BubbleTooltip } from "../../builder/BubbleIcons";
|
|
8
9
|
|
|
9
10
|
function getContrastColor(hex: string): string {
|
|
10
11
|
if (!isValidHex(hex)) return "#000";
|
|
@@ -119,14 +120,14 @@ export function ColorsEditor({
|
|
|
119
120
|
<div className="absolute inset-0 bg-black/15 flex items-center justify-center gap-1.5">
|
|
120
121
|
<button
|
|
121
122
|
onClick={(e) => { e.stopPropagation(); setEditingIndex(i); setPickerOpen(true); }}
|
|
122
|
-
className="w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[11px] cursor-pointer hover:bg-white transition-colors border-none"
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
className="group/bb relative w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[11px] cursor-pointer hover:bg-white transition-colors border-none"
|
|
124
|
+
aria-label="Edit color"
|
|
125
|
+
>✎<BubbleTooltip>Edit color</BubbleTooltip></button>
|
|
125
126
|
<button
|
|
126
127
|
onClick={(e) => { e.stopPropagation(); removeSwatch(i); }}
|
|
127
|
-
className="w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[13px] text-red-500 cursor-pointer hover:bg-white transition-colors border-none"
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
className="group/bb relative w-6 h-6 rounded-full bg-white/90 flex items-center justify-center text-[13px] text-red-500 cursor-pointer hover:bg-white transition-colors border-none"
|
|
129
|
+
aria-label="Remove"
|
|
130
|
+
>×<BubbleTooltip>Remove</BubbleTooltip></button>
|
|
130
131
|
</div>
|
|
131
132
|
)}
|
|
132
133
|
</div>
|
|
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef } from "react";
|
|
|
4
4
|
import { csrfHeaders } from "../../../lib/csrf-client";
|
|
5
5
|
import type { FontFamily, FontVariant } from "../../../lib/sanity/types";
|
|
6
6
|
import { Section, SaveButton } from "./shared";
|
|
7
|
+
import { BubbleTooltip } from "../../builder/BubbleIcons";
|
|
7
8
|
|
|
8
9
|
export function FontsEditor({ fonts, onSave, saving }: { fonts: FontFamily[]; onSave: (fonts: FontFamily[]) => void; saving: boolean }) {
|
|
9
10
|
const [localFonts, setLocalFonts] = useState<FontFamily[]>(fonts);
|
|
@@ -204,8 +205,9 @@ export function FontsEditor({ fonts, onSave, saving }: { fonts: FontFamily[]; on
|
|
|
204
205
|
<option value="normal">Normal</option>
|
|
205
206
|
<option value="italic">Italic</option>
|
|
206
207
|
</select>
|
|
207
|
-
<span className="text-neutral-400 truncate flex-1"
|
|
208
|
+
<span className="group/bb relative text-neutral-400 truncate flex-1" aria-label={v.original_filename}>
|
|
208
209
|
{v.original_filename}
|
|
210
|
+
<BubbleTooltip>{v.original_filename}</BubbleTooltip>
|
|
209
211
|
</span>
|
|
210
212
|
{!font.is_builtin && (
|
|
211
213
|
<button
|
|
@@ -21,7 +21,7 @@ import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
|
|
|
21
21
|
import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
|
|
22
22
|
import BlockRenderer from "./BlockRenderer";
|
|
23
23
|
import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
24
|
-
import { getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign } from "../../lib/builder/layout-styles";
|
|
24
|
+
import { getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, getBackgroundStyles, getBorderStyles } from "../../lib/builder/layout-styles";
|
|
25
25
|
import { assetUrl } from "../../lib/assets";
|
|
26
26
|
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
27
27
|
import { normalizeRowHeights } from "../../lib/builder/store-cover";
|
|
@@ -305,6 +305,11 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
305
305
|
const alignSelf = rowAlign === "center" ? "center" : rowAlign === "end" ? "end" : "start";
|
|
306
306
|
const colJustify = getColumnVerticalAlign(col.blocks || []);
|
|
307
307
|
|
|
308
|
+
const colLayoutStyles = {
|
|
309
|
+
...getBackgroundStyles(col, process.env.NEXT_PUBLIC_ASSET_BASE_URL),
|
|
310
|
+
...getBorderStyles(col),
|
|
311
|
+
};
|
|
312
|
+
|
|
308
313
|
const columnContent = (
|
|
309
314
|
<div
|
|
310
315
|
key={col._key}
|
|
@@ -319,6 +324,7 @@ export default function CoverSectionRenderer({ section, pageEnterAnimation }: Co
|
|
|
319
324
|
height: "100%",
|
|
320
325
|
minWidth: 0,
|
|
321
326
|
overflow: "hidden",
|
|
327
|
+
...colLayoutStyles,
|
|
322
328
|
}}
|
|
323
329
|
>
|
|
324
330
|
{(col.blocks || []).map((block) => {
|
|
@@ -24,7 +24,7 @@ import type { EnterAnimationConfig } from "../../lib/animation/enter-types";
|
|
|
24
24
|
import { resolveEnterAnimation } from "../../lib/animation/enter-resolve";
|
|
25
25
|
import BlockRenderer from "./BlockRenderer";
|
|
26
26
|
import EnterAnimationWrapper from "./EnterAnimationWrapper";
|
|
27
|
-
import { getRowLayoutStyles, getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, hexToRgba } from "../../lib/builder/layout-styles";
|
|
27
|
+
import { getRowLayoutStyles, getBlockAlignmentStyles, hasBlockAlignment, getColumnVerticalAlign, getBackgroundStyles, getBorderStyles, hexToRgba } from "../../lib/builder/layout-styles";
|
|
28
28
|
import { parseColorField, colorToOverrideRule, borderColorToOverrideRule } from "../../lib/color-utils";
|
|
29
29
|
import { BREAKPOINTS } from "../../lib/builder/constants";
|
|
30
30
|
|
|
@@ -261,6 +261,12 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
261
261
|
// Column-level vertical alignment from blocks' align_v settings
|
|
262
262
|
const colJustify = getColumnVerticalAlign(col.blocks || []);
|
|
263
263
|
|
|
264
|
+
// Column-level background + border (desktop-only).
|
|
265
|
+
const colLayoutStyles = {
|
|
266
|
+
...getBackgroundStyles(col, process.env.NEXT_PUBLIC_ASSET_BASE_URL),
|
|
267
|
+
...getBorderStyles(col),
|
|
268
|
+
};
|
|
269
|
+
|
|
264
270
|
const columnContent = (
|
|
265
271
|
<div
|
|
266
272
|
key={col._key}
|
|
@@ -275,6 +281,7 @@ export default function SectionV2Renderer({ section, pageEnterAnimation }: Secti
|
|
|
275
281
|
height: "100%",
|
|
276
282
|
minWidth: 0,
|
|
277
283
|
overflow: "hidden",
|
|
284
|
+
...colLayoutStyles,
|
|
278
285
|
}}
|
|
279
286
|
>
|
|
280
287
|
{(col.blocks || []).map((block) => {
|
|
@@ -88,3 +88,17 @@ export function BubbleTooltip({ children }: { children: ReactNode }) {
|
|
|
88
88
|
</span>
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
|
+
|
|
92
|
+
/** Same styling as BubbleTooltip but positioned ABOVE the trigger.
|
|
93
|
+
* For bottom-anchored toolbars (e.g. the floating canvas toolbar). */
|
|
94
|
+
export function BubbleTooltipAbove({ children }: { children: ReactNode }) {
|
|
95
|
+
return (
|
|
96
|
+
<span
|
|
97
|
+
role="tooltip"
|
|
98
|
+
className="pointer-events-none absolute left-1/2 bottom-full z-50 mb-1.5 whitespace-nowrap rounded-md border border-white/10 bg-[#2a2d33] px-2.5 py-1.5 text-[11px] font-medium text-white shadow-lg opacity-0 transition-[opacity,margin] duration-150 ease-out group-hover/bb:opacity-100 group-hover/bb:mb-3"
|
|
99
|
+
style={{ transform: "translateX(-50%)" }}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</span>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
5
5
|
import { DEVICE_WIDTHS } from "../../lib/builder/types";
|
|
6
|
+
import { BubbleTooltipAbove } from "./BubbleIcons";
|
|
6
7
|
|
|
7
8
|
// ============================================
|
|
8
9
|
// CanvasMinimap — Shows viewport position in canvas
|
|
@@ -121,80 +122,96 @@ export default function CanvasMinimap({
|
|
|
121
122
|
return (
|
|
122
123
|
<button
|
|
123
124
|
onClick={() => setIsCollapsed(false)}
|
|
124
|
-
className="absolute bottom-6 right-4 z-40 w-8 h-8 flex items-center justify-center rounded-lg bg-[#1a1a1a]/80 text-white/60 hover:text-white hover:bg-[#1a1a1a] transition-colors shadow-lg backdrop-blur-sm"
|
|
125
|
-
title="Show minimap"
|
|
125
|
+
className="group/bb absolute bottom-6 right-4 z-40 w-8 h-8 flex items-center justify-center rounded-lg bg-[#1a1a1a]/80 text-white/60 hover:text-white hover:bg-[#1a1a1a] transition-colors shadow-lg backdrop-blur-sm"
|
|
126
126
|
aria-label="Show minimap"
|
|
127
127
|
>
|
|
128
128
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
|
|
129
129
|
<rect x="1" y="1" width="12" height="12" rx="1.5" stroke="currentColor" strokeWidth="1.2" />
|
|
130
130
|
<rect x="3" y="3" width="4" height="3" rx="0.5" stroke="currentColor" strokeWidth="0.8" opacity="0.6" />
|
|
131
131
|
</svg>
|
|
132
|
+
<BubbleTooltipAbove>Show minimap</BubbleTooltipAbove>
|
|
132
133
|
</button>
|
|
133
134
|
);
|
|
134
135
|
}
|
|
135
136
|
|
|
136
137
|
return (
|
|
137
138
|
<div
|
|
138
|
-
className="absolute bottom-6 right-4 z-40 rounded-lg overflow-hidden shadow-
|
|
139
|
+
className="absolute bottom-6 right-4 z-40 rounded-lg overflow-hidden shadow-xl backdrop-blur-sm"
|
|
139
140
|
style={{
|
|
140
141
|
width: MINIMAP_WIDTH,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
142
|
+
backgroundColor: "rgba(26, 26, 26, 0.92)",
|
|
143
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
144
144
|
userSelect: "none",
|
|
145
145
|
}}
|
|
146
|
-
onMouseDown={handleMinimapMouseDown}
|
|
147
|
-
onMouseMove={handleMouseMove}
|
|
148
|
-
onMouseUp={handleMouseUp}
|
|
149
|
-
onMouseLeave={handleMouseUp}
|
|
150
146
|
>
|
|
151
|
-
{/*
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
147
|
+
{/* Header — label + close button. Matches the toolbar's dark pill aesthetic. */}
|
|
148
|
+
<div
|
|
149
|
+
className="flex items-center justify-between px-2 py-1"
|
|
150
|
+
style={{
|
|
151
|
+
borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
|
|
152
|
+
background: "rgba(255, 255, 255, 0.02)",
|
|
156
153
|
}}
|
|
157
|
-
className="absolute top-1 right-1 z-50 w-4 h-4 flex items-center justify-center rounded text-white/40 hover:text-white/80 transition-colors"
|
|
158
|
-
title="Hide minimap"
|
|
159
|
-
aria-label="Hide minimap"
|
|
160
154
|
>
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
</
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
155
|
+
<span className="text-[9px] font-semibold tracking-[0.08em] uppercase text-white/45">
|
|
156
|
+
Overview
|
|
157
|
+
</span>
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => setIsCollapsed(true)}
|
|
160
|
+
className="group/bb relative w-4 h-4 flex items-center justify-center rounded text-white/40 hover:text-white/90 hover:bg-white/10 transition-colors"
|
|
161
|
+
aria-label="Hide minimap"
|
|
162
|
+
>
|
|
163
|
+
<svg width="8" height="8" viewBox="0 0 8 8" fill="none">
|
|
164
|
+
<path d="M1 1L7 7M7 1L1 7" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
165
|
+
</svg>
|
|
166
|
+
<BubbleTooltipAbove>Hide minimap</BubbleTooltipAbove>
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
{/* Content area — all pan/drag interactions live here (not the header). */}
|
|
171
|
+
<div
|
|
172
|
+
className="relative"
|
|
173
|
+
style={{
|
|
174
|
+
width: MINIMAP_WIDTH,
|
|
175
|
+
height: MINIMAP_HEIGHT,
|
|
176
|
+
}}
|
|
177
|
+
onMouseDown={handleMinimapMouseDown}
|
|
178
|
+
onMouseMove={handleMouseMove}
|
|
179
|
+
onMouseUp={handleMouseUp}
|
|
180
|
+
onMouseLeave={handleMouseUp}
|
|
181
|
+
>
|
|
182
|
+
{/* Device frame outlines */}
|
|
183
|
+
{frames.map((f, i) => (
|
|
184
|
+
<div
|
|
185
|
+
key={i}
|
|
186
|
+
className="absolute"
|
|
187
|
+
style={{
|
|
188
|
+
left: f.x,
|
|
189
|
+
top: 8 * scale,
|
|
190
|
+
width: f.w,
|
|
191
|
+
height: (contentEstimateHeight - 40) * scale,
|
|
192
|
+
border: `1px solid ${f.active ? "rgba(53, 128, 249, 0.6)" : "rgba(255, 255, 255, 0.15)"}`,
|
|
193
|
+
borderRadius: 2,
|
|
194
|
+
backgroundColor: f.active ? "rgba(53, 128, 249, 0.08)" : "rgba(255, 255, 255, 0.03)",
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
))}
|
|
198
|
+
|
|
199
|
+
{/* Viewport indicator */}
|
|
168
200
|
<div
|
|
169
|
-
key={i}
|
|
170
201
|
className="absolute"
|
|
171
202
|
style={{
|
|
172
|
-
left:
|
|
173
|
-
top:
|
|
174
|
-
width:
|
|
175
|
-
height: (
|
|
176
|
-
border:
|
|
203
|
+
left: Math.max(0, viewX),
|
|
204
|
+
top: Math.max(0, viewY),
|
|
205
|
+
width: Math.min(viewW, MINIMAP_WIDTH),
|
|
206
|
+
height: Math.min(viewH, MINIMAP_HEIGHT),
|
|
207
|
+
border: "1.5px solid rgba(237, 56, 33, 0.7)",
|
|
177
208
|
borderRadius: 2,
|
|
178
|
-
backgroundColor:
|
|
209
|
+
backgroundColor: "rgba(237, 56, 33, 0.06)",
|
|
210
|
+
cursor: isDragging ? "grabbing" : "grab",
|
|
211
|
+
pointerEvents: "none",
|
|
179
212
|
}}
|
|
180
213
|
/>
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
{/* Viewport indicator */}
|
|
184
|
-
<div
|
|
185
|
-
className="absolute"
|
|
186
|
-
style={{
|
|
187
|
-
left: Math.max(0, viewX),
|
|
188
|
-
top: Math.max(0, viewY),
|
|
189
|
-
width: Math.min(viewW, MINIMAP_WIDTH),
|
|
190
|
-
height: Math.min(viewH, MINIMAP_HEIGHT),
|
|
191
|
-
border: "1.5px solid rgba(237, 56, 33, 0.7)",
|
|
192
|
-
borderRadius: 2,
|
|
193
|
-
backgroundColor: "rgba(237, 56, 33, 0.06)",
|
|
194
|
-
cursor: isDragging ? "grabbing" : "grab",
|
|
195
|
-
pointerEvents: "none",
|
|
196
|
-
}}
|
|
197
|
-
/>
|
|
214
|
+
</div>
|
|
198
215
|
</div>
|
|
199
216
|
);
|
|
200
217
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRef, useState, useEffect, useCallback } from "react";
|
|
4
4
|
import { useBuilderStore } from "../../lib/builder/store";
|
|
5
|
+
import { BubbleTooltipAbove } from "./BubbleIcons";
|
|
5
6
|
|
|
6
7
|
// ============================================
|
|
7
8
|
// CanvasToolbar — Floating mini toolbar
|
|
@@ -72,66 +73,68 @@ export default function CanvasToolbar({ viewportRef, onAnimatedAction }: CanvasT
|
|
|
72
73
|
const zoomPercent = Math.round(zoom * 100);
|
|
73
74
|
|
|
74
75
|
return (
|
|
75
|
-
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-
|
|
76
|
+
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 z-40 flex items-center gap-0.5 rounded-full bg-[#1a1a1a] shadow-xl px-1.5 py-1.5"
|
|
76
77
|
style={{ userSelect: "none" }}
|
|
77
78
|
>
|
|
78
|
-
{/* Select tool — Figma-style filled cursor
|
|
79
|
+
{/* Select tool — Figma-style filled cursor.
|
|
80
|
+
Active state uses the app's primary blue so selection signals
|
|
81
|
+
match the rest of the builder (block/column accents). */}
|
|
79
82
|
<button
|
|
80
83
|
onClick={() => setCanvasTool("select")}
|
|
81
|
-
className={`flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
|
|
84
|
+
className={`group/bb relative flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
|
|
82
85
|
tool === "select"
|
|
83
|
-
? "bg-
|
|
86
|
+
? "bg-[#3580f9] text-white"
|
|
84
87
|
: "text-neutral-400 hover:text-white hover:bg-white/10"
|
|
85
88
|
}`}
|
|
86
|
-
title="Select tool (V)"
|
|
87
89
|
aria-label="Select tool"
|
|
88
90
|
>
|
|
89
91
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round">
|
|
90
92
|
<path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z" />
|
|
91
93
|
</svg>
|
|
94
|
+
<BubbleTooltipAbove>Select <span className="ml-1 text-white/55">V</span></BubbleTooltipAbove>
|
|
92
95
|
</button>
|
|
93
96
|
|
|
94
|
-
{/* Hand tool —
|
|
97
|
+
{/* Hand tool — tabler hand-stop (outline) */}
|
|
95
98
|
<button
|
|
96
99
|
onClick={() => setCanvasTool("hand")}
|
|
97
|
-
className={`flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
|
|
100
|
+
className={`group/bb relative flex items-center justify-center w-8 h-8 rounded-full transition-colors ${
|
|
98
101
|
tool === "hand"
|
|
99
|
-
? "bg-
|
|
102
|
+
? "bg-[#3580f9] text-white"
|
|
100
103
|
: "text-neutral-400 hover:text-white hover:bg-white/10"
|
|
101
104
|
}`}
|
|
102
|
-
title="Hand tool (H)"
|
|
103
105
|
aria-label="Hand tool"
|
|
104
106
|
>
|
|
105
|
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="
|
|
106
|
-
<path d="
|
|
107
|
-
<path d="
|
|
108
|
-
<path d="
|
|
109
|
-
<path d="
|
|
107
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
108
|
+
<path d="M8 13v-7.5a1.5 1.5 0 0 1 3 0v6.5" />
|
|
109
|
+
<path d="M11 5.5v-2a1.5 1.5 0 1 1 3 0v8.5" />
|
|
110
|
+
<path d="M14 5.5a1.5 1.5 0 0 1 3 0v6.5" />
|
|
111
|
+
<path d="M17 7.5a1.5 1.5 0 0 1 3 0v8.5a6 6 0 0 1 -6 6h-2h.208a6 6 0 0 1 -5.012 -2.7a69.74 69.74 0 0 1 -.196 -.3c-.312 -.479 -1.407 -2.388 -3.286 -5.728a1.5 1.5 0 0 1 .536 -2.022a1.867 1.867 0 0 1 2.28 .28l1.47 1.47" />
|
|
110
112
|
</svg>
|
|
113
|
+
<BubbleTooltipAbove>Hand <span className="ml-1 text-white/55">H</span></BubbleTooltipAbove>
|
|
111
114
|
</button>
|
|
112
115
|
|
|
113
116
|
{/* Divider */}
|
|
114
|
-
<div className="w-px h-5 bg-white/
|
|
117
|
+
<div className="w-px h-5 bg-white/15 mx-2" />
|
|
115
118
|
|
|
116
119
|
{/* Zoom out */}
|
|
117
120
|
<button
|
|
118
121
|
onClick={handleZoomOut}
|
|
119
|
-
className="flex items-center justify-center w-7 h-8 text-neutral-400 hover:text-white transition-colors text-sm"
|
|
120
|
-
title="Zoom out (Ctrl + −)"
|
|
122
|
+
className="group/bb relative flex items-center justify-center w-7 h-8 text-neutral-400 hover:text-white transition-colors text-sm"
|
|
121
123
|
aria-label="Zoom out"
|
|
122
124
|
>
|
|
123
125
|
−
|
|
126
|
+
<BubbleTooltipAbove>Zoom out <span className="ml-1 text-white/55">Ctrl −</span></BubbleTooltipAbove>
|
|
124
127
|
</button>
|
|
125
128
|
|
|
126
129
|
{/* Zoom percentage (clickable dropdown) */}
|
|
127
130
|
<div className="relative" ref={presetsRef}>
|
|
128
131
|
<button
|
|
129
132
|
onClick={() => setShowPresets((v) => !v)}
|
|
130
|
-
className="flex items-center justify-center min-w-[48px] h-8 text-white text-[11px] font-medium hover:bg-white/10 rounded-md transition-colors px-1"
|
|
131
|
-
title="Click to select zoom level"
|
|
133
|
+
className="group/bb relative flex items-center justify-center min-w-[48px] h-8 text-white text-[11px] font-medium hover:bg-white/10 rounded-md transition-colors px-1"
|
|
132
134
|
aria-label="Select zoom level"
|
|
133
135
|
>
|
|
134
136
|
{zoomPercent}%
|
|
137
|
+
<BubbleTooltipAbove>Zoom level</BubbleTooltipAbove>
|
|
135
138
|
</button>
|
|
136
139
|
|
|
137
140
|
{showPresets && (
|
|
@@ -156,40 +159,27 @@ export default function CanvasToolbar({ viewportRef, onAnimatedAction }: CanvasT
|
|
|
156
159
|
{/* Zoom in */}
|
|
157
160
|
<button
|
|
158
161
|
onClick={handleZoomIn}
|
|
159
|
-
className="flex items-center justify-center w-7 h-8 text-neutral-400 hover:text-white transition-colors text-sm"
|
|
160
|
-
title="Zoom in (Ctrl + +)"
|
|
162
|
+
className="group/bb relative flex items-center justify-center w-7 h-8 text-neutral-400 hover:text-white transition-colors text-sm"
|
|
161
163
|
aria-label="Zoom in"
|
|
162
164
|
>
|
|
163
165
|
+
|
|
166
|
+
<BubbleTooltipAbove>Zoom in <span className="ml-1 text-white/55">Ctrl +</span></BubbleTooltipAbove>
|
|
164
167
|
</button>
|
|
165
168
|
|
|
166
169
|
{/* Divider */}
|
|
167
|
-
<div className="w-px h-5 bg-white/
|
|
170
|
+
<div className="w-px h-5 bg-white/15 mx-2" />
|
|
168
171
|
|
|
169
|
-
{/* Fit */}
|
|
172
|
+
{/* Fit — tabler zoom (magnifying glass, outline) */}
|
|
170
173
|
<button
|
|
171
174
|
onClick={handleFit}
|
|
172
|
-
className="flex items-center justify-center w-8 h-8 rounded-full text-neutral-400 hover:text-white hover:bg-white/10 transition-colors"
|
|
173
|
-
title="Zoom to fit (Ctrl + 0)"
|
|
175
|
+
className="group/bb relative flex items-center justify-center w-8 h-8 rounded-full text-neutral-400 hover:text-white hover:bg-white/10 transition-colors"
|
|
174
176
|
aria-label="Zoom to fit"
|
|
175
177
|
>
|
|
176
|
-
<svg width="
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
y="2"
|
|
180
|
-
width="10"
|
|
181
|
-
height="10"
|
|
182
|
-
rx="1.5"
|
|
183
|
-
stroke="currentColor"
|
|
184
|
-
strokeWidth="1.5"
|
|
185
|
-
/>
|
|
186
|
-
<path
|
|
187
|
-
d="M5 2V0M9 2V0M5 14V12M9 14V12M0 5H2M0 9H2M12 5H14M12 9H14"
|
|
188
|
-
stroke="currentColor"
|
|
189
|
-
strokeWidth="1"
|
|
190
|
-
opacity="0.5"
|
|
191
|
-
/>
|
|
178
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
179
|
+
<path d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
|
|
180
|
+
<path d="M21 21l-6 -6" />
|
|
192
181
|
</svg>
|
|
182
|
+
<BubbleTooltipAbove>Zoom to fit <span className="ml-1 text-white/55">Ctrl 0</span></BubbleTooltipAbove>
|
|
193
183
|
</button>
|
|
194
184
|
</div>
|
|
195
185
|
);
|