/// /** * TypePicker Module - Controller pattern for selecting record type * * This is a "controller module" - it doesn't just store data, it ACTS on * the parent container's state. It uses the ContainerCoordinationContext * protocol to modify the parent's entries list. * * Key architecture: * - Receives ContainerCoordinationContext as INPUT * - Context's entries/trashedEntries are Cells that survive serialization * - Can call .get() and .set() on context Cells from handlers * - Trashes itself after applying a template * * See: community-docs/superstitions/2025-12-19-auto-init-use-two-lift-pattern.md */ import { Cell, type Default, handler, NAME, pattern, UI } from "commontools"; import type { ContainerCoordinationContext, ModuleMetadata, } from "./container-protocol.ts"; import { createTemplateModules, getTemplateList, type TemplateDefinition, } from "./record/template-registry.ts"; import type { SubCharmEntry, TrashedSubCharmEntry } from "./record/types.ts"; // ===== Self-Describing Metadata ===== export const MODULE_METADATA: ModuleMetadata = { type: "type-picker", label: "Type Picker", icon: "\u{1F3AF}", // target emoji internal: true, // Don't show in Add dropdown }; // ===== Types ===== interface TypePickerInput { // Container coordination context - passed from parent context: ContainerCoordinationContext; // Internal state dismissed?: Default; } interface TypePickerOutput { dismissed?: Default; } // ===== Handlers ===== // Apply a template and trash self // Note: We find self by type "type-picker" since there should only be one // Handler receives the context components separately for proper typing const applyTemplate = handler< unknown, { entries: Cell; trashedEntries: Cell; // deno-lint-ignore no-explicit-any createModule: any; templateId: string; } >((_event, { entries, trashedEntries, createModule, templateId }) => { const current = entries.get() || []; // Find and keep the notes module (should be first) const notesEntry = current.find((e) => e?.type === "notes"); if (!notesEntry) { console.warn("TypePicker: No notes module found, cannot apply template"); return; } // Find self by type (there should only be one type-picker) const selfEntry = current.find((e) => e?.type === "type-picker"); // Create factory for Notes using context's createModule const createNotesCharm = () => createModule("notes"); // Create template modules (skip notes since we keep existing one) const templateEntries = createTemplateModules(templateId, createNotesCharm); const newModules = templateEntries.filter((e) => e.type !== "notes"); // Build new list: notes + new template modules (excluding type-picker) const updatedList = [ notesEntry, ...newModules, ...current.filter((e) => e?.type !== "notes" && e?.type !== "type-picker"), ]; entries.set(updatedList); // Trash self if (selfEntry) { const trashedSelf: TrashedSubCharmEntry = { ...selfEntry, trashedAt: new Date().toISOString(), }; trashedEntries.push(trashedSelf); } }); // Dismiss without applying (user can restore from trash) // Note: We find self by type "type-picker" since there should only be one const dismiss = handler< unknown, { entries: Cell; trashedEntries: Cell; } >((_event, { entries, trashedEntries }) => { const current = entries.get() || []; // Find self by type const selfEntry = current.find((e) => e?.type === "type-picker"); if (!selfEntry) return; // Remove from active list entries.set(current.filter((e) => e?.type !== "type-picker")); // Add to trash const trashedSelf: TrashedSubCharmEntry = { ...selfEntry, trashedAt: new Date().toISOString(), }; trashedEntries.push(trashedSelf); }); // ===== The Pattern ===== export const TypePickerModule = pattern( ({ context, dismissed }) => { // Extract context components for handlers const { entries, trashedEntries, createModule } = context; // Get templates to display (excluding blank) const templates = getTemplateList().filter( (t: TemplateDefinition) => t.id !== "blank", ); return { [NAME]: "Choose Type", [UI]: ( What kind of record is this? ✕ {templates.map((template: TemplateDefinition) => ( {template.icon} {template.name} ))} ), dismissed, }; }, ); export default TypePickerModule;