/// /** * Address Module - Pattern for physical address with customizable label * * A composable pattern that can be used standalone or embedded in containers * like Record. Stores street, city, state, and ZIP with a label (Home, Work, etc.) */ import { computed, type Default, NAME, recipe, UI } from "commontools"; import type { ModuleMetadata } from "./container-protocol.ts"; // ===== Standard Labels ===== export const STANDARD_LABELS = ["Home", "Work", "Billing", "Shipping", "Other"]; // ===== Self-Describing Metadata ===== export const MODULE_METADATA: ModuleMetadata = { type: "address", label: "Address", icon: "\u{1F4CD}", // pin emoji allowMultiple: true, // Show "add another" button for multiple addresses schema: { street: { type: "string", description: "Street address" }, city: { type: "string", description: "City" }, state: { type: "string", description: "State/Province" }, zip: { type: "string", description: "ZIP/Postal code" }, addressLabel: { type: "string", enum: STANDARD_LABELS, description: "Address label (Home, Work, etc.)", }, }, fieldMapping: ["street", "city", "state", "zip", "label"], }; // ===== Types ===== export interface AddressModuleInput { /** Label for this address (Home, Work, Billing, etc.) */ label: Default; /** Street address */ street: Default; /** City */ city: Default; /** State/Province */ state: Default; /** ZIP/Postal code */ zip: Default; } // ===== The Pattern ===== export const AddressModule = recipe( "AddressModule", ({ label, street, city, state, zip }) => { // Build display text from non-empty fields const displayText = computed(() => { const parts = [city, state].filter((v) => v?.trim()); return parts.length > 0 ? parts.join(", ") : "Not set"; }); // Build autocomplete items from standard labels const labelItems = STANDARD_LABELS.map((l) => ({ value: l, label: l })); return { [NAME]: computed( () => `${MODULE_METADATA.icon} ${label}: ${displayText}`, ), [UI]: (
), label, street, city, state, zip, }; }, ); export default AddressModule;