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