| 1 | import type React from "react"; |
| 2 | |
| 3 | import { |
| 4 | calculateHeightFromRatio, |
| 5 | getSlideBaseWidth, |
| 6 | } from "@/config/slideFormats"; |
| 7 | import { type PlateSlide } from "../../utils/parser"; |
| 8 | import { getTypographyCSSVariables } from "./typography"; |
| 9 | |
| 10 | /** |
| 11 | * Get layout flex direction classes based on layout type. |
| 12 | */ |
| 13 | export function getLayoutClasses(layoutType: PlateSlide["layoutType"]): string { |
| 14 | switch (layoutType) { |
| 15 | case "right": |
| 16 | return "flex-row"; |
| 17 | case "vertical": |
| 18 | return "flex-col-reverse justify-end"; |
| 19 | case "left": |
| 20 | return "flex-row-reverse"; |
| 21 | case "background": |
| 22 | return "flex-col"; |
| 23 | case "none": |
| 24 | case undefined: |
| 25 | return "flex-col"; |
| 26 | default: |
| 27 | return "flex-col"; |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | /** |
| 32 | * Get present mode specific classes. |
| 33 | * @param isPresenting - Whether in present mode |
| 34 | * @param formatCategory - The slide format category (affects how width is handled) |
| 35 | */ |
| 36 | export function getPresentingClasses( |
| 37 | isPresenting: boolean, |
| 38 | formatCategory?: PlateSlide["formatCategory"], |
| 39 | ): string { |
| 40 | if (!isPresenting) { |
| 41 | return "min-h-125 border border-(--presentation-accent)"; |
| 42 | } |
| 43 | |
| 44 | // For social format, don't force w-full so the aspect ratio is preserved |
| 45 | // The width will come from the slide's defined dimensions (via styleByFormat) |
| 46 | if (formatCategory === "social") { |
| 47 | return "border-0"; |
| 48 | } |
| 49 | |
| 50 | // For presentation/fluid formats, take full width. |
| 51 | // On larger screens (md+), force min-h-dvh so slide fills the viewport. |
| 52 | // On phones (below md), maintain a 16:9 aspect ratio so the slide acts |
| 53 | // like `object-fit: contain` and preserves its layout. |
| 54 | return "w-full border-0 md:min-h-dvh max-md:aspect-video"; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Calculate slide format styles (width/height) for the presentation root. |
| 59 | */ |
| 60 | export function getSlideFormatStyles( |
| 61 | category: PlateSlide["formatCategory"], |
| 62 | width: PlateSlide["width"], |
| 63 | aspectRatio: PlateSlide["aspectRatio"], |
| 64 | ): React.CSSProperties { |
| 65 | const slideWidthSize = width ?? "M"; |
| 66 | const formatCategory = category ?? "presentation"; |
| 67 | const aspect = aspectRatio ?? { type: "fluid", value: "" }; |
| 68 | |
| 69 | const baseWidth = getSlideBaseWidth( |
| 70 | formatCategory, |
| 71 | slideWidthSize as "S" | "M" | "L", |
| 72 | aspect, |
| 73 | ); |
| 74 | |
| 75 | const heightConfig = calculateHeightFromRatio(baseWidth, aspect); |
| 76 | |
| 77 | return { |
| 78 | ...(aspect.type !== "fluid" ? { width: `${baseWidth}px` } : {}), |
| 79 | ...(heightConfig.minHeightCSS |
| 80 | ? { minHeight: heightConfig.minHeightCSS } |
| 81 | : {}), |
| 82 | }; |
| 83 | } |
| 84 | |
| 85 | /** |
| 86 | * Get custom inline styles for the slide based on content settings. |
| 87 | */ |
| 88 | export function getSlideCustomStyles( |
| 89 | initialContent: PlateSlide | undefined, |
| 90 | isPresenting: boolean, |
| 91 | styleByFormat: React.CSSProperties, |
| 92 | ): React.CSSProperties { |
| 93 | // Determine if we should apply format dimensions in present mode |
| 94 | // For social format, we WANT strict dimensions to preserve aspect ratio |
| 95 | // For presentation format, we rely on the CSS classes (min-h-dvh! w-full) |
| 96 | const shouldApplyDimensions = |
| 97 | !isPresenting || initialContent?.formatCategory === "social"; |
| 98 | |
| 99 | return { |
| 100 | borderRadius: isPresenting |
| 101 | ? "0" |
| 102 | : "var(--presentation-slide-border-radius)", |
| 103 | boxShadow: isPresenting ? "none" : "var(--presentation-slide-shadow)", |
| 104 | |
| 105 | // Apply dimensions if not presenting OR if it's social format |
| 106 | ...(shouldApplyDimensions && styleByFormat), |
| 107 | |
| 108 | // Background color override |
| 109 | ...(initialContent?.bgColor && { |
| 110 | "--presentation-background": initialContent.bgColor as string, |
| 111 | backgroundColor: initialContent.bgColor as string, |
| 112 | }), |
| 113 | // Background image for background layout |
| 114 | backgroundImage: |
| 115 | initialContent?.layoutType === "background" && |
| 116 | initialContent?.rootImage?.url |
| 117 | ? `url(${initialContent.rootImage.url})` |
| 118 | : undefined, |
| 119 | backgroundSize: "cover", |
| 120 | backgroundPosition: "center", |
| 121 | backgroundRepeat: "no-repeat", |
| 122 | // Typography |
| 123 | ...getTypographyCSSVariables(initialContent?.fontSize), |
| 124 | ...(initialContent?.fontFamily?.body && { |
| 125 | "--presentation-body-font": initialContent.fontFamily.body as string, |
| 126 | }), |
| 127 | ...(initialContent?.fontFamily?.heading && { |
| 128 | "--presentation-heading-font": initialContent.fontFamily |
| 129 | .heading as string, |
| 130 | }), |
| 131 | fontFamily: "var(--presentation-body-font)", |
| 132 | } as React.CSSProperties; |
| 133 | } |
| 134 |