Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ works with claude, chatgpt, gemini, your local agent — anywhere that reads pla
## features

- **live preview** — ~50 ms render, shiki code highlighting (36 langs, lazy-loaded), mermaid diagrams
- **csv preview** — open `.csv` files as a capped, read-only table for quick data checks beside your notes
- **grouped themes** — mono, mono dark, catppuccin family, crafted palettes, plus AI-inspired Claude / Codex / Gemini / Cursor themes · animated sections + hover-to-preview
- **reading mode** — ⌘. distraction-free preview with iA-style typography
- **editor-only mode** — ⌘⇧. hide the preview when you want to focus on writing
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "marka-md",
"private": true,
"version": "1.5.3",
"version": "1.5.4",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "marka"
version = "1.5.3"
version = "1.5.4"
description = "marka.md — a local markdown editor for the notes you share with ai"
authors = ["Matt Enarle"]
edition = "2021"
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "marka.md",
"mainBinaryName": "marka.md",
"version": "1.5.3",
"version": "1.5.4",
"identifier": "com.mattenarle.markamd",
"build": {
"beforeDevCommand": "bun run dev",
Expand Down
16 changes: 8 additions & 8 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
formatContextBundle,
getContextBundleStats,
getWhatsNewToastMessage,
isMarkdownPath,
isSupportedTextPath,
PdfExportError,
pickFolder,
pickMarkdownFile,
Expand Down Expand Up @@ -447,15 +447,15 @@ export function App() {
e.preventDefault();
reset();
const files = Array.from(e.dataTransfer?.files ?? []);
const firstMd = files.find((f) => isMarkdownPath(f.name));
if (firstMd) {
const firstSupported = files.find((f) => isSupportedTextPath(f.name));
if (firstSupported) {
// WKWebView doesn't expose file path; load content as an untitled buffer.
try {
const text = await firstMd.text();
const text = await firstSupported.text();
startNewBuffer(text);
} catch (err) {
console.error("marka.md: file drop read failed", err);
setLoadError({ message: `could not read ${firstMd.name} — ${err}` });
setLoadError({ message: `could not read ${firstSupported.name} — ${err}` });
}
} else if (files.length > 0) {
setLoadError({
Expand All @@ -479,7 +479,7 @@ export function App() {
window.removeEventListener("dragend", reset);
window.removeEventListener("blur", reset);
};
}, [setActivePath, t]);
}, [setLoadError, startNewBuffer, t]);

const shortcuts = useMemo(
() => ({
Expand Down Expand Up @@ -688,7 +688,7 @@ export function App() {
<main className="mdv-shell">
{readingMode ? (
<>
<Preview source={debouncedPreview} />
<Preview source={debouncedPreview} filePath={activePath} />
<ReadingFind
open={findOpen}
onClose={() => setFindOpen(false)}
Expand Down Expand Up @@ -735,7 +735,7 @@ export function App() {
) : (
<Splitter
left={<Editor value={source} onChange={setSource} vimOn={vimOn} onVimMode={setVimMode} />}
right={<Preview source={debouncedPreview} />}
right={<Preview source={debouncedPreview} filePath={activePath} />}
/>
)}
</div>
Expand Down
57 changes: 57 additions & 0 deletions src/components/editor/csv-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { CSV_PREVIEW_MAX_COLUMNS, CSV_PREVIEW_MAX_ROWS, parseCsvPreview } from "@/lib";

type CsvPreviewProps = {
source: string;
fileName?: string;
};

export function CsvPreview({ source, fileName }: CsvPreviewProps) {
const preview = parseCsvPreview(source);

if (preview.headers.length === 0) {
return (
<div className="mdv-csv">
<div className="mdv-csv__empty">empty csv file</div>
</div>
);
}

return (
<article className="mdv-csv" aria-label={fileName ? `${fileName} csv preview` : "csv preview"}>
<header className="mdv-csv__header">
<div>
<p className="mdv-csv__eyebrow">csv preview</p>
<h1 className="mdv-csv__title">{fileName ?? "data.csv"}</h1>
</div>
<p className="mdv-csv__meta">
{preview.totalRows.toLocaleString()} rows · {preview.totalColumns.toLocaleString()} columns
</p>
</header>
{(preview.truncatedRows || preview.truncatedColumns) ? (
<p className="mdv-csv__notice">
showing first {CSV_PREVIEW_MAX_ROWS} rows and {CSV_PREVIEW_MAX_COLUMNS} columns
</p>
) : null}
<div className="mdv-csv__table-wrap">
<table className="mdv-csv__table">
<thead>
<tr>
{preview.headers.map((header, i) => (
<th key={`${header}-${i}`}>{header}</th>
))}
</tr>
</thead>
<tbody>
{preview.rows.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td key={cellIndex}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</article>
);
}
1 change: 1 addition & 0 deletions src/components/editor/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { Editor } from "./editor";
export { CsvPreview } from "./csv-preview";
export { OpenTabs } from "./open-tabs";
export { Preview } from "./preview";
export { ReadingFind } from "./reading-find";
Expand Down
38 changes: 23 additions & 15 deletions src/components/editor/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { useEffect, useRef, useState } from "react";
import { ensureMarkdownReady, renderMarkdown, useTheme } from "@/lib";
import inspectUrl from "@/assets/mascot/inspect.png";
import { renderMermaidBlocks } from "@/lib/mermaid";
import { basename, isCsvPath } from "@/lib";
import { CsvPreview } from "./csv-preview";

type PreviewProps = {
source: string;
filePath?: string | null;
};

// hand-written lucide copy + check icons so we don't drag in react-dom/server
Expand Down Expand Up @@ -62,10 +65,11 @@ function decorateCodeBlocks(root: HTMLElement): () => void {
return () => cleanups.forEach((fn) => fn());
}

export function Preview({ source }: PreviewProps) {
export function Preview({ source, filePath }: PreviewProps) {
const theme = useTheme();
const [ready, setReady] = useState(false);
const articleRef = useRef<HTMLElement>(null);
const csvPreview = filePath ? isCsvPath(filePath) : false;

useEffect(() => {
let cancelled = false;
Expand All @@ -82,38 +86,38 @@ export function Preview({ source }: PreviewProps) {
// renderMarkdown is async (lazy-loads shiki themes + langs on demand).
// Cancelled flag guards against stale renders on rapid file/theme switches.
useEffect(() => {
if (!ready) return;
if (!ready || csvPreview) return;
let cancelled = false;
void renderMarkdown(source, theme).then((h) => {
if (!cancelled) setHtml(h);
});
return () => {
cancelled = true;
};
}, [source, theme, ready]);
}, [source, theme, ready, csvPreview]);

// Imperatively set innerHTML — React's dangerouslySetInnerHTML re-applies the
// string on each parent re-render even when the value is unchanged, which
// wipes mermaid's post-render DOM mutations (and shiki's decorate-codeblock
// wrappers). Setting innerHTML in a useEffect that only fires when `html`
// actually changes preserves mermaid SVGs across save / saveStatus updates.
useEffect(() => {
if (!articleRef.current) return;
if (!articleRef.current || csvPreview) return;
articleRef.current.innerHTML = html;
}, [html]);
}, [html, csvPreview]);

useEffect(() => {
if (!articleRef.current) return;
if (!articleRef.current || csvPreview) return;
return decorateCodeBlocks(articleRef.current);
}, [html]);
}, [html, csvPreview]);

useEffect(() => {
if (!articleRef.current) return;
if (!articleRef.current || csvPreview) return;
const mermaidTheme = theme === "latte" || theme === "matcha" ? "default" : "dark";
void renderMermaidBlocks(articleRef.current, mermaidTheme);
}, [html, theme]);
}, [html, theme, csvPreview]);

if (source.trim().length === 0) {
if (!csvPreview && source.trim().length === 0) {
return (
<div className="mdv-preview" data-theme={theme}>
<div className="mdv-preview__empty">
Expand All @@ -135,11 +139,15 @@ export function Preview({ source }: PreviewProps) {

return (
<div className="mdv-preview" data-theme={theme}>
<article
ref={articleRef}
className="mdv-prose"
data-theme={theme}
/>
{csvPreview ? (
<CsvPreview source={source} fileName={filePath ? basename(filePath) : undefined} />
) : (
<article
ref={articleRef}
className="mdv-prose"
data-theme={theme}
/>
)}
</div>
);
}
6 changes: 3 additions & 3 deletions src/components/files/folder-node.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { Check, ChevronRight, FilePlus2, FileText, Folder, FolderOpen } from "lucide-react";
import { Check, ChevronRight, FilePlus2, FileText, Folder, FolderOpen, Table2 } from "lucide-react";
import { Icon } from "@/components/primitives";
import type { FileEntry } from "@/lib";
import { isCsvPath, type FileEntry } from "@/lib";
import { FileTree, type NewEntry } from "./file-tree";

export const DRAG_MIME = "application/x-marka-path";
Expand Down Expand Up @@ -188,7 +188,7 @@ export function FileNode({
title={entry.path}
>
<span className="mdv-tree__icon">
<Icon icon={FileText} size={13} strokeWidth={1.5} />
<Icon icon={isCsvPath(entry.name) ? Table2 : FileText} size={13} strokeWidth={1.5} />
</span>
<span className="mdv-tree__name">{entry.name}</span>
</button>
Expand Down
4 changes: 2 additions & 2 deletions src/components/files/sidebar-search.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { dirname, walkMarkdownFiles, type FlatFileEntry } from "@/lib";
import { dirname, walkSupportedTextFiles, type FlatFileEntry } from "@/lib";

const MAX_RESULTS = 80;

Expand All @@ -23,7 +23,7 @@ export function SearchResults({
useEffect(() => {
let cancelled = false;
setIndex(null);
walkMarkdownFiles(rootPath)
walkSupportedTextFiles(rootPath)
.then((items) => {
if (!cancelled) setIndex(items);
})
Expand Down
21 changes: 19 additions & 2 deletions src/components/overlays/about-overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Download, Globe, Star, X } from "lucide-react";
import { Download, FileText, Globe, Layers3, Palette, Star, Table2, X } from "lucide-react";
import { getVersion } from "@tauri-apps/api/app";
import { openUrl } from "@tauri-apps/plugin-opener";
import { Button, Icon, Overlay } from "@/components/primitives";
Expand All @@ -15,6 +15,13 @@ const REPO_URL = "https://github.com/mattenarle10/markamd";
const SITE_URL = "https://markamd.vercel.app";
const AUTHOR_PERSONAL_URL = "https://mattenarle.com";

const FEATURES = [
{ icon: FileText, label: "markdown", detail: "write + preview" },
{ icon: Layers3, label: "context", detail: "bundle for ai" },
{ icon: Table2, label: "csv", detail: "quick table view" },
{ icon: Palette, label: "themes", detail: "calm palettes" },
];

let cachedVersion: string | null = null;

export function AboutOverlay({ open, onClose, onCheckForUpdates }: AboutOverlayProps) {
Expand Down Expand Up @@ -97,9 +104,19 @@ export function AboutOverlay({ open, onClose, onCheckForUpdates }: AboutOverlayP
</button>
) : null}
<p className="mdv-about__tagline">
a local markdown editor, built for the notes you share with ai.
a local markdown workspace for notes, data snippets, pdf export, and the context you share with ai.
</p>

<div className="mdv-about__features" aria-label="marka.md features">
{FEATURES.map((feature) => (
<div key={feature.label} className="mdv-about__feature">
<Icon icon={feature.icon} size={13} strokeWidth={1.6} />
<span className="mdv-about__feature-label">{feature.label}</span>
<span className="mdv-about__feature-detail">{feature.detail}</span>
</div>
))}
</div>

<div className="mdv-about__links">
<button
type="button"
Expand Down
Loading