import { IconCheck, IconPlus, IconX } from "@tabler/icons-react"; import { cn } from "../../utils.js"; import { defineBlock } from "../types.js"; import type { BlockReadProps, BlockEditProps } from "../types.js"; import { checklistSchema, checklistMdx, type ChecklistData, type ChecklistItem, } from "./checklist.config.js"; /** * Standard `checklist` block. A list of toggleable items, each with a label and * an optional note. Lives in core so any app can register it. * * `Read` mirrors the legacy plan `PlanBlockView` checklist branch byte-for-byte * (same `plan-block` section, toggle buttons, `IconCheck` marker, and the * existing toggle-via-`onChange` behavior) so converting the block to the * registry does not change rendered output. The plan CSS classes * (`plan-block`, `text-plan-*`, `border-plan-line`) resolve against the plan * app's stylesheet at render time, exactly as before. * * `Edit` is a custom editor (the schema auto-editor can't edit an array of * objects): it lets you add, remove, toggle, and relabel items inline. */ /** Mint a reasonably-unique item id without pulling a dep into core. */ function newItemId(): string { return `item-${Math.random().toString(36).slice(2, 10)}`; } /** * Read renderer. Note `onToggle` is supplied by the block dispatcher for the * historical click-to-toggle behavior; in pure read contexts it is omitted and * the markers render statically. */ export function ChecklistBlock({ data, blockId, title, onToggle, }: BlockReadProps & { onToggle?: (itemId: string) => void; }) { return (
{title &&
{title}
}
{data.items.map((item) => onToggle ? ( ) : (
), )}
); } function ChecklistMarker({ checked }: { checked?: boolean }) { return ( {checked && } ); } function ChecklistItemBody({ item }: { item: ChecklistItem }) { return ( {item.label} {item.note && ( {item.note} )} ); } /** Custom editor: toggle, relabel, add, and remove items. */ export function ChecklistEditor({ data, onChange, editable, }: BlockEditProps) { const items = data.items; const update = (next: ChecklistItem[]) => onChange({ items: next }); const toggle = (id: string) => update( items.map((item) => item.id === id ? { ...item, checked: !item.checked } : item, ), ); const setLabel = (id: string, label: string) => update(items.map((item) => (item.id === id ? { ...item, label } : item))); const remove = (id: string) => update(items.filter((item) => item.id !== id)); const add = () => update([...items, { id: newItemId(), label: "", checked: false }]); return (
{items.map((item) => (
setLabel(item.id, event.target.value)} />
))}
); } /** * The standard checklist block spec (with React `Read`/`Edit`). Apps register * this in their browser registry. The schema + MDX config come from * `./checklist.config.ts`, the exact same object server / agent code registers, * so rendering and source round-trip never drift. * * `Read` is typed against `BlockReadProps`; the optional * `onToggle` the dispatcher injects for the legacy click-to-toggle behavior is * an extra prop the registry's `BlockView` passes through harmlessly when * present (it lives outside `BlockReadProps`, so it's wired by the app's block * dispatch rather than the generic `BlockView`). */ export const checklistBlock = defineBlock({ type: "checklist", schema: checklistSchema, mdx: checklistMdx, Read: ChecklistBlock as never, Edit: ChecklistEditor, placement: ["block"], editSurface: "inline", // A checklist maps to NFM to-do items, so it round-trips to Notion. notionCompatible: true, label: "Checklist", icon: IconCheck, description: "A list of toggleable items, each with a label and an optional note.", empty: () => ({ items: [] }), });