diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 2b6aaefc3e..687487f5f2 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -60,7 +60,9 @@ "identifier": "opener:allow-open-path", "allow": [ { "path": "$APPDATA/*" }, - { "path": "$APPDATA/**" } + { "path": "$APPDATA/**" }, + { "path": "$DOWNLOAD/*" }, + { "path": "$DOWNLOAD/**" } ] }, { @@ -78,7 +80,9 @@ "identifier": "fs:allow-write-file", "allow": [ { "path": "$APPDATA/*" }, - { "path": "$APPDATA/**" } + { "path": "$APPDATA/**" }, + { "path": "$DOWNLOAD/*" }, + { "path": "$DOWNLOAD/**" } ] }, { diff --git a/apps/desktop/src/components/toolbar/buttons/share-button.tsx b/apps/desktop/src/components/toolbar/buttons/share-button.tsx index 2d4be60077..63bc0374d4 100644 --- a/apps/desktop/src/components/toolbar/buttons/share-button.tsx +++ b/apps/desktop/src/components/toolbar/buttons/share-button.tsx @@ -23,7 +23,7 @@ import { Button } from "@hypr/ui/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@hypr/ui/components/ui/select"; import { useSession } from "@hypr/utils/contexts"; -import { exportToPDF } from "../utils/pdf-export"; +import { exportToPDF, getAvailableThemes, type ThemeName } from "../utils/pdf-export"; export function ShareButton() { const param = useParams({ from: "/app/note/$id", shouldThrow: false }); @@ -38,6 +38,7 @@ function ShareButtonInNote() { const [open, setOpen] = useState(false); const [expandedId, setExpandedId] = useState(null); const [selectedObsidianFolder, setSelectedObsidianFolder] = useState("default"); + const [selectedPdfTheme, setSelectedPdfTheme] = useState("default"); const [includeTranscript, setIncludeTranscript] = useState(false); const [copySuccess, setCopySuccess] = useState(false); const hasEnhancedNote = !!session?.enhanced_memo_html; @@ -161,7 +162,7 @@ function ShareButtonInNote() { if (optionId === "copy") { result = await exportHandlers.copy(session); } else if (optionId === "pdf") { - result = await exportHandlers.pdf(session); + result = await exportHandlers.pdf(session, selectedPdfTheme); } else if (optionId === "email") { try { // fetch participants directly, bypassing cache @@ -331,6 +332,29 @@ function ShareButtonInNote() { + {option.id === "pdf" && ( +
+ + +
+ )} + {option.id === "obsidian" && ( <>
@@ -440,8 +464,11 @@ const exportHandlers = { } }, - pdf: async (session: Session): Promise => { - const path = await exportToPDF(session); + pdf: async (session: Session, theme: ThemeName = "default"): Promise => { + const path = await exportToPDF(session, theme); + if (path) { + await message(`Meeting summary saved to your 'Downloads' folder ("${path}")`); + } return { type: "pdf", path }; }, diff --git a/apps/desktop/src/components/toolbar/utils/pdf-export.ts b/apps/desktop/src/components/toolbar/utils/pdf-export.ts index 3c8cf3fdb3..c00c68ac25 100644 --- a/apps/desktop/src/components/toolbar/utils/pdf-export.ts +++ b/apps/desktop/src/components/toolbar/utils/pdf-export.ts @@ -1,8 +1,11 @@ -import { appDataDir } from "@tauri-apps/api/path"; +import { downloadDir } from "@tauri-apps/api/path"; import { writeFile } from "@tauri-apps/plugin-fs"; import { jsPDF } from "jspdf"; import { commands as dbCommands, type Event, type Human, type Session } from "@hypr/plugin-db"; +import { getPDFTheme, type ThemeName } from "./pdf-themes"; + +export { getAvailableThemes, getPDFTheme, type PDFTheme, type ThemeName } from "./pdf-themes"; export type SessionData = Session & { participants?: Human[]; @@ -11,26 +14,61 @@ export type SessionData = Session & { interface TextSegment { text: string; - bold?: boolean; - italic?: boolean; - isHeader?: number; // 1, 2, 3 for h1, h2, h3 + isHeader?: number; isListItem?: boolean; + listType?: "ordered" | "unordered"; + listLevel?: number; + listItemNumber?: number; + bulletType?: "filled-circle" | "hollow-circle" | "square" | "triangle"; +} + +interface ListContext { + type: "ordered" | "unordered"; + level: number; + counters: number[]; } -// TODO: -// 1. Tiptap already has structured output - toJSON(). Should be cleaner than htmlToStructuredText. -// 2. Fetch should happen outside. This file should be only do the rendering. (Ideally writeFile should be happened outside too) -// 3. exportToPDF should be composed with multiple steps. +const getOrderedListMarker = (counter: number, level: number): string => { + switch (level) { + case 0: + return `${counter}.`; + case 1: + return `${String.fromCharCode(96 + counter)}.`; + default: + return `${toRomanNumeral(counter)}.`; + } +}; + +const toRomanNumeral = (num: number): string => { + const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]; + const numerals = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"]; + + let result = ""; + for (let i = 0; i < values.length; i++) { + while (num >= values[i]) { + result += numerals[i]; + num -= values[i]; + } + } + return result; +}; const htmlToStructuredText = (html: string): TextSegment[] => { if (!html) { return []; } + const cleanedHtml = html + .replace(/<\/?strong>/gi, "") + .replace(/<\/?b>/gi, "") + .replace(/<\/?em>/gi, "") + .replace(/<\/?i>/gi, ""); + const tempDiv = document.createElement("div"); - tempDiv.innerHTML = html; + tempDiv.innerHTML = cleanedHtml; const segments: TextSegment[] = []; + const listStack: ListContext[] = []; const processNode = (node: Node) => { if (node.nodeType === Node.TEXT_NODE) { @@ -52,35 +90,102 @@ const htmlToStructuredText = (html: string): TextSegment[] => { case "h3": segments.push({ text: element.textContent || "", isHeader: 3 }); break; - case "strong": - case "b": - segments.push({ text: element.textContent || "", bold: true }); + + case "ul": + processListContainer(element, "unordered"); break; - case "em": - case "i": - segments.push({ text: element.textContent || "", italic: true }); + case "ol": + processListContainer(element, "ordered"); break; case "li": - segments.push({ text: `• ${element.textContent || ""}`, isListItem: true }); + processListItem(element); break; + case "p": if (element.textContent?.trim()) { - // Process inline formatting within paragraphs processInlineFormatting(element, segments); - segments.push({ text: "\n" }); // Add paragraph break + segments.push({ text: "\n" }); } break; case "br": segments.push({ text: "\n" }); break; default: - // For other elements, process children Array.from(node.childNodes).forEach(processNode); break; } } }; + const processListContainer = (listElement: Element, type: "ordered" | "unordered") => { + const level = listStack.length; + + const counters = [...(listStack[listStack.length - 1]?.counters || [])]; + if (counters.length <= level) { + counters[level] = 0; + } + + listStack.push({ type, level, counters }); + + Array.from(listElement.children).forEach((child, index) => { + if (child.tagName.toLowerCase() === "li") { + if (type === "ordered") { + counters[level] = index + 1; + } + processNode(child); + } + }); + + listStack.pop(); + }; + + const processListItem = (liElement: Element) => { + const currentList = listStack[listStack.length - 1]; + if (!currentList) { + return; + } + + const { type, level, counters } = currentList; + + const textContent = getListItemText(liElement); + + const bulletTypes = ["filled-circle", "hollow-circle", "square"] as const; + + segments.push({ + text: type === "ordered" + ? `${getOrderedListMarker(counters[level], level)} ${textContent}` + : textContent, + isListItem: true, + listType: type, + listLevel: level, + listItemNumber: type === "ordered" ? counters[level] : undefined, + bulletType: type === "unordered" + ? (level <= 2 ? bulletTypes[level] : "square") + : undefined, + }); + + Array.from(liElement.children).forEach(child => { + if (child.tagName.toLowerCase() === "ul" || child.tagName.toLowerCase() === "ol") { + processNode(child); + } + }); + }; + + const getListItemText = (liElement: Element): string => { + let text = ""; + for (const child of liElement.childNodes) { + if (child.nodeType === Node.TEXT_NODE) { + text += child.textContent || ""; + } else if (child.nodeType === Node.ELEMENT_NODE) { + const element = child as Element; + if (!["ul", "ol"].includes(element.tagName.toLowerCase())) { + text += element.textContent || ""; + } + } + } + return text.trim(); + }; + const processInlineFormatting = (element: Element, segments: TextSegment[]) => { Array.from(element.childNodes).forEach(child => { if (child.nodeType === Node.TEXT_NODE) { @@ -90,23 +195,10 @@ const htmlToStructuredText = (html: string): TextSegment[] => { } } else if (child.nodeType === Node.ELEMENT_NODE) { const childElement = child as Element; - const tagName = childElement.tagName.toLowerCase(); const text = childElement.textContent || ""; if (text.trim()) { - switch (tagName) { - case "strong": - case "b": - segments.push({ text, bold: true }); - break; - case "em": - case "i": - segments.push({ text, italic: true }); - break; - default: - segments.push({ text }); - break; - } + segments.push({ text }); } } }); @@ -116,7 +208,6 @@ const htmlToStructuredText = (html: string): TextSegment[] => { return segments; }; -// Split text into lines that fit within the PDF width const splitTextToLines = (text: string, pdf: jsPDF, maxWidth: number): string[] => { const words = text.split(" "); const lines: string[] = []; @@ -141,7 +232,6 @@ const splitTextToLines = (text: string, pdf: jsPDF, maxWidth: number): string[] return lines; }; -// Fetch additional session data (participants and event info) const fetchSessionMetadata = async (sessionId: string): Promise<{ participants: Human[]; event: Event | null }> => { try { const [participants, event] = await Promise.all([ @@ -155,10 +245,70 @@ const fetchSessionMetadata = async (sessionId: string): Promise<{ participants: } }; -export const exportToPDF = async (session: SessionData): Promise => { +const drawVectorBullet = ( + pdf: jsPDF, + bulletType: "filled-circle" | "hollow-circle" | "square" | "triangle", + x: number, + y: number, + size: number = 1.0, + color: readonly [number, number, number] = [50, 50, 50], // Accept color parameter +) => { + // Save current state + const currentFillColor = pdf.getFillColor(); + const currentDrawColor = pdf.getDrawColor(); + + pdf.setFillColor(...color); + pdf.setDrawColor(...color); + pdf.setLineWidth(0.2); + + const bulletY = y - (size / 2); + + switch (bulletType) { + case "filled-circle": + pdf.circle(x, bulletY, size * 0.85, "F"); + break; + + case "hollow-circle": + pdf.circle(x, bulletY, size * 0.85, "S"); + break; + + case "square": + const squareSize = size * 1.4; + pdf.rect( + x - squareSize / 2, + bulletY - squareSize / 2, + squareSize, + squareSize, + "F", + ); + break; + + case "triangle": + const triangleSize = size * 1.15; + pdf.triangle( + x, + bulletY - triangleSize / 2, // top point + x - triangleSize / 2, + bulletY + triangleSize / 2, // bottom left + x + triangleSize / 2, + bulletY + triangleSize / 2, // bottom right + "F", + ); + break; + } + + pdf.setFillColor(currentFillColor); + pdf.setDrawColor(currentDrawColor); +}; + +export const exportToPDF = async ( + session: SessionData, + themeName: ThemeName = "default", +): Promise => { const { participants, event } = await fetchSessionMetadata(session.id); - // Generate filename + const PDF_STYLES = getPDFTheme(themeName); + const filename = session?.title ? `${session.title.replace(/[^a-z0-9]/gi, "_").toLowerCase()}.pdf` : `note_${new Date().toISOString().split("T")[0]}.pdf`; @@ -171,46 +321,58 @@ export const exportToPDF = async (session: SessionData): Promise => { const maxWidth = pageWidth - (margin * 2); const lineHeight = 6; + const applyBackgroundColor = () => { + if ( + PDF_STYLES.colors.background[0] !== 255 + || PDF_STYLES.colors.background[1] !== 255 + || PDF_STYLES.colors.background[2] !== 255 + ) { + pdf.setFillColor(...PDF_STYLES.colors.background); + pdf.rect(0, 0, pageWidth, pageHeight, "F"); + } + }; + + const addNewPage = () => { + pdf.addPage(); + applyBackgroundColor(); + }; + let yPosition = margin; - // Add title with text wrapping + applyBackgroundColor(); + const title = session?.title || "Untitled Note"; pdf.setFontSize(16); - pdf.setFont("helvetica", "bold"); - pdf.setTextColor(0, 0, 0); // Black + pdf.setFont(PDF_STYLES.font, "bold"); + pdf.setTextColor(...PDF_STYLES.colors.headers); - // Split title into multiple lines if it's too long const titleLines = splitTextToLines(title, pdf, maxWidth); for (const titleLine of titleLines) { pdf.text(titleLine, margin, yPosition); yPosition += lineHeight; } - yPosition += lineHeight; // Extra space after title + yPosition += lineHeight; - // Add creation date ONLY if there's no event info if (!event && session?.created_at) { pdf.setFontSize(10); - pdf.setFont("helvetica", "normal"); - pdf.setTextColor(100, 100, 100); // Gray + pdf.setFont(PDF_STYLES.font, "normal"); + pdf.setTextColor(...PDF_STYLES.colors.metadata); const createdAt = `Created: ${new Date(session.created_at).toLocaleDateString()}`; pdf.text(createdAt, margin, yPosition); yPosition += lineHeight; } - // Add event info if available if (event) { pdf.setFontSize(10); - pdf.setFont("helvetica", "normal"); - pdf.setTextColor(100, 100, 100); // Gray + pdf.setFont(PDF_STYLES.font, "normal"); + pdf.setTextColor(...PDF_STYLES.colors.metadata); // Use metadata color - // Event name if (event.name) { pdf.text(`Event: ${event.name}`, margin, yPosition); yPosition += lineHeight; } - // Event date/time if (event.start_date) { const startDate = new Date(event.start_date); const endDate = event.end_date ? new Date(event.end_date) : null; @@ -223,7 +385,6 @@ export const exportToPDF = async (session: SessionData): Promise => { pdf.text(dateText, margin, yPosition); yPosition += lineHeight; - // Time const timeText = endDate ? `Time: ${startDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} - ${ endDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) @@ -234,11 +395,10 @@ export const exportToPDF = async (session: SessionData): Promise => { } } - // Add participants if available if (participants && participants.length > 0) { pdf.setFontSize(10); - pdf.setFont("helvetica", "normal"); - pdf.setTextColor(100, 100, 100); // Gray + pdf.setFont(PDF_STYLES.font, "normal"); + pdf.setTextColor(...PDF_STYLES.colors.metadata); const participantNames = participants .filter(p => p.full_name) @@ -256,83 +416,94 @@ export const exportToPDF = async (session: SessionData): Promise => { } } - // Add attribution with clickable "Hyprnote" pdf.setFontSize(10); - pdf.setFont("helvetica", "normal"); - pdf.setTextColor(100, 100, 100); // Gray + pdf.setFont(PDF_STYLES.font, "normal"); + pdf.setTextColor(...PDF_STYLES.colors.metadata); pdf.text("Summarized by ", margin, yPosition); - // Calculate width of "Summarized by " to position "Hyprnote" const madeByWidth = pdf.getTextWidth("Summarized by "); - pdf.setTextColor(37, 99, 235); // Blue color for Hyprnote + pdf.setTextColor(...PDF_STYLES.colors.hyprnoteLink); - // Create clickable link for Hyprnote const hyprnoteText = "Hyprnote"; pdf.textWithLink(hyprnoteText, margin + madeByWidth, yPosition, { url: "https://www.hyprnote.com" }); yPosition += lineHeight * 2; - // Add separator line - pdf.setDrawColor(200, 200, 200); // Light gray line + pdf.setDrawColor(...PDF_STYLES.colors.separatorLine); pdf.line(margin, yPosition, pageWidth - margin, yPosition); yPosition += lineHeight; - // Convert HTML to structured text and add content const segments = htmlToStructuredText(session?.enhanced_memo_html || "No content available"); for (const segment of segments) { - // Check if we need a new page if (yPosition > pageHeight - margin) { - pdf.addPage(); + addNewPage(); yPosition = margin; } - // Set font style based on segment properties if (segment.isHeader) { const headerSizes = { 1: 14, 2: 13, 3: 12 }; pdf.setFontSize(headerSizes[segment.isHeader as keyof typeof headerSizes]); - pdf.setFont("helvetica", "bold"); - pdf.setTextColor(0, 0, 0); // Black for headers - yPosition += lineHeight; // Extra space before headers + pdf.setFont(PDF_STYLES.font, "bold"); + pdf.setTextColor(...PDF_STYLES.colors.headers); + yPosition += lineHeight; } else { pdf.setFontSize(12); - const fontStyle = segment.bold && segment.italic - ? "bolditalic" - : segment.bold - ? "bold" - : segment.italic - ? "italic" - : "normal"; - pdf.setFont("helvetica", fontStyle); - pdf.setTextColor(50, 50, 50); // Dark gray for content + pdf.setFont(PDF_STYLES.font, "normal"); + pdf.setTextColor(...PDF_STYLES.colors.mainContent); } - // Handle list items with indentation - const xPosition = segment.isListItem ? margin + 5 : margin; + let xPosition = margin; + let bulletSpace = 0; + + if (segment.isListItem && segment.listLevel !== undefined) { + const baseIndent = 5; + const levelIndent = 8; + xPosition = margin + baseIndent + (segment.listLevel * levelIndent); + + bulletSpace = segment.listType === "ordered" ? 0 : 6; + } - // Split long text into multiple lines - const lines = splitTextToLines(segment.text, pdf, maxWidth - (segment.isListItem ? 5 : 0)); + const effectiveMaxWidth = maxWidth - (xPosition - margin) - bulletSpace; + const lines = splitTextToLines(segment.text, pdf, effectiveMaxWidth); for (let i = 0; i < lines.length; i++) { if (yPosition > pageHeight - margin) { - pdf.addPage(); + addNewPage(); yPosition = margin; } - pdf.text(lines[i], xPosition, yPosition); + if ( + segment.isListItem + && segment.listType === "unordered" + && segment.bulletType + && i === 0 + ) { + drawVectorBullet( + pdf, + segment.bulletType, + xPosition + 2, + yPosition - 1, + 1.0, + PDF_STYLES.colors.bullets, + ); + } + + const textXPosition = xPosition + bulletSpace; + + pdf.text(lines[i], textXPosition, yPosition); yPosition += lineHeight; } - // Add extra space after headers and paragraphs if (segment.isHeader || segment.text === "\n") { - yPosition += lineHeight * 0.5; + yPosition += lineHeight * 0.25; } } const pdfArrayBuffer = pdf.output("arraybuffer"); const uint8Array = new Uint8Array(pdfArrayBuffer); - const downloadsPath = await appDataDir(); + const downloadsPath = await downloadDir(); const filePath = downloadsPath.endsWith("/") ? `${downloadsPath}${filename}` : `${downloadsPath}/${filename}`; diff --git a/apps/desktop/src/components/toolbar/utils/pdf-themes.ts b/apps/desktop/src/components/toolbar/utils/pdf-themes.ts new file mode 100644 index 0000000000..dfcbb362bc --- /dev/null +++ b/apps/desktop/src/components/toolbar/utils/pdf-themes.ts @@ -0,0 +1,235 @@ +export type ThemeName = + | "default" + | "light" + | "dark" + | "corporate" + | "ocean" + | "sunset" + | "forest" + | "cyberpunk" + | "retro" + | "spring" + | "summer" + | "winter"; + +export interface PDFTheme { + font: string; + colors: { + background: readonly [number, number, number]; + mainContent: readonly [number, number, number]; + headers: readonly [number, number, number]; + metadata: readonly [number, number, number]; + hyprnoteLink: readonly [number, number, number]; + separatorLine: readonly [number, number, number]; + bullets: readonly [number, number, number]; + }; +} + +export const getPDFTheme = (themeName: ThemeName): PDFTheme => { + const themes: Record = { + default: { + font: "helvetica", + colors: { + background: [255, 255, 255], // Pure white (kept as requested) + mainContent: [33, 33, 33], // Dark charcoal + headers: [0, 0, 0], // Black + metadata: [102, 102, 102], // Medium gray + hyprnoteLink: [59, 130, 246], // Blue + separatorLine: [229, 229, 229], // Light gray + bullets: [75, 85, 99], // Slate gray + }, + }, + + light: { + font: "helvetica", + colors: { + background: [248, 250, 252], // Very light blue + mainContent: [30, 58, 138], // Deep blue + headers: [15, 23, 42], // Navy + metadata: [100, 116, 139], // Steel blue + hyprnoteLink: [37, 99, 235], // Bright blue + separatorLine: [186, 230, 253], // Light sky blue + bullets: [59, 130, 246], // Blue + }, + }, + + dark: { + font: "helvetica", + colors: { + background: [15, 15, 15], // Almost black + mainContent: [220, 220, 220], // Light gray + headers: [255, 255, 255], // White + metadata: [140, 140, 140], // Medium gray + hyprnoteLink: [96, 165, 250], // Light blue + separatorLine: [40, 40, 40], // Dark gray + bullets: [180, 180, 180], // Light gray + }, + }, + + corporate: { + font: "times", + colors: { + background: [255, 255, 255], // Pure white (kept unchanged) + mainContent: [15, 23, 42], // Slate 900 + headers: [30, 41, 59], // Slate 800 + metadata: [100, 116, 139], // Slate 500 + hyprnoteLink: [30, 64, 175], // Professional blue + separatorLine: [203, 213, 225], // Slate 300 + bullets: [51, 65, 85], // Slate 700 + }, + }, + + ocean: { + font: "helvetica", + colors: { + background: [240, 249, 255], // Light ocean blue + mainContent: [7, 89, 133], // Deep ocean blue + headers: [12, 74, 110], // Ocean blue + metadata: [14, 116, 144], // Teal + hyprnoteLink: [6, 182, 212], // Cyan + separatorLine: [165, 243, 252], // Light cyan + bullets: [34, 211, 238], // Bright cyan + }, + }, + + sunset: { + font: "times", + colors: { + background: [255, 247, 237], // Warm cream + mainContent: [120, 53, 15], // Dark brown + headers: [194, 65, 12], // Orange red + metadata: [156, 105, 23], // Amber + hyprnoteLink: [234, 88, 12], // Bright orange + separatorLine: [254, 215, 170], // Peach + bullets: [251, 146, 60], // Orange + }, + }, + + forest: { + font: "helvetica", + colors: { + background: [236, 253, 245], // Mint green + mainContent: [20, 83, 45], // Forest green + headers: [5, 46, 22], // Dark forest green + metadata: [21, 128, 61], // Green + hyprnoteLink: [34, 197, 94], // Bright green + separatorLine: [167, 243, 208], // Light green + bullets: [74, 222, 128], // Lime green + }, + }, + + cyberpunk: { + font: "helvetica", + colors: { + background: [3, 7, 18], // Deep space black + mainContent: [0, 255, 204], // Matrix green + headers: [0, 255, 255], // Electric cyan + metadata: [102, 204, 255], // Neon blue + hyprnoteLink: [255, 0, 255], // Electric magenta + separatorLine: [0, 102, 153], // Dark blue + bullets: [0, 255, 128], // Bright green + }, + }, + + retro: { + font: "courier", + colors: { + background: [139, 69, 19], // Dark brown (much darker!) + mainContent: [255, 248, 220], // Cream text + headers: [255, 215, 0], // Gold + metadata: [222, 184, 135], // Burlywood + hyprnoteLink: [255, 140, 0], // Dark orange + separatorLine: [160, 82, 45], // Saddle brown + bullets: [255, 165, 0], // Orange + }, + }, + + spring: { + font: "courier", + colors: { + background: [254, 249, 195], // Light yellow green + mainContent: [56, 142, 60], // Green + headers: [27, 94, 32], // Dark green + metadata: [76, 175, 80], // Light green + hyprnoteLink: [139, 195, 74], // Lime + separatorLine: [200, 230, 201], // Very light green + bullets: [104, 159, 56], // Olive green + }, + }, + + summer: { + font: "helvetica", + colors: { + background: [255, 235, 59], // Bright yellow + mainContent: [191, 54, 12], // Deep red orange + headers: [213, 0, 0], // Red + metadata: [255, 87, 34], // Orange red + hyprnoteLink: [255, 152, 0], // Orange + separatorLine: [255, 193, 7], // Amber + bullets: [244, 67, 54], // Red + }, + }, + + winter: { + font: "times", + colors: { + background: [233, 242, 251], // Icy blue + mainContent: [13, 71, 161], // Deep blue + headers: [25, 118, 210], // Blue + metadata: [66, 165, 245], // Light blue + hyprnoteLink: [33, 150, 243], // Sky blue + separatorLine: [187, 222, 251], // Very light blue + bullets: [100, 181, 246], // Light blue + }, + }, + }; + + return themes[themeName] || themes.default; +}; + +export const getAvailableThemes = (): ThemeName[] => { + return [ + "default", + "light", + "dark", + "corporate", + "ocean", + "sunset", + "forest", + "cyberpunk", + "retro", + "spring", + "summer", + "winter", + ]; +}; + +export const getThemePreview = (themeName: ThemeName) => { + const theme = getPDFTheme(themeName); + return { + name: themeName, + font: theme.font, + primaryColor: theme.colors.headers, + backgroundColor: theme.colors.background, + description: getThemeDescription(themeName), + }; +}; + +const getThemeDescription = (themeName: ThemeName): string => { + const descriptions: Record = { + default: "Clean charcoal text on white with Helvetica", + light: "Deep blues on light blue with Helvetica", + dark: "Light text on deep black with Helvetica", + corporate: "Professional navy on white with Times", + ocean: "Ocean blues on light cyan with Helvetica", + sunset: "Warm browns and oranges on cream with Times", + forest: "Forest greens on mint background with Courier", + cyberpunk: "Matrix green on space black with Courier", + retro: "Gold text on dark brown with Courier", + spring: "Fresh greens on yellow-green with Courier", + summer: "Bright reds on yellow with Courier", + winter: "Deep blues on icy background with Times", + }; + + return descriptions[themeName] || descriptions.default; +};