Merge pull request #543 from presenton/feat/new_template

feat: New template pitch deck
This commit is contained in:
Shiva Raj Badu 2026-04-26 22:53:10 +05:45 committed by GitHub
commit 34bae816d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 8414 additions and 497 deletions

View file

@ -147,7 +147,7 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
className={` w-full group font-syne `}
>
{/* <V1ContentRender slide={slide} isEditMode={true} theme={null} /> */}
<SlideScale slide={slide} theme={presentationData?.theme || null} />
<SlideScale slide={slide} theme={presentationData?.theme || null} />
{!showNewSlideSelection && (
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
<ToolTip content="Add new slide below">

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,195 @@
import * as z from "zod";
import { ImageSchema } from "@/app/presentation-templates/defaultSchemes";
export const slideLayoutId = "adaptive-media-card-grid";
export const slideLayoutName = "Adaptive Media Card Grid";
export const slideLayoutDescription =
"A responsive media-card grid that supports compact and dense arrangements.";
const CardSchema = z.object({
label: z.string().max(14).meta({
description: "Small top label shown on each card.",
}),
title: z.string().max(18).meta({
description: "Primary card title.",
}),
description: z.string().max(40).meta({
description: "Short supporting description.",
}),
image: ImageSchema.meta({
description: "Media image.",
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Top heading text.",
}),
cards: z
.array(CardSchema)
.max(8)
.default([
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=12",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=13",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=14",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=12",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=13",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=14",
__image_prompt__: "Media card image",
},
},
])
.meta({
description: "Media cards rendered in an adaptive grid.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const CARD_WIDTH_BY_COUNT: Record<number, number> = {
3: 308,
4: 227,
8: 227,
};
const IMAGE_HEIGHT_BY_COUNT: Record<number, number> = {
3: 301,
4: 222,
8: 222,
};
const AdaptiveMediaCardGrid = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const count = slideData.cards.length;
const columns = count <= 4 ? count : 4;
const cardWidth = CARD_WIDTH_BY_COUNT[count] ?? 236;
const imageHeight = IMAGE_HEIGHT_BY_COUNT[count] ?? 224;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div
className="px-[42px] "
style={{
marginTop: slideData.cards.length > 4 ? "10px" : "72px",
}}
>
<h2
className="font-serif text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="pt-[52px] px-[54px] grid justify-items-start gap-x-[30px] gap-y-[48px]"
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
>
{slideData.cards.map((card, index) => (
<div
key={`${card.title}-${index}`}
className="relative"
style={{ width: cardWidth }}
>
<img
src={card.image.__image_url__}
alt={card.image.__image_prompt__}
className="w-full object-cover"
style={{ height: imageHeight }}
/>
<div
className="absolute right-0 -bottom-10 mx-auto w-[66%] p-[13px] text-center"
style={{ backgroundColor: "var(--background-color,#27292d)" }}
>
<p
className="text-[19px] font-bold leading-none"
style={{ color: "var(--primary-color,#dddac7)" }}
>
{card.label}
</p>
<p
className="mt-[8px] text-[18px] leading-none"
style={{ color: "var(--primary-text,#d7d3be)" }}
>
{card.title}
</p>
<p
className="mt-[10px] text-[12px] "
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{card.description}
</p>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default AdaptiveMediaCardGrid;

View file

@ -0,0 +1,254 @@
import * as z from "zod";
export const slideLayoutId = "adaptive-value-card-grid";
export const slideLayoutName = "Adaptive Value Card Grid";
export const slideLayoutDescription =
"A card grid that supports even layouts and odd-count variants with an emphasized trailing card.";
const ValueCardSchema = z.object({
value: z.string().max(6).meta({
description: "Primary card value text.",
}),
label: z.string().max(28).meta({
description: "Secondary label under the card.",
}),
icon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string(),
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Top-left heading.",
}),
items: z
.array(ValueCardSchema)
.max(8)
.default([
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
// {
// value: "X 5", label: "Lorem ipsum dolor sit.", icon: {
// __icon_url__:
// "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
// __icon_query__: "check icon",
// }
// },
])
.meta({
description: "Value cards displayed in an adaptive grid.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const RIGHT_RATIO_BY_ODD_COUNT: Record<number, string> = {
3: "49%",
5: "33%",
7: "29%",
};
function Card({
value,
label,
icon,
}: {
value: string;
label: string;
icon: any;
}) {
return (
<div
className="flex h-full flex-col items-center justify-center border text-center p-[11px]"
style={{
borderColor: "var(--border-color,#8d8a7d)",
color: "var(--primary-text,#d7d3be)",
}}
>
<div className="flex items-center gap-[14px]">
<span
className="inline-flex items-center justify-center rounded-full"
style={{
width: 56,
height: 56,
backgroundColor: "var(--primary-color,#dddac7)",
color: "var(--background-color,#27292d)",
}}
>
<img
src={icon.__icon_url__}
alt={icon.__icon_query__}
className="w-7 h-7"
/>
</span>
<p className="text-[45px] font-semibold leading-none tracking-[0.01em]">
{value}
</p>
</div>
<p
className="mt-[14px] text-[30px] leading-[1.06]"
style={{ color: "var(--primary-text,#d7d3be)" }}
>
{label}
</p>
</div>
);
}
const AdaptiveValueCardGrid = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const count = slideData.items.length;
const isOdd = count % 2 === 1;
const leftCards = isOdd ? slideData.items.slice(0, -1) : slideData.items;
const rightTallCard = isOdd
? slideData.items[slideData.items.length - 1]
: null;
const evenColumns = Math.max(2, Math.min(4, count / 2));
const oddLeftColumns = Math.max(1, Math.ceil(leftCards.length / 2));
const rightRatio = RIGHT_RATIO_BY_ODD_COUNT[count] ?? "33%";
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[48px] pt-[72px]">
<h2
className=" text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--primary-text,#d7d3be)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
{!isOdd && (
<div
className="absolute left-[36px] right-[36px] top-[200px] grid gap-[21px]"
style={{
gridTemplateColumns: `repeat(${evenColumns}, minmax(0, 1fr))`,
gridAutoRows: "236px",
}}
>
{slideData.items.map((card, index) => (
<Card
key={`${card.value}-${index}`}
value={card.value}
label={card.label}
icon={card.icon}
/>
))}
</div>
)}
{isOdd && rightTallCard && (
<div
className="absolute left-[36px] right-[36px] top-[200px] grid h-[490px] gap-[21px]"
style={{ gridTemplateColumns: `minmax(0, 1fr) ${rightRatio}` }}
>
<div
className="grid gap-[18px]"
style={{
gridTemplateColumns: `repeat(${oddLeftColumns}, minmax(0, 1fr))`,
gridAutoRows: "236px",
}}
>
{leftCards.map((card, index) => (
<Card
key={`${card.value}-${index}`}
value={card.value}
label={card.label}
icon={card.icon}
/>
))}
</div>
<Card
value={rightTallCard.value}
label={rightTallCard.label}
icon={rightTallCard.icon}
/>
</div>
)}
</div>
</>
);
};
export default AdaptiveValueCardGrid;

View file

@ -0,0 +1,237 @@
"use client";
import * as z from "zod";
import PitchDeckChart from "./PitchDeckChart";
import { ChartPayloadSchema } from "./pitchDeckSchemas";
export const slideLayoutId = "cards-with-chart-split";
export const slideLayoutName = "Cards with Chart Split";
export const slideLayoutDescription =
"A split layout with cards on the left and a chart panel on the right.";
const DEFAULT_CHART = {
chartType: "line" as const,
legendLabel: "Series Label",
yAxisLabel: "Y axis name",
barData: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
],
pieData: [
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
],
scatterData: [
{ label: "label", value: 7 },
{ label: "label", value: 2 },
{ label: "label", value: 92 },
{ label: "label", value: 15 },
{ label: "label", value: 91 },
{ label: "label", value: 73 },
{ label: "label", value: 56 },
{ label: "label", value: 90 },
],
lineData: [
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
],
stackedBarData: [
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
],
};
const ValueCardSchema = z.object({
value: z.string().max(6).meta({
description: "Card value text.",
}),
label: z.string().max(28).meta({
description: "Card supporting label.",
}),
icon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string(),
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Main heading.",
}),
items: z
.array(ValueCardSchema)
.max(4)
.default([
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
])
.meta({
description: "Cards shown beside the chart.",
}),
chart: ChartPayloadSchema.default(DEFAULT_CHART).meta({
description: "Chart configuration for the right panel.",
}),
showAccentGlow: z.boolean().default(true).meta({
description:
"Whether to render the subtle decorative glow near bottom-left.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
function Card({
value,
label,
icon,
}: {
value: string;
label: string;
icon: { __icon_url__: string; __icon_query__: string };
}) {
return (
<div
className="flex h-full flex-col items-center justify-center border text-center"
style={{
borderColor: "var(--border-color,#8d8a7d)",
color: "var(--primary-text,#d7d3be)",
}}
>
<div className="flex items-center gap-[14px]">
<span
className="inline-flex items-center justify-center rounded-full"
style={{
width: 56,
height: 56,
backgroundColor: "var(--primary-color,#dddac7)",
color: "var(--background-color,#27292d)",
}}
>
<img
src={icon.__icon_url__}
alt={icon.__icon_query__}
className="w-7 h-7"
/>
</span>
<p className="text-[45px] font-semibold leading-none tracking-[0.01em]">
{value}
</p>
</div>
<p
className="mt-[14px] max-w-[82%] text-[30px] leading-[1.06]"
style={{ color: "var(--primary-text,#d7d3be)" }}
>
{label}
</p>
</div>
);
}
const CardsWithChartSplit = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<h2
className="px-[36px] pt-[72px] text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
<div
className="absolute bottom-[36px] left-[36px] right-[80px] top-[198px] grid min-h-0 gap-[32px]"
style={{ gridTemplateColumns: "minmax(0, 45fr) minmax(0, 55fr)" }}
>
<div className="min-h-0">
<div
className="grid grid-cols-2 gap-[20px]"
style={{ gridAutoRows: "216px" }}
>
{slideData.items.map((card, index) => (
<Card
key={`${card.value}-${index}`}
value={card.value}
label={card.label}
icon={card.icon}
/>
))}
</div>
</div>
<div className="h-full min-h-0 overflow-hidden pt-[10px]">
<PitchDeckChart payload={slideData.chart ?? DEFAULT_CHART} />
</div>
</div>
</div>
</>
);
};
export default CardsWithChartSplit;

View file

@ -0,0 +1,78 @@
import * as z from "zod";
export const slideLayoutId = "centered-cover-with-footer-meta";
export const slideLayoutName = "Centered Cover with Footer Metadata";
export const slideLayoutDescription =
"A single-focus cover layout with centered title, subtitle, and footer metadata groups.";
const FooterMetaSchema = z.object({
label: z.string().max(14).meta({
description: "Footer metadata label.",
}),
value: z.string().max(24).meta({
description: "Footer metadata value.",
}),
});
export const Schema = z.object({
title: z.string().max(20).default("Presentation").meta({
description: "Main centered cover title.",
}),
subtitle: z.string().max(34).default("WORKFORCE OPERATIONS").meta({
description: "Subtitle beneath the title.",
}),
footerItems: z
.array(FooterMetaSchema)
.max(2)
.default([
{ label: "PRESENTED BY", value: "PRESENTER NAME" },
{ label: "DATE", value: "2026 DECEMBER 4" },
])
.meta({
description: "Footer metadata groups.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const CenteredCoverWithFooterMeta = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
<div
className="relative h-[720px] w-[1280px] flex items-center justify-center overflow-hidden "
style={{ backgroundColor: "var(--background-color,#27292d)", fontFamily: "var(--body-font-family,'DM Serif Display')" }}
>
<div className=" text-center ">
<h1
className="text-[124px] leading-[124.6px]"
style={{ color: "var(--primary-color,#dddac7)", fontFamily: "var(--heading-font-family,'DM Serif Display')" }}
>
{slideData.title}
</h1>
<p className="mt-[22px] text-[33px] tracking-[0.02em]" style={{ color: "var(--primary-text,#d7d3be)" }}>
{slideData.subtitle}
</p>
</div>
<div className="absolute bottom-[34px] left-[36px] flex gap-[74px]" style={{ color: "var(--primary-text,#d7d3be)" }}>
{slideData.footerItems.map((item, index) => (
<div key={`${item.label}-${index}`}>
<p className="text-[14px] font-semibold tracking-[0.04em]" style={{ color: "var(--background-text,#cbc7b2)" }}>
{item.label}
</p>
<p className="mt-[12px] text-[15px] leading-none">{item.value}</p>
</div>
))}
</div>
</div>
</>
);
};
export default CenteredCoverWithFooterMeta;

View file

@ -0,0 +1,55 @@
import * as z from "zod";
export const slideLayoutId = "full-width-statement";
export const slideLayoutName = "Full-Width Statement";
export const slideLayoutDescription =
"A minimalist emphasis layout with a compact label and a large full-width statement block.";
export const Schema = z.object({
label: z.string().max(12).default("Label").meta({
description: "Small label above the statement.",
}),
statement: z
.string()
.max(96)
.default("This is a sample statement used for placeholder content in presentations.")
.meta({
description: "Main statement text.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const FullWidthStatement = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
<div
className="relative h-[720px] w-[1280px] flex flex-col justify-end pb-[74px] overflow-hidden "
style={{ backgroundColor: "var(--background-color,#27292d)", fontFamily: "var(--body-font-family,'DM Serif Display')" }}
>
<div className="px-[46px] ">
<p className="text-[32px] leading-none" style={{ color: "var(--primary-text,#d7d3be)" }}>
{slideData.label}
</p>
<p
className="mt-[61px] text-[100px] leading-[100%]"
style={{ color: "var(--primary-color,#dddac7)", fontFamily: "var(--heading-font-family,'DM Serif Display')" }}
>
{slideData.statement}
</p>
</div>
</div>
</>
);
};
export default FullWidthStatement;

View file

@ -0,0 +1,130 @@
import * as z from "zod";
export const slideLayoutId = "headline-with-detail-columns";
export const slideLayoutName = "Headline with Detail Columns";
export const slideLayoutDescription =
"A layout with a large headline and detail columns containing markers, text, and bullets.";
const SectionSchema = z.object({
number: z.string().max(2).meta({
description: "Numeric marker value.",
}),
title: z.string().max(16).meta({
description: "Section title.",
}),
description: z.string().max(150).meta({
description: "Section paragraph.",
}),
bullets: z.array(z.string().max(40)).max(4).meta({
description: "Bullet list content.",
}),
highlighted: z.boolean().default(false).meta({
description: "Whether the top marker is filled.",
}),
});
export const Schema = z.object({
title: z.string().max(24).default("Focus Areas").meta({
description: "Large left-side heading.",
}),
sections: z
.array(SectionSchema)
.max(2)
.default([
{
number: "1",
title: "Column A",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.",
bullets: [
"Ut enim ad minima veniam, quis nostrum",
"Exercitationem ullam corporis suscipit",
"Laboriosam, nisi ut alUt enim ad minima",
],
highlighted: true,
},
{
number: "2",
title: "Column B",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.",
bullets: [
"Ut enim ad minima veniam, quis nostrum",
"Exercitationem ullam corporis suscipit",
"Laboriosam, nisi ut alUt enim ad minima",
],
highlighted: false,
},
])
.meta({
description: "Right-side detail columns.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const HeadlineWithDetailColumns = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{ backgroundColor: "var(--background-color,#27292d)", fontFamily: "var(--body-font-family,'DM Serif Display')" }}
>
<div className="grid h-full grid-cols-[35%_34%_34%] gap-x-[8px] px-[36px] pt-[48px]">
<h2
className="whitespace-pre-line font-serif text-[100px] leading-[0.94] tracking-[-0.02em]"
style={{ color: "var(--primary-color,#dddac7)", fontFamily: "var(--heading-font-family,'DM Serif Display')" }}
>
{slideData.title}
</h2>
{slideData.sections.map((section, index) => (
<div key={`${section.title}-${index}`} className="pr-[20px]">
<div
className="flex h-[70px] w-[70px] rounded-full items-center justify-center text-[30px] font-semibold "
style={{
borderColor: section.highlighted ? "var(--primary-color,#dddac7)" : "var(--line-color,#4c4e53)",
color: section.highlighted ? "var(--background-color,#27292d)" : "var(--primary-color,#dddac7)",
backgroundColor: section.highlighted ? "var(--primary-color,#dddac7)" : "transparent",
}}
>
{section.number}
</div>
<p className="mt-[20px] text-[32px] leading-none" style={{ color: "var(--primary-text,#d7d3be)" }}>
{section.title}
</p>
<p className="mt-[20px] text-[22px] leading-[1.15]" style={{ color: "var(--background-text,#cbc7b2)" }}>
{section.description}
</p>
<div className="mt-[26px] space-y-[2px] pl-[16px] text-[22px] leading-[1.14]" style={{ color: "var(--background-text,#d7d3be)" }}>
{section.bullets.map((bullet, bulletIndex) => (
<p key={`${section.title}-bullet-${bulletIndex}`} className="flex gap-2 items-start">
<p className="w-1 h-1 rounded-full mt-3 "
style={{
background: 'var(--background-text,#ffffff)'
}}
/>
<p>{bullet}</p>
</p>
))}
</div>
</div>
))}
</div>
</div>
</>
);
};
export default HeadlineWithDetailColumns;

View file

@ -0,0 +1,315 @@
import * as z from "zod";
export const slideLayoutId = "horizontal-timeline";
export const slideLayoutName = "Horizontal Timeline";
export const slideLayoutDescription =
"A horizontal timeline with step markers, item text, continuation state, and optional endpoint label.";
const MAX_TIMELINE_ITEMS_PER_SLIDE = 5;
const DEFAULT_ICON = {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "shield check icon",
};
const TimelineItemSchema = z.object({
label: z.string().max(10).meta({
description: "Short label above marker icon.",
}),
icon: z
.object({
__icon_url__: z.string(),
__icon_query__: z.string(),
})
.default(DEFAULT_ICON),
title: z.string().max(16).meta({
description: "Heading below marker icon.",
}),
description: z.string().max(132).meta({
description: "Supporting copy for each timeline item.",
}),
});
const DEFAULT_DESCRIPTION =
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.";
export const Schema = z.object({
title: z.string().max(18).default("Timeline").meta({
description: "Top-left heading.",
}),
isContinue: z.boolean().default(false).meta({
description:
"Whether this slide continues a previous timeline slide. Continuation slides use the Continue... heading and draw the axis in from the left edge.",
}),
showEndLabel: z.boolean().default(true).meta({
description: "Whether to show right-end label near timeline axis.",
}),
endLabel: z.string().max(12).default("THE END").meta({
description: "Right-end label text.",
}),
items: z
.array(TimelineItemSchema)
.max(MAX_TIMELINE_ITEMS_PER_SLIDE)
.default([
{
label: "Phase 1",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 2",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 3",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 4",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 5",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
])
.meta({
description: "Timeline items from left to right.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const SLIDE_WIDTH = 1280;
const TIMELINE_AXIS_Y = 355;
const TIMELINE_LEFT_CENTER = 106;
const TIMELINE_RIGHT_CENTER = 1058;
const THREE_STEP_CENTERS = [106, 580, 1058];
const CONTINUATION_TITLE = "Continue...";
const getTimelineCenters = (count: number) => {
if (count === 3) {
return THREE_STEP_CENTERS;
}
if (count <= 1) {
return [TIMELINE_LEFT_CENTER];
}
return Array.from({ length: count }, (_, index) => {
return (
TIMELINE_LEFT_CENTER +
((TIMELINE_RIGHT_CENTER - TIMELINE_LEFT_CENTER) * index) / (count - 1)
);
});
};
const getTimelineStyle = (count: number) => {
if (count <= 3) {
return {
badgeSize: 88,
labelTop: 268,
titleTop: 423,
contentWidth: 260,
labelFontSize: 25,
titleFontSize: 25,
bodyFontSize: 17,
};
}
if (count === 4) {
return {
badgeSize: 76,
labelTop: 274,
titleTop: 417,
contentWidth: 240,
labelFontSize: 23,
titleFontSize: 25,
bodyFontSize: 17,
};
}
return {
badgeSize: 66,
labelTop: 280,
titleTop: 412,
contentWidth: 210,
labelFontSize: 20,
titleFontSize: 25,
bodyFontSize: 17,
};
};
function TimelineIconBadge({
icon,
size,
}: {
icon: { __icon_url__: string; __icon_query__: string };
size: number;
}) {
return (
<span
className="inline-flex items-center justify-center rounded-full"
style={{
width: size,
height: size,
backgroundColor: "var(--primary-color,#dddac7)",
}}
>
<img
src={icon.__icon_url__}
alt={icon.__icon_query__}
className="object-contain"
style={{ width: size * 0.42, height: size * 0.42 }}
/>
</span>
);
}
const HorizontalTimeline = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const timelineItems = slideData.items;
const timelineCenters = getTimelineCenters(timelineItems.length);
const timelineStyle = getTimelineStyle(timelineItems.length);
const firstCenter = timelineCenters[0] ?? TIMELINE_LEFT_CENTER;
const lastCenter =
timelineCenters[timelineCenters.length - 1] ?? TIMELINE_RIGHT_CENTER;
const axisStart = slideData.isContinue ? 0 : firstCenter;
const axisEnd = slideData.showEndLabel ? lastCenter : SLIDE_WIDTH;
const endLabelLeft = Math.min(
SLIDE_WIDTH - 152,
lastCenter + timelineStyle.badgeSize / 2 + 18
);
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[34px] pt-[40px]">
<h2
className="font-serif text-[100px] leading-none"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="absolute h-[2px]"
style={{
left: axisStart,
top: TIMELINE_AXIS_Y,
width: axisEnd - axisStart,
backgroundColor: "var(--primary-color,#dddac7)",
}}
/>
{timelineItems.map((phase, index) => {
const centerX = timelineCenters[index] ?? TIMELINE_LEFT_CENTER;
const textLeft = Math.max(
30,
Math.min(
SLIDE_WIDTH - timelineStyle.contentWidth - 30,
centerX - timelineStyle.badgeSize / 2
)
);
return (
<div key={`${phase.label}-${index}`}>
<p
className="absolute leading-none"
style={{
left: textLeft,
top: timelineStyle.labelTop,
color: "var(--primary-text,#d7d3be)",
fontSize: timelineStyle.labelFontSize,
}}
>
{phase.label}
</p>
<div
className="absolute"
style={{
left: centerX - timelineStyle.badgeSize / 2,
top: TIMELINE_AXIS_Y - timelineStyle.badgeSize / 2,
}}
>
<TimelineIconBadge
icon={phase.icon}
size={timelineStyle.badgeSize}
/>
</div>
<div
className="absolute"
style={{
left: textLeft,
top: timelineStyle.titleTop,
width: timelineStyle.contentWidth,
}}
>
<p
className="leading-none"
style={{
color: "var(--primary-text,#d7d3be)",
fontSize: timelineStyle.titleFontSize,
}}
>
{phase.title}
</p>
<p
className="mt-[12px] leading-[1.16]"
style={{
color: "var(--background-text,#cbc7b2)",
fontSize: timelineStyle.bodyFontSize,
}}
>
{phase.description}
</p>
</div>
</div>
);
})}
{slideData.showEndLabel && (
<p
className="absolute top-[346px] text-[25px] leading-none"
style={{
left: endLabelLeft,
color: "var(--primary-text,#d7d3be)",
}}
>
{slideData.endLabel}
</p>
)}
</div>
</>
);
};
export default HorizontalTimeline;

View file

@ -0,0 +1,105 @@
import * as z from "zod";
import { ImageSchema } from "@/app/presentation-templates/defaultSchemes";
export const slideLayoutId = "media-and-text-split";
export const slideLayoutName = "Media and Text Split";
export const slideLayoutDescription =
"A split composition with a title and media block on the left and supporting narrative plus footer text on the right.";
export const Schema = z.object({
title: z.string().max(16).default("Overview").meta({
description: "Left panel heading.",
}),
sidePanelMode: z.enum(["solid", "image"]).default("image").meta({
description: "Left media panel mode.",
}),
sidePanelColor: z.string().max(20).default("#d3d0bc").meta({
description: "Left media color used in solid mode.",
}),
sidePanelImage: ImageSchema.default({
__image_url__:
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1400&q=80",
__image_prompt__: "Glass skyscraper perspective",
}).meta({
description: "Left media image used in image mode.",
}),
headline: z
.string()
.max(50)
.default("This is a sample text to tell story for audience is written here")
.meta({
description: "Main headline text on the right.",
}),
body: z
.string()
.max(128)
.default(
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam."
)
.meta({
description: "Supporting paragraph text.",
}),
footerText: z.string().max(28).default("Footer text").meta({
description: "Footer text at the bottom-right.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const MediaAndTextSplit = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{ backgroundColor: "var(--background-color,#27292d)", fontFamily: "var(--body-font-family,'DM Serif Display')" }}
>
<h2
className="px-[38px] pt-[48px] text-[100px] leading-none tracking-[-0.02em]"
style={{ color: "var(--primary-color,#dddac7)", fontFamily: "var(--heading-font-family,'DM Serif Display')" }}
>
{slideData.title}
</h2>
<div className="flex items-center mt-[30px]">
<div
className=" w-[572px] h-[542px]"
style={{ backgroundColor: slideData.sidePanelMode === "solid" ? slideData.sidePanelColor : "transparent" }}
>
{slideData.sidePanelMode === "image" && (
<img
src={slideData.sidePanelImage.__image_url__}
alt={slideData.sidePanelImage.__image_prompt__}
className="h-full w-full object-cover"
/>
)}
</div>
<div className="px-[66px] flex-1 mt-[31px] flex flex-col h-full">
<div className="flex-1">
<h3 className="max-w-[610px] text-[32px] leading-[1.08]" style={{ color: "var(--primary-text,#d7d3be)" }}>
{slideData.headline}
</h3>
<p className="mt-[34px] max-w-[610px] text-[22px] leading-[1.16]" style={{ color: "var(--background-text,#cbc7b2)" }}>
{slideData.body}
</p>
</div>
<p className="mt-[100px] text-[34px] leading-none" style={{ color: "var(--primary-color,#dddac7)" }}>
{slideData.footerText}
</p>
</div>
</div>
</div>
</>
);
};
export default MediaAndTextSplit;

View file

@ -0,0 +1,97 @@
import * as z from "zod";
export const slideLayoutId = "numbered-multi-column-overview";
export const slideLayoutName = "Numbered Multi-Column Overview";
export const slideLayoutDescription =
"A multi-column layout with numbered markers, short titles, and descriptive body text.";
const ColumnSchema = z.object({
marker: z.string().max(2).meta({
description: "Circular marker value.",
}),
title: z.string().max(14).meta({
description: "Column title.",
}),
description: z.string().max(118).meta({
description: "Column description paragraph.",
}),
highlighted: z.boolean().default(false).meta({
description: "Whether marker circle is filled.",
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Overview").meta({
description: "Main heading text.",
}),
items: z
.array(ColumnSchema)
.max(4)
.default([
{ marker: "1", title: "Heading 1", description: "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.", highlighted: true },
{ marker: "2", title: "Heading 2", description: "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.", highlighted: false },
{ marker: "3", title: "Heading 3", description: "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.", highlighted: false },
// { marker: "4", title: "Heading 4", description: "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.", highlighted: false },
])
.meta({
description: "Columns rendered across the slide.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const NumberedMultiColumnOverview = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const columns = slideData.items.length;
return (
<>
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap" rel="stylesheet" />
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{ backgroundColor: "var(--background-color,#27292d)", fontFamily: "var(--body-font-family,'DM Serif Display')" }}
>
<div className="px-[48px] pt-[72px]">
<h2
className=" text-[100px] leading-none tracking-[-0.02em]"
style={{ color: "var(--primary-color,#dddac7)", fontFamily: "var(--heading-font-family,'DM Serif Display')" }}
>
{slideData.title}
</h2>
</div>
<div className="px-[48px] pt-[54px] grid gap-[26px]" style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}>
{slideData.items.map((column, index) => (
<div key={`${column.title}-${index}`}>
<div
className="flex h-[70px] w-[70px] items-center justify-center rounded-full border text-[29px] font-semibold leading-none"
style={{
borderColor: column.highlighted ? "var(--primary-color,#dddac7)" : "var(--border-color,#8d8a7d)",
color: column.highlighted ? "var(--background-color,#27292d)" : "var(--primary-color,#dddac7)",
backgroundColor: column.highlighted ? "var(--primary-color,#dddac7)" : "transparent",
}}
>
{column.marker}
</div>
<p className="mt-[28px] text-[32px] leading-none" style={{ color: "var(--primary-text,#d7d3be)" }}>
{column.title}
</p>
<p className="mt-[34px] text-[22px] leading-[1.14]" style={{ color: "var(--background-text,#cbc7b2)" }}>
{column.description}
</p>
</div>
))}
</div>
</div>
</>
);
};
export default NumberedMultiColumnOverview;

View file

@ -0,0 +1,215 @@
import * as z from "zod";
export const slideLayoutId = "overlapping-circle-cards";
export const slideLayoutName = "Overlapping Circle Cards";
export const slideLayoutDescription =
"A horizontal row of overlapping circular cards with markers, titles, and text.";
const CardSchema = z.object({
number: z.string().max(2).meta({
description: "Short card marker.",
}),
title: z.string().max(16).meta({
description: "Card title.",
}),
description: z.string().max(132).meta({
description: "Card description text.",
}),
});
const DEFAULT_DESCRIPTION =
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.";
export const Schema = z.object({
title: z.string().max(12).default("Cards").meta({
description: "Main heading text.",
}),
items: z
.array(CardSchema)
.max(5)
.default([
{
number: "01",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "02",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "03",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "04",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "05",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
])
.meta({
description: "Circle cards from left to right.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const CIRCLE_CARD_LAYOUT_BY_COUNT: Record<
number,
{
circleSize: number;
connectorSize: number;
overlap: number;
rowLeft: number;
rowTop: number;
numberFontSize: number;
titleFontSize: number;
bodyFontSize: number;
bodyWidth: number;
}
> = {
3: {
circleSize: 384,
connectorSize: 84,
overlap: -21,
rowLeft: 34,
rowTop: 238,
numberFontSize: 32,
titleFontSize: 30,
bodyFontSize: 20,
bodyWidth: 250,
},
4: {
circleSize: 318,
connectorSize: 70,
overlap: -21,
rowLeft: 44,
rowTop: 238,
numberFontSize: 30,
titleFontSize: 28,
bodyFontSize: 18,
bodyWidth: 214,
},
5: {
circleSize: 272,
connectorSize: 56,
overlap: -37,
rowLeft: 34,
rowTop: 238,
numberFontSize: 24,
titleFontSize: 22,
bodyFontSize: 16,
bodyWidth: 172,
},
};
const getConnectorLeft = (overlap: number, connectorSize: number) =>
(Math.abs(overlap) - connectorSize) / 2;
const OverlappingCircleCards = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const count = slideData.items.length;
const layout =
CIRCLE_CARD_LAYOUT_BY_COUNT[count] ?? CIRCLE_CARD_LAYOUT_BY_COUNT[5];
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[36px] pt-[44px]">
<h2
className="font-serif text-[100px] leading-none"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="absolute flex items-center"
style={{ left: layout.rowLeft, top: layout.rowTop }}
>
{slideData.items.map((item, index) => (
<div
key={`${item.number}-${index}`}
className="relative"
style={{ marginLeft: index === 0 ? 0 : layout.overlap }}
>
{index > 0 && (
<span
className="absolute top-1/2 -translate-y-1/2 rounded-full"
style={{
left: getConnectorLeft(
layout.overlap,
layout.connectorSize
),
width: layout.connectorSize,
height: layout.connectorSize,
backgroundColor: "var(--primary-color,#dddac7)",
}}
/>
)}
<div
className="flex flex-col items-center justify-center rounded-full border text-center"
style={{
width: layout.circleSize,
height: layout.circleSize,
borderColor: "var(--primary-color,#dddac7)",
borderWidth: 2,
color: "var(--primary-text,#d7d3be)",
}}
>
<p
className="font-semibold leading-none"
style={{ fontSize: layout.numberFontSize }}
>
{item.number}
</p>
<p
className="mt-[16px] leading-none"
style={{ fontSize: layout.titleFontSize }}
>
{item.title}
</p>
<p
className="mt-[12px] leading-[1.15]"
style={{
width: layout.bodyWidth,
color: "var(--background-text,#cbc7b2)",
fontSize: layout.bodyFontSize,
}}
>
{item.description}
</p>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default OverlappingCircleCards;

View file

@ -0,0 +1,201 @@
import * as z from "zod";
import { ImageSchema } from "@/app/presentation-templates/defaultSchemes";
export const slideLayoutId = "panel-list-with-media";
export const slideLayoutName = "Panel List with Media";
export const slideLayoutDescription =
"A table layout with a left multi-column item list and a configurable right media or color panel.";
const ItemSchema = z.object({
title: z.string().max(18).meta({
description: "Item title in the list.",
}),
number: z.string().max(2).meta({
description: "Section number shown on the right.",
}),
description: z.string().max(50).optional().meta({
description: "Optional item description used in description variants.",
}),
});
export const Schema = z.object({
title: z.string().max(14).default("List").meta({
description: "Main heading text.",
}),
rowVariant: z
.enum(["titleOnly", "titleWithDescription"])
.default("titleWithDescription")
.meta({
description: "Layout variant for item rows.",
}),
sidePanelMode: z.enum(["solid", "image"]).default("solid").meta({
description: "Right-side panel style.",
}),
sidePanelImage: ImageSchema.default({
__image_url__:
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1600&q=80",
__image_prompt__: "Skyscraper perspective photo",
}).meta({
description: "Right-side panel image used in image mode.",
}),
items: z
.array(ItemSchema)
.max(10)
.default([
{
title: "Section Title",
number: "1",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Clarity",
number: "2",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Design Principles",
number: "3",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Visual Structure",
number: "4",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Typography",
number: "5",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Color & Space",
number: "6",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Audience Focus",
number: "7",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Layout System",
number: "8",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Presentation Flow",
number: "9",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Key Takeaways",
number: "10",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
])
.meta({
description: "Items shown in the left panel.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const PanelListWithMedia = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const showDescriptions = slideData.rowVariant === "titleWithDescription";
const midpoint = Math.ceil(slideData.items.length / 2);
const leftItems = slideData.items.slice(0, midpoint);
const rightItems = slideData.items.slice(midpoint);
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="flex h-full ">
<div className="px-[44px] pt-[40px] flex-1">
<h2
className="font-serif text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
<div className="mt-[34px] grid grid-cols-2 gap-x-[54px]">
{[leftItems, rightItems].map((column, columnIndex) => (
<div key={`column-${columnIndex}`}>
{column.map((item) => (
<div
key={`${columnIndex}-${item.number}-${item.title}`}
className={
showDescriptions
? "border-b pb-[12px] pt-[12px]"
: "border-b py-[16px]"
}
style={{ borderColor: "var(--stroke,#4c4e53)" }}
>
<div className="flex items-start justify-between gap-[14px]">
<p
className="text-[28px] leading-[1.08]"
style={{ color: "var(--primary-text,#d7d3be)" }}
>
{item.title}
</p>
<p
className="pt-[2px] text-[28px] font-semibold leading-none"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{item.number}
</p>
</div>
{showDescriptions && (
<p
className="mt-[6px] text-[22px] leading-[1.08]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{item.description ?? "Ut enim ad minima veniam."}
</p>
)}
</div>
))}
</div>
))}
</div>
</div>
<div
className="h-full w-[408px]"
style={{ backgroundColor: "var(--primary-color,#d7d3be)" }}
>
{slideData.sidePanelMode === "image" && (
<img
src={slideData.sidePanelImage.__image_url__}
alt={slideData.sidePanelImage.__image_prompt__}
className="h-full w-full object-cover"
/>
)}
</div>
</div>
</div>
</>
);
};
export default PanelListWithMedia;

View file

@ -0,0 +1,477 @@
"use client";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
LabelList,
Line,
LineChart,
Pie,
PieChart,
ResponsiveContainer,
Scatter,
ScatterChart,
XAxis,
YAxis,
} from "recharts";
export type PitchChartType = "bar" | "pie" | "scatter" | "stackedBar" | "line";
export type PitchBarDatum = {
label: string;
value: number;
value2?: number;
};
export type PitchPieDatum = {
label: string;
value: number;
color: string;
};
export type PitchScatterDatum = {
label: string;
value: number;
};
export type PitchChartPayload = {
chartType: PitchChartType;
legendLabel: string;
yAxisLabel: string;
barData: PitchBarDatum[];
pieData: PitchPieDatum[];
scatterData: PitchScatterDatum[];
lineData: PitchBarDatum[];
stackedBarData: PitchBarDatum[];
};
type Props = {
payload?: Partial<PitchChartPayload> | null;
};
type PieLabelProps = {
cx?: number;
cy?: number;
midAngle?: number;
outerRadius?: number;
value?: string | number;
};
const DEFAULT_CHART_COLORS = [
"#8B5CF6",
"#06B6D4",
"#10B981",
"#F59E0B",
"#EF4444",
"#EC4899",
];
const AXIS = "var(--background-text,#d8d4bf)";
const GRID = "var(--background-text,#585a61)";
const CHART_RIGHT_MARGIN = 36;
const graphColors = (index: number, fallbackColor?: string) => {
const fallback =
fallbackColor || DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
const DEFAULT_CHART_PAYLOAD: PitchChartPayload = {
chartType: "bar",
legendLabel: "Series Label",
yAxisLabel: "Y axis name",
barData: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
],
pieData: [
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
],
scatterData: [
{ label: "Mon", value: 7 },
{ label: "Tue", value: 2 },
{ label: "Wed", value: 92 },
{ label: "Thu", value: 15 },
{ label: "Fri", value: 91 },
{ label: "Sat", value: 73 },
{ label: "Sun", value: 56 },
],
lineData: [
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
],
stackedBarData: [
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
],
};
function resolveChartPayload(
payload?: Partial<PitchChartPayload> | null
): PitchChartPayload {
return {
...DEFAULT_CHART_PAYLOAD,
...payload,
barData: payload?.barData?.length
? payload.barData
: DEFAULT_CHART_PAYLOAD.barData,
pieData: payload?.pieData?.length
? payload.pieData
: DEFAULT_CHART_PAYLOAD.pieData,
scatterData: payload?.scatterData?.length
? payload.scatterData
: DEFAULT_CHART_PAYLOAD.scatterData,
lineData: payload?.lineData?.length
? payload.lineData
: DEFAULT_CHART_PAYLOAD.lineData,
stackedBarData: payload?.stackedBarData?.length
? payload.stackedBarData
: DEFAULT_CHART_PAYLOAD.stackedBarData,
chartType: payload?.chartType || DEFAULT_CHART_PAYLOAD.chartType,
legendLabel: payload?.legendLabel || DEFAULT_CHART_PAYLOAD.legendLabel,
yAxisLabel: payload?.yAxisLabel || DEFAULT_CHART_PAYLOAD.yAxisLabel,
};
}
function PiePercentLabel({
cx = 0,
cy = 0,
midAngle = 0,
outerRadius = 0,
value = "",
}: PieLabelProps) {
const radius = outerRadius * 0.72;
const x = cx + radius * Math.cos((-midAngle * Math.PI) / 180);
const y = cy + radius * Math.sin((-midAngle * Math.PI) / 180);
const text = `${value}%`;
return (
<g>
<rect
x={x - 24}
y={y - 13}
rx={11}
ry={11}
width={48}
height={26}
fill={"var(--card-color,#ececeb)"}
/>
<text
x={x}
y={y + 5}
textAnchor="middle"
fontSize={16}
fontWeight={600}
fill={"var(--background-text,#2f2f2f)"}
>
{text}
</text>
</g>
);
}
function Legend({
label,
color = graphColors(0),
}: {
label: string;
color?: string;
}) {
return (
<div
className="flex shrink-0 items-center justify-center gap-[10px] pt-[8px]"
style={{ color: AXIS }}
>
<span
className="h-[16px] w-[16px] rounded-full"
style={{ backgroundColor: color }}
/>
<span className="text-[18px] leading-none">{label}</span>
</div>
);
}
export default function PitchDeckChart({ payload }: Props) {
const {
chartType,
barData,
pieData,
scatterData,
lineData,
stackedBarData,
legendLabel,
yAxisLabel,
} = resolveChartPayload(payload);
if (chartType === "pie") {
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<PieChart
margin={{ top: 18, right: CHART_RIGHT_MARGIN, bottom: 18, left: 28 }}
>
<Pie
data={pieData}
dataKey="value"
nameKey="label"
cx="50%"
cy="50%"
outerRadius="74%"
stroke="none"
labelLine={false}
label={({ cx, cy, midAngle, outerRadius, value }) => (
<PiePercentLabel
cx={Number(cx)}
cy={Number(cy)}
midAngle={Number(midAngle)}
outerRadius={Number(outerRadius)}
value={String(value)}
/>
)}
isAnimationActive={false}
>
{pieData.map((entry, index) => (
<Cell
key={`${entry.label}-${index}`}
fill={graphColors(index, entry.color)}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div
className="flex shrink-0 items-center justify-center gap-[26px] pb-[2px] pt-[8px] text-[18px] leading-none"
style={{ color: AXIS }}
>
{pieData.map((entry, index) => (
<span key={entry.label} className="flex items-center gap-[10px]">
<span
className="h-[15px] w-[15px] rounded-full"
style={{ backgroundColor: graphColors(index, entry.color) }}
/>
{entry.label}
</span>
))}
</div>
</div>
);
}
if (chartType === "scatter") {
const points = scatterData.map((item, index) => ({
x: index + 1,
y: item.value,
label: item.label,
}));
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart
margin={{ top: 18, right: CHART_RIGHT_MARGIN, left: 18, bottom: 20 }}
>
<CartesianGrid
vertical={false}
horizontal={false}
stroke={GRID}
opacity={0.7}
/>
<XAxis
type="number"
dataKey="x"
domain={[1, Math.max(points.length, 2)]}
tickCount={points.length}
tickFormatter={(value) =>
points[Number(value) - 1]?.label ?? "label"
}
tick={{ fill: AXIS, fontSize: 18 }}
axisLine={{ stroke: AXIS }}
tickLine={false}
/>
<YAxis
type="number"
dataKey="y"
domain={[0, 100]}
tick={{ fill: AXIS, fontSize: 18 }}
axisLine={false}
tickLine={false}
tickSize={0}
width={64}
label={{
value: yAxisLabel,
angle: -90,
position: "insideLeft",
fill: AXIS,
fontSize: 18,
offset: 0,
}}
/>
<Scatter
data={points}
fill={graphColors(0)}
isAnimationActive={false}
/>
</ScatterChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}
if (chartType === "line") {
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={lineData}
margin={{ top: 24, right: CHART_RIGHT_MARGIN, left: 8, bottom: 20 }}
>
<CartesianGrid stroke={GRID} vertical={false} opacity={0.7} />
<XAxis
dataKey="label"
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={{ stroke: AXIS }}
/>
<YAxis
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={false}
width={34}
/>
<Line
dataKey="value"
stroke={graphColors(0)}
strokeWidth={4}
dot={{ r: 5, fill: graphColors(0) }}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}
if (chartType === "stackedBar") {
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={stackedBarData}
margin={{ top: 28, right: CHART_RIGHT_MARGIN, left: 8, bottom: 20 }}
>
<XAxis
dataKey="label"
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={{ stroke: AXIS }}
/>
<YAxis
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={false}
width={34}
/>
<Bar
dataKey="value"
stackId="stack"
fill={graphColors(1)}
radius={[5, 5, 0, 0]}
isAnimationActive={false}
>
<LabelList
dataKey="value"
position="insideTop"
fill={"var(--primary-text,#ffffff)"}
fontSize={16}
/>
</Bar>
<Bar
dataKey="value2"
stackId="stack"
fill={graphColors(0)}
radius={[5, 5, 0, 0]}
isAnimationActive={false}
>
<LabelList
dataKey="value2"
position="insideTop"
fill={"var(--primary-text,#ffffff)"}
fontSize={16}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={barData}
margin={{ top: 36, right: CHART_RIGHT_MARGIN, left: 8, bottom: 20 }}
>
<XAxis
dataKey="label"
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={{ stroke: AXIS }}
/>
<YAxis
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={false}
width={34}
/>
<Bar
dataKey="value"
fill={graphColors(0)}
radius={[5, 5, 0, 0]}
barSize={34}
isAnimationActive={false}
>
<LabelList
dataKey="value"
position="top"
fill={AXIS}
fontSize={16}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}

View file

@ -0,0 +1,151 @@
"use client";
import * as z from "zod";
import PitchDeckChart from "./PitchDeckChart";
import { ChartPayloadSchema } from "./pitchDeckSchemas";
export const slideLayoutId = "text-and-chart-split-layout";
export const slideLayoutName = "Text and Chart Split Layout";
export const slideLayoutDescription =
"A split layout with narrative text on the left and a configurable chart canvas on the right.";
const DEFAULT_CHART = {
chartType: "scatter" as const,
legendLabel: "Series Label",
yAxisLabel: "Y axis name",
barData: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
],
pieData: [
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
],
scatterData: [
{ label: "label", value: 7 },
{ label: "label", value: 2 },
{ label: "label", value: 92 },
{ label: "label", value: 15 },
{ label: "label", value: 91 },
{ label: "label", value: 73 },
{ label: "label", value: 56 },
{ label: "label", value: 90 },
],
lineData: [
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
],
stackedBarData: [
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
],
};
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Main heading on the left.",
}),
leadText: z
.string()
.max(52)
.default("This is a sample text to tell story for audience is written here")
.meta({
description: "Primary narrative line above supporting text.",
}),
supportingText: z
.string()
.max(126)
.default(
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam."
)
.meta({
description: "Supporting paragraph text.",
}),
chart: ChartPayloadSchema.default(DEFAULT_CHART).meta({
description: "Chart configuration payload rendered on the right side.",
}),
showAccentGlow: z.boolean().default(true).meta({
description:
"Whether to render the subtle decorative glow near bottom-left.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const TextAndChartSplit = ({
data,
}: {
data: Partial<SchemaType>;
}) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden rounded-[24px]"
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="grid h-full grid-cols-[47.5%_52.5%]">
<div className="px-[36px] pt-[44px]">
<h2
className="font-serif text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
<p
className="mt-[76px] max-w-[520px] text-[32px] leading-[1.12]"
style={{ color: "var(--primary-text,#d7d3be)" }}
>
{slideData.leadText}
</p>
<p
className="mt-[38px] max-w-[530px] text-[22px] leading-[1.16]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{slideData.supportingText}
</p>
</div>
<div className="h-full min-h-0 overflow-hidden px-[24px] pb-[52px] pt-[142px]">
<PitchDeckChart payload={slideData.chart ?? DEFAULT_CHART} />
</div>
</div>
</div>
</>
);
};
export default TextAndChartSplit;

View file

@ -0,0 +1,132 @@
import * as z from "zod";
export const DARK_BG = "var(--background-color,#27292d)";
export const ACCENT_TEXT = "var(--primary-color,#dddac7)";
export const BODY_TEXT = "var(--primary-text,#d7d3be)";
export const MUTED_TEXT = "var(--background-text,#cbc7b2)";
export const BORDER = "var(--border-color,#8d8a7d)";
export const SUBTLE_LINE = "var(--line-color,#4c4e53)";
export const ChartTypeSchema = z.enum(["bar", "pie", "scatter", "stackedBar", "line"]);
export const BarDatumSchema = z.object({
label: z.string().max(10).meta({
description: "X-axis label for bar/line/stacked charts.",
}),
value: z.number().max(300).meta({
description: "Primary numeric value.",
}),
value2: z.number().max(300).optional().meta({
description: "Secondary stacked value when using stacked bar charts.",
}),
});
export const PieDatumSchema = z.object({
label: z.string().max(16).meta({
description: "Legend label for pie slices.",
}),
value: z.number().max(100).meta({
description: "Slice percentage value.",
}),
color: z.string().max(20).meta({
description: "Slice fill color.",
}),
});
export const ScatterDatumSchema = z.object({
label: z.string().max(10).meta({
description: "X-axis label for scatter points.",
}),
value: z.number().max(100).meta({
description: "Y-axis value for the point.",
}),
});
export const ChartPayloadSchema = z.object({
chartType: ChartTypeSchema.default("bar").meta({
description: "Chart type rendered on the right side.",
}),
legendLabel: z.string().max(30).default("Series Label").meta({
description: "Single-series legend label for non-pie charts.",
}),
yAxisLabel: z.string().max(16).default("Y axis name").meta({
description: "Y-axis title used in scatter charts.",
}),
barData: z
.array(BarDatumSchema)
.max(8)
.default([
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
])
.meta({
description: "Dataset for regular bar charts.",
}),
pieData: z
.array(PieDatumSchema)
.max(3)
.default([
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
])
.meta({
description: "Pie chart dataset.",
}),
scatterData: z
.array(ScatterDatumSchema)
.max(10)
.default([
{ label: "label", value: 7 },
{ label: "label", value: 2 },
{ label: "label", value: 92 },
{ label: "label", value: 15 },
{ label: "label", value: 91 },
{ label: "label", value: 73 },
{ label: "label", value: 56 },
{ label: "label", value: 90 },
])
.meta({
description: "Scatter points for distribution charts.",
}),
lineData: z
.array(BarDatumSchema)
.max(8)
.default([
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
])
.meta({
description: "Dataset for line charts.",
}),
stackedBarData: z
.array(BarDatumSchema)
.max(8)
.default([
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
])
.meta({
description: "Dataset for stacked bar charts using value and value2.",
}),
});

View file

@ -0,0 +1,5 @@
{
"description": "Dark, theme-ready presentation layouts with covers, structured content grids, timelines, narrative splits, and chart-driven slides",
"ordered": false,
"default": false
}

View file

@ -60,6 +60,20 @@ import PerformanceSnapshotSlide, { Schema as RepPerformanceSnapshotSchema, slide
import ReportServicesSlide, { Schema as RepServicesSchema, slideLayoutId as RepServicesId, slideLayoutName as RepServicesName, slideLayoutDescription as RepServicesDesc } from "./Report/TitleWorkflowWithTitleDescriptionSlide";
import ReportTeamSlide, { Schema as RepTeamSchema, slideLayoutId as RepTeamId, slideLayoutName as RepTeamName, slideLayoutDescription as RepTeamDesc } from "./Report/HorizontalHeightSpanningImagesWithTitleSlide";
// Pitch Deck templates
import PitchDeckCenteredCoverWithFooterMeta, { Schema as PitchDeckCenteredCoverWithFooterMetaSchema, slideLayoutId as PitchDeckCenteredCoverWithFooterMetaId, slideLayoutName as PitchDeckCenteredCoverWithFooterMetaName, slideLayoutDescription as PitchDeckCenteredCoverWithFooterMetaDesc } from "./pitch-deck/CenteredCoverWithFooterMeta";
import PitchDeckFullWidthStatement, { Schema as PitchDeckFullWidthStatementSchema, slideLayoutId as PitchDeckFullWidthStatementId, slideLayoutName as PitchDeckFullWidthStatementName, slideLayoutDescription as PitchDeckFullWidthStatementDesc } from "./pitch-deck/FullWidthStatement";
import PitchDeckMediaAndTextSplit, { Schema as PitchDeckMediaAndTextSplitSchema, slideLayoutId as PitchDeckMediaAndTextSplitId, slideLayoutName as PitchDeckMediaAndTextSplitName, slideLayoutDescription as PitchDeckMediaAndTextSplitDesc } from "./pitch-deck/MediaAndTextSplit";
import PitchDeckTextAndChartSplit, { Schema as PitchDeckTextAndChartSplitSchema, slideLayoutId as PitchDeckTextAndChartSplitId, slideLayoutName as PitchDeckTextAndChartSplitName, slideLayoutDescription as PitchDeckTextAndChartSplitDesc } from "./pitch-deck/TextAndChartSplit";
import PitchDeckCardsWithChartSplit, { Schema as PitchDeckCardsWithChartSplitSchema, slideLayoutId as PitchDeckCardsWithChartSplitId, slideLayoutName as PitchDeckCardsWithChartSplitName, slideLayoutDescription as PitchDeckCardsWithChartSplitDesc } from "./pitch-deck/CardsWithChartSplit";
import PitchDeckAdaptiveValueCardGrid, { Schema as PitchDeckAdaptiveValueCardGridSchema, slideLayoutId as PitchDeckAdaptiveValueCardGridId, slideLayoutName as PitchDeckAdaptiveValueCardGridName, slideLayoutDescription as PitchDeckAdaptiveValueCardGridDesc } from "./pitch-deck/AdaptiveValueCardGrid";
import PitchDeckAdaptiveMediaCardGrid, { Schema as PitchDeckAdaptiveMediaCardGridSchema, slideLayoutId as PitchDeckAdaptiveMediaCardGridId, slideLayoutName as PitchDeckAdaptiveMediaCardGridName, slideLayoutDescription as PitchDeckAdaptiveMediaCardGridDesc } from "./pitch-deck/AdaptiveMediaCardGrid";
import PitchDeckHeadlineWithDetailColumns, { Schema as PitchDeckHeadlineWithDetailColumnsSchema, slideLayoutId as PitchDeckHeadlineWithDetailColumnsId, slideLayoutName as PitchDeckHeadlineWithDetailColumnsName, slideLayoutDescription as PitchDeckHeadlineWithDetailColumnsDesc } from "./pitch-deck/HeadlineWithDetailColumns";
import PitchDeckNumberedMultiColumnOverview, { Schema as PitchDeckNumberedMultiColumnOverviewSchema, slideLayoutId as PitchDeckNumberedMultiColumnOverviewId, slideLayoutName as PitchDeckNumberedMultiColumnOverviewName, slideLayoutDescription as PitchDeckNumberedMultiColumnOverviewDesc } from "./pitch-deck/NumberedMultiColumnOverview";
import PitchDeckPanelListWithMedia, { Schema as PitchDeckPanelListWithMediaSchema, slideLayoutId as PitchDeckPanelListWithMediaId, slideLayoutName as PitchDeckPanelListWithMediaName, slideLayoutDescription as PitchDeckPanelListWithMediaDesc } from "./pitch-deck/PanelListWithMedia";
import PitchDeckHorizontalTimeline, { Schema as PitchDeckHorizontalTimelineSchema, slideLayoutId as PitchDeckHorizontalTimelineId, slideLayoutName as PitchDeckHorizontalTimelineName, slideLayoutDescription as PitchDeckHorizontalTimelineDesc } from "./pitch-deck/HorizontalTimeline";
import PitchDeckOverlappingCircleCards, { Schema as PitchDeckOverlappingCircleCardsSchema, slideLayoutId as PitchDeckOverlappingCircleCardsId, slideLayoutName as PitchDeckOverlappingCircleCardsName, slideLayoutDescription as PitchDeckOverlappingCircleCardsDesc } from "./pitch-deck/OverlappingCircleCards";
// General templates
import GeneralIntroSlideLayout, { Schema as GeneralIntroSchema, layoutId as GeneralIntroId, layoutName as GeneralIntroName, layoutDescription as GeneralIntroDesc } from "./general/IntroSlideLayout";
import BasicInfoSlideLayout, { Schema as BasicInfoSchema, layoutId as BasicInfoId, layoutName as BasicInfoName, layoutDescription as BasicInfoDesc } from "./general/BasicInfoSlideLayout";
@ -236,6 +250,7 @@ import codeSettings from "./Code/settings.json";
import educationSettings from "./Education/settings.json";
import productOverviewSettings from "./ProductOverview/settings.json";
import reportSettings from "./Report/settings.json";
import pitchDeckSettings from "./pitch-deck/settings.json";
// Helper to create template entry
@ -304,6 +319,21 @@ export const reportTemplates: TemplateWithData[] = [
createTemplateEntry(ReportTeamSlide, RepTeamSchema, RepTeamId, RepTeamName, RepTeamDesc, "report", "HorizontalHeightSpanningImagesWithTitleSlide"),
];
export const pitchDeckTemplates: TemplateWithData[] = [
createTemplateEntry(PitchDeckCenteredCoverWithFooterMeta, PitchDeckCenteredCoverWithFooterMetaSchema, PitchDeckCenteredCoverWithFooterMetaId, PitchDeckCenteredCoverWithFooterMetaName, PitchDeckCenteredCoverWithFooterMetaDesc, "pitch-deck", "CenteredCoverWithFooterMeta"),
createTemplateEntry(PitchDeckFullWidthStatement, PitchDeckFullWidthStatementSchema, PitchDeckFullWidthStatementId, PitchDeckFullWidthStatementName, PitchDeckFullWidthStatementDesc, "pitch-deck", "FullWidthStatement"),
createTemplateEntry(PitchDeckMediaAndTextSplit, PitchDeckMediaAndTextSplitSchema, PitchDeckMediaAndTextSplitId, PitchDeckMediaAndTextSplitName, PitchDeckMediaAndTextSplitDesc, "pitch-deck", "MediaAndTextSplit"),
createTemplateEntry(PitchDeckTextAndChartSplit, PitchDeckTextAndChartSplitSchema, PitchDeckTextAndChartSplitId, PitchDeckTextAndChartSplitName, PitchDeckTextAndChartSplitDesc, "pitch-deck", "TextAndChartSplit"),
createTemplateEntry(PitchDeckCardsWithChartSplit, PitchDeckCardsWithChartSplitSchema, PitchDeckCardsWithChartSplitId, PitchDeckCardsWithChartSplitName, PitchDeckCardsWithChartSplitDesc, "pitch-deck", "CardsWithChartSplit"),
createTemplateEntry(PitchDeckAdaptiveValueCardGrid, PitchDeckAdaptiveValueCardGridSchema, PitchDeckAdaptiveValueCardGridId, PitchDeckAdaptiveValueCardGridName, PitchDeckAdaptiveValueCardGridDesc, "pitch-deck", "AdaptiveValueCardGrid"),
createTemplateEntry(PitchDeckAdaptiveMediaCardGrid, PitchDeckAdaptiveMediaCardGridSchema, PitchDeckAdaptiveMediaCardGridId, PitchDeckAdaptiveMediaCardGridName, PitchDeckAdaptiveMediaCardGridDesc, "pitch-deck", "AdaptiveMediaCardGrid"),
createTemplateEntry(PitchDeckHeadlineWithDetailColumns, PitchDeckHeadlineWithDetailColumnsSchema, PitchDeckHeadlineWithDetailColumnsId, PitchDeckHeadlineWithDetailColumnsName, PitchDeckHeadlineWithDetailColumnsDesc, "pitch-deck", "HeadlineWithDetailColumns"),
createTemplateEntry(PitchDeckNumberedMultiColumnOverview, PitchDeckNumberedMultiColumnOverviewSchema, PitchDeckNumberedMultiColumnOverviewId, PitchDeckNumberedMultiColumnOverviewName, PitchDeckNumberedMultiColumnOverviewDesc, "pitch-deck", "NumberedMultiColumnOverview"),
createTemplateEntry(PitchDeckPanelListWithMedia, PitchDeckPanelListWithMediaSchema, PitchDeckPanelListWithMediaId, PitchDeckPanelListWithMediaName, PitchDeckPanelListWithMediaDesc, "pitch-deck", "PanelListWithMedia"),
createTemplateEntry(PitchDeckHorizontalTimeline, PitchDeckHorizontalTimelineSchema, PitchDeckHorizontalTimelineId, PitchDeckHorizontalTimelineName, PitchDeckHorizontalTimelineDesc, "pitch-deck", "HorizontalTimeline"),
createTemplateEntry(PitchDeckOverlappingCircleCards, PitchDeckOverlappingCircleCardsSchema, PitchDeckOverlappingCircleCardsId, PitchDeckOverlappingCircleCardsName, PitchDeckOverlappingCircleCardsDesc, "pitch-deck", "OverlappingCircleCards"),
];
export const neoGeneralTemplates: TemplateWithData[] = [
createTemplateEntry(TextSplitWithEmphasisBlockLayout, TextSplitWithEmphasisBlockSchema, TextSplitWithEmphasisBlockId, TextSplitWithEmphasisBlockName, TextSplitWithEmphasisBlockDesc, 'neo-general', 'TextSplitWithEmphasisBlock'),
@ -478,6 +508,7 @@ export const allLayouts: TemplateWithData[] = [
...educationTemplates,
...productOverviewTemplates,
...reportTemplates,
...pitchDeckTemplates,
];
@ -540,6 +571,13 @@ export const templates: TemplateLayoutsWithSettings[] = [
settings: reportSettings as TemplateGroupSettings,
layouts: reportTemplates,
},
{
id: "pitch-deck",
name: "Pitch Deck",
description: pitchDeckSettings.description,
settings: pitchDeckSettings as TemplateGroupSettings,
layouts: pitchDeckTemplates,
},
{
id: "neo-general",
name: "Neo General",
@ -606,4 +644,4 @@ export function getLayoutByLayoutId(layout: string): TemplateWithData | undefine
return template.layouts.find((t) => t.layoutId === layout);
}
return undefined;
}
}

View file

@ -0,0 +1,198 @@
import * as z from "zod";
export const slideLayoutId = "adaptive-media-card-grid";
export const slideLayoutName = "Adaptive Media Card Grid";
export const slideLayoutDescription =
"A responsive media-card grid that supports compact and dense arrangements.";
const CardSchema = z.object({
label: z.string().max(14).meta({
description: "Small top label shown on each card.",
}),
title: z.string().max(18).meta({
description: "Primary card title.",
}),
description: z.string().max(40).meta({
description: "Short supporting description.",
}),
image: z.object({
__image_url__: z.string().url().meta({
description: "Image URL for the media card.",
}),
__image_prompt__: z.string().meta({
description: "A short prompt describing the image content.",
}),
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Top heading text.",
}),
cards: z
.array(CardSchema)
.max(8)
.default([
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=12",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=13",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=14",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=12",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=13",
__image_prompt__: "Media card image",
},
},
{
label: "LEAD",
title: "Insert Title",
description: "Ut enim ad minima veniam, quis nostrum",
image: {
__image_url__: "https://i.pravatar.cc/800?img=14",
__image_prompt__: "Media card image",
},
},
])
.meta({
description: "Media cards rendered in an adaptive grid.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const CARD_WIDTH_BY_COUNT: Record<number, number> = {
3: 308,
4: 227,
8: 227,
};
const IMAGE_HEIGHT_BY_COUNT: Record<number, number> = {
3: 301,
4: 222,
8: 222,
};
const AdaptiveMediaCardGrid = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const count = slideData.cards.length;
const columns = count <= 4 ? count : 4;
const cardWidth = CARD_WIDTH_BY_COUNT[count] ?? 236;
const imageHeight = IMAGE_HEIGHT_BY_COUNT[count] ?? 224;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div
className="px-[42px] "
style={{
marginTop: slideData.cards.length > 4 ? "10px" : "72px",
}}
>
<h2
className="font-serif text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="pt-[52px] px-[54px] grid justify-items-start gap-x-[30px] gap-y-[48px]"
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
>
{slideData.cards.map((card, index) => (
<div
key={`${card.title}-${index}`}
className="relative"
style={{ width: cardWidth }}
>
<img
src={card.image.__image_url__}
alt={card.image.__image_prompt__}
className="w-full object-cover"
style={{ height: imageHeight }}
/>
<div
className="absolute right-0 -bottom-10 mx-auto w-[66%] p-[13px] text-center"
style={{ backgroundColor: "var(--background-color,#27292d)" }}
>
<p
className="text-[19px] font-bold leading-none"
style={{ color: "var(--background-text,#dddac7)" }}
>
{card.label}
</p>
<p
className="mt-[8px] text-[18px] leading-none"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{card.title}
</p>
<p
className="mt-[10px] text-[12px] "
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{card.description}
</p>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default AdaptiveMediaCardGrid;

View file

@ -0,0 +1,262 @@
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
import * as z from "zod";
export const slideLayoutId = "adaptive-value-card-grid";
export const slideLayoutName = "Adaptive Value Card Grid";
export const slideLayoutDescription =
"A card grid that supports even layouts and odd-count variants with an emphasized trailing card.";
const ValueCardSchema = z.object({
value: z.string().max(6).meta({
description: "Primary card value text.",
}),
label: z.string().max(28).meta({
description: "Secondary label under the card.",
}),
icon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string(),
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Top-left heading.",
}),
items: z
.array(ValueCardSchema)
.max(8)
.default([
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
// {
// value: "X 5", label: "Lorem ipsum dolor sit.", icon: {
// __icon_url__:
// "https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
// __icon_query__: "check icon",
// }
// },
])
.meta({
description: "Value cards displayed in an adaptive grid.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const RIGHT_RATIO_BY_ODD_COUNT: Record<number, string> = {
3: "49%",
5: "33%",
7: "29%",
};
function Card({
value,
label,
icon,
}: {
value: string;
label: string;
icon: any;
}) {
return (
<div
className="flex h-full flex-col items-center justify-center border text-center p-[11px]"
style={{
borderColor: "var(--border-color,#8d8a7d)",
color: "var(--background-text,#d7d3be)",
}}
>
<div className="flex items-center gap-[14px]">
<span
className="inline-flex items-center justify-center rounded-full"
style={{
width: 56,
height: 56,
backgroundColor: "var(--primary-color,#dddac7)",
color: "var(--background-color,#27292d)",
}}
>
{/* <img
src={icon.__icon_url__}
alt={icon.__icon_query__}
className="w-7 h-7"
/> */}
<RemoteSvgIcon
url={icon.__icon_url__}
className="w-7 h-7"
strokeColor={"currentColor"}
color="var(--primary-text,#27292d)"
title={icon.__icon_query__}
/>
</span>
<p className="text-[45px] font-semibold leading-none tracking-[0.01em]">
{value}
</p>
</div>
<p
className="mt-[14px] text-[30px] leading-[1.06]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{label}
</p>
</div>
);
}
const AdaptiveValueCardGrid = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const count = slideData.items.length;
const isOdd = count % 2 === 1;
const leftCards = isOdd ? slideData.items.slice(0, -1) : slideData.items;
const rightTallCard = isOdd
? slideData.items[slideData.items.length - 1]
: null;
const evenColumns = Math.max(2, Math.min(4, count / 2));
const oddLeftColumns = Math.max(1, Math.ceil(leftCards.length / 2));
const rightRatio = RIGHT_RATIO_BY_ODD_COUNT[count] ?? "33%";
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[48px] pt-[72px]">
<h2
className=" text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#d7d3be)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
{!isOdd && (
<div
className="absolute left-[36px] right-[36px] top-[200px] grid gap-[21px]"
style={{
gridTemplateColumns: `repeat(${evenColumns}, minmax(0, 1fr))`,
gridAutoRows: "236px",
}}
>
{slideData.items.map((card, index) => (
<Card
key={`${card.value}-${index}`}
value={card.value}
label={card.label}
icon={card.icon}
/>
))}
</div>
)}
{isOdd && rightTallCard && (
<div
className="absolute left-[36px] right-[36px] top-[200px] grid h-[490px] gap-[21px]"
style={{ gridTemplateColumns: `minmax(0, 1fr) ${rightRatio}` }}
>
<div
className="grid gap-[18px]"
style={{
gridTemplateColumns: `repeat(${oddLeftColumns}, minmax(0, 1fr))`,
gridAutoRows: "236px",
}}
>
{leftCards.map((card, index) => (
<Card
key={`${card.value}-${index}`}
value={card.value}
label={card.label}
icon={card.icon}
/>
))}
</div>
<Card
value={rightTallCard.value}
label={rightTallCard.label}
icon={rightTallCard.icon}
/>
</div>
)}
</div>
</>
);
};
export default AdaptiveValueCardGrid;

View file

@ -0,0 +1,240 @@
"use client";
import * as z from "zod";
import PitchDeckChart from "./PitchDeckChart";
import { ChartPayloadSchema } from "./pitchDeckSchemas";
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
export const slideLayoutId = "cards-with-chart-split";
export const slideLayoutName = "Cards with Chart Split";
export const slideLayoutDescription =
"A split layout with cards on the left and a chart panel on the right.";
const DEFAULT_CHART = {
chartType: "pie" as const,
legendLabel: "Series Label",
yAxisLabel: "Y axis name",
barData: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
],
pieData: [
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
],
scatterData: [
{ label: "label", value: 7 },
{ label: "label", value: 2 },
{ label: "label", value: 92 },
{ label: "label", value: 15 },
{ label: "label", value: 91 },
{ label: "label", value: 73 },
{ label: "label", value: 56 },
{ label: "label", value: 90 },
],
lineData: [
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
],
stackedBarData: [
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
],
};
const ValueCardSchema = z.object({
value: z.string().max(6).meta({
description: "Card value text.",
}),
label: z.string().max(28).meta({
description: "Card supporting label.",
}),
icon: z.object({
__icon_url__: z.string(),
__icon_query__: z.string(),
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Main heading.",
}),
items: z
.array(ValueCardSchema)
.max(4)
.default([
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
{
value: "X 5",
label: "Lorem ipsum dolor sit.",
icon: {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "check icon",
},
},
])
.meta({
description: "Cards shown beside the chart.",
}),
chart: ChartPayloadSchema.default(DEFAULT_CHART).meta({
description: "Chart configuration for the right panel.",
}),
showAccentGlow: z.boolean().default(true).meta({
description:
"Whether to render the subtle decorative glow near bottom-left.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
function Card({
value,
label,
icon,
}: {
value: string;
label: string;
icon: { __icon_url__: string; __icon_query__: string };
}) {
return (
<div
className="flex h-full flex-col items-center justify-center border text-center"
style={{
borderColor: "var(--stroke,#8d8a7d)",
color: "var(--background-text,#d7d3be)",
}}
>
<div className="flex items-center gap-[14px]">
<span
className="inline-flex items-center justify-center rounded-full"
style={{
width: 56,
height: 56,
backgroundColor: "var(--primary-color,#dddac7)",
color: "var(--primary-color,#27292d)",
}}
>
<RemoteSvgIcon
url={icon.__icon_url__}
className="w-7 h-7"
strokeColor={"currentColor"}
color="var(--primary-text,#27292d)"
title={icon.__icon_query__}
/>
</span>
<p className="text-[45px] font-semibold leading-none tracking-[0.01em]">
{value}
</p>
</div>
<p
className="mt-[14px] max-w-[82%] text-[30px] leading-[1.06]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{label}
</p>
</div>
);
}
const CardsWithChartSplit = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<h2
className="px-[36px] pt-[72px] text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
<div
className="absolute bottom-[36px] left-[36px] right-[80px] top-[198px] grid min-h-0 gap-[32px]"
style={{ gridTemplateColumns: "minmax(0, 45fr) minmax(0, 55fr)" }}
>
<div className="min-h-0">
<div
className="grid grid-cols-2 gap-[20px]"
style={{ gridAutoRows: "216px" }}
>
{slideData.items.map((card, index) => (
<Card
key={`${card.value}-${index}`}
value={card.value}
label={card.label}
icon={card.icon}
/>
))}
</div>
</div>
<div className="h-full min-h-0 overflow-hidden pt-[10px]">
<PitchDeckChart payload={slideData.chart ?? DEFAULT_CHART} />
</div>
</div>
</div>
</>
);
};
export default CardsWithChartSplit;

View file

@ -0,0 +1,96 @@
import * as z from "zod";
export const slideLayoutId = "centered-cover-with-footer-meta";
export const slideLayoutName = "Centered Cover with Footer Metadata";
export const slideLayoutDescription =
"A single-focus cover layout with centered title, subtitle, and footer metadata groups.";
const FooterMetaSchema = z.object({
label: z.string().max(14).meta({
description: "Footer metadata label.",
}),
value: z.string().max(24).meta({
description: "Footer metadata value.",
}),
});
export const Schema = z.object({
title: z.string().max(20).default("Presentation").meta({
description: "Main centered cover title.",
}),
subtitle: z.string().max(34).default("WORKFORCE OPERATIONS").meta({
description: "Subtitle beneath the title.",
}),
footerItems: z
.array(FooterMetaSchema)
.max(2)
.default([
{ label: "PRESENTED BY", value: "PRESENTER NAME" },
{ label: "DATE", value: "2026 DECEMBER 4" },
])
.meta({
description: "Footer metadata groups.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const CenteredCoverWithFooterMeta = ({
data,
}: {
data: Partial<SchemaType>;
}) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] flex items-center justify-center overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className=" text-center ">
<h1
className="text-[124px] leading-[124.6px]"
style={{ color: "var(--background-text,#dddac7)" }}
>
{slideData.title}
</h1>
<p
className="mt-[22px] text-[33px] tracking-[0.02em]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{slideData.subtitle}
</p>
</div>
<div
className="absolute bottom-[34px] left-[36px] flex gap-[74px]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{slideData.footerItems.map((item, index) => (
<div key={`${item.label}-${index}`}>
<p
className="text-[14px] font-semibold tracking-[0.04em]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{item.label}
</p>
<p className="mt-[12px] text-[15px] leading-none">{item.value}</p>
</div>
))}
</div>
</div>
</>
);
};
export default CenteredCoverWithFooterMeta;

View file

@ -0,0 +1,65 @@
import * as z from "zod";
export const slideLayoutId = "full-width-statement";
export const slideLayoutName = "Full-Width Statement";
export const slideLayoutDescription =
"A minimalist emphasis layout with a compact label and a large full-width statement block.";
export const Schema = z.object({
label: z.string().max(12).default("Label").meta({
description: "Small label above the statement.",
}),
statement: z
.string()
.max(90)
.default(
"This is a sample statement used for placeholder content in presentations. This is a sample statement used for placeholder"
)
.meta({
description: "Main statement text, with max 90 characters.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const FullWidthStatement = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] flex flex-col justify-end pb-[74px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[46px] ">
<p
className="text-[32px] leading-none"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{slideData.label}
</p>
<p
className="mt-[61px] text-[100px] leading-[100%]"
style={{
color: "var(--background-text,#dddac7)",
}}
>
{slideData.statement}
</p>
</div>
</div>
</>
);
};
export default FullWidthStatement;

View file

@ -0,0 +1,152 @@
import * as z from "zod";
export const slideLayoutId = "headline-with-detail-columns";
export const slideLayoutName = "Headline with Detail Columns";
export const slideLayoutDescription =
"A layout with a large headline and detail columns containing markers, text, and bullets.";
const SectionSchema = z.object({
number: z.string().max(2).meta({
description: "Numeric marker value.",
}),
title: z.string().max(16).meta({
description: "Section title.",
}),
description: z.string().max(150).meta({
description: "Section paragraph.",
}),
bullets: z.array(z.string().max(40)).max(4).meta({
description: "Bullet list content.",
}),
highlighted: z.boolean().default(false).meta({
description: "Whether the top marker is filled.",
}),
});
export const Schema = z.object({
title: z.string().max(24).default("Focus Areas").meta({
description: "Large left-side heading.",
}),
sections: z
.array(SectionSchema)
.max(2)
.default([
{
number: "1",
title: "Column A",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.",
bullets: [
"Ut enim ad minima veniam, quis nostrum",
"Exercitationem ullam corporis suscipit",
"Laboriosam, nisi ut alUt enim ad minima",
],
highlighted: true,
},
{
number: "2",
title: "Column B",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.",
bullets: [
"Ut enim ad minima veniam, quis nostrum",
"Exercitationem ullam corporis suscipit",
"Laboriosam, nisi ut alUt enim ad minima",
],
highlighted: false,
},
])
.meta({
description: "Right-side detail columns.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const HeadlineWithDetailColumns = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="grid h-full grid-cols-[35%_34%_34%] gap-x-[8px] px-[36px] pt-[48px]">
<h2
className="whitespace-pre-line font-serif text-[100px] leading-[0.94] tracking-[-0.02em]"
style={{ color: "var(--background-text,#dddac7)" }}
>
{slideData.title}
</h2>
{slideData.sections.map((section, index) => (
<div key={`${section.title}-${index}`} className="pr-[20px]">
<div
className="flex h-[70px] w-[70px] border rounded-full items-center justify-center text-[30px] font-semibold "
style={{
borderColor: section.highlighted
? "var(--primary-color,#dddac7)"
: "var(--primary-color,#dddac7)",
color: section.highlighted
? "var(--primary-text,#27292d)"
: "var(--background-text,#dddac7)",
backgroundColor: section.highlighted
? "var(--primary-color,#dddac7)"
: "transparent",
}}
>
{section.number}
</div>
<p
className="mt-[20px] text-[32px] leading-none"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{section.title}
</p>
<p
className="mt-[20px] text-[22px] leading-[1.15]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{section.description}
</p>
<div
className="mt-[26px] space-y-[2px] pl-[16px] text-[22px] leading-[1.14]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{section.bullets.map((bullet, bulletIndex) => (
<p
key={`${section.title}-bullet-${bulletIndex}`}
className="flex gap-2 items-start"
>
<p
className="w-1 h-1 rounded-full mt-3 "
style={{
background: "var(--background-text,#ffffff)",
}}
/>
<p>{bullet}</p>
</p>
))}
</div>
</div>
))}
</div>
</div>
</>
);
};
export default HeadlineWithDetailColumns;

View file

@ -0,0 +1,323 @@
import { RemoteSvgIcon } from "@/app/hooks/useRemoteSvgIcon";
import * as z from "zod";
export const slideLayoutId = "horizontal-timeline";
export const slideLayoutName = "Horizontal Timeline";
export const slideLayoutDescription =
"A horizontal timeline with step markers, item text, continuation state, and optional endpoint label.";
const MAX_TIMELINE_ITEMS_PER_SLIDE = 5;
const DEFAULT_ICON = {
__icon_url__:
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/icons/placeholder.svg",
__icon_query__: "shield check icon",
};
const TimelineItemSchema = z.object({
label: z.string().max(10).meta({
description: "Short label above marker icon.",
}),
icon: z
.object({
__icon_url__: z.string(),
__icon_query__: z.string(),
})
.default(DEFAULT_ICON),
title: z.string().max(16).meta({
description: "Heading below marker icon.",
}),
description: z.string().max(132).meta({
description: "Supporting copy for each timeline item.",
}),
});
const DEFAULT_DESCRIPTION =
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.";
export const Schema = z.object({
title: z.string().max(18).default("Timeline").meta({
description: "Top-left heading.",
}),
isContinue: z.boolean().default(false).meta({
description:
"Whether this slide continues a previous timeline slide. Continuation slides use the Continue... heading and draw the axis in from the left edge.",
}),
showEndLabel: z.boolean().default(true).meta({
description: "Whether to show right-end label near timeline axis.",
}),
endLabel: z.string().max(12).default("THE END").meta({
description: "Right-end label text.",
}),
items: z
.array(TimelineItemSchema)
.max(MAX_TIMELINE_ITEMS_PER_SLIDE)
.default([
{
label: "Phase 1",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 2",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 3",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 4",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
label: "Phase 5",
icon: DEFAULT_ICON,
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
])
.meta({
description: "Timeline items from left to right.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const SLIDE_WIDTH = 1280;
const TIMELINE_AXIS_Y = 355;
const TIMELINE_LEFT_CENTER = 106;
const TIMELINE_RIGHT_CENTER = 1058;
const THREE_STEP_CENTERS = [106, 580, 1058];
const CONTINUATION_TITLE = "Continue...";
const getTimelineCenters = (count: number) => {
if (count === 3) {
return THREE_STEP_CENTERS;
}
if (count <= 1) {
return [TIMELINE_LEFT_CENTER];
}
return Array.from({ length: count }, (_, index) => {
return (
TIMELINE_LEFT_CENTER +
((TIMELINE_RIGHT_CENTER - TIMELINE_LEFT_CENTER) * index) / (count - 1)
);
});
};
const getTimelineStyle = (count: number) => {
if (count <= 3) {
return {
badgeSize: 88,
labelTop: 268,
titleTop: 423,
contentWidth: 260,
labelFontSize: 25,
titleFontSize: 25,
bodyFontSize: 17,
};
}
if (count === 4) {
return {
badgeSize: 76,
labelTop: 274,
titleTop: 417,
contentWidth: 240,
labelFontSize: 23,
titleFontSize: 25,
bodyFontSize: 17,
};
}
return {
badgeSize: 66,
labelTop: 280,
titleTop: 412,
contentWidth: 210,
labelFontSize: 20,
titleFontSize: 25,
bodyFontSize: 17,
};
};
function TimelineIconBadge({
icon,
size,
}: {
icon: { __icon_url__: string; __icon_query__: string };
size: number;
}) {
return (
<span
className="inline-flex items-center justify-center rounded-full"
style={{
width: size,
height: size,
backgroundColor: "var(--primary-color,#dddac7)",
}}
>
{/* <img
src={icon.__icon_url__}
alt={icon.__icon_query__}
className="object-contain"
style={{ width: size * 0.42, height: size * 0.42 }}
/> */}
<RemoteSvgIcon
url={icon.__icon_url__}
className={`w-[${size}px] h-[${size}px]`}
strokeColor={"currentColor"}
color="var(--primary-text,#27292d)"
title={icon.__icon_query__}
/>
</span>
);
}
const HorizontalTimeline = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const timelineItems = slideData.items;
const timelineCenters = getTimelineCenters(timelineItems.length);
const timelineStyle = getTimelineStyle(timelineItems.length);
const firstCenter = timelineCenters[0] ?? TIMELINE_LEFT_CENTER;
const lastCenter =
timelineCenters[timelineCenters.length - 1] ?? TIMELINE_RIGHT_CENTER;
const axisStart = slideData.isContinue ? 0 : firstCenter;
const axisEnd = slideData.showEndLabel ? lastCenter : SLIDE_WIDTH;
const endLabelLeft = Math.min(
SLIDE_WIDTH - 152,
lastCenter + timelineStyle.badgeSize / 2 + 18
);
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[34px] pt-[40px]">
<h2
className="font-serif text-[100px] leading-none"
style={{
color: "var(--primary-color,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="absolute h-[2px]"
style={{
left: axisStart,
top: TIMELINE_AXIS_Y,
width: axisEnd - axisStart,
backgroundColor: "var(--primary-color,#dddac7)",
}}
/>
{timelineItems.map((phase, index) => {
const centerX = timelineCenters[index] ?? TIMELINE_LEFT_CENTER;
const textLeft = Math.max(
30,
Math.min(
SLIDE_WIDTH - timelineStyle.contentWidth - 30,
centerX - timelineStyle.badgeSize / 2
)
);
return (
<div key={`${phase.label}-${index}`}>
<p
className="absolute leading-none"
style={{
left: textLeft,
top: timelineStyle.labelTop,
color: "var(--primary-text,#d7d3be)",
fontSize: timelineStyle.labelFontSize,
}}
>
{phase.label}
</p>
<div
className="absolute"
style={{
left: centerX - timelineStyle.badgeSize / 2,
top: TIMELINE_AXIS_Y - timelineStyle.badgeSize / 2,
}}
>
<TimelineIconBadge
icon={phase.icon}
size={timelineStyle.badgeSize}
/>
</div>
<div
className="absolute"
style={{
left: textLeft,
top: timelineStyle.titleTop,
width: timelineStyle.contentWidth,
}}
>
<p
className="leading-none"
style={{
color: "var(--background-text,#d7d3be)",
fontSize: timelineStyle.titleFontSize,
}}
>
{phase.title}
</p>
<p
className="mt-[12px] leading-[1.16]"
style={{
color: "var(--background-text,#cbc7b2)",
fontSize: timelineStyle.bodyFontSize,
}}
>
{phase.description}
</p>
</div>
</div>
);
})}
{slideData.showEndLabel && (
<p
className="absolute top-[346px] text-[25px] leading-none"
style={{
left: endLabelLeft,
color: "var(--primary-text,#d7d3be)",
}}
>
{slideData.endLabel}
</p>
)}
</div>
</>
);
};
export default HorizontalTimeline;

View file

@ -0,0 +1,147 @@
import * as z from "zod";
export const slideLayoutId = "media-and-text-split";
export const slideLayoutName = "Media and Text Split";
export const slideLayoutDescription =
"A split composition with a title and media block on the left and supporting narrative plus footer text on the right.";
export const Schema = z
.object({
title: z
.string()
.max(16)
.meta({
description: "Left panel heading.",
})
.default("Overview"),
sidePanelMode: z.enum(["solid", "image"]).default("image").meta({
description: "Left media panel mode.",
}),
sidePanelColor: z.string().max(20).default("#d3d0bc").meta({
description: "Left media color used in solid mode.",
}),
sidePanelImage: z
.object({
__image_url__: z
.string()
.default(
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1400&q=80"
),
__image_prompt__: z.string().default("Glass skyscraper perspective"),
})
.meta({
description: "Left media image used in image mode.",
}),
headline: z
.string()
.max(50)
.default(
"This is a sample text to tell story for audience is written here"
)
.meta({
description: "Main headline text on the right.",
}),
body: z
.string()
.max(128)
.default(
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam."
)
.meta({
description: "Supporting paragraph text.",
}),
footerText: z.string().max(28).default("Footer text").meta({
description: "Footer text at the bottom-right.",
}),
})
.default({
title: "Overview",
sidePanelMode: "image",
sidePanelColor: "#d3d0bc",
sidePanelImage: {
__image_url__:
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1400&q=80",
__image_prompt__: "Glass skyscraper perspective",
},
headline:
"This is a sample text to tell story for audience is written here",
body: "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.",
footerText: "Footer text",
});
export type SchemaType = z.infer<typeof Schema>;
const MediaAndTextSplit = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<h2
className="px-[38px] pt-[48px] text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#dddac7)",
}}
>
{slideData.title}
</h2>
<div className="flex items-center mt-[30px]">
<div
className=" w-[572px] h-[542px]"
style={{
backgroundColor:
slideData.sidePanelMode === "solid"
? slideData.sidePanelColor
: "transparent",
}}
>
{slideData.sidePanelMode === "image" && (
<img
src={slideData.sidePanelImage.__image_url__}
alt={slideData.sidePanelImage.__image_prompt__}
className="h-full w-full object-cover"
/>
)}
</div>
<div className="px-[66px] flex-1 mt-[31px] flex flex-col h-full">
<div className="flex-1">
<h3
className="max-w-[610px] text-[32px] leading-[1.08]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{slideData.headline}
</h3>
<p
className="mt-[34px] max-w-[610px] text-[22px] leading-[1.16]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{slideData.body}
</p>
</div>
<p
className="mt-[100px] text-[34px] leading-none"
style={{ color: "var(--background-text,#dddac7)" }}
>
{slideData.footerText}
</p>
</div>
</div>
</div>
</>
);
};
export default MediaAndTextSplit;

View file

@ -0,0 +1,140 @@
import * as z from "zod";
export const slideLayoutId = "numbered-multi-column-overview";
export const slideLayoutName = "Numbered Multi-Column Overview";
export const slideLayoutDescription =
"A multi-column layout with numbered markers, short titles, and descriptive body text.";
const ColumnSchema = z.object({
marker: z.string().max(2).meta({
description: "Circular marker value.",
}),
title: z.string().max(14).meta({
description: "Column title.",
}),
description: z.string().max(118).meta({
description: "Column description paragraph.",
}),
highlighted: z.boolean().default(false).meta({
description: "Whether marker circle is filled.",
}),
});
export const Schema = z.object({
title: z.string().max(16).default("Overview").meta({
description: "Main heading text.",
}),
items: z
.array(ColumnSchema)
.max(4)
.default([
{
marker: "1",
title: "Heading 1",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.",
highlighted: true,
},
{
marker: "2",
title: "Heading 2",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.",
highlighted: false,
},
{
marker: "3",
title: "Heading 3",
description:
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.",
highlighted: false,
},
// { marker: "4", title: "Heading 4", description: "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim.", highlighted: false },
])
.meta({
description: "Columns rendered across the slide.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const NumberedMultiColumnOverview = ({
data,
}: {
data: Partial<SchemaType>;
}) => {
const slideData = data as SchemaType;
const columns = slideData.items.length;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[48px] pt-[72px]">
<h2
className=" text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="px-[48px] pt-[54px] grid gap-[26px]"
style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}
>
{slideData.items.map((column, index) => (
<div key={`${column.title}-${index}`}>
<div
className="flex h-[70px] w-[70px] items-center justify-center rounded-full border text-[29px] font-semibold leading-none"
style={{
borderColor: column.highlighted
? "var(--primary-color,#dddac7)"
: "var(--primary-color,#8d8a7d)",
color: column.highlighted
? "var(--primary-text,#27292d)"
: "var(--background-text,#dddac7)",
backgroundColor: column.highlighted
? "var(--primary-color,#dddac7)"
: "transparent",
}}
>
{column.marker}
</div>
<p
className="mt-[28px] text-[32px] leading-none"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{column.title}
</p>
<p
className="mt-[34px] text-[22px] leading-[1.14]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{column.description}
</p>
</div>
))}
</div>
</div>
</>
);
};
export default NumberedMultiColumnOverview;

View file

@ -0,0 +1,221 @@
import * as z from "zod";
export const slideLayoutId = "overlapping-circle-cards";
export const slideLayoutName = "Overlapping Circle Cards";
export const slideLayoutDescription =
"A horizontal row of overlapping circular cards with markers, titles, and text.";
const CardSchema = z.object({
number: z.string().max(2).meta({
description: "Short card marker.",
}),
title: z.string().max(16).meta({
description: "Card title.",
}),
description: z.string().max(132).meta({
description: "Card description text.",
}),
});
const DEFAULT_DESCRIPTION =
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam.";
export const Schema = z.object({
title: z.string().max(12).default("Cards").meta({
description: "Main heading text.",
}),
items: z
.array(CardSchema)
.max(5)
.default([
{
number: "01",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "02",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "03",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "04",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
{
number: "05",
title: "Insert text here",
description: DEFAULT_DESCRIPTION,
},
])
.meta({
description: "Circle cards from left to right.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const CIRCLE_CARD_LAYOUT_BY_COUNT: Record<
number,
{
circleSize: number;
connectorSize: number;
overlap: number;
rowLeft: number;
rowTop: number;
numberFontSize: number;
titleFontSize: number;
bodyFontSize: number;
bodyWidth: number;
}
> = {
3: {
circleSize: 384,
connectorSize: 84,
overlap: -21,
rowLeft: 34,
rowTop: 238,
numberFontSize: 32,
titleFontSize: 30,
bodyFontSize: 20,
bodyWidth: 250,
},
4: {
circleSize: 318,
connectorSize: 70,
overlap: -21,
rowLeft: 44,
rowTop: 238,
numberFontSize: 30,
titleFontSize: 28,
bodyFontSize: 18,
bodyWidth: 214,
},
5: {
circleSize: 272,
connectorSize: 56,
overlap: -37,
rowLeft: 34,
rowTop: 238,
numberFontSize: 24,
titleFontSize: 22,
bodyFontSize: 16,
bodyWidth: 172,
},
};
const getConnectorLeft = (overlap: number, connectorSize: number) =>
(Math.abs(overlap) - connectorSize) / 2;
const OverlappingCircleCards = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const count = slideData.items.length;
const layout =
CIRCLE_CARD_LAYOUT_BY_COUNT[count] ?? CIRCLE_CARD_LAYOUT_BY_COUNT[5];
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="px-[36px] pt-[44px]">
<h2
className="font-serif text-[100px] leading-none"
style={{
color: "var(--background-text,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
</div>
<div
className="absolute flex items-center"
style={{ left: layout.rowLeft, top: layout.rowTop }}
>
{slideData.items.map((item, index) => (
<div
key={`${item.number}-${index}`}
className="relative"
style={{ marginLeft: index === 0 ? 0 : layout.overlap }}
>
{index > 0 && (
<span
className="absolute top-1/2 -translate-y-1/2 rounded-full"
style={{
left: getConnectorLeft(
layout.overlap,
layout.connectorSize
),
width: layout.connectorSize,
height: layout.connectorSize,
backgroundColor: "var(--primary-color,#dddac7)",
}}
/>
)}
<div
className="flex flex-col items-center justify-center rounded-full border text-center"
style={{
width: layout.circleSize,
height: layout.circleSize,
borderColor: "var(--stroke,#dddac7)",
borderWidth: 2,
color: "var(--background-text,#d7d3be)",
}}
>
<p
className="font-semibold leading-none"
style={{
fontSize: layout.numberFontSize,
color: "var(--background-text,#dddac7)",
}}
>
{item.number}
</p>
<p
className="mt-[16px] leading-none"
style={{
fontSize: layout.titleFontSize,
color: "var(--background-text,#d7d3be)",
}}
>
{item.title}
</p>
<p
className="mt-[12px] leading-[1.15]"
style={{
width: layout.bodyWidth,
color: "var(--background-text,#cbc7b2)",
fontSize: layout.bodyFontSize,
}}
>
{item.description}
</p>
</div>
</div>
))}
</div>
</div>
</>
);
};
export default OverlappingCircleCards;

View file

@ -0,0 +1,269 @@
import * as z from "zod";
export const slideLayoutId = "panel-list-with-media";
export const slideLayoutName = "Panel List with Media";
export const slideLayoutDescription =
"A table layout with a left multi-column item list and a configurable right media or color panel.";
const ItemSchema = z.object({
title: z.string().max(18).meta({
description: "Item title in the list.",
}),
number: z.string().max(2).meta({
description: "Section number shown on the right.",
}),
description: z.string().max(40).optional().meta({
description: "Optional item description used in description variants.",
}),
});
export const Schema = z
.object({
title: z.string().max(14).default("List").meta({
description: "Main heading text.",
}),
rowVariant: z
.enum(["titleOnly", "titleWithDescription"])
.default("titleWithDescription")
.meta({
description: "Layout variant for item rows.",
}),
sidePanelMode: z.enum(["solid", "image"]).default("solid").meta({
description: "Right-side panel style.",
}),
sidePanelImage: z
.object({
__image_url__: z
.string()
.url()
.default(
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1600&q=80"
),
__image_prompt__: z.string().default("Skyscraper perspective photo"),
})
.meta({
description: "Right-side panel image used in image mode.",
}),
items: z
.array(ItemSchema)
.max(10)
.default([
{
title: "Section Title",
number: "1",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Clarity",
number: "2",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Design Principles",
number: "3",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Visual Structure",
number: "4",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Typography",
number: "5",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Color & Space",
number: "6",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Audience Focus",
number: "7",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Layout System",
number: "8",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Presentation Flow",
number: "9",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Key Takeaways",
number: "10",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
])
.meta({
description: "Items shown in the left panel.",
}),
})
.default({
title: "List",
rowVariant: "titleWithDescription",
sidePanelMode: "image",
sidePanelImage: {
__image_url__:
"https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1600&q=80",
__image_prompt__: "Skyscraper perspective photo",
},
items: [
{
title: "Section Title",
number: "1",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Clarity",
number: "2",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Design Principles",
number: "3",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Visual Structure",
number: "4 ",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Typography",
number: "5",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Color & Space",
number: "6",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Audience Focus",
number: "7",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Layout System",
number: "8",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Presentation Flow",
number: "9",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
{
title: "Key Takeaways",
number: "10",
description: "Ut enim ad minima. Ut enim ad minima veniam.",
},
],
});
export type SchemaType = z.infer<typeof Schema>;
const PanelListWithMedia = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
const showDescriptions = slideData.rowVariant === "titleWithDescription";
const midpoint = Math.ceil(slideData.items && slideData.items.length / 2);
const leftItems = slideData.items && slideData.items.slice(0, midpoint);
const rightItems = slideData.items && slideData.items.slice(midpoint);
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="flex h-full ">
<div className="px-[44px] pt-[40px] flex-1">
<h2
className="font-serif text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#dddac7)",
fontFamily: "var(--heading-font-family,'DM Serif Display')",
}}
>
{slideData.title}
</h2>
<div className="mt-[34px] grid grid-cols-2 gap-x-[54px]">
{[leftItems, rightItems].map((column, columnIndex) => (
<div key={`column-${columnIndex}`}>
{column &&
column.map((item) => (
<div
key={`${columnIndex}-${item.number}-${item.title}`}
className={
showDescriptions
? "border-b pb-[12px] pt-[12px]"
: "border-b py-[16px]"
}
style={{ borderColor: "var(--stroke,#4c4e53)" }}
>
<div className="flex items-start justify-between gap-[14px]">
<p
className="text-[28px] leading-[1.08]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{item.title}
</p>
<p
className="pt-[2px] text-[28px] font-semibold leading-none"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{item.number}
</p>
</div>
{showDescriptions && (
<p
className="mt-[6px] text-[22px] leading-[1.08]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{item.description ?? "Ut enim ad minima veniam."}
</p>
)}
</div>
))}
</div>
))}
</div>
</div>
<div
className="h-full w-[408px]"
style={{ backgroundColor: "var(--primary-color,#d7d3be)" }}
>
{slideData.sidePanelMode === "image" && (
<img
src={slideData.sidePanelImage.__image_url__}
alt={slideData.sidePanelImage.__image_prompt__}
className="h-full w-full object-cover"
/>
)}
</div>
</div>
</div>
</>
);
};
export default PanelListWithMedia;

View file

@ -0,0 +1,477 @@
"use client";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
LabelList,
Line,
LineChart,
Pie,
PieChart,
ResponsiveContainer,
Scatter,
ScatterChart,
XAxis,
YAxis,
} from "recharts";
export type PitchChartType = "bar" | "pie" | "scatter" | "stackedBar" | "line";
export type PitchBarDatum = {
label: string;
value: number;
value2?: number;
};
export type PitchPieDatum = {
label: string;
value: number;
color: string;
};
export type PitchScatterDatum = {
label: string;
value: number;
};
export type PitchChartPayload = {
chartType: PitchChartType;
legendLabel: string;
yAxisLabel: string;
barData: PitchBarDatum[];
pieData: PitchPieDatum[];
scatterData: PitchScatterDatum[];
lineData: PitchBarDatum[];
stackedBarData: PitchBarDatum[];
};
type Props = {
payload?: Partial<PitchChartPayload> | null;
};
type PieLabelProps = {
cx?: number;
cy?: number;
midAngle?: number;
outerRadius?: number;
value?: string | number;
};
const DEFAULT_CHART_COLORS = [
"#8B5CF6",
"#06B6D4",
"#10B981",
"#F59E0B",
"#EF4444",
"#EC4899",
];
const AXIS = "var(--background-text,#d8d4bf)";
const GRID = "var(--background-text,#585a61)";
const CHART_RIGHT_MARGIN = 36;
const graphColors = (index: number, fallbackColor?: string) => {
const fallback =
fallbackColor || DEFAULT_CHART_COLORS[index % DEFAULT_CHART_COLORS.length];
return `var(--graph-${index}, ${fallback})`;
};
const DEFAULT_CHART_PAYLOAD: PitchChartPayload = {
chartType: "bar",
legendLabel: "Series Label",
yAxisLabel: "Y axis name",
barData: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
],
pieData: [
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
],
scatterData: [
{ label: "Mon", value: 7 },
{ label: "Tue", value: 2 },
{ label: "Wed", value: 92 },
{ label: "Thu", value: 15 },
{ label: "Fri", value: 91 },
{ label: "Sat", value: 73 },
{ label: "Sun", value: 56 },
],
lineData: [
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
],
stackedBarData: [
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
],
};
function resolveChartPayload(
payload?: Partial<PitchChartPayload> | null
): PitchChartPayload {
return {
...DEFAULT_CHART_PAYLOAD,
...payload,
barData: payload?.barData?.length
? payload.barData
: DEFAULT_CHART_PAYLOAD.barData,
pieData: payload?.pieData?.length
? payload.pieData
: DEFAULT_CHART_PAYLOAD.pieData,
scatterData: payload?.scatterData?.length
? payload.scatterData
: DEFAULT_CHART_PAYLOAD.scatterData,
lineData: payload?.lineData?.length
? payload.lineData
: DEFAULT_CHART_PAYLOAD.lineData,
stackedBarData: payload?.stackedBarData?.length
? payload.stackedBarData
: DEFAULT_CHART_PAYLOAD.stackedBarData,
chartType: payload?.chartType || DEFAULT_CHART_PAYLOAD.chartType,
legendLabel: payload?.legendLabel || DEFAULT_CHART_PAYLOAD.legendLabel,
yAxisLabel: payload?.yAxisLabel || DEFAULT_CHART_PAYLOAD.yAxisLabel,
};
}
function PiePercentLabel({
cx = 0,
cy = 0,
midAngle = 0,
outerRadius = 0,
value = "",
}: PieLabelProps) {
const radius = outerRadius * 0.72;
const x = cx + radius * Math.cos((-midAngle * Math.PI) / 180);
const y = cy + radius * Math.sin((-midAngle * Math.PI) / 180);
const text = `${value}%`;
return (
<g>
<rect
x={x - 24}
y={y - 13}
rx={11}
ry={11}
width={48}
height={26}
fill={"var(--card-color,#ececeb)"}
/>
<text
x={x}
y={y + 5}
textAnchor="middle"
fontSize={16}
fontWeight={600}
fill={"var(--background-text,#2f2f2f)"}
>
{text}
</text>
</g>
);
}
function Legend({
label,
color = graphColors(0),
}: {
label: string;
color?: string;
}) {
return (
<div
className="flex shrink-0 items-center justify-center gap-[10px] pt-[8px]"
style={{ color: AXIS }}
>
<span
className="h-[16px] w-[16px] rounded-full"
style={{ backgroundColor: color }}
/>
<span className="text-[18px] leading-none">{label}</span>
</div>
);
}
export default function PitchDeckChart({ payload }: Props) {
const {
chartType,
barData,
pieData,
scatterData,
lineData,
stackedBarData,
legendLabel,
yAxisLabel,
} = resolveChartPayload(payload);
if (chartType === "pie") {
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<PieChart
margin={{ top: 18, right: CHART_RIGHT_MARGIN, bottom: 18, left: 28 }}
>
<Pie
data={pieData}
dataKey="value"
nameKey="label"
cx="50%"
cy="50%"
outerRadius="74%"
stroke="none"
labelLine={false}
label={({ cx, cy, midAngle, outerRadius, value }) => (
<PiePercentLabel
cx={Number(cx)}
cy={Number(cy)}
midAngle={Number(midAngle)}
outerRadius={Number(outerRadius)}
value={String(value)}
/>
)}
isAnimationActive={false}
>
{pieData.map((entry, index) => (
<Cell
key={`${entry.label}-${index}`}
fill={graphColors(index, entry.color)}
/>
))}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
<div
className="flex shrink-0 items-center justify-center gap-[26px] pb-[2px] pt-[8px] text-[18px] leading-none"
style={{ color: AXIS }}
>
{pieData.map((entry, index) => (
<span key={entry.label} className="flex items-center gap-[10px]">
<span
className="h-[15px] w-[15px] rounded-full"
style={{ backgroundColor: graphColors(index, entry.color) }}
/>
{entry.label}
</span>
))}
</div>
</div>
);
}
if (chartType === "scatter") {
const points = scatterData.map((item, index) => ({
x: index + 1,
y: item.value,
label: item.label,
}));
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart
margin={{ top: 18, right: CHART_RIGHT_MARGIN, left: 18, bottom: 20 }}
>
<CartesianGrid
vertical={false}
horizontal={false}
stroke={GRID}
opacity={0.7}
/>
<XAxis
type="number"
dataKey="x"
domain={[1, Math.max(points.length, 2)]}
tickCount={points.length}
tickFormatter={(value) =>
points[Number(value) - 1]?.label ?? "label"
}
tick={{ fill: AXIS, fontSize: 18 }}
axisLine={{ stroke: AXIS }}
tickLine={false}
/>
<YAxis
type="number"
dataKey="y"
domain={[0, 100]}
tick={{ fill: AXIS, fontSize: 18 }}
axisLine={false}
tickLine={false}
tickSize={0}
width={64}
label={{
value: yAxisLabel,
angle: -90,
position: "insideLeft",
fill: AXIS,
fontSize: 18,
offset: 0,
}}
/>
<Scatter
data={points}
fill={graphColors(0)}
isAnimationActive={false}
/>
</ScatterChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}
if (chartType === "line") {
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={lineData}
margin={{ top: 24, right: CHART_RIGHT_MARGIN, left: 8, bottom: 20 }}
>
<CartesianGrid stroke={GRID} vertical={false} opacity={0.7} />
<XAxis
dataKey="label"
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={{ stroke: AXIS }}
/>
<YAxis
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={false}
width={34}
/>
<Line
dataKey="value"
stroke={graphColors(0)}
strokeWidth={4}
dot={{ r: 5, fill: graphColors(0) }}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}
if (chartType === "stackedBar") {
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={stackedBarData}
margin={{ top: 28, right: CHART_RIGHT_MARGIN, left: 8, bottom: 20 }}
>
<XAxis
dataKey="label"
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={{ stroke: AXIS }}
/>
<YAxis
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={false}
width={34}
/>
<Bar
dataKey="value"
stackId="stack"
fill={graphColors(1)}
radius={[5, 5, 0, 0]}
isAnimationActive={false}
>
<LabelList
dataKey="value"
position="insideTop"
fill={"var(--primary-text,#ffffff)"}
fontSize={16}
/>
</Bar>
<Bar
dataKey="value2"
stackId="stack"
fill={graphColors(0)}
radius={[5, 5, 0, 0]}
isAnimationActive={false}
>
<LabelList
dataKey="value2"
position="insideTop"
fill={"var(--primary-text,#ffffff)"}
fontSize={16}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}
return (
<div className="flex h-full min-h-[320px] w-full flex-col overflow-hidden">
<div className="min-h-0 flex-1">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={barData}
margin={{ top: 36, right: CHART_RIGHT_MARGIN, left: 8, bottom: 20 }}
>
<XAxis
dataKey="label"
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={{ stroke: AXIS }}
/>
<YAxis
tick={{ fill: AXIS, fontSize: 18 }}
tickLine={false}
axisLine={false}
width={34}
/>
<Bar
dataKey="value"
fill={graphColors(0)}
radius={[5, 5, 0, 0]}
barSize={34}
isAnimationActive={false}
>
<LabelList
dataKey="value"
position="top"
fill={AXIS}
fontSize={16}
/>
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
<Legend label={legendLabel} />
</div>
);
}

View file

@ -0,0 +1,145 @@
"use client";
import * as z from "zod";
import PitchDeckChart from "./PitchDeckChart";
import { ChartPayloadSchema } from "./pitchDeckSchemas";
export const slideLayoutId = "text-and-chart-split-layout";
export const slideLayoutName = "Text and Chart Split Layout";
export const slideLayoutDescription =
"A split layout with narrative text on the left and a configurable chart canvas on the right.";
const DEFAULT_CHART = {
chartType: "scatter" as const,
legendLabel: "Series Label",
yAxisLabel: "Y axis name",
barData: [
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
],
pieData: [
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
],
scatterData: [
{ label: "label", value: 7 },
{ label: "label", value: 2 },
{ label: "label", value: 92 },
{ label: "label", value: 15 },
{ label: "label", value: 91 },
{ label: "label", value: 73 },
{ label: "label", value: 56 },
{ label: "label", value: 90 },
],
lineData: [
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
],
stackedBarData: [
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
],
};
export const Schema = z.object({
title: z.string().max(16).default("Highlights").meta({
description: "Main heading on the left.",
}),
leadText: z
.string()
.max(52)
.default("This is a sample text to tell story for audience is written here")
.meta({
description: "Primary narrative line above supporting text.",
}),
supportingText: z
.string()
.max(126)
.default(
"Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut alUt enim ad minima veniam."
)
.meta({
description: "Supporting paragraph text.",
}),
chart: ChartPayloadSchema.default(DEFAULT_CHART).meta({
description: "Chart configuration payload rendered on the right side.",
}),
showAccentGlow: z.boolean().default(true).meta({
description:
"Whether to render the subtle decorative glow near bottom-left.",
}),
});
export type SchemaType = z.infer<typeof Schema>;
const TextAndChartSplit = ({ data }: { data: Partial<SchemaType> }) => {
const slideData = data as SchemaType;
return (
<>
<link
href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&display=swap"
rel="stylesheet"
/>
<div
className="relative h-[720px] w-[1280px] overflow-hidden "
style={{
backgroundColor: "var(--background-color,#27292d)",
fontFamily: "var(--body-font-family,'DM Serif Display')",
}}
>
<div className="grid h-full grid-cols-[47.5%_52.5%]">
<div className="px-[36px] pt-[44px]">
<h2
className="font-serif text-[100px] leading-none tracking-[-0.02em]"
style={{
color: "var(--background-text,#dddac7)",
}}
>
{slideData.title}
</h2>
<p
className="mt-[76px] max-w-[520px] text-[32px] leading-[1.12]"
style={{ color: "var(--background-text,#d7d3be)" }}
>
{slideData.leadText}
</p>
<p
className="mt-[38px] max-w-[530px] text-[22px] leading-[1.16]"
style={{ color: "var(--background-text,#cbc7b2)" }}
>
{slideData.supportingText}
</p>
</div>
<div className="h-full min-h-0 overflow-hidden px-[24px] pb-[52px] pt-[142px]">
<PitchDeckChart payload={slideData.chart ?? DEFAULT_CHART} />
</div>
</div>
</div>
</>
);
};
export default TextAndChartSplit;

View file

@ -0,0 +1,132 @@
import * as z from "zod";
export const DARK_BG = "var(--background-color,#27292d)";
export const ACCENT_TEXT = "var(--primary-color,#dddac7)";
export const BODY_TEXT = "var(--primary-text,#d7d3be)";
export const MUTED_TEXT = "var(--background-text,#cbc7b2)";
export const BORDER = "var(--border-color,#8d8a7d)";
export const SUBTLE_LINE = "var(--line-color,#4c4e53)";
export const ChartTypeSchema = z.enum(["bar", "pie", "scatter", "stackedBar", "line"]);
export const BarDatumSchema = z.object({
label: z.string().max(10).meta({
description: "X-axis label for bar/line/stacked charts.",
}),
value: z.number().max(300).meta({
description: "Primary numeric value.",
}),
value2: z.number().max(300).optional().meta({
description: "Secondary stacked value when using stacked bar charts.",
}),
});
export const PieDatumSchema = z.object({
label: z.string().max(16).meta({
description: "Legend label for pie slices.",
}),
value: z.number().max(100).meta({
description: "Slice percentage value.",
}),
color: z.string().max(20).meta({
description: "Slice fill color.",
}),
});
export const ScatterDatumSchema = z.object({
label: z.string().max(10).meta({
description: "X-axis label for scatter points.",
}),
value: z.number().max(100).meta({
description: "Y-axis value for the point.",
}),
});
export const ChartPayloadSchema = z.object({
chartType: ChartTypeSchema.default("bar").meta({
description: "Chart type rendered on the right side.",
}),
legendLabel: z.string().max(30).default("Series Label").meta({
description: "Single-series legend label for non-pie charts.",
}),
yAxisLabel: z.string().max(16).default("Y axis name").meta({
description: "Y-axis title used in scatter charts.",
}),
barData: z
.array(BarDatumSchema)
.max(8)
.default([
{ label: "Mon", value: 120 },
{ label: "Tue", value: 200 },
{ label: "Wed", value: 150 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 70 },
{ label: "Sat", value: 110 },
{ label: "Sun", value: 130 },
])
.meta({
description: "Dataset for regular bar charts.",
}),
pieData: z
.array(PieDatumSchema)
.max(3)
.default([
{ label: "Category A", value: 55, color: "#d8d4bf" },
{ label: "Category B", value: 25, color: "#b8b4a3" },
{ label: "Category C", value: 20, color: "#a2a091" },
])
.meta({
description: "Pie chart dataset.",
}),
scatterData: z
.array(ScatterDatumSchema)
.max(10)
.default([
{ label: "label", value: 7 },
{ label: "label", value: 2 },
{ label: "label", value: 92 },
{ label: "label", value: 15 },
{ label: "label", value: 91 },
{ label: "label", value: 73 },
{ label: "label", value: 56 },
{ label: "label", value: 90 },
])
.meta({
description: "Scatter points for distribution charts.",
}),
lineData: z
.array(BarDatumSchema)
.max(8)
.default([
{ label: "Mon", value: 30 },
{ label: "Tue", value: 48 },
{ label: "Wed", value: 64 },
{ label: "Thu", value: 42 },
{ label: "Fri", value: 58 },
{ label: "Sat", value: 70 },
{ label: "Sun", value: 90 },
])
.meta({
description: "Dataset for line charts.",
}),
stackedBarData: z
.array(BarDatumSchema)
.max(8)
.default([
{ label: "Mon", value: 50, value2: 50 },
{ label: "Tue", value: 80, value2: 70 },
{ label: "Wed", value: 90, value2: 90 },
{ label: "Thu", value: 40, value2: 60 },
{ label: "Fri", value: 80, value2: 70 },
{ label: "Sat", value: 90, value2: 90 },
{ label: "Sun", value: 70, value2: 80 },
])
.meta({
description: "Dataset for stacked bar charts using value and value2.",
}),
});

View file

@ -0,0 +1,5 @@
{
"description": "Dark, theme-ready presentation layouts with covers, structured content grids, timelines, narrative splits, and chart-driven slides",
"ordered": false,
"default": false
}