import { computed, handler, ifElse, NAME, pattern, Stream, UI, type VNode, wish, Writable, } from "commonfabric"; import type { BackwardsCompatibleProfile, SetProfileBioEvent, SetProfileNameEvent, SetProfilePictureEvent, } from "./profile-home.tsx"; /** * Profile embed (CT-1833) — a clean, host-embeddable presentation of the * VIEWER's own profile. * * Mechanism decision (Ben): this is a SEPARATE labs-owned presentation pattern, * NOT a new UI-variant kind, NOT a Lit context, NOT a wish-query extension. UI * variants (chip/tile) stay a pure size spectrum; "which chrome" is expressed by * choosing which pattern a host mounts. A host (e.g. Loom's "Your Profile") * instantiates this pattern standalone and mounts it at a known location; it can * also be linked from the main profile surface later. * * The pattern takes NO profile input — it wishes `#profile` itself and renders * the viewer's profile. The profile it renders is always the viewer's own * (`#profile` resolves the viewer's default profile), so "owner" reduces to * "profile resolved": once `result` is present the viewer is the owner and gets * the amend-in-place edit affordance. Visitors never reach this pattern with * someone else's profile — there is no profile input to point elsewhere. * * Presentation is deliberately minimal and theme-token compliant: a * `` bound to the REAL live profile cell (the * blessed identity idiom — bind the cell, not a snapshot) plus a bio paragraph. * Pinned elements / "Pin a piece" developer chrome are HIDDEN in this v1 (it is * a presentation of upstream-owned data, not a second source of truth). * * Editing is amend-in-place: prefilled draft cells (seeded from the current * values when the viewer enters edit mode) write back through the profile's * EXPORTED owner-protected streams (`setName` / `setPicture` / `setBio`) on the * wish `result`. Cross-pattern stream dispatch is the sanctioned path; this * pattern owns NO copy of name/picture/bio beyond those transient drafts. * CT-1828's empty-string guards make an accidental empty submit safe, and the * UI additionally avoids sending empty names. */ // The wish `result` for `#profile` is the profile-home pattern's output: it // carries the readable `name`/`picture`/`bio` fields AND the exported // owner-protected write streams we amend through. type ProfileResult = BackwardsCompatibleProfile; const trimmed = (value?: string): string => (value ?? "").trim(); /** * Enter edit mode and seed the draft inputs from the current profile values so * the viewer amends (not retypes from blank). Seeding is done here — at the * transition into edit mode — rather than with a two-way `$value` onto the * upstream (owner-protected) cells, which CFC would reject. */ const startEditing = handler< void, { editing: Writable; nameDraft: Writable; bioDraft: Writable; currentName?: string; currentBio?: string; } >((_event, state) => { state.nameDraft.set(trimmed(state.currentName)); state.bioDraft.set(trimmed(state.currentBio)); state.editing.set(true); }); const stopEditing = handler }>( (_event, state) => { state.editing.set(false); }, ); /** * Amend the profile name: reads the draft and dispatches into the profile's * exported `setName` stream (the one owner-authorized writer). Empty/whitespace * sends are suppressed here too so the UI never asks the upstream guard to * reject — a name is never intentionally cleared. */ const saveName = handler< void, { draft: Writable; setName?: Stream } >((_event, state) => { const name = trimmed(state.draft.get()); if (!name) return; state.setName?.send({ name }); }); /** Amend the picture with the image `` hands on. */ const savePicture = handler< SetProfilePictureEvent, { setPicture?: Stream } >((event, state) => { const picture = event.detail?.bytes; if (!picture?.length) return; state.setPicture?.send({ picture }); }); const saveBio = handler< void, { draft: Writable; setBio?: Stream } >((_event, state) => { // Bio is deliberately clearable (unlike name/picture), so an empty draft is a // valid amend. CT-1828's guards keep this safe upstream. state.setBio?.send({ bio: trimmed(state.draft.get()) }); }); export type ProfileEmbedInput = Record; export type ProfileEmbedOutput = { [NAME]: string; [UI]: VNode; // Whether the viewer's profile has resolved (a profile exists). When false the // wish fallback (the trusted create surface) is rendered. hasProfile: boolean; // Raw edit-mode toggle state (the viewer's intent). isEditing: boolean; }; export default pattern(() => { // Resolve the viewer's own profile. `result` is undefined at zero profiles or // while resolution is pending; `[UI]` is the wish's fallback surface (the // trusted create surface when empty). We render `result ?? fallback`. const profileWish = wish({ query: "#profile" }); // Transient local drafts backing the amend inputs. Not a second source of // truth — seeded from the live values on entering edit mode, cleared/written // through the exported streams on save. const nameDraft = new Writable("").for("nameDraft"); const bioDraft = new Writable("").for("bioDraft"); // View toggle: presentation by default, amend form when the owner opts in. const editing = new Writable(false).for("editing"); const hasProfile = computed(() => profileWish.result !== undefined); const isEditing = computed(() => editing.get() === true); const showEditForm = computed(() => profileWish.result !== undefined && editing.get() === true ); const showPresentation = computed(() => profileWish.result !== undefined && editing.get() !== true ); const bio = computed(() => trimmed(profileWish.result?.bio as string)); const hasBio = computed(() => trimmed(profileWish.result?.bio as string).length > 0 ); const displayName = computed(() => { const name = trimmed(profileWish.result?.name as string); return name.length > 0 ? name : "Profile"; }); return { [NAME]: displayName, hasProfile, isEditing, [UI]: ( { /* No profile yet (or pending): render the wish fallback — the trusted create surface at zero profiles. `result ?? fallback`. */ } {ifElse( hasProfile, null,
{profileWish[UI]}
, )} { /* Clean presentation: hero badge bound to the REAL live profile cell (the wish result) + bio. Elements / "Pin a piece" chrome hidden. */ } {ifElse( showPresentation, {ifElse( hasBio,

{bio}

, null, )} Edit profile
, null, )} { /* Amend-in-place: prefilled drafts write back through the profile's exported owner-protected streams. No developer chrome. */ } {ifElse( showEditForm, Save name Save bio Done , null, )}
), }; });