import { IconCheck, IconCode, IconCopy, IconPencil } from "@tabler/icons-react"; import { useId, useEffect, useMemo, useRef, useState, type ChangeEvent, type UIEvent, } from "react"; import { Popover, PopoverContent, PopoverTrigger, } from "../../components/ui/popover.js"; import { cn } from "../../utils.js"; import { ltrCodeBlockProps } from "../code-block-direction.js"; import { defineBlock } from "../types.js"; import type { BlockReadProps, BlockEditProps } from "../types.js"; import { CodeFilenameLabel } from "./code-filename-label.js"; import { highlightCode, inferLanguageFromFilename, normalizeCodeLanguage, } from "./code-highlight.js"; import { codeSchema, codeMdx, type CodeData } from "./code.config.js"; import { CodeSurface, DEFAULT_CODE_MAX_LINES } from "./HighlightedCode.js"; /** * Standard `code` block (STANDARD core library): THE primitive single code * snippet, used everywhere in plan + content. Notion-style — one border, a * hover-revealed language switcher + copy, and the shared collapse-to-N-lines * read surface. A "file rail" of several files is just the `tabs` primitive * holding `code` blocks; there is no bespoke "code-tabs" container. * * Read = the shared {@link CodeSurface} (Shiki, single border, language label, * "Show N more lines"). Edit = a clean, single-border editable surface (no * drag-to-resize; it auto-grows to its content) with the same hover chrome. */ /** Language options for the hover switcher; "" is the Auto-detect sentinel. */ const CODE_LANGUAGES: ReadonlyArray<{ value: string; label: string }> = [ { value: "", label: "Auto" }, { value: "typescript", label: "TypeScript" }, { value: "javascript", label: "JavaScript" }, { value: "tsx", label: "TSX" }, { value: "jsx", label: "JSX" }, { value: "json", label: "JSON" }, { value: "html", label: "HTML" }, { value: "css", label: "CSS" }, { value: "bash", label: "Bash" }, { value: "python", label: "Python" }, { value: "sql", label: "SQL" }, { value: "yaml", label: "YAML" }, { value: "markdown", label: "Markdown" }, { value: "graphql", label: "GraphQL" }, { value: "go", label: "Go" }, { value: "rust", label: "Rust" }, { value: "diff", label: "Diff" }, ]; function CopyButton({ value }: { value: string }) { const [copied, setCopied] = useState(false); return ( ); } /* ── Read ──────────────────────────────────────────────────────────────────── */ function CodeRead({ data, blockId }: BlockReadProps) { const language = normalizeCodeLanguage(data.language) ?? inferLanguageFromFilename(data.filename) ?? undefined; const hasFilename = Boolean(data.filename?.trim()); return (
{hasFilename && (
)} {!hasFilename && ( )} {data.caption &&

{data.caption}

}
); } /* ── Edit (single border, no resize, auto-grow, hover chrome) ──────────────── */ const SETTINGS_INPUT = "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"; /** Hover "settings" (pencil) → popover to edit the filename + max-lines cap. */ function CodeSettingsPopover({ filename, maxLines, onFilenameChange, onMaxLinesChange, }: { filename?: string; maxLines?: number; onFilenameChange: (filename: string | undefined) => void; onMaxLinesChange: (maxLines: number | undefined) => void; }) { const [filenameDraft, setFilenameDraft] = useState(filename ?? ""); useEffect(() => { setFilenameDraft(filename ?? ""); }, [filename]); const commitFilename = () => { const next = filenameDraft.trim(); onFilenameChange(next || undefined); }; return (
Code block
); } function CodeEditorSurface({ code, language, filename, maxLines, editable, onCodeChange, onLanguageChange, onFilenameChange, onMaxLinesChange, }: { code: string; language?: string; filename?: string; maxLines?: number; editable: boolean; onCodeChange: (code: string) => void; onLanguageChange: (language: string | undefined) => void; onFilenameChange: (filename: string | undefined) => void; onMaxLinesChange: (maxLines: number | undefined) => void; }) { const [expanded, setExpanded] = useState(false); const highlightLayerRef = useRef(null); const selectId = useId(); const resolvedLanguage = normalizeCodeLanguage(language) ?? inferLanguageFromFilename(filename); const highlighted = useMemo( () => highlightCode(code, resolvedLanguage), [resolvedLanguage, code], ); // Size the editor to its content by line count — deterministic, no layout // measurement. `wrap="off"` means one row per line. Long snippets collapse to // `cap` lines behind a "Show N more lines" toggle, matching the read surface // and the file-tree block. `maxLines` omitted ⇒ DEFAULT (40); `0` ⇒ never // collapse (show everything). const lineCount = code ? code.split("\n").length : 1; const cap = maxLines == null ? DEFAULT_CODE_MAX_LINES : maxLines > 0 ? maxLines : null; const collapsible = cap != null && lineCount > cap; const collapsed = collapsible && !expanded; const hiddenLines = collapsible ? lineCount - (cap as number) : 0; const rows = collapsed ? (cap as number) : lineCount + 1; const hasFilename = Boolean(filename?.trim()); const syncScroll = (event: UIEvent) => { const layer = highlightLayerRef.current; if (!layer) return; layer.scrollLeft = event.currentTarget.scrollLeft; layer.scrollTop = event.currentTarget.scrollTop; }; const chrome = ( <> {editable && ( )} ); return (
{hasFilename && (
{chrome}
)}