-
-
-
- {meta.label}
+ {/* Content column: a single vertical stack that fills the full width on
+ mobile (no competing right-hand controls) and reads top-to-bottom. */}
+
+ {/* Meta band: identity on the left, recency + overflow on the right.
+ Not part of the clickable headline, so the menu never toggles it. */}
+
+
+
+
+ {meta.label}
+
+ {sevBadge && (
+
+ {sevBadge.label}
- {sevBadge && (
-
- {sevBadge.label}
-
- )}
- {item.relatedIssue?.identifier && (
- e.stopPropagation()}
- >
- {item.relatedIssue.identifier}
-
- )}
-
-
-
-
- {item.subject.title ?? meta.label}
-
-
{detailLine}
-
- {item.project && (
-
- )}
-
+ )}
+ {item.relatedIssue?.identifier && (
+
e.stopPropagation()}
+ >
+ {item.relatedIssue.identifier}
+
+ )}
- {images.length > 0 &&
}
-
-
-
- {/* Controls: kept as siblings (not inside the clickable header) so they
- never toggle expand and stay valid interactive targets. */}
-
-
- {isHidden && snoozedUntil ? (
-
- Reappears {reappearLabel(snoozedUntil)}
-
- ) : (
-
{relativeTime(item.activityAt)}
- )}
- {!isHidden && (
-
-
-
-
-
-
-
- {onSnooze && onSnooze(item, iso)} />}
- onDismiss(item)}>
-
- Dismiss
-
- {href && (
- <>
-
-
- Open source
-
- >
- )}
-
-
- )}
+
+ {isHidden && snoozedUntil ? (
+
+ Reappears {reappearLabel(snoozedUntil)}
+
+ ) : (
+ {relativeTime(item.activityAt)}
+ )}
+ {!isHidden && (
+
+
+
+
+
+
+
+ {onSnooze && onSnooze(item, iso)} />}
+ onDismiss(item)}>
+
+ Dismiss
+
+ {href && (
+ <>
+
+
+ Open source
+
+ >
+ )}
+
+
+ )}
+
-
- {!expanded &&
onToggleExpand(item)} />}
+ {/* Headline — the primary expand target for inline rows. Title now wraps
+ to two lines instead of truncating to a sliver on narrow screens. */}
+
+
+ {item.subject.title ?? meta.label}
+
+
{detailLine}
+
-
- {!inline && href && (
-
-
+ {/* Context row: project identity and evidence thumbnails move below the
+ text so they never squeeze the headline on mobile. */}
+ {(item.project || images.length > 0) && (
+
+ {item.project &&
}
+ {images.length > 0 &&
}
+
+ )}
+
+ {/* Action bar: full-width, thumb-reachable buttons on mobile;
+ right-aligned dense pills on desktop. Sibling of the headline so
+ taps never toggle expand. */}
+ {showActionBar && (
+
+ {showCompact && (
+ onToggleExpand(item)}
+ />
+ )}
+
+ {showOpen && (
+
+
Open
)}
- {isHidden && onRestore && (
- onRestore(item)}>
+ {showRestore && (
+ onRestore(item)}
+ >
Restore
)}
-
+ )}
@@ -325,6 +364,14 @@ function compactDecisionAction(item: AttentionItem, verbId: string): CompactDeci
return null;
}
+/** The compact accept/reject verbs a collapsed row can resolve in place. */
+function collectCompactActions(item: AttentionItem): Array<{ action: CompactDecisionAction; label: string; id: string }> {
+ return item.decisionVerbs.slice(0, 3).flatMap((verb) => {
+ const action = compactDecisionAction(item, verb.id);
+ return action ? [{ action, label: verb.label, id: verb.id }] : [];
+ });
+}
+
function CompactDecisionActions({
item,
companyId,
@@ -336,12 +383,7 @@ function CompactDecisionActions({
}) {
const queryClient = useQueryClient();
const { pushToast } = useToastActions();
- const actions = item.decisionVerbs
- .slice(0, 3)
- .flatMap((verb) => {
- const action = compactDecisionAction(item, verb.id);
- return action ? [{ action, label: verb.label, id: verb.id }] : [];
- });
+ const actions = collectCompactActions(item);
const decision = useMutation
({
mutationFn: (action: CompactDecisionAction) => {
@@ -387,13 +429,14 @@ function CompactDecisionActions({
if (actions.length === 0) return null;
return (
-
+
{actions.map(({ action, id, label }) => (
{
event.stopPropagation();
diff --git a/ui/storybook/stories/what-needs-me.stories.tsx b/ui/storybook/stories/what-needs-me.stories.tsx
index 3724611721..05bc618b14 100644
--- a/ui/storybook/stories/what-needs-me.stories.tsx
+++ b/ui/storybook/stories/what-needs-me.stories.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useState } from "react";
+import { useEffect, useMemo, useState, type ReactNode } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { ArrowUpDown, CheckCircle2, Inbox, Layers, ListFilter } from "lucide-react";
import type { AttentionItem, AttentionSourceKind, AttentionSeverity, InboxDismissalKind } from "@paperclipai/shared";
@@ -504,3 +504,54 @@ export const DismissUndoToast: StoryObj = {
export const ZeroState: Story = {
args: { items: [] },
};
+
+/**
+ * A 390×844 phone frame. Rows use container queries, so the stacked mobile
+ * layout renders here at any Storybook viewport — the row reflows off its own
+ * column width, not the browser width.
+ */
+function PhoneFrame({ children }: { children: ReactNode }) {
+ return (
+
+ );
+}
+
+/** Mobile: the populated queue at phone width — full-width headlines + actions. */
+export const MobilePopulated: StoryObj = {
+ name: "Mobile · Populated",
+ render: () => (
+
+
+
+ ),
+};
+
+/** Mobile: the type-color + detail + thumbnail showcase at phone width. */
+export const MobileShowcase: StoryObj = {
+ name: "Mobile · Type colors & detail",
+ render: () => (
+
+
+
+ ),
+};
+
+/** Mobile: snoozed / dismissed curtains and the restore affordance at phone width. */
+export const MobileCurtains: StoryObj = {
+ name: "Mobile · Curtains",
+ render: () => (
+
+
+
+ ),
+};