Merge pull request #514 from presenton/refactor/template_improvement
refactor/template improvement
This commit is contained in:
commit
9f7d64d578
31 changed files with 910 additions and 281 deletions
|
|
@ -6,13 +6,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Presenton</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Unbounded";
|
||||
src: url("../assets/fonts/Unbounded-Medium.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Syne";
|
||||
src: url("../assets/fonts/Syne-Regular.ttf") format("truetype");
|
||||
|
|
@ -62,10 +55,7 @@
|
|||
overflow: hidden;
|
||||
font-family: "Syne", "Segoe UI", system-ui, sans-serif;
|
||||
color: var(--title);
|
||||
background:
|
||||
radial-gradient(circle at 20% 18%, rgba(127, 104, 255, 0.16), transparent 28%),
|
||||
radial-gradient(circle at 82% 84%, rgba(108, 144, 255, 0.12), transparent 30%),
|
||||
linear-gradient(135deg, var(--bg-top) 0%, #f8f9ff 42%, var(--bg-bottom) 100%);
|
||||
|
||||
}
|
||||
|
||||
body::before {
|
||||
|
|
@ -73,11 +63,7 @@
|
|||
position: fixed;
|
||||
inset: -20%;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(circle at center, rgba(255, 255, 255, 0.55), transparent 45%),
|
||||
radial-gradient(circle at 30% 20%, rgba(145, 124, 255, 0.14), transparent 24%);
|
||||
filter: blur(90px);
|
||||
opacity: 0.9;
|
||||
|
||||
}
|
||||
|
||||
.launch-screen {
|
||||
|
|
@ -98,7 +84,7 @@
|
|||
font-size: clamp(1.3rem, 2.05vw, 1.72rem);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.045em;
|
||||
font-family: "Unbounded", "Syne", system-ui, sans-serif;
|
||||
font-family: "Syne", system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -142,12 +128,7 @@
|
|||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.12) 36%,
|
||||
rgba(255, 255, 255, 0.58) 50%,
|
||||
rgba(255, 255, 255, 0.12) 64%,
|
||||
transparent 100%);
|
||||
|
||||
animation: shimmer 1.85s linear infinite;
|
||||
}
|
||||
|
||||
|
|
@ -209,4 +190,4 @@
|
|||
<script src="./script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
|
@ -158,8 +158,7 @@ const SettingsPage = () => {
|
|||
isDisabled: false,
|
||||
text: "Save Configuration",
|
||||
}));
|
||||
trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" });
|
||||
router.push("/upload");
|
||||
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
|
|
|
|||
|
|
@ -178,6 +178,8 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
"SCRIPT",
|
||||
"STYLE",
|
||||
"NOSCRIPT", // Script/style elements
|
||||
"PRE",
|
||||
"CODE", // Code block elements
|
||||
];
|
||||
|
||||
// List of class patterns that indicate ignored element trees
|
||||
|
|
@ -197,6 +199,10 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
"flowchart",
|
||||
"mermaid",
|
||||
"diagram",
|
||||
"prism",
|
||||
"token",
|
||||
"code-block",
|
||||
"language-",
|
||||
];
|
||||
|
||||
// Check if current element or any parent is in ignored list
|
||||
|
|
@ -231,6 +237,14 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
return true;
|
||||
}
|
||||
|
||||
// Skip syntax-highlighted and code-rendered areas entirely
|
||||
if (
|
||||
currentElement.classList.contains("prism-code-block") ||
|
||||
!!currentElement.closest("pre, code, .prism-code-block")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ export const SlidePreviewSection: React.FC<SlidePreviewSectionProps> = ({
|
|||
size="lg"
|
||||
onClick={onInitTemplate}
|
||||
disabled={isLoading}
|
||||
className="px-4 py-2 h-auto text-xs font-semibold rounded-full shadow-lg hover:shadow-xl transition-all duration-300 "
|
||||
className="px-4 py-2 h-auto text-xs font-syne font-medium rounded-full shadow-lg hover:shadow-xl transition-all duration-300 "
|
||||
style={{
|
||||
background: isLoading
|
||||
? '#E5E7EB'
|
||||
|
|
@ -91,7 +91,7 @@ export const SlidePreviewSection: React.FC<SlidePreviewSectionProps> = ({
|
|||
</>
|
||||
) : (
|
||||
<>
|
||||
<Sparkles className="w-4 h-4 mr-1" />
|
||||
|
||||
Generate Template
|
||||
<ChevronRight className="w-4 h-4 ml-1" />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import "../utils/prism-languages";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -62,6 +63,10 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
const data = await DashboardApi.getPresentation(presentation_id);
|
||||
dispatch(setPresentationData(data));
|
||||
setContentLoading(false);
|
||||
|
||||
if (data.fonts) {
|
||||
useFontLoader(data.fonts);
|
||||
}
|
||||
if (data?.theme) {
|
||||
try {
|
||||
applyTheme(data.theme);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import React from "react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import PdfMakerPage from "./PdfMakerPage";
|
||||
import "../utils/prism-languages";
|
||||
const page = () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -389,6 +389,9 @@ const PresentationHeader = ({
|
|||
) : (
|
||||
titleBlock
|
||||
)}
|
||||
<a href={`/pdf-maker?id=${presentation_id}`}>
|
||||
pdf-maker
|
||||
</a>
|
||||
<div className="flex items-center gap-2.5">
|
||||
|
||||
{isPresentationSaving && <div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import React, { useLayoutEffect, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import "../../utils/prism-languages";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import PresentationMode from "./PresentationMode";
|
||||
import SidePanel from "./SidePanel";
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const ThemeSelector = ({ current_theme, themes: allThemes }: { current_theme: an
|
|||
dispatch(updateTheme(theme));
|
||||
};
|
||||
const resetTheme = async () => {
|
||||
dispatch(updateTheme({} as any));
|
||||
dispatch(updateTheme(null));
|
||||
clearPresentationThemeFromElement(
|
||||
document.getElementById("presentation-slides-wrapper")
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { DashboardApi } from '../../services/api/dashboard';
|
|||
import { clearHistory } from "@/store/slices/undoRedoSlice";
|
||||
import { applyPresentationThemeToElement } from "../utils/applyPresentationThemeDom";
|
||||
import { resolveBackendAssetUrl } from "@/utils/api";
|
||||
import { useFontLoader } from "../../hooks/useFontLoad";
|
||||
|
||||
const normalizePresentationAssets = <T,>(input: T): T => {
|
||||
if (Array.isArray(input)) {
|
||||
|
|
@ -46,6 +47,9 @@ export const usePresentationData = (
|
|||
dispatch(clearHistory());
|
||||
setLoading(false);
|
||||
}
|
||||
if (data.fonts) {
|
||||
useFontLoader(data.fonts);
|
||||
}
|
||||
if (normalizedData?.theme) {
|
||||
const el = document.getElementById("presentation-slides-wrapper");
|
||||
applyPresentationThemeToElement(el, normalizedData.theme);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React from "react";
|
|||
import PresentationPage from "./components/PresentationPage";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import "../utils/prism-languages";
|
||||
const page = () => {
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|||
import { Card } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeft, Home, Loader2, Trash2 } from "lucide-react";
|
||||
import "../../utils/prism-languages";
|
||||
|
||||
import { MixpanelEvent, trackEvent } from "@/utils/mixpanel";
|
||||
import TemplateService from "../../services/api/template";
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import React, { Suspense } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import GroupLayoutPreview from "./components/TemplatePreviewClient";
|
||||
import "../utils/prism-languages";
|
||||
|
||||
const TemplatePreviewPage = () => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-typescript";
|
||||
import "prismjs/components/prism-jsx";
|
||||
import "prismjs/components/prism-tsx";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-bash";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import "prismjs/components/prism-markdown";
|
||||
|
||||
void Prism;
|
||||
|
||||
export {};
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import * as z from "zod";
|
||||
import { fitCodeBlock, PRISM_CODE_BLOCK_STYLES } from "./codeBlockFitting";
|
||||
|
||||
|
||||
export const slideLayoutId = "api-request-response-slide";
|
||||
|
|
@ -63,15 +64,43 @@ export const Schema = z.object({
|
|||
|
||||
export type SchemaType = z.infer<typeof Schema>;
|
||||
|
||||
function normalizeApiJsonSnippet(content?: string) {
|
||||
return (content || "")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/^\s*\/\s*$/gm, ",")
|
||||
.replace(/\n\s*:\s*\n\s*/g, ": ")
|
||||
.replace(/\n\s*\/\s*\n/g, ",\n")
|
||||
.replace(/,\s*([}\]])/g, "$1")
|
||||
.trimEnd();
|
||||
}
|
||||
|
||||
const CodeSlide03ApiRequestResponse = ({
|
||||
data,
|
||||
}: {
|
||||
data: Partial<SchemaType>;
|
||||
}) => {
|
||||
const requestCode = fitCodeBlock({
|
||||
language: "json",
|
||||
content: normalizeApiJsonSnippet(data.requestSnippet?.content),
|
||||
maxWidth: 540,
|
||||
maxHeight: 230,
|
||||
maxFontSize: 14,
|
||||
minFontSize: 8,
|
||||
});
|
||||
|
||||
const responseCode = fitCodeBlock({
|
||||
language: "json",
|
||||
content: normalizeApiJsonSnippet(data.responseSnippet?.content),
|
||||
maxWidth: 540,
|
||||
maxHeight: 430,
|
||||
maxFontSize: 14,
|
||||
minFontSize: 8,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<style>{PRISM_CODE_BLOCK_STYLES}</style>
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
|
|
@ -83,8 +112,8 @@ const CodeSlide03ApiRequestResponse = ({
|
|||
<div className="relative z-10 flex h-full flex-col">
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[22px] grid flex-1 grid-cols-2 gap-[22px]">
|
||||
<div className="flex flex-col gap-[12px] ">
|
||||
<div className="mt-[22px] grid min-h-0 flex-1 grid-cols-2 gap-[22px]">
|
||||
<div className="flex min-h-0 flex-col gap-[12px]">
|
||||
<div
|
||||
className="rounded-[14px] border p-[14px]"
|
||||
style={{
|
||||
|
|
@ -113,7 +142,7 @@ const CodeSlide03ApiRequestResponse = ({
|
|||
</div>
|
||||
|
||||
<div
|
||||
className=" flex-1 border rounded-[18px]"
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[18px] border"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
|
|
@ -129,17 +158,27 @@ const CodeSlide03ApiRequestResponse = ({
|
|||
>
|
||||
{data.requestSnippet?.fileName}
|
||||
</p>
|
||||
<pre className=" w-full px-[14px] py-[20px] whitespace-pre-wrap break-words overflow-hidden" style={{ color: "var(--background-text,#ffffff)" }}>
|
||||
|
||||
<code className="w-full ">
|
||||
{data.requestSnippet?.content}
|
||||
</code>
|
||||
</pre>
|
||||
<div className="min-h-0 w-full flex-1 overflow-hidden px-[14px] py-[20px]">
|
||||
<pre
|
||||
className="prism-code-block m-0 w-full overflow-hidden"
|
||||
style={{
|
||||
color: "var(--background-text,#ffffff)",
|
||||
fontFamily: requestCode.fontFamily,
|
||||
fontSize: `${requestCode.fontSize}px`,
|
||||
lineHeight: `${requestCode.lineHeight}px`,
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "normal",
|
||||
tabSize: 2,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: requestCode.highlightedHtml }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className=" flex-1 border rounded-[18px]"
|
||||
className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[18px] border"
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#0F172B80)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
|
|
@ -155,12 +194,22 @@ const CodeSlide03ApiRequestResponse = ({
|
|||
>
|
||||
{data.responseSnippet?.fileName}
|
||||
</p>
|
||||
<pre className=" w-full px-[14px] py-[20px] whitespace-pre-wrap break-words overflow-hidden" style={{ color: "var(--background-text,#ffffff)" }}>
|
||||
|
||||
<code className="w-full ">
|
||||
{data.responseSnippet?.content}
|
||||
</code>
|
||||
</pre>
|
||||
<div className="min-h-0 w-full flex-1 overflow-hidden px-[14px] py-[20px]">
|
||||
<pre
|
||||
className="prism-code-block m-0 w-full overflow-hidden"
|
||||
style={{
|
||||
color: "var(--background-text,#ffffff)",
|
||||
fontFamily: responseCode.fontFamily,
|
||||
fontSize: `${responseCode.fontSize}px`,
|
||||
lineHeight: `${responseCode.lineHeight}px`,
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "normal",
|
||||
tabSize: 2,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: responseCode.highlightedHtml }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ const CodeSlide04FeatureGrid = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
url={feature.icon?.__icon_url__}
|
||||
strokeColor={"currentColor"}
|
||||
className="h-[24px] w-[24px] object-contain"
|
||||
color="var(--primary-text, #000000)"
|
||||
color="var(--primary-text, #ffffff)"
|
||||
title={feature.icon.__icon_query__}
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,126 +1,5 @@
|
|||
import * as z from "zod";
|
||||
|
||||
const CODE_BLOCK_MAX_FONT_SIZE = 16;
|
||||
const CODE_BLOCK_MIN_FONT_SIZE = 8;
|
||||
const CODE_BLOCK_WIDTH = 506;
|
||||
const CODE_BLOCK_HEIGHT = 430;
|
||||
const CODE_CHAR_WIDTH_RATIO = 0.62;
|
||||
const CODE_LINE_HEIGHT_RATIO = 1.25;
|
||||
const CODE_FONT_FAMILY = "var(--code-font-family,'Liberation Mono', monospace)";
|
||||
|
||||
function splitCollapsedPythonImports(line: string) {
|
||||
const importSegments = line
|
||||
.split(/(?=\sfrom\s+[A-Za-z0-9_.]+\s+import\s+)/g)
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return importSegments.length > 1 ? importSegments : [line];
|
||||
}
|
||||
|
||||
function expandInlinePythonStatement(line: string) {
|
||||
const inlineReturnMatch = line.match(/^(\s*def\s+[^(]+\([^)]*\):)\s+return\s+(.+)$/);
|
||||
|
||||
if (!inlineReturnMatch) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
return [inlineReturnMatch[1], ` return ${inlineReturnMatch[2]}`];
|
||||
}
|
||||
|
||||
function expandPathListAssignment(line: string) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (!trimmedLine.startsWith("urlpatterns = [") || !trimmedLine.endsWith("]")) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
const pathCalls = trimmedLine.match(/path\([^)]*\)/g);
|
||||
|
||||
if (!pathCalls?.length) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
return [
|
||||
"urlpatterns = [",
|
||||
...pathCalls.map((pathCall) => ` ${pathCall},`),
|
||||
"]",
|
||||
];
|
||||
}
|
||||
|
||||
function normalizePythonCode(content: string) {
|
||||
const normalizedLines: string[] = [];
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
const importLines = splitCollapsedPythonImports(line);
|
||||
|
||||
for (const importLine of importLines) {
|
||||
const expandedPathLines = expandPathListAssignment(importLine);
|
||||
|
||||
for (const expandedPathLine of expandedPathLines) {
|
||||
normalizedLines.push(...expandInlinePythonStatement(expandedPathLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedLines.join("\n").replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
|
||||
function normalizeCodeContent(language?: string, content?: string) {
|
||||
let normalizedContent = (content || "")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/\\\[/g, "[")
|
||||
.replace(/\\\]/g, "]")
|
||||
.trimEnd();
|
||||
|
||||
if (language?.toLowerCase() === "python") {
|
||||
normalizedContent = normalizePythonCode(normalizedContent);
|
||||
}
|
||||
|
||||
return normalizedContent;
|
||||
}
|
||||
|
||||
function getCodeBlockTypography(content?: string) {
|
||||
const normalizedLines = (content || "").replace(/\t/g, " ").split("\n");
|
||||
const longestLineLength = Math.max(
|
||||
1,
|
||||
...normalizedLines.map((line) => line.length)
|
||||
);
|
||||
|
||||
for (let fontSize = CODE_BLOCK_MAX_FONT_SIZE; fontSize >= CODE_BLOCK_MIN_FONT_SIZE; fontSize -= 0.5) {
|
||||
const lineHeight = Math.round(fontSize * CODE_LINE_HEIGHT_RATIO);
|
||||
const fitsWidth = longestLineLength * fontSize * CODE_CHAR_WIDTH_RATIO <= CODE_BLOCK_WIDTH;
|
||||
const fitsHeight = normalizedLines.length * lineHeight <= CODE_BLOCK_HEIGHT;
|
||||
|
||||
if (fitsWidth && fitsHeight) {
|
||||
return { fontSize, lineHeight };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fontSize: CODE_BLOCK_MIN_FONT_SIZE,
|
||||
lineHeight: Math.round(CODE_BLOCK_MIN_FONT_SIZE * CODE_LINE_HEIGHT_RATIO),
|
||||
};
|
||||
}
|
||||
|
||||
function getCodeLineRuns(content: string, lineHeight: number) {
|
||||
const codeLineRuns: { text: string; marginTop: number }[] = [];
|
||||
let blankLineCount = 0;
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
if (line.length === 0) {
|
||||
blankLineCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
codeLineRuns.push({
|
||||
text: line,
|
||||
marginTop: blankLineCount * lineHeight,
|
||||
});
|
||||
blankLineCount = 0;
|
||||
}
|
||||
|
||||
return codeLineRuns;
|
||||
}
|
||||
import { fitCodeBlock, PRISM_CODE_BLOCK_STYLES } from "./codeBlockFitting";
|
||||
|
||||
export const slideLayoutId = "code-explanation-split-slide";
|
||||
export const slideLayoutName = "Code Explanation Split Slide";
|
||||
|
|
@ -186,16 +65,19 @@ const CodeSlide02CodeExplanationSplit = ({
|
|||
}: {
|
||||
data: Partial<SchemaType>;
|
||||
}) => {
|
||||
const normalizedCodeContent = normalizeCodeContent(
|
||||
data.codeSnippet?.language,
|
||||
data.codeSnippet?.content
|
||||
);
|
||||
const codeTypography = getCodeBlockTypography(normalizedCodeContent);
|
||||
const codeLineRuns = getCodeLineRuns(normalizedCodeContent, codeTypography.lineHeight);
|
||||
const fittedCode = fitCodeBlock({
|
||||
language: data.codeSnippet?.language,
|
||||
content: data.codeSnippet?.content,
|
||||
maxWidth: 506,
|
||||
maxHeight: 430,
|
||||
maxFontSize: 16,
|
||||
minFontSize: 8,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet" />
|
||||
<style>{PRISM_CODE_BLOCK_STYLES}</style>
|
||||
<div
|
||||
className="relative h-[720px] w-[1280px] overflow-hidden p-[53px]"
|
||||
style={{
|
||||
|
|
@ -229,22 +111,21 @@ const CodeSlide02CodeExplanationSplit = ({
|
|||
className="min-h-0 w-full flex-1 overflow-hidden px-[32px] py-[20px]"
|
||||
style={{
|
||||
color: "var(--background-text,#ffffff)",
|
||||
fontFamily: CODE_FONT_FAMILY,
|
||||
}}
|
||||
>
|
||||
{codeLineRuns.map((codeLineRun, index) => (
|
||||
<div
|
||||
key={`code-line-${index}`}
|
||||
style={{
|
||||
marginTop: codeLineRun.marginTop ? `${codeLineRun.marginTop}px` : undefined,
|
||||
fontSize: `${codeTypography.fontSize}px`,
|
||||
lineHeight: `${codeTypography.lineHeight}px`,
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
{codeLineRun.text}
|
||||
</div>
|
||||
))}
|
||||
<pre
|
||||
className="prism-code-block m-0 w-full overflow-hidden"
|
||||
style={{
|
||||
fontFamily: fittedCode.fontFamily,
|
||||
fontSize: `${fittedCode.fontSize}px`,
|
||||
lineHeight: `${fittedCode.lineHeight}px`,
|
||||
whiteSpace: "pre-wrap",
|
||||
overflowWrap: "break-word",
|
||||
wordBreak: "normal",
|
||||
tabSize: 2,
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: fittedCode.highlightedHtml }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -91,11 +91,16 @@ const CodeSlide06Workflow = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
|
||||
<h2 className="text-[64px] font-medium" style={{ color: "var(--background-text,#ffffff)" }}>{data.title}</h2>
|
||||
|
||||
<div className="mt-[52px] grid flex-1 grid-cols-[1fr_auto_1fr_auto_1fr_auto_1fr] items-center gap-[12px]">
|
||||
<div className="mt-[52px] grid flex-1 justify-center items-center gap-[12px]"
|
||||
|
||||
style={{
|
||||
gridTemplateColumns: data.steps?.length === 1 ? '1fr' : data?.steps?.length === 2 ? '1fr auto 1fr' : data?.steps?.length === 3 ? '1fr auto 1fr auto 1fr' : data?.steps?.length === 4 ? '1fr auto 1fr auto 1fr auto 1fr' : '1fr auto 1fr auto 1fr auto 1fr auto 1fr',
|
||||
}}
|
||||
>
|
||||
{data?.steps?.map((step, index) => (
|
||||
<Fragment key={`${step.title}-${index}`}>
|
||||
<div
|
||||
className="rounded-[18px] border p-[21px] text-center"
|
||||
className="rounded-[18px] border max-w-[500px] mx-auto p-[21px] text-center"
|
||||
style={{
|
||||
boxShadow: "0 33.333px 66.667px -16px rgba(0, 0, 0, 0.25)",
|
||||
borderColor: "var(--stroke,#1D293D80)",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,659 @@
|
|||
import { jsonrepair } from "jsonrepair";
|
||||
import Prism from "prismjs";
|
||||
import "prismjs/components/prism-json";
|
||||
import "prismjs/components/prism-javascript";
|
||||
import "prismjs/components/prism-typescript";
|
||||
import "prismjs/components/prism-jsx";
|
||||
import "prismjs/components/prism-tsx";
|
||||
import "prismjs/components/prism-python";
|
||||
import "prismjs/components/prism-bash";
|
||||
import "prismjs/components/prism-yaml";
|
||||
import "prismjs/components/prism-markdown";
|
||||
|
||||
const DEFAULT_CODE_CHAR_WIDTH_RATIO = 0.62;
|
||||
const DEFAULT_CODE_LINE_HEIGHT_RATIO = 1.25;
|
||||
const DEFAULT_FONT_STEP = 0.5;
|
||||
const HARD_MIN_FONT_SIZE = 4;
|
||||
|
||||
export const DEFAULT_CODE_FONT_FAMILY = "var(--code-font-family,'Liberation Mono', monospace)";
|
||||
export const PRISM_CODE_BLOCK_STYLES = `
|
||||
.prism-code-block .token {
|
||||
display: inline !important;
|
||||
white-space: inherit !important;
|
||||
}
|
||||
|
||||
.prism-code-block .token.comment,
|
||||
.prism-code-block .token.prolog,
|
||||
.prism-code-block .token.doctype,
|
||||
.prism-code-block .token.cdata {
|
||||
color: #7b8ebf;
|
||||
}
|
||||
|
||||
.prism-code-block .token.punctuation {
|
||||
color: #a8b7e0;
|
||||
}
|
||||
|
||||
.prism-code-block .token.property,
|
||||
.prism-code-block .token.tag,
|
||||
.prism-code-block .token.constant,
|
||||
.prism-code-block .token.symbol,
|
||||
.prism-code-block .token.deleted {
|
||||
color: #7bc4ff;
|
||||
}
|
||||
|
||||
.prism-code-block .token.boolean,
|
||||
.prism-code-block .token.number {
|
||||
color: #f5c97b;
|
||||
}
|
||||
|
||||
.prism-code-block .token.selector,
|
||||
.prism-code-block .token.attr-name,
|
||||
.prism-code-block .token.string,
|
||||
.prism-code-block .token.char,
|
||||
.prism-code-block .token.builtin,
|
||||
.prism-code-block .token.inserted {
|
||||
color: #9fe6b8;
|
||||
}
|
||||
|
||||
.prism-code-block .token.operator,
|
||||
.prism-code-block .token.entity,
|
||||
.prism-code-block .token.url,
|
||||
.prism-code-block .token.variable {
|
||||
color: #f5a97f;
|
||||
}
|
||||
|
||||
.prism-code-block .token.atrule,
|
||||
.prism-code-block .token.attr-value,
|
||||
.prism-code-block .token.function,
|
||||
.prism-code-block .token.class-name {
|
||||
color: #b8a8ff;
|
||||
}
|
||||
|
||||
.prism-code-block .token.keyword {
|
||||
color: #7aa2ff;
|
||||
}
|
||||
|
||||
.prism-code-block .token.regex,
|
||||
.prism-code-block .token.important {
|
||||
color: #f9e2af;
|
||||
}
|
||||
`;
|
||||
|
||||
interface FitCodeBlockOptions {
|
||||
language?: string;
|
||||
content?: string;
|
||||
maxWidth: number;
|
||||
maxHeight: number;
|
||||
maxFontSize?: number;
|
||||
minFontSize?: number;
|
||||
fontStep?: number;
|
||||
charWidthRatio?: number;
|
||||
lineHeightRatio?: number;
|
||||
}
|
||||
|
||||
interface TypographyCandidate {
|
||||
lineHeight: number;
|
||||
maxCharsPerLine: number;
|
||||
renderedLineCount: number;
|
||||
}
|
||||
|
||||
export interface FittedCodeBlock {
|
||||
text: string;
|
||||
highlightedHtml: string;
|
||||
prismLanguage: string;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
fontFamily: string;
|
||||
}
|
||||
|
||||
function splitCollapsedPythonImports(line: string) {
|
||||
const importSegments = line
|
||||
.split(/(?=\sfrom\s+[A-Za-z0-9_.]+\s+import\s+)/g)
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
return importSegments.length > 1 ? importSegments : [line];
|
||||
}
|
||||
|
||||
function expandInlinePythonStatement(line: string) {
|
||||
const inlineReturnMatch = line.match(/^(\s*def\s+[^(]+\([^)]*\):)\s+return\s+(.+)$/);
|
||||
|
||||
if (!inlineReturnMatch) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
return [inlineReturnMatch[1], `return ${inlineReturnMatch[2]}`];
|
||||
}
|
||||
|
||||
function expandPathListAssignment(line: string) {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
if (!trimmedLine.startsWith("urlpatterns = [") || !trimmedLine.endsWith("]")) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
const pathCalls = trimmedLine.match(/path\([^)]*\)/g);
|
||||
|
||||
if (!pathCalls?.length) {
|
||||
return [line];
|
||||
}
|
||||
|
||||
return [
|
||||
"urlpatterns = [",
|
||||
...pathCalls.map((pathCall) => ` ${pathCall},`),
|
||||
"]",
|
||||
];
|
||||
}
|
||||
|
||||
function normalizePythonCode(content: string) {
|
||||
const normalizedLines: string[] = [];
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
const importLines = splitCollapsedPythonImports(line);
|
||||
|
||||
for (const importLine of importLines) {
|
||||
const expandedPathLines = expandPathListAssignment(importLine);
|
||||
|
||||
for (const expandedPathLine of expandedPathLines) {
|
||||
normalizedLines.push(...expandInlinePythonStatement(expandedPathLine));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedLines.join("\n").replace(/\n{3,}/g, "\n\n");
|
||||
}
|
||||
|
||||
function tryFormatJson(content: string) {
|
||||
const trimmedContent = content.replace(/^\uFEFF/, "").trim();
|
||||
|
||||
if (!trimmedContent) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const normalizedSeparatorsContent = trimmedContent
|
||||
.replace(/^\s*\/\s*$/gm, ",")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/\n\s*:\s*/g, ": ")
|
||||
.replace(/\n\s*,\s*/g, ", ");
|
||||
|
||||
const parseAndFormat = (raw: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (typeof parsed === "string") {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(parsed), null, 2);
|
||||
} catch {
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const extractedJsonMatch = normalizedSeparatorsContent.match(/[\[{][\s\S]*[\]}]/);
|
||||
const extractedJsonCandidate = extractedJsonMatch?.[0];
|
||||
|
||||
const candidates = [
|
||||
normalizedSeparatorsContent,
|
||||
trimmedContent,
|
||||
extractedJsonCandidate,
|
||||
].filter((candidate): candidate is string => Boolean(candidate));
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const direct = parseAndFormat(candidate);
|
||||
if (direct !== null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
try {
|
||||
const repairedJson = jsonrepair(candidate);
|
||||
const repaired = parseAndFormat(repairedJson);
|
||||
if (repaired !== null) {
|
||||
return repaired;
|
||||
}
|
||||
} catch {
|
||||
// Try next parsing strategy.
|
||||
}
|
||||
}
|
||||
|
||||
const jsonLikeTokenMatch = normalizedSeparatorsContent.match(
|
||||
/"(?:\\.|[^"\\])*"|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\]:,\/]/g
|
||||
);
|
||||
|
||||
if (jsonLikeTokenMatch?.length) {
|
||||
const normalizedTokens = jsonLikeTokenMatch.map((token) => (token === "/" ? "," : token));
|
||||
let rebuilt = "";
|
||||
|
||||
for (const token of normalizedTokens) {
|
||||
if (token === ":" || token === ",") {
|
||||
rebuilt = rebuilt.replace(/\s*$/, "");
|
||||
rebuilt += `${token} `;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "}" || token === "]") {
|
||||
rebuilt = rebuilt.replace(/\s*$/, "");
|
||||
rebuilt += token;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "{" || token === "[") {
|
||||
rebuilt = rebuilt.replace(/\s*$/, "");
|
||||
rebuilt += token;
|
||||
continue;
|
||||
}
|
||||
|
||||
rebuilt += token;
|
||||
}
|
||||
|
||||
const rebuiltDirect = parseAndFormat(rebuilt);
|
||||
if (rebuiltDirect !== null) {
|
||||
return rebuiltDirect;
|
||||
}
|
||||
|
||||
try {
|
||||
const rebuiltRepaired = parseAndFormat(jsonrepair(rebuilt));
|
||||
if (rebuiltRepaired !== null) {
|
||||
return rebuiltRepaired;
|
||||
}
|
||||
} catch {
|
||||
// Continue to best-effort line merge fallback below.
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedLines = normalizedSeparatorsContent
|
||||
.replace(/\n\s*:\s*\n\s*/g, ": ")
|
||||
.replace(/\n\s*\/\s*\n/g, ",\n")
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/\s+$/g, ""));
|
||||
|
||||
const mergedLines: string[] = [];
|
||||
|
||||
for (const rawLine of normalizedLines) {
|
||||
const trimmedLine = rawLine.trim();
|
||||
|
||||
if (!trimmedLine) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedLine === ":") {
|
||||
if (mergedLines.length > 0) {
|
||||
mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]}:`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmedLine === "/") {
|
||||
if (mergedLines.length > 0 && !mergedLines[mergedLines.length - 1].trim().endsWith(",")) {
|
||||
mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]},`;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousLine = mergedLines[mergedLines.length - 1]?.trim() || "";
|
||||
if (previousLine.endsWith(":")) {
|
||||
mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]} ${trimmedLine}`;
|
||||
continue;
|
||||
}
|
||||
|
||||
mergedLines.push(rawLine);
|
||||
}
|
||||
|
||||
for (let index = 0; index < mergedLines.length - 1; index += 1) {
|
||||
const currentLine = mergedLines[index].trim();
|
||||
const nextLine = mergedLines[index + 1].trim();
|
||||
const currentEndsWithComma = currentLine.endsWith(",");
|
||||
const currentIsContainerStart = currentLine.endsWith("{") || currentLine.endsWith("[");
|
||||
const nextStartsNewKey = nextLine.startsWith("\"");
|
||||
const nextIsContainerEnd = nextLine.startsWith("}") || nextLine.startsWith("]");
|
||||
|
||||
if (!currentEndsWithComma && !currentIsContainerStart && nextStartsNewKey && !nextIsContainerEnd) {
|
||||
mergedLines[index] = `${mergedLines[index]},`;
|
||||
}
|
||||
}
|
||||
|
||||
return mergedLines
|
||||
.join("\n")
|
||||
.replace(/,\s*([}\]])/g, "$1");
|
||||
}
|
||||
|
||||
function isValidJsonContent(content: string) {
|
||||
try {
|
||||
JSON.parse(content.trim());
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function seemsJsonLike(content: string) {
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (/^[\[{]/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /"[^"\n]+"\s*:/.test(trimmed) || /[\[{][\s\S]*[\]}]/.test(trimmed);
|
||||
}
|
||||
|
||||
function unwrapMarkdownCodeFence(content: string) {
|
||||
const trimmedContent = content.trim();
|
||||
const fencedCodeMatch = trimmedContent.match(/^```([^\n`]*)\n([\s\S]*?)\n```$/);
|
||||
|
||||
if (!fencedCodeMatch) {
|
||||
return {
|
||||
content: content,
|
||||
fenceLanguage: undefined as string | undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: fencedCodeMatch[2],
|
||||
fenceLanguage: fencedCodeMatch[1]?.trim().toLowerCase() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(content: string) {
|
||||
return content
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function resolvePrismLanguage(language?: string) {
|
||||
const normalizedLanguage = language?.toLowerCase().trim();
|
||||
|
||||
if (!normalizedLanguage) {
|
||||
return "clike";
|
||||
}
|
||||
|
||||
if (normalizedLanguage.includes("json")) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
if (normalizedLanguage.includes("python")) {
|
||||
return "python";
|
||||
}
|
||||
|
||||
if (normalizedLanguage === "ts") {
|
||||
return "typescript";
|
||||
}
|
||||
|
||||
if (normalizedLanguage === "js") {
|
||||
return "javascript";
|
||||
}
|
||||
|
||||
if (normalizedLanguage === "py") {
|
||||
return "python";
|
||||
}
|
||||
|
||||
if (normalizedLanguage === "sh" || normalizedLanguage === "shell") {
|
||||
return "bash";
|
||||
}
|
||||
|
||||
if (normalizedLanguage === "yml") {
|
||||
return "yaml";
|
||||
}
|
||||
|
||||
if (Prism.languages[normalizedLanguage]) {
|
||||
return normalizedLanguage;
|
||||
}
|
||||
|
||||
return "clike";
|
||||
}
|
||||
|
||||
function highlightCode(content: string, language?: string) {
|
||||
const prismLanguage = resolvePrismLanguage(language);
|
||||
const grammar = Prism.languages[prismLanguage];
|
||||
|
||||
if (!grammar) {
|
||||
return {
|
||||
html: escapeHtml(content),
|
||||
prismLanguage,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
html: Prism.highlight(content, grammar, prismLanguage),
|
||||
prismLanguage,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
html: escapeHtml(content),
|
||||
prismLanguage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCodeContent(language?: string, content?: string) {
|
||||
let normalizedContent = (content || "")
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/\\\[/g, "[")
|
||||
.replace(/\\\]/g, "]");
|
||||
const unwrappedContent = unwrapMarkdownCodeFence(normalizedContent);
|
||||
normalizedContent = unwrappedContent.content.trimEnd();
|
||||
|
||||
const normalizedLanguage = language?.toLowerCase()?.trim() || unwrappedContent.fenceLanguage;
|
||||
const isJsonLanguage = normalizedLanguage?.includes("json");
|
||||
const looksLikeJsonPayload = seemsJsonLike(normalizedContent);
|
||||
|
||||
if (normalizedLanguage === "python") {
|
||||
normalizedContent = normalizePythonCode(normalizedContent);
|
||||
} else if (isJsonLanguage || looksLikeJsonPayload) {
|
||||
const formattedJson = tryFormatJson(normalizedContent);
|
||||
normalizedContent = formattedJson;
|
||||
}
|
||||
|
||||
return normalizedContent;
|
||||
}
|
||||
|
||||
function countRenderedLines(content: string, maxCharsPerLine: number) {
|
||||
const rawLines = content.split("\n");
|
||||
let renderedLineCount = 0;
|
||||
|
||||
for (const rawLine of rawLines) {
|
||||
const expandedLine = rawLine.replace(/\t/g, " ");
|
||||
|
||||
if (expandedLine.length === 0) {
|
||||
renderedLineCount += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
renderedLineCount += Math.max(1, Math.ceil(expandedLine.length / maxCharsPerLine));
|
||||
}
|
||||
|
||||
return Math.max(1, renderedLineCount);
|
||||
}
|
||||
|
||||
function splitLineForLineBudget(line: string, maxCharsPerLine: number) {
|
||||
if (line.length === 0) {
|
||||
return [""];
|
||||
}
|
||||
|
||||
const chunks: string[] = [];
|
||||
|
||||
for (let start = 0; start < line.length; start += maxCharsPerLine) {
|
||||
chunks.push(line.slice(start, start + maxCharsPerLine));
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function truncateContentToLineBudget(
|
||||
content: string,
|
||||
lineBudget: number,
|
||||
maxCharsPerLine: number
|
||||
) {
|
||||
const linesForDisplay: string[] = [];
|
||||
const rawLines = content.split("\n");
|
||||
|
||||
for (const rawLine of rawLines) {
|
||||
const expandedLine = rawLine.replace(/\t/g, " ");
|
||||
const chunks = splitLineForLineBudget(expandedLine, maxCharsPerLine);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
if (linesForDisplay.length >= lineBudget) {
|
||||
const lastLineIndex = Math.max(0, lineBudget - 1);
|
||||
const ellipsis = "...";
|
||||
const existingLastLine = linesForDisplay[lastLineIndex] ?? "";
|
||||
linesForDisplay[lastLineIndex] = `${existingLastLine.slice(
|
||||
0,
|
||||
Math.max(0, maxCharsPerLine - ellipsis.length)
|
||||
)}${ellipsis}`;
|
||||
return linesForDisplay.join("\n");
|
||||
}
|
||||
|
||||
linesForDisplay.push(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
return linesForDisplay.join("\n");
|
||||
}
|
||||
|
||||
function createTypographyCandidate(
|
||||
normalizedContent: string,
|
||||
fontSize: number,
|
||||
maxWidth: number,
|
||||
charWidthRatio: number,
|
||||
lineHeightRatio: number
|
||||
): TypographyCandidate {
|
||||
const lineHeight = Math.max(1, Math.round(fontSize * lineHeightRatio));
|
||||
const maxCharsPerLine = Math.max(1, Math.floor(maxWidth / (fontSize * charWidthRatio)));
|
||||
const renderedLineCount = countRenderedLines(normalizedContent, maxCharsPerLine);
|
||||
|
||||
return {
|
||||
lineHeight,
|
||||
maxCharsPerLine,
|
||||
renderedLineCount,
|
||||
};
|
||||
}
|
||||
|
||||
function findFittingTypography(
|
||||
normalizedContent: string,
|
||||
startFontSize: number,
|
||||
minFontSize: number,
|
||||
maxWidth: number,
|
||||
maxHeight: number,
|
||||
fontStep: number,
|
||||
charWidthRatio: number,
|
||||
lineHeightRatio: number
|
||||
) {
|
||||
for (let fontSize = startFontSize; fontSize >= minFontSize; fontSize -= fontStep) {
|
||||
const candidate = createTypographyCandidate(
|
||||
normalizedContent,
|
||||
fontSize,
|
||||
maxWidth,
|
||||
charWidthRatio,
|
||||
lineHeightRatio
|
||||
);
|
||||
|
||||
if (candidate.renderedLineCount * candidate.lineHeight <= maxHeight) {
|
||||
return {
|
||||
candidate,
|
||||
fontSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function fitCodeBlock({
|
||||
language,
|
||||
content,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
maxFontSize = 16,
|
||||
minFontSize = 8,
|
||||
fontStep = DEFAULT_FONT_STEP,
|
||||
charWidthRatio = DEFAULT_CODE_CHAR_WIDTH_RATIO,
|
||||
lineHeightRatio = DEFAULT_CODE_LINE_HEIGHT_RATIO,
|
||||
}: FitCodeBlockOptions): FittedCodeBlock {
|
||||
const normalizedContent = normalizeCodeContent(language, content);
|
||||
const highlightLanguage =
|
||||
isValidJsonContent(normalizedContent) || seemsJsonLike(normalizedContent)
|
||||
? "json"
|
||||
: language;
|
||||
const preferredMinFont = Math.max(1, minFontSize);
|
||||
const hardMinFont = Math.max(1, Math.min(preferredMinFont, HARD_MIN_FONT_SIZE));
|
||||
const startFont = Math.max(maxFontSize, preferredMinFont);
|
||||
|
||||
const preferredFit = findFittingTypography(
|
||||
normalizedContent,
|
||||
startFont,
|
||||
preferredMinFont,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
fontStep,
|
||||
charWidthRatio,
|
||||
lineHeightRatio
|
||||
);
|
||||
|
||||
if (preferredFit) {
|
||||
const highlighted = highlightCode(normalizedContent, highlightLanguage);
|
||||
return {
|
||||
text: normalizedContent,
|
||||
highlightedHtml: highlighted.html,
|
||||
prismLanguage: highlighted.prismLanguage,
|
||||
fontSize: Math.round(preferredFit.fontSize * 10) / 10,
|
||||
lineHeight: preferredFit.candidate.lineHeight,
|
||||
fontFamily: DEFAULT_CODE_FONT_FAMILY,
|
||||
};
|
||||
}
|
||||
|
||||
if (hardMinFont < preferredMinFont) {
|
||||
const emergencyFit = findFittingTypography(
|
||||
normalizedContent,
|
||||
preferredMinFont - fontStep,
|
||||
hardMinFont,
|
||||
maxWidth,
|
||||
maxHeight,
|
||||
fontStep,
|
||||
charWidthRatio,
|
||||
lineHeightRatio
|
||||
);
|
||||
|
||||
if (emergencyFit) {
|
||||
const highlighted = highlightCode(normalizedContent, highlightLanguage);
|
||||
return {
|
||||
text: normalizedContent,
|
||||
highlightedHtml: highlighted.html,
|
||||
prismLanguage: highlighted.prismLanguage,
|
||||
fontSize: Math.round(emergencyFit.fontSize * 10) / 10,
|
||||
lineHeight: emergencyFit.candidate.lineHeight,
|
||||
fontFamily: DEFAULT_CODE_FONT_FAMILY,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = createTypographyCandidate(
|
||||
normalizedContent,
|
||||
hardMinFont,
|
||||
maxWidth,
|
||||
charWidthRatio,
|
||||
lineHeightRatio
|
||||
);
|
||||
const fallbackLineBudget = Math.max(1, Math.floor(maxHeight / fallback.lineHeight));
|
||||
const fallbackText = truncateContentToLineBudget(
|
||||
normalizedContent,
|
||||
fallbackLineBudget,
|
||||
fallback.maxCharsPerLine
|
||||
);
|
||||
const highlighted = highlightCode(fallbackText, highlightLanguage);
|
||||
return {
|
||||
text: fallbackText,
|
||||
highlightedHtml: highlighted.html,
|
||||
prismLanguage: highlighted.prismLanguage,
|
||||
fontSize: Math.round(hardMinFont * 10) / 10,
|
||||
lineHeight: fallback.lineHeight,
|
||||
fontFamily: DEFAULT_CODE_FONT_FAMILY,
|
||||
};
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ const StatisticSchema = z.object({
|
|||
value: z.string().max(8).meta({
|
||||
description: "Main metric value shown at the top of one card.",
|
||||
}),
|
||||
label: z.string().max(20).meta({
|
||||
label: z.string().max(45).meta({
|
||||
description: "Label shown under the value.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -28,14 +28,14 @@ export const Schema = z.object({
|
|||
.min(2)
|
||||
.max(8)
|
||||
.default([
|
||||
{ value: "120", label: "Sales Team Strength" },
|
||||
{ value: "15", label: "Senior Sales Officer" },
|
||||
{ value: "1", label: "National Manager" },
|
||||
{ value: "25", label: "Sales Officers" },
|
||||
{ value: "2", label: "Regional Manager" },
|
||||
{ value: "50", label: "Distributor Reps" },
|
||||
{ value: "5", label: "Zonal Manager" },
|
||||
{ value: "20", label: "Merchandising Team" },
|
||||
{ value: "120", label: "Sales Team Strength with a long label to test the layouts" },
|
||||
{ value: "15", label: "Senior Sales Officer with a long label to test the layout" },
|
||||
{ value: "1", label: "National Manager with a long label to test the layout" },
|
||||
{ value: "25", label: "Sales Officers with a long label to test the layout" },
|
||||
{ value: "2", label: "Regional Manager with a long label to test the layout" },
|
||||
{ value: "50", label: "Distributor Reps with a long label to test the layout" },
|
||||
{ value: "5", label: "Zonal Manager with a long label to test the layout" },
|
||||
{ value: "20", label: "Merchandising Team with a long label to the layout" },
|
||||
])
|
||||
.meta({
|
||||
description: "statistic cards, with value and label each in a card",
|
||||
|
|
@ -93,29 +93,35 @@ const EducationStatisticsGridSlide = ({ data }: { data: Partial<SchemaType> }) =
|
|||
|
||||
|
||||
{data.stats && data.stats?.length > 4 && data.stats?.length <= 8 && (() => {
|
||||
const rightArray = data.stats?.slice(0, Math.floor(data.stats?.length / 2));
|
||||
const leftArray = data.stats?.slice(Math.floor(data.stats?.length / 2));
|
||||
// const rightArray = data.stats?.slice(0, Math.floor(data.stats?.length / 2));
|
||||
// const leftArray = data.stats?.slice(Math.floor(data.stats?.length / 2));
|
||||
|
||||
return (
|
||||
<div className="h-full flex w-full">
|
||||
<div className="flex flex-col h-full flex-1">
|
||||
<div className="h-full grid grid-cols-2 w-full">
|
||||
{/* <div className="flex flex-col h-full flex-1"> */}
|
||||
|
||||
{leftArray?.map((stat: any, index: number) => (
|
||||
<div
|
||||
key={`${stat?.value}-${index}`}
|
||||
className="px-[52px] pt-[22px] h-full"
|
||||
style={{ backgroundColor: index % 2 === 0 ? 'var(--card-color,#5C0FD908)' : 'var(--card-color,white)' }}
|
||||
>
|
||||
<p className=" text-[58px] leading-[56px]" style={{ color: "var(--background-text,#283E51)" }}>
|
||||
{stat?.value}
|
||||
</p>
|
||||
<p className="mt-[12px] text-[24px]" style={{ color: "var(--background-text,#434A63)" }}>
|
||||
{stat?.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col flex-1">
|
||||
{data.stats?.map((stat: any, index: number) => (
|
||||
<div
|
||||
key={`${stat?.value}-${index}`}
|
||||
className="px-[52px] pt-[22px] h-full"
|
||||
style={{
|
||||
backgroundColor: [0, 3, 4, 7].includes(index)
|
||||
? 'var(--card-color,#5C0FD908)'
|
||||
: 'var(--card-color,white)'
|
||||
}}
|
||||
>
|
||||
<p className=" text-[58px] leading-[56px]" style={{ color: "var(--background-text,#283E51)" }}>
|
||||
{stat?.value}
|
||||
</p>
|
||||
<p className="mt-[12px] text-[24px]" style={{ color: "var(--background-text,#434A63)" }}>
|
||||
{stat?.label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* </div> */}
|
||||
|
||||
{/* <div className="flex flex-col flex-1">
|
||||
|
||||
{rightArray?.map((stat: any, index: number) => (
|
||||
<div
|
||||
|
|
@ -131,7 +137,7 @@ const EducationStatisticsGridSlide = ({ data }: { data: Partial<SchemaType> }) =
|
|||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const RowSchema = z.union([GeneralRowSchema, LegacyRowSchema]);
|
|||
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().max(14).default("Comparison Chart").meta({
|
||||
title: z.string().max(24).default("Comparison Chart Comparison").meta({
|
||||
description: "Main heading shown above the table.",
|
||||
}),
|
||||
subtitle: z.string().max(80).default(
|
||||
|
|
|
|||
|
|
@ -8,15 +8,7 @@ export const slideLayoutDescription =
|
|||
"A cover slide with a compact logo in the top-left, a date/text/label in the top-right, a centered title, and a image anchored to the bottom with a soft fade into the background.";
|
||||
|
||||
export const Schema = z.object({
|
||||
image: z.object({
|
||||
__image_url__: z.string().default("https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg"),
|
||||
|
||||
__image_prompt__: z.string().default("Image of the company"),
|
||||
}).optional().default({
|
||||
__image_url__:
|
||||
"https://presenton-public.s3.ap-southeast-1.amazonaws.com/static/images/placeholder.jpg",
|
||||
__image_prompt__: "Image of the company",
|
||||
}),
|
||||
label: z.string().min(3).max(16).optional().default("MARCH 2026").meta({
|
||||
description: "Date/text/label shown at the top-right corner.",
|
||||
}),
|
||||
|
|
@ -56,11 +48,7 @@ const CoverSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
{data.image?.__image_url__ ? <img
|
||||
src={data.image?.__image_url__ ?? ''}
|
||||
alt={data.image?.__image_prompt__ || ''}
|
||||
className="h-[42px] w-[171px] object-cover"
|
||||
/> : <p></p>}
|
||||
<p></p>
|
||||
|
||||
<p
|
||||
className="text-[18px] font-normal leading-[18.991px] text-[#15342D]"
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const IntroBlockSchema = z.object({
|
|||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(4).max(16).default("Introduction").meta({
|
||||
title: z.string().max(15).default("Introduction").meta({
|
||||
description: "Primary title in the right column.",
|
||||
}),
|
||||
portraitImage: z.object({
|
||||
|
|
|
|||
|
|
@ -81,13 +81,13 @@ const MetricCardSchema = z.object({
|
|||
});
|
||||
|
||||
export const Schema = z.object({
|
||||
title: z.string().min(4).max(12).default("Report").meta({
|
||||
title: z.string().max(24).default("Report Report Report Report").meta({
|
||||
description: "Slide heading text.",
|
||||
}),
|
||||
taglineLabel: z.string().min(3).max(10).default("TAGLINE").meta({
|
||||
taglineLabel: z.string().max(24).default("TAGLINE").meta({
|
||||
description: "Small label above intro paragraph.",
|
||||
}),
|
||||
taglineBody: z.string().min(40).max(120).default(
|
||||
taglineBody: z.string().max(120).default(
|
||||
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
).meta({
|
||||
description: "Intro paragraph shown beneath the heading.",
|
||||
|
|
@ -361,7 +361,7 @@ const ReportSnapshotSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
{title}
|
||||
</h2>
|
||||
|
||||
<div className="mt-[14px] w-[560px]">
|
||||
<div className="mt-[14px] ">
|
||||
<p
|
||||
className="text-[20px] font-semibold tracking-[2.074px] text-[#083F37]"
|
||||
style={{ color: "var(--primary-color,#083F37)" }}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import * as z from "zod";
|
|||
|
||||
|
||||
const AnalysisItemSchema = z.object({
|
||||
title: z.string().max(12).meta({
|
||||
title: z.string().max(30).meta({
|
||||
description: "Short item title displayed next to the icon.",
|
||||
}),
|
||||
description: z.string().max(30).meta({
|
||||
description: z.string().max(60).meta({
|
||||
description: "Supporting sentence shown below the title.",
|
||||
}),
|
||||
});
|
||||
|
|
@ -35,12 +35,12 @@ export const Schema = z.object({
|
|||
|
||||
.max(6)
|
||||
.default([
|
||||
{ title: "Title 1", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 3", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 2", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 4", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 2", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 5", description: "Ut enim ad minima veniam, quis." },
|
||||
{ title: "Title 1 title 1 title 1 title 1 title 1", description: "Ut enim ad minima veniam, quis. Ut enim ad minima veniam, quis. Ut enim" },
|
||||
{ title: "Title 3 title 3 title 3 title 3 title 3", description: "Ut enim ad minima veniam, quis. Ut enim ad minima veniam, quis. Ut enim " },
|
||||
{ title: "Title 2 title 2 title 2 title 2 title 2", description: "Ut enim ad minima veniam, quis. Ut enim ad minima veniam, quis. Ut enim " },
|
||||
{ title: "Title 4 title 4 title 4 title 4 title 4", description: "Ut enim ad minima veniam, quis. Ut enim ad minima veniam, quis. Ut enim " },
|
||||
{ title: "Title 2 title 2 title 2 title 2 title 2", description: "Ut enim ad minima veniam, quis. Ut enim ad minima veniam, quis. Ut enim " },
|
||||
{ title: "Title 5 title 5 title 5 title 5 title 5", description: "Ut enim ad minima veniam, quis. Ut enim ad minima veniam, quis. Ut enim " },
|
||||
])
|
||||
.meta({
|
||||
description: "List of points contains a title and description.",
|
||||
|
|
@ -76,7 +76,7 @@ const DataAnalysisListSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-[112px] gap-y-[52px] px-[82px] pt-[58px]">
|
||||
<div className="grid grid-cols-2 gap-x-[92px] gap-y-[42px] px-[82px] pt-[58px]">
|
||||
{items?.map((item, index) => (
|
||||
<div key={`${item.title}-${index}`}>
|
||||
<div className="flex items-center gap-[14px]">
|
||||
|
|
|
|||
|
|
@ -136,9 +136,9 @@ function SummaryCard({
|
|||
return (
|
||||
<div className="flex gap-[10px] items-center rounded-[14px] py-[9px]">
|
||||
<div
|
||||
className="flex h-[36px] w-[36px] border border-[#ECF5FE] shrink-0 items-center justify-center rounded-full bg-[#ECF5FE] "
|
||||
className="flex h-[36px] w-[36px] items-center justify-center border border-[#ECF5FE] shrink-0 rounded-full bg-[#ECF5FE] "
|
||||
style={{
|
||||
backgroundColor: "var(--card-color,#ECF5FE)",
|
||||
backgroundColor: "var(--primary-color,#ECF5FE)",
|
||||
borderColor: "var(--stroke,#ECF5FE)",
|
||||
}}
|
||||
>
|
||||
|
|
@ -149,12 +149,7 @@ function SummaryCard({
|
|||
color="var(--primary-text, #000000)"
|
||||
title={iconAlt ?? ""}
|
||||
/>
|
||||
{/* <img
|
||||
src={iconUrl ?? ""}
|
||||
alt={iconAlt ?? ""}
|
||||
className="h-[18px] w-[18px] object-contain"
|
||||
|
||||
/> */}
|
||||
</div>
|
||||
<div className="">
|
||||
<p
|
||||
|
|
@ -224,7 +219,11 @@ const DataAnalysisDashboardSlide = ({ data }: { data: Partial<SchemaType> }) =>
|
|||
</div>}
|
||||
<div className="flex-1 flex flex-col pb-[30px]">
|
||||
|
||||
{halfChart && halfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1">
|
||||
{halfChart && halfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1 "
|
||||
style={{
|
||||
height: otherHalfChart && otherHalfChart?.length > 0 ? '200px' : 'auto',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`grid h-full bg-white p-[13px] rounded-[14px] min-h-0 gap-[10px] `}
|
||||
style={{
|
||||
|
|
@ -240,13 +239,16 @@ const DataAnalysisDashboardSlide = ({ data }: { data: Partial<SchemaType> }) =>
|
|||
>
|
||||
|
||||
<div className="flex-1 " >
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
<ResponsiveContainer width="100%" height="100%" maxHeight={otherHalfChart && otherHalfChart?.length > 0 ? 200 : 400}>
|
||||
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>}
|
||||
{otherHalfChart && otherHalfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1">
|
||||
{otherHalfChart && otherHalfChart.length > 0 && <div className="mt-[14px] px-[64px] flex-1 h-[200px] ">
|
||||
<div
|
||||
className={`grid h-full bg-white p-[13px] rounded-[14px] min-h-0 gap-[10px] `}
|
||||
style={{
|
||||
|
|
@ -260,10 +262,10 @@ const DataAnalysisDashboardSlide = ({ data }: { data: Partial<SchemaType> }) =>
|
|||
className="rounded-[6px] flex flex-col overflow-hidden"
|
||||
>
|
||||
<div className="flex-1 " >
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ResponsiveContainer width="100%" height="100%" maxHeight={180}>
|
||||
|
||||
<FlexibleReportChart density="compact" chartType={chart.type} data={chart.data} series={chart.series} />
|
||||
</ResponsiveContainer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const MilestoneItemSchema = z.object({
|
|||
description: "Heading displayed below the milestone marker.",
|
||||
}),
|
||||
description: z.string().min(10).max(80).meta({
|
||||
description: "Supporting milestone description shown under the heading.",
|
||||
description: "Supporting milestone description shown under the heading. with max 80 characters",
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -32,27 +32,27 @@ export const Schema = z.object({
|
|||
{
|
||||
bulletNumber: "01",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,",
|
||||
},
|
||||
{
|
||||
bulletNumber: "02",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
description: "Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet,",
|
||||
},
|
||||
{
|
||||
bulletNumber: "03",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
description: "Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, Lorem ipsum dolor sit amet",
|
||||
},
|
||||
{
|
||||
bulletNumber: "04",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,",
|
||||
},
|
||||
{
|
||||
bulletNumber: "05",
|
||||
heading: "Heading",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet,",
|
||||
},
|
||||
])
|
||||
.meta({
|
||||
|
|
@ -118,7 +118,7 @@ const MilestoneSlide = ({ data }: { data: Partial<SchemaType> }) => {
|
|||
</div>
|
||||
<div
|
||||
key={`${item.heading}-${index}`}
|
||||
className={`text-center mt-[20px] text-[#232223] ${index > 0 ? 'pr-[33px]' : ''} ${index === 0 ? 'px-[33px]' : ''}`}
|
||||
className={`text-center h-[130px] mt-[20px] text-[#232223] ${index > 0 ? 'pr-[33px]' : ''} ${index === 0 ? 'px-[33px]' : ''}`}
|
||||
style={{ color: "var(--background-text,#232223)" }}
|
||||
>
|
||||
<h3 className="text-[20px] text-[#232223] font-medium tracking-[2.074px]" style={{ color: "var(--background-text,#232223)" }}>
|
||||
|
|
|
|||
|
|
@ -382,8 +382,6 @@ export function FlexibleReportChart({
|
|||
case "bar":
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
|
||||
|
||||
<BarChart data={normalizedData as any[]} {...commonProps}>
|
||||
<CartesianGrid vertical={false} {...gridProps} />
|
||||
<XAxis
|
||||
|
|
|
|||
|
|
@ -46,11 +46,11 @@ export const ProgressBar = ({ duration, onComplete }: ProgressBarProps) => {
|
|||
|
||||
return (
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex justify-end items-center text-white/80 text-sm">
|
||||
<div className="flex justify-end items-center text-sm">
|
||||
{/* <span>Processing...</span> */}
|
||||
<span className='font-inter text-end font-medium text-xs'>{Math.round(progress)}%</span>
|
||||
<span className='font-inter text-[#191919]/80 text-end font-medium text-xs'>{Math.round(progress)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-white rounded-full h-2 overflow-hidden">
|
||||
<div className="w-full bg-white/40 rounded-full h-2 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#9034EA] via-[#5146E5] to-[#9034EA] rounded-full animate-gradient transition-all duration-300 ease-out"
|
||||
style={{
|
||||
|
|
|
|||
|
|
@ -1,9 +1,25 @@
|
|||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { BadgeCheck, Loader2, ShieldAlert } from "lucide-react"
|
||||
import { BadgeCheck, Info, Loader2, ShieldAlert } from "lucide-react"
|
||||
import { Toaster as Sonner, toast as sonnerToast } from "sonner"
|
||||
|
||||
/** Blue circle for neutral / informational toasts (matches web `servers/nextjs` Toaster). */
|
||||
function NeutralToastIcon() {
|
||||
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="19" height="19" viewBox="0 0 19 19" fill="none">
|
||||
<path d="M9.12333 17.4567C13.7257 17.4567 17.4567 13.7257 17.4567 9.12337C17.4567 4.521 13.7257 0.790039 9.12333 0.790039C4.52096 0.790039 0.790001 4.521 0.790001 9.12337C0.790001 13.7257 4.52096 17.4567 9.12333 17.4567Z" fill="url(#paint0_linear_4686_451)" stroke="#2863A3" strokeWidth="1.58" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_4686_451" x1="9.12333" y1="0.790039" x2="9.12333" y2="17.4567" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#1880F6" />
|
||||
<stop offset="1" stopColor="#75B5FF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** Toasts with both title and description (matches styled [data-title] / [data-description]). */
|
||||
export const notify = {
|
||||
error: (title: string, description: string) =>
|
||||
|
|
@ -20,7 +36,7 @@ const Toaster = ({ icons, ...props }: ToasterProps) => {
|
|||
const defaultIcons: NonNullable<ToasterProps["icons"]> = {
|
||||
success: <BadgeCheck aria-hidden="true" />,
|
||||
error: <ShieldAlert aria-hidden="true" />,
|
||||
info: <ShieldAlert aria-hidden="true" />,
|
||||
info: <Info className="fill-[#1880F6] stroke-white" />,
|
||||
warning: <ShieldAlert aria-hidden="true" />,
|
||||
loading: <Loader2 aria-hidden="true" className="animate-spin" />,
|
||||
close: <span aria-hidden="true">Got it!</span>,
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ interface CodexModel {
|
|||
}
|
||||
|
||||
const CHATGPT_MODELS: CodexModel[] = [
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
{ id: "gpt-5.1", name: "GPT-5.1" },
|
||||
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
||||
{ id: "gpt-5.2", name: "GPT-5.2" },
|
||||
{ id: "gpt-5.2-codex", name: "GPT-5.2 Codex" },
|
||||
{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" },
|
||||
{ id: "gpt-5.4-mini", name: "GPT-5.4 Mini" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4" },
|
||||
];
|
||||
|
||||
const DEFAULT_CODEX_MODEL = "gpt-5.4-mini";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue