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