/// /** * 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 { action, computed, Default, NAME, pattern, Stream, UI, type VNode, Writable, } 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 ===== export interface SimpleListItem { text: string; indented: Default; done: Default; } export interface SimpleListInput { items?: Writable>; } interface SimpleListOutput { [NAME]: string; [UI]: VNode; items: SimpleListItem[]; summary: string; toggleIndent: Stream<{ index: number }>; setIndent: Stream<{ index: number; indented: boolean }>; deleteItem: Stream<{ index: number }>; addItem: Stream<{ text: string }>; } // ===== The Pattern ===== export const SimpleListModule = pattern< SimpleListInput, SimpleListOutput >(({ items }) => { // Pattern-body actions - preferred for single-use handlers const toggleIndent = action(({ index }: { index: number }) => { 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); }); const setIndent = action( ({ index, indented }: { index: number; indented: boolean }) => { const current = items.get() || []; if (index < 0 || index >= current.length) return; const updated = [...current]; updated[index] = { ...updated[index], indented }; items.set(updated); }, ); const deleteItem = action(({ index }: { index: number }) => { const current = items.get() || []; if (index < 0 || index >= current.length) return; items.set(current.toSpliced(index, 1)); }); const addItem = action(({ text }: { text: string }) => { const trimmed = text.trim(); if (trimmed) { items.push({ text: trimmed, indented: false, done: false }); } }); // 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}`; }); const summary = computed(() => { return (items.get() || []) .map((item) => `${item.done ? "✓" : "○"} ${item.text}`) .join(", "); }); 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)) { setIndent.send({ index, indented: true }); } // Cmd+[ or Ctrl+[ = outdent if (d.key === "[" && (d.metaKey || d.ctrlKey)) { setIndent.send({ index, indented: false }); } }} /> {/* Indent toggle */} {/* Delete - subtle until hover */} ))} {/* Add item input - at bottom for natural list growth */} { const text = e.detail?.message; if (text) { addItem.send({ text }); } }} /> ), items, summary, toggleIndent, setIndent, deleteItem, addItem, }; }); export default SimpleListModule;