/// /** * Relationship Module - Pattern for people connections * * A composable pattern that can be used standalone or embedded in containers * like Record. Tracks relationship types, closeness, and inner circle status. */ 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: "relationship", label: "Relationship", icon: "\u{1F465}", // busts in silhouette emoji schema: { relationTypes: { type: "array", items: { type: "string" }, description: "Relationship types", }, closeness: { type: "string", enum: ["intimate", "close", "casual", "distant"], description: "Closeness level", }, howWeMet: { type: "string", description: "How we met" }, innerCircle: { type: "boolean", description: "Inner circle member" }, }, fieldMapping: ["relationTypes", "closeness", "howWeMet", "innerCircle"], }; // ===== Types ===== /** Closeness level for relationships */ type ClosenessLevel = "intimate" | "close" | "casual" | "distant"; export interface RelationshipModuleInput { /** Relationship types (e.g., friend, family, colleague) */ relationTypes: Default; /** Closeness level */ closeness: Default; /** How we met */ howWeMet: Default; /** Inner circle member */ innerCircle: Default; } // ===== Constants ===== const RELATION_TYPE_OPTIONS = [ "friend", "family", "colleague", "neighbor", "mentor", "mentee", "acquaintance", ]; const CLOSENESS_OPTIONS = [ { value: "", label: "Not set" }, { value: "intimate", label: "💜 Intimate" }, { value: "close", label: "💙 Close" }, { value: "casual", label: "💚 Casual" }, { value: "distant", label: "🤍 Distant" }, ]; // ===== Handlers ===== // Handler to toggle a relation type - type is in context const toggleRelationType = handler< unknown, { relationTypes: Cell; type: string } >((_event, { relationTypes, type }) => { const current = relationTypes.get() || []; if (current.includes(type)) { relationTypes.set(current.filter((t) => t !== type)); } else { relationTypes.set([...current, type]); } }); // Handler to toggle inner circle const toggleInnerCircle = handler< unknown, { innerCircle: Cell } >((_event, { innerCircle }) => { innerCircle.set(!innerCircle.get()); }); // ===== The Pattern ===== export const RelationshipModule = recipe< RelationshipModuleInput, RelationshipModuleInput >( "RelationshipModule", ({ relationTypes, closeness, howWeMet, innerCircle }) => { const displayText = computed(() => { const types = relationTypes || []; const count = types.length || 0; if (count > 0) return types.join(", "); const opt = CLOSENESS_OPTIONS.find((o) => o.value === closeness); return opt?.label || "Not set"; }); return { [NAME]: computed(() => `${MODULE_METADATA.icon} Relationship: ${displayText}` ), [UI]: ( {/* Relation types (multi-select chips) */} {RELATION_TYPE_OPTIONS.map((type, index) => { const isSelected = computed(() => (relationTypes || []).some((t: string) => t === type) ); return ( ); })} {/* Closeness */} {/* How we met */} {/* Inner circle toggle */} Inner Circle ⭐ ), relationTypes, closeness, howWeMet, innerCircle, }; }, ); export default RelationshipModule;