import { z } from "zod"; import type { BlockMdxConfig } from "../types.js"; /** * Pure (React-free) part of the PLAN-SPECIFIC `file-tree` block: its data schema * and MDX round-trip config. Shared by the server MDX adapter (`plan-mdx.ts` via * `plan-block-registry.ts`) and the client spec (`planBlocks.tsx`). Keeping this * React-free means importing it into a server module never pulls React into the * Nitro/SSR bundle. * * The block renders a VS Code / GitHub-explorer style file/change tree: a flat * list of `entries` (each a slash-delimited `path` with an optional change kind, * note, and code snippet) from which the RENDERER derives the nested folder tree * — the model never carries the folder structure, only the leaf paths. This keeps * the data lean and the tree always consistent with the paths. * * The schema MUST stay data-compatible with the `file-tree` member of * `planBlockSchema` (`plan-content.ts`), and the MDX `tag` (`FileTree`) + * attribute shape MUST match it so stored `.mdx` round-trips: the whole `entries` * array is one JSON prop (``). */ /** The kind of change applied to a file, driving its change badge. */ export type FileTreeChange = "added" | "modified" | "removed" | "renamed"; export const FILE_TREE_CHANGES: FileTreeChange[] = [ "added", "modified", "removed", "renamed", ]; /** * One file in the tree. `path` is slash-delimited (`src/routes/git.ts`); the * renderer derives the folder structure from its segments. `change` drives the * change badge (A/M/D/R); `note` + `snippet` (with optional `language`) make the * file row expandable to show why it changes and a code preview. */ export interface FileTreeEntry { path: string; change?: FileTreeChange; note?: string; snippet?: string; language?: string; } export interface FileTreeData { /** Optional heading shown above the tree (e.g. "Files touched"). */ title?: string; entries: FileTreeEntry[]; } const entrySchema = z.object({ path: z.string().trim().min(1).max(500), change: z.enum(["added", "modified", "removed", "renamed"]).optional(), note: z.string().trim().max(2_000).optional(), snippet: z.string().max(50_000).optional(), language: z.string().trim().max(40).optional(), }) as z.ZodType; /** * Data-compatible with the inline `file-tree` member of `planBlockSchema` * (`plan-content.ts`). `entries` is required (at least one file); `title` is * optional so a fresh tree validates from a couple of files. */ export const fileTreeSchema = z.object({ title: z.string().trim().max(180).optional(), entries: z.array(entrySchema).min(1).max(200), }) as unknown as z.ZodType; /** * MDX config: `title` is a flat string attribute and the whole `entries` array is * serialized as one JSON prop on a self-closing element — the `` form. `toAttrs` emits `title` then `entries` in a STABLE * order (the shared `prop()` encoder drops `title` when it is undefined). * * `fromAttrs` tolerates missing/partial attributes for backward-compat: a missing * `title` decodes to `undefined` and a missing `entries` decodes to `[]` so a * plan written before this block existed still parses. */ export const fileTreeMdx: BlockMdxConfig = { tag: "FileTree", toAttrs: (data) => ({ title: data.title, entries: data.entries, }), fromAttrs: (attrs) => ({ title: attrs.string("title"), entries: attrs.array("entries") ?? [], }), };