From ae8856156816af9fc073fd34afdf0e6c176b7170 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sun, 5 Jul 2026 11:17:18 +0000 Subject: [PATCH] Add interaction redesign lab with 10 lighter design variants (PAP-12679) Storybook lab page rendering ten candidate treatments for issue-thread interaction cards against the shared fixture trio (pending questions, pending checkbox confirmation, resolved plan approval). Co-Authored-By: Paperclip --- .../interaction-redesign-lab.stories.tsx | 1410 +++++++++++++++++ 1 file changed, 1410 insertions(+) create mode 100644 ui/storybook/stories/interaction-redesign-lab.stories.tsx diff --git a/ui/storybook/stories/interaction-redesign-lab.stories.tsx b/ui/storybook/stories/interaction-redesign-lab.stories.tsx new file mode 100644 index 00000000000..46ba104566f --- /dev/null +++ b/ui/storybook/stories/interaction-redesign-lab.stories.tsx @@ -0,0 +1,1410 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + Check, + ChevronDown, + ChevronRight, + CircleHelp, + ListChecks, + MessageSquareQuote, + SquareCheck, +} from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + pendingAskUserQuestionsInteraction, + pendingRequestCheckboxConfirmationInteraction, + planApprovalAcceptedRequestConfirmationInteraction, +} from "@/fixtures/issueThreadInteractionFixtures"; +import { cn } from "@/lib/utils"; + +/** + * Interaction Redesign Lab — PAP-12679 (parent PAP-12669). + * + * Ten candidate "lighter" treatments for issue-thread interaction cards, all + * rendered from the same shared fixtures so visual weight can be compared + * directly. Each variant renders the same trio: + * + * 1. pending ask_user_questions (two questions, single + multi select) + * 2. pending request_checkbox_confirmation (four options) + * 3. resolved (accepted) plan request_confirmation + * + * These are lab prototypes, not production components. The winning direction + * gets implemented for real in IssueThreadInteractionCard in P4. + */ + +// --------------------------------------------------------------------------- +// Shared fixture data +// --------------------------------------------------------------------------- + +const qInteraction = pendingAskUserQuestionsInteraction; +const qPayload = qInteraction.payload; +const cbInteraction = pendingRequestCheckboxConfirmationInteraction; +const cbPayload = cbInteraction.payload; +const cbOptions = cbPayload.options ?? []; +const doneInteraction = planApprovalAcceptedRequestConfirmationInteraction; + +const AGENT_NAME = "CodexCoder"; +const RESOLVER_NAME = "Riley Board"; +const CREATED_LABEL = "Apr 20"; +const RESOLVED_LABEL = "Apr 20"; + +// --------------------------------------------------------------------------- +// Shared local state helpers (lab-only interactivity) +// --------------------------------------------------------------------------- + +function useAnswers() { + const [answers, setAnswers] = useState>({}); + function toggle(questionId: string, optionId: string, mode: "single" | "multi") { + setAnswers((prev) => { + const current = prev[questionId] ?? []; + if (mode === "single") { + return { ...prev, [questionId]: current[0] === optionId ? [] : [optionId] }; + } + return { + ...prev, + [questionId]: current.includes(optionId) + ? current.filter((id) => id !== optionId) + : [...current, optionId], + }; + }); + } + return { answers, toggle }; +} + +function useChecked() { + const [checked, setChecked] = useState>(new Set()); + function toggle(id: string) { + setChecked((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + } + return { checked, toggle }; +} + +// --------------------------------------------------------------------------- +// Thread scaffolding so every variant is judged in feed context +// --------------------------------------------------------------------------- + +function ThreadComment({ + author, + initials, + body, + time, +}: { + author: string; + initials: string; + body: string; + time: string; +}) { + return ( +
+
+ {initials} +
+
+
+ {author} + {time} +
+

{body}

