diff --git a/src/components/sidebar-file-menu.tsx b/src/components/sidebar-file-menu.tsx index ec1577b..aa1edd5 100644 --- a/src/components/sidebar-file-menu.tsx +++ b/src/components/sidebar-file-menu.tsx @@ -35,17 +35,8 @@ import { getDocumentTitle, addCopyToTitle } from "@/lib/document-utils" import { exportDocument, saveDocumentAs, type ExportAsset } from "@/lib/export" import type { MarkdownEditorRef } from "@/editor/editor" -import { - findThemeByName, - getThemeName, - getPresetName, - findPresetByName, - getThemePresets, - findPresetByAppearance, - type LoadedThemes, -} from "@/lib/document-theme" -import { buildPrintableHtml, openPrintWindow } from "@/lib/pdf-export" -import { Marked } from "marked" +import { type LoadedThemes } from "@/lib/document-theme" +import { printToPdf } from "@/lib/print-to-pdf" export { SidebarFileMenu } @@ -109,6 +100,13 @@ function SidebarFileMenu({ doc, editor, me, spaceId }: SidebarFileMenuProps) { let isAdmin = docGroup?.myRole() === "admin" let isPinned = parseFrontmatter(content).frontmatter?.pinned === true let isPresentation = getPresentationMode(content) + let handlePrintPdf = makePrintPdf( + content, + meWithThemes.$isLoaded ? meWithThemes.root?.themes : undefined, + meWithThemes.$isLoaded + ? (meWithThemes.root?.settings?.defaultPreviewTheme ?? null) + : null, + ) return ( <> @@ -190,15 +188,7 @@ function SidebarFileMenu({ doc, editor, me, spaceId }: SidebarFileMenuProps) { Save as... {modKey}S - + Print to PDF {modKey}P @@ -518,53 +508,7 @@ function makePrintPdf( defaultPreviewTheme: string | null, ) { return async function handlePrintPdf() { - let { body } = parseFrontmatter(content) - let title = getDocumentTitle(content) - - // Resolve theme and preset from frontmatter (same logic as useDocumentTheme) - let themeName = getThemeName(content) - let presetName = getPresetName(content) - - // Handle "light"/"dark" as appearance-only, not theme names - let isAppearanceOnlyTheme = themeName === "light" || themeName === "dark" - let effectiveThemeName = isAppearanceOnlyTheme ? null : themeName - - // Fall back to default preview theme from settings - if (!effectiveThemeName && defaultPreviewTheme) { - effectiveThemeName = defaultPreviewTheme - } - - let theme = effectiveThemeName - ? findThemeByName(themes ?? null, effectiveThemeName) - : null - let preset = null - - if (theme && presetName) { - preset = findPresetByName(theme, presetName) - } else if (theme) { - // PDF always uses light mode - preset = findPresetByAppearance(theme, "light") - if (!preset) { - let presets = getThemePresets(theme) - preset = presets[0] ?? null - } - } - - // Render markdown to HTML - let marked = new Marked() - marked.setOptions({ gfm: true, breaks: true }) - let htmlContent = await marked.parse(body) - - // Build printable HTML with theme styles - let printableHtml = await buildPrintableHtml({ - title, - htmlContent, - theme, - preset, - }) - - // Open print window - openPrintWindow(printableHtml) + await printToPdf({ content, themes, defaultPreviewTheme }) } } diff --git a/src/lib/editor-utils.ts b/src/lib/editor-utils.ts index eed4086..78d4074 100644 --- a/src/lib/editor-utils.ts +++ b/src/lib/editor-utils.ts @@ -11,6 +11,7 @@ import { getDocumentTitle } from "@/lib/document-utils" import { copyDocumentToMyList } from "@/lib/documents" import { saveDocumentAs } from "@/lib/export" import { useCoState, useAccount } from "jazz-tools/react" +import { toast } from "sonner" export { makeUploadImage, @@ -280,8 +281,27 @@ function setupKeyboardShortcuts(opts: { toggleRight: () => void toggleFocusMode: () => void openFind?: () => void + onPrintPdf?: () => void docWithContent: MaybeDocWithContent }) { + function downloadCurrentDocument() { + if (!opts.docWithContent?.$isLoaded) return + let title = getDocumentTitle(opts.docWithContent) + saveDocumentAs(opts.docWithContent.content?.toString() ?? "", title) + } + + function showAutosaveToast() { + toast("Alkalyte saves automatically", { + description: + "Changes are saved locally and synced to the cloud while you type.", + action: { + label: "Download", + onClick: downloadCurrentDocument, + }, + id: "editor-save-shortcut", + }) + } + function handleKeyDown(e: KeyboardEvent) { if ( (e.metaKey || e.ctrlKey) && @@ -320,11 +340,19 @@ function setupKeyboardShortcuts(opts: { opts.openFind?.() return } + if ( + (e.metaKey || e.ctrlKey) && + !e.shiftKey && + !e.altKey && + e.key.toLowerCase() === "p" + ) { + e.preventDefault() + opts.onPrintPdf?.() + return + } if ((e.metaKey || e.ctrlKey) && e.key === "s") { e.preventDefault() - if (!opts.docWithContent?.$isLoaded) return - let title = getDocumentTitle(opts.docWithContent) - saveDocumentAs(opts.docWithContent.content?.toString() ?? "", title) + showAutosaveToast() } } diff --git a/src/lib/print-to-pdf.ts b/src/lib/print-to-pdf.ts new file mode 100644 index 0000000..7372c68 --- /dev/null +++ b/src/lib/print-to-pdf.ts @@ -0,0 +1,63 @@ +import { Marked } from "marked" +import { parseFrontmatter } from "@/editor/frontmatter" +import { + findThemeByName, + getThemeName, + getPresetName, + findPresetByName, + getThemePresets, + findPresetByAppearance, + type LoadedThemes, +} from "@/lib/document-theme" +import { getDocumentTitle } from "@/lib/document-utils" +import { buildPrintableHtml, openPrintWindow } from "@/lib/pdf-export" + +export { printToPdf } + +async function printToPdf(params: { + content: string + themes: LoadedThemes | undefined + defaultPreviewTheme: string | null +}) { + let { content, themes, defaultPreviewTheme } = params + let { body } = parseFrontmatter(content) + let title = getDocumentTitle(content) + + let themeName = getThemeName(content) + let presetName = getPresetName(content) + + let isAppearanceOnlyTheme = themeName === "light" || themeName === "dark" + let effectiveThemeName = isAppearanceOnlyTheme ? null : themeName + + if (!effectiveThemeName && defaultPreviewTheme) { + effectiveThemeName = defaultPreviewTheme + } + + let theme = effectiveThemeName + ? findThemeByName(themes ?? null, effectiveThemeName) + : null + let preset = null + + if (theme && presetName) { + preset = findPresetByName(theme, presetName) + } else if (theme) { + preset = findPresetByAppearance(theme, "light") + if (!preset) { + let presets = getThemePresets(theme) + preset = presets[0] ?? null + } + } + + let marked = new Marked() + marked.setOptions({ gfm: true, breaks: true }) + let htmlContent = await marked.parse(body) + + let printableHtml = await buildPrintableHtml({ + title, + htmlContent, + theme, + preset, + }) + + openPrintWindow(printableHtml) +} diff --git a/src/routes/doc.$id.index.tsx b/src/routes/doc.$id.index.tsx index d435cf1..3012b5b 100644 --- a/src/routes/doc.$id.index.tsx +++ b/src/routes/doc.$id.index.tsx @@ -79,6 +79,7 @@ import { Button } from "@/components/ui/button" import { usePWA } from "@/lib/pwa" import { HelpMenu } from "@/components/help-menu" import { useTrackLastOpened } from "@/lib/use-track-last-opened" +import { printToPdf } from "@/lib/print-to-pdf" export { Route } let Route = createFileRoute("/doc/$id/")({ @@ -174,6 +175,9 @@ let personalMeResolve = { root: { documents: { $each: { content: true } }, settings: true, + themes: { + $each: { css: true, template: true, assets: { $each: { data: true } } }, + }, }, } as const @@ -322,9 +326,27 @@ function EditorContent({ doc, docId }: EditorContentProps) { document.documentElement.dataset.focusMode = String(!current) }, openFind: () => editor.current?.openFind(), + onPrintPdf: () => { + void printToPdf({ + content, + themes: me.$isLoaded ? me.root?.themes : undefined, + defaultPreviewTheme: me.$isLoaded + ? (me.root?.settings?.defaultPreviewTheme ?? null) + : null, + }) + }, docWithContent, }) - }, [navigate, docId, toggleLeft, toggleRight, docWithContent, editor]) + }, [ + navigate, + docId, + toggleLeft, + toggleRight, + content, + me, + docWithContent, + editor, + ]) let allDocs = getPersonalDocs(me) diff --git a/src/routes/doc.$id.preview.tsx b/src/routes/doc.$id.preview.tsx index 79585f2..f43a0d7 100644 --- a/src/routes/doc.$id.preview.tsx +++ b/src/routes/doc.$id.preview.tsx @@ -1,7 +1,8 @@ +import { useEffect } from "react" import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" -import { useCoState } from "jazz-tools/react" +import { useCoState, useAccount } from "jazz-tools/react" import { type ResolveQuery } from "jazz-tools" -import { Document } from "@/schema" +import { Document, UserAccount } from "@/schema" import { getDocumentTitle } from "@/lib/document-utils" import { altModKey } from "@/lib/platform" import { EllipsisIcon, Pencil } from "lucide-react" @@ -27,6 +28,7 @@ import { useDocTitles, type ResolvedDoc, } from "@/lib/doc-resolver" +import { printToPdf } from "@/lib/print-to-pdf" export { Route } @@ -35,6 +37,15 @@ let resolve = { assets: { $each: { image: true } }, } as const satisfies ResolveQuery +let themesResolve = { + root: { + settings: true, + themes: { + $each: { css: true, template: true, assets: { $each: { data: true } } }, + }, + }, +} as const + let Route = createFileRoute("/doc/$id/preview")({ loader: async ({ params }) => { let doc = await Document.load(params.id, { @@ -68,6 +79,7 @@ function PreviewPage() { let navigate = useNavigate() let subscribedDoc = useCoState(Document, id, { resolve }) + let meWithThemes = useAccount(UserAccount, { resolve: themesResolve }) // Extract content for wikilinks (use loader data as fallback, empty if neither) let content = @@ -76,6 +88,24 @@ function PreviewPage() { let wikilinkIds = parseWikiLinks(content).map(w => w.id) let wikilinkCache = useDocTitles(wikilinkIds, data.wikilinkCache) + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (!(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return + if (e.key.toLowerCase() !== "p") return + e.preventDefault() + void printToPdf({ + content, + themes: meWithThemes.$isLoaded ? meWithThemes.root?.themes : undefined, + defaultPreviewTheme: meWithThemes.$isLoaded + ? (meWithThemes.root?.settings?.defaultPreviewTheme ?? null) + : null, + }) + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [content, meWithThemes]) + // Error states from loader if (!data.doc) { if (data.loadingState === "unauthorized") return diff --git a/src/routes/spaces.$spaceId.doc.$id.index.tsx b/src/routes/spaces.$spaceId.doc.$id.index.tsx index a67f375..cc44840 100644 --- a/src/routes/spaces.$spaceId.doc.$id.index.tsx +++ b/src/routes/spaces.$spaceId.doc.$id.index.tsx @@ -81,6 +81,7 @@ import { Button } from "@/components/ui/button" import { usePWA } from "@/lib/pwa" import { HelpMenu } from "@/components/help-menu" import { useTrackLastOpened } from "@/lib/use-track-last-opened" +import { printToPdf } from "@/lib/print-to-pdf" export { Route } @@ -339,9 +340,27 @@ function SpaceEditorContent({ document.documentElement.dataset.focusMode = String(!current) }, openFind: () => editor.current?.openFind(), + onPrintPdf: () => { + void printToPdf({ + content, + themes: me.$isLoaded ? me.root?.themes : undefined, + defaultPreviewTheme: me.$isLoaded + ? (me.root?.settings?.defaultPreviewTheme ?? null) + : null, + }) + }, docWithContent, }) - }, [navigate, docId, toggleLeft, toggleRight, docWithContent, editor]) + }, [ + navigate, + docId, + toggleLeft, + toggleRight, + content, + me, + docWithContent, + editor, + ]) let allDocs = getSpaceDocs(space) let spaceDocs = space.documents?.$isLoaded ? space.documents : null @@ -641,6 +660,9 @@ let spaceMeResolve = { root: { documents: { $each: { content: true } }, settings: true, + themes: { + $each: { css: true, template: true, assets: { $each: { data: true } } }, + }, }, } as const