Merge pull request #543 from presenton/feat/new_template
feat: New template pitch deck
This commit is contained in:
commit
34bae816d6
33 changed files with 8414 additions and 497 deletions
|
|
@ -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
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue