mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:41:32 +01:00
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:
1 parent
b3cf40aa29
commit
c295c19985
5 files changed
+79
-21
No files matched your search
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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>}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user