/// /** * Simple List Module - A checklist with indent support * * A composable pattern that can be used standalone or embedded in containers * like Record. Provides rapid keyboard entry, checkboxes, and indent toggle. */ import { Cell, computed, type Default, handler, NAME, recipe, UI, } from "commontools"; import type { ModuleMetadata } from "./container-protocol.ts"; // ===== Self-Describing Metadata ===== export const MODULE_METADATA: ModuleMetadata = { type: "simple-list", label: "Simple List", icon: "\u2611", // ☑ ballot box with check allowMultiple: true, schema: { items: { type: "array", items: { type: "object", properties: { text: { type: "string", description: "Item text" }, indented: { type: "boolean", description: "Whether item is indented", }, done: { type: "boolean", description: "Whether item is completed" }, }, }, description: "List items", }, }, fieldMapping: ["items", "checklist", "list"], }; // ===== Types ===== interface SimpleListItem { text: string; indented: Default; done: Default; } export interface SimpleListModuleInput { items: Cell>; } // ===== Handlers ===== // Toggle indent on an item const toggleIndent = handler< unknown, { items: Cell; index: number } >((_event, { items, index }) => { const current = items.get() || []; if (index < 0 || index >= current.length) return; const updated = [...current]; updated[index] = { ...updated[index], indented: !updated[index].indented, }; items.set(updated); }); // Delete an item const deleteItem = handler< unknown, { items: Cell; index: number } >((_event, { items, index }) => { const current = items.get() || []; if (index < 0 || index >= current.length) return; items.set(current.toSpliced(index, 1)); }); // ===== The Pattern ===== export const SimpleListModule = recipe< SimpleListModuleInput, SimpleListModuleInput >( "SimpleListModule", ({ items }) => { // Computed summary for NAME const displayText = computed(() => { const list = items.get() || []; const total = list.length; if (total === 0) return "Empty"; const done = list.filter((item) => item.done).length; return `${done}/${total}`; }); return { [NAME]: computed(() => `${MODULE_METADATA.icon} List: ${displayText}`), [UI]: ( {/* List items */} {items.map((item, index: number) => ( {/* Checkbox */} {/* Editable text with Cmd+[ / Cmd+] for indent */} { const d = e.detail; if (!d) return; // Cmd+] or Ctrl+] = indent if (d.key === "]" && (d.metaKey || d.ctrlKey)) { const current = items.get() || []; if (index >= 0 && index < current.length) { const updated = [...current]; updated[index] = { ...updated[index], indented: true }; items.set(updated); } } // Cmd+[ or Ctrl+[ = outdent if (d.key === "[" && (d.metaKey || d.ctrlKey)) { const current = items.get() || []; if (index >= 0 && index < current.length) { const updated = [...current]; updated[index] = { ...updated[index], indented: false }; items.set(updated); } } }} /> {/* Indent toggle */} {/* Delete - subtle until hover */} ))} {/* Add item input - at bottom for natural list growth */} { const text = e.detail?.message?.trim(); if (text) { items.push({ text, indented: false, done: false }); } }} /> ), items, }; }, ); export default SimpleListModule;