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