diff --git a/components/canvas/DetailSurfaceOverlayRenderer.tsx b/components/canvas/DetailSurfaceOverlayRenderer.tsx
index b511a7ea8..f7e1f36c7 100644
--- a/components/canvas/DetailSurfaceOverlayRenderer.tsx
+++ b/components/canvas/DetailSurfaceOverlayRenderer.tsx
@@ -1,6 +1,7 @@
"use client";
import type { CSSProperties } from "react";
+import { ArrowRight, CheckCircle2, ShieldCheck, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
import type { DetailUiScreenOverlaySnapshot } from "@/lib/detail-ui-surface-adapter";
@@ -58,6 +59,90 @@ function resolveOverlayText(
return String(component.props?.content || "");
}
+function renderOverlayComponentContent(
+ snapshot: DetailUiScreenOverlaySnapshot,
+ component: RenderableOverlayComponent,
+) {
+ const content = resolveOverlayText(snapshot, component);
+ const lines = content.split("\n").filter(Boolean);
+
+ if (component.type === "detail_feature_card") {
+ const headline = lines[0] || content;
+ const detail = lines.slice(1).join("\n");
+ return (
+
+
+
+
+
+
+
{headline}
+ {detail ? (
+
+ {detail}
+
+ ) : null}
+
+
+
+ );
+ }
+
+ if (component.type === "detail_feature_tag") {
+ return (
+
+
+ {content}
+
+ );
+ }
+
+ if (component.type === "detail_compare_before" || component.type === "detail_compare_after") {
+ const title = lines[0] || "";
+ const body = lines.slice(1).join("\n");
+ return (
+
+
+ {title}
+
+
+
+ {body}
+
+
+ );
+ }
+
+ if (component.type === "detail_proof_bar") {
+ return (
+
+ );
+ }
+
+ if (component.type === "detail_cta_button") {
+ return (
+
+ );
+ }
+
+ if (component.type === "detail_cta_support" || component.type === "detail_cta_banner") {
+ return (
+
+
{content}
+ {component.type === "detail_cta_banner" ?
: null}
+
+ );
+ }
+
+ return content;
+}
+
function getOverlayComponentStyle(params: {
component: RenderableOverlayComponent;
snapshot: DetailUiScreenOverlaySnapshot;
@@ -198,7 +283,7 @@ export function DetailSurfaceOverlayRenderer(params: {
content,
})}
>
- {content}
+ {renderOverlayComponentContent(params.overlay!, component)}
);
})}
diff --git a/lib/detail-ui-design-tokens.ts b/lib/detail-ui-design-tokens.ts
new file mode 100644
index 000000000..a5a4e4e3e
--- /dev/null
+++ b/lib/detail-ui-design-tokens.ts
@@ -0,0 +1,244 @@
+export type DetailUiTokenVariant = "hero_left" | "hero_right" | "split_bottom";
+
+export interface DetailUiDesignTokens {
+ canvas: {
+ sectionGap: number;
+ sectionRadius: number;
+ };
+ typography: {
+ title: {
+ fontSize: number;
+ fontWeight: string;
+ lineHeight: number;
+ letterSpacing: number;
+ color: string;
+ };
+ subtitle: {
+ fontSize: number;
+ fontWeight: string;
+ lineHeight: number;
+ color: string;
+ };
+ body: {
+ fontSize: number;
+ fontWeight: string;
+ lineHeight: number;
+ color: string;
+ };
+ label: {
+ fontSize: number;
+ fontWeight: string;
+ lineHeight: number;
+ color: string;
+ };
+ };
+ badge: {
+ backgroundColor: string;
+ color: string;
+ borderColor: string;
+ };
+ heroCard: {
+ backgroundColor: string;
+ borderColor: string;
+ shadow: string;
+ };
+ subtitleCard: {
+ backgroundColor: string;
+ borderColor: string;
+ shadow: string;
+ color: string;
+ };
+ featureTag: {
+ primary: { backgroundColor: string; color: string; borderColor: string };
+ secondary: { backgroundColor: string; color: string; borderColor: string };
+ neutral: { backgroundColor: string; color: string; borderColor: string };
+ };
+ featureCard: {
+ primary: { backgroundColor: string; borderColor: string; shadow: string };
+ secondary: { backgroundColor: string; borderColor: string; shadow: string };
+ neutral: { backgroundColor: string; borderColor: string; shadow: string };
+ };
+ table: {
+ labelBackground: string;
+ labelBorder: string;
+ valueBackground: string;
+ valueBorder: string;
+ sectionShadow: string;
+ };
+ compare: {
+ beforeBackground: string;
+ beforeBorder: string;
+ beforeColor: string;
+ afterBackground: string;
+ afterBorder: string;
+ afterColor: string;
+ };
+ proof: {
+ backgroundColor: string;
+ borderColor: string;
+ color: string;
+ };
+ cta: {
+ supportBackground: string;
+ supportBorder: string;
+ supportColor: string;
+ buttonBackground: string;
+ buttonBorder: string;
+ buttonColor: string;
+ bannerBackground: string;
+ bannerBorder: string;
+ bannerColor: string;
+ };
+}
+
+const BASE_TOKENS: DetailUiDesignTokens = {
+ canvas: {
+ sectionGap: 16,
+ sectionRadius: 28,
+ },
+ typography: {
+ title: {
+ fontSize: 52,
+ fontWeight: "800",
+ lineHeight: 1.08,
+ letterSpacing: -0.8,
+ color: "#0f172a",
+ },
+ subtitle: {
+ fontSize: 28,
+ fontWeight: "700",
+ lineHeight: 1.18,
+ color: "#f8fafc",
+ },
+ body: {
+ fontSize: 18,
+ fontWeight: "600",
+ lineHeight: 1.34,
+ color: "#111827",
+ },
+ label: {
+ fontSize: 14,
+ fontWeight: "700",
+ lineHeight: 1.15,
+ color: "#334155",
+ },
+ },
+ badge: {
+ backgroundColor: "rgba(15,23,42,0.82)",
+ color: "#f8fafc",
+ borderColor: "rgba(255,255,255,0.18)",
+ },
+ heroCard: {
+ backgroundColor: "rgba(255,255,255,0.88)",
+ borderColor: "rgba(255,255,255,0.42)",
+ shadow: "0 22px 58px rgba(15,23,42,0.18)",
+ },
+ subtitleCard: {
+ backgroundColor: "rgba(30,41,59,0.78)",
+ borderColor: "rgba(255,255,255,0.18)",
+ shadow: "0 18px 42px rgba(15,23,42,0.18)",
+ color: "#f8fafc",
+ },
+ featureTag: {
+ primary: {
+ backgroundColor: "rgba(219,234,254,0.96)",
+ color: "#1d4ed8",
+ borderColor: "rgba(59,130,246,0.24)",
+ },
+ secondary: {
+ backgroundColor: "rgba(237,233,254,0.96)",
+ color: "#7c3aed",
+ borderColor: "rgba(124,58,237,0.24)",
+ },
+ neutral: {
+ backgroundColor: "rgba(241,245,249,0.96)",
+ color: "#334155",
+ borderColor: "rgba(148,163,184,0.24)",
+ },
+ },
+ featureCard: {
+ primary: {
+ backgroundColor: "rgba(255,255,255,0.94)",
+ borderColor: "rgba(59,130,246,0.42)",
+ shadow: "0 18px 42px rgba(15,23,42,0.12)",
+ },
+ secondary: {
+ backgroundColor: "rgba(248,250,252,0.92)",
+ borderColor: "rgba(124,58,237,0.34)",
+ shadow: "0 18px 42px rgba(15,23,42,0.10)",
+ },
+ neutral: {
+ backgroundColor: "rgba(248,250,252,0.82)",
+ borderColor: "rgba(148,163,184,0.28)",
+ shadow: "0 18px 42px rgba(15,23,42,0.10)",
+ },
+ },
+ table: {
+ labelBackground: "rgba(241,245,249,0.96)",
+ labelBorder: "rgba(148,163,184,0.22)",
+ valueBackground: "rgba(255,255,255,0.94)",
+ valueBorder: "rgba(59,130,246,0.16)",
+ sectionShadow: "0 18px 42px rgba(15,23,42,0.12)",
+ },
+ compare: {
+ beforeBackground: "rgba(255,255,255,0.92)",
+ beforeBorder: "rgba(148,163,184,0.20)",
+ beforeColor: "#334155",
+ afterBackground: "rgba(37,99,235,0.88)",
+ afterBorder: "rgba(255,255,255,0.18)",
+ afterColor: "#eff6ff",
+ },
+ proof: {
+ backgroundColor: "rgba(255,255,255,0.88)",
+ borderColor: "rgba(15,23,42,0.10)",
+ color: "#1e293b",
+ },
+ cta: {
+ supportBackground: "rgba(255,255,255,0.88)",
+ supportBorder: "rgba(37,99,235,0.14)",
+ supportColor: "#0f172a",
+ buttonBackground: "rgba(37,99,235,0.94)",
+ buttonBorder: "rgba(255,255,255,0.24)",
+ buttonColor: "#f8fafc",
+ bannerBackground: "rgba(15,23,42,0.74)",
+ bannerBorder: "rgba(255,255,255,0.16)",
+ bannerColor: "#eff6ff",
+ },
+};
+
+export function getDetailUiDesignTokens(params: {
+ variant: DetailUiTokenVariant;
+ screenType?: string;
+ isFirstScreen?: boolean;
+}) {
+ const tokens: DetailUiDesignTokens = JSON.parse(JSON.stringify(BASE_TOKENS));
+
+ if (params.variant === "hero_right") {
+ tokens.subtitleCard.backgroundColor = "rgba(37,99,235,0.82)";
+ tokens.heroCard.shadow = "0 24px 60px rgba(15,23,42,0.20)";
+ }
+
+ if (params.variant === "split_bottom") {
+ tokens.featureCard.primary.backgroundColor = "rgba(255,255,255,0.96)";
+ tokens.featureCard.secondary.backgroundColor = "rgba(255,255,255,0.94)";
+ tokens.featureCard.neutral.backgroundColor = "rgba(255,255,255,0.92)";
+ tokens.canvas.sectionGap = 18;
+ }
+
+ if (params.screenType === "A") {
+ tokens.badge.backgroundColor = "rgba(15,23,42,0.90)";
+ tokens.compare.afterBackground = "rgba(15,23,42,0.92)";
+ }
+
+ if (params.screenType === "C") {
+ tokens.cta.buttonBackground = "rgba(124,58,237,0.94)";
+ tokens.cta.buttonBorder = "rgba(255,255,255,0.28)";
+ }
+
+ if (params.isFirstScreen) {
+ tokens.typography.title.fontSize = 58;
+ tokens.heroCard.shadow = "0 28px 70px rgba(15,23,42,0.22)";
+ }
+
+ return tokens;
+}
diff --git a/lib/detail-ui-surface-adapter.ts b/lib/detail-ui-surface-adapter.ts
index 512810230..40ac7f562 100644
--- a/lib/detail-ui-surface-adapter.ts
+++ b/lib/detail-ui-surface-adapter.ts
@@ -6,6 +6,7 @@ import type {
DetailUiScreenProtocol,
DetailUiScreenReviewPatch,
} from "@/lib/detail-ui-protocol";
+import { getDetailUiDesignTokens } from "@/lib/detail-ui-design-tokens";
import type {
UiSurfaceComponent,
UiSurfacePatch,
@@ -255,6 +256,11 @@ function buildSectionedTextComponents(params: {
screenIndex: params.screenIndex,
screenType: params.screen.screenType,
});
+ const tokens = getDetailUiDesignTokens({
+ variant,
+ screenType: params.screen.screenType,
+ isFirstScreen: params.screenIndex === 1,
+ });
const badgeLabel = buildScreenBadgeLabel({
screenIndex: params.screenIndex,
screenType: params.screen.screenType,
@@ -316,14 +322,14 @@ function buildSectionedTextComponents(params: {
content: badgeLabel,
role: "screen_badge",
style: {
- fontSize: 16,
+ fontSize: tokens.typography.label.fontSize + 2,
fontWeight: "700",
- color: "#f8fafc",
+ color: tokens.badge.color,
textAlign: "center",
lineHeight: 1.15,
letterSpacing: 0.4,
- backgroundColor: "rgba(15,23,42,0.82)",
- borderColor: "rgba(255,255,255,0.18)",
+ backgroundColor: tokens.badge.backgroundColor,
+ borderColor: tokens.badge.borderColor,
borderWidth: 1,
borderRadius: 999,
padding: 8,
@@ -354,18 +360,18 @@ function buildSectionedTextComponents(params: {
role: "title_card",
style: {
fontFamily: titleBlock?.style.fontFamily || undefined,
- fontSize: Math.max(34, Number(titleBlock?.style.fontSize || 52)),
- fontWeight: titleBlock?.style.fontWeight || "800",
- color: "#0f172a",
+ fontSize: Math.max(34, Number(titleBlock?.style.fontSize || tokens.typography.title.fontSize)),
+ fontWeight: titleBlock?.style.fontWeight || tokens.typography.title.fontWeight,
+ color: tokens.typography.title.color,
textAlign: "left",
- lineHeight: 1.08,
- letterSpacing: -0.8,
- backgroundColor: "rgba(255,255,255,0.86)",
- borderColor: "rgba(255,255,255,0.38)",
+ lineHeight: tokens.typography.title.lineHeight,
+ letterSpacing: tokens.typography.title.letterSpacing,
+ backgroundColor: tokens.heroCard.backgroundColor,
+ borderColor: tokens.heroCard.borderColor,
borderWidth: 1,
borderRadius: 28,
padding: 24,
- boxShadow: "0 22px 58px rgba(15,23,42,0.18)",
+ boxShadow: tokens.heroCard.shadow,
backdropBlur: 14,
},
metadata: {
@@ -392,18 +398,17 @@ function buildSectionedTextComponents(params: {
role: "subtitle_card",
style: {
fontFamily: subtitleBlock?.style.fontFamily || undefined,
- fontSize: Math.max(22, Number(subtitleBlock?.style.fontSize || 28)),
- fontWeight: subtitleBlock?.style.fontWeight || "700",
- color: "#f8fafc",
+ fontSize: Math.max(22, Number(subtitleBlock?.style.fontSize || tokens.typography.subtitle.fontSize)),
+ fontWeight: subtitleBlock?.style.fontWeight || tokens.typography.subtitle.fontWeight,
+ color: tokens.subtitleCard.color,
textAlign: "left",
- lineHeight: 1.18,
- backgroundColor:
- variant === "hero_right" ? "rgba(37,99,235,0.82)" : "rgba(30,41,59,0.78)",
- borderColor: "rgba(255,255,255,0.18)",
+ lineHeight: tokens.typography.subtitle.lineHeight,
+ backgroundColor: tokens.subtitleCard.backgroundColor,
+ borderColor: tokens.subtitleCard.borderColor,
borderWidth: 1,
borderRadius: 22,
padding: 18,
- boxShadow: "0 18px 42px rgba(15,23,42,0.18)",
+ boxShadow: tokens.subtitleCard.shadow,
backdropBlur: 12,
},
metadata: {
@@ -477,29 +482,29 @@ function buildSectionedTextComponents(params: {
zIndex: 3,
content: `✦ ${highlight.tag}`,
role: "feature_tag",
- style: {
- fontSize: 13,
- fontWeight: "700",
- color:
- highlight.accent === "primary"
- ? "#1d4ed8"
+ style: {
+ fontSize: 13,
+ fontWeight: "700",
+ color:
+ highlight.accent === "primary"
+ ? tokens.featureTag.primary.color
: highlight.accent === "secondary"
- ? "#7c3aed"
- : "#334155",
- textAlign: "center",
- lineHeight: 1.1,
- backgroundColor:
- highlight.accent === "primary"
- ? "rgba(219,234,254,0.96)"
+ ? tokens.featureTag.secondary.color
+ : tokens.featureTag.neutral.color,
+ textAlign: "center",
+ lineHeight: 1.1,
+ backgroundColor:
+ highlight.accent === "primary"
+ ? tokens.featureTag.primary.backgroundColor
: highlight.accent === "secondary"
- ? "rgba(237,233,254,0.96)"
- : "rgba(241,245,249,0.96)",
- borderColor:
- highlight.accent === "primary"
- ? "rgba(59,130,246,0.24)"
+ ? tokens.featureTag.secondary.backgroundColor
+ : tokens.featureTag.neutral.backgroundColor,
+ borderColor:
+ highlight.accent === "primary"
+ ? tokens.featureTag.primary.borderColor
: highlight.accent === "secondary"
- ? "rgba(124,58,237,0.24)"
- : "rgba(148,163,184,0.24)",
+ ? tokens.featureTag.secondary.borderColor
+ : tokens.featureTag.neutral.borderColor,
borderWidth: 1,
borderRadius: 999,
padding: 6,
@@ -526,25 +531,34 @@ function buildSectionedTextComponents(params: {
content: `${index + 1}. ${highlight.title}${highlight.detail ? `\n${highlight.detail}` : ""}`,
role: "support_card",
style: {
- fontSize: bodyBlock ? Math.max(18, Number(bodyBlock.style.fontSize || 22)) : 20,
+ fontSize: bodyBlock ? Math.max(18, Number(bodyBlock.style.fontSize || tokens.typography.body.fontSize)) : 20,
fontWeight: "600",
- color: "#111827",
+ color: tokens.typography.body.color,
textAlign: "left",
- lineHeight: 1.34,
+ lineHeight: tokens.typography.body.lineHeight,
backgroundColor:
variant === "split_bottom"
- ? "rgba(255,255,255,0.92)"
- : "rgba(248,250,252,0.82)",
+ ? tokens.featureCard.primary.backgroundColor
+ : highlight.accent === "secondary"
+ ? tokens.featureCard.secondary.backgroundColor
+ : highlight.accent === "primary"
+ ? tokens.featureCard.primary.backgroundColor
+ : tokens.featureCard.neutral.backgroundColor,
borderColor:
highlight.accent === "primary"
- ? "rgba(59,130,246,0.42)"
+ ? tokens.featureCard.primary.borderColor
: highlight.accent === "secondary"
- ? "rgba(124,58,237,0.34)"
- : "rgba(148,163,184,0.28)",
+ ? tokens.featureCard.secondary.borderColor
+ : tokens.featureCard.neutral.borderColor,
borderWidth: 1,
borderRadius: 24,
padding: 20,
- boxShadow: "0 18px 42px rgba(15,23,42,0.12)",
+ boxShadow:
+ highlight.accent === "primary"
+ ? tokens.featureCard.primary.shadow
+ : highlight.accent === "secondary"
+ ? tokens.featureCard.secondary.shadow
+ : tokens.featureCard.neutral.shadow,
backdropBlur: 12,
},
metadata: {
@@ -601,13 +615,13 @@ function buildSectionedTextComponents(params: {
content: row.label,
role: "table_label",
style: {
- fontSize: 14,
- fontWeight: "700",
- color: "#334155",
+ fontSize: tokens.typography.label.fontSize,
+ fontWeight: tokens.typography.label.fontWeight,
+ color: tokens.typography.label.color,
textAlign: "center",
- lineHeight: 1.15,
- backgroundColor: "rgba(241,245,249,0.96)",
- borderColor: "rgba(148,163,184,0.22)",
+ lineHeight: tokens.typography.label.lineHeight,
+ backgroundColor: tokens.table.labelBackground,
+ borderColor: tokens.table.labelBorder,
borderWidth: 1,
borderRadius: 14,
padding: 10,
@@ -636,15 +650,15 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 15,
fontWeight: "600",
- color: "#0f172a",
+ color: tokens.typography.title.color,
textAlign: "left",
lineHeight: 1.18,
- backgroundColor: "rgba(255,255,255,0.94)",
- borderColor: "rgba(59,130,246,0.16)",
+ backgroundColor: tokens.table.valueBackground,
+ borderColor: tokens.table.valueBorder,
borderWidth: 1,
borderRadius: 14,
padding: 12,
- boxShadow: "0 8px 22px rgba(15,23,42,0.08)",
+ boxShadow: tokens.table.sectionShadow,
},
metadata: {
screenIndex: params.screenIndex,
@@ -676,11 +690,11 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 17,
fontWeight: "700",
- color: "#334155",
+ color: tokens.compare.beforeColor,
textAlign: "left",
lineHeight: 1.4,
- backgroundColor: "rgba(255,255,255,0.92)",
- borderColor: "rgba(148,163,184,0.20)",
+ backgroundColor: tokens.compare.beforeBackground,
+ borderColor: tokens.compare.beforeBorder,
borderWidth: 1,
borderRadius: 26,
padding: 20,
@@ -711,11 +725,11 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 17,
fontWeight: "800",
- color: "#eff6ff",
+ color: tokens.compare.afterColor,
textAlign: "left",
lineHeight: 1.4,
- backgroundColor: "rgba(37,99,235,0.88)",
- borderColor: "rgba(255,255,255,0.18)",
+ backgroundColor: tokens.compare.afterBackground,
+ borderColor: tokens.compare.afterBorder,
borderWidth: 1,
borderRadius: 26,
padding: 20,
@@ -755,11 +769,11 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 16,
fontWeight: "600",
- color: "#1e293b",
+ color: tokens.proof.color,
textAlign: "left",
lineHeight: 1.22,
- backgroundColor: "rgba(255,255,255,0.88)",
- borderColor: "rgba(15,23,42,0.10)",
+ backgroundColor: tokens.proof.backgroundColor,
+ borderColor: tokens.proof.borderColor,
borderWidth: 1,
borderRadius: 18,
padding: 14,
@@ -792,11 +806,11 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 18,
fontWeight: "800",
- color: "#0f172a",
+ color: tokens.cta.supportColor,
textAlign: "left",
lineHeight: 1.2,
- backgroundColor: "rgba(255,255,255,0.88)",
- borderColor: "rgba(37,99,235,0.14)",
+ backgroundColor: tokens.cta.supportBackground,
+ borderColor: tokens.cta.supportBorder,
borderWidth: 1,
borderRadius: 24,
padding: 18,
@@ -827,12 +841,12 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 19,
fontWeight: "800",
- color: "#f8fafc",
+ color: tokens.cta.buttonColor,
textAlign: "center",
lineHeight: 1.05,
letterSpacing: 0.2,
- backgroundColor: "rgba(37,99,235,0.94)",
- borderColor: "rgba(255,255,255,0.24)",
+ backgroundColor: tokens.cta.buttonBackground,
+ borderColor: tokens.cta.buttonBorder,
borderWidth: 1,
borderRadius: 999,
padding: 18,
@@ -864,11 +878,11 @@ function buildSectionedTextComponents(params: {
style: {
fontSize: 15,
fontWeight: "600",
- color: "#eff6ff",
+ color: tokens.cta.bannerColor,
textAlign: "left",
lineHeight: 1.22,
- backgroundColor: "rgba(15,23,42,0.74)",
- borderColor: "rgba(255,255,255,0.16)",
+ backgroundColor: tokens.cta.bannerBackground,
+ borderColor: tokens.cta.bannerBorder,
borderWidth: 1,
borderRadius: 22,
padding: 16,