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 {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -67,7 +68,9 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
<div className="space-y-4">
<div className="space-y-2">
<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>
<Textarea
id="resolution"
@@ -77,7 +80,7 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
rows={4}
/>
</div>
<div className="flex justify-end gap-2">
<DialogFooter>
<HelpTip label="Closes without sending anything to the agent">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
@@ -85,12 +88,15 @@ export function ResolveWaitDialog({ agentId }: ResolveWaitDialogProps) {
</HelpTip>
<HelpTip label="Delivers the message above to the agent so it can resume">
<span>
<Button onClick={handleResolve} disabled={resolveWait.isPending}>
<Button
onClick={handleResolve}
disabled={resolveWait.isPending}
>
{resolveWait.isPending ? "Sending..." : "Send Resolution"}
</Button>
</span>
</HelpTip>
</div>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
@@ -9,6 +9,7 @@ import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -124,7 +125,7 @@ export function SpawnAgentDialog({
placeholder="Initial instructions for the agent"
/>
</div>
<div className="flex justify-end gap-2">
<DialogFooter>
<HelpTip label="Closes without spawning">
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
@@ -137,7 +138,7 @@ export function SpawnAgentDialog({
</Button>
</span>
</HelpTip>
</div>
</DialogFooter>
</div>
</DialogContent>
</Dialog>
@@ -13,9 +13,11 @@ import { AgentSelector } from "@/components/agents/agent-selector";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import {
Select,
@@ -96,7 +98,7 @@ export function CreateFlagDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogContent className={DIALOG_SIZES.md}>
<DialogHeader>
<DialogTitle>Create Quality Flag</DialogTitle>
<DialogDescription>
@@ -194,7 +196,7 @@ export function CreateFlagDialog({
</div>
</div>
<div className="flex justify-end gap-2 pt-4">
<DialogFooter className="pt-4">
<HelpTip label="Discards this draft and closes the dialog">
<Button
type="button"
@@ -214,7 +216,7 @@ export function CreateFlagDialog({
</Button>
</span>
</HelpTip>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
@@ -12,6 +12,7 @@ import {
DialogTitle,
DialogDescription,
DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import {
Tooltip,
@@ -121,12 +122,12 @@ function QuickActionsCustomizeDialog() {
</Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="max-w-md">
<DialogContent className={DIALOG_SIZES.sm}>
<DialogHeader>
<DialogTitle>Customize Quick Actions</DialogTitle>
<DialogDescription>
Pick which shortcuts show on the Overview dashboard and reorder
them with the arrows. Saved to this browser only.
Pick which shortcuts show on the Overview dashboard and reorder them
with the arrows. Saved to this browser only.
</DialogDescription>
</DialogHeader>
@@ -14,6 +14,7 @@ import {
DialogTitle,
DialogFooter,
DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import {
AlertDialog,
@@ -306,7 +307,7 @@ export function GitActionsPanel({
</DialogTrigger>
</span>
</HelpTip>
<DialogContent className="max-w-lg">
<DialogContent className={DIALOG_SIZES.md}>
<DialogHeader>
<DialogTitle>Create Pull Request</DialogTitle>
</DialogHeader>
@@ -378,7 +379,7 @@ export function GitActionsPanel({
</DialogTrigger>
</span>
</HelpTip>
<DialogContent className="max-w-sm">
<DialogContent className={DIALOG_SIZES.sm}>
<DialogHeader>
<DialogTitle>Merge Pull Request</DialogTitle>
</DialogHeader>
@@ -74,6 +74,9 @@ export function CommandPalette() {
return (
<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
showCloseButton={false}
className="top-[20%] max-w-xl translate-y-0 gap-0 overflow-hidden p-0"
@@ -12,6 +12,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -113,7 +114,9 @@ export function CreateProductDialog() {
</Button>
</DialogTrigger>
</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}>
<DialogHeader>
<DialogTitle>Create Product</DialogTitle>
@@ -172,7 +175,9 @@ export function CreateProductDialog() {
</HelpTip>
{cells.map((cell) => (
<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
htmlFor={`cell-${cell.value}`}
className="text-sm text-muted-foreground"
@@ -11,6 +11,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -165,7 +166,9 @@ function EditProductForm({
</HelpTip>
{cells.map((cell) => (
<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
htmlFor={`cell-${cell.value}`}
className="text-sm text-muted-foreground"
@@ -216,7 +219,9 @@ export function EditProductDialog({
return (
<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 ? (
<div className="space-y-4 py-4">
<Skeleton className="h-8 w-48" />
@@ -11,6 +11,7 @@ import {
DialogHeader,
DialogTitle,
DialogTrigger,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -31,6 +32,7 @@ import {
type SelectedRepo,
} from "@/components/projects/select-repo-picker";
import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
const cells: { value: Team; label: string }[] = [
{ value: Team.BACKEND, label: "Backend" },
@@ -152,7 +154,7 @@ export function CreateProjectDialog() {
</Button>
</DialogTrigger>
</HelpTip>
<DialogContent className="sm:max-w-[525px]">
<DialogContent className={DIALOG_SIZES.md}>
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Create Project</DialogTitle>
@@ -315,151 +317,145 @@ export function CreateProjectDialog() {
}
/>
{/* Advanced Options Toggle */}
<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
type="button"
variant="ghost"
className="justify-start px-0 text-muted-foreground"
onClick={() => setShowAdvanced(!showAdvanced)}
>
{showAdvanced ? "Hide" : "Show"} CI/CD Commands
</Button>
</HelpTip>
{/* 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.">
<span>CI/CD Commands</span>
</HelpTip>
}
>
{/* CI/CD Commands */}
<div className="grid gap-2">
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
<Label htmlFor="test_command">Test Command</Label>
</HelpTip>
<Input
id="test_command"
value={formData.test_command || ""}
onChange={(e) =>
setFormData({ ...formData, test_command: e.target.value })
}
placeholder="uv run pytest"
/>
</div>
{showAdvanced && (
<>
{/* CI/CD Commands */}
<div className="grid gap-2">
<HelpTip label="Reference only — not yet wired into any automated gate or CI run by RoboCo itself.">
<Label htmlFor="test_command">Test Command</Label>
</HelpTip>
<Input
id="test_command"
value={formData.test_command || ""}
onChange={(e) =>
setFormData({ ...formData, test_command: e.target.value })
}
placeholder="uv run pytest"
/>
</div>
<div className="grid gap-2">
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="lint_command">Lint Command</Label>
</HelpTip>
<Input
id="lint_command"
value={formData.lint_command || ""}
onChange={(e) =>
setFormData({ ...formData, lint_command: e.target.value })
}
placeholder="uv run ruff check ."
/>
</div>
<div className="grid gap-2">
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Typecheck — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="lint_command">Lint Command</Label>
</HelpTip>
<Input
id="lint_command"
value={formData.lint_command || ""}
onChange={(e) =>
setFormData({ ...formData, lint_command: e.target.value })
}
placeholder="uv run ruff check ."
/>
</div>
<div className="grid gap-2">
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
<Label htmlFor="format_command">Format Command</Label>
</HelpTip>
<Input
id="format_command"
value={formData.format_command || ""}
onChange={(e) =>
setFormData({
...formData,
format_command: e.target.value,
})
}
placeholder="uv run ruff format ."
/>
</div>
<div className="grid gap-2">
<HelpTip label="Reference only — deliberately excluded from the automated gate since formatting mutates files.">
<Label htmlFor="format_command">Format Command</Label>
</HelpTip>
<Input
id="format_command"
value={formData.format_command || ""}
onChange={(e) =>
setFormData({
...formData,
format_command: e.target.value,
})
}
placeholder="uv run ruff format ."
/>
</div>
<div className="grid gap-2">
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="typecheck_command">Typecheck Command</Label>
</HelpTip>
<Input
id="typecheck_command"
value={formData.typecheck_command || ""}
onChange={(e) =>
setFormData({
...formData,
typecheck_command: e.target.value,
})
}
placeholder="uv run mypy src/"
/>
</div>
<div className="grid gap-2">
<HelpTip label="Runs at the dev's pre-submit gate (i_am_done) alongside Lint — unless Quality Gate Command below is set, which replaces both.">
<Label htmlFor="typecheck_command">Typecheck Command</Label>
</HelpTip>
<Input
id="typecheck_command"
value={formData.typecheck_command || ""}
onChange={(e) =>
setFormData({
...formData,
typecheck_command: e.target.value,
})
}
placeholder="uv run mypy src/"
/>
</div>
<div className="grid gap-2">
<HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
<Label htmlFor="build_command">Build Command</Label>
</HelpTip>
<Input
id="build_command"
value={formData.build_command || ""}
onChange={(e) =>
setFormData({
...formData,
build_command: e.target.value,
})
}
placeholder="pnpm build"
/>
</div>
<div className="grid gap-2">
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
<Label htmlFor="quality_command">
Quality Gate Command
</Label>
</HelpTip>
<Input
id="quality_command"
value={formData.quality_command || ""}
onChange={(e) =>
setFormData({
...formData,
quality_command: e.target.value,
})
}
placeholder="make gate"
/>
<p className="text-xs text-muted-foreground">
Fast pre-submit gate (lint + types + complexity, no tests)
run in the dev&apos;s workspace at hand-off to QA.
</p>
</div>
<div className="grid gap-2">
<HelpTip label="Command that regenerates checked-in generated files, e.g. `make codegen`; run and committed before push so codegen drift never fails CI. Leave blank if the project has no generated artifacts.">
<Label htmlFor="codegen_command">Codegen Command</Label>
</HelpTip>
<Input
id="codegen_command"
value={formData.codegen_command || ""}
onChange={(e) =>
setFormData({
...formData,
codegen_command: e.target.value,
})
}
placeholder="make codegen"
/>
<p className="text-xs text-muted-foreground">
Regenerates checked-in generated artifacts; any drift is
committed in the task&apos;s workspace before each push.
</p>
</div>
<div className="grid gap-2">
<HelpTip label="Reference only — not run automatically; the slow build/test suite is left to CI.">
<Label htmlFor="build_command">Build Command</Label>
</HelpTip>
<Input
id="build_command"
value={formData.build_command || ""}
onChange={(e) =>
setFormData({
...formData,
build_command: e.target.value,
})
}
placeholder="pnpm build"
/>
</div>
<div className="grid gap-2">
<HelpTip label="When set, replaces the Lint + Typecheck pair as the dev's complete pre-submit gate command.">
<Label htmlFor="quality_command">Quality Gate Command</Label>
</HelpTip>
<Input
id="quality_command"
value={formData.quality_command || ""}
onChange={(e) =>
setFormData({
...formData,
quality_command: e.target.value,
})
}
placeholder="make gate"
/>
<p className="text-xs text-muted-foreground">
Autonomous maintenance (CI-watch, video engine,
dependency-update bot, sandbox DB/Redis/Mongo) is configured
after creation, from this project&apos;s Edit Project dialog.
Fast pre-submit gate (lint + types + complexity, no tests) run
in the dev&apos;s workspace at hand-off to QA.
</p>
</>
)}
</div>
<div className="grid gap-2">
<HelpTip label="Command that regenerates checked-in generated files, e.g. `make codegen`; run and committed before push so codegen drift never fails CI. Leave blank if the project has no generated artifacts.">
<Label htmlFor="codegen_command">Codegen Command</Label>
</HelpTip>
<Input
id="codegen_command"
value={formData.codegen_command || ""}
onChange={(e) =>
setFormData({
...formData,
codegen_command: e.target.value,
})
}
placeholder="make codegen"
/>
<p className="text-xs text-muted-foreground">
Regenerates checked-in generated artifacts; any drift is
committed in the task&apos;s workspace before each push.
</p>
</div>
<p className="text-xs text-muted-foreground">
Autonomous maintenance (CI-watch, video engine,
dependency-update bot, sandbox DB/Redis/Mongo) is configured
after creation, from this project&apos;s Edit Project dialog.
</p>
</CollapsibleSection>
</div>
<DialogFooter>
<Button
@@ -11,6 +11,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -174,7 +175,7 @@ export function QuickEditProjectDialog({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className={DIALOG_SIZES.sm}>
{isLoading ? (
<div className="space-y-4 py-4">
<Skeleton className="h-8 w-48" />
@@ -11,6 +11,7 @@ import {
DialogDescription,
DialogHeader,
DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
@@ -124,7 +125,7 @@ export function SelectRepoDialog({ onSelect }: SelectRepoDialogProps) {
</span>
</HelpTip>
<DialogContent className="sm:max-w-[480px]">
<DialogContent className={DIALOG_SIZES.md}>
<DialogHeader>
<DialogTitle>Select a repository</DialogTitle>
<DialogDescription>
+154 -163
View File
@@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -22,12 +23,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Plus, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
import { Plus, GitBranch } from "lucide-react";
import { toast } from "sonner";
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
import { MarkdownEditor } from "./markdown-editor";
@@ -72,7 +69,8 @@ const TASK_TYPE_OPTIONS = [
// (branch, commits, PR) — this only classifies the kind of artifact.
const TASK_TYPE_DESCRIPTIONS: Record<TaskType, string> = {
[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]:
"Research findings, committed as notes. Follows the full git workflow.",
[TaskType.PLANNING]:
@@ -157,7 +155,8 @@ export function CreateTaskDialog() {
const trimmedSequence = sequence.trim();
if (
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";
}
@@ -235,6 +234,9 @@ export function CreateTaskDialog() {
New Task
</Button>
</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">
<DialogHeader>
<DialogTitle>Create New Task</DialogTitle>
@@ -390,169 +392,158 @@ export function CreateTaskDialog() {
/>
{/* Advanced Options */}
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
type="button"
className="w-full justify-between"
>
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
<span>Advanced Options</span>
<CollapsibleSection
variant="button"
open={advancedOpen}
onOpenChange={setAdvancedOpen}
title={
<HelpTip label="Parent task, agent assignment, task type, and repo/product routing.">
<span>Advanced Options</span>
</HelpTip>
}
>
{/* Parent Task */}
<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.">
<Label>Parent Task (for subtasks)</Label>
</HelpTip>
<TaskSelector
value={parentTaskId}
onChange={setParentTaskId}
placeholder="Select parent task (optional)..."
filterByTeam={team}
/>
<p className="text-xs text-muted-foreground">
Make this task a subtask of an existing task
</p>
</div>
{/* Sequence */}
<div className="space-y-2">
<HelpTip label="Order within siblings under the same parent — lower runs first, ties run in parallel. Leave blank to default to 0.">
<Label htmlFor="sequence">Sequence</Label>
</HelpTip>
<Input
id="sequence"
type="number"
min="0"
step="1"
placeholder="0"
value={sequence}
onChange={(e) => setSequence(e.target.value)}
className={errors.sequence ? "border-destructive" : ""}
/>
{errors.sequence && (
<p className="text-xs text-destructive">{errors.sequence}</p>
)}
</div>
{/* Assign To */}
<div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
<Label>Assign To</Label>
</HelpTip>
<AgentSelector
value={assignedTo}
onChange={setAssignedTo}
placeholder="Unassigned (orchestrator will route)"
filterByTeam={team}
/>
<p className="text-xs text-muted-foreground">
Leave unassigned to let the orchestrator route automatically, or
manually assign to a specific agent
</p>
</div>
{/* Git Configuration Section */}
<div className="space-y-4 pt-4 border-t">
<div className="flex items-center gap-2 mb-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<HelpTip label="Every task type follows the same branch → commits → PR workflow; these fields just route it.">
<span className="font-medium text-sm">
Git & Work Configuration
</span>
</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 */}
<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.">
<Label>Parent Task (for subtasks)</Label>
</HelpTip>
<TaskSelector
value={parentTaskId}
onChange={setParentTaskId}
placeholder="Select parent task (optional)..."
filterByTeam={team}
/>
<p className="text-xs text-muted-foreground">
Make this task a subtask of an existing task
</p>
</div>
{/* Sequence */}
{/* Task Type */}
<div className="space-y-2">
<HelpTip label="Order within siblings under the same parent — lower runs first, ties run in parallel. Leave blank to default to 0.">
<Label htmlFor="sequence">Sequence</Label>
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
<Label>Task Type</Label>
</HelpTip>
<Input
id="sequence"
type="number"
min="0"
step="1"
placeholder="0"
value={sequence}
onChange={(e) => setSequence(e.target.value)}
className={errors.sequence ? "border-destructive" : ""}
/>
{errors.sequence && (
<p className="text-xs text-destructive">{errors.sequence}</p>
)}
</div>
{/* Assign To */}
<div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
<Label>Assign To</Label>
</HelpTip>
<AgentSelector
value={assignedTo}
onChange={setAssignedTo}
placeholder="Unassigned (orchestrator will route)"
filterByTeam={team}
/>
<p className="text-xs text-muted-foreground">
Leave unassigned to let the orchestrator route automatically,
or manually assign to a specific agent
</p>
</div>
{/* Git Configuration Section */}
<div className="space-y-4 pt-4 border-t">
<div className="flex items-center gap-2 mb-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<HelpTip label="Every task type follows the same branch → commits → PR workflow; these fields just route it.">
<span className="font-medium text-sm">
Git & Work Configuration
</span>
</HelpTip>
</div>
{/* Task Type */}
<div className="space-y-2">
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
<Label>Task Type</Label>
</HelpTip>
<Select
value={taskType}
onValueChange={(v) => setTaskType(v as TaskType)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TASK_TYPE_OPTIONS.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Type of work: code, documentation, research, etc.
</p>
</div>
{/* Project — required UNLESS a Product is picked (fan-out task) */}
<div className="space-y-2">
<HelpTip label="The repo the branch/PR are opened against once this task is claimed.">
<Label>Project</Label>
</HelpTip>
<ProjectSelector
value={projectId || null}
onChange={(value) => setProjectId(value || "")}
placeholder="Select project..."
/>
{errors.project_id && (
<p className="text-xs text-destructive">
{errors.project_id}
</p>
)}
<p className="text-xs text-muted-foreground">
The repo this task targets. Optional if you pick a Product
below a fan-out task routes each cell&apos;s subtask via
the Product instead.
</p>
</div>
{/* Product (optional) — drives per-cell project routing of subtasks */}
<div className="space-y-2">
<HelpTip label="Fan-out: each delegated subtask routes to that cell's own mapped project instead of one repo.">
<Label>Product</Label>
</HelpTip>
<Select
value={productId || "none"}
onValueChange={(v) => setProductId(v === "none" ? "" : v)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="None" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
None (single project)
<Select
value={taskType}
onValueChange={(v) => setTaskType(v as TaskType)}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{TASK_TYPE_OPTIONS.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
{products.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Optional. When set, delegated subtasks route to each
cell&apos;s mapped project (manage these in Products).
</p>
</div>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Type of work: code, documentation, research, etc.
</p>
</div>
</CollapsibleContent>
</Collapsible>
{/* Project — required UNLESS a Product is picked (fan-out task) */}
<div className="space-y-2">
<HelpTip label="The repo the branch/PR are opened against once this task is claimed.">
<Label>Project</Label>
</HelpTip>
<ProjectSelector
value={projectId || null}
onChange={(value) => setProjectId(value || "")}
placeholder="Select project..."
/>
{errors.project_id && (
<p className="text-xs text-destructive">
{errors.project_id}
</p>
)}
<p className="text-xs text-muted-foreground">
The repo this task targets. Optional if you pick a Product
below a fan-out task routes each cell&apos;s subtask via the
Product instead.
</p>
</div>
{/* Product (optional) — drives per-cell project routing of subtasks */}
<div className="space-y-2">
<HelpTip label="Fan-out: each delegated subtask routes to that cell's own mapped project instead of one repo.">
<Label>Product</Label>
</HelpTip>
<Select
value={productId || "none"}
onValueChange={(v) => setProductId(v === "none" ? "" : v)}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="None" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">None (single project)</SelectItem>
{products.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
Optional. When set, delegated subtasks route to each
cell&apos;s mapped project (manage these in Products).
</p>
</div>
</div>
</CollapsibleSection>
{/* 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.">
<Button
type="button"
@@ -575,7 +566,7 @@ export function CreateTaskDialog() {
</Button>
</span>
</HelpTip>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
+133 -142
View File
@@ -9,9 +9,11 @@ import { Label } from "@/components/ui/label";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogDescription,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import {
Select,
@@ -20,12 +22,8 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { ChevronDown, ChevronRight, GitBranch } from "lucide-react";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
import { GitBranch } from "lucide-react";
import { toast } from "sonner";
import { MarkdownEditor } from "./markdown-editor";
import { AcceptanceCriteriaEditor } from "./acceptance-criteria-editor";
@@ -202,7 +200,9 @@ function EditTaskDialogInner({
return (
<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>
<DialogTitle>Edit Task</DialogTitle>
<DialogDescription>
@@ -321,149 +321,140 @@ function EditTaskDialogInner({
/>
{/* Advanced Options */}
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
type="button"
className="w-full justify-between"
>
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
<span>Advanced Options</span>
</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 */}
<div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
<Label>Assigned To</Label>
</HelpTip>
<AgentSelector
value={assignedTo}
onChange={setAssignedTo}
placeholder="Unassigned"
filterByTeam={team}
/>
</div>
<CollapsibleSection
variant="button"
open={advancedOpen}
onOpenChange={setAdvancedOpen}
title={
<HelpTip label="Agent assignment, target date, task type, and repo routing.">
<span>Advanced Options</span>
</HelpTip>
}
>
{/* Assigned To */}
<div className="space-y-2">
<HelpTip label="Pin a specific agent; leave unassigned to let the orchestrator route by role, team, and availability.">
<Label>Assigned To</Label>
</HelpTip>
<AgentSelector
value={assignedTo}
onChange={setAssignedTo}
placeholder="Unassigned"
filterByTeam={team}
/>
</div>
{/* Target Date */}
<div className="space-y-2">
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
<Label>Target Date</Label>
</HelpTip>
<Input
type="datetime-local"
value={targetDate}
onChange={(e) => setTargetDate(e.target.value)}
/>
</div>
{/* Target Date */}
<div className="space-y-2">
<HelpTip label="Informational deadline only — nothing in the lifecycle enforces or auto-escalates on it.">
<Label>Target Date</Label>
</HelpTip>
<Input
type="datetime-local"
value={targetDate}
onChange={(e) => setTargetDate(e.target.value)}
/>
</div>
{/* Sequence */}
<div className="space-y-2">
<HelpTip label="Order within siblings under the same parent — lower runs first. A sibling with a lower sequence must reach a terminal state before this one is claimable; ties run in parallel.">
<Label htmlFor="edit-sequence">Sequence</Label>
</HelpTip>
<Input
id="edit-sequence"
type="number"
min="0"
step="1"
value={sequence}
onChange={(e) => setSequence(e.target.value)}
/>
</div>
{/* Sequence */}
<div className="space-y-2">
<HelpTip label="Order within siblings under the same parent — lower runs first. A sibling with a lower sequence must reach a terminal state before this one is claimable; ties run in parallel.">
<Label htmlFor="edit-sequence">Sequence</Label>
</HelpTip>
<Input
id="edit-sequence"
type="number"
min="0"
step="1"
value={sequence}
onChange={(e) => setSequence(e.target.value)}
/>
</div>
{/* Budget (USD) */}
<div className="space-y-2">
<HelpTip label="Caps this task's own agent-spawn spend; only enforced when the task-budgets feature flag is on. Empty = no cap.">
<Label>Budget (USD)</Label>
</HelpTip>
<Input
type="number"
min="0.01"
step="0.01"
placeholder="No cap"
value={budgetUsd}
onChange={(e) => setBudgetUsd(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Must be greater than 0 a 0 budget would block the task
before it spends a cent. Leave blank for no cap: budgets
enforce only when explicitly set.
{/* Budget (USD) */}
<div className="space-y-2">
<HelpTip label="Caps this task's own agent-spawn spend; only enforced when the task-budgets feature flag is on. Empty = no cap.">
<Label>Budget (USD)</Label>
</HelpTip>
<Input
type="number"
min="0.01"
step="0.01"
placeholder="No cap"
value={budgetUsd}
onChange={(e) => setBudgetUsd(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Must be greater than 0 a 0 budget would block the task before
it spends a cent. Leave blank for no cap: budgets enforce only
when explicitly set.
</p>
{spendUsd != null && (
<p
className="text-xs text-muted-foreground"
data-testid="task-spend"
>
Spent: ${spendUsd.toFixed(2)}
{budgetUsd.trim() && !Number.isNaN(Number(budgetUsd))
? ` / $${Number(budgetUsd).toFixed(2)}`
: ""}
</p>
{spendUsd != null && (
<p
className="text-xs text-muted-foreground"
data-testid="task-spend"
>
Spent: ${spendUsd.toFixed(2)}
{budgetUsd.trim() && !Number.isNaN(Number(budgetUsd))
? ` / $${Number(budgetUsd).toFixed(2)}`
: ""}
)}
</div>
{/* Git Configuration Section */}
<div className="space-y-4 pt-4 border-t">
<div className="flex items-center gap-2 mb-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="font-medium text-sm">
Git & Work Configuration
</span>
</div>
{/* Task Type */}
<div className="space-y-2">
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
<Label>Task Type</Label>
</HelpTip>
<Select
value={taskType}
onValueChange={(v) => setTaskType(v as TaskType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TASK_TYPE_OPTIONS.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Project Selector (required - all tasks follow git workflow) */}
<div className="space-y-2">
<HelpTip label="The repo the branch/PR are opened against. Locked once a branch exists — see below.">
<Label>Project</Label>
</HelpTip>
<ProjectSelector
value={projectId || null}
onChange={(value) => setProjectId(value || "")}
placeholder="Select project..."
disabled={!!task.branch_name}
/>
{task.branch_name && (
<p className="text-xs text-muted-foreground">
Project cannot be changed after work has started
</p>
)}
</div>
{/* Git Configuration Section */}
<div className="space-y-4 pt-4 border-t">
<div className="flex items-center gap-2 mb-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="font-medium text-sm">
Git & Work Configuration
</span>
</div>
{/* Task Type */}
<div className="space-y-2">
<HelpTip label={TASK_TYPE_DESCRIPTIONS[taskType]}>
<Label>Task Type</Label>
</HelpTip>
<Select
value={taskType}
onValueChange={(v) => setTaskType(v as TaskType)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TASK_TYPE_OPTIONS.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Project Selector (required - all tasks follow git workflow) */}
<div className="space-y-2">
<HelpTip label="The repo the branch/PR are opened against. Locked once a branch exists — see below.">
<Label>Project</Label>
</HelpTip>
<ProjectSelector
value={projectId || null}
onChange={(value) => setProjectId(value || "")}
placeholder="Select project..."
disabled={!!task.branch_name}
/>
{task.branch_name && (
<p className="text-xs text-muted-foreground">
Project cannot be changed after work has started
</p>
)}
</div>
</div>
</CollapsibleContent>
</Collapsible>
</div>
</CollapsibleSection>
{/* 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.">
<Button
type="button"
@@ -483,7 +474,7 @@ function EditTaskDialogInner({
</Button>
</span>
</HelpTip>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
@@ -9,7 +9,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Plus, Trash2, Edit3, Check, X } from "lucide-react";
import { toast } from "sonner";
import { HelpTip } from "@/components/ui/help-tip";
import { CollapsibleSection } from "./collapsible-section";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
interface AcceptanceCriteriaProps {
task: Task;
@@ -196,7 +196,11 @@ export function AcceptanceCriteria({ task }: AcceptanceCriteriaProps) {
</HelpTip>
{!isAdding && (
<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" />
Add
</Button>
@@ -9,7 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { HelpTip } from "@/components/ui/help-tip";
import { ListChecks } from "lucide-react";
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
// 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
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 {
FileText,
@@ -42,9 +42,7 @@ type NoteField =
function prReviewBadge(task: Task): React.ReactNode {
const verdict = (
task.notes_structured as
| { pr_review?: { verdict?: string } }
| null
| undefined
{ pr_review?: { verdict?: string } } | null | undefined
)?.pr_review?.verdict;
if (!verdict) {
return (
@@ -80,9 +78,7 @@ function prReviewBadge(task: Task): React.ReactNode {
function prReviewCardBg(task: Task): string {
const verdict = (
task.notes_structured as
| { pr_review?: { verdict?: string } }
| null
| undefined
{ pr_review?: { verdict?: string } } | null | undefined
)?.pr_review?.verdict;
const map: Record<string, string> = {
approved:
@@ -116,9 +112,7 @@ const FIELD_TO_SECTION: Record<NoteField, string> = {
// populated field never renders with no timestamp at all.
function writtenAt(task: Task, field: NoteField): string | null {
const sections = task.notes_structured as
| Record<string, { written_at?: string }>
| null
| undefined;
Record<string, { written_at?: string }> | null | undefined;
const stamp =
sections?.[FIELD_TO_SECTION[field]]?.written_at ?? task.created_at;
if (!stamp) return null;
@@ -168,8 +162,8 @@ function EditableNoteCard({
const [localEditValue, setLocalEditValue] = useState("");
const [editMode, setEditMode] = useState<"write" | "preview">("write");
// Long content starts collapsed; short content starts expanded.
const [sectionOpen, setSectionOpen] = useState(() =>
!exceedsReadabilityThreshold(currentValue ?? ""),
const [sectionOpen, setSectionOpen] = useState(
() => !exceedsReadabilityThreshold(currentValue ?? ""),
);
// 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 { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown";
import { CollapsibleSection } from "./collapsible-section";
import { CollapsibleSection } from "@/components/ui/collapsible-section";
import {
Select,
SelectContent,
@@ -312,7 +312,11 @@ function SubTasksSection({ task, plan }: { task: Task; plan: TaskPlan }) {
</span>
{!isAdding && (
<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" />
Add
</Button>
@@ -9,6 +9,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
DIALOG_SIZES,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
@@ -159,7 +160,11 @@ export function CeoRejectDialog({
Cancel
</Button>
</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
className="inline-block"
tabIndex={!notes.trim() ? 0 : undefined}
@@ -298,7 +303,9 @@ export function CeoApproveDialog({
</HelpTip>
{/* Button's own disabled:pointer-events-none would swallow hover, so
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}>
<Button onClick={handleConfirm} disabled={tooShort || isPending}>
{isPending ? "Approving..." : "Approve & Merge"}
@@ -521,7 +528,7 @@ export function CreatePRDialog({
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="sm:max-w-[525px]">
<DialogContent className={DIALOG_SIZES.md}>
<DialogHeader>
<DialogTitle>Create Pull Request</DialogTitle>
<DialogDescription>
@@ -8,7 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Markdown } from "@/components/ui/markdown";
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 { toast } from "sonner";
@@ -12,6 +12,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ChevronDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { exceedsReadabilityThreshold } from "@/lib/content-readability";
@@ -19,7 +20,7 @@ import { exceedsReadabilityThreshold } from "@/lib/content-readability";
interface CollapsibleSectionProps {
/** Card title content (icon + text + badges as needed) */
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;
/** Controlled open state (e.g. force-open while a section is mid-edit). Omit for uncontrolled. */
open?: boolean;
@@ -39,6 +40,15 @@ interface CollapsibleSectionProps {
className?: string;
headerClassName?: string;
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,
headerClassName,
children,
variant = "card",
contentClassName,
}: CollapsibleSectionProps) {
// Resolve the starting state: explicit defaultOpen > content-derived > default to true
const resolvedDefaultOpen =
@@ -78,6 +90,40 @@ export function CollapsibleSection({
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 (
<Card className={className}>
<Collapsible open={open} onOpenChange={setOpen} className="contents">
@@ -113,13 +159,7 @@ export function CollapsibleSection({
)}
</div>
</CardHeader>
<CollapsibleContent
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",
)}
>
<CollapsibleContent className={motionClassName}>
<CardContent>{children}</CardContent>
</CollapsibleContent>
</Collapsible>
+15
View File
@@ -6,6 +6,21 @@ import { XIcon } from "lucide-react";
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({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {