///
/**
* 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;