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