mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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's workspace at hand-off to QA.
|
in the dev'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's Edit Project dialog.
|
after creation, from this project'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's subtask via
|
below — a fan-out task routes each cell'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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
+48
-8
@@ -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>
|
||||||
@@ -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>) {
|
||||||
|
|||||||
Reference in New Issue
Block a user