+
+
+ ); +} + +function ThreadScaffold({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} + +
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 01 — Compact card +// Keep the card, shrink everything: single-line header, xs type, tight rows. +// --------------------------------------------------------------------------- + +function CompactHeader({ + icon: Icon, + title, + status, + statusTone, +}: { + icon: React.ComponentType<{ className?: string }>; + title: string; + status: string; + statusTone: "pending" | "done"; +}) { + return ( +
+ + {title} + + {status} + + + {AGENT_NAME} · {CREATED_LABEL} + +
+ ); +} + +function Variant01Compact() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+ +
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+
+ + +
+
+ +
+ +
+ {cbOptions.map((option) => ( + + ))} +
+
+ {checked.size} selected +
+ + +
+
+
+ +
+
+ + {doneInteraction.title} + + Approved by {RESOLVER_NAME} · {RESOLVED_LABEL} + +
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 02 — Borderless inline +// No card chrome at all; a 2px status-colored accent bar carries identity. +// --------------------------------------------------------------------------- + +function Variant02Borderless() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+
+ Questions · {AGENT_NAME} · {CREATED_LABEL} +
+
{qInteraction.title}
+
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+
+ + +
+
+ +
+
+ Confirm selection · {AGENT_NAME} · {CREATED_LABEL} +
+
{cbPayload.prompt}
+
+ {cbOptions.map((option) => ( + + ))} +
+
+ + +
+
+ +
+ + + Plan approved — {RESOLVER_NAME}, {RESOLVED_LABEL} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 03 — Collapsed by default +// One-line summary row; expanding reveals the form. Resolved is permanently one line. +// --------------------------------------------------------------------------- + +function CollapsedRow({ + icon: Icon, + title, + meta, + tone, + defaultOpen, + children, +}: { + icon: React.ComponentType<{ className?: string }>; + title: string; + meta: string; + tone: "pending" | "done"; + defaultOpen?: boolean; + children?: React.ReactNode; +}) { + const [open, setOpen] = useState(defaultOpen ?? false); + const expandable = Boolean(children); + return ( +
+ + {expandable && open ?
{children}
: null} +
+ ); +} + +function Variant03Collapsed({ expandFirst }: { expandFirst?: boolean }) { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+ +
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+ + +
+
+
+ + +
+ {cbOptions.map((option) => ( + + ))} +
+
+ + +
+
+ + +
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 04 — Chat-native +// The interaction is an agent message bubble with an inline action row. +// --------------------------------------------------------------------------- + +function AgentBubble({ children }: { children: React.ReactNode }) { + return ( +
+
+ CX +
+
+
+ {AGENT_NAME} + 2:18 PM +
+
{children}
+
+
+ ); +} + +function Variant04ChatNative() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+ +

{qPayload.title}

+
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+
+ + 2 required +
+
+ + +

{cbPayload.prompt}

+
+ {cbOptions.map((option) => ( + + ))} +
+
+ + +
+
+ +
+
+ CX +
+
+
+ {AGENT_NAME} + 2:30 PM +
+
+

{doneInteraction.payload.prompt}

