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