Edit Contact v0 prompt (the Organization Affiliations section, revision 3)
Purpose
This record explains the generation work used to present a contact's organization relationships and edit one relationship in a drawer. It concerns the revision-3 affiliations change to an existing contact screen after Andrew rejected the earlier design. The two prompts and retained notes establish this generation's scope; they do not establish a regeneration of the complete Edit Contact page. The artifact records existing evidence rather than submitting a new run.
What the Prompt Was Generated From
The prompt describes Andrew's revision-3 direction: one affiliation section with explicit current and historical relationships and an optional primary relationship. It specifies presentational contracts for the row, section and drawer, cites the console copy rules, and limits the files v0 may inspect. It asks for no data fetching, wiring, dependencies or changes to other screens.
Those contracts appear in the retained prompt files rather than contemporaneous Component Designs in this corpus. The linked Screen Design is retrospective. The prompt and notes support Andrew's rejection of the earlier structure, but this review does not identify the individual who composed each prompt sentence. The missing affiliation component registrations remain a finding.
The Prompt
Redesign, revision 3: **Organization Affiliations** on a contact, inside the existing Rhythm React console (Next.js App Router, React 19, Tailwind v4, shadcn on Base UI — `@base-ui/react`, NOT Radix — lucide icons). This branch holds a first design Andrew REJECTED: an identity-band Organization field, an Organization card with "use organization address/phone" toggles, an "Additional Affiliations" section and an "Organization History" tab. Do not reuse that structure under new labels (do not read or copy `components/affiliations/affiliation-list.tsx`, `affiliation-row-editor.tsx`, `add-another-organization.tsx` or `primary-organization-change.tsx`). He wants ONE straightforward feature: every relationship in one place, past ones visibly historical, one optionally primary.
Rules: presentational components only, no data fetching, no wiring, no new dependencies, do not edit `components/ui/*`, do not touch any other screen. Follow `AGENTS.md` copy rules (Title Case headings and buttons, sentence-case helper text, `cursor-pointer` on custom clickables). Read at most `components/ui/badge.tsx` and `components/ui/dropdown-menu.tsx`, then write immediately: this turn must end with the two files below written.
## Write `components/affiliations/affiliation-row.tsx`
```ts
export type AffiliationStatus = "current" | "past" | "future"
/** One affiliation, already resolved to display strings by the caller. */
export interface AffiliationRowView {
id: string
organizationName: string
/** Link target for the organization name, when it has a page. */
organizationHref?: string
/** "Employer", or "Board Member (inactive)" for a deactivated type. */
typeName: string
jobTitle: string | null
/** Role names; `dates` only when the role has its own dates, e.g. "2016 – 2019". */
roles: { id: string; name: string; dates?: string }[]
/** Display strings, already formatted: "Mar 1, 2014" or null. */
startDate: string | null
/** Inclusive; null while it continues. */
endDate: string | null
status: AffiliationStatus
isPrimary: boolean
/** The contact-information rows linked through this affiliation, already worded: ["Address: Headquarters → Business", "Phone: Main Line → Direct"]. Empty = "Address: Do not link · Phone: Do not link" is passed explicitly by the caller as strings too. */
links: string[]
}
export interface AffiliationRowProps {
row: AffiliationRowView
/** Opens the affiliation editor. Without it the row is read-only. */
onEdit?: () => void
/** Offered for a current, non-primary, open-ended affiliation. */
onMakePrimary?: () => void
onRemove?: () => void
/** A validation problem on this affiliation, shown on the row. */
issue?: string
/** "compact" for dense lists (an organization's people). */
density?: "default" | "compact"
/** Replaces the organization name line, e.g. the person's name on an organization's people list. */
title?: React.ReactNode
className?: string
}
export function AffiliationRow(props: AffiliationRowProps): JSX.Element
/** The explicit status label: "Current", "Past" with "Ended Jun 30, 2019", "Starts Nov 1, 2026". Never colour alone: icon + word. */
export function AffiliationStatusLabel(props: { status: AffiliationStatus; startDate: string | null; endDate: string | null }): JSX.Element
export function AffiliationPrimaryBadge(): JSX.Element
export function AffiliationRoleList(props: { roles: AffiliationRowView["roles"] }): JSX.Element
/** "Mar 1, 2014 – Jun 30, 2019", "Since Mar 1, 2019", "No dates set". */
export function AffiliationDateRange(props: { startDate: string | null; endDate: string | null }): JSX.Element
/** The compact link summary line: "Address: Headquarters → Business · Phone: Do not link", link icon, muted small text. */
export function AffiliationLinkSummary(props: { links: string[] }): JSX.ElementOne row (a bordered rounded row with a building icon, same visual language as the console's other record lists): organization name (semibold, a link when organizationHref), a Primary badge when primary (star icon + the word "Primary"), the affiliation type as a quiet outline badge, the job title, the date range, the explicit status label, the role chips, and the link summary line. A past row gets a historical treatment that stays fully readable: a muted background and a dashed border, the "Past" label with a history icon and "Ended <date>"; never colour alone, never strikethrough, never collapsed or truncated text. Actions on the right: an Edit icon button (pencil, tooltip, accessible name "Edit <organization> affiliation") and an overflow menu (accessible name "More actions for <organization>") holding Make Primary (only when onMakePrimary) and Remove (only when onRemove, destructive tone). The whole row is not clickable. data-testid="affiliation-row", data-status, data-primary.
Write components/affiliations/organization-affiliations-section.tsx
import type { AffiliationRowView } from "./affiliation-row"
export interface OrganizationAffiliationsSectionProps {
/** Every affiliation, current and past together, ALREADY ORDERED: primary first, then current, then upcoming, then past. */
rows: AffiliationRowView[]
readOnly?: boolean
onAdd?: () => void
onEdit?: (id: string) => void
onMakePrimary?: (id: string) => void
onRemove?: (id: string) => void
/** Issues keyed by affiliation id. */
issues?: Record<string, string>
/** A contact with current affiliations but none primary: shown as a quiet inline notice. */
noPrimaryNotice?: string
/** Where a read-only Contact 360 sends "Manage" (the contact editor), when the viewer may edit. */
manageHref?: string
}
export function OrganizationAffiliationsSection(props: OrganizationAffiliationsSectionProps): JSX.ElementThe INSIDE of a section card (the caller supplies the card and its "Organization Affiliations" title): a slim toolbar with counts ("2 Current · 1 Past"), an optional filter (All / Current / Past, a small segmented control from components/ui/toggle-group.tsx) whose default is All so nothing is hidden by default, and an "Add Affiliation" button (not when readOnly; when read-only and manageHref, a "Manage" link instead); the noPrimaryNotice as a quiet inline notice; then the rows in the given order — past rows after a thin "Past" divider label, in the same list and fully visible. Empty state: "No organization affiliations yet." with the Add button. data-testid="organization-affiliations". Rows get onMakePrimary only when the row is current or upcoming, not primary, and has no end date.
---- Step 2 (sent in the same chat once step 1 settled) ----
Step 2 of 2. Presentational only, as in step 1. Read at most components/ui/sheet.tsx, components/ui/select.tsx, components/ui/checkbox.tsx and components/affiliations/affiliation-row.tsx (yours, from step 1), then write immediately: this turn must end with the file below written.
Write components/affiliations/affiliation-editor.tsx
ONE affiliation editor, a right-hand drawer (Sheet) used both to add and to edit an affiliation. It edits a draft; the contact's own action bar saves the contact later, so the footer is "Cancel" and "Add Affiliation" (add) or "Apply" (edit), with helper text "Changes are saved with the contact." The header is a tinted band (AGENTS.md: dialog headers use a tinted header band) titled "Add Affiliation" / "Edit Affiliation" (never the organization's name in the title) with a one-line description.
There are NO address or phone link controls in this editor. Contact information is linked by reference on the contact's own Addresses and Phone Numbers sections. The editor only (a) says what the affiliation SUPPLIES, and (b) for a NEW affiliation previews the rows its type's defaults will link, each deselectable.
export interface AffiliationDraft {
organizationId: string
organizationName: string
affiliationTypeId: string
isPrimary: boolean
jobTitle: string
startDate: string // YYYY-MM-DD or ""
endDate: string // YYYY-MM-DD or "", inclusive
notes: string
/** Contact-role ids; the caller keeps each role's own dates. */
roleIds: string[]
}
export interface TypeOption { id: string; name: string }
/** One contact-information row the new affiliation's type would link. */
export interface DefaultLinkPreview {
key: string
kind: "address" | "phone"
/** "Link Business Address to Northwind Logistics's Headquarters" */
label: string
/** e.g. "100 Harbor Way, Boston, MA 02110", or why it can't: "Northwind Logistics has no Billing Office address". */
detail?: string
/** "replaces" when the contact row exists but is empty; "skipped" when the row already has a value (never overwritten) — shown, unchecked and disabled, with the reason. */
state: "new" | "replaces" | "skipped" | "missing"
selected: boolean
}
/** A message under one control; "error" blocks applying, "warning" does not. */
export interface EditorMessage { tone: "error" | "warning"; text: string }
export interface AffiliationEditorProps {
open: boolean
onOpenChange: (open: boolean) => void
mode: "add" | "edit"
draft: AffiliationDraft
onDraftChange: (next: AffiliationDraft) => void
onApply: () => void
/** The console's organization picker. */
renderOrganizationPicker: (props: { id: string; value: string | null; onChange: (id: string | null, name: string | null) => void; invalid?: boolean }) => React.ReactNode
/** Active types, plus the held inactive one already labelled "Name (inactive)". */
affiliationTypes: TypeOption[]
contactRoles: TypeOption[]
/** Read-only: what this affiliation supplies to the contact today, e.g. ["Business Address", "Direct Phone"]. Empty = "Supplies no contact information." */
supplies: string[]
/** Add mode only: the rows the chosen type's defaults would link. Empty with a type chosen = "Employer links no contact information by default." */
defaultLinks?: DefaultLinkPreview[]
onToggleDefaultLink?: (key: string, selected: boolean) => void
/** Messages keyed by control. */
messages: Partial<Record<"organizationId" | "affiliationTypeId" | "startDate" | "endDate" | "isPrimary", EditorMessage>>
/** Set when the affiliation has an end date: Primary needs an open-ended affiliation. */
primaryDisabledReason?: string
/** E.g. "Ending Northwind Logistics leaves Amara Okafor with no primary affiliation until you make another one primary." */
primaryNotice?: string
/** When an end date is set on an affiliation that supplies linked rows: ["Business Address", "Direct Phone"] — shown as a warning "These rows will stop resolving: …" listing them. */
stopsResolving?: string[]
}
export function AffiliationEditor(props: AffiliationEditorProps): JSX.ElementBody, in this order, in clear groups with small section headings:
- Organization (the render prop) and Affiliation Type (select).
- Job Title, Start Date, End Date (native date inputs are fine; helper text under End Date: "Leave empty while the affiliation continues. The end date is the last day."). Under End Date, the
stopsResolvingwarning when present (amber, icon, text always present). - Roles: a multi-select of
contactRolesshown as removable chips (a popover with checkable options is fine). - Primary Affiliation: a switch with helper text "The contact's main organization. Only one affiliation can be primary." Disabled with
primaryDisabledReasonshown when set. ShowprimaryNoticeas an inline notice here. - Contact Information: a read-only line "Supplies Business Address, Direct Phone" (link icon) or "Supplies no contact information."; in add mode, under it, a sub-heading "Link When Added" and one checkbox row per
defaultLinksentry: the label in normal text, thedetailmuted under it, a small state tag ("Replaces empty row", "Already has a value — not changed", "No source value"); helper text "From the affiliation type's defaults. Linked rows follow the organization and can be changed later on the contact's Addresses and Phone Numbers." - Notes (textarea).
Every control has a visible label and an id (affiliation-editor-<field>) so validation can focus it; the Apply / Add button is disabled while any message has tone "error". data-testid="affiliation-editor"; each default-link checkbox has data-testid="default-link-<key>".
## Required Behavior and Mock Behavior
The request requires presentational components that accept the stated props, display readable historical relationships, identify status through words and icons, and expose accessible action names. The drawer applies changes to a contact draft; its footer does not save the contact independently. These are the recorded generation requirements, not evidence that a complete accessibility audit passes.
The prompts supply display contracts and examples rather than backend integration. The generated components rely on caller-provided data. The integration notes describe mock fixtures and subsequent form, source-resolution and write wiring outside v0. Mock behavior belongs to that surrounding integration; saying the generation contains no mock data would obscure the illustrative values in the prompt.
## Generation Record
The notes identify chat p8Pdclr2GT9 and its row/section and drawer versions. The driver sends sorted group-a prompt files in sequence, polls for a settled version and copies changed output into its temporary directory. The notes attribute the integrated components to those versions and say rewritten library files and the manifest were excluded.
The retained record dates the work to 2026-10-13 and the reviewed integration to 2026-10-14. Available local git history exposes later retained commits, so it does not independently prove the earlier run timestamp or a precise operator. No run model or project identifier is recorded. The newer one-project policy does not establish which project held this historical chat.
## Evaluation Against the Design
The notes document removal of an unsupported ToggleGroup size prop and exclusion of v0's library and manifest changes. They also attribute direct role checkboxes and the separate Affiliations tab to Andrew on 2026-10-14. The pinned form retains the separate tab, but its drawer now uses dated role rows with selectors, Add Role and Add Period rather than direct role checkboxes. The notes establish the historical integration choices, not unchanged generation fidelity at the current commit or approval of the complete screen artifact.
The corpus Screen Design is retrospective, so this review does not claim a field-by-field evaluation against an approved design at generation time. The prompt record retains its evaluated status and discrepancy resolutions; open differences elsewhere in the contact screen remain in the Screen Implementation and findings.
## Open Questions
The generated affiliation row, section and drawer still need registered component contracts for future screen and prompt reuse. This record establishes their retained generation references but leaves the model, historical project and individual operator unrecorded. The retrofit can resolve component ownership and registration without inventing those missing execution details or treating a retrospective design as prior approval.