Make DocumentRow responsive on mobile (PAP-10572)

Fixes the PAP-10569 mobile regression where DocumentRow collapsed at
390px: the mono doc key overlapped the title, the summary line vanished,
and the status pill / owner / count chips / relative time clipped at the
right edge.

EntityRow's single horizontal flex row gave meta + trailing `shrink-0`
priority, so at narrow widths only the title cell shrank — squeezing it
to ~120px while trailing chips ran off the edge. Add an opt-in
`responsive` prop that stacks the row below `sm` (title → meta →
trailing on their own lines, title keeps full width) and reflows to the
unchanged single-row desktop layout at `sm`+. DocumentRow opts in;
issues/projects lists are untouched.

Also:
- Storybook inline-card fixture rendered a raw "## Goal" markdown literal
  even though production renders via MarkdownBody; use plain prose so
  sign-off screenshots reflect what users actually see.
- IssueDocumentsTab empty state now passes action/onAction so the
  "Add document" affordance sits at the empty state too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-06-07 02:18:11 +00:00
1 parent b3cf40aa29
commit c295c19985
5 files changed
+79 -21

No files matched your search

+20
View File
@@ -46,4 +46,24 @@ describe("EntityRow", () => {
const markup = renderToStaticMarkup(<EntityRow title="Alpha" />);
expect(markup).toContain("min-w-0 flex-1");
});
it("stacks title/meta/trailing below sm when responsive", () => {
const markup = renderToStaticMarkup(
<EntityRow
title="Alpha"
responsive
meta={<span data-testid="meta-cell">draft</span>}
trailing={<span data-testid="trailing-cell">2h</span>}
/>,
);
// inner wrapper stacks on mobile and reflows to a row at sm+
expect(markup).toContain("flex flex-col gap-1.5 sm:flex-row");
// the spacer that pins trailing right only exists at sm+ (no empty line on mobile)
expect(markup).toContain("hidden sm:block sm:flex-1");
// leading column top-aligns while stacked
expect(markup).toContain("items-start sm:items-center");
expect(markup).toContain("meta-cell");
expect(markup).toContain("trailing-cell");
});
});
+50 -20
View File
@@ -20,6 +20,14 @@ interface EntityRowProps {
className?: string;
titleClassName?: string;
reserveSubtitleSpace?: boolean;
/**
* Stack the row vertically below `sm` (title → meta → trailing on their own
* lines) instead of forcing the single horizontal flex layout. Use for rows
* with heavy `meta`/`trailing` (e.g. `DocumentRow`) that otherwise collapse
* the title cell and clip trailing chips on narrow viewports. Desktop layout
* is unchanged.
*/
responsive?: boolean;
}
export function EntityRow({
@@ -35,36 +43,58 @@ export function EntityRow({
className,
titleClassName,
reserveSubtitleSpace,
responsive,
}: EntityRowProps) {
const isClickable = !!(to || onClick);
const classes = cn(
"flex items-center gap-3 px-4 py-2 text-sm border-b border-border last:border-b-0 transition-colors",
"flex gap-3 px-4 py-2 text-sm border-b border-border last:border-b-0 transition-colors",
// Top-align the leading column while stacked so the status dot sits with the
// title line; recenter once the row collapses back to a single line at sm.
responsive ? "items-start sm:items-center" : "items-center",
isClickable && "cursor-pointer hover:bg-accent/50",
selected && "bg-accent/30",
className
);
const content = (
const titleBlock = (
<div className={cn("min-w-0", !meta && "flex-1", titleClassName)}>
<div className="flex items-center gap-2">
{identifier && (
<span className="text-xs text-muted-foreground font-mono shrink-0 relative top-[1px]">
{identifier}
</span>
)}
<span className="truncate" title={title}>{title}</span>
</div>
{(subtitle || reserveSubtitleSpace) && (
<p
className={cn("text-xs text-muted-foreground truncate mt-0.5 min-h-4", !subtitle && "invisible")}
aria-hidden={!subtitle}
>
{subtitle}
</p>
)}
</div>
);
// Responsive variant: below `sm` the title, meta, and trailing stack onto
// their own lines (title cell keeps full width, nothing clips); at `sm`+ the
// inner wrapper reflows to the same single-row layout as the default path
// (content-sized title, meta hugging it, flex-1 spacer, trailing pinned right).
const content = responsive ? (
<>
{leading && <div className="flex items-center gap-2 shrink-0 pt-0.5 sm:pt-0">{leading}</div>}
<div className="min-w-0 flex-1 flex flex-col gap-1.5 sm:flex-row sm:items-center sm:gap-3">
{titleBlock}
{meta && <div className="flex items-center gap-2 shrink-0">{meta}</div>}
<div className="hidden sm:block sm:flex-1" />
{trailing && <div className="flex items-center gap-2 shrink-0">{trailing}</div>}
</div>
</>
) : (
<>
{leading && <div className="flex items-center gap-2 shrink-0">{leading}</div>}
<div className={cn("min-w-0", !meta && "flex-1", titleClassName)}>
<div className="flex items-center gap-2">
{identifier && (
<span className="text-xs text-muted-foreground font-mono shrink-0 relative top-[1px]">
{identifier}
</span>
)}
<span className="truncate" title={title}>{title}</span>
</div>
{(subtitle || reserveSubtitleSpace) && (
<p
className={cn("text-xs text-muted-foreground truncate mt-0.5 min-h-4", !subtitle && "invisible")}
aria-hidden={!subtitle}
>
{subtitle}
</p>
)}
</div>
{titleBlock}
{meta && <div className="flex items-center gap-2 shrink-0">{meta}</div>}
{meta && <div className="flex-1" />}
{trailing && <div className="flex items-center gap-2 shrink-0">{trailing}</div>}
+2
View File
@@ -137,6 +137,8 @@ export function IssueDocumentsTab({ issueId, companyId, issueIdentifier, agentMa
<EmptyState
icon={FileText}
message="Only the plan document is linked. Add a spec, brief, or report to keep more context here."
action="Add document"
onAction={() => setLinkPickerOpen(true)}
/>
)
) : (
@@ -89,6 +89,7 @@ export function DocumentRow({ document, to, owner, identifier, selected, company
<EntityRow
to={to}
selected={selected}
responsive
leading={
<>
<span
@@ -156,8 +156,13 @@ function InlineDocumentCard({ doc, docKey }: { doc: CompanyDocumentSummary; docK
</div>
</div>
<div className="mt-3 text-[15px] leading-7 text-muted-foreground">
{/*
Production renders the body through `MarkdownBody`; this static fixture uses
plain prose (no raw markdown literal) so the sign-off screenshot reflects what
users actually see rather than printing unrendered "## Goal" syntax.
*/}
{docKey === "plan"
? "## Goal\nPreserve inline plan editing while making linked docs discoverable…"
? "Preserve inline plan editing while making linked docs discoverable from the issue."
: doc.summary}
</div>
</div>