chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,699 @@
|
||||
import type { TemplateV2InsertComponent } from "@/components/slide-editor/events/events";
|
||||
import { normalizeChartTypeName } from "@/components/slide-editor/charts/chart-data";
|
||||
import {
|
||||
measureNoWrapTextWidth,
|
||||
rawFont,
|
||||
} from "@/components/slide-editor/text/template-v2-text";
|
||||
import type {
|
||||
ChartType,
|
||||
Fill,
|
||||
Font,
|
||||
Marker,
|
||||
SlideElement,
|
||||
TableCell,
|
||||
} from "@/components/slide-editor/types";
|
||||
|
||||
const DEFAULT_CHART_INSERT_POSITION = { x: 128, y: 115 };
|
||||
const DEFAULT_CHART_INSERT_SIZE = { width: 717, height: 410 };
|
||||
const DEFAULT_IMAGE_PLACEHOLDER_SRC = "/placeholder.jpg";
|
||||
const TEXT_INSERT_HORIZONTAL_PADDING_PX = 8;
|
||||
const TEXT_INSERT_VERTICAL_PADDING_PX = 14;
|
||||
const IMAGE_RADIUS = { tl: 10, tr: 10, bl: 10, br: 10 };
|
||||
|
||||
export type EditorInsertContent = {
|
||||
elements?: SlideElement[];
|
||||
components?: TemplateV2InsertComponent[];
|
||||
};
|
||||
|
||||
function fittedTextHeight(
|
||||
lineCount: number,
|
||||
fontSize: number,
|
||||
lineHeight: number,
|
||||
) {
|
||||
return Math.ceil(
|
||||
lineCount * fontSize * lineHeight + TEXT_INSERT_VERTICAL_PADDING_PX,
|
||||
);
|
||||
}
|
||||
|
||||
function makeTextElement({
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
size,
|
||||
color = "101323",
|
||||
bold = false,
|
||||
italic = false,
|
||||
lineHeight = 1.1,
|
||||
horizontal = "left",
|
||||
}: {
|
||||
text: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
size: number;
|
||||
color?: string;
|
||||
bold?: boolean;
|
||||
italic?: boolean;
|
||||
lineHeight?: number;
|
||||
horizontal?: "left" | "center" | "right";
|
||||
}): SlideElement {
|
||||
return {
|
||||
type: "text",
|
||||
position: { x, y },
|
||||
size: { width, height },
|
||||
alignment: { horizontal, vertical: "top" },
|
||||
runs: [{ text }],
|
||||
font: {
|
||||
family: "Arial",
|
||||
size,
|
||||
color,
|
||||
bold,
|
||||
italic,
|
||||
line_height: lineHeight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeTableCell({
|
||||
text,
|
||||
font,
|
||||
color,
|
||||
alignment = "left",
|
||||
}: {
|
||||
text: string;
|
||||
font: Font;
|
||||
color?: Fill;
|
||||
alignment?: TableCell["alignment"];
|
||||
}): TableCell {
|
||||
return {
|
||||
alignment,
|
||||
color,
|
||||
runs: [{ text, font }],
|
||||
};
|
||||
}
|
||||
|
||||
function makeBulletListElement(marker: Marker): SlideElement {
|
||||
const baseFont = {
|
||||
size: 18,
|
||||
family: "Inter",
|
||||
color: "#111111",
|
||||
bold: false,
|
||||
italic: false,
|
||||
line_height: 1.4,
|
||||
letter_spacing: 0,
|
||||
ellipsis: false,
|
||||
};
|
||||
const items = [
|
||||
[
|
||||
{
|
||||
text: "Clarify the goal and audience",
|
||||
font: { ...baseFont, bold: true },
|
||||
},
|
||||
],
|
||||
[{ text: "Show the strongest supporting point", font: { ...baseFont } }],
|
||||
[
|
||||
{
|
||||
text: "Close with the next action",
|
||||
font: { ...baseFont, italic: true },
|
||||
},
|
||||
],
|
||||
];
|
||||
const renderFont = rawFont({ font: baseFont });
|
||||
const fittedWidth = Math.ceil(
|
||||
Math.max(
|
||||
...items.map((item, index) => {
|
||||
const prefix =
|
||||
marker === "bullet"
|
||||
? "• "
|
||||
: marker === "number"
|
||||
? `${index + 1}. `
|
||||
: "";
|
||||
const text = item.map((run) => run.text).join("");
|
||||
return measureNoWrapTextWidth(`${prefix}${text}`, renderFont);
|
||||
}),
|
||||
) + TEXT_INSERT_HORIZONTAL_PADDING_PX,
|
||||
);
|
||||
|
||||
return {
|
||||
type: "text-list",
|
||||
position: { x: 122, y: 128 },
|
||||
size: {
|
||||
width: fittedWidth,
|
||||
height: fittedTextHeight(
|
||||
items.length,
|
||||
baseFont.size,
|
||||
baseFont.line_height,
|
||||
),
|
||||
},
|
||||
rotation: 0,
|
||||
font: baseFont,
|
||||
|
||||
marker,
|
||||
items,
|
||||
decorative: false,
|
||||
name: "Project task list",
|
||||
max_items: 6,
|
||||
min_items: 3,
|
||||
max_item_length: 60,
|
||||
min_item_length: 30,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTextInsertElements(kind?: string): SlideElement[] {
|
||||
switch (kind) {
|
||||
case "title-block":
|
||||
return [
|
||||
makeTextElement({
|
||||
text: "Add a clear slide title",
|
||||
x: 109,
|
||||
y: 109,
|
||||
width: 986,
|
||||
height: fittedTextHeight(1, 38, 1.1),
|
||||
size: 38,
|
||||
bold: true,
|
||||
}),
|
||||
];
|
||||
case "subtitle":
|
||||
return [
|
||||
makeTextElement({
|
||||
text: "Add a concise supporting subtitle",
|
||||
x: 122,
|
||||
y: 154,
|
||||
width: 870,
|
||||
height: fittedTextHeight(1, 24, 1.2),
|
||||
size: 24,
|
||||
color: "344054",
|
||||
lineHeight: 1.2,
|
||||
}),
|
||||
];
|
||||
case "bullet-list":
|
||||
return [makeBulletListElement("bullet")];
|
||||
case "numbered-list":
|
||||
return [makeBulletListElement("number")];
|
||||
case "list-item":
|
||||
return [makeBulletListElement("none")];
|
||||
case "quote":
|
||||
return [
|
||||
makeTextElement({
|
||||
text: '"Add a memorable quote or customer insight here."',
|
||||
x: 122,
|
||||
y: 147,
|
||||
width: 858,
|
||||
height: fittedTextHeight(2, 24, 1.25),
|
||||
size: 24,
|
||||
color: "101323",
|
||||
italic: true,
|
||||
lineHeight: 1.25,
|
||||
}),
|
||||
];
|
||||
case "body-text":
|
||||
return [
|
||||
makeTextElement({
|
||||
text: "Add body text here. Use this space for a short paragraph or supporting detail.",
|
||||
x: 122,
|
||||
y: 154,
|
||||
width: 858,
|
||||
height: fittedTextHeight(2, 18, 1.28),
|
||||
size: 18,
|
||||
color: "344054",
|
||||
lineHeight: 1.28,
|
||||
}),
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function chartTypeFromPaletteId(id?: string): ChartType | null {
|
||||
const normalized = normalizeChartTypeName(id);
|
||||
switch (normalized) {
|
||||
case "area":
|
||||
case "bar":
|
||||
case "donut":
|
||||
case "horizontal_bar":
|
||||
case "line":
|
||||
case "pie":
|
||||
case "polar_area":
|
||||
case "radar":
|
||||
case "scatter":
|
||||
return normalized as ChartType;
|
||||
case "stackedbar":
|
||||
case "stacked_bar":
|
||||
return "stacked_bar";
|
||||
case "horizontalstackbar":
|
||||
case "horizontalstackedbar":
|
||||
case "horizontal_stack_bar":
|
||||
case "horizontal_stacked_bar":
|
||||
return "horizontal_stacked_bar";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function chartData(
|
||||
categories: string[],
|
||||
values: number[],
|
||||
colors: string[],
|
||||
) {
|
||||
return categories.map((category, index) => ({
|
||||
label: category,
|
||||
value: values[index] ?? 0,
|
||||
color: colors[index % colors.length] ?? colors[0],
|
||||
}));
|
||||
}
|
||||
|
||||
function chartExample(chartType: ChartType) {
|
||||
switch (chartType) {
|
||||
case "donut":
|
||||
return {
|
||||
title: "Revenue Share by Segment",
|
||||
categories: ["Enterprise", "Mid-market", "Small business", "Consumer"],
|
||||
values: [42, 28, 18, 12],
|
||||
seriesName: "Revenue Share",
|
||||
colors: ["7F22FE", "155DFC", "F59E0B", "12B76A"],
|
||||
};
|
||||
case "horizontal_bar":
|
||||
return {
|
||||
title: "Qualified Leads by Channel",
|
||||
categories: ["Email", "Search", "Social", "Referral"],
|
||||
values: [68, 54, 47, 38],
|
||||
seriesName: "Qualified Leads",
|
||||
colors: ["7F22FE", "155DFC", "F59E0B", "12B76A"],
|
||||
};
|
||||
case "polar_area":
|
||||
return {
|
||||
title: "Support Tickets by Priority",
|
||||
categories: ["Critical", "High", "Medium", "Low"],
|
||||
values: [18, 32, 46, 24],
|
||||
seriesName: "Ticket Volume",
|
||||
colors: ["7F22FE", "155DFC", "F59E0B", "12B76A"],
|
||||
};
|
||||
case "radar":
|
||||
return {
|
||||
title: "Product Readiness Score",
|
||||
categories: ["Design", "Reliability", "Speed", "Security", "Usability"],
|
||||
values: [82, 74, 88, 69, 91],
|
||||
seriesName: "Readiness Score",
|
||||
colors: ["7F22FE", "155DFC", "F59E0B", "12B76A", "06B6D4"],
|
||||
};
|
||||
case "scatter":
|
||||
return {
|
||||
title: "Campaign Conversion Results",
|
||||
categories: ["Campaign A", "Campaign B", "Campaign C", "Campaign D"],
|
||||
values: [42, 57, 63, 76],
|
||||
seriesName: "Conversions",
|
||||
colors: ["7F22FE", "155DFC", "F59E0B", "12B76A"],
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: "Quarterly Revenue Trend",
|
||||
categories: ["Q1", "Q2", "Q3", "Q4"],
|
||||
values: [38, 54, 47, 68],
|
||||
seriesName: "Revenue",
|
||||
colors: ["7F22FE", "155DFC", "F59E0B", "12B76A"],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function makeChartElement(chartType: ChartType): SlideElement {
|
||||
if (chartType === "bar") {
|
||||
const categories = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
|
||||
const values = [70, 120, 45, 145, 105, 105, 45];
|
||||
const colors = [
|
||||
"4D20C5",
|
||||
"155DFC",
|
||||
"F59E0B",
|
||||
"12B76A",
|
||||
"EF4444",
|
||||
"06B6D4",
|
||||
"8B5CF6",
|
||||
];
|
||||
|
||||
return {
|
||||
type: "chart",
|
||||
position: { ...DEFAULT_CHART_INSERT_POSITION },
|
||||
size: { ...DEFAULT_CHART_INSERT_SIZE },
|
||||
chart_type: "bar",
|
||||
title: "Weekly Website Visits\nJun 10-16",
|
||||
color: "4D20C5",
|
||||
axis_color: "D8D8D8",
|
||||
grid_color: "D8D8D8",
|
||||
data_labels: "top",
|
||||
x_axis_grid: true,
|
||||
y_axis_grid: true,
|
||||
x_axis: true,
|
||||
y_axis: true,
|
||||
categories,
|
||||
series: [{ name: "Visits", values }],
|
||||
colors,
|
||||
data: chartData(categories, values, colors),
|
||||
};
|
||||
}
|
||||
|
||||
if (chartType === "line") {
|
||||
const categories = ["2021", "2022", "2023", "2024", "2025", "2026"];
|
||||
const values = [15, 45, 85, 50, 75, 95];
|
||||
const colors = [
|
||||
"4D20C5",
|
||||
"155DFC",
|
||||
"F59E0B",
|
||||
"12B76A",
|
||||
"EF4444",
|
||||
"06B6D4",
|
||||
];
|
||||
|
||||
return {
|
||||
type: "chart",
|
||||
position: { ...DEFAULT_CHART_INSERT_POSITION },
|
||||
size: { ...DEFAULT_CHART_INSERT_SIZE },
|
||||
chart_type: "line",
|
||||
title: "Revenue Growth\n2021-2026",
|
||||
color: "4D20C5",
|
||||
axis_color: "D8D8D8",
|
||||
grid_color: "D8D8D8",
|
||||
data_labels: null,
|
||||
x_axis_grid: true,
|
||||
y_axis_grid: true,
|
||||
x_axis: true,
|
||||
y_axis: false,
|
||||
categories,
|
||||
series: [{ name: "Revenue", values }],
|
||||
colors,
|
||||
data: chartData(categories, values, colors),
|
||||
};
|
||||
}
|
||||
|
||||
if (chartType === "area") {
|
||||
const categories = ["2021", "2022", "2023", "2024", "2025", "2026"];
|
||||
const values = [25, 48, 46, 57, 62, 78];
|
||||
const colors = [
|
||||
"4D20C5",
|
||||
"155DFC",
|
||||
"F59E0B",
|
||||
"12B76A",
|
||||
"EF4444",
|
||||
"06B6D4",
|
||||
];
|
||||
|
||||
return {
|
||||
type: "chart",
|
||||
position: { ...DEFAULT_CHART_INSERT_POSITION },
|
||||
size: { ...DEFAULT_CHART_INSERT_SIZE },
|
||||
chart_type: "area",
|
||||
title: "Monthly Active Users\n2021-2026",
|
||||
color: "7555F6",
|
||||
axis_color: "D8D8D8",
|
||||
grid_color: "D8D8D8",
|
||||
data_labels: null,
|
||||
x_axis_grid: true,
|
||||
y_axis_grid: true,
|
||||
x_axis: true,
|
||||
y_axis: false,
|
||||
categories,
|
||||
series: [{ name: "Active Users", values }],
|
||||
colors,
|
||||
data: chartData(categories, values, colors),
|
||||
};
|
||||
}
|
||||
|
||||
if (chartType === "pie") {
|
||||
const categories = ["Product", "Services", "Support", "Training"];
|
||||
const values = [45, 30, 15, 10];
|
||||
const colors = ["7555F6", "155DFC", "F59E0B", "12B76A"];
|
||||
|
||||
return {
|
||||
type: "chart",
|
||||
position: { ...DEFAULT_CHART_INSERT_POSITION },
|
||||
size: { ...DEFAULT_CHART_INSERT_SIZE },
|
||||
chart_type: "pie",
|
||||
title: "Revenue Mix by Offering",
|
||||
color: "7555F6",
|
||||
axis_color: "D8D8D8",
|
||||
grid_color: "D8D8D8",
|
||||
data_labels: "top",
|
||||
categories,
|
||||
series: [{ name: "Revenue Share", values }],
|
||||
colors,
|
||||
data: chartData(categories, values, colors),
|
||||
};
|
||||
}
|
||||
|
||||
if (chartType === "stacked_bar" || chartType === "horizontal_stacked_bar") {
|
||||
const categories = ["Q1", "Q2", "Q3", "Q4"];
|
||||
const values = [38, 54, 47, 68];
|
||||
const secondaryValues = [24, 36, 31, 42];
|
||||
const colors = ["7F22FE", "155DFC"];
|
||||
|
||||
return {
|
||||
type: "chart",
|
||||
position: { ...DEFAULT_CHART_INSERT_POSITION },
|
||||
size: { width: 538, height: 410 },
|
||||
chart_type: chartType,
|
||||
title: "Quarterly Revenue by Segment",
|
||||
color: "7F22FE",
|
||||
axis_color: "D0D5DD",
|
||||
grid_color: "D0D5DD",
|
||||
data_labels: "top",
|
||||
legend: true,
|
||||
x_axis_grid: true,
|
||||
y_axis_grid: true,
|
||||
categories,
|
||||
series: [
|
||||
{ name: "New Business", values },
|
||||
{ name: "Expansion", values: secondaryValues },
|
||||
],
|
||||
colors,
|
||||
data: chartData(categories, values, colors),
|
||||
};
|
||||
}
|
||||
|
||||
const { title, categories, values, seriesName, colors } =
|
||||
chartExample(chartType);
|
||||
|
||||
return {
|
||||
type: "chart",
|
||||
position: { ...DEFAULT_CHART_INSERT_POSITION },
|
||||
size: { width: 538, height: 410 },
|
||||
chart_type: chartType,
|
||||
title,
|
||||
color: "7F22FE",
|
||||
axis_color: "D0D5DD",
|
||||
grid_color: "D0D5DD",
|
||||
data_labels: "top",
|
||||
x_axis_grid: true,
|
||||
y_axis_grid: true,
|
||||
categories,
|
||||
series: [{ name: seriesName, values }],
|
||||
colors,
|
||||
data: chartData(categories, values, colors),
|
||||
};
|
||||
}
|
||||
|
||||
export function createChartInsertElements(kind?: string): SlideElement[] {
|
||||
const chartType = chartTypeFromPaletteId(kind);
|
||||
return chartType ? [makeChartElement(chartType)] : [];
|
||||
}
|
||||
|
||||
function makeSimpleTableElement(): SlideElement {
|
||||
const baseFont: Font = {
|
||||
family: "Inter",
|
||||
size: 14,
|
||||
color: "#344054",
|
||||
line_height: 1.2,
|
||||
};
|
||||
const headerFont: Font = {
|
||||
...baseFont,
|
||||
color: "#101323",
|
||||
bold: true,
|
||||
};
|
||||
const headerFill: Fill = { color: "#F2F4F7", opacity: 1 };
|
||||
const bodyFill: Fill = { color: "#FFFFFF", opacity: 1 };
|
||||
|
||||
return {
|
||||
type: "table",
|
||||
position: { x: 122, y: 128 },
|
||||
size: { width: 819, height: 186 },
|
||||
font: baseFont,
|
||||
columns: [
|
||||
makeTableCell({ text: "Metric", font: headerFont, color: headerFill }),
|
||||
makeTableCell({ text: "Current", font: headerFont, color: headerFill }),
|
||||
makeTableCell({ text: "Target", font: headerFont, color: headerFill }),
|
||||
],
|
||||
rows: [
|
||||
[
|
||||
makeTableCell({ text: "Activation", font: baseFont, color: bodyFill }),
|
||||
makeTableCell({ text: "68%", font: baseFont, color: bodyFill }),
|
||||
makeTableCell({ text: "75%", font: baseFont, color: bodyFill }),
|
||||
],
|
||||
[
|
||||
makeTableCell({ text: "Retention", font: baseFont, color: bodyFill }),
|
||||
makeTableCell({ text: "42%", font: baseFont, color: bodyFill }),
|
||||
makeTableCell({ text: "50%", font: baseFont, color: bodyFill }),
|
||||
],
|
||||
],
|
||||
min_columns: 2,
|
||||
max_columns: 6,
|
||||
min_rows: 2,
|
||||
max_rows: 8,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTableInsertElements(kind?: string): SlideElement[] {
|
||||
return kind === "simple-table" ? [makeSimpleTableElement()] : [];
|
||||
}
|
||||
|
||||
function makeImageElement({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
name = "Image",
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
name?: string;
|
||||
}): SlideElement {
|
||||
return {
|
||||
type: "image",
|
||||
position: { x, y },
|
||||
size: { width, height },
|
||||
data: DEFAULT_IMAGE_PLACEHOLDER_SRC,
|
||||
fit: "cover",
|
||||
name,
|
||||
border_radius: IMAGE_RADIUS,
|
||||
};
|
||||
}
|
||||
|
||||
export function createImageInsertContent(kind?: string): EditorInsertContent {
|
||||
switch (kind) {
|
||||
case "image":
|
||||
return {
|
||||
elements: [
|
||||
makeImageElement({
|
||||
x: 134,
|
||||
y: 128,
|
||||
width: 666,
|
||||
height: 397,
|
||||
}),
|
||||
],
|
||||
};
|
||||
case "image-text":
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
id: "image_text",
|
||||
description: "Image with heading and supporting text",
|
||||
position: { x: 122, y: 128 },
|
||||
size: { width: 986, height: 371 },
|
||||
elements: [
|
||||
makeImageElement({ x: 0, y: 0, width: 486, height: 371 }),
|
||||
makeTextElement({
|
||||
text: "Add a heading",
|
||||
x: 525,
|
||||
y: 15,
|
||||
width: 442,
|
||||
height: 74,
|
||||
size: 24,
|
||||
bold: true,
|
||||
}),
|
||||
makeTextElement({
|
||||
text: "Add supporting text that explains why this visual matters.",
|
||||
x: 525,
|
||||
y: 108,
|
||||
width: 442,
|
||||
height: 134,
|
||||
size: 16,
|
||||
color: "475467",
|
||||
lineHeight: 1.3,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
case "image-grid":
|
||||
return {
|
||||
components: [
|
||||
{
|
||||
id: "image_grid",
|
||||
description: "Two-by-two image grid",
|
||||
position: { x: 128, y: 122 },
|
||||
size: { width: 717, height: 454 },
|
||||
elements: [
|
||||
makeImageElement({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 346,
|
||||
height: 211,
|
||||
name: "Image 1",
|
||||
}),
|
||||
makeImageElement({
|
||||
x: 371,
|
||||
y: 0,
|
||||
width: 346,
|
||||
height: 211,
|
||||
name: "Image 2",
|
||||
}),
|
||||
makeImageElement({
|
||||
x: 0,
|
||||
y: 243,
|
||||
width: 346,
|
||||
height: 211,
|
||||
name: "Image 3",
|
||||
}),
|
||||
makeImageElement({
|
||||
x: 371,
|
||||
y: 243,
|
||||
width: 346,
|
||||
height: 211,
|
||||
name: "Image 4",
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function createElementInsertElements(kind?: string): SlideElement[] {
|
||||
switch (kind) {
|
||||
case "rectangle":
|
||||
return [
|
||||
{
|
||||
type: "rectangle",
|
||||
position: { x: 134, y: 134 },
|
||||
size: { width: 384, height: 192 },
|
||||
fill: { color: "F4F3FF", opacity: 1 },
|
||||
stroke: { color: "7A5AF8", width: 1.5 },
|
||||
border_radius: { tl: 10, tr: 10, bl: 10, br: 10 },
|
||||
},
|
||||
];
|
||||
case "ellipse":
|
||||
return [
|
||||
{
|
||||
type: "ellipse",
|
||||
position: { x: 134, y: 134 },
|
||||
size: { width: 346, height: 198 },
|
||||
fill: { color: "F4F3FF", opacity: 1 },
|
||||
stroke: { color: "7A5AF8", width: 1.5 },
|
||||
},
|
||||
];
|
||||
case "line":
|
||||
return [
|
||||
{
|
||||
type: "line",
|
||||
position: { x: 134, y: 218 },
|
||||
size: { width: 435, height: 1 },
|
||||
stroke: { color: "101323", width: 2 },
|
||||
},
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user