/// /** * Tags Module - Pattern for tag/label management * * A composable pattern that can be used standalone or embedded in containers * like Record. Provides add/remove tag functionality with chip display. */ 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: "tags", label: "Tags", icon: "\u{1F3F7}", // label emoji schema: { tags: { type: "array", items: { type: "string" }, description: "Tags" }, }, fieldMapping: ["tags"], }; // ===== Types ===== export interface TagsModuleInput { /** Tags or labels */ tags: Default; } // ===== Handlers ===== // Handler to add a new tag const addTag = handler< unknown, { tags: Cell; tagInput: Cell } >((_event, { tags, tagInput }) => { const newTag = tagInput.get().trim(); if (!newTag) return; const current = tags.get() || []; if (!current.includes(newTag)) { tags.set([...current, newTag]); } tagInput.set(""); }); // Handler to remove a tag by index const removeTag = handler< unknown, { tags: Cell; index: number } >((_event, { tags, index }) => { const current = tags.get() || []; tags.set(current.toSpliced(index, 1)); }); // ===== The Pattern ===== export const TagsModule = recipe( "TagsModule", ({ tags }) => { const tagInput = Cell.of(""); const displayText = computed(() => { const count = (tags || []).length || 0; return count > 0 ? `${count} tag${count !== 1 ? "s" : ""}` : "No tags"; }); return { [NAME]: computed(() => `${MODULE_METADATA.icon} Tags: ${displayText}`), [UI]: ( {/* Tag input */} Add {/* Tag chips */} {tags.map((tag: string, index: number) => ( {tag} ))} ), tags, }; }, ); export default TagsModule;