chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths (Wave C-2) (#695)

* feat(panel): promote project settings to a full page

The edit-project dialog carried ~30 fields across 7 concerns in one
flat scroll with a per-tab width swap — outgrown. Project settings now
live at /projects/[id]/settings as a card-per-concern grid (the
settings page's own pattern) with per-card save and Conventions as a
page-level tab at natural width; the list Edit action routes there,
and a slim quick-edit dialog (name/cell/active) replaces the
kitchen-sink.

* chore(panel): one disclosure primitive, DialogFooter everywhere, three dialog widths

collapsible-section moves to ui/ as the single sectioned-disclosure
primitive (task dialogs' raw Collapsible and create-project's ad-hoc
showAdvanced converge onto it); every hand-rolled dialog footer becomes
DialogFooter; dialog widths collapse from ten ad-hoc classes to three
named sizes, with deliberate outliers annotated. No behavioral change.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-24 22:02:17 +02:00
committed by GitHub
co-authored by Renn F
parent 646be2351e
commit 8f01446243
22 changed files with 561 additions and 493 deletions
@@ -8,6 +8,7 @@ import { Label } from "@/components/ui/label";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
@@ -67,7 +68,9 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Delivered to the agent verbatim as the answer to what it's waiting on"> <HelpTip label="Delivered to the agent verbatim as the answer to what it's waiting on">
<Label htmlFor="resolution" className="w-fit">Resolution Message</Label> <Label htmlFor="resolution" className="w-fit">
Resolution Message
</Label>
</HelpTip> </HelpTip>
<Textarea <Textarea
id="resolution" id="resolution"
@@ -77,7 +80,7 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
rows={4} rows={4}
/> />
</div> </div>
<div className="flex justify-end gap-2"> <DialogFooter>
<HelpTip label="Closes without sending anything to the agent"> <HelpTip label="Closes without sending anything to the agent">
<Button variant="outline" onClick={() => setOpen(false)}> <Button variant="outline" onClick={() => setOpen(false)}>
Cancel Cancel
@@ -85,12 +88,15 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
</HelpTip> </HelpTip>
<HelpTip label="Delivers the message above to the agent so it can resume"> <HelpTip label="Delivers the message above to the agent so it can resume">
<span> <span>
<Button onClick={handleResolve} disabled={resolveWait.isPending}> <Button
onClick={handleResolve}
disabled={resolveWait.isPending}
>
{resolveWait.isPending ? "Sending..." : "Send Resolution"} {resolveWait.isPending ? "Sending..." : "Send Resolution"}
</Button> </Button>
</span> </span>
</HelpTip> </HelpTip>
</div> </DialogFooter>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
@@ -124,7 +125,7 @@ export function SpawnAgentDialog({
placeholder="Initial instructions for the agent" placeholder="Initial instructions for the agent"
/> />
</div> </div>
<div className="flex justify-end gap-2"> <DialogFooter>
<HelpTip label="Closes without spawning"> <HelpTip label="Closes without spawning">
<Button variant="outline" onClick={() => setOpen(false)}> <Button variant="outline" onClick={() => setOpen(false)}>
Cancel Cancel
@@ -137,7 +138,7 @@ export function SpawnAgentDialog({
</Button> </Button>
</span> </span>
</HelpTip> </HelpTip>
</div> </DialogFooter>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -13,9 +13,11 @@ import { AgentSelector } from "@/components/agents/agent-selector";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
Select, Select,
@@ -96,7 +98,7 @@ export function CreateFlagDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg"> <DialogContent className={DIALOG_SIZES.md}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Quality Flag</DialogTitle> <DialogTitle>Create Quality Flag</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -194,7 +196,7 @@ export function CreateFlagDialog({
</div> </div>
</div> </div>
<div className="flex justify-end gap-2 pt-4"> <DialogFooter className="pt-4">
<HelpTip label="Discards this draft and closes the dialog"> <HelpTip label="Discards this draft and closes the dialog">
<Button <Button
type="button" type="button"
@@ -214,7 +216,7 @@ export function CreateFlagDialog({
</Button> </Button>
</span> </span>
</HelpTip> </HelpTip>
</div> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -12,6 +12,7 @@ import {
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DialogTrigger, DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
Tooltip, Tooltip,
@@ -121,12 +122,12 @@ function QuickActionsCustomizeDialog() {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</HelpTip> </HelpTip>
<DialogContent className="max-w-md"> <DialogContent className={DIALOG_SIZES.sm}>
<DialogHeader> <DialogHeader>
<DialogTitle>Customize Quick Actions</DialogTitle> <DialogTitle>Customize Quick Actions</DialogTitle>
<DialogDescription> <DialogDescription>
Pick which shortcuts show on the Overview dashboard and reorder Pick which shortcuts show on the Overview dashboard and reorder them
them with the arrows. Saved to this browser only. with the arrows. Saved to this browser only.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -14,6 +14,7 @@ import {
DialogTitle, DialogTitle,
DialogFooter, DialogFooter,
DialogTrigger, DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
AlertDialog, AlertDialog,
@@ -306,7 +307,7 @@ export function GitActionsPanel({
</DialogTrigger> </DialogTrigger>
</span> </span>
</HelpTip> </HelpTip>
<DialogContent className="max-w-lg"> <DialogContent className={DIALOG_SIZES.md}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Pull Request</DialogTitle> <DialogTitle>Create Pull Request</DialogTitle>
</DialogHeader> </DialogHeader>
@@ -378,7 +379,7 @@ export function GitActionsPanel({
</DialogTrigger> </DialogTrigger>
</span> </span>
</HelpTip> </HelpTip>
<DialogContent className="max-w-sm"> <DialogContent className={DIALOG_SIZES.sm}>
<DialogHeader> <DialogHeader>
<DialogTitle>Merge Pull Request</DialogTitle> <DialogTitle>Merge Pull Request</DialogTitle>
</DialogHeader> </DialogHeader>
@@ -74,6 +74,9 @@ export function CommandPalette() {
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
{/* Outlier: kept explicit — this isn't just a width, it's a full custom
position/spacing override (top-anchored, no gap, no padding) that a
DIALOG_SIZES bucket can't express. */}
<DialogContent <DialogContent
showCloseButton={false} showCloseButton={false}
className="top-[20%] max-w-xl translate-y-0 gap-0 overflow-hidden p-0" className="top-[20%] max-w-xl translate-y-0 gap-0 overflow-hidden p-0"
@@ -12,6 +12,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -113,7 +114,9 @@ export function CreateProductDialog() {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</HelpTip> </HelpTip>
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto"> <DialogContent
className={`${DIALOG_SIZES.md} max-h-[90vh] overflow-y-auto`}
>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Product</DialogTitle> <DialogTitle>Create Product</DialogTitle>
@@ -172,7 +175,9 @@ export function CreateProductDialog() {
</HelpTip> </HelpTip>
{cells.map((cell) => ( {cells.map((cell) => (
<div key={cell.value} className="grid gap-2"> <div key={cell.value} className="grid gap-2">
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}> <HelpTip
label={`Which project the ${cell.label} cell works on for this product.`}
>
<Label <Label
htmlFor={`cell-${cell.value}`} htmlFor={`cell-${cell.value}`}
className="text-sm text-muted-foreground" className="text-sm text-muted-foreground"
@@ -11,6 +11,7 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -165,7 +166,9 @@ function EditProductForm({
</HelpTip> </HelpTip>
{cells.map((cell) => ( {cells.map((cell) => (
<div key={cell.value} className="grid gap-2"> <div key={cell.value} className="grid gap-2">
<HelpTip label={`Which project the ${cell.label} cell works on for this product.`}> <HelpTip
label={`Which project the ${cell.label} cell works on for this product.`}
>
<Label <Label
htmlFor={`cell-${cell.value}`} htmlFor={`cell-${cell.value}`}
className="text-sm text-muted-foreground" className="text-sm text-muted-foreground"
@@ -216,7 +219,9 @@ export function EditProductDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto"> <DialogContent
className={`${DIALOG_SIZES.md} max-h-[90vh] overflow-y-auto`}
>
{isLoading ? ( {isLoading ? (
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<Skeleton className="h-8 w-48" /> <Skeleton className="h-8 w-48" />
@@ -11,6 +11,7 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -31,6 +32,7 @@ import {
type SelectedRepo, type SelectedRepo,
} from "@/components/projects/select-repo-picker"; } from "@/components/projects/select-repo-picker";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
const cells: { value: Team; label: string }[] = [ const cells: { value: Team; label: string }[] = [
{ value: Team.BACKEND, label: "Backend" }, { value: Team.BACKEND, label: "Backend" },
@@ -152,7 +154,7 @@ export function CreateProjectDialog() {
</Button> </Button>
</DialogTrigger> </DialogTrigger>
</HelpTip> </HelpTip>
<DialogContent className="sm:max-w-[525px]"> <DialogContent className={DIALOG_SIZES.md}>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Project</DialogTitle> <DialogTitle>Create Project</DialogTitle>
@@ -315,20 +317,17 @@ export function CreateProjectDialog() {
} }
/> />
{/* Advanced Options Toggle */} {/* Advanced Options */}
<CollapsibleSection
variant="button"
open={showAdvanced}
onOpenChange={setShowAdvanced}
title={
<HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate."> <HelpTip label="Test/Format/Build are reference-only today; Lint + Typecheck (or Quality Gate below, which replaces both) run automatically at the dev's pre-submit gate.">
<Button <span>CI/CD Commands</span>
type="button"
variant="ghost"
className="justify-start px-0 text-muted-foreground"
onClick={() => setShowAdvanced(!showAdvanced)}
>
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
</Button>
</HelpTip> </HelpTip>
}
{showAdvanced && ( >
<>
{/* CI/CD Commands */} {/* CI/CD Commands */}
<div className="grid gap-2"> <div className="grid gap-2">
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself."> <HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
@@ -411,9 +410,7 @@ export function CreateProjectDialog() {
<div className="grid gap-2"> <div className="grid gap-2">
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command."> <HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
<Label htmlFor="quality_command"> <Label htmlFor="quality_command">Quality Gate Command</Label>
Quality Gate Command
</Label>
</HelpTip> </HelpTip>
<Input <Input
id="quality_command" id="quality_command"
@@ -427,8 +424,8 @@ export function CreateProjectDialog() {
placeholder="make gate" placeholder="make gate"
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Fast pre-submit gate (lint + types + complexity, no tests) Fast pre-submit gate (lint + types + complexity, no tests) run
run in the dev&apos;s workspace at hand-off to QA. in the dev&apos;s workspace at hand-off to QA.
</p> </p>
</div> </div>
@@ -458,8 +455,7 @@ export function CreateProjectDialog() {
dependency-update bot, sandbox DB/Redis/Mongo) is configured dependency-update bot, sandbox DB/Redis/Mongo) is configured
after creation, from this project&apos;s Edit Project dialog. after creation, from this project&apos;s Edit Project dialog.
</p> </p>
</> </CollapsibleSection>
)}
</div> </div>
<DialogFooter> <DialogFooter>
<Button <Button
@@ -11,6 +11,7 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -174,7 +175,7 @@ export function QuickEditProjectDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]"> <DialogContent className={DIALOG_SIZES.sm}>
{isLoading ? ( {isLoading ? (
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<Skeleton className="h-8 w-48" /> <Skeleton className="h-8 w-48" />
@@ -11,6 +11,7 @@ import {
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
@@ -124,7 +125,7 @@ export function SelectRepoDialog({ onSelect }: SelectRepoDialogProps) {
</span> </span>
</HelpTip> </HelpTip>
<DialogContent className="sm:max-w-[480px]"> <DialogContent className={DIALOG_SIZES.md}>
<DialogHeader> <DialogHeader>
<DialogTitle>Select a repository</DialogTitle> <DialogTitle>Select a repository</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
@@ -22,12 +23,8 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { import { CollapsibleSection } from "@/components/ui/collapsible-section";
Collapsible, import { Plus, GitBranch } from "lucide-react";
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Plus, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor"; import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
import { MarkdownEditor } from "./markdown-editor"; import { MarkdownEditor } from "./markdown-editor";
@@ -72,7 +69,8 @@ const TASK_TYPE_OPTIONS = [
// (branch, commits, PR) — this only classifies the kind of artifact. // (branch, commits, PR) — this only classifies the kind of artifact.
const TASK_TYPE_DESCRIPTIONS: Record<TaskType, string> = { const TASK_TYPE_DESCRIPTIONS: Record<TaskType, string> = {
[TaskType.CODE]: "Source code changes. Follows the full git workflow.", [TaskType.CODE]: "Source code changes. Follows the full git workflow.",
[TaskType.DOCUMENTATION]: "Documentation updates. Follows the full git workflow.", [TaskType.DOCUMENTATION]:
"Documentation updates. Follows the full git workflow.",
[TaskType.RESEARCH]: [TaskType.RESEARCH]:
"Research findings, committed as notes. Follows the full git workflow.", "Research findings, committed as notes. Follows the full git workflow.",
[TaskType.PLANNING]: [TaskType.PLANNING]:
@@ -157,7 +155,8 @@ export function CreateTaskDialog() {
const trimmedSequence = sequence.trim(); const trimmedSequence = sequence.trim();
if ( if (
trimmedSequence && trimmedSequence &&
(!Number.isInteger(Number(trimmedSequence)) || Number(trimmedSequence) < 0) (!Number.isInteger(Number(trimmedSequence)) ||
Number(trimmedSequence) < 0)
) { ) {
newErrors.sequence = "Sequence must be a non-negative whole number"; newErrors.sequence = "Sequence must be a non-negative whole number";
} }
@@ -235,6 +234,9 @@ export function CreateTaskDialog() {
New Task New Task
</Button> </Button>
</DialogTrigger> </DialogTrigger>
{/* Outlier: kept explicit the lg:grid-cols-5 team/priority/complexity/
status/nature row needs real width; DIALOG_SIZES.lg (max-w-2xl)
would cramp it into a single column. */}
<DialogContent className="w-full max-w-[95vw] sm:max-w-xl md:max-w-3xl lg:max-w-5xl max-h-[90vh] overflow-y-auto"> <DialogContent className="w-full max-w-[95vw] sm:max-w-xl md:max-w-3xl lg:max-w-5xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Create New Task</DialogTitle> <DialogTitle>Create New Task</DialogTitle>
@@ -390,24 +392,16 @@ export function CreateTaskDialog() {
/> />
{/* Advanced Options */} {/* Advanced Options */}
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> <CollapsibleSection
<CollapsibleTrigger asChild> variant="button"
<Button open={advancedOpen}
variant="ghost" onOpenChange={setAdvancedOpen}
type="button" title={
className="w-full justify-between"
>
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing."> <HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
<span>Advanced Options</span> <span>Advanced Options</span>
</HelpTip> </HelpTip>
{advancedOpen ? ( }
<ChevronDown className="h-4 w-4" /> >
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-4">
{/* Parent Task */} {/* Parent Task */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Nests this task under another — its claim order is then gated by sequence relative to siblings under the same parent."> <HelpTip label="Nests this task under another — its claim order is then gated by sequence relative to siblings under the same parent.">
@@ -456,8 +450,8 @@ export function CreateTaskDialog() {
filterByTeam={team} filterByTeam={team}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Leave unassigned to let the orchestrator route automatically, Leave unassigned to let the orchestrator route automatically, or
or manually assign to a specific agent manually assign to a specific agent
</p> </p>
</div> </div>
@@ -514,8 +508,8 @@ export function CreateTaskDialog() {
)} )}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
The repo this task targets. Optional if you pick a Product The repo this task targets. Optional if you pick a Product
below a fan-out task routes each cell&apos;s subtask via below a fan-out task routes each cell&apos;s subtask via the
the Product instead. Product instead.
</p> </p>
</div> </div>
@@ -532,9 +526,7 @@ export function CreateTaskDialog() {
<SelectValue placeholder="None" /> <SelectValue placeholder="None" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="none"> <SelectItem value="none">None (single project)</SelectItem>
None (single project)
</SelectItem>
{products.map((p) => ( {products.map((p) => (
<SelectItem key={p.id} value={p.id}> <SelectItem key={p.id} value={p.id}>
{p.name} {p.name}
@@ -548,11 +540,10 @@ export function CreateTaskDialog() {
</p> </p>
</div> </div>
</div> </div>
</CollapsibleContent> </CollapsibleSection>
</Collapsible>
{/* Actions */} {/* Actions */}
<div className="flex justify-end gap-2 pt-4 border-t"> <DialogFooter className="border-t pt-4">
<HelpTip label="Discards everything entered above without creating a task."> <HelpTip label="Discards everything entered above without creating a task.">
<Button <Button
type="button" type="button"
@@ -575,7 +566,7 @@ export function CreateTaskDialog() {
</Button> </Button>
</span> </span>
</HelpTip> </HelpTip>
</div> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
+20 -29
View File
@@ -9,9 +9,11 @@ import { Label } from "@/components/ui/label";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { import {
Select, Select,
@@ -20,12 +22,8 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { import { CollapsibleSection } from "@/components/ui/collapsible-section";
Collapsible, import { GitBranch } from "lucide-react";
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown, ChevronRight, GitBranch } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { MarkdownEditor } from "./markdown-editor"; import { MarkdownEditor } from "./markdown-editor";
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor"; import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
@@ -202,7 +200,9 @@ function EditTaskDialogInner({
return ( return (
<Dialog open={true} onOpenChange={onOpenChange}> <Dialog open={true} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent
className={`${DIALOG_SIZES.lg} max-h-[90vh] overflow-y-auto`}
>
<DialogHeader> <DialogHeader>
<DialogTitle>Edit Task</DialogTitle> <DialogTitle>Edit Task</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -321,24 +321,16 @@ function EditTaskDialogInner({
/> />
{/* Advanced Options */} {/* Advanced Options */}
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}> <CollapsibleSection
<CollapsibleTrigger asChild> variant="button"
<Button open={advancedOpen}
variant="ghost" onOpenChange={setAdvancedOpen}
type="button" title={
className="w-full justify-between"
>
<HelpTip label="Agent assignment, target date, task type, and repo routing."> <HelpTip label="Agent assignment, target date, task type, and repo routing.">
<span>Advanced Options</span> <span>Advanced Options</span>
</HelpTip> </HelpTip>
{advancedOpen ? ( }
<ChevronDown className="h-4 w-4" /> >
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-4">
{/* Assigned To */} {/* Assigned To */}
<div className="space-y-2"> <div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability."> <HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
@@ -393,9 +385,9 @@ function EditTaskDialogInner({
onChange={(e) => setBudgetUsd(e.target.value)} onChange={(e) => setBudgetUsd(e.target.value)}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Must be greater than 0 a 0 budget would block the task Must be greater than 0 a 0 budget would block the task before
before it spends a cent. Leave blank for no cap: budgets it spends a cent. Leave blank for no cap: budgets enforce only
enforce only when explicitly set. when explicitly set.
</p> </p>
{spendUsd != null && ( {spendUsd != null && (
<p <p
@@ -459,11 +451,10 @@ function EditTaskDialogInner({
)} )}
</div> </div>
</div> </div>
</CollapsibleContent> </CollapsibleSection>
</Collapsible>
{/* Actions */} {/* Actions */}
<div className="flex justify-end gap-2 pt-4 border-t"> <DialogFooter className="border-t pt-4">
<HelpTip label="Discards any edits made above and closes without saving."> <HelpTip label="Discards any edits made above and closes without saving.">
<Button <Button
type="button" type="button"
@@ -483,7 +474,7 @@ function EditTaskDialogInner({
</Button> </Button>
</span> </span>
</HelpTip> </HelpTip>
</div> </DialogFooter>
</form> </form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
@@ -9,7 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Plus, Trash2, Edit3, Check, X } from "lucide-react"; import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "./collapsible-section"; import { CollapsibleSection } from "@/components/ui/collapsible-section";
interface AcceptanceCriteriaProps { interface AcceptanceCriteriaProps {
task: Task; task: Task;
@@ -196,7 +196,11 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
</HelpTip> </HelpTip>
{!isAdding && ( {!isAdding && (
<HelpTip label="Adds a new empty criterion to this list"> <HelpTip label="Adds a new empty criterion to this list">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button
size="sm"
variant="ghost"
onClick={() => setIsAdding(true)}
>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
@@ -9,7 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import { ListChecks } from "lucide-react"; import { ListChecks } from "lucide-react";
import { CodeSnippet } from "@/components/git/code-snippet"; import { CodeSnippet } from "@/components/git/code-snippet";
import { CollapsibleSection } from "./collapsible-section"; import { CollapsibleSection } from "@/components/ui/collapsible-section";
// Mirrors the server-side shape gate (Finding._file_repo_relative in // Mirrors the server-side shape gate (Finding._file_repo_relative in
// roboco/foundation/policy/content/models.py) — keep the character classes // roboco/foundation/policy/content/models.py) — keep the character classes
@@ -8,7 +8,7 @@ import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { CollapsibleSection } from "./collapsible-section"; import { CollapsibleSection } from "@/components/ui/collapsible-section";
import { exceedsReadabilityThreshold } from "@/lib/content-readability"; import { exceedsReadabilityThreshold } from "@/lib/content-readability";
import { import {
FileText, FileText,
@@ -42,9 +42,7 @@ type NoteField =
function prReviewBadge(task: Task): React.ReactNode { function prReviewBadge(task: Task): React.ReactNode {
const verdict = ( const verdict = (
task.notes_structured as task.notes_structured as
| { pr_review?: { verdict?: string } } { pr_review?: { verdict?: string } } | null | undefined
| null
| undefined
)?.pr_review?.verdict; )?.pr_review?.verdict;
if (!verdict) { if (!verdict) {
return ( return (
@@ -80,9 +78,7 @@ function prReviewBadge(task: Task): React.ReactNode {
function prReviewCardBg(task: Task): string { function prReviewCardBg(task: Task): string {
const verdict = ( const verdict = (
task.notes_structured as task.notes_structured as
| { pr_review?: { verdict?: string } } { pr_review?: { verdict?: string } } | null | undefined
| null
| undefined
)?.pr_review?.verdict; )?.pr_review?.verdict;
const map: Record<string, string> = { const map: Record<string, string> = {
approved: approved:
@@ -116,9 +112,7 @@ const FIELD_TO_SECTION: Record<NoteField, string> = {
// populated field never renders with no timestamp at all. // populated field never renders with no timestamp at all.
function writtenAt(task: Task, field: NoteField): string | null { function writtenAt(task: Task, field: NoteField): string | null {
const sections = task.notes_structured as const sections = task.notes_structured as
| Record<string, { written_at?: string }> Record<string, { written_at?: string }> | null | undefined;
| null
| undefined;
const stamp = const stamp =
sections?.[FIELD_TO_SECTION[field]]?.written_at ?? task.created_at; sections?.[FIELD_TO_SECTION[field]]?.written_at ?? task.created_at;
if (!stamp) return null; if (!stamp) return null;
@@ -168,8 +162,8 @@ function EditableNoteCard({
const [localEditValue, setLocalEditValue] = useState(""); const [localEditValue, setLocalEditValue] = useState("");
const [editMode, setEditMode] = useState<"write" | "preview">("write"); const [editMode, setEditMode] = useState<"write" | "preview">("write");
// Long content starts collapsed; short content starts expanded. // Long content starts collapsed; short content starts expanded.
const [sectionOpen, setSectionOpen] = useState(() => const [sectionOpen, setSectionOpen] = useState(
!exceedsReadabilityThreshold(currentValue ?? ""), () => !exceedsReadabilityThreshold(currentValue ?? ""),
); );
// Display prop value when not editing, local value when editing // Display prop value when not editing, local value when editing
@@ -12,7 +12,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { CollapsibleSection } from "./collapsible-section"; import { CollapsibleSection } from "@/components/ui/collapsible-section";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -312,7 +312,11 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</span> </span>
{!isAdding && ( {!isAdding && (
<HelpTip label="Add a new sub-task to this plan"> <HelpTip label="Add a new sub-task to this plan">
<Button size="sm" variant="ghost" onClick={() => setIsAdding(true)}> <Button
size="sm"
variant="ghost"
onClick={() => setIsAdding(true)}
>
<Plus className="h-4 w-4 mr-1" /> <Plus className="h-4 w-4 mr-1" />
Add Add
</Button> </Button>
@@ -9,6 +9,7 @@ import {
DialogFooter, DialogFooter,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
@@ -159,7 +160,11 @@ export function CeoRejectDialog({
Cancel Cancel
</Button> </Button>
</HelpTip> </HelpTip>
<HelpTip label={!notes.trim() ? "Explain what changes are needed to enable" : null}> <HelpTip
label={
!notes.trim() ? "Explain what changes are needed to enable" : null
}
>
<span <span
className="inline-block" className="inline-block"
tabIndex={!notes.trim() ? 0 : undefined} tabIndex={!notes.trim() ? 0 : undefined}
@@ -298,7 +303,9 @@ export function CeoApproveDialog({
</HelpTip> </HelpTip>
{/* Button's own disabled:pointer-events-none would swallow hover, so {/* Button's own disabled:pointer-events-none would swallow hover, so
the tip sits on a wrapping span rather than the Button itself. */} the tip sits on a wrapping span rather than the Button itself. */}
<HelpTip label={remainingCharsTip(notes.trim().length, _CEO_NOTES_MIN)}> <HelpTip
label={remainingCharsTip(notes.trim().length, _CEO_NOTES_MIN)}
>
<span className="inline-block" tabIndex={tooShort ? 0 : undefined}> <span className="inline-block" tabIndex={tooShort ? 0 : undefined}>
<Button onClick={handleConfirm} disabled={tooShort || isPending}> <Button onClick={handleConfirm} disabled={tooShort || isPending}>
{isPending ? "Approving..." : "Approve & Merge"} {isPending ? "Approving..." : "Approve & Merge"}
@@ -521,7 +528,7 @@ export function CreatePRDialog({
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[525px]"> <DialogContent className={DIALOG_SIZES.md}>
<DialogHeader> <DialogHeader>
<DialogTitle>Create Pull Request</DialogTitle> <DialogTitle>Create Pull Request</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -8,7 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown"; import { Markdown } from "@/components/ui/markdown";
import { HelpTip } from "@/components/ui/help-tip"; import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "./collapsible-section"; import { CollapsibleSection } from "@/components/ui/collapsible-section";
import { Edit3, Eye, Check, X, ShieldAlert } from "lucide-react"; import { Edit3, Eye, Check, X, ShieldAlert } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -12,6 +12,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ChevronDown } from "lucide-react"; import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { exceedsReadabilityThreshold } from "@/lib/content-readability"; import { exceedsReadabilityThreshold } from "@/lib/content-readability";
@@ -19,7 +20,7 @@ import { exceedsReadabilityThreshold } from "@/lib/content-readability";
interface CollapsibleSectionProps { interface CollapsibleSectionProps {
/** Card title content (icon + text + badges as needed) */ /** Card title content (icon + text + badges as needed) */
title: ReactNode; title: ReactNode;
/** Right-aligned header controls (edit/preview toggles, buttons) — always visible */ /** Right-aligned header controls (edit/preview toggles, buttons) — always visible. "button" variant ignores this. */
actions?: ReactNode; actions?: ReactNode;
/** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */ /** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */
open?: boolean; open?: boolean;
@@ -39,6 +40,15 @@ interface CollapsibleSectionProps {
className?: string; className?: string;
headerClassName?: string; headerClassName?: string;
children: ReactNode; children: ReactNode;
/**
* "card" (default): the existing Card/CardHeader/CardContent chrome used
* across task-detail tabs. "button": no Card, no hover tooltip a
* full-width ghost-button trigger (title left, chevron right), for a
* dialog's inline "Advanced Options" disclosure.
*/
variant?: "card" | "button";
/** "button" variant only: CollapsibleContent wrapper classes (default "space-y-4 pt-4"). */
contentClassName?: string;
} }
/** /**
@@ -66,6 +76,8 @@ export function CollapsibleSection({
className, className,
headerClassName, headerClassName,
children, children,
variant = "card",
contentClassName,
}: CollapsibleSectionProps) { }: CollapsibleSectionProps) {
// Resolve the starting state: explicit defaultOpen > content-derived > default to true // Resolve the starting state: explicit defaultOpen > content-derived > default to true
const resolvedDefaultOpen = const resolvedDefaultOpen =
@@ -78,6 +90,40 @@ export function CollapsibleSection({
if (openProp === undefined) setInternalOpen(next); if (openProp === undefined) setInternalOpen(next);
}; };
const motionClassName = cn(
"duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
);
if (variant === "button") {
return (
<Collapsible open={open} onOpenChange={setOpen} className={className}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
type="button"
className={cn("w-full justify-between", headerClassName)}
>
{title}
<ChevronDown
aria-hidden="true"
className={cn(
"h-4 w-4 shrink-0 transition-transform duration-200",
!open && "-rotate-90",
)}
/>
</Button>
</CollapsibleTrigger>
<CollapsibleContent
className={cn(motionClassName, contentClassName ?? "space-y-4 pt-4")}
>
{children}
</CollapsibleContent>
</Collapsible>
);
}
return ( return (
<Card className={className}> <Card className={className}>
<Collapsible open={open} onOpenChange={setOpen} className="contents"> <Collapsible open={open} onOpenChange={setOpen} className="contents">
@@ -113,13 +159,7 @@ export function CollapsibleSection({
)} )}
</div> </div>
</CardHeader> </CardHeader>
<CollapsibleContent <CollapsibleContent className={motionClassName}>
className={cn(
"duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in",
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
"data-[state=closed]:slide-out-to-top-1 data-[state=open]:slide-in-from-top-1",
)}
>
<CardContent>{children}</CardContent> <CardContent>{children}</CardContent>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
+15
View File
@@ -6,6 +6,21 @@ import { XIcon } from "lucide-react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
// Three width buckets collapsing 10+ ad-hoc DialogContent max-w overrides
// audited across panel/src. Chosen to minimize visual drift from what was
// already shipped: sm covers the compact single-purpose dialogs (previously
// max-w-sm/max-w-md/~425px); md equals DialogContent's own unstyled default
// (sm:max-w-lg) and covers the common ~480-525px form dialogs (project/
// product create-edit, PR/repo pickers); lg covers the wider edit-task
// dialog (max-w-2xl). A dialog whose width is load-bearing and far from any
// bucket (a wide multi-column form, a custom-positioned command palette)
// keeps its own explicit class with a one-line comment instead.
export const DIALOG_SIZES = {
sm: "sm:max-w-md",
md: "sm:max-w-lg",
lg: "max-w-2xl",
} as const;
function Dialog({ function Dialog({
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {