diff --git a/package.json b/package.json --- a/package.json +++ b/package.json @@ -34,6 +34,9 @@ "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/libre-franklin": "^5.2.8", "@fontsource-variable/space-grotesk": "^5.2.10", + "@fontsource/noto-sans-jp": "^5.2.9", + "@fontsource/noto-sans-kr": "^5.2.9", + "@fontsource/noto-sans-sc": "^5.2.9", "@lezer/highlight": "^1.2.3", "@lezer/markdown": "^1.6.3", "@react-pdf/renderer": "^4.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,15 @@ '@fontsource-variable/space-grotesk': specifier: ^5.2.10 version: 5.2.10 + '@fontsource/noto-sans-jp': + specifier: ^5.2.9 + version: 5.2.9 + '@fontsource/noto-sans-kr': + specifier: ^5.2.9 + version: 5.2.9 + '@fontsource/noto-sans-sc': + specifier: ^5.2.9 + version: 5.2.9 '@lezer/highlight': specifier: ^1.2.3 version: 1.2.3 @@ -659,6 +668,15 @@ '@fontsource-variable/space-grotesk@5.2.10': resolution: {integrity: sha512-yJQO/o35/hAP3CFnpdFTwQku2yzJOae2HIpBmqkOVoxhhXJaQP3g+b6Jrz7u+eI7A5ZdCIf88uMWpBJdFiGr5w==} + + '@fontsource/noto-sans-jp@5.2.9': + resolution: {integrity: sha512-6yVGiofnrnbiJjU8ch4JGSs2HqyozxMvsVyVmFwOnDH3u//qQKLqmKM4n0lqN0637uaJNzUW9nIJqbbMgHVQzw==} + + '@fontsource/noto-sans-kr@5.2.9': + resolution: {integrity: sha512-pbF8F0rUzKkDmnk/KYGAbxjgW4TrIHdFKnV6OQ+kUC9ELCn5utrAURcEgMqq3J9SRX+FvjOz+aBCmfcC8/r7cw==} + + '@fontsource/noto-sans-sc@5.2.9': + resolution: {integrity: sha512-bTUIWGBgJDpwi5qAr+x0/lcgv80IHTB9vl6s2f6EymZEa7qYV99yNRBZuKFT+SYDKVunZrjCEhWtpxqmbXWl5Q==} '@iconify-json/bi@1.2.7': resolution: {integrity: sha512-IPz8WNxmLkH1I9msl+0Q4OnmjjvP4uU0Z61a4i4sqonB6vKSbMGUWuGn8/YuuszlReVj8rf+3gNv5JU8Xoljyg==} @@ -3576,6 +3594,12 @@ '@fontsource-variable/libre-franklin@5.2.8': {} '@fontsource-variable/space-grotesk@5.2.10': {} + + '@fontsource/noto-sans-jp@5.2.9': {} + + '@fontsource/noto-sans-kr@5.2.9': {} + + '@fontsource/noto-sans-sc@5.2.9': {} '@iconify-json/bi@1.2.7': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,5 @@ - dprint - esbuild - fsevents + - sharp + - workerd diff --git a/src/types.ts b/src/types.ts --- a/src/types.ts +++ b/src/types.ts @@ -8,6 +8,7 @@ | "IBM Plex Mono" | "IBM Plex Sans Variable" | "IBM Plex Serif" + | "Maple Mono" | "Monaspace Argon" | "Monaspace Krypton" | "Monaspace Neon" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,14 @@ with: tag_name: ${{ github.ref_name }} name: 'Writer ${{ github.ref_name }}' - body: 'See the assets to download and install this version.' + body: | + See the assets to download and install this version. + + **macOS users:** The app is not yet signed/notarized. macOS will block it on first launch. To fix: + 1. Open **System Settings → Privacy & Security**, scroll down, and click **Open Anyway**, or + 2. Run `xattr -c Writer*.dmg` in Terminal before opening the DMG. + + Signing is in progress — this will be resolved in a future release. draft: true prerelease: false diff --git a/docs/tasks/image-handling.md b/docs/tasks/image-handling.md --- a/docs/tasks/image-handling.md +++ b/docs/tasks/image-handling.md @@ -119,7 +119,7 @@ - Render `` with `maxWidth: 100%`, preserve aspect ratio - [ ] Update `usePdfExport.tsx` to preload images before render - Scan PdfNodes for Image variants, resolve all paths, then render -- [ ] Handle SVG gracefully — skip or render placeholder if `@react-pdf/renderer` doesn't support it +- [ ] Handle SVG gracefully —> convert to PNG on backend, then embed and render ## Test/QA Plan diff --git a/docs/tasks/parking-lot.md b/docs/tasks/parking-lot.md --- a/docs/tasks/parking-lot.md +++ b/docs/tasks/parking-lot.md @@ -2,18 +2,51 @@ title: "Parking Lot" description: > A collection of ideas/proposals for new features and quick bug notes. -updated: 2026-03-21 +updated: 2026-03-23 --- -1. **Outline utilization** +1. **CJK Font Support** ✅ + + Maple Mono (4 TTF weights, ~1.1 MB bundled) is now the CJK-capable font. `@react-pdf/renderer` has no fallback chains[^4] — CJK fonts must be registered and selected explicitly. + + **Remaining:** Noto Sans CJK SC (~16 MB) — requires on-demand Tauri download to avoid bundle bloat. Noto SC/JP/KR woff2 subsets are loaded for editor display only via `@fontsource`. + + - [x] Bundle Maple Mono TTF in `public/fonts/` (`maple-mono-{400,700}-{normal,italic}.ttf`) + - [x] Extend `FontName` (`src/pdf/types.ts`), `EditorFontFamily` (`src/types.ts`), `FONT_PATHS` + `BUILTIN_FONT_FAMILY_MAP` (`src/pdf/fonts.ts`) + - [x] Add `@font-face` rules in `src/styles/fonts.css`; import Noto SC/JP/KR subsets for editor + - [x] Add `FontConfig` entries in `src/pdf/fonts.ts` + - [x] Add Maple Mono to `EDITOR_FONT_OPTIONS` (`FontRows.tsx`) + - [x] `hasCjkContent` + `resolvePdfFont` in `src/pdf/fonts.ts`; auto-switch to Maple Mono when Latin-only font + CJK detected; `PdfCjkWarning` banner in ExportOptions + - [ ] Noto Sans CJK SC PDF support — on-demand Tauri download command (separate ticket) + +2. **Corrupted DMG / Gatekeeper quarantine**[^5][^6] + + `release.yml` passes no Apple signing secrets to `tauri-action`[^7]; `tauri.conf.json` has no `macOS` signing block. Gatekeeper quarantines the unsigned DMG. + + **Subtasks:** + - [ ] Obtain Apple Developer credentials (Developer ID cert `.p12`, Apple ID, app-specific password, Team ID) + - [ ] Add GitHub secrets: `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID` + - [ ] Pass secrets as env vars to `tauri-apps/tauri-action` in `release.yml` + - [ ] Add `bundle.macOS.signingIdentity` to `tauri.conf.json` + - [ ] Verify with `spctl --assess` after a test release + - [ ] Interim: add `xattr -c` workaround to release notes +3. **Outline utilization** - Use Rust-generated `metadata.outline` from `markdown_render` in the UI for document structure navigation/jump-to-heading behavior -2. **Perf** +4. **Perf** - Incremental render scheduling (debounce, worker thread) - Indexing in background with progress events with UI feedback -3. **Recovery** +5. **Recovery** - Corrupt settings/workspace → app resets safely - Missing location root → UI prompts to relink/remove --- - We should let users distinctly pick editor and PDF font in settings and export + +[^1]: [Noto Sans CJK — Google Fonts](https://fonts.google.com/noto/specimen/Noto+Sans+SC) +[^2]: [Maple Mono — GitHub](https://github.com/subframe7536/maple-font) +[^3]: [IBM Plex CJK coverage gap](https://github.com/IBM/plex/issues/148) — IBM Plex does not include CJK glyphs. +[^4]: [`@react-pdf/renderer` font registration — no fallback chain](https://react-pdf.org/fonts#register) +[^5]: [Apple Gatekeeper and notarization](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) +[^6]: [`xattr -c` workaround for quarantined DMGs](https://support.apple.com/en-us/102445) +[^7]: [`tauri-apps/tauri-action` signing docs](https://v2.tauri.app/distribute/sign/macos/) diff --git a/public/fonts/maple-mono-400-italic.ttf b/public/fonts/maple-mono-400-italic.ttf new file mode 100644 --- /dev/null +++ b/public/fonts/maple-mono-400-italic.ttf diff --git a/public/fonts/maple-mono-400-normal.ttf b/public/fonts/maple-mono-400-normal.ttf new file mode 100644 --- /dev/null +++ b/public/fonts/maple-mono-400-normal.ttf diff --git a/public/fonts/maple-mono-700-italic.ttf b/public/fonts/maple-mono-700-italic.ttf new file mode 100644 --- /dev/null +++ b/public/fonts/maple-mono-700-italic.ttf diff --git a/public/fonts/maple-mono-700-normal.ttf b/public/fonts/maple-mono-700-normal.ttf new file mode 100644 --- /dev/null +++ b/public/fonts/maple-mono-700-normal.ttf diff --git a/src/__tests__/Editor.test.tsx b/src/__tests__/Editor.test.tsx --- a/src/__tests__/Editor.test.tsx +++ b/src/__tests__/Editor.test.tsx @@ -3,6 +3,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { Editor } from "../components/Editor"; +const makeClipboardData = (items: { type: string; file?: File }[]) => ({ + items: items.map(({ type, file }) => ({ type, getAsFile: () => file ?? null })), +}); + describe(Editor, () => { const mockOnChange = vi.fn(); @@ -291,10 +295,6 @@ }); describe("image paste handling", () => { - function makeClipboardData(items: { type: string; file?: File }[]) { - return { items: items.map(({ type, file }) => ({ type, getAsFile: () => file ?? null })) }; - } - it("calls onImageFilePaste with image File when image is pasted", () => { const onImageFilePaste = vi.fn(); render(); diff --git a/src/__tests__/useEditorImageHandlers.test.ts b/src/__tests__/useEditorImageHandlers.test.ts --- a/src/__tests__/useEditorImageHandlers.test.ts +++ b/src/__tests__/useEditorImageHandlers.test.ts @@ -27,6 +27,9 @@ await act(async () => { result.current.handleImageFilePaste(file); + await new Promise((r) => { + setTimeout(r, 0); + }); }); expect(writeFile).not.toHaveBeenCalled(); @@ -40,6 +43,9 @@ await act(async () => { result.current.handleImageFilePaste(file); + await new Promise((r) => { + setTimeout(r, 0); + }); }); expect(writeFile).not.toHaveBeenCalled(); @@ -56,7 +62,9 @@ await act(async () => { result.current.handleImageFilePaste(file); - await new Promise((r) => setTimeout(r, 0)); + await new Promise((r) => { + setTimeout(r, 0); + }); }); expect(writeFile).toHaveBeenCalledOnce(); @@ -78,7 +86,9 @@ await act(async () => { result.current.handleImageFilePaste(file); - await new Promise((r) => setTimeout(r, 0)); + await new Promise((r) => { + setTimeout(r, 0); + }); }); const [writtenPath] = vi.mocked(writeFile).mock.calls[0]; @@ -94,7 +104,9 @@ await act(async () => { result.current.handleImageFilePaste(file); - await new Promise((r) => setTimeout(r, 0)); + await new Promise((r) => { + setTimeout(r, 0); + }); }); expect(result.current.insertAt).toBeNull(); @@ -111,14 +123,18 @@ await act(async () => { result.current.handleImageFilePaste(file); - await new Promise((r) => setTimeout(r, 0)); + await new Promise((r) => { + setTimeout(r, 0); + }); }); expect(result.current.insertAt?.requestId).toBe(1); await act(async () => { result.current.handleImageFilePaste(file); - await new Promise((r) => setTimeout(r, 0)); + await new Promise((r) => { + setTimeout(r, 0); + }); }); expect(result.current.insertAt?.requestId).toBe(2); @@ -211,8 +227,8 @@ } as never); const editorEl = document.createElement("div"); - editorEl.setAttribute("data-testid", "editor-container"); - document.body.appendChild(editorEl); + editorEl.dataset.testid = "editor-container"; + document.body.append(editorEl); vi.spyOn(document, "elementFromPoint").mockReturnValue(editorEl); mockImportImage.mockResolvedValueOnce(".writer-assets/drop.png"); @@ -228,7 +244,7 @@ expect(mockImportImage).toHaveBeenCalledWith(2, "/home/user/drop.png"); expect(result.current.insertAt).toStrictEqual({ text: "![image](.writer-assets/drop.png)", requestId: 1 }); - document.body.removeChild(editorEl); + editorEl.remove(); }); it("skips non-image file paths in drop", async () => { @@ -241,8 +257,8 @@ } as never); const editorEl = document.createElement("div"); - editorEl.setAttribute("data-testid", "editor-container"); - document.body.appendChild(editorEl); + editorEl.dataset.testid = "editor-container"; + document.body.append(editorEl); vi.spyOn(document, "elementFromPoint").mockReturnValue(editorEl); renderHook(() => useEditorImageHandlers(1)); @@ -255,7 +271,7 @@ expect(mockImportImage).not.toHaveBeenCalled(); - document.body.removeChild(editorEl); + editorEl.remove(); }); it("ignores non-drop event types", async () => { diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx --- a/src/components/Editor.tsx +++ b/src/components/Editor.tsx @@ -97,6 +97,7 @@ "IBM Plex Sans Variable": "\"Writer IBM Plex Sans\", \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, sans-serif", "IBM Plex Serif": "\"Writer IBM Plex Serif\", \"IBM Plex Serif\", Georgia, \"Times New Roman\", serif", + "Maple Mono": "\"Writer Maple Mono\", \"Maple Mono\", \"SF Mono\", Monaco, monospace", "Monaspace Argon": "\"Writer Monaspace Argon\", \"Writer IBM Plex Mono\", monospace", "Monaspace Krypton": "\"Writer Monaspace Krypton\", \"Writer IBM Plex Mono\", monospace", "Monaspace Neon": "\"Writer Monaspace Neon\", \"Writer IBM Plex Mono\", monospace", diff --git a/src/components/Preview.tsx b/src/components/Preview.tsx --- a/src/components/Preview.tsx +++ b/src/components/Preview.tsx @@ -17,6 +17,7 @@ "IBM Plex Sans Variable": "\"Writer IBM Plex Sans\", \"IBM Plex Sans\", -apple-system, BlinkMacSystemFont, sans-serif", "IBM Plex Serif": "\"Writer IBM Plex Serif\", \"IBM Plex Serif\", Georgia, \"Times New Roman\", serif", + "Maple Mono": "\"Writer Maple Mono\", \"Maple Mono\", \"SF Mono\", Monaco, monospace", "Monaspace Argon": "\"Writer Monaspace Argon\", \"Writer IBM Plex Mono\", monospace", "Monaspace Krypton": "\"Writer Monaspace Krypton\", \"Writer IBM Plex Mono\", monospace", "Monaspace Neon": "\"Writer Monaspace Neon\", \"Writer IBM Plex Mono\", monospace", diff --git a/src/hooks/usePdfExport.tsx b/src/hooks/usePdfExport.tsx --- a/src/hooks/usePdfExport.tsx +++ b/src/hooks/usePdfExport.tsx @@ -1,7 +1,7 @@ import { MarkdownPdfDocument } from "$components/export/MarkdownPdfDocument"; import { PDFError } from "$pdf/errors"; -import { describePdfFont, ensurePdfFontRegistered } from "$pdf/fonts"; -import type { FontStrategy, PdfExportOptions, PdfRenderResult } from "$pdf/types"; +import { describePdfFont, ensurePdfFontRegistered, resolvePdfFont } from "$pdf/fonts"; +import type { FontName, FontStrategy, PdfExportOptions, PdfRenderResult } from "$pdf/types"; import { renderMarkdownForPdf, runCmd } from "$ports"; import { usePdfDialogUiState, usePdfExportActions } from "$state/selectors"; import type { EditorFontFamily, Tab } from "$types"; @@ -193,7 +193,8 @@ void runCmd(renderMarkdownForPdf(docRef.location_id, docRef.rel_path, text, void 0, resolve, reject)); }); - const didExport = await exportPdf(renderResult, options, editorFontFamily); + const resolvedFont = resolvePdfFont(editorFontFamily as FontName, text); + const didExport = await exportPdf(renderResult, options, resolvedFont); if (didExport) { setPdfExportDialogOpen(false); resetPdfExport(); diff --git a/src/pdf/fonts.ts b/src/pdf/fonts.ts --- a/src/pdf/fonts.ts +++ b/src/pdf/fonts.ts @@ -46,6 +46,15 @@ { file: "ibm-plex-serif-700-italic.ttf", fontWeight: "bold", fontStyle: "italic" }, ], }, + "Maple Mono": { + family: "MapleMono", + files: [ + { file: "maple-mono-400-normal.ttf", fontWeight: "normal", fontStyle: "normal" }, + { file: "maple-mono-400-italic.ttf", fontWeight: "normal", fontStyle: "italic" }, + { file: "maple-mono-700-normal.ttf", fontWeight: "bold", fontStyle: "normal" }, + { file: "maple-mono-700-italic.ttf", fontWeight: "bold", fontStyle: "italic" }, + ], + }, "Monaspace Argon": { family: "MonaspaceArgon", files: [ @@ -101,11 +110,43 @@ "IBM Plex Mono": "Courier", "IBM Plex Sans Variable": "Helvetica", "IBM Plex Serif": "Times-Roman", + "Maple Mono": "Courier", "Monaspace Argon": "Courier", "Monaspace Krypton": "Courier", "Monaspace Neon": "Courier", "Monaspace Radon": "Courier", "Monaspace Xenon": "Courier", +}; + +const CJK_RANGE = /[\u4E00-\u9FFF\u3040-\u309F\u30A0-\u30FF\uAC00-\uD7AF]/; + +/** Returns true if the text contains any CJK (Chinese, Japanese, or Korean) characters. */ +export const hasCjkContent = (text: string): boolean => CJK_RANGE.test(text); + +/** + * Latin-only fonts that have no CJK glyph coverage. + * When CJK content is detected, the PDF renderer will substitute Maple Mono. + */ +export const LATIN_ONLY_FONT_NAMES: ReadonlySet = new Set([ + "IBM Plex Mono", + "IBM Plex Sans Variable", + "IBM Plex Serif", + "Monaspace Argon", + "Monaspace Krypton", + "Monaspace Neon", + "Monaspace Radon", + "Monaspace Xenon", +]); + +/** + * Returns the PDF font to use, auto-switching to Maple Mono when the chosen + * font has no CJK coverage but the content contains CJK characters. + */ +export const resolvePdfFont = (fontName: FontName, text: string): FontName => { + if (LATIN_ONLY_FONT_NAMES.has(fontName) && hasCjkContent(text)) { + return "Maple Mono"; + } + return fontName; }; const getBuiltinFamily = (fontName: FontName): string => BUILTIN_FONT_FAMILY_MAP[fontName] ?? "Helvetica"; diff --git a/src/pdf/types.ts b/src/pdf/types.ts --- a/src/pdf/types.ts +++ b/src/pdf/types.ts @@ -35,6 +35,7 @@ | "IBM Plex Mono" | "IBM Plex Sans Variable" | "IBM Plex Serif" + | "Maple Mono" | "Monaspace Argon" | "Monaspace Krypton" | "Monaspace Neon" diff --git a/src/styles/fonts.css b/src/styles/fonts.css --- a/src/styles/fonts.css +++ b/src/styles/fonts.css @@ -1,6 +1,12 @@ @import "@fontsource-variable/space-grotesk"; @import "@fontsource-variable/libre-franklin"; @import "@fontsource-variable/jetbrains-mono"; +@import "@fontsource/noto-sans-sc/chinese-simplified-400.css"; +@import "@fontsource/noto-sans-sc/chinese-simplified-700.css"; +@import "@fontsource/noto-sans-jp/japanese-400.css"; +@import "@fontsource/noto-sans-jp/japanese-700.css"; +@import "@fontsource/noto-sans-kr/korean-400.css"; +@import "@fontsource/noto-sans-kr/korean-700.css"; @font-face { font-family: "Writer IBM Plex Mono"; @@ -253,6 +259,38 @@ @font-face { font-family: "Writer Monaspace Xenon"; src: url("/fonts/monaspace-xenon-700-italic.otf") format("opentype"); + font-style: italic; + font-weight: 700; + font-display: swap; +} + +@font-face { + font-family: "Writer Maple Mono"; + src: url("/fonts/maple-mono-400-normal.ttf") format("truetype"); + font-style: normal; + font-weight: 400; + font-display: swap; +} + +@font-face { + font-family: "Writer Maple Mono"; + src: url("/fonts/maple-mono-400-italic.ttf") format("truetype"); + font-style: italic; + font-weight: 400; + font-display: swap; +} + +@font-face { + font-family: "Writer Maple Mono"; + src: url("/fonts/maple-mono-700-normal.ttf") format("truetype"); + font-style: normal; + font-weight: 700; + font-display: swap; +} + +@font-face { + font-family: "Writer Maple Mono"; + src: url("/fonts/maple-mono-700-italic.ttf") format("truetype"); font-style: italic; font-weight: 700; font-display: swap; diff --git a/src/components/AppLayout/LayoutSettingsPanel/FontRows.tsx b/src/components/AppLayout/LayoutSettingsPanel/FontRows.tsx --- a/src/components/AppLayout/LayoutSettingsPanel/FontRows.tsx +++ b/src/components/AppLayout/LayoutSettingsPanel/FontRows.tsx @@ -5,6 +5,7 @@ { label: "IBM Plex Mono", value: "IBM Plex Mono" }, { label: "IBM Plex Sans Variable", value: "IBM Plex Sans Variable" }, { label: "IBM Plex Serif", value: "IBM Plex Serif" }, + { label: "Maple Mono (CJK)", value: "Maple Mono" }, { label: "Monaspace Argon", value: "Monaspace Argon" }, { label: "Monaspace Krypton", value: "Monaspace Krypton" }, { label: "Monaspace Neon", value: "Monaspace Neon" }, diff --git a/src/components/export/ExportDialog/ExportDialog.tsx b/src/components/export/ExportDialog/ExportDialog.tsx --- a/src/components/export/ExportDialog/ExportDialog.tsx +++ b/src/components/export/ExportDialog/ExportDialog.tsx @@ -7,7 +7,8 @@ import { useTextExportUI } from "$hooks/useTextExport"; import { useViewportTier } from "$hooks/useViewportTier"; import { Tangled } from "$icons"; -import type { PdfExportOptions, PdfRenderResult } from "$pdf/types"; +import { resolvePdfFont } from "$pdf/fonts"; +import type { FontName, PdfExportOptions, PdfRenderResult } from "$pdf/types"; import { runCmd, stringCreate } from "$ports"; import { useAtProtoUiState, @@ -29,7 +30,7 @@ import { type ChangeEventHandler, type MouseEvent, useCallback, useEffect, useMemo, useState } from "react"; import { ExportDialogFooter, PdfExportDialogFooter } from "./ExportFooter"; import { ExportDialogHeader } from "./ExportHeader"; -import { PdfExportDialogOptions } from "./ExportOptions"; +import { PdfCjkWarning, PdfExportDialogOptions } from "./ExportOptions"; export type ExportDialogProps = { onExport: (options: PdfExportOptions) => Promise; @@ -179,13 +180,16 @@ previewResult: PdfRenderResult | null; options: PdfExportOptions; editorFontFamily: EditorFontFamily; + documentText: string; handleExportClick: () => Promise; }; function PdfExportContent( - { onCancel, showPreview, previewResult, options, editorFontFamily, handleExportClick }: PdfExportContentProps, + { onCancel, showPreview, previewResult, options, editorFontFamily, documentText, handleExportClick }: + PdfExportContentProps, ) { const { pdfExportError: error } = usePdfExportState(); + const previewFont = resolvePdfFont(editorFontFamily as FontName, documentText); return (
@@ -193,12 +197,13 @@ title="PDF Export" description="Tune pagination and typography, then export a polished, print-ready PDF." /> +
{showPreview - ? + ? : null}
@@ -572,11 +577,12 @@ resetDocxExport(); }, [resetPdfExport, resetTextExport, resetDocxExport]); - const pdfExportProps = useMemo(() => ({ showPreview, previewResult, options, editorFontFamily }), [ + const pdfExportProps = useMemo(() => ({ showPreview, previewResult, options, editorFontFamily, documentText }), [ showPreview, previewResult, options, editorFontFamily, + documentText, ]); return ( diff --git a/src/components/export/ExportDialog/ExportOptions.tsx b/src/components/export/ExportDialog/ExportOptions.tsx --- a/src/components/export/ExportDialog/ExportOptions.tsx +++ b/src/components/export/ExportDialog/ExportOptions.tsx @@ -1,6 +1,8 @@ import { ORIENTATIONS, PAGE_SIZES } from "$pdf/constants"; -import { MarginSide, Orientation, PageSize } from "$pdf/types"; +import { hasCjkContent, LATIN_ONLY_FONT_NAMES } from "$pdf/fonts"; +import { type FontName, MarginSide, Orientation, PageSize } from "$pdf/types"; import { usePdfDialogUiState } from "$state/selectors"; +import type { EditorFontFamily } from "$types"; import { type ReactNode, useCallback } from "react"; type OptionSectionProps = { title: string; description: string; children: ReactNode }; @@ -172,3 +174,16 @@ ); + +type PdfCjkWarningProps = { documentText: string; editorFontFamily: EditorFontFamily }; + +export const PdfCjkWarning = ({ documentText, editorFontFamily }: PdfCjkWarningProps) => { + const isLatinOnly = LATIN_ONLY_FONT_NAMES.has(editorFontFamily as FontName); + const hasCjk = hasCjkContent(documentText); + if (!isLatinOnly || !hasCjk) return null; + return ( +
+ CJK characters detected. PDF will use Maple Mono since {editorFontFamily} lacks CJK glyphs. +
+ ); +};