+
+
+ + Approved by {RESOLVER_NAME} · {RESOLVED_LABEL} +
+
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 05 — Trimmed body + footer bar +// Prompt + controls only; secondary copy behind "Show details"; actions live +// in a compact muted footer bar. +// --------------------------------------------------------------------------- + +function DetailsDisclosure({ text }: { text: string }) { + const [open, setOpen] = useState(false); + return ( +
+ + {open ?

{text}

: null} +
+ ); +} + +function Variant05TrimmedFooter() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+
+
{qInteraction.title}
+
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} + +
+
+
+ + {AGENT_NAME} asked · {CREATED_LABEL} + +
+ + +
+
+
+ +
+
+
{cbPayload.prompt}
+
+ {cbOptions.map((option) => ( + + ))} +
+
+ +
+
+
+ {checked.size} selected +
+ + +
+
+
+ +
+ {doneInteraction.title} + + + Approved · {RESOLVER_NAME} · {RESOLVED_LABEL} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 06 — Dense two-column form +// Label left, control right; divider rows like a properties panel. +// --------------------------------------------------------------------------- + +function TwoColRow({ label, children }: { label: React.ReactNode; children: React.ReactNode }) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +function Variant06DenseTwoColumn() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+
+ {qInteraction.title} + + {AGENT_NAME} · {CREATED_LABEL} + +
+
+ {qPayload.questions.map((question) => ( + +
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+ + +
+
+
+ +
+
+ {cbInteraction.title} + + {AGENT_NAME} · {CREATED_LABEL} + +
+
+ +
+ {cbOptions.map((option) => ( + + ))} +
+
+
+ + +
+
+
+ +
+ + + + Approved · {RESOLVER_NAME} · {RESOLVED_LABEL} + + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 07 — Chip answers +// Every option is a selectable pill; forms read as sentences plus chips. +// --------------------------------------------------------------------------- + +function Chip({ + selected, + onClick, + children, +}: { + selected?: boolean; + onClick?: () => void; + children: React.ReactNode; +}) { + return ( + + ); +} + +function Variant07Chips() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+
{qInteraction.title}
+
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => ( + toggle(question.id, option.id, question.selectionMode)} + > + {option.label} + + ))} +
+
+ ))} +
+
+ + {AGENT_NAME} · {CREATED_LABEL} + + +
+
+ +
+
{cbPayload.prompt}
+
+ {cbOptions.map((option) => ( + toggleChecked(option.id)}> + {option.label} + + ))} +
+
+ {checked.size} selected +
+ + +
+
+
+ +
+ {doneInteraction.title} + + + Approved + + + {RESOLVER_NAME} · {RESOLVED_LABEL} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 08 — Banner + disclosure +// A thin one-line banner in the thread; the full form opens in a popover. +// --------------------------------------------------------------------------- + +function Variant08Banner() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+ + {qInteraction.title} + + + + + +
{qPayload.title}
+
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+
+ +
+
+
+
+ +
+ + {cbInteraction.title} + + + + + +
{cbPayload.prompt}
+
+ {cbOptions.map((option) => ( + + ))} +
+
+ + +
+
+
+
+ +
+ + {doneInteraction.title} + + Approved · {RESOLVER_NAME} · {RESOLVED_LABEL} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 09 — Split surface +// One-line stub in the thread; the whole form lives in a dialog. +// --------------------------------------------------------------------------- + +function Variant09Split({ dialogOpen }: { dialogOpen?: boolean }) { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+ + + {AGENT_NAME} asks: {qInteraction.title} + + + + + + + + {qPayload.title} + +
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+ {question.helpText ? ( +

{question.helpText}

+ ) : null} +
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+ + +
+
+
+
+
+ +
+ + + {AGENT_NAME} needs a selection: {cbInteraction.title} + + + + + + + + {cbInteraction.title} + +
+ {cbOptions.map((option) => ( + + ))} +
+
+ + +
+
+
+
+ +
+ + + {doneInteraction.title} — approved by {RESOLVER_NAME},{" "} + {RESOLVED_LABEL} + +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Variant 10 — Minimal monochrome +// No icons, no badges, no status color. Typography-only hierarchy. +// --------------------------------------------------------------------------- + +function Variant10Monochrome() { + const { answers, toggle } = useAnswers(); + const { checked, toggle: toggleChecked } = useChecked(); + + return ( +
+
+
+ Questions — {AGENT_NAME}, {CREATED_LABEL} +
+
{qInteraction.title}
+
+ {qPayload.questions.map((question) => ( +
+
{question.prompt}
+
+ {(question.options ?? []).map((option) => { + const selected = (answers[question.id] ?? []).includes(option.id); + return ( + + ); + })} +
+
+ ))} +
+
+ + +
+
+ +
+
+ Confirm selection — {AGENT_NAME}, {CREATED_LABEL} +
+
{cbPayload.prompt}
+
+ {cbOptions.map((option) => { + const selected = checked.has(option.id); + return ( + + ); + })} +
+
+ + +
+
+ +
+
+ {doneInteraction.title} — approved by {RESOLVER_NAME},{" "} + {RESOLVED_LABEL}. +
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Story plumbing +// --------------------------------------------------------------------------- + +const variants: Array<{ + index: number; + name: string; + thesis: string; + render: () => React.ReactNode; +}> = [ + { + index: 1, + name: "Compact card", + thesis: "Keep card identity; cut padding, type scale, badges. Resolved is a single line inside card chrome.", + render: () => , + }, + { + index: 2, + name: "Borderless inline", + thesis: "No card chrome. A 2px status accent bar and small caps eyebrow carry all identity.", + render: () => , + }, + { + index: 3, + name: "Collapsed by default", + thesis: "One-line rows that expand to act; resolved rows are permanently one line. (First row shown expanded.)", + render: () => , + }, + { + index: 4, + name: "Chat-native", + thesis: "Interactions are agent message bubbles with inline options and an action row; resolved is a reply chip.", + render: () => , + }, + { + index: 5, + name: "Trimmed body + footer bar", + thesis: "Prompt and controls only; secondary copy behind 'Show details'; actions in a muted footer bar.", + render: () => , + }, + { + index: 6, + name: "Dense two-column form", + thesis: "Properties-panel grammar: question label left, controls right, divider rows.", + render: () => , + }, + { + index: 7, + name: "Chip answers", + thesis: "Options are selectable pills; the form reads as prompts plus chips, no option descriptions.", + render: () => , + }, + { + index: 8, + name: "Banner + disclosure", + thesis: "Thin one-line banners in the thread; the full form opens in a popover. (First popover shown open.)", + render: () => , + }, + { + index: 9, + name: "Split surface", + thesis: "One-line stubs in the thread; forms live in a dialog. Thread cost is near zero.", + render: () => , + }, + { + index: 10, + name: "Minimal monochrome", + thesis: "No icons, badges, or status color. Typography-only hierarchy with mono checkmarks.", + render: () => , + }, +]; + +function StoryFrame({ children }: { children: React.ReactNode }) { + return ( +
+
{children}
+
+ ); +} + +function VariantPage({ index }: { index: number }) { + const variant = variants[index - 1]; + return ( + +
+
+
+ Redesign lab · Variant {String(variant.index).padStart(2, "0")} +
+

{variant.name}

+

{variant.thesis}

+
+
+ {variant.render()} +
+
+
+ ); +} + +const meta = { + title: "Issue Thread/Interaction Redesign Lab", + parameters: { + layout: "fullscreen", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const AllVariants: Story = { + render: () => ( + +
+
+
PAP-12679 · Redesign lab
+

Interaction redesign lab — all 10 variants

+

+ Every variant renders the identical fixture trio: pending questions, pending checkbox confirmation, + resolved plan approval. Compare vertical cost, scannability, and how quiet the resolved state gets. +

+
+
+ {variants.map((variant) => ( +
+
+ + {String(variant.index).padStart(2, "0")} + +

{variant.name}

+

{variant.thesis}

+
+ {variant.render()} +
+ ))} +
+
+
+ ), +}; + +export const V01CompactCard: Story = { render: () => }; +export const V02BorderlessInline: Story = { render: () => }; +export const V03CollapsedByDefault: Story = { render: () => }; +export const V04ChatNative: Story = { render: () => }; +export const V05TrimmedFooterBar: Story = { render: () => }; +export const V06DenseTwoColumn: Story = { render: () => }; +export const V07ChipAnswers: Story = { render: () => }; +export const V08BannerDisclosure: Story = { render: () => }; +export const V09SplitSurface: Story = { render: () => }; +export const V09SplitSurfaceDialogOpen: Story = { + render: () => ( + +
+
+
Redesign lab · Variant 09
+

Split surface — dialog open

+
+
+ + + +
+
+
+ ), +}; +export const V10MinimalMonochrome: Story = { render: () => };