diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"@codemirror/view": "^6.39.14",
"@fontsource-variable/ibm-plex-sans": "^5.2.8",
"@fontsource/ibm-plex-mono": "^5.2.7",
+ "@fontsource/ibm-plex-sans": "^5.2.8",
"@fontsource/ibm-plex-serif": "^5.2.7",
"@fontsource/monaspace-argon": "^5.2.5",
"@fontsource/monaspace-krypton": "^5.2.5",
@@ -35,9 +36,9 @@
"@fontsource/monaspace-xenon": "^5.2.5",
"@lezer/highlight": "^1.2.3",
"@lezer/markdown": "^1.6.3",
+ "@react-pdf/renderer": "^4.3.0",
"@tailwindcss/vite": "^4.1.18",
"@tauri-apps/api": "^2",
- "@react-pdf/renderer": "^4.3.0",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-log": "^2.8.0",
@@ -52,6 +53,7 @@
"devDependencies": {
"@egoist/tailwindcss-icons": "^1.9.2",
"@iconify-json/ri": "^1.2.10",
+ "@tailwindcss/forms": "^0.5.11",
"@tauri-apps/cli": "^2",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@
'@fontsource/ibm-plex-mono':
specifier: ^5.2.7
version: 5.2.7
+ '@fontsource/ibm-plex-sans':
+ specifier: ^5.2.8
+ version: 5.2.8
'@fontsource/ibm-plex-serif':
specifier: ^5.2.7
version: 5.2.7
@@ -108,6 +111,9 @@
'@iconify-json/ri':
specifier: ^1.2.10
version: 1.2.10
+ '@tailwindcss/forms':
+ specifier: ^0.5.11
+ version: 0.5.11(tailwindcss@4.1.18)
'@tauri-apps/cli':
specifier: ^2
version: 2.10.0
@@ -561,6 +567,9 @@
'@fontsource/ibm-plex-mono@5.2.7':
resolution: {integrity: sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==}
+ '@fontsource/ibm-plex-sans@5.2.8':
+ resolution: {integrity: sha512-eztSXjDhPhcpxNIiGTgMebdLP9qS4rWkysuE1V7c+DjOR0qiezaiDaTwQE7bTnG5HxAY/8M43XKDvs3cYq6ZYQ==}
+
'@fontsource/ibm-plex-serif@5.2.7':
resolution: {integrity: sha512-04owmc2OQQ/DAMjXjEMJc+7V4dBVmR01aIFOg4cuqS8pQ4HbvtlYs/u6+O0vCt21EMx5M/azIbgx43iKyisEOA==}
@@ -918,6 +927,11 @@
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
+
+ '@tailwindcss/forms@0.5.11':
+ resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
+ peerDependencies:
+ tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
'@tailwindcss/node@4.1.18':
resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==}
@@ -1627,6 +1641,10 @@
min-indent@1.0.1:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
+
+ mini-svg-data-uri@1.4.4:
+ resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
+ hasBin: true
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
@@ -2409,6 +2427,8 @@
'@fontsource/ibm-plex-mono@5.2.7': {}
+ '@fontsource/ibm-plex-sans@5.2.8': {}
+
'@fontsource/ibm-plex-serif@5.2.7': {}
'@fontsource/monaspace-argon@5.2.5': {}
@@ -2727,6 +2747,11 @@
'@swc/helpers@0.5.18':
dependencies:
tslib: 2.8.1
+
+ '@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)':
+ dependencies:
+ mini-svg-data-uri: 1.4.4
+ tailwindcss: 4.1.18
'@tailwindcss/node@4.1.18':
dependencies:
@@ -3398,6 +3423,8 @@
media-engine@1.0.3: {}
min-indent@1.0.1: {}
+
+ mini-svg-data-uri@1.4.4: {}
mlly@1.8.0:
dependencies:
diff --git a/tsconfig.json b/tsconfig.json
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
- "target": "ES2021",
+ "target": "ES2022",
"useDefineForClassFields": true,
- "lib": ["ES2021", "DOM", "DOM.Iterable"],
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
diff --git a/vite.config.ts b/vite.config.ts
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,5 +1,6 @@
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
+import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import type { ViteUserConfig } from "vitest/config";
@@ -17,6 +18,7 @@
};
const host = process.env.TAURI_DEV_HOST;
+const resolveFromRoot = (path: string) => fileURLToPath(new URL(path, import.meta.url));
// TODO: aliases here and in tsconfig.json
// - $hooks
@@ -41,12 +43,12 @@
test,
resolve: {
alias: {
- "$ports": "./src/ports.ts",
- "$types": "./src/types.ts",
- "$logger": "./src/logger.ts",
- "$icons": "./src/components/icons.tsx",
- "$pdf": "./src/pdf",
- "$components": "./src/components",
+ "$ports": resolveFromRoot("./src/ports.ts"),
+ "$types": resolveFromRoot("./src/types.ts"),
+ "$logger": resolveFromRoot("./src/logger.ts"),
+ "$icons": resolveFromRoot("./src/components/icons.tsx"),
+ "$pdf": resolveFromRoot("./src/pdf"),
+ "$components": resolveFromRoot("./src/components"),
},
},
});
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -13,7 +13,6 @@
"bundle": {
"active": true,
"targets": "all",
- "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
- "resources": { "resources/fonts/**/*": "fonts/" }
+ "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"]
}
}
diff --git a/src/App.css b/src/App.css
--- a/src/App.css
+++ b/src/App.css
@@ -1,5 +1,6 @@
@import "tailwindcss";
@plugin "@egoist/tailwindcss-icons";
+@plugin "@tailwindcss/forms";
@theme {
--font-sans:
diff --git a/src/App.tsx b/src/App.tsx
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -309,6 +309,7 @@
initialText: editorModel.text,
theme: layoutState.theme,
showLineNumbers: layoutState.lineNumbersVisible,
+ textWrappingEnabled: layoutState.textWrappingEnabled,
syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled,
fontSize: layoutState.editorFontSize,
fontFamily: layoutState.editorFontFamily,
@@ -321,6 +322,7 @@
editorModel.text,
layoutState.theme,
layoutState.lineNumbersVisible,
+ layoutState.textWrappingEnabled,
layoutState.syntaxHighlightingEnabled,
layoutState.editorFontSize,
layoutState.editorFontFamily,
@@ -394,6 +396,7 @@
topBarsCollapsed: layoutState.topBarsCollapsed,
statusBarCollapsed: layoutState.statusBarCollapsed,
lineNumbersVisible: layoutState.lineNumbersVisible,
+ textWrappingEnabled: layoutState.textWrappingEnabled,
syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled,
editorFontSize: layoutState.editorFontSize,
editorFontFamily: layoutState.editorFontFamily,
@@ -401,6 +404,7 @@
onSetTopBarsCollapsed: layoutActions.setTopBarsCollapsed,
onSetStatusBarCollapsed: layoutActions.setStatusBarCollapsed,
onSetLineNumbersVisible: layoutActions.setLineNumbersVisible,
+ onSetTextWrappingEnabled: layoutActions.setTextWrappingEnabled,
onSetSyntaxHighlightingEnabled: layoutActions.setSyntaxHighlightingEnabled,
onSetEditorFontSize: layoutActions.setEditorFontSize,
onSetEditorFontFamily: layoutActions.setEditorFontFamily,
@@ -412,6 +416,7 @@
layoutState.topBarsCollapsed,
layoutState.statusBarCollapsed,
layoutState.lineNumbersVisible,
+ layoutState.textWrappingEnabled,
layoutState.syntaxHighlightingEnabled,
layoutState.editorFontSize,
layoutState.editorFontFamily,
@@ -419,6 +424,7 @@
layoutActions.setTopBarsCollapsed,
layoutActions.setStatusBarCollapsed,
layoutActions.setLineNumbersVisible,
+ layoutActions.setTextWrappingEnabled,
layoutActions.setSyntaxHighlightingEnabled,
layoutActions.setEditorFontSize,
layoutActions.setEditorFontFamily,
@@ -437,6 +443,7 @@
charCount: charCount,
selectionCount: selectionCount,
lineNumbersVisible: layoutState.lineNumbersVisible,
+ textWrappingEnabled: layoutState.textWrappingEnabled,
syntaxHighlightingEnabled: layoutState.syntaxHighlightingEnabled,
editorFontSize: layoutState.editorFontSize,
editorFontFamily: layoutState.editorFontFamily,
@@ -457,6 +464,7 @@
charCount,
selectionCount,
layoutState.lineNumbersVisible,
+ layoutState.textWrappingEnabled,
layoutState.syntaxHighlightingEnabled,
layoutState.editorFontSize,
layoutState.editorFontFamily,
@@ -503,6 +511,7 @@
layoutActions.setTopBarsCollapsed(settings.top_bars_collapsed);
layoutActions.setStatusBarCollapsed(settings.status_bar_collapsed);
layoutActions.setLineNumbersVisible(settings.line_numbers_visible);
+ layoutActions.setTextWrappingEnabled(settings.text_wrapping_enabled);
layoutActions.setSyntaxHighlightingEnabled(settings.syntax_highlighting_enabled);
layoutActions.setEditorFontSize(settings.editor_font_size);
layoutActions.setEditorFontFamily(settings.editor_font_family);
@@ -530,6 +539,7 @@
top_bars_collapsed: layoutState.topBarsCollapsed,
status_bar_collapsed: layoutState.statusBarCollapsed,
line_numbers_visible: layoutState.lineNumbersVisible,
+ text_wrapping_enabled: layoutState.textWrappingEnabled,
syntax_highlighting_enabled: layoutState.syntaxHighlightingEnabled,
editor_font_size: layoutState.editorFontSize,
editor_font_family: layoutState.editorFontFamily,
@@ -544,6 +554,7 @@
layoutState.topBarsCollapsed,
layoutState.statusBarCollapsed,
layoutState.lineNumbersVisible,
+ layoutState.textWrappingEnabled,
layoutState.syntaxHighlightingEnabled,
layoutState.editorFontSize,
layoutState.editorFontFamily,
diff --git a/src/ports.ts b/src/ports.ts
--- a/src/ports.ts
+++ b/src/ports.ts
@@ -44,6 +44,7 @@
top_bars_collapsed: boolean;
status_bar_collapsed: boolean;
line_numbers_visible: boolean;
+ text_wrapping_enabled: boolean;
syntax_highlighting_enabled: boolean;
editor_font_size: number;
editor_font_family: EditorFontFamily;
diff --git a/public/fonts/ibm-plex-mono-400-italic.ttf b/public/fonts/ibm-plex-mono-400-italic.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-mono-400-italic.ttf
diff --git a/public/fonts/ibm-plex-mono-400-normal.ttf b/public/fonts/ibm-plex-mono-400-normal.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-mono-400-normal.ttf
diff --git a/public/fonts/ibm-plex-mono-700-italic.ttf b/public/fonts/ibm-plex-mono-700-italic.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-mono-700-italic.ttf
diff --git a/public/fonts/ibm-plex-mono-700-normal.ttf b/public/fonts/ibm-plex-mono-700-normal.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-mono-700-normal.ttf
diff --git a/public/fonts/ibm-plex-sans-400-italic.ttf b/public/fonts/ibm-plex-sans-400-italic.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-sans-400-italic.ttf
diff --git a/public/fonts/ibm-plex-sans-400-normal.ttf b/public/fonts/ibm-plex-sans-400-normal.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-sans-400-normal.ttf
diff --git a/public/fonts/ibm-plex-sans-700-italic.ttf b/public/fonts/ibm-plex-sans-700-italic.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-sans-700-italic.ttf
diff --git a/public/fonts/ibm-plex-sans-700-normal.ttf b/public/fonts/ibm-plex-sans-700-normal.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-sans-700-normal.ttf
diff --git a/public/fonts/ibm-plex-serif-400-italic.ttf b/public/fonts/ibm-plex-serif-400-italic.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-serif-400-italic.ttf
diff --git a/public/fonts/ibm-plex-serif-400-normal.ttf b/public/fonts/ibm-plex-serif-400-normal.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-serif-400-normal.ttf
diff --git a/public/fonts/ibm-plex-serif-700-italic.ttf b/public/fonts/ibm-plex-serif-700-italic.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-serif-700-italic.ttf
diff --git a/public/fonts/ibm-plex-serif-700-normal.ttf b/public/fonts/ibm-plex-serif-700-normal.ttf
new file mode 100644
--- /dev/null
+++ b/public/fonts/ibm-plex-serif-700-normal.ttf
diff --git a/public/fonts/monaspace-argon-400-italic.otf b/public/fonts/monaspace-argon-400-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-argon-400-italic.otf
diff --git a/public/fonts/monaspace-argon-400-normal.otf b/public/fonts/monaspace-argon-400-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-argon-400-normal.otf
diff --git a/public/fonts/monaspace-argon-700-italic.otf b/public/fonts/monaspace-argon-700-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-argon-700-italic.otf
diff --git a/public/fonts/monaspace-argon-700-normal.otf b/public/fonts/monaspace-argon-700-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-argon-700-normal.otf
diff --git a/public/fonts/monaspace-krypton-400-italic.otf b/public/fonts/monaspace-krypton-400-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-krypton-400-italic.otf
diff --git a/public/fonts/monaspace-krypton-400-normal.otf b/public/fonts/monaspace-krypton-400-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-krypton-400-normal.otf
diff --git a/public/fonts/monaspace-krypton-700-italic.otf b/public/fonts/monaspace-krypton-700-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-krypton-700-italic.otf
diff --git a/public/fonts/monaspace-krypton-700-normal.otf b/public/fonts/monaspace-krypton-700-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-krypton-700-normal.otf
diff --git a/public/fonts/monaspace-neon-400-italic.otf b/public/fonts/monaspace-neon-400-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-neon-400-italic.otf
diff --git a/public/fonts/monaspace-neon-400-normal.otf b/public/fonts/monaspace-neon-400-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-neon-400-normal.otf
diff --git a/public/fonts/monaspace-neon-700-italic.otf b/public/fonts/monaspace-neon-700-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-neon-700-italic.otf
diff --git a/public/fonts/monaspace-neon-700-normal.otf b/public/fonts/monaspace-neon-700-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-neon-700-normal.otf
diff --git a/public/fonts/monaspace-radon-400-italic.otf b/public/fonts/monaspace-radon-400-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-radon-400-italic.otf
diff --git a/public/fonts/monaspace-radon-400-normal.otf b/public/fonts/monaspace-radon-400-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-radon-400-normal.otf
diff --git a/public/fonts/monaspace-radon-700-italic.otf b/public/fonts/monaspace-radon-700-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-radon-700-italic.otf
diff --git a/public/fonts/monaspace-radon-700-normal.otf b/public/fonts/monaspace-radon-700-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-radon-700-normal.otf
diff --git a/public/fonts/monaspace-xenon-400-italic.otf b/public/fonts/monaspace-xenon-400-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-xenon-400-italic.otf
diff --git a/public/fonts/monaspace-xenon-400-normal.otf b/public/fonts/monaspace-xenon-400-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-xenon-400-normal.otf
diff --git a/public/fonts/monaspace-xenon-700-italic.otf b/public/fonts/monaspace-xenon-700-italic.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-xenon-700-italic.otf
diff --git a/public/fonts/monaspace-xenon-700-normal.otf b/public/fonts/monaspace-xenon-700-normal.otf
new file mode 100644
--- /dev/null
+++ b/public/fonts/monaspace-xenon-700-normal.otf
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
@@ -44,6 +44,16 @@
expect(container.querySelector(".cm-lineNumbers")).not.toBeInTheDocument();
});
+ it("should enable text wrapping by default", () => {
+ const { container } = render();
+ expect(container.querySelector(".cm-lineWrapping")).toBeInTheDocument();
+ });
+
+ it("should disable text wrapping when disabled via prop", () => {
+ const { container } = render();
+ expect(container.querySelector(".cm-lineWrapping")).not.toBeInTheDocument();
+ });
+
it("should apply custom font family and size variables", () => {
render();
const container = screen.getByTestId("editor-container");
@@ -181,6 +191,18 @@
expect(secondEditorRoot).toBeInTheDocument();
expect(secondEditorRoot).not.toBe(firstEditorRoot);
expect(container.querySelector(".cm-lineNumbers")).not.toBeInTheDocument();
+ expect(container.querySelector(".cm-content")).toHaveTextContent("Persistent");
+ });
+
+ it("recreates the editor view when text wrapping changes", () => {
+ const { container, rerender } = render();
+ const firstEditorRoot = container.querySelector(".cm-editor");
+ rerender();
+
+ const secondEditorRoot = container.querySelector(".cm-editor");
+ expect(secondEditorRoot).toBeInTheDocument();
+ expect(secondEditorRoot).not.toBe(firstEditorRoot);
+ expect(container.querySelector(".cm-lineWrapping")).not.toBeInTheDocument();
expect(container.querySelector(".cm-content")).toHaveTextContent("Persistent");
});
diff --git a/src/__tests__/appStore.test.ts b/src/__tests__/appStore.test.ts
--- a/src/__tests__/appStore.test.ts
+++ b/src/__tests__/appStore.test.ts
@@ -151,6 +151,7 @@
expect(layoutState.current.topBarsCollapsed).toBeFalsy();
expect(layoutState.current.statusBarCollapsed).toBeFalsy();
expect(layoutState.current.lineNumbersVisible).toBeTruthy();
+ expect(layoutState.current.textWrappingEnabled).toBeTruthy();
expect(layoutState.current.syntaxHighlightingEnabled).toBeTruthy();
expect(layoutState.current.editorFontSize).toBe(16);
expect(layoutState.current.editorFontFamily).toBe("IBM Plex Mono");
@@ -162,6 +163,7 @@
layoutActions.current.toggleTabBarCollapsed();
layoutActions.current.toggleStatusBarCollapsed();
layoutActions.current.toggleLineNumbersVisible();
+ layoutActions.current.toggleTextWrappingEnabled();
layoutActions.current.toggleSyntaxHighlightingEnabled();
layoutActions.current.setEditorFontSize(20);
layoutActions.current.setEditorFontFamily("Monaspace Neon");
@@ -175,6 +177,7 @@
expect(layoutState.current.topBarsCollapsed).toBeTruthy();
expect(layoutState.current.statusBarCollapsed).toBeTruthy();
expect(layoutState.current.lineNumbersVisible).toBeFalsy();
+ expect(layoutState.current.textWrappingEnabled).toBeFalsy();
expect(layoutState.current.syntaxHighlightingEnabled).toBeFalsy();
expect(layoutState.current.editorFontSize).toBe(20);
expect(layoutState.current.editorFontFamily).toBe("Monaspace Neon");
diff --git a/src/__tests__/pdfFonts.test.ts b/src/__tests__/pdfFonts.test.ts
new file mode 100644
--- /dev/null
+++ b/src/__tests__/pdfFonts.test.ts
@@ -0,0 +1,74 @@
+import { describePdfFont, ensurePdfFontRegistered } from "$pdf/fonts";
+import { Font } from "@react-pdf/renderer";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("@react-pdf/renderer", () => ({
+ Font: {
+ register: vi.fn(),
+ load: vi.fn(async () => {}),
+ },
+}));
+
+describe("pdf fonts", () => {
+ const fetchMock = vi.fn(async () =>
+ new Response(new Uint8Array([0x00, 0x01, 0x00, 0x00, 0x00, 0x00]), {
+ status: 200,
+ headers: { "content-type": "font/ttf" },
+ }));
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ vi.stubGlobal("fetch", fetchMock);
+ });
+
+ afterEach(() => {
+ vi.unstubAllGlobals();
+ });
+
+ it("registers static IBM Plex Sans with italic support", async () => {
+ await ensurePdfFontRegistered("IBM Plex Sans Variable", "custom");
+
+ expect(Font.register).toHaveBeenCalledTimes(1);
+ expect(Font.register).toHaveBeenCalledWith({
+ family: "IBMPlexSans",
+ fonts: expect.arrayContaining([
+ expect.objectContaining({
+ fontWeight: "normal",
+ fontStyle: "italic",
+ src: expect.stringMatching(/^data:font\/ttf;base64,/),
+ }),
+ expect.objectContaining({
+ fontWeight: "bold",
+ fontStyle: "italic",
+ src: expect.stringMatching(/^data:font\/ttf;base64,/),
+ }),
+ ]),
+ });
+ expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining("/fonts/ibm-plex-sans-400-normal.ttf"), {
+ method: "GET",
+ });
+ expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining("/fonts/ibm-plex-sans-700-italic.ttf"), {
+ method: "GET",
+ });
+ });
+
+ it("describes custom font sources for export diagnostics", () => {
+ const description = describePdfFont("Monaspace Xenon", "custom");
+
+ expect(description.family).toBe("MonaspaceXenon");
+ expect(description.sources).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ file: "monaspace-xenon-400-italic.otf",
+ fontStyle: "italic",
+ fontWeight: "normal",
+ }),
+ expect.objectContaining({
+ file: "monaspace-xenon-700-italic.otf",
+ fontStyle: "italic",
+ fontWeight: "bold",
+ }),
+ ]),
+ );
+ });
+});
diff --git a/src/__tests__/ports.test.ts b/src/__tests__/ports.test.ts
--- a/src/__tests__/ports.test.ts
+++ b/src/__tests__/ports.test.ts
@@ -664,6 +664,7 @@
top_bars_collapsed: false,
status_bar_collapsed: true,
line_numbers_visible: false,
+ text_wrapping_enabled: true,
syntax_highlighting_enabled: false,
editor_font_size: 18,
editor_font_family: "Monaspace Neon",
@@ -680,6 +681,7 @@
top_bars_collapsed: false,
status_bar_collapsed: true,
line_numbers_visible: false,
+ text_wrapping_enabled: true,
syntax_highlighting_enabled: false,
editor_font_size: 18,
editor_font_family: "Monaspace Neon",
diff --git a/src/__tests__/usePdfExport.test.tsx b/src/__tests__/usePdfExport.test.tsx
new file mode 100644
--- /dev/null
+++ b/src/__tests__/usePdfExport.test.tsx
@@ -0,0 +1,129 @@
+import { logger } from "$logger";
+import { DEFAULT_OPTIONS } from "$pdf/constants";
+import { describePdfFont, ensurePdfFontRegistered } from "$pdf/fonts";
+import type { PdfRenderResult } from "$pdf/types";
+import { save } from "@tauri-apps/plugin-dialog";
+import { writeFile } from "@tauri-apps/plugin-fs";
+import { pdf } from "@react-pdf/renderer";
+import { act, renderHook } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { usePdfExport } from "../hooks/usePdfExport";
+
+vi.mock("$components/pdf/MarkdownPdfDocument", () => ({
+ MarkdownPdfDocument: () => null,
+}));
+
+vi.mock("$logger", () => ({
+ logger: {
+ init: vi.fn(),
+ trace: vi.fn(),
+ debug: vi.fn(),
+ info: vi.fn(),
+ warn: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
+vi.mock("$pdf/fonts", () => ({
+ ensurePdfFontRegistered: vi.fn(),
+ describePdfFont: vi.fn((fontName: string, strategy: string) => ({
+ fontName,
+ strategy,
+ family: `${fontName}-${strategy}`,
+ sources: strategy === "custom" ? [{ file: "font.woff", src: "/fonts/font.woff", fontWeight: "normal" }] : [],
+ })),
+}));
+
+vi.mock("@tauri-apps/plugin-dialog", () => ({
+ save: vi.fn(),
+}));
+
+vi.mock("@tauri-apps/plugin-fs", () => ({
+ writeFile: vi.fn(),
+}));
+
+const toBlobMock = vi.fn();
+
+vi.mock("@react-pdf/renderer", () => ({
+ Font: { register: vi.fn() },
+ Document: "Document",
+ Page: "Page",
+ Text: "Text",
+ View: "View",
+ StyleSheet: { create: (styles: unknown) => styles },
+ pdf: vi.fn(() => ({ toBlob: toBlobMock })),
+}));
+
+const renderResult: PdfRenderResult = {
+ title: "Doc",
+ word_count: 2,
+ nodes: [{ type: "paragraph", content: "Hello world" }],
+};
+
+describe(usePdfExport, () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ toBlobMock.mockReset();
+ });
+
+ it("retries with built-in fonts on any custom render failure", async () => {
+ toBlobMock
+ .mockRejectedValueOnce(new Error("Custom font failure without known substrings"))
+ .mockResolvedValueOnce(new Blob(["pdf-bytes"], { type: "application/pdf" }));
+ vi.mocked(save).mockResolvedValue("/tmp/output.pdf");
+
+ const { result } = renderHook(() => usePdfExport());
+
+ let didExport = false;
+ await act(async () => {
+ didExport = await result.current.exportPdf(renderResult, DEFAULT_OPTIONS, "IBM Plex Sans Variable");
+ });
+
+ expect(didExport).toBeTruthy();
+ expect(pdf).toHaveBeenCalledTimes(2);
+ expect(vi.mocked(ensurePdfFontRegistered)).toHaveBeenNthCalledWith(1, "IBM Plex Sans Variable", "custom");
+ expect(vi.mocked(ensurePdfFontRegistered)).toHaveBeenNthCalledWith(2, "IBM Plex Mono", "custom");
+ expect(vi.mocked(ensurePdfFontRegistered)).toHaveBeenNthCalledWith(3, "IBM Plex Sans Variable", "builtin");
+ expect(vi.mocked(ensurePdfFontRegistered)).toHaveBeenNthCalledWith(4, "IBM Plex Mono", "builtin");
+ expect(vi.mocked(writeFile)).toHaveBeenCalledOnce();
+ expect(vi.mocked(logger.warn)).toHaveBeenCalledWith(
+ "PDF export custom font render failed; retrying with built-in fonts",
+ expect.objectContaining({
+ editorFontFamily: "IBM Plex Sans Variable",
+ error: expect.objectContaining({ message: "Custom font failure without known substrings" }),
+ }),
+ );
+ expect(vi.mocked(logger.warn)).toHaveBeenCalledWith(
+ "PDF export completed with built-in fonts after custom font failure",
+ expect.objectContaining({
+ customError: expect.objectContaining({ message: "Custom font failure without known substrings" }),
+ }),
+ );
+ expect(vi.mocked(describePdfFont)).toHaveBeenCalled();
+ });
+
+ it("fails with combined diagnostics when both custom and built-in rendering fail", async () => {
+ toBlobMock.mockRejectedValueOnce(new Error("custom failed")).mockRejectedValueOnce(new Error("builtin failed"));
+
+ const { result } = renderHook(() => usePdfExport());
+
+ await act(async () => {
+ await expect(result.current.exportPdf(renderResult, DEFAULT_OPTIONS, "IBM Plex Sans Variable")).rejects.toThrow(
+ "Failed to render PDF using both custom and built-in fonts. Check logs for details.",
+ );
+ });
+
+ expect(vi.mocked(save)).not.toHaveBeenCalled();
+ expect(result.current.state.isExporting).toBeFalsy();
+ expect(result.current.state.error).toBe(
+ "Failed to render PDF using both custom and built-in fonts. Check logs for details.",
+ );
+ expect(vi.mocked(logger.error)).toHaveBeenCalledWith(
+ "PDF export failed with both custom and built-in fonts",
+ expect.objectContaining({
+ customError: expect.objectContaining({ message: "custom failed" }),
+ builtinError: expect.objectContaining({ message: "builtin failed" }),
+ }),
+ );
+ });
+});
diff --git a/src/components/Editor.tsx b/src/components/Editor.tsx
--- a/src/components/Editor.tsx
+++ b/src/components/Editor.tsx
@@ -17,6 +17,7 @@
theme?: EditorTheme;
disabled?: boolean;
showLineNumbers?: boolean;
+ textWrappingEnabled?: boolean;
syntaxHighlightingEnabled?: boolean;
fontSize?: number;
fontFamily?: EditorFontFamily;
@@ -36,6 +37,7 @@
theme: EditorTheme;
disabled: boolean;
showLineNumbers: boolean;
+ textWrappingEnabled: boolean;
syntaxHighlightingEnabled: boolean;
placeholder?: string;
updateListener: ReturnType;
@@ -55,8 +57,17 @@
};
function createEditorState(
- { doc, theme, disabled, showLineNumbers, syntaxHighlightingEnabled, placeholder, updateListener, onSave }:
- CreateEditorStateOptions,
+ {
+ doc,
+ theme,
+ disabled,
+ showLineNumbers,
+ textWrappingEnabled,
+ syntaxHighlightingEnabled,
+ placeholder,
+ updateListener,
+ onSave,
+ }: CreateEditorStateOptions,
): CMEditorState {
const themeExtension = theme === "dark" ? oxocarbonDark : oxocarbonLight;
@@ -73,6 +84,7 @@
doc,
extensions: [
...(showLineNumbers ? [lineNumbers()] : []),
+ ...(textWrappingEnabled ? [EditorView.lineWrapping] : []),
history(),
...(syntaxHighlightingEnabled ? [markdown({ base: markdownLanguage, codeLanguages: [], addKeymap: true })] : []),
themeExtension,
@@ -91,6 +103,7 @@
theme = "dark",
disabled = false,
showLineNumbers = true,
+ textWrappingEnabled = true,
syntaxHighlightingEnabled = true,
fontSize = 16,
fontFamily = "IBM Plex Mono",
@@ -109,7 +122,14 @@
const debounceMsRef = useRef(debounceMs);
const onChangeTimeoutRef = useRef | null>(null);
const initialTextRef = useRef(initialText);
- const presentationRef = useRef({ theme, disabled, showLineNumbers, syntaxHighlightingEnabled, placeholder });
+ const presentationRef = useRef({
+ theme,
+ disabled,
+ showLineNumbers,
+ textWrappingEnabled,
+ syntaxHighlightingEnabled,
+ placeholder,
+ });
const [isReady, setIsReady] = useState(false);
useEffect(() => {
@@ -168,6 +188,7 @@
theme: currentPresentation.theme,
disabled: currentPresentation.disabled,
showLineNumbers: currentPresentation.showLineNumbers,
+ textWrappingEnabled: currentPresentation.textWrappingEnabled,
syntaxHighlightingEnabled: currentPresentation.syntaxHighlightingEnabled,
placeholder: currentPresentation.placeholder,
updateListener: createUpdateListener(),
@@ -214,13 +235,21 @@
previousPresentation.theme === theme
&& previousPresentation.disabled === disabled
&& previousPresentation.showLineNumbers === showLineNumbers
+ && previousPresentation.textWrappingEnabled === textWrappingEnabled
&& previousPresentation.syntaxHighlightingEnabled === syntaxHighlightingEnabled
&& previousPresentation.placeholder === placeholder
) {
return;
}
- presentationRef.current = { theme, disabled, showLineNumbers, syntaxHighlightingEnabled, placeholder };
+ presentationRef.current = {
+ theme,
+ disabled,
+ showLineNumbers,
+ textWrappingEnabled,
+ syntaxHighlightingEnabled,
+ placeholder,
+ };
const view = viewRef.current;
if (!view) {
@@ -239,7 +268,7 @@
}
viewRef.current = nextView;
- }, [createView, disabled, placeholder, showLineNumbers, syntaxHighlightingEnabled, theme]);
+ }, [createView, disabled, placeholder, showLineNumbers, textWrappingEnabled, syntaxHighlightingEnabled, theme]);
useEffect(() => {
const view = viewRef.current;
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx
--- a/src/components/Toolbar.tsx
+++ b/src/components/Toolbar.tsx
@@ -185,7 +185,7 @@
shortcut="Ctrl+F" />
-
+
{onExportPdf && (
{
+ if (depth > 4) {
+ return { name: "TruncatedError", message: "Error cause chain exceeded depth limit" };
+ }
+
+ if (error instanceof Error) {
+ const withFields = error as Error & { code?: string; details?: unknown; cause?: unknown };
+ const serialized: SerializedError = { name: error.name, message: error.message, stack: error.stack };
+
+ if (withFields.code) {
+ serialized.code = withFields.code;
+ }
+ if (withFields.details) {
+ serialized.details = withFields.details;
+ }
+ if (withFields.cause) {
+ serialized.cause = serializeError(withFields.cause, depth + 1);
+ }
+ return serialized;
+ }
+
+ return { name: "UnknownError", message: String(error) };
+};
+
+const runtimeContext = () => ({
+ href: globalThis.location?.href ?? null,
+ origin: globalThis.location?.origin ?? null,
+ userAgent: globalThis.navigator?.userAgent ?? null,
+});
+
export function usePdfExport(): UsePdfExportReturn {
const [state, setState] = useState(initialState);
+
+ const renderPdfBlob = useCallback(
+ async (
+ result: PdfRenderResult,
+ options: PdfExportOptions,
+ editorFontFamily: EditorFontFamily,
+ strategy: FontStrategy,
+ ) => {
+ const bodyFont = describePdfFont(editorFontFamily, strategy);
+ const codeFont = describePdfFont("IBM Plex Mono", strategy);
+ logger.debug("PDF export render attempt started", { strategy, bodyFont, codeFont, runtime: runtimeContext() });
+
+ await ensurePdfFontRegistered(editorFontFamily, strategy);
+ await ensurePdfFontRegistered("IBM Plex Mono", strategy);
+
+ const blob = await pdf(
+ ,
+ ).toBlob();
+
+ logger.debug("PDF export render attempt completed", { strategy, outputSizeBytes: blob.size });
+
+ return blob;
+ },
+ [],
+ );
const exportPdf = useCallback(
async (result: PdfRenderResult, options: PdfExportOptions, editorFontFamily: EditorFontFamily) => {
setState({ isExporting: true, error: null });
try {
- const blob = await pdf(
- ,
- ).toBlob();
+ let blob: Blob;
+ let customRenderError: unknown = null;
+ try {
+ blob = await renderPdfBlob(result, options, editorFontFamily, "custom");
+ } catch (initialError) {
+ customRenderError = initialError;
+ logger.warn("PDF export custom font render failed; retrying with built-in fonts", {
+ editorFontFamily,
+ error: serializeError(initialError),
+ });
+
+ try {
+ blob = await renderPdfBlob(result, options, editorFontFamily, "builtin");
+ logger.warn("PDF export completed with built-in fonts after custom font failure", {
+ editorFontFamily,
+ customError: serializeError(customRenderError),
+ });
+ } catch (builtinError) {
+ logger.error("PDF export failed with both custom and built-in fonts", {
+ editorFontFamily,
+ customError: serializeError(customRenderError),
+ builtinError: serializeError(builtinError),
+ });
+ throw new Error("Failed to render PDF using both custom and built-in fonts. Check logs for details.", {
+ cause: builtinError,
+ });
+ }
+ }
const arrayBuffer = await blob.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
@@ -40,21 +132,27 @@
const filePath = await save({ filters: [{ name: "PDF", extensions: ["pdf"] }], defaultPath: defaultFileName });
if (!filePath) {
+ logger.info("PDF export canceled before writing file", {
+ editorFontFamily,
+ fallbackUsed: customRenderError !== null,
+ });
setState({ isExporting: false, error: null });
return false;
}
await writeFile(filePath, uint8Array);
+ logger.info("PDF export completed", { filePath, editorFontFamily, fallbackUsed: customRenderError !== null });
setState({ isExporting: false, error: null });
return true;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to export PDF";
+ logger.error("PDF export failed", { editorFontFamily, error: serializeError(err) });
setState({ isExporting: false, error: errorMessage });
throw err;
}
},
- [],
+ [renderPdfBlob],
);
const reset = useCallback(() => {
diff --git a/src/pdf/errors.ts b/src/pdf/errors.ts
new file mode 100644
--- /dev/null
+++ b/src/pdf/errors.ts
@@ -0,0 +1,32 @@
+// oxlint-disable max-classes-per-file
+
+import type { PdfFontResolution, PdfFontSourceDescriptor } from "./types";
+
+export class PdfFontSourceFetchError extends Error {
+ readonly code = "PDF_FONT_SOURCE_FETCH_FAILED";
+ readonly details: { source: PdfFontSourceDescriptor; status?: number; statusText?: string; contentType?: string };
+
+ constructor(
+ source: PdfFontSourceDescriptor,
+ message: string,
+ extras?: { status?: number; statusText?: string; contentType?: string; cause?: unknown },
+ ) {
+ super(message, { cause: extras?.cause instanceof Error ? extras.cause : undefined });
+ this.name = "PdfFontSourceFetchError";
+ this.details = { source, status: extras?.status, statusText: extras?.statusText, contentType: extras?.contentType };
+ }
+}
+
+export class PdfFontRegistrationError extends Error {
+ readonly code = "PDF_FONT_REGISTRATION_FAILED";
+ readonly details: PdfFontResolution;
+
+ constructor(details: PdfFontResolution, cause: unknown) {
+ const message = cause instanceof Error ? cause.message : String(cause);
+ super(`Failed to register PDF font "${details.fontName}" (${details.family}): ${message}`, {
+ cause: cause instanceof Error ? cause : undefined,
+ });
+ this.name = "PdfFontRegistrationError";
+ this.details = details;
+ }
+}
diff --git a/src/pdf/fonts.ts b/src/pdf/fonts.ts
--- a/src/pdf/fonts.ts
+++ b/src/pdf/fonts.ts
@@ -1,90 +1,273 @@
+// oxlint-disable no-await-in-loop
+
import { Font } from "@react-pdf/renderer";
-import { FontName } from "./types";
+import { PdfFontRegistrationError, PdfFontSourceFetchError } from "./errors";
+import type {
+ FontConfig,
+ FontName,
+ FontPayloadFormat,
+ FontStrategy,
+ PdfFontResolution,
+ PdfFontSourceDescriptor,
+} from "./types";
-type FontWeight =
- | number
- | "thin"
- | "ultralight"
- | "light"
- | "normal"
- | "medium"
- | "semibold"
- | "bold"
- | "ultrabold"
- | "heavy";
+const staticFont = (filename: string): string => {
+ const relativePath = `/fonts/${filename}`;
+ const origin = globalThis.location?.origin;
+ if (!origin || origin === "null") {
+ return relativePath;
+ }
+ return `${origin}${relativePath}`;
+};
-type FontSource = { src: string; fontWeight?: FontWeight };
-
-const FONT_PATHS: Record = {
+const FONT_PATHS: Record = {
"IBM Plex Mono": {
family: "IBMPlexMono",
- fonts: [{ src: "asset:///fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2", fontWeight: "normal" }, {
- src: "asset:///fonts/ibm-plex-mono/ibm-plex-mono-latin-700-normal.woff2",
- fontWeight: "bold",
- }],
+ files: [
+ { file: "ibm-plex-mono-400-normal.ttf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "ibm-plex-mono-400-italic.ttf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "ibm-plex-mono-700-normal.ttf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "ibm-plex-mono-700-italic.ttf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"IBM Plex Sans Variable": {
family: "IBMPlexSans",
- fonts: [{ src: "asset:///fonts/ibm-plex-sans/ibm-plex-sans-latin-wght-normal.woff2", fontWeight: "normal" }],
+ files: [
+ { file: "ibm-plex-sans-400-normal.ttf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "ibm-plex-sans-400-italic.ttf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "ibm-plex-sans-700-normal.ttf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "ibm-plex-sans-700-italic.ttf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"IBM Plex Serif": {
family: "IBMPlexSerif",
- fonts: [{ src: "asset:///fonts/ibm-plex-serif/ibm-plex-serif-latin-400-normal.woff2", fontWeight: "normal" }, {
- src: "asset:///fonts/ibm-plex-serif/ibm-plex-serif-latin-700-normal.woff2",
- fontWeight: "bold",
- }],
+ files: [
+ { file: "ibm-plex-serif-400-normal.ttf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "ibm-plex-serif-400-italic.ttf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "ibm-plex-serif-700-normal.ttf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "ibm-plex-serif-700-italic.ttf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"Monaspace Argon": {
family: "MonaspaceArgon",
- fonts: [{ src: "asset:///fonts/monaspace-argon/monaspace-argon-latin-400-normal.woff2", fontWeight: "normal" }],
+ files: [
+ { file: "monaspace-argon-400-normal.otf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "monaspace-argon-400-italic.otf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "monaspace-argon-700-normal.otf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "monaspace-argon-700-italic.otf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"Monaspace Krypton": {
family: "MonaspaceKrypton",
- fonts: [{ src: "asset:///fonts/monaspace-krypton/monaspace-krypton-latin-400-normal.woff2", fontWeight: "normal" }],
+ files: [
+ { file: "monaspace-krypton-400-normal.otf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "monaspace-krypton-400-italic.otf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "monaspace-krypton-700-normal.otf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "monaspace-krypton-700-italic.otf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"Monaspace Neon": {
family: "MonaspaceNeon",
- fonts: [{ src: "asset:///fonts/monaspace-neon/monaspace-neon-latin-400-normal.woff2", fontWeight: "normal" }],
+ files: [
+ { file: "monaspace-neon-400-normal.otf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "monaspace-neon-400-italic.otf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "monaspace-neon-700-normal.otf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "monaspace-neon-700-italic.otf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"Monaspace Radon": {
family: "MonaspaceRadon",
- fonts: [{ src: "asset:///fonts/monaspace-radon/monaspace-radon-latin-400-normal.woff2", fontWeight: "normal" }],
+ files: [
+ { file: "monaspace-radon-400-normal.otf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "monaspace-radon-400-italic.otf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "monaspace-radon-700-normal.otf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "monaspace-radon-700-italic.otf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
"Monaspace Xenon": {
family: "MonaspaceXenon",
- fonts: [{ src: "asset:///fonts/monaspace-xenon/monaspace-xenon-latin-400-normal.woff2", fontWeight: "normal" }],
+ files: [
+ { file: "monaspace-xenon-400-normal.otf", fontWeight: "normal", fontStyle: "normal" },
+ { file: "monaspace-xenon-400-italic.otf", fontWeight: "normal", fontStyle: "italic" },
+ { file: "monaspace-xenon-700-normal.otf", fontWeight: "bold", fontStyle: "normal" },
+ { file: "monaspace-xenon-700-italic.otf", fontWeight: "bold", fontStyle: "italic" },
+ ],
},
};
const registeredFonts = new Set();
-export function registerPdfFont(fontName: FontName): string {
- const config = FONT_PATHS[fontName];
+const preloadedFontSources = new Map();
- if (!config) {
- throw new Error(`Unknown font: ${fontName}. Available fonts: ${Object.keys(FONT_PATHS).join(", ")}`);
- }
+const BUILTIN_FONT_FAMILY_MAP: Record = {
+ "IBM Plex Mono": "Courier",
+ "IBM Plex Sans Variable": "Helvetica",
+ "IBM Plex Serif": "Times-Roman",
+ "Monaspace Argon": "Courier",
+ "Monaspace Krypton": "Courier",
+ "Monaspace Neon": "Courier",
+ "Monaspace Radon": "Courier",
+ "Monaspace Xenon": "Courier",
+};
- if (!registeredFonts.has(config.family)) {
- Font.register({ family: config.family, fonts: config.fonts });
- registeredFonts.add(config.family);
- }
+const getBuiltinFamily = (fontName: FontName): string => BUILTIN_FONT_FAMILY_MAP[fontName] ?? "Helvetica";
- return config.family;
-}
-
-export function getPdfFontFamily(fontName: FontName): string {
+const getCustomConfig = (fontName: FontName): FontConfig => {
if (!(fontName in FONT_PATHS)) {
- return registerPdfFont("IBM Plex Mono");
+ return FONT_PATHS["IBM Plex Mono"];
+ }
+ return FONT_PATHS[fontName];
+};
+
+const toHex = (value: number): string => value.toString(16).padStart(2, "0");
+
+const getFormatFromFilename = (filename: string): FontPayloadFormat | null => {
+ const lower = filename.toLowerCase();
+ if (lower.endsWith(".woff")) {
+ return "woff";
+ }
+ if (lower.endsWith(".ttf")) {
+ return "ttf";
+ }
+ if (lower.endsWith(".otf")) {
+ return "otf";
+ }
+ return null;
+};
+
+const detectFormatFromHeader = (bytes: Uint8Array): FontPayloadFormat | null => {
+ if (bytes.length < 4) {
+ return null;
+ }
+ if (bytes[0] === 0x77 && bytes[1] === 0x4f && bytes[2] === 0x46 && bytes[3] === 0x46) {
+ return "woff";
+ }
+ if (bytes[0] === 0x00 && bytes[1] === 0x01 && bytes[2] === 0x00 && bytes[3] === 0x00) {
+ return "ttf";
+ }
+ if (bytes[0] === 0x4f && bytes[1] === 0x54 && bytes[2] === 0x54 && bytes[3] === 0x4f) {
+ return "otf";
+ }
+ return null;
+};
+
+const toBase64 = (bytes: Uint8Array): string => {
+ let binary = "";
+ const chunkSize = 0x8000;
+ for (let index = 0; index < bytes.length; index += chunkSize) {
+ const chunk = bytes.subarray(index, index + chunkSize);
+ binary += String.fromCodePoint(...chunk);
+ }
+ return btoa(binary);
+};
+
+const preloadFontSource = async (source: PdfFontSourceDescriptor): Promise => {
+ const cached = preloadedFontSources.get(source.src);
+ if (cached) {
+ return cached;
}
- return registerPdfFont(fontName);
+ let response: Response;
+ try {
+ response = await fetch(source.src, { method: "GET" });
+ } catch (error) {
+ throw new PdfFontSourceFetchError(source, `Failed to fetch font source: ${source.src}`, { cause: error });
+ }
+
+ const contentType = response.headers.get("content-type") ?? undefined;
+ if (!response.ok) {
+ throw new PdfFontSourceFetchError(
+ source,
+ `Font source request failed (${response.status} ${response.statusText}) for ${source.src}`,
+ { status: response.status, statusText: response.statusText, contentType },
+ );
+ }
+
+ const bytes = new Uint8Array(await response.arrayBuffer());
+ const detectedFormat = detectFormatFromHeader(bytes);
+ const expectedFormat = getFormatFromFilename(source.file);
+ if (!detectedFormat || (expectedFormat && detectedFormat !== expectedFormat)) {
+ const signature = bytes.length >= 4
+ ? `${toHex(bytes[0])} ${toHex(bytes[1])} ${toHex(bytes[2])} ${toHex(bytes[3])}`
+ : "too-short";
+ throw new PdfFontSourceFetchError(
+ source,
+ `Font payload format mismatch (${source.src}, expected: ${expectedFormat ?? "unknown"}, detected: ${
+ detectedFormat ?? "unknown"
+ }, signature: ${signature})`,
+ { status: response.status, statusText: response.statusText, contentType },
+ );
+ }
+
+ const mimeType = detectedFormat === "woff" ? "font/woff" : detectedFormat === "ttf" ? "font/ttf" : "font/otf";
+ const dataUrl = `data:${mimeType};base64,${toBase64(bytes)}`;
+ preloadedFontSources.set(source.src, dataUrl);
+ return dataUrl;
+};
+
+export function describePdfFont(fontName: FontName, strategy: FontStrategy = "custom"): PdfFontResolution {
+ if (strategy === "builtin") {
+ return { fontName, strategy, family: getBuiltinFamily(fontName), sources: [] };
+ }
+
+ const config = getCustomConfig(fontName);
+ return {
+ fontName,
+ strategy,
+ family: config.family,
+ sources: config.files.map((file) => ({
+ file: file.file,
+ src: staticFont(file.file),
+ fontWeight: file.fontWeight,
+ fontStyle: file.fontStyle,
+ })),
+ };
}
-export function registerAllPdfFonts(): void {
- for (const [fontName] of Object.entries(FONT_PATHS)) {
- registerPdfFont(fontName as FontName);
+export async function ensurePdfFontRegistered(fontName: FontName, strategy: FontStrategy = "custom"): Promise {
+ const resolution = describePdfFont(fontName, strategy);
+ if (strategy === "builtin") {
+ return resolution.family;
+ }
+
+ if (!registeredFonts.has(resolution.family)) {
+ try {
+ const resolvedSources = await Promise.all(
+ resolution.sources.map(async (source) => ({ ...source, resolvedSrc: await preloadFontSource(source) })),
+ );
+
+ Font.register({
+ family: resolution.family,
+ fonts: resolvedSources.map((source) => ({
+ src: source.resolvedSrc,
+ fontWeight: source.fontWeight,
+ fontStyle: source.fontStyle,
+ })),
+ });
+
+ for (const source of resolvedSources) {
+ await Font.load({
+ fontFamily: resolution.family,
+ fontWeight: source.fontWeight ?? "normal",
+ fontStyle: source.fontStyle ?? "normal",
+ });
+ }
+
+ registeredFonts.add(resolution.family);
+ } catch (error) {
+ throw new PdfFontRegistrationError(resolution, error);
+ }
+ }
+
+ return resolution.family;
+}
+
+export const getPdfFontFamily = (f: FontName, s: FontStrategy = "custom"): string => describePdfFont(f, s).family;
+
+export async function registerAllPdfFonts(): Promise {
+ for (const fontName in FONT_PATHS) {
+ await ensurePdfFontRegistered(fontName as FontName);
}
}
-export const getCodeFontFamily = (): string => getPdfFontFamily("IBM Plex Mono");
+export const getCodeFontFamily = (s: FontStrategy = "custom"): string => getPdfFontFamily("IBM Plex Mono", s);
diff --git a/src/pdf/types.ts b/src/pdf/types.ts
--- a/src/pdf/types.ts
+++ b/src/pdf/types.ts
@@ -39,3 +39,34 @@
| "Monaspace Neon"
| "Monaspace Radon"
| "Monaspace Xenon";
+
+export type FontPayloadFormat = "woff" | "ttf" | "otf";
+
+export type PdfFontSourceDescriptor = { file: string; src: string; fontWeight?: FontWeight; fontStyle?: FontStyle };
+
+export type PdfFontResolution = {
+ fontName: FontName;
+ strategy: FontStrategy;
+ family: string;
+ sources: PdfFontSourceDescriptor[];
+};
+
+export type FontWeight =
+ | number
+ | "thin"
+ | "ultralight"
+ | "light"
+ | "normal"
+ | "medium"
+ | "semibold"
+ | "bold"
+ | "ultrabold"
+ | "heavy";
+
+export type FontStyle = "normal" | "italic";
+
+export type FontStrategy = "custom" | "builtin";
+
+export type FontFile = { file: string; fontWeight?: FontWeight; fontStyle?: FontStyle };
+
+export type FontConfig = { family: string; files: FontFile[] };
diff --git a/src/state/appStore.ts b/src/state/appStore.ts
--- a/src/state/appStore.ts
+++ b/src/state/appStore.ts
@@ -15,6 +15,7 @@
topBarsCollapsed: boolean;
statusBarCollapsed: boolean;
lineNumbersVisible: boolean;
+ textWrappingEnabled: boolean;
syntaxHighlightingEnabled: boolean;
editorFontSize: number;
editorFontFamily: EditorFontFamily;
@@ -34,6 +35,8 @@
toggleStatusBarCollapsed: () => void;
setLineNumbersVisible: (value: boolean) => void;
toggleLineNumbersVisible: () => void;
+ setTextWrappingEnabled: (value: boolean) => void;
+ toggleTextWrappingEnabled: () => void;
setSyntaxHighlightingEnabled: (value: boolean) => void;
toggleSyntaxHighlightingEnabled: () => void;
setEditorFontSize: (value: number) => void;
@@ -87,6 +90,7 @@
topBarsCollapsed: false,
statusBarCollapsed: false,
lineNumbersVisible: true,
+ textWrappingEnabled: true,
syntaxHighlightingEnabled: true,
editorFontSize: 16,
editorFontFamily: "IBM Plex Mono",
@@ -123,6 +127,9 @@
setLineNumbersVisible: (value) => set({ lineNumbersVisible: value }),
toggleLineNumbersVisible: () => set((state) => ({ lineNumbersVisible: !state.lineNumbersVisible })),
+
+ setTextWrappingEnabled: (value) => set({ textWrappingEnabled: value }),
+ toggleTextWrappingEnabled: () => set((state) => ({ textWrappingEnabled: !state.textWrappingEnabled })),
setSyntaxHighlightingEnabled: (value) => set({ syntaxHighlightingEnabled: value }),
toggleSyntaxHighlightingEnabled: () =>
@@ -289,6 +296,7 @@
topBarsCollapsed: state.topBarsCollapsed,
statusBarCollapsed: state.statusBarCollapsed,
lineNumbersVisible: state.lineNumbersVisible,
+ textWrappingEnabled: state.textWrappingEnabled,
syntaxHighlightingEnabled: state.syntaxHighlightingEnabled,
editorFontSize: state.editorFontSize,
editorFontFamily: state.editorFontFamily,
@@ -311,6 +319,8 @@
toggleStatusBarCollapsed: state.toggleStatusBarCollapsed,
setLineNumbersVisible: state.setLineNumbersVisible,
toggleLineNumbersVisible: state.toggleLineNumbersVisible,
+ setTextWrappingEnabled: state.setTextWrappingEnabled,
+ toggleTextWrappingEnabled: state.toggleTextWrappingEnabled,
setSyntaxHighlightingEnabled: state.setSyntaxHighlightingEnabled,
toggleSyntaxHighlightingEnabled: state.toggleSyntaxHighlightingEnabled,
setEditorFontSize: state.setEditorFontSize,
diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs
--- a/crates/store/src/lib.rs
+++ b/crates/store/src/lib.rs
@@ -38,6 +38,8 @@
#[serde(default = "default_true")]
pub line_numbers_visible: bool,
#[serde(default = "default_true")]
+ pub text_wrapping_enabled: bool,
+ #[serde(default = "default_true")]
pub syntax_highlighting_enabled: bool,
#[serde(default = "default_editor_font_size")]
pub editor_font_size: u16,
@@ -52,6 +54,7 @@
top_bars_collapsed: false,
status_bar_collapsed: false,
line_numbers_visible: true,
+ text_wrapping_enabled: true,
syntax_highlighting_enabled: true,
editor_font_size: default_editor_font_size(),
editor_font_family: default_editor_font_family(),
@@ -1598,6 +1601,7 @@
top_bars_collapsed: false,
status_bar_collapsed: true,
line_numbers_visible: false,
+ text_wrapping_enabled: false,
syntax_highlighting_enabled: false,
editor_font_size: 18,
editor_font_family: "Monaspace Neon".to_string(),
@@ -1630,6 +1634,7 @@
let loaded = store.ui_layout_get().unwrap();
assert!(loaded.line_numbers_visible);
+ assert!(loaded.text_wrapping_enabled);
assert!(loaded.syntax_highlighting_enabled);
assert_eq!(loaded.editor_font_size, 16);
assert_eq!(loaded.editor_font_family, "IBM Plex Mono");
diff --git a/src/components/layout/FocusModePanel.tsx b/src/components/layout/FocusModePanel.tsx
--- a/src/components/layout/FocusModePanel.tsx
+++ b/src/components/layout/FocusModePanel.tsx
@@ -13,6 +13,7 @@
charCount: number;
selectionCount?: number;
lineNumbersVisible: boolean;
+ textWrappingEnabled: boolean;
syntaxHighlightingEnabled: boolean;
editorFontSize: number;
editorFontFamily: EditorFontFamily;
@@ -49,6 +50,7 @@
charCount,
selectionCount,
lineNumbersVisible,
+ textWrappingEnabled,
syntaxHighlightingEnabled,
editorFontSize,
editorFontFamily,
@@ -68,6 +70,7 @@
initialText={text}
theme={theme}
showLineNumbers={lineNumbersVisible}
+ textWrappingEnabled={textWrappingEnabled}
syntaxHighlightingEnabled={syntaxHighlightingEnabled}
fontSize={editorFontSize}
fontFamily={editorFontFamily}
diff --git a/src/components/layout/LayoutSettingsPanel.tsx b/src/components/layout/LayoutSettingsPanel.tsx
--- a/src/components/layout/LayoutSettingsPanel.tsx
+++ b/src/components/layout/LayoutSettingsPanel.tsx
@@ -8,6 +8,7 @@
topBarsCollapsed: boolean;
statusBarCollapsed: boolean;
lineNumbersVisible: boolean;
+ textWrappingEnabled: boolean;
syntaxHighlightingEnabled: boolean;
editorFontSize: number;
editorFontFamily: EditorFontFamily;
@@ -15,6 +16,7 @@
onSetTopBarsCollapsed: (value: boolean) => void;
onSetStatusBarCollapsed: (value: boolean) => void;
onSetLineNumbersVisible: (value: boolean) => void;
+ onSetTextWrappingEnabled: (value: boolean) => void;
onSetSyntaxHighlightingEnabled: (value: boolean) => void;
onSetEditorFontSize: (value: number) => void;
onSetEditorFontFamily: (value: EditorFontFamily) => void;
@@ -130,6 +132,7 @@
topBarsCollapsed,
statusBarCollapsed,
lineNumbersVisible,
+ textWrappingEnabled,
syntaxHighlightingEnabled,
editorFontSize,
editorFontFamily,
@@ -137,6 +140,7 @@
onSetTopBarsCollapsed,
onSetStatusBarCollapsed,
onSetLineNumbersVisible,
+ onSetTextWrappingEnabled,
onSetSyntaxHighlightingEnabled,
onSetEditorFontSize,
onSetEditorFontFamily,
@@ -158,6 +162,10 @@
const toggleLineNumbers = useCallback(() => {
onSetLineNumbersVisible(!lineNumbersVisible);
}, [lineNumbersVisible, onSetLineNumbersVisible]);
+
+ const toggleTextWrapping = useCallback(() => {
+ onSetTextWrappingEnabled(!textWrappingEnabled);
+ }, [onSetTextWrappingEnabled, textWrappingEnabled]);
const toggleSyntaxHighlighting = useCallback(() => {
onSetSyntaxHighlightingEnabled(!syntaxHighlightingEnabled);
@@ -202,6 +210,11 @@
description="Show or hide line numbers in the editor gutter."
isVisible={lineNumbersVisible}
onToggle={toggleLineNumbers} />
+
;
+const PDF_TEXT_COLOR = "#161616";
+const PDF_MUTED_TEXT_COLOR = "#525252";
+const PDF_PAGE_BACKGROUND = "#ffffff";
const toPdfPageSize = (size: PageSize): PageSize => {
if (typeof size === "string") {
@@ -39,27 +42,37 @@
paddingBottom: options.margins.bottom,
paddingLeft: options.margins.left,
fontFamily: bodyFont,
+ color: PDF_TEXT_COLOR,
+ backgroundColor: PDF_PAGE_BACKGROUND,
},
header: { marginBottom: 12, borderBottomWidth: 1, borderBottomColor: "#e0e0e0", paddingBottom: 8 },
- footer: { fontSize: baseFontSize - 3, color: "#666", textAlign: "center" },
+ footer: { fontSize: baseFontSize - 3, color: PDF_MUTED_TEXT_COLOR, textAlign: "center" },
content: { flexGrow: 1 },
- title: { fontSize: 24, marginBottom: 20, fontWeight: "bold" },
- heading1: { fontSize: 20, marginTop: 20, marginBottom: 10, fontWeight: "bold" },
- heading2: { fontSize: 16, marginTop: 15, marginBottom: 8, fontWeight: "bold" },
- heading3: { fontSize: 14, marginTop: 12, marginBottom: 6, fontWeight: "bold" },
- paragraph: { fontSize: baseFontSize, lineHeight, marginBottom: 10 },
+ title: { fontSize: 24, marginBottom: 20, fontWeight: "bold", color: PDF_TEXT_COLOR },
+ heading1: { fontSize: 20, marginTop: 20, marginBottom: 10, fontWeight: "bold", color: PDF_TEXT_COLOR },
+ heading2: { fontSize: 16, marginTop: 15, marginBottom: 8, fontWeight: "bold", color: PDF_TEXT_COLOR },
+ heading3: { fontSize: 14, marginTop: 12, marginBottom: 6, fontWeight: "bold", color: PDF_TEXT_COLOR },
+ paragraph: { fontSize: baseFontSize, lineHeight, marginBottom: 10, color: PDF_TEXT_COLOR },
code: {
fontFamily: codeFont,
fontSize: baseFontSize - 2,
backgroundColor: "#f4f4f4",
+ color: PDF_TEXT_COLOR,
padding: 8,
marginBottom: 10,
},
list: { marginLeft: 20, marginBottom: 10 },
- listItem: { fontSize: baseFontSize, lineHeight, marginBottom: 4 },
+ listItem: { fontSize: baseFontSize, lineHeight, marginBottom: 4, color: PDF_TEXT_COLOR },
blockquote: { marginLeft: 20, paddingLeft: 10, borderLeftWidth: 2, borderLeftColor: "#ddd", marginBottom: 10 },
- blockquoteText: { fontSize: baseFontSize, lineHeight, fontStyle: "italic", color: "#666" },
- footnote: { fontSize: baseFontSize - 2, marginTop: 20, paddingTop: 10, borderTopWidth: 1, borderTopColor: "#ddd" },
+ blockquoteText: { fontSize: baseFontSize, lineHeight, fontStyle: "italic", color: PDF_MUTED_TEXT_COLOR },
+ footnote: {
+ fontSize: baseFontSize - 2,
+ marginTop: 20,
+ paddingTop: 10,
+ borderTopWidth: 1,
+ borderTopColor: "#ddd",
+ color: PDF_MUTED_TEXT_COLOR,
+ },
});
type DocumentNodeProps = { node: MarkdownNode; styles: TStyleSheet };
@@ -110,6 +123,7 @@
title?: string;
options: PdfExportOptions;
editorFontFamily: FontName;
+ useBuiltinFonts?: boolean;
};
const DocumentTitle = (
@@ -144,9 +158,12 @@
},
) => ;
-export const MarkdownPdfDocument = ({ nodes, title, options, editorFontFamily }: MarkdownPdfDocumentProps) => {
- const bodyFont = getPdfFontFamily(editorFontFamily);
- const codeFont = getCodeFontFamily();
+export const MarkdownPdfDocument = (
+ { nodes, title, options, editorFontFamily, useBuiltinFonts = false }: MarkdownPdfDocumentProps,
+) => {
+ const fontStrategy = useBuiltinFonts ? "builtin" : "custom";
+ const bodyFont = getPdfFontFamily(editorFontFamily, fontStrategy);
+ const codeFont = getCodeFontFamily(fontStrategy);
const styles = createStyles(bodyFont, codeFont, options, options.fontSize, options.lineHeight);
const renderPageNumber = useCallback(
({ pageNumber, totalPages }: { pageNumber: number; totalPages: number }) => `${pageNumber} / ${totalPages}`,
diff --git a/src-tauri/resources/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2 b/src-tauri/resources/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/ibm-plex-mono/ibm-plex-mono-latin-400-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/ibm-plex-mono/ibm-plex-mono-latin-700-normal.woff2 b/src-tauri/resources/fonts/ibm-plex-mono/ibm-plex-mono-latin-700-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/ibm-plex-mono/ibm-plex-mono-latin-700-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/ibm-plex-sans/ibm-plex-sans-latin-wght-normal.woff2 b/src-tauri/resources/fonts/ibm-plex-sans/ibm-plex-sans-latin-wght-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/ibm-plex-sans/ibm-plex-sans-latin-wght-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/ibm-plex-serif/ibm-plex-serif-latin-400-normal.woff2 b/src-tauri/resources/fonts/ibm-plex-serif/ibm-plex-serif-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/ibm-plex-serif/ibm-plex-serif-latin-400-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/ibm-plex-serif/ibm-plex-serif-latin-700-normal.woff2 b/src-tauri/resources/fonts/ibm-plex-serif/ibm-plex-serif-latin-700-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/ibm-plex-serif/ibm-plex-serif-latin-700-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/monaspace-argon/monaspace-argon-latin-400-normal.woff2 b/src-tauri/resources/fonts/monaspace-argon/monaspace-argon-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/monaspace-argon/monaspace-argon-latin-400-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/monaspace-krypton/monaspace-krypton-latin-400-normal.woff2 b/src-tauri/resources/fonts/monaspace-krypton/monaspace-krypton-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/monaspace-krypton/monaspace-krypton-latin-400-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/monaspace-neon/monaspace-neon-latin-400-normal.woff2 b/src-tauri/resources/fonts/monaspace-neon/monaspace-neon-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/monaspace-neon/monaspace-neon-latin-400-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/monaspace-radon/monaspace-radon-latin-400-normal.woff2 b/src-tauri/resources/fonts/monaspace-radon/monaspace-radon-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/monaspace-radon/monaspace-radon-latin-400-normal.woff2
+++ /dev/null
diff --git a/src-tauri/resources/fonts/monaspace-xenon/monaspace-xenon-latin-400-normal.woff2 b/src-tauri/resources/fonts/monaspace-xenon/monaspace-xenon-latin-400-normal.woff2
deleted file mode 100644
--- a/src-tauri/resources/fonts/monaspace-xenon/monaspace-xenon-latin-400-normal.woff2
+++ /dev/null