fix(ui): routines list — border each group as a card, drop row dividers (PAP-10748)

Previously the whole list was one bordered box with a divider line under
every routine row. Per feedback, promote each group into its own bordered
card and remove the per-row dividers so routines sit borderless inside.

- Routines.tsx: stack groups with gap-3; wrap each Collapsible in its own
  rounded border card (overflow-hidden); header border-b only while open.
- RoutineList.tsx: add divider prop (default true to preserve other lists);
  the grouped list passes divider={false}.
- Add storybook story for the grouped-cards layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Opus 4.8 committed 2026-06-09 22:59:27 +00:00
1 parent dd70dd5355
commit 64cb3c1a4e
3 files changed
+162 -45

No files matched your search

+6 -1
View File
@@ -63,6 +63,7 @@ export function RoutineListRow<TRoutine extends RoutineListRowItem>({
disableRunNow = false,
disableToggle = false,
hideArchiveAction = false,
divider = true,
onRunNow,
onToggleEnabled,
onToggleArchived,
@@ -80,6 +81,8 @@ export function RoutineListRow<TRoutine extends RoutineListRowItem>({
disableRunNow?: boolean;
disableToggle?: boolean;
hideArchiveAction?: boolean;
/** Render a bottom divider between consecutive rows. Off when the group is its own card. */
divider?: boolean;
onRunNow: (routine: TRoutine) => void;
onToggleEnabled: (routine: TRoutine, enabled: boolean) => void;
onToggleArchived?: (routine: TRoutine) => void;
@@ -95,7 +98,9 @@ export function RoutineListRow<TRoutine extends RoutineListRowItem>({
return (
<Link
to={href}
className="group flex flex-col gap-3 border-b border-border px-3 py-3 transition-colors hover:bg-accent/50 last:border-b-0 sm:flex-row sm:items-center no-underline text-inherit"
className={`group flex flex-col gap-3 px-3 py-3 transition-colors hover:bg-accent/50 sm:flex-row sm:items-center no-underline text-inherit${
divider ? " border-b border-border last:border-b-0" : ""
}`}
>
<div className="min-w-0 flex-1 space-y-1.5">
<div className="flex flex-wrap items-center gap-2">
+53 -44
View File
@@ -885,51 +885,60 @@ export function Routines() {
/>
</div>
) : (
<div className="rounded-lg border border-border">
{routineGroups.map((group) => (
<Collapsible
key={group.key}
open={!routineViewState.collapsedGroups.includes(group.key)}
onOpenChange={(open) => {
updateRoutineView({
collapsedGroups: open
? routineViewState.collapsedGroups.filter((item) => item !== group.key)
: [...routineViewState.collapsedGroups, group.key],
});
}}
>
{group.label ? (
<div className="flex items-center gap-2 border-b border-border px-3 py-2">
<CollapsibleTrigger className="flex items-center gap-1.5">
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90" />
<span className="text-sm font-semibold uppercase tracking-wide">
{group.label}
<div className="flex flex-col gap-3">
{routineGroups.map((group) => {
const isOpen = !routineViewState.collapsedGroups.includes(group.key);
return (
<Collapsible
key={group.key}
open={isOpen}
onOpenChange={(open) => {
updateRoutineView({
collapsedGroups: open
? routineViewState.collapsedGroups.filter((item) => item !== group.key)
: [...routineViewState.collapsedGroups, group.key],
});
}}
className="overflow-hidden rounded-lg border border-border"
>
{group.label ? (
<div
className={`flex items-center gap-2 px-3 py-2${
isOpen ? " border-b border-border" : ""
}`}
>
<CollapsibleTrigger className="flex items-center gap-1.5">
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90" />
<span className="text-sm font-semibold uppercase tracking-wide">
{group.label}
</span>
</CollapsibleTrigger>
<span className="text-xs text-muted-foreground">
{group.items.length}
</span>
</CollapsibleTrigger>
<span className="text-xs text-muted-foreground">
{group.items.length}
</span>
</div>
) : null}
<CollapsibleContent>
{group.items.map((routine) => (
<RoutineListRow
key={routine.id}
routine={routine}
projectById={projectById}
agentById={agentById}
runningRoutineId={runningRoutineId}
statusMutationRoutineId={statusMutationRoutineId}
href={`/routines/${routine.id}`}
runNowButton
onRunNow={handleRunNow}
onToggleEnabled={handleToggleEnabled}
onToggleArchived={handleToggleArchived}
/>
))}
</CollapsibleContent>
</Collapsible>
))}
</div>
) : null}
<CollapsibleContent>
{group.items.map((routine) => (
<RoutineListRow
key={routine.id}
routine={routine}
projectById={projectById}
agentById={agentById}
runningRoutineId={runningRoutineId}
statusMutationRoutineId={statusMutationRoutineId}
href={`/routines/${routine.id}`}
runNowButton
divider={false}
onRunNow={handleRunNow}
onToggleEnabled={handleToggleEnabled}
onToggleArchived={handleToggleArchived}
/>
))}
</CollapsibleContent>
</Collapsible>
);
})}
</div>
)}
</div>
@@ -0,0 +1,103 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import { ChevronRight } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import {
RoutineListRow,
type RoutineListAgentSummary,
type RoutineListProjectSummary,
type RoutineListRowItem,
} from "@/components/RoutineList";
const projectById = new Map<string, RoutineListProjectSummary>([
["p1", { name: "Board UI", color: "#6366f1" }],
["p2", { name: "Growth", color: "#10b981" }],
]);
const agentById = new Map<string, RoutineListAgentSummary>([
["a1", { name: "CodexCoder", icon: null }],
["a2", { name: "Digest Bot", icon: null }],
]);
type Group = { key: string; label: string | null; items: RoutineListRowItem[] };
const GROUPS: Group[] = [
{
key: "p1",
label: "Board UI",
items: [
{ id: "r1", title: "Weekly digest", status: "active", projectId: "p1", assigneeAgentId: "a2", lastRun: { triggeredAt: "2026-06-09T08:00:00Z", status: "succeeded" } },
{ id: "r2", title: "Triage stale issues", status: "active", projectId: "p1", assigneeAgentId: "a1", lastRun: { triggeredAt: "2026-06-08T08:00:00Z", status: "succeeded" } },
{ id: "r3", title: "Nightly changelog draft", status: "paused", projectId: "p1", assigneeAgentId: "a1", lastRun: null },
],
},
{
key: "p2",
label: "Growth",
items: [
{ id: "r4", title: "Lead enrichment sweep", status: "active", projectId: "p2", assigneeAgentId: "a1", lastRun: { triggeredAt: "2026-06-09T06:00:00Z", status: "running" } },
{ id: "r5", title: "Outbound follow-ups", status: "active", projectId: "p2", assigneeAgentId: "a2", lastRun: { triggeredAt: "2026-06-07T06:00:00Z", status: "failed" } },
],
},
];
function GroupedList() {
const [collapsed, setCollapsed] = useState<string[]>([]);
return (
<div className="mx-auto max-w-4xl p-6">
<div className="flex flex-col gap-3">
{GROUPS.map((group) => {
const isOpen = !collapsed.includes(group.key);
return (
<Collapsible
key={group.key}
open={isOpen}
onOpenChange={(open) =>
setCollapsed((prev) => (open ? prev.filter((k) => k !== group.key) : [...prev, group.key]))
}
className="overflow-hidden rounded-lg border border-border"
>
{group.label ? (
<div className={`flex items-center gap-2 px-3 py-2${isOpen ? " border-b border-border" : ""}`}>
<CollapsibleTrigger className="flex items-center gap-1.5">
<ChevronRight className="h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform [[data-state=open]>&]:rotate-90" />
<span className="text-sm font-semibold uppercase tracking-wide">{group.label}</span>
</CollapsibleTrigger>
<span className="text-xs text-muted-foreground">{group.items.length}</span>
</div>
) : null}
<CollapsibleContent>
{group.items.map((routine) => (
<RoutineListRow
key={routine.id}
routine={routine}
projectById={projectById}
agentById={agentById}
runningRoutineId={null}
statusMutationRoutineId={null}
href={`#${routine.id}`}
runNowButton
divider={false}
onRunNow={() => {}}
onToggleEnabled={() => {}}
onToggleArchived={() => {}}
/>
))}
</CollapsibleContent>
</Collapsible>
);
})}
</div>
</div>
);
}
const meta: Meta<typeof GroupedList> = {
title: "Product/Routines · List (grouped cards)",
component: GroupedList,
parameters: { layout: "fullscreen" },
};
export default meta;
type Story = StoryObj<typeof GroupedList>;
export const GroupedCards: Story = {};