Other Packages
Smaller utility packages that serve focused roles in the zudotext app.
@takazudo/file-utils
File system utilities for markdown frontmatter parsing, filename generation, and path safety. Designed for Node.js — used by the Rust backend's predecessor and by build scripts.
Exports
import {
parseFrontmatter,
stripFrontmatter,
extractFrontmatterBlock,
extractDateFromFilename,
generateFilename,
makeSlug,
nowIsoLocal,
safePath,
} from "@takazudo/file-utils";
import type { FrontmatterResult } from "@takazudo/file-utils";
// Subpath export for frontmatter only
import { parseFrontmatter } from "@takazudo/file-utils/frontmatter";parseFrontmatter()
Parses YAML frontmatter from a markdown string. Supports quoted values, multi-line scalars (>-, >, |-, |), and CRLF normalization.
function parseFrontmatter(content: string): FrontmatterResult;
interface FrontmatterResult {
meta: Record<string, string>;
body: string;
}const { meta, body } = parseFrontmatter(`---
title: Hello
date: 2026-01-15
---
Content here.`);
// meta = { title: "Hello", date: "2026-01-15" }
// body = "\nContent here."stripFrontmatter() / extractFrontmatterBlock()
// Remove frontmatter, return body only
function stripFrontmatter(content: string): string;
// Extract the raw frontmatter block (including delimiters)
function extractFrontmatterBlock(content: string): string;generateFilename()
Generates a date-stamped filename from a name string:
function generateFilename(name: string): string;generateFilename("Project Update");
// "20260320-project-update.md"extractDateFromFilename()
Extracts a date string from a timestamped filename:
function extractDateFromFilename(filename: string): string | null;extractDateFromFilename("20260115-1430-meeting.md");
// "2026-01-15T14:30:00"makeSlug()
Converts a string to a URL-safe slug:
function makeSlug(name: string): string;makeSlug("Project Update");
// "project-update"nowIsoLocal()
Returns the current timestamp in ISO local format:
function nowIsoLocal(): string;safePath()
Prevents directory traversal attacks by ensuring the resolved path stays within the given directory:
function safePath(dir: string, filename: string): string;
// Throws "Invalid filename" if the resolved path escapes `dir`@takazudo/code-block
React component for rendering markdown code blocks and blockquotes with a toolbar.
Exports
import { CodeBlock, markdownCodeComponents } from "@takazudo/code-block";CodeBlock
Wraps <code> content with a toolbar that provides:
Copy button — copies the code text to clipboard
Word-wrap toggle — toggles word wrapping on/off
markdownCodeComponents
A component map for markdown renderers. Drop it into a markdown component provider to enhance <pre>, <code>, and <blockquote> elements:
import { markdownCodeComponents } from "@takazudo/code-block";
<MDXProvider components={markdownCodeComponents}>
<MarkdownContent />
</MDXProvider>The <pre> wrapper adds a context so <code> inside <pre> renders as a CodeBlock with toolbar. Standalone <code> (inline) renders as a plain <code> element. The <blockquote> wrapper adds a copy button.
Peer dependency: react >= 18.
@takazudo/command-palette
A VS Code-style command palette React component with fuzzy filtering, keyboard navigation, and category grouping.
Exports
import { CommandPalette } from "@takazudo/command-palette";
import type { Command } from "@takazudo/command-palette";Command Interface
interface Command {
id: string;
label: string;
description?: string;
category?: string;
shortcut?: string;
keepOpen?: boolean;
action: () => void;
}Usage
import { CommandPalette } from "@takazudo/command-palette";
const commands = [
{
id: "new-draft",
label: "New Draft",
category: "File",
shortcut: "Cmd+N",
action: () => createNewDraft(),
},
{
id: "toggle-vim",
label: "Toggle Vim Mode",
category: "Editor",
action: () => toggleVim(),
},
];
<CommandPalette
open={isOpen}
onClose={() => setIsOpen(false)}
commands={commands}
placeholder="Type a command..."
/>Features:
Case-insensitive filtering by label and category
Arrow key navigation with
Enterto executeEscapeto closeAutomatic category grouping
Optional
keepOpento prevent closing after actionShortcut badge display
Peer dependency: react >= 18.
@takazudo/shortcut-engine
Keyboard shortcut engine with chord support (two-step shortcuts like Mod+B → S).
Exports
import {
createShortcutEngine,
formatShortcutForDisplay,
formatComboForDisplay,
} from "@takazudo/shortcut-engine";
import type {
ShortcutEngine,
ShortcutBinding,
ChordState,
} from "@takazudo/shortcut-engine";createShortcutEngine()
function createShortcutEngine(
onChordStateChange?: (state: ChordState | null) => void,
): ShortcutEngine;
interface ShortcutEngine {
handleKeyDown: (e: KeyboardEvent) => void;
setBindings: (bindings: ShortcutBinding[]) => void;
reset: () => void;
destroy: () => void;
}
interface ShortcutBinding {
id: string;
shortcut: string; // "Mod+E" or "Mod+B → S"
action: () => void;
}Usage
import { createShortcutEngine } from "@takazudo/shortcut-engine";
const engine = createShortcutEngine((chordState) => {
if (chordState?.pending) {
showChordIndicator(chordState.prefix); // e.g., "Cmd+B"
} else {
hideChordIndicator();
}
});
engine.setBindings([
{ id: "save", shortcut: "Mod+S", action: () => save() },
{ id: "bold", shortcut: "Mod+B → B", action: () => toggleBold() },
{ id: "strikethrough", shortcut: "Mod+B → S", action: () => toggleStrike() },
]);
document.addEventListener("keydown", engine.handleKeyDown);
// Cleanup
engine.destroy();Key behaviors:
Modnormalization —Modmaps toCmdon macOS,Ctrlon other platformsChord timeout — 1500ms to complete the second key after the prefix
Escape cancellation — pressing
Escapeduring chord-pending cancels the chordModifier filtering — standalone modifier key presses (
Shift,Ctrl, etc.) are ignored
Display Formatting
formatComboForDisplay("Mod+S");
// macOS: "Cmd+S", others: "Ctrl+S"
formatShortcutForDisplay("Mod+B → S");
// macOS: "Cmd+B → S", others: "Ctrl+B → S"@takazudo/kanban-board
React component library for a drag-and-drop kanban board. Built with @dnd-kit for drag-and-drop and depends on @takazudo/kanban-parser for data types.
Exports
import {
KanbanBoard,
KanbanCard,
KanbanColumn,
KanbanCollapsedColumn,
KanbanSwimlaneGrid,
KanbanGridCell,
KanbanViewSelector,
SwimlaneToggle,
InlineEditField,
CardContextMenu,
CardQuickEditMenu,
KanbanFilterBar,
LabelBadge,
LabelBadgeList,
} from "@takazudo/kanban-board";
import type {
KanbanCardMode,
KanbanView,
LabelDef,
RenderKanbanCardBody,
} from "@takazudo/kanban-board";KanbanBoard
The main container component. Renders columns with drag-and-drop card movement.
Key props:
board: KanbanBoard— board data (from kanban-parser)board.cardMode?: KanbanCardMode—"normal"|"bigger"|"biggest"; absent resolves to NormalrenderCardBody?: RenderKanbanCardBody— host-owned raw-Markdown renderer for Bigger and Biggest cards; receives{ card, mode, hasLeadingAtxTitle }labelDefs?: LabelDef[]— label color definitionsonCardMove,onCardReorder,onCardUpdate— mutation callbacksonColumnToggleCollapse— column collapse togglegroupBy?: string— enable swimlane grouping by fieldviews?: KanbanView[]/activeView?/onSelectView?— saved view supportonAddCard?,onCardDelete?,onEditCard?— CRUD callbacksfocusedCardId?/onCardHover?— keyboard navigation support
Hooks
useKanbanKeyboard — Vim-style keyboard navigation (
J/K/H/Lfor movement,Eto edit,Nto add,Dto delete). Key bindings are configurable.
Features
Drag-and-drop cards between columns
Swimlane grouping (by priority, labels, or custom field)
Column collapsing
Inline editing for card titles/bodies
Per-board card modes (Normal, Bigger, Biggest) with ratio-derived desktop geometry
Raw-Markdown card-body rendering seam owned by the host application
Context menu and quick-edit popover
Image enlargement dialog
View selector for saved filters/groupings
AI actions (label suggestion, subtask breakdown, checklist generation)
Dependencies: @takazudo/app-defaults, @takazudo/color-themes, @takazudo/kanban-parser, @takazudo/ui-components, @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities. Peer dependencies: react >= 18, react-dom >= 18.
cardWidth is the canonical Base width. The package derives List card widths with exact Normal/Bigger/Biggest ratios of 1/1, 15/11, and 20/11 at every viewport width; column, header, cell, Add Column, and drag-overlay geometry adds 16 px around the derived card width. The package does not define or accept CardDensity.
The board package deliberately does not parse Markdown. In Normal mode it uses the compact card presentation. In Bigger and Biggest it calls the memo-stable renderCardBody seam once per visible card. The application host implements that seam with read-only shared Preview components, card-scoped heading IDs, relative asset resolution, and nested-interaction isolation. The hasLeadingAtxTitle flag lets the host omit only a title already consumed by the parser; marker truncation remains a host rendering concern.
@takazudo/kanban-parser
Pure markdown parser, serializer, and directory-model adapter for kanban board data. Zero dependencies.
The package uses the directory-as-board model: a directory is a kanban board if and only if it contains a KANBAN.md manifest. The manifest carries board props in YAML frontmatter and one ## Column heading per column, each followed by an ordered markdown link-list of card files. Each card is its own .md file. See Kanban Directory Model for the full spec.
Directory-model exports
import {
KANBAN_FILENAME,
parseKanbanManifest,
serializeKanbanManifest,
parseCardFile,
serializeCardFile,
assembleBoard,
planDirectoryWrites,
} from "@takazudo/kanban-parser";
import type { BoardMeta } from "@takazudo/kanban-parser";KANBAN_FILENAME
const KANBAN_FILENAME = "KANBAN.md";The manifest filename constant. A directory is a kanban board iff it contains this file.
parseKanbanManifest()
Parses the KANBAN.md manifest: extracts board props from frontmatter and a list of { name, cardFiles[] } column objects from the body's ## Heading / link-list structure.
function parseKanbanManifest(md: string): {
meta: BoardMeta;
columns: { name: string; cardFiles: string[] }[];
};serializeKanbanManifest()
Serializes a KanbanBoard to the KANBAN.md manifest. Body link paths come from fileIndex (keyed by idStable); link text comes from each card's current title.
function serializeKanbanManifest(
board: KanbanBoard,
fileIndex: Map<string, string>,
): string;parseCardFile()
Parses a single card file. Returns the card (without status/order — those are derived from the manifest) plus the idStable key. Title falls back to the first H1, then the filename slug.
function parseCardFile(
content: string,
filename: string,
): { card: Omit<KanbanCard, "status" | "order">; idStable: string };serializeCardFile()
Serializes a card to its file. Preserves unknown frontmatter keys from prevContent (e.g. created_at, updated_at, custom fields) verbatim. Pass null for brand-new files.
function serializeCardFile(card: KanbanCard, prevContent: string | null): string;assembleBoard()
Read path: assembles a full in-memory board from the manifest and all card documents. Performs card and column reconciliation (orphan documents, dangling manifest links, unknown column headings) and returns non-blocking warnings. Never mutates the workspace.
function assembleBoard(
manifestMd: string,
cardFiles: { filename: string; content: string }[],
): { board: KanbanBoard; fileIndex: Map<string, string>; warnings: FormatWarning[] };planDirectoryWrites()
Write path: plans the workspace writes for a board edit. Allocates stable filenames for new cards, identifies documents to delete, and serializes the updated manifest. A move/reorder-only change produces no cardWrites — only the manifest changes.
function planDirectoryWrites(
prev: KanbanBoard | null,
next: KanbanBoard,
fileIndex: Map<string, string>,
prevContents: Map<string, string>,
): {
board: KanbanBoard;
manifest: string;
cardWrites: { filename: string; content: string }[];
cardDeletes: string[];
fileIndex: Map<string, string>;
};Board Operations (unchanged)
All operations are immutable — they return a new KanbanBoard:
import {
moveCard,
moveCardToPosition,
reorderCard,
updateCard,
addCard,
removeCard,
toggleColumnCollapse,
getColumnCards,
} from "@takazudo/kanban-parser";| Function | Description |
|---|---|
moveCard(board, cardId, newStatus) | Move card to a new column |
moveCardToPosition(board, cardId, newStatus, newOrder) | Move card to specific position |
reorderCard(board, cardId, newOrder) | Reorder within same column |
updateCard(board, cardId, updates, fileIndex?) | Update card properties; directory callers pass the stable filename index for titleless fallbacks |
addCard(board, title, status) | Create a new card |
removeCard(board, cardId) | Delete a card |
toggleColumnCollapse(board, columnId) | Toggle column collapse |
getColumnCards(board, columnId) | Get cards in a column (sorted by order) |
Utilities
countChecklist(body)— count- [x]and- [ ]items, returns{ total, checked }slugify(text)— convert to kebab-case slug (preserves non-ASCII)uniqueSlug(title, existingIds)— generate unique slug with-2,-3suffixextractImages(markdown)— extract image URLs frompatterns
@takazudo/todo-board
React component library for a TODO checklist board. Displays items split into TODO / DONE sections with checkbox toggling, inline title editing, and an add-item input. Built on @takazudo/todo-parser for data types and @takazudo/ui-components for the inline edit field.
Exports
import {
TodoBoard,
TodoItemRow,
TodoAddInput,
InlineEditField,
} from "@takazudo/todo-board";
import type {
TodoBoardProps,
TodoItemRowProps,
TodoAddInputProps,
InlineEditFieldProps,
TodoItem,
} from "@takazudo/todo-board";TodoBoard
The main container component. Splits items into TODO and DONE sections, renders each as a TodoItemRow, and provides an add button.
Key props:
items: TodoItem[]— the full list (both todo and done)onToggle: (itemId: string) => void— checkbox toggleonAdd: (title: string) => void— new item creationonRemove: (itemId: string) => void— item deletiononUpdate: (itemId: string, updates) => void— inline title/body edit
TodoItemRow
A single checklist row with checkbox, title, and remove button. Supports inline editing via onUpdate.
TodoAddInput
Text input for adding new items. Shows on "+ Add" click, submits on Enter, cancels on Escape.
Peer dependencies: react >= 18, react-dom >= 18.
@takazudo/todo-parser
Pure markdown parser and serializer for TODO checklist data. Parses markdown files with type: todo frontmatter into structured TodoList objects. All operations are immutable. Zero dependencies.
Exports
import {
parseTodoMarkdown,
serializeTodoMarkdown,
hasTodoFrontmatter,
toggleItem,
addItem,
removeItem,
updateItem,
reorderItem,
slugify,
uniqueSlug,
} from "@takazudo/todo-parser";
import type { TodoItem, TodoList } from "@takazudo/todo-parser";parseTodoMarkdown()
Parses a markdown string into a TodoList. The format uses YAML frontmatter for metadata and ## TODO / ## DONE sections with checkbox items:
const list = parseTodoMarkdown(`---
type: todo
title: Sprint Tasks
---
## TODO
- [ ] Write tests
Some notes about testing
- [ ] Update docs
## DONE
- [x] Set up CI
`);
// list.items = [
// { id: "write-tests", title: "Write tests", done: false, body: "Some notes about testing" },
// { id: "update-docs", title: "Update docs", done: false, body: "" },
// { id: "set-up-ci", title: "Set up CI", done: true, body: "" },
// ]serializeTodoMarkdown()
Converts a TodoList back to markdown. Preserves the original section order and any extra sections.
hasTodoFrontmatter()
Returns true if the markdown content has type: todo in its frontmatter. Used to detect TODO files.
Operations
All operations are immutable — they return a new TodoList:
| Function | Description |
|---|---|
toggleItem(list, itemId) | Toggle an item's done state |
addItem(list, title) | Add a new TODO item |
removeItem(list, itemId) | Remove an item |
updateItem(list, itemId, updates) | Update title and/or body (re-generates ID on title change) |
reorderItem(list, itemId, newIndex) | Move an item to a new position |
Utilities
slugify(text)— convert to kebab-case slug (preserves non-ASCII characters)uniqueSlug(title, existingIds)— generate unique slug with-2,-3suffix
@takazudo/cloud-crypto
End-to-end encryption primitives for cloud sync. Uses the Web Crypto API (PBKDF2, AES-256-GCM, HMAC-SHA256) — no external cryptography dependencies.
Exports
import {
deriveKeys,
deriveVerificationHash,
encryptContent,
decryptContent,
computeHmac,
computeHmacRaw,
encryptPath,
decryptPath,
serializeBlob,
deserializeBlob,
BLOB_VERSION,
toBase64Url,
fromBase64Url,
concatBuffers,
hexEncode,
hexDecode,
} from "@takazudo/cloud-crypto";
import type {
DerivedKeys,
KeyDerivationParams,
EncryptedPayload,
} from "@takazudo/cloud-crypto";Key Derivation
deriveKeys() derives three independent sub-keys from a single password using PBKDF2 (600,000 iterations by default):
const keys = await deriveKeys({ password: "user-password" });
// keys.encryptionKey — AES-256-GCM for file content
// keys.hmacKey — HMAC-SHA256 for integrity
// keys.pathKey — AES-256-GCM for path encryption
// keys.salt — 32-byte salt (generated or provided)deriveVerificationHash() produces a hex hash for password verification without exposing encryption keys. It uses a distinct salt prefix ("verify:") so compromising this hash does not leak the encryption keys.
const hash = await deriveVerificationHash("user-password", keys.salt);
// hex string for server-side password verificationEncrypt / Decrypt
// Encrypt file content
const payload = await encryptContent(keys.encryptionKey, "plaintext");
// payload.ciphertext: ArrayBuffer, payload.iv: Uint8Array (12 bytes)
// Decrypt
const plainBuffer = await decryptContent(keys.encryptionKey, payload);
const text = new TextDecoder().decode(plainBuffer);Path Encryption
File paths are encrypted separately so the server cannot see filenames:
const encrypted = await encryptPath(keys.pathKey, "notes/hello.md");
const original = await decryptPath(keys.pathKey, encrypted);Blob Serialization
serializeBlob() / deserializeBlob() pack an encrypted payload (IV + ciphertext) into a versioned binary format for storage and transport.
HMAC
computeHmac() returns a hex-encoded HMAC-SHA256 digest. computeHmacRaw() returns the raw ArrayBuffer.
@takazudo/cloud-sync
Client-side cloud sync engine with WebSocket real-time updates, local change tracking, offline queuing, and conflict resolution.
Exports
import {
CloudSyncClient,
CloudSyncError,
ChangeTracker,
OfflineQueue,
WsManager,
detectConflictType,
resolveCloudConflict,
autoResolveConflicts,
} from "@takazudo/cloud-sync";
import type {
CloudSyncOptions,
ChangeNotification,
TrackedChange,
QueuedChange,
CloudConflictType,
CloudConflictInfo,
CloudConflictResolution,
WsConnectionState,
WsManagerOptions,
} from "@takazudo/cloud-sync";CloudSyncClient
HTTP client for the cloud sync REST API. Handles workspace operations, push/pull sync, file versioning, and device management.
const client = new CloudSyncClient({
serverUrl: "https://sync.example.com",
token: "auth-token",
workspaceId: "workspace-id",
deviceId: "device-id",
});ChangeTracker
Tracks local file changes (upsert/delete) in memory. Changes are accumulated until a sync push is triggered.
const tracker = new ChangeTracker();
tracker.track("notes/hello.md", "upsert", "sha256-hash");
const pending = tracker.getPending(); // TrackedChange[]
tracker.acknowledge(["notes/hello.md"]); // clear after successful pushOfflineQueue
Persists queued changes to localStorage so they survive app restarts. Provides compaction (deduplicating by path) and batch flush.
const queue = new OfflineQueue("workspace-id", localStorage);
queue.enqueue({ path: "notes/hello.md", action: "upsert", contentHash: "..." });
const result = await queue.flush(pushFn);
// result.accepted: string[], result.failed: QueuedChange[]WsManager
WebSocket connection manager with automatic reconnection (exponential backoff), ping/pong keep-alive, and cursor-based change streaming.
const ws = new WsManager({
url: "wss://sync.example.com/ws",
token: "auth-token",
deviceId: "device-id",
cursor: 0,
onChanges: (changes, cursor) => { /* real-time updates */ },
onConnected: () => { /* ... */ },
onDisconnected: () => { /* ... */ },
onError: (err) => { /* ... */ },
});Conflict Resolution
detectConflictType(info)— Classifies a conflict as"concurrent-edit","edit-delete", or"delete-edit"resolveCloudConflict(info, strategy)— Resolves a single conflict with the given strategyautoResolveConflicts(conflicts, strategy)— Batch-resolves conflicts using a default strategy
@takazudo/sync-logger
Structured logging utility for sync operations. Provides a module-scoped logger with configurable log levels and pluggable handlers. Used by the sync subsystem (cloud-sync, sync-client, backend-bridge) for consistent, filterable logging.
Exports
import {
createSyncLogger,
setSyncLogHandler,
setSyncLogLevel,
} from "@takazudo/sync-logger";
import {
consoleLogHandler,
jsonLogHandler,
createCallbackHandler,
} from "@takazudo/sync-logger";
import type {
LogHandler,
LogLevel,
SyncLogEntry,
SyncLogger,
} from "@takazudo/sync-logger";Types
type LogLevel = "debug" | "info" | "warn" | "error";
interface SyncLogEntry {
timestamp: string;
level: LogLevel;
module: string;
message: string;
data?: Record<string, unknown>;
}
type LogHandler = (entry: SyncLogEntry) => void;
interface SyncLogger {
debug(message: string, data?: Record<string, unknown>): void;
info(message: string, data?: Record<string, unknown>): void;
warn(message: string, data?: Record<string, unknown>): void;
error(message: string, data?: Record<string, unknown>): void;
}Usage
import { createSyncLogger, setSyncLogHandler, setSyncLogLevel } from "@takazudo/sync-logger";
import { consoleLogHandler } from "@takazudo/sync-logger";
// Configure global handler and minimum level
setSyncLogHandler(consoleLogHandler);
setSyncLogLevel("debug");
// Create a module-scoped logger
const log = createSyncLogger("cloud-sync");
log.info("Sync started", { workspaceId: "abc123" });
log.error("Push failed", { path: "notes/hello.md", status: 500 });Built-in Handlers
| Handler | Description |
|---|---|
consoleLogHandler | Formatted output to console.log / console.warn / console.error with timestamp, level, and module prefix |
jsonLogHandler | Outputs each entry as a single-line JSON string via console.log |
createCallbackHandler(fn) | Wraps any (entry: SyncLogEntry) => void callback as a LogHandler |
Design
Global singleton — one handler and one minimum level for the entire app. Call
setSyncLogHandler()once at app startup.Module-scoped loggers —
createSyncLogger(module)returns a logger that tags every entry with the module name.Level filtering — entries below
setSyncLogLevel()threshold are silently dropped.Error-safe — if the handler throws, the error is swallowed so logging never crashes the caller.
@takazudo/css-playground
Interactive CSS development tool for designing and testing UI patterns. Built with React 19, React Router, Tailwind CSS 4, and Vite. Runs on port 5380.
This is a development-only tool and is not shipped with the app.
@takazudo/directive-registry
Registry of MDX directive definitions and their attribute schemas. Centralizes directive metadata so authoring help dialogs, validation, and the renderer all share a single source of truth.
Exports
import {
directives,
imageAttrs,
validateDirectiveAttrs,
validateImageAttrs,
validateAttrsAgainstSchema,
suggestDirectiveName,
} from "@takazudo/directive-registry";
import type { AttrSchema, DirectiveDef, ValidationResult } from "@takazudo/directive-registry";directives
Array of all registered DirectiveDef objects. Each entry carries a name, kind ("container" or "leaf"), an attrs schema, a description, an exampleSrc, and a category.
Built-in categories: "admonition" (note, tip, info, warning, danger) and "layout" (steps, card, card-grid, image, video, …).
validateDirectiveAttrs()
Validates an attribute map against a named directive's schema:
function validateDirectiveAttrs(
name: string,
attrs: Record<string, unknown>,
): ValidationResult;Returns { ok: true } on success or { ok: false, errors: string[] } on failure.
suggestDirectiveName()
Returns the closest registered directive name to the given input (Levenshtein distance), or null if nothing is close enough. Used to surface "did you mean :::note?" messages.
@takazudo/frameset
Split-panel frame layout system for the writing app. Manages a tree of horizontal/vertical splits, frame chrome (header, collapse/zoom/close controls), and the active-frame context shared across panes.
Primary exports
import {
Frameset,
FrameChrome,
LeafRenderer,
ActiveFrameProvider,
useActiveFrame,
useFramesetCommands,
createFrameStateMachine,
framesetsEquivalent,
findMatchingSavedFrameset,
} from "@takazudo/frameset";
import type {
FramesetProps,
FramesetHandle,
FrameChromeProps,
FrameId,
ProviderMeta,
SavedFrameset,
} from "@takazudo/frameset";Frameset
The root split-panel container. Takes a FramesetTree from @takazudo/view-provider and renders the recursive split/leaf layout with resize dividers. Exposes a FramesetHandle ref for imperative focus control.
FrameChrome
Per-leaf chrome wrapper (header bar, split/close/zoom/collapse buttons, active-frame highlight overlay). Consumers wrap each leaf's content in FrameChrome.
useFramesetCommands()
Hook that wires window-level events (FRAME_SPLIT_RIGHT_EVENT, FRAME_CLOSE_EVENT, etc.) to dispatch tree mutations on the active leaf. Called once at the app level.
createFrameStateMachine()
Returns a mutable frameset state machine that owns the FramesetTree and exposes typed mutation methods (split, close, zoom, collapse, replaceProvider, …). Used by the write-page to drive the persisted frameset.
Persistence helpers
framesetsEquivalent(a, b) — pure comparison of two saved framesets by their canonical content signature. findMatchingSavedFrameset(tree, saved[]) — finds the best-matching saved frameset from a list, used by the restore hook.
Dependencies: @takazudo/view-provider, @takazudo/ui-components, @takazudo/app-defaults, @takazudo/color-themes. Peer dependencies: react >= 19, react-dom >= 19.
@takazudo/view-provider
Core type system and tree utilities for the frameset architecture. Defines the ViewProvider interface, FramesetTree shape, and pure tree mutation functions consumed by @takazudo/frameset and the writing app.
Exports
import {
createProviderRegistry,
makeMarkdownFileProvider,
createConsumesBus,
splitLeaf,
removeLeaf,
replaceLeafProvider,
findLeaf,
validateFramesetTree,
FRAME_STATES,
SCROLL_SYNC_CHANNEL,
} from "@takazudo/view-provider";
import type {
ViewProvider,
FramesetTree,
LeafNode,
SplitNode,
FrameState,
FrameContext,
ProviderRegistry,
LayoutDef,
JsonValue,
} from "@takazudo/view-provider";ViewProvider
The interface every pane implements. Key methods: serialize(props) / deserialize(blob) for persistence, renderLeaf(props, context) to render content, optional renderSettings / canClose / acceptsFileDrop. Each provider declares a stable id string.
createProviderRegistry()
Creates the runtime registry that maps provider id strings to ViewProvider instances. The frameset shell calls registry.renderLeaf for each leaf node.
makeMarkdownFileProvider()
Factory that creates a full ViewProvider from a config object — handles file open/save, debounced writes, layout switching, and file-drop. Used by the kanban, mindmap, and todo providers to avoid duplicating the same ~100-line hook pattern.
createConsumesBus()
Pub/sub bus for cross-frame data sharing (scroll sync, active-file broadcast). Producers call bus.publish(key, value); consumers call bus.subscribe(key, handler).
Tree utilities
splitLeaf, removeLeaf, replaceLeafProvider, findLeaf, zoomLeaf, collapseLeaf, restoreLeaf, setRatio, validateFramesetTree — pure functions that return a new FramesetTree without mutating the input.
@takazudo/settings-sections
Shared AppSettings editor sections used by both the writing app's settings dialog and the ROOT's generate-child wizard step. Each section takes { value: AppSettings, onChange } so the same component works in both contexts.
Exports
import {
GeneralSettings,
EditorSettings,
StylesSettings,
SettingInputRow,
applyDisplayScale,
getDisplayScaleLabel,
VALID_DISPLAY_SCALES,
loadSystemFonts,
inputCls,
selectCls,
labelCls,
} from "@takazudo/settings-sections";
import type {
SettingInputRowProps,
DisplayScale,
} from "@takazudo/settings-sections";Section components
GeneralSettings — app name, display scale, Spotlight search toggle, and other general preferences
EditorSettings — font family, font size, line height, tab size, vim mode, word wrap
StylesSettings — color ramp editing plus semantic-token ramp-reference mapping
These sections are consumed by both the writing app's Settings dialog and the generate-child flow in ROOT. Writing-app-only sections (AI, sync, shortcuts, iOS) are not in this package.
applyDisplayScale()
Applies a DisplayScale multiplier to the document root font size. Called at app startup and on settings change. Valid scales: VALID_DISPLAY_SCALES = [0.8, 0.9, 1.0, 1.1, 1.25, 1.5].
@takazudo/inline-command-skills
Skill loader, parser, resolver, and CodeMirror autocomplete provider for the @@ inline AI command. Skills are markdown files with YAML frontmatter discovered from two directories at runtime.
Exports
import {
parseSkillFile,
resolveSkill,
createSkillLoader,
skillAutocomplete,
panelSkillAutocomplete,
WORKSPACE_SKILLS_DIR_REL,
} from "@takazudo/inline-command-skills";
import type {
Skill,
SkillRegistry,
SkillResolution,
SkillLoader,
LoaderConfig,
LoaderEvent,
SkillMode,
SkillSourceLayer,
} from "@takazudo/inline-command-skills";createSkillLoader()
Creates a hot-reloading skill loader. Scans a workspace-scoped dir (<workspace>/.zudotext/skills/) and a user-scoped dir (~/.config/zudotext/<appname>/skills/). On name collision the workspace file wins; the user copy is reported as shadowed.
const loader = createSkillLoader({ workspaceDir, userDir, fs });
const registry = await loader.load();
const unsub = loader.watch((event: LoaderEvent) => { /* added/updated/removed/shadowed */ });parseSkillFile()
Parses a single skill file. Returns ParseResult — either { ok: true, skill: Skill } or { ok: false, reason }. Validation failures are soft — callers log and skip, never throw.
resolveSkill()
Extracts the / token and trailing args from an inline command string:
resolveSkill("/translate-en hello world", registry);
// { skill: Skill, args: "hello world" }skillAutocomplete / panelSkillAutocomplete
Standard @codemirror/autocomplete CompletionSource functions. Pass into autocompletion({ override: [...] }) to show skill name completions as the user types @@ /....
@takazudo/cm-inline-command
CodeMirror extension that drives the @@ inline AI command capture surface. Detects the @@ trigger on the current line, enters capture mode, renders a status pill, and dispatches keymap events to consumer-provided handlers.
Exports
import {
inlineCommandExtension,
inlineCommandStateField,
enterCaptureEffect,
exitCaptureEffect,
setCommandTextEffect,
setComposingEffect,
composingField,
captureStateChanged,
detectTriggerOnLine,
isInFencedCode,
} from "@takazudo/cm-inline-command";
import type {
InlineCommandOptions,
InlineCommandState,
SubmitPayload,
CancelPayload,
TriggerMatch,
} from "@takazudo/cm-inline-command";inlineCommandExtension()
Main entry point. Returns a Extension[] array to add to a CodeMirror editor:
const ext = inlineCommandExtension({
insertShortcut: "Mod-j", // shortcut to insert "@@ " at cursor
submitInline: (payload) => { /* Tab gesture: bake inline */ },
submitPanel: (payload) => { /* Mod-Enter gesture: open panel */ },
cancel: (payload) => { /* Escape: cancel capture */ },
});When @@ is present at the cursor line, the extension enters capture mode: a cm-inline-command-line CSS class marks the line and an end-of-line status pill ("Tab to insert · ⌘↵ panel · Esc to cancel") appears. Tab, Mod-Enter, and Escape are intercepted only while capture is active.
inlineCommandStateField
StateField<InlineCommandState> — holds { active, lineFrom, command, requestId }. Read via view.state.field(inlineCommandStateField). The requestId monotonically increments on each fresh @@ capture so downstream stream handlers can discard stale responses.
Peer dependencies: @codemirror/state >= 6, @codemirror/view >= 6.
@takazudo/cm-ghost-text
CodeMirror extension for inline ghost-text (AI suggestion) display with Tab-to-accept. Renders the suggestion as a non-editable widget at the cursor position and intercepts Tab to apply it.
Exports
import {
ghostTextExtension,
ghostTextField,
setGhostEffect,
setComposingEffect,
composingField,
ghostAcceptAnnotation,
dispatchSetSuggestion,
dispatchClearSuggestion,
trimDuplicatePrefix,
} from "@takazudo/cm-ghost-text";ghostTextExtension()
Returns a Extension array. Accepts one option: acceptKey (CodeMirror key string, e.g. "Tab").
const ext = ghostTextExtension({ acceptKey: "Tab" });dispatchSetSuggestion() / dispatchClearSuggestion()
Convenience dispatch helpers for the consuming driver:
dispatchSetSuggestion(view, "suggested completion text", requestId);
dispatchClearSuggestion(view);ghostTextField
StateField<{ text: string | null; requestId: number }>. The requestId lets the stream driver discard suggestions for stale requests. Read via view.state.field(ghostTextField).
trimDuplicatePrefix()
Utility that removes the longest common prefix between the current document line and an AI suggestion string, so the ghost text only shows the novel suffix.
Peer dependencies: @codemirror/state >= 6, @codemirror/view >= 6.
@takazudo/cm-search-highlight
CodeMirror extension for the two-tier search highlight layer (normal matches + one active match). Receives pre-computed ranges from the cross-pane search orchestrator and renders them as find-match / find-match-active decorations.
Exports
import {
searchHighlightExtension,
searchHighlightField,
setSearchHighlights,
setSearchHighlightsEffect,
searchScrollbarMarkersExtension,
} from "@takazudo/cm-search-highlight";
import type { SearchHighlightRange } from "@takazudo/cm-search-highlight";searchHighlightExtension()
Returns a Extension[] array. Adds searchHighlightField and the decoration provider. Does not register any key bindings — the orchestrator owns the Cmd+F shortcut at the React level.
setSearchHighlights()
Factory for setSearchHighlightsEffect. Pass the result to view.dispatch:
view.dispatch({ effects: setSearchHighlights(ranges) });ranges is SearchHighlightRange[] — each item has from, to (document offsets), and active: boolean. Pass an empty array to clear all highlights.
CSS styles for find-match / find-match-active come from @takazudo/.
Peer dependencies: @codemirror/state >= 6, @codemirror/view >= 6.
@takazudo/cross-pane-search
Orchestrator for the bilateral search highlight feature. Given an editor EditorView and a preview HTMLElement that share the same markdown source, runs a query against both panes, pairs hits by source line, and keeps next/prev navigation in lockstep.
Exports
import {
createCrossPaneSearch,
findEditorHits,
findInnermostAnchors,
wrapPreviewMatchesInAnchor,
clearPreviewMarks,
pairHitsByLine,
} from "@takazudo/cross-pane-search";
import type {
CrossPaneSearch,
CrossPaneSearchOptions,
SearchState,
EditorHit,
PreviewMark,
CanonicalHit,
} from "@takazudo/cross-pane-search";createCrossPaneSearch()
Main entry point. Returns a CrossPaneSearch controller:
const search = createCrossPaneSearch({ view, previewContainer });
search.apply(query); // run search, highlight both panes
search.next(); // advance active hit
search.prev(); // reverse active hit
search.clear(); // remove all highlights
search.destroy(); // teardown subscriptionsThe orchestrator uses the innermost-anchor selector ([data-source-line="N"]:not(:has([data-source-line="N"]))) from @takazudo/remark-source-line to locate preview anchors, then walks text nodes to place <mark> wrappers. Editor decorations are dispatched via @takazudo/cm-search-highlight.
Dependencies: @takazudo/cm-search-highlight. Peer dependencies: @codemirror/state >= 6, @codemirror/view >= 6.
@takazudo/timeline-board
React component library for a virtualized timeline view of drafts. Displays drafts as vertically or horizontally scrolling cards with a minimap, keyboard navigation, and a detail panel.
Exports
import {
TimelineBoard,
TimelineCard,
TimelineMinimap,
TimelineDetailPanel,
TimelineLayoutShell,
useTimelineKeyboard,
} from "@takazudo/timeline-board";
import type {
TimelineBoardProps,
TimelineBoardToolbar,
TimelineCardProps,
TimelineDetailPanelProps,
TimelineLayoutShellProps,
UseTimelineKeyboardOptions,
} from "@takazudo/timeline-board";TimelineBoard
Main container. Renders a virtualized list of draft cards (powered by @tanstack/react-virtual) in horizontal or vertical layout. Supports:
activeDraft/draftCount— draft index managementloadItemContent(n)— async loader for inactive card contentonFilesChangedSubscribe/onExternalActivateSubscribe— external event hookstoolbar: TimelineBoardToolbar— controls for layout direction, card width, sort, and full-content toggle
TimelineLayoutShell
Wraps TimelineBoard with a collapsible TimelineDetailPanel. Manages the panel open/close state.
useTimelineKeyboard()
Hook for keyboard navigation. Handles arrow keys for next/prev draft, configurable key bindings.
Dependencies: @takazudo/ui-components, @takazudo/app-defaults, @takazudo/file-utils, @tanstack/react-virtual. Peer dependencies: react >= 18, react-dom >= 18.
@takazudo/frontmatter-schema
Frontmatter field schema definitions, parser, validator, and auto-field logic for markdown documents. Zero runtime dependencies.
Exports
import {
parseSchema,
validateFrontmatter,
getDefaultSchema,
applyAutoFields,
readLegacyDate,
LEGACY_FIELD_ALIASES,
toIsoLocal,
toDateOnly,
} from "@takazudo/frontmatter-schema";
import type {
Schema,
FieldDef,
FieldType,
StringFieldDef,
NumberFieldDef,
BooleanFieldDef,
DateFieldDef,
DatetimeFieldDef,
EnumFieldDef,
StringArrayFieldDef,
ValidationError,
ValidationResult,
FrontmatterValue,
FrontmatterObject,
AutoFieldEvent,
} from "@takazudo/frontmatter-schema";parseSchema()
Parses a YAML/JSON schema definition into a typed Schema object with fields: FieldDef[].
validateFrontmatter()
Validates a frontmatter object against a schema:
const result = validateFrontmatter(frontmatter, schema);
// result: { ok: true } | { ok: false, errors: ValidationError[] }applyAutoFields()
Applies auto: "on-create" / auto: "on-save" fields (e.g. created_at, updated_at). Takes an AutoFieldEvent ("create" | "save") and the existing frontmatter object; returns an updated copy.
getDefaultSchema()
Returns the built-in default schema (date, title, tags fields) used when no per-directory .schema.yaml is found.
@takazudo/frontmatter-ui
React field renderer components for editing frontmatter values. Each component receives a typed FieldDef and a current value, and fires onChange on edit.
Exports
import {
StringField,
NumberField,
BooleanField,
DateField,
DateTimeField,
EnumField,
TagsField,
StringArrayField,
FrontmatterPanel,
formatRelativeTime,
} from "@takazudo/frontmatter-ui";
import type {
StringFieldProps,
NumberFieldProps,
BooleanFieldProps,
DateFieldProps,
DateTimeFieldProps,
EnumFieldProps,
TagsFieldProps,
StringArrayFieldProps,
FrontmatterPanelProps,
} from "@takazudo/frontmatter-ui";Field components
| Component | Input type | Notes |
|---|---|---|
StringField | Text input | Single-line string value |
NumberField | Number input | Numeric value |
BooleanField | Checkbox | Boolean toggle |
DateField | Date picker | ISO date (YYYY-MM-DD) |
DateTimeField | Datetime input | ISO datetime; formatRelativeTime() formats for display |
EnumField | Select dropdown | Values from EnumFieldDef.values |
TagsField | Tag chip input | Comma/Enter delimited tags (string array) |
StringArrayField | Multi-value input | Generic string array |
FrontmatterPanel
Container that renders all fields for a document's frontmatter, given a Schema and a current FrontmatterObject. Wires each field component to the appropriate FieldDef and calls onChange(updatedFrontmatter) on any edit.
Peer dependencies: react >= 18, react-dom >= 18.
@takazudo/gfm-table
Strict GFM (GitHub Flavored Markdown) table parser and emitter. Refuses malformed input with a typed error code rather than silently producing corrupt output.
Exports
import { parse, emit, makeEmpty, addRowAt, addColAt, removeRowAt, removeColAt, setHeader, setCell, setAlignment } from "@takazudo/gfm-table";
import type { TableData, Alignment, ParseResult, ParseError, ParseOk, ContentWarning } from "@takazudo/gfm-table";parse()
function parse(source: string): ParseResult;Returns { ok: true, data: TableData, warnings: ContentWarning[] } on success. On failure returns a typed error with one of four reason values: "empty", "line-not-row", "missing-separator-row", "column-count-mismatch".
ContentWarning covers content that parses but renders unexpectedly: "list-in-cell", "code-fence-in-cell".
emit()
Serializes a TableData back to GFM markdown. Escapes embedded pipe characters.
Table mutation helpers
All operations are immutable — they return a new TableData:
| Function | Description |
|---|---|
makeEmpty(rows?, cols?) | Create an empty table |
addRowAt(data, index) | Insert a blank row at the given index |
addColAt(data, index) | Insert a blank column at the given index |
removeRowAt(data, index) | Delete a row |
removeColAt(data, index) | Delete a column (no-op if only one column) |
setHeader(data, col, value) | Update a header cell |
setCell(data, row, col, value) | Update a body cell |
setAlignment(data, col, align) | Set column alignment ("left", "center", "right", or null) |
Zero dependencies.
@takazudo/design-token-lint
Linter that checks Tailwind CSS class names against the project design system tokens. Flags raw spacing/color values that should use semantic tokens instead.
Exports
import {
checkClass,
checkClassWithConfig,
setConfig,
getConfig,
extractClasses,
lintFile,
lintContent,
loadConfig,
compileConfig,
compilePattern,
lintCssZIndex,
lintTsxZIndex,
DEFAULT_CONFIG,
} from "@takazudo/design-token-lint";
import type {
Violation,
ExtractedClass,
ExtractorOptions,
LintResult,
LintConfig,
CompiledConfig,
CompiledRule,
ZIndexViolation,
} from "@takazudo/design-token-lint";checkClass()
Checks a single Tailwind class name against the active config. Returns a Violation (with className and reason) or null if the class is permitted.
lintContent() / lintFile()
Scan a JSX/TSX string or file path for class violations:
const result: LintResult = lintContent(source, config);
// result.violations: Violation[]
// result.warnings: string[]lintFile(filePath, config) reads the file and delegates to lintContent.
loadConfig() / compileConfig()
loadConfig(dir) searches for a design-token-lint.config.json in the given directory and returns a LintConfig. compileConfig(config) converts pattern strings (with {n}, {color}, {shade} placeholders) into regex-based CompiledRule objects.
z-index lint
lintCssZIndex(cssSource) / lintTsxZIndex(tsxSource) — specialized linters that flag raw z-index integer values not coming from the design token scale.
@takazudo/remark-source-line
remark plugin that stamps data-source-line="<n>" on every block-level mdast node. The attribute is used by the cross-pane search orchestrator (@takazudo/cross-pane-search) to locate the preview DOM element corresponding to a given editor source line.
Usage
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import remarkSourceLine from "@takazudo/remark-source-line";
const file = await unified()
.use(remarkParse)
.use(remarkSourceLine)
.use(remarkRehype)
.process(markdownSource);The default export is the remark plugin. No named exports.
Stamped node types
Stamps every block-level mdast node: paragraph, heading, code, blockquote, list, listItem, table, tableRow, tableCell, thematicBreak, footnoteDefinition, containerDirective, leafDirective. Inline nodes are not stamped — they inherit the block ancestor's line via DOM ancestry.
Multi-line blocks use the start line (position.start.line, 1-based).
Query-time contract (innermost-anchor selector)
Because nested blocks both carry a stamp, the cross-pane search orchestrator must query with:
[data-source-line="N"]:not(:has([data-source-line="N"]))to select the innermost element for line N and avoid false multi-matches. GFM table cells are an exception — sibling cells on the same row share the same line number and both match; the orchestrator handles this by walking all candidates in document order.
Zero dependencies (only unist-util-visit as a dev/peer). Works in any unified pipeline.
Shared discovery primitives
@takazudo/ui-components exports EmptyState for illustrated guidance, primary/secondary actions and optional Manual access. Its callback receives manualHref verbatim. Renderer callers use the central openManual helper with a relative manual path, rather than resolving the URL twice.
@takazudo/frameset exports FrameHeaderActionGroup and FrameHeaderActionButton; @takazudo/view-provider exposes the optional HeaderActions slot and registry capability metadata. See Frame Component Contract.
The Help catalog and Dashboard widget registry are renderer data, not separate workspace packages. Add feature definitions in tauri-; the Help dialog, empty-frame picker and discovery widgets reuse those definitions and illustrations.