mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: integrate planning mode functionality across components
- Added a new PlanningMode feature to manage default planning strategies for features. - Updated the FeatureDefaultsSection to include a dropdown for selecting the default planning mode. - Enhanced AddFeatureDialog and EditFeatureDialog to support planning mode selection and state management. - Introduced PlanningModeSelector component for better user interaction with planning modes. - Updated app state management to include default planning mode and related specifications. - Refactored various UI components to ensure compatibility with new planning mode features.
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-label": "^2.1.8",
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slider": "^1.3.6",
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
|||||||
@@ -13,9 +13,23 @@ interface CheckboxProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElemen
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CheckboxRoot = CheckboxPrimitive.Root as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLButtonElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const CheckboxIndicator = CheckboxPrimitive.Indicator as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Indicator> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLSpanElement>
|
||||||
|
>;
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
||||||
({ className, onCheckedChange, ...props }, ref) => (
|
({ className, onCheckedChange, children: _children, ...props }, ref) => (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxRoot
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80",
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground hover:border-primary/80",
|
||||||
@@ -29,12 +43,12 @@ const Checkbox = React.forwardRef<HTMLButtonElement, CheckboxProps>(
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
<CheckboxIndicator
|
||||||
className={cn("flex items-center justify-center text-current")}
|
className={cn("flex items-center justify-center text-current")}
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</CheckboxPrimitive.Indicator>
|
</CheckboxIndicator>
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxRoot>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|||||||
@@ -6,6 +6,36 @@ import { XIcon } from "lucide-react";
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Type-safe wrappers for Radix UI primitives (React 19 compatibility)
|
||||||
|
const DialogContentPrimitive = DialogPrimitive.Content as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DialogClosePrimitive = DialogPrimitive.Close as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLButtonElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DialogTitlePrimitive = DialogPrimitive.Title as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLHeadingElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DialogDescriptionPrimitive = DialogPrimitive.Description as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
} & React.RefAttributes<HTMLParagraphElement>
|
||||||
|
>;
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
@@ -30,12 +60,20 @@ function DialogClose({
|
|||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DialogOverlayPrimitive = DialogPrimitive.Overlay as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
function DialogOverlay({
|
function DialogOverlay({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay> & {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Overlay
|
<DialogOverlayPrimitive
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
|
"fixed inset-0 z-50 bg-black/60 backdrop-blur-sm",
|
||||||
@@ -66,7 +104,7 @@ function DialogContent({
|
|||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay />
|
<DialogOverlay />
|
||||||
<DialogPrimitive.Content
|
<DialogContentPrimitive
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-[50%] left-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
"fixed top-[50%] left-[50%] z-50 translate-x-[-50%] translate-y-[-50%]",
|
||||||
@@ -91,7 +129,7 @@ function DialogContent({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<DialogPrimitive.Close
|
<DialogClosePrimitive
|
||||||
data-slot="dialog-close"
|
data-slot="dialog-close"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute rounded-lg opacity-60 transition-all duration-200 cursor-pointer",
|
"absolute rounded-lg opacity-60 transition-all duration-200 cursor-pointer",
|
||||||
@@ -105,9 +143,9 @@ function DialogContent({
|
|||||||
>
|
>
|
||||||
<XIcon />
|
<XIcon />
|
||||||
<span className="sr-only">Close</span>
|
<span className="sr-only">Close</span>
|
||||||
</DialogPrimitive.Close>
|
</DialogClosePrimitive>
|
||||||
)}
|
)}
|
||||||
</DialogPrimitive.Content>
|
</DialogContentPrimitive>
|
||||||
</DialogPortal>
|
</DialogPortal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -137,27 +175,42 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Title> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogTitlePrimitive
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold tracking-tight", className)}
|
className={cn("text-lg leading-none font-semibold tracking-tight", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</DialogTitlePrimitive>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogDescription({
|
function DialogDescription({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
|
title,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
}: React.ComponentProps<typeof DialogPrimitive.Description> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
title?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogDescriptionPrimitive
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-muted-foreground text-sm leading-relaxed", className)}
|
className={cn("text-muted-foreground text-sm leading-relaxed", className)}
|
||||||
|
title={title}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</DialogDescriptionPrimitive>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,83 @@ import { Check, ChevronRight, Circle } from "lucide-react"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Type-safe wrappers for Radix UI primitives (React 19 compatibility)
|
||||||
|
const DropdownMenuTriggerPrimitive = DropdownMenuPrimitive.Trigger as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
} & React.RefAttributes<HTMLButtonElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuSubTriggerPrimitive = DropdownMenuPrimitive.SubTrigger as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroupPrimitive = DropdownMenuPrimitive.RadioGroup as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioGroup> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuItemPrimitive = DropdownMenuPrimitive.Item as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuRadioItemPrimitive = DropdownMenuPrimitive.RadioItem as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuLabelPrimitive = DropdownMenuPrimitive.Label as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItemPrimitive = DropdownMenuPrimitive.CheckboxItem as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuItemIndicatorPrimitive = DropdownMenuPrimitive.ItemIndicator as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.ItemIndicator> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
} & React.RefAttributes<HTMLSpanElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const DropdownMenuSeparatorPrimitive = DropdownMenuPrimitive.Separator as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> & {
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
function DropdownMenuTrigger({
|
||||||
|
children,
|
||||||
|
asChild,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuTriggerPrimitive asChild={asChild} {...props}>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuTriggerPrimitive>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
@@ -16,15 +90,26 @@ const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
|||||||
|
|
||||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
function DropdownMenuRadioGroup({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup> & { children?: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<DropdownMenuRadioGroupPrimitive {...props}>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuRadioGroupPrimitive>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const DropdownMenuSubTrigger = React.forwardRef<
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
|
children?: React.ReactNode
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
>(({ className, inset, children, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuSubTriggerPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent hover:bg-accent",
|
"flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent hover:bg-accent",
|
||||||
@@ -35,13 +120,15 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<ChevronRight className="ml-auto h-4 w-4" />
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.SubTrigger>
|
</DropdownMenuSubTriggerPrimitive>
|
||||||
))
|
))
|
||||||
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
const DropdownMenuSubContent = React.forwardRef<
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> & {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
@@ -58,7 +145,9 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
|
|||||||
|
|
||||||
const DropdownMenuContent = React.forwardRef<
|
const DropdownMenuContent = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Portal>
|
<DropdownMenuPrimitive.Portal>
|
||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
@@ -78,9 +167,10 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
}
|
children?: React.ReactNode
|
||||||
>(({ className, inset, ...props }, ref) => (
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
<DropdownMenuPrimitive.Item
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuItemPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
"relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
||||||
@@ -88,15 +178,20 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuItemPrimitive>
|
||||||
))
|
))
|
||||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
>(({ className, children, checked, ...props }, ref) => (
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuCheckboxItemPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
||||||
@@ -106,21 +201,23 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuItemIndicatorPrimitive>
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuItemIndicatorPrimitive>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.CheckboxItem>
|
</DropdownMenuCheckboxItemPrimitive>
|
||||||
))
|
))
|
||||||
DropdownMenuCheckboxItem.displayName =
|
DropdownMenuCheckboxItem.displayName =
|
||||||
DropdownMenuPrimitive.CheckboxItem.displayName
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
const DropdownMenuRadioItem = React.forwardRef<
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||||
|
children?: React.ReactNode
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
>(({ className, children, ...props }, ref) => (
|
>(({ className, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuRadioItemPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed hover:bg-accent",
|
||||||
@@ -129,12 +226,12 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<DropdownMenuPrimitive.ItemIndicator>
|
<DropdownMenuItemIndicatorPrimitive>
|
||||||
<Circle className="h-2 w-2 fill-current" />
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
</DropdownMenuPrimitive.ItemIndicator>
|
</DropdownMenuItemIndicatorPrimitive>
|
||||||
</span>
|
</span>
|
||||||
{children}
|
{children}
|
||||||
</DropdownMenuPrimitive.RadioItem>
|
</DropdownMenuRadioItemPrimitive>
|
||||||
))
|
))
|
||||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
@@ -142,9 +239,11 @@ const DropdownMenuLabel = React.forwardRef<
|
|||||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
|
children?: React.ReactNode
|
||||||
|
className?: string
|
||||||
}
|
}
|
||||||
>(({ className, inset, ...props }, ref) => (
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuLabelPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
@@ -152,15 +251,19 @@ const DropdownMenuLabel = React.forwardRef<
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuLabelPrimitive>
|
||||||
))
|
))
|
||||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
const DropdownMenuSeparator = React.forwardRef<
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> & {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuSeparatorPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -5,6 +5,20 @@ import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Type-safe wrappers for Radix UI primitives (React 19 compatibility)
|
||||||
|
const PopoverTriggerPrimitive = PopoverPrimitive.Trigger as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
} & React.RefAttributes<HTMLButtonElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const PopoverContentPrimitive = PopoverPrimitive.Content as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
function Popover({
|
function Popover({
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
@@ -12,9 +26,18 @@ function Popover({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function PopoverTrigger({
|
function PopoverTrigger({
|
||||||
|
children,
|
||||||
|
asChild,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger> & {
|
||||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
children?: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<PopoverTriggerPrimitive data-slot="popover-trigger" asChild={asChild} {...props}>
|
||||||
|
{children}
|
||||||
|
</PopoverTriggerPrimitive>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PopoverContent({
|
function PopoverContent({
|
||||||
@@ -22,10 +45,12 @@ function PopoverContent({
|
|||||||
align = "center",
|
align = "center",
|
||||||
sideOffset = 4,
|
sideOffset = 4,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
}: React.ComponentProps<typeof PopoverPrimitive.Content> & {
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<PopoverPrimitive.Portal>
|
<PopoverPrimitive.Portal>
|
||||||
<PopoverPrimitive.Content
|
<PopoverContentPrimitive
|
||||||
data-slot="popover-content"
|
data-slot="popover-content"
|
||||||
align={align}
|
align={align}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
|
|||||||
160
apps/app/src/components/ui/select.tsx
Normal file
160
apps/app/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root;
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group;
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value;
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
));
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
));
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
));
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName;
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
));
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
));
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
};
|
||||||
@@ -4,6 +4,33 @@ import * as React from "react";
|
|||||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
// Type-safe wrappers for Radix UI primitives (React 19 compatibility)
|
||||||
|
const SliderRootPrimitive = SliderPrimitive.Root as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLSpanElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const SliderTrackPrimitive = SliderPrimitive.Track as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Track> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLSpanElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const SliderRangePrimitive = SliderPrimitive.Range as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Range> & {
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLSpanElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const SliderThumbPrimitive = SliderPrimitive.Thumb as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Thumb> & {
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLSpanElement>
|
||||||
|
>;
|
||||||
|
|
||||||
interface SliderProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "defaultValue" | "dir"> {
|
interface SliderProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "defaultValue" | "dir"> {
|
||||||
value?: number[];
|
value?: number[];
|
||||||
defaultValue?: number[];
|
defaultValue?: number[];
|
||||||
@@ -21,7 +48,7 @@ interface SliderProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, "defau
|
|||||||
|
|
||||||
const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<SliderPrimitive.Root
|
<SliderRootPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full touch-none select-none items-center",
|
"relative flex w-full touch-none select-none items-center",
|
||||||
@@ -29,11 +56,11 @@ const Slider = React.forwardRef<HTMLSpanElement, SliderProps>(
|
|||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SliderPrimitive.Track className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
|
<SliderTrackPrimitive className="slider-track relative h-1.5 w-full grow overflow-hidden rounded-full bg-muted cursor-pointer">
|
||||||
<SliderPrimitive.Range className="slider-range absolute h-full bg-primary" />
|
<SliderRangePrimitive className="slider-range absolute h-full bg-primary" />
|
||||||
</SliderPrimitive.Track>
|
</SliderTrackPrimitive>
|
||||||
<SliderPrimitive.Thumb className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
|
<SliderThumbPrimitive className="slider-thumb block h-4 w-4 rounded-full border border-border bg-card shadow transition-colors cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed hover:bg-accent" />
|
||||||
</SliderPrimitive.Root>
|
</SliderRootPrimitive>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
Slider.displayName = SliderPrimitive.Root.displayName;
|
Slider.displayName = SliderPrimitive.Root.displayName;
|
||||||
|
|||||||
@@ -5,41 +5,86 @@ import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Type-safe wrappers for Radix UI primitives (React 19 compatibility)
|
||||||
|
const TabsRootPrimitive = TabsPrimitive.Root as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const TabsListPrimitive = TabsPrimitive.List as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const TabsTriggerPrimitive = TabsPrimitive.Trigger as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLButtonElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const TabsContentPrimitive = TabsPrimitive.Content as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Root> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Root
|
<TabsRootPrimitive
|
||||||
data-slot="tabs"
|
data-slot="tabs"
|
||||||
className={cn("flex flex-col gap-2", className)}
|
className={cn("flex flex-col gap-2", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</TabsRootPrimitive>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
}: React.ComponentProps<typeof TabsPrimitive.List> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.List
|
<TabsListPrimitive
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px] border border-border",
|
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px] border border-border",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</TabsListPrimitive>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Trigger
|
<TabsTriggerPrimitive
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 cursor-pointer",
|
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all duration-200 cursor-pointer",
|
||||||
@@ -51,20 +96,28 @@ function TabsTrigger({
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</TabsTriggerPrimitive>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
}: React.ComponentProps<typeof TabsPrimitive.Content> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<TabsPrimitive.Content
|
<TabsContentPrimitive
|
||||||
data-slot="tabs-content"
|
data-slot="tabs-content"
|
||||||
className={cn("flex-1 outline-none", className)}
|
className={cn("flex-1 outline-none", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
>
|
||||||
|
{children}
|
||||||
|
</TabsContentPrimitive>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,18 +5,47 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
// Type-safe wrappers for Radix UI primitives (React 19 compatibility)
|
||||||
|
const TooltipTriggerPrimitive = TooltipPrimitive.Trigger as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
} & React.RefAttributes<HTMLButtonElement>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const TooltipContentPrimitive = TooltipPrimitive.Content as React.ForwardRefExoticComponent<
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
||||||
|
className?: string;
|
||||||
|
} & React.RefAttributes<HTMLDivElement>
|
||||||
|
>;
|
||||||
|
|
||||||
const TooltipProvider = TooltipPrimitive.Provider
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
const Tooltip = TooltipPrimitive.Root
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
function TooltipTrigger({
|
||||||
|
children,
|
||||||
|
asChild,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger> & {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TooltipTriggerPrimitive asChild={asChild} {...props}>
|
||||||
|
{children}
|
||||||
|
</TooltipTriggerPrimitive>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const TooltipContent = React.forwardRef<
|
const TooltipContent = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> & {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
>(({ className, sideOffset = 6, ...props }, ref) => (
|
>(({ className, sideOffset = 6, ...props }, ref) => (
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipPrimitive.Content
|
<TooltipContentPrimitive
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
FeatureImagePath as DescriptionImagePath,
|
FeatureImagePath as DescriptionImagePath,
|
||||||
ImagePreviewMap,
|
ImagePreviewMap,
|
||||||
} from "@/components/ui/description-image-dropzone";
|
} from "@/components/ui/description-image-dropzone";
|
||||||
import { MessageSquare, Settings2, FlaskConical, Sparkles, ChevronDown } from "lucide-react";
|
import { MessageSquare, Settings2, SlidersHorizontal, Sparkles, ChevronDown } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { modelSupportsThinking } from "@/lib/utils";
|
import { modelSupportsThinking } from "@/lib/utils";
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
FeatureImage,
|
FeatureImage,
|
||||||
AIProfile,
|
AIProfile,
|
||||||
|
PlanningMode,
|
||||||
} from "@/store/app-store";
|
} from "@/store/app-store";
|
||||||
import {
|
import {
|
||||||
ModelSelector,
|
ModelSelector,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
ProfileQuickSelect,
|
ProfileQuickSelect,
|
||||||
TestingTabContent,
|
TestingTabContent,
|
||||||
PrioritySelector,
|
PrioritySelector,
|
||||||
|
PlanningModeSelector,
|
||||||
} from "../shared";
|
} from "../shared";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -57,6 +59,7 @@ interface AddFeatureDialogProps {
|
|||||||
model: AgentModel;
|
model: AgentModel;
|
||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
planningMode: PlanningMode;
|
||||||
}) => void;
|
}) => void;
|
||||||
categorySuggestions: string[];
|
categorySuggestions: string[];
|
||||||
defaultSkipTests: boolean;
|
defaultSkipTests: boolean;
|
||||||
@@ -92,19 +95,21 @@ export function AddFeatureDialog({
|
|||||||
const [descriptionError, setDescriptionError] = useState(false);
|
const [descriptionError, setDescriptionError] = useState(false);
|
||||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||||
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
|
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
|
||||||
|
const [planningMode, setPlanningMode] = useState<PlanningMode>('skip');
|
||||||
|
|
||||||
// Get enhancement model from store
|
// Get enhancement model and default planning mode from store
|
||||||
const { enhancementModel } = useAppStore();
|
const { enhancementModel, defaultPlanningMode } = useAppStore();
|
||||||
|
|
||||||
// Sync skipTests default when dialog opens
|
// Sync defaults when dialog opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
setNewFeature((prev) => ({
|
setNewFeature((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
skipTests: defaultSkipTests,
|
skipTests: defaultSkipTests,
|
||||||
}));
|
}));
|
||||||
|
setPlanningMode(defaultPlanningMode);
|
||||||
}
|
}
|
||||||
}, [open, defaultSkipTests]);
|
}, [open, defaultSkipTests, defaultPlanningMode]);
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
if (!newFeature.description.trim()) {
|
if (!newFeature.description.trim()) {
|
||||||
@@ -128,6 +133,7 @@ export function AddFeatureDialog({
|
|||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
thinkingLevel: normalizedThinking,
|
thinkingLevel: normalizedThinking,
|
||||||
priority: newFeature.priority,
|
priority: newFeature.priority,
|
||||||
|
planningMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
@@ -142,6 +148,7 @@ export function AddFeatureDialog({
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
thinkingLevel: "none",
|
thinkingLevel: "none",
|
||||||
});
|
});
|
||||||
|
setPlanningMode(defaultPlanningMode);
|
||||||
setNewFeaturePreviewMap(new Map());
|
setNewFeaturePreviewMap(new Map());
|
||||||
setShowAdvancedOptions(false);
|
setShowAdvancedOptions(false);
|
||||||
setDescriptionError(false);
|
setDescriptionError(false);
|
||||||
@@ -209,13 +216,13 @@ export function AddFeatureDialog({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
compact={!isMaximized}
|
compact={!isMaximized}
|
||||||
data-testid="add-feature-dialog"
|
data-testid="add-feature-dialog"
|
||||||
onPointerDownOutside={(e) => {
|
onPointerDownOutside={(e: CustomEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onInteractOutside={(e) => {
|
onInteractOutside={(e: CustomEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -241,9 +248,9 @@ export function AddFeatureDialog({
|
|||||||
<Settings2 className="w-4 h-4 mr-2" />
|
<Settings2 className="w-4 h-4 mr-2" />
|
||||||
Model
|
Model
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="testing" data-testid="tab-testing">
|
<TabsTrigger value="options" data-testid="tab-options">
|
||||||
<FlaskConical className="w-4 h-4 mr-2" />
|
<SlidersHorizontal className="w-4 h-4 mr-2" />
|
||||||
Testing
|
Options
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@@ -395,8 +402,20 @@ export function AddFeatureDialog({
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Testing Tab */}
|
{/* Options Tab */}
|
||||||
<TabsContent value="testing" className="space-y-4 overflow-y-auto cursor-default">
|
<TabsContent value="options" className="space-y-4 overflow-y-auto cursor-default">
|
||||||
|
{/* Planning Mode Section */}
|
||||||
|
<PlanningModeSelector
|
||||||
|
mode={planningMode}
|
||||||
|
onModeChange={setPlanningMode}
|
||||||
|
featureDescription={newFeature.description}
|
||||||
|
testIdPrefix="add-feature"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="border-t border-border my-4" />
|
||||||
|
|
||||||
|
{/* Testing Section */}
|
||||||
<TestingTabContent
|
<TestingTabContent
|
||||||
skipTests={newFeature.skipTests}
|
skipTests={newFeature.skipTests}
|
||||||
onSkipTestsChange={(skipTests) =>
|
onSkipTestsChange={(skipTests) =>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
FeatureImagePath as DescriptionImagePath,
|
FeatureImagePath as DescriptionImagePath,
|
||||||
ImagePreviewMap,
|
ImagePreviewMap,
|
||||||
} from "@/components/ui/description-image-dropzone";
|
} from "@/components/ui/description-image-dropzone";
|
||||||
import { MessageSquare, Settings2, FlaskConical, Sparkles, ChevronDown, GitBranch } from "lucide-react";
|
import { MessageSquare, Settings2, SlidersHorizontal, Sparkles, ChevronDown, GitBranch } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
import { modelSupportsThinking } from "@/lib/utils";
|
import { modelSupportsThinking } from "@/lib/utils";
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
AIProfile,
|
AIProfile,
|
||||||
useAppStore,
|
useAppStore,
|
||||||
|
PlanningMode,
|
||||||
} from "@/store/app-store";
|
} from "@/store/app-store";
|
||||||
import {
|
import {
|
||||||
ModelSelector,
|
ModelSelector,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
ProfileQuickSelect,
|
ProfileQuickSelect,
|
||||||
TestingTabContent,
|
TestingTabContent,
|
||||||
PrioritySelector,
|
PrioritySelector,
|
||||||
|
PlanningModeSelector,
|
||||||
} from "../shared";
|
} from "../shared";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -59,6 +61,7 @@ interface EditFeatureDialogProps {
|
|||||||
thinkingLevel: ThinkingLevel;
|
thinkingLevel: ThinkingLevel;
|
||||||
imagePaths: DescriptionImagePath[];
|
imagePaths: DescriptionImagePath[];
|
||||||
priority: number;
|
priority: number;
|
||||||
|
planningMode: PlanningMode;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
categorySuggestions: string[];
|
categorySuggestions: string[];
|
||||||
@@ -85,13 +88,16 @@ export function EditFeatureDialog({
|
|||||||
const [isEnhancing, setIsEnhancing] = useState(false);
|
const [isEnhancing, setIsEnhancing] = useState(false);
|
||||||
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
|
const [enhancementMode, setEnhancementMode] = useState<'improve' | 'technical' | 'simplify' | 'acceptance'>('improve');
|
||||||
const [showDependencyTree, setShowDependencyTree] = useState(false);
|
const [showDependencyTree, setShowDependencyTree] = useState(false);
|
||||||
|
const [planningMode, setPlanningMode] = useState<PlanningMode>(feature?.planningMode ?? 'skip');
|
||||||
|
|
||||||
// Get enhancement model from store
|
// Get enhancement model from store
|
||||||
const { enhancementModel } = useAppStore();
|
const { enhancementModel } = useAppStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditingFeature(feature);
|
setEditingFeature(feature);
|
||||||
if (!feature) {
|
if (feature) {
|
||||||
|
setPlanningMode(feature.planningMode ?? 'skip');
|
||||||
|
} else {
|
||||||
setEditFeaturePreviewMap(new Map());
|
setEditFeaturePreviewMap(new Map());
|
||||||
setShowEditAdvancedOptions(false);
|
setShowEditAdvancedOptions(false);
|
||||||
}
|
}
|
||||||
@@ -114,6 +120,7 @@ export function EditFeatureDialog({
|
|||||||
thinkingLevel: normalizedThinking,
|
thinkingLevel: normalizedThinking,
|
||||||
imagePaths: editingFeature.imagePaths ?? [],
|
imagePaths: editingFeature.imagePaths ?? [],
|
||||||
priority: editingFeature.priority ?? 2,
|
priority: editingFeature.priority ?? 2,
|
||||||
|
planningMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
onUpdate(editingFeature.id, updates);
|
onUpdate(editingFeature.id, updates);
|
||||||
@@ -186,13 +193,13 @@ export function EditFeatureDialog({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
compact={!isMaximized}
|
compact={!isMaximized}
|
||||||
data-testid="edit-feature-dialog"
|
data-testid="edit-feature-dialog"
|
||||||
onPointerDownOutside={(e) => {
|
onPointerDownOutside={(e: CustomEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onInteractOutside={(e) => {
|
onInteractOutside={(e: CustomEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
if (target.closest('[data-testid="category-autocomplete-list"]')) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -216,9 +223,9 @@ export function EditFeatureDialog({
|
|||||||
<Settings2 className="w-4 h-4 mr-2" />
|
<Settings2 className="w-4 h-4 mr-2" />
|
||||||
Model
|
Model
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="testing" data-testid="edit-tab-testing">
|
<TabsTrigger value="options" data-testid="edit-tab-options">
|
||||||
<FlaskConical className="w-4 h-4 mr-2" />
|
<SlidersHorizontal className="w-4 h-4 mr-2" />
|
||||||
Testing
|
Options
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@@ -381,8 +388,20 @@ export function EditFeatureDialog({
|
|||||||
)}
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* Testing Tab */}
|
{/* Options Tab */}
|
||||||
<TabsContent value="testing" className="space-y-4 overflow-y-auto cursor-default">
|
<TabsContent value="options" className="space-y-4 overflow-y-auto cursor-default">
|
||||||
|
{/* Planning Mode Section */}
|
||||||
|
<PlanningModeSelector
|
||||||
|
mode={planningMode}
|
||||||
|
onModeChange={setPlanningMode}
|
||||||
|
featureDescription={editingFeature.description}
|
||||||
|
testIdPrefix="edit-feature"
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="border-t border-border my-4" />
|
||||||
|
|
||||||
|
{/* Testing Section */}
|
||||||
<TestingTabContent
|
<TestingTabContent
|
||||||
skipTests={editingFeature.skipTests ?? false}
|
skipTests={editingFeature.skipTests ?? false}
|
||||||
onSkipTestsChange={(skipTests) =>
|
onSkipTestsChange={(skipTests) =>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function FollowUpDialog({
|
|||||||
<DialogContent
|
<DialogContent
|
||||||
compact={!isMaximized}
|
compact={!isMaximized}
|
||||||
data-testid="follow-up-dialog"
|
data-testid="follow-up-dialog"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e: React.KeyboardEvent) => {
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && prompt.trim()) {
|
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && prompt.trim()) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onSend();
|
onSend();
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from "./thinking-level-selector";
|
|||||||
export * from "./profile-quick-select";
|
export * from "./profile-quick-select";
|
||||||
export * from "./testing-tab-content";
|
export * from "./testing-tab-content";
|
||||||
export * from "./priority-selector";
|
export * from "./priority-selector";
|
||||||
|
export * from "./planning-mode-selector";
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Zap, ClipboardList, FileText, ScrollText,
|
||||||
|
Loader2, Check, Eye, RefreshCw, Sparkles
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
|
export interface PlanSpec {
|
||||||
|
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||||
|
content?: string;
|
||||||
|
version: number;
|
||||||
|
generatedAt?: string;
|
||||||
|
approvedAt?: string;
|
||||||
|
reviewedByUser: boolean;
|
||||||
|
tasksCompleted?: number;
|
||||||
|
tasksTotal?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PlanningModeSelectorProps {
|
||||||
|
mode: PlanningMode;
|
||||||
|
onModeChange: (mode: PlanningMode) => void;
|
||||||
|
planSpec?: PlanSpec;
|
||||||
|
onGenerateSpec?: () => void;
|
||||||
|
onApproveSpec?: () => void;
|
||||||
|
onRejectSpec?: () => void;
|
||||||
|
onViewSpec?: () => void;
|
||||||
|
isGenerating?: boolean;
|
||||||
|
featureDescription?: string; // For auto-generation context
|
||||||
|
testIdPrefix?: string;
|
||||||
|
compact?: boolean; // For use in dialogs vs settings
|
||||||
|
}
|
||||||
|
|
||||||
|
const modes = [
|
||||||
|
{
|
||||||
|
value: 'skip' as const,
|
||||||
|
label: 'Skip',
|
||||||
|
description: 'Direct implementation, no upfront planning',
|
||||||
|
icon: Zap,
|
||||||
|
color: 'text-emerald-500',
|
||||||
|
bgColor: 'bg-emerald-500/10',
|
||||||
|
borderColor: 'border-emerald-500/30',
|
||||||
|
badge: 'Default',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lite' as const,
|
||||||
|
label: 'Lite',
|
||||||
|
description: 'Think through approach, create task list',
|
||||||
|
icon: ClipboardList,
|
||||||
|
color: 'text-blue-500',
|
||||||
|
bgColor: 'bg-blue-500/10',
|
||||||
|
borderColor: 'border-blue-500/30',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'spec' as const,
|
||||||
|
label: 'Spec',
|
||||||
|
description: 'Generate spec with acceptance criteria',
|
||||||
|
icon: FileText,
|
||||||
|
color: 'text-purple-500',
|
||||||
|
bgColor: 'bg-purple-500/10',
|
||||||
|
borderColor: 'border-purple-500/30',
|
||||||
|
badge: 'Approval Required',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'full' as const,
|
||||||
|
label: 'Full',
|
||||||
|
description: 'Comprehensive spec with phased plan',
|
||||||
|
icon: ScrollText,
|
||||||
|
color: 'text-amber-500',
|
||||||
|
bgColor: 'bg-amber-500/10',
|
||||||
|
borderColor: 'border-amber-500/30',
|
||||||
|
badge: 'Approval Required',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function PlanningModeSelector({
|
||||||
|
mode,
|
||||||
|
onModeChange,
|
||||||
|
planSpec,
|
||||||
|
onGenerateSpec,
|
||||||
|
onApproveSpec,
|
||||||
|
onRejectSpec,
|
||||||
|
onViewSpec,
|
||||||
|
isGenerating = false,
|
||||||
|
featureDescription,
|
||||||
|
testIdPrefix = 'planning',
|
||||||
|
compact = false,
|
||||||
|
}: PlanningModeSelectorProps) {
|
||||||
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
|
const selectedMode = modes.find(m => m.value === mode);
|
||||||
|
const requiresApproval = mode === 'spec' || mode === 'full';
|
||||||
|
const canGenerate = requiresApproval && featureDescription?.trim() && !isGenerating;
|
||||||
|
const hasSpec = planSpec && planSpec.content;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Header with icon */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className={cn(
|
||||||
|
"w-8 h-8 rounded-lg flex items-center justify-center",
|
||||||
|
selectedMode?.bgColor || "bg-muted"
|
||||||
|
)}>
|
||||||
|
{selectedMode && <selectedMode.icon className={cn("h-4 w-4", selectedMode.color)} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-medium">Planning Mode</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Choose how much upfront planning before implementation
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick action buttons when spec/full mode */}
|
||||||
|
{requiresApproval && hasSpec && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onViewSpec}
|
||||||
|
className="h-7 px-2"
|
||||||
|
>
|
||||||
|
<Eye className="h-3.5 w-3.5 mr-1" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mode Selection Cards */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"grid gap-2",
|
||||||
|
compact ? "grid-cols-2" : "grid-cols-2 sm:grid-cols-4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{modes.map((m) => {
|
||||||
|
const isSelected = mode === m.value;
|
||||||
|
const Icon = m.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={m.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onModeChange(m.value)}
|
||||||
|
data-testid={`${testIdPrefix}-mode-${m.value}`}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center gap-2 p-3 rounded-xl cursor-pointer transition-all duration-200",
|
||||||
|
"border-2 hover:border-primary/50",
|
||||||
|
isSelected
|
||||||
|
? cn("border-primary", m.bgColor)
|
||||||
|
: "border-border/50 bg-card/50 hover:bg-accent/30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
"w-10 h-10 rounded-full flex items-center justify-center transition-colors",
|
||||||
|
isSelected ? m.bgColor : "bg-muted"
|
||||||
|
)}>
|
||||||
|
<Icon className={cn(
|
||||||
|
"h-5 w-5 transition-colors",
|
||||||
|
isSelected ? m.color : "text-muted-foreground"
|
||||||
|
)} />
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-1">
|
||||||
|
<span className={cn(
|
||||||
|
"font-medium text-sm",
|
||||||
|
isSelected ? "text-foreground" : "text-muted-foreground"
|
||||||
|
)}>
|
||||||
|
{m.label}
|
||||||
|
</span>
|
||||||
|
{m.badge && (
|
||||||
|
<span className={cn(
|
||||||
|
"text-[9px] px-1 py-0.5 rounded font-medium",
|
||||||
|
m.badge === 'Default'
|
||||||
|
? "bg-emerald-500/15 text-emerald-500"
|
||||||
|
: "bg-amber-500/15 text-amber-500"
|
||||||
|
)}>
|
||||||
|
{m.badge === 'Default' ? 'Default' : 'Review'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!compact && (
|
||||||
|
<p className="text-[10px] text-muted-foreground mt-0.5 line-clamp-2">
|
||||||
|
{m.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spec Preview/Actions Panel - Only for spec/full modes */}
|
||||||
|
{requiresApproval && (
|
||||||
|
<div className={cn(
|
||||||
|
"rounded-xl border transition-all duration-300",
|
||||||
|
planSpec?.status === 'approved'
|
||||||
|
? "border-emerald-500/30 bg-emerald-500/5"
|
||||||
|
: planSpec?.status === 'generated'
|
||||||
|
? "border-amber-500/30 bg-amber-500/5"
|
||||||
|
: "border-border/50 bg-muted/30"
|
||||||
|
)}>
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
{/* Status indicator */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{isGenerating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin text-primary" />
|
||||||
|
<span className="text-sm text-muted-foreground">Generating {mode === 'full' ? 'comprehensive spec' : 'spec'}...</span>
|
||||||
|
</>
|
||||||
|
) : planSpec?.status === 'approved' ? (
|
||||||
|
<>
|
||||||
|
<Check className="h-4 w-4 text-emerald-500" />
|
||||||
|
<span className="text-sm text-emerald-500 font-medium">Spec Approved</span>
|
||||||
|
</>
|
||||||
|
) : planSpec?.status === 'generated' ? (
|
||||||
|
<>
|
||||||
|
<Eye className="h-4 w-4 text-amber-500" />
|
||||||
|
<span className="text-sm text-amber-500 font-medium">Spec Ready for Review</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Spec will be generated when feature starts
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto-generate toggle area */}
|
||||||
|
{!planSpec?.status && canGenerate && onGenerateSpec && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onGenerateSpec}
|
||||||
|
disabled={isGenerating}
|
||||||
|
className="h-7"
|
||||||
|
>
|
||||||
|
<Sparkles className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Pre-generate
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spec content preview */}
|
||||||
|
{hasSpec && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setShowPreview(!showPreview)}
|
||||||
|
className="w-full justify-between h-8 px-2"
|
||||||
|
>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{showPreview ? 'Hide Preview' : 'Show Preview'}
|
||||||
|
</span>
|
||||||
|
<Eye className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showPreview && (
|
||||||
|
<div className="rounded-lg bg-background/80 border border-border/50 p-3 max-h-48 overflow-y-auto">
|
||||||
|
<pre className="text-xs text-muted-foreground whitespace-pre-wrap font-mono">
|
||||||
|
{planSpec.content}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons when spec is generated */}
|
||||||
|
{planSpec?.status === 'generated' && (
|
||||||
|
<div className="flex items-center gap-2 pt-2 border-t border-border/30">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onRejectSpec}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Request Changes
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={onApproveSpec}
|
||||||
|
className="flex-1 bg-emerald-500 hover:bg-emerald-600 text-white"
|
||||||
|
>
|
||||||
|
<Check className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Approve Spec
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Regenerate option when approved */}
|
||||||
|
{planSpec?.status === 'approved' && onGenerateSpec && (
|
||||||
|
<div className="flex items-center justify-end pt-2 border-t border-border/30">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onGenerateSpec}
|
||||||
|
className="h-7"
|
||||||
|
>
|
||||||
|
<RefreshCw className="h-3.5 w-3.5 mr-1" />
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info text for non-approval modes */}
|
||||||
|
{!requiresApproval && (
|
||||||
|
<p className="text-xs text-muted-foreground bg-muted/30 rounded-lg p-3">
|
||||||
|
{mode === 'skip'
|
||||||
|
? "The agent will start implementing immediately without creating a plan or spec."
|
||||||
|
: "The agent will create a planning outline before implementing, but won't wait for approval."}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,6 +38,8 @@ export function SettingsView() {
|
|||||||
setMuteDoneSound,
|
setMuteDoneSound,
|
||||||
currentProject,
|
currentProject,
|
||||||
moveProjectToTrash,
|
moveProjectToTrash,
|
||||||
|
defaultPlanningMode,
|
||||||
|
setDefaultPlanningMode,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Convert electron Project to settings-view Project type
|
// Convert electron Project to settings-view Project type
|
||||||
@@ -119,9 +121,11 @@ export function SettingsView() {
|
|||||||
showProfilesOnly={showProfilesOnly}
|
showProfilesOnly={showProfilesOnly}
|
||||||
defaultSkipTests={defaultSkipTests}
|
defaultSkipTests={defaultSkipTests}
|
||||||
useWorktrees={useWorktrees}
|
useWorktrees={useWorktrees}
|
||||||
|
defaultPlanningMode={defaultPlanningMode}
|
||||||
onShowProfilesOnlyChange={setShowProfilesOnly}
|
onShowProfilesOnlyChange={setShowProfilesOnly}
|
||||||
onDefaultSkipTestsChange={setDefaultSkipTests}
|
onDefaultSkipTestsChange={setDefaultSkipTests}
|
||||||
onUseWorktreesChange={setUseWorktrees}
|
onUseWorktreesChange={setUseWorktrees}
|
||||||
|
onDefaultPlanningModeChange={setDefaultPlanningMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "danger":
|
case "danger":
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { FlaskConical, Settings2, TestTube, GitBranch } from "lucide-react";
|
import {
|
||||||
|
FlaskConical, Settings2, TestTube, GitBranch,
|
||||||
|
Zap, ClipboardList, FileText, ScrollText
|
||||||
|
} from "lucide-react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
interface FeatureDefaultsSectionProps {
|
interface FeatureDefaultsSectionProps {
|
||||||
showProfilesOnly: boolean;
|
showProfilesOnly: boolean;
|
||||||
defaultSkipTests: boolean;
|
defaultSkipTests: boolean;
|
||||||
useWorktrees: boolean;
|
useWorktrees: boolean;
|
||||||
|
defaultPlanningMode: PlanningMode;
|
||||||
onShowProfilesOnlyChange: (value: boolean) => void;
|
onShowProfilesOnlyChange: (value: boolean) => void;
|
||||||
onDefaultSkipTestsChange: (value: boolean) => void;
|
onDefaultSkipTestsChange: (value: boolean) => void;
|
||||||
onUseWorktreesChange: (value: boolean) => void;
|
onUseWorktreesChange: (value: boolean) => void;
|
||||||
|
onDefaultPlanningModeChange: (value: PlanningMode) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FeatureDefaultsSection({
|
export function FeatureDefaultsSection({
|
||||||
showProfilesOnly,
|
showProfilesOnly,
|
||||||
defaultSkipTests,
|
defaultSkipTests,
|
||||||
useWorktrees,
|
useWorktrees,
|
||||||
|
defaultPlanningMode,
|
||||||
onShowProfilesOnlyChange,
|
onShowProfilesOnlyChange,
|
||||||
onDefaultSkipTestsChange,
|
onDefaultSkipTestsChange,
|
||||||
onUseWorktreesChange,
|
onUseWorktreesChange,
|
||||||
|
onDefaultPlanningModeChange,
|
||||||
}: FeatureDefaultsSectionProps) {
|
}: FeatureDefaultsSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -43,6 +59,76 @@ export function FeatureDefaultsSection({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-5">
|
<div className="p-6 space-y-5">
|
||||||
|
{/* Planning Mode Default */}
|
||||||
|
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||||
|
<div className={cn(
|
||||||
|
"w-10 h-10 mt-0.5 rounded-xl flex items-center justify-center shrink-0",
|
||||||
|
defaultPlanningMode === 'skip' ? "bg-emerald-500/10" :
|
||||||
|
defaultPlanningMode === 'lite' ? "bg-blue-500/10" :
|
||||||
|
defaultPlanningMode === 'spec' ? "bg-purple-500/10" :
|
||||||
|
"bg-amber-500/10"
|
||||||
|
)}>
|
||||||
|
{defaultPlanningMode === 'skip' && <Zap className="w-5 h-5 text-emerald-500" />}
|
||||||
|
{defaultPlanningMode === 'lite' && <ClipboardList className="w-5 h-5 text-blue-500" />}
|
||||||
|
{defaultPlanningMode === 'spec' && <FileText className="w-5 h-5 text-purple-500" />}
|
||||||
|
{defaultPlanningMode === 'full' && <ScrollText className="w-5 h-5 text-amber-500" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label className="text-foreground font-medium">
|
||||||
|
Default Planning Mode
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={defaultPlanningMode}
|
||||||
|
onValueChange={(v: string) => onDefaultPlanningModeChange(v as PlanningMode)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="w-[160px] h-8"
|
||||||
|
data-testid="default-planning-mode-select"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="skip">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap className="h-3.5 w-3.5 text-emerald-500" />
|
||||||
|
<span>Skip</span>
|
||||||
|
<span className="text-[10px] text-muted-foreground">(Default)</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="lite">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ClipboardList className="h-3.5 w-3.5 text-blue-500" />
|
||||||
|
<span>Lite Planning</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="spec">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FileText className="h-3.5 w-3.5 text-purple-500" />
|
||||||
|
<span>Spec (Lite SDD)</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="full">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ScrollText className="h-3.5 w-3.5 text-amber-500" />
|
||||||
|
<span>Full (SDD)</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground/80 leading-relaxed">
|
||||||
|
{defaultPlanningMode === 'skip' && "Jump straight to implementation without upfront planning."}
|
||||||
|
{defaultPlanningMode === 'lite' && "Create a quick planning outline with tasks before building."}
|
||||||
|
{defaultPlanningMode === 'spec' && "Generate a specification with acceptance criteria for approval."}
|
||||||
|
{defaultPlanningMode === 'full' && "Create comprehensive spec with phased implementation plan."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Separator */}
|
||||||
|
<div className="border-t border-border/30" />
|
||||||
|
|
||||||
{/* Profiles Only Setting */}
|
{/* Profiles Only Setting */}
|
||||||
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
<div className="group flex items-start space-x-3 p-3 rounded-xl hover:bg-accent/30 transition-colors duration-200 -mx-3">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -260,6 +260,9 @@ export type ModelProvider = "claude";
|
|||||||
// Thinking level (budget_tokens) options
|
// Thinking level (budget_tokens) options
|
||||||
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
||||||
|
|
||||||
|
// Planning mode for feature specifications
|
||||||
|
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
// AI Provider Profile - user-defined presets for model configurations
|
// AI Provider Profile - user-defined presets for model configurations
|
||||||
export interface AIProfile {
|
export interface AIProfile {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -297,6 +300,20 @@ export interface Feature {
|
|||||||
worktreePath?: string; // Path to the worktree directory
|
worktreePath?: string; // Path to the worktree directory
|
||||||
branchName?: string; // Name of the feature branch
|
branchName?: string; // Name of the feature branch
|
||||||
justFinishedAt?: string; // ISO timestamp when agent just finished and moved to waiting_approval (shows badge for 2 minutes)
|
justFinishedAt?: string; // ISO timestamp when agent just finished and moved to waiting_approval (shows badge for 2 minutes)
|
||||||
|
planningMode?: PlanningMode; // Planning mode for this feature
|
||||||
|
planSpec?: PlanSpec; // Generated spec/plan data
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlanSpec status for feature planning/specification
|
||||||
|
export interface PlanSpec {
|
||||||
|
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||||
|
content?: string; // The actual spec/plan markdown content
|
||||||
|
version: number;
|
||||||
|
generatedAt?: string; // ISO timestamp
|
||||||
|
approvedAt?: string; // ISO timestamp
|
||||||
|
reviewedByUser: boolean; // True if user has seen the spec
|
||||||
|
tasksCompleted?: number;
|
||||||
|
tasksTotal?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File tree node for project analysis
|
// File tree node for project analysis
|
||||||
@@ -442,6 +459,8 @@ export interface AppState {
|
|||||||
// Spec Creation State (per-project, keyed by project path)
|
// Spec Creation State (per-project, keyed by project path)
|
||||||
// Tracks which project is currently having its spec generated
|
// Tracks which project is currently having its spec generated
|
||||||
specCreatingForProject: string | null;
|
specCreatingForProject: string | null;
|
||||||
|
|
||||||
|
defaultPlanningMode: PlanningMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default background settings for board backgrounds
|
// Default background settings for board backgrounds
|
||||||
@@ -651,6 +670,8 @@ export interface AppActions {
|
|||||||
setSpecCreatingForProject: (projectPath: string | null) => void;
|
setSpecCreatingForProject: (projectPath: string | null) => void;
|
||||||
isSpecCreatingForProject: (projectPath: string) => boolean;
|
isSpecCreatingForProject: (projectPath: string) => boolean;
|
||||||
|
|
||||||
|
setDefaultPlanningMode: (mode: PlanningMode) => void;
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
@@ -736,6 +757,7 @@ const initialState: AppState = {
|
|||||||
defaultFontSize: 14,
|
defaultFontSize: 14,
|
||||||
},
|
},
|
||||||
specCreatingForProject: null,
|
specCreatingForProject: null,
|
||||||
|
defaultPlanningMode: 'skip' as PlanningMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAppStore = create<AppState & AppActions>()(
|
export const useAppStore = create<AppState & AppActions>()(
|
||||||
@@ -2115,6 +2137,8 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
return get().specCreatingForProject === projectPath;
|
return get().specCreatingForProject === projectPath;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setDefaultPlanningMode: (mode) => set({ defaultPlanningMode: mode }),
|
||||||
|
|
||||||
// Reset
|
// Reset
|
||||||
reset: () => set(initialState),
|
reset: () => set(initialState),
|
||||||
}),
|
}),
|
||||||
@@ -2180,6 +2204,7 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
lastSelectedSessionByProject: state.lastSelectedSessionByProject,
|
||||||
// Board background settings
|
// Board background settings
|
||||||
boardBackgroundByProject: state.boardBackgroundByProject,
|
boardBackgroundByProject: state.boardBackgroundByProject,
|
||||||
|
defaultPlanningMode: state.defaultPlanningMode,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,6 +23,73 @@ import { isAbortError, classifyError } from "../lib/error-handler.js";
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
|
interface PlanSpec {
|
||||||
|
status: 'pending' | 'generating' | 'generated' | 'approved' | 'rejected';
|
||||||
|
content?: string;
|
||||||
|
version: number;
|
||||||
|
generatedAt?: string;
|
||||||
|
approvedAt?: string;
|
||||||
|
reviewedByUser: boolean;
|
||||||
|
tasksCompleted?: number;
|
||||||
|
tasksTotal?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLANNING_PROMPTS = {
|
||||||
|
lite: `## Planning Phase (Lite Mode)
|
||||||
|
|
||||||
|
Before implementing, create a brief planning outline:
|
||||||
|
|
||||||
|
1. **Goal**: What are we accomplishing? (1 sentence)
|
||||||
|
2. **Approach**: How will we do it? (2-3 sentences)
|
||||||
|
3. **Files to Touch**: List files and what changes
|
||||||
|
4. **Tasks**: Numbered task list (3-7 items)
|
||||||
|
5. **Risks**: Any gotchas to watch for
|
||||||
|
|
||||||
|
Present this outline, then proceed with implementation.`,
|
||||||
|
|
||||||
|
spec: `## Specification Phase (Spec Mode)
|
||||||
|
|
||||||
|
Before implementing, generate a specification and WAIT for approval:
|
||||||
|
|
||||||
|
1. **Problem**: What problem are we solving? (user perspective)
|
||||||
|
2. **Solution**: Brief approach (1 sentence)
|
||||||
|
3. **Acceptance Criteria**: 3-5 items in GIVEN-WHEN-THEN format
|
||||||
|
4. **Files to Modify**: Table with File, Purpose, Action
|
||||||
|
5. **Tasks**: Numbered implementation tasks
|
||||||
|
|
||||||
|
After generating the spec, output:
|
||||||
|
"[SPEC_GENERATED] Please review the specification above. Reply with 'approved' to proceed or provide feedback for revisions."
|
||||||
|
|
||||||
|
DO NOT proceed with implementation until you receive explicit approval.`,
|
||||||
|
|
||||||
|
full: `## Full Specification Phase (Full SDD Mode)
|
||||||
|
|
||||||
|
Before implementing, generate a comprehensive specification and WAIT for approval:
|
||||||
|
|
||||||
|
1. **Problem Statement**: 2-3 sentences, user perspective
|
||||||
|
2. **User Story**: As a [user], I want [goal], so that [benefit]
|
||||||
|
3. **Acceptance Criteria**: Multiple scenarios with GIVEN-WHEN-THEN
|
||||||
|
- Happy path scenario
|
||||||
|
- Edge case scenarios
|
||||||
|
- Error handling scenarios
|
||||||
|
4. **Technical Context**:
|
||||||
|
- Files to modify (table format)
|
||||||
|
- Dependencies
|
||||||
|
- Constraints
|
||||||
|
- Existing patterns to follow
|
||||||
|
5. **Non-Goals**: What this feature explicitly does NOT include
|
||||||
|
6. **Implementation Plan**: Phased tasks (Phase 1: Foundation, Phase 2: Core, etc.)
|
||||||
|
7. **Success Metrics**: How we know it's done
|
||||||
|
8. **Risks & Mitigations**: Table of risks
|
||||||
|
|
||||||
|
After generating, output:
|
||||||
|
"[SPEC_GENERATED] Please review the comprehensive specification above. Reply with 'approved' to proceed or provide feedback for revisions."
|
||||||
|
|
||||||
|
DO NOT proceed with implementation until you receive explicit approval.`
|
||||||
|
};
|
||||||
|
|
||||||
interface Feature {
|
interface Feature {
|
||||||
id: string;
|
id: string;
|
||||||
category: string;
|
category: string;
|
||||||
@@ -41,6 +108,8 @@ interface Feature {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
planningMode?: PlanningMode;
|
||||||
|
planSpec?: PlanSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RunningFeature {
|
interface RunningFeature {
|
||||||
@@ -235,8 +304,19 @@ export class AutoModeService {
|
|||||||
// Update feature status to in_progress
|
// Update feature status to in_progress
|
||||||
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
await this.updateFeatureStatus(projectPath, featureId, "in_progress");
|
||||||
|
|
||||||
// Build the prompt
|
// Build the prompt with planning phase if needed
|
||||||
const prompt = this.buildFeaturePrompt(feature);
|
const featurePrompt = this.buildFeaturePrompt(feature);
|
||||||
|
const planningPrefix = this.getPlanningPromptPrefix(feature);
|
||||||
|
const prompt = planningPrefix + featurePrompt;
|
||||||
|
|
||||||
|
// Emit planning mode info
|
||||||
|
if (feature.planningMode && feature.planningMode !== 'skip') {
|
||||||
|
this.emitAutoModeEvent('planning_started', {
|
||||||
|
featureId: feature.id,
|
||||||
|
mode: feature.planningMode,
|
||||||
|
message: `Starting ${feature.planningMode} planning phase`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Extract image paths from feature
|
// Extract image paths from feature
|
||||||
const imagePaths = feature.imagePaths?.map((img) =>
|
const imagePaths = feature.imagePaths?.map((img) =>
|
||||||
@@ -993,6 +1073,24 @@ Format your response as a structured markdown document.`;
|
|||||||
return firstLine.substring(0, 57) + "...";
|
return firstLine.substring(0, 57) + "...";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the planning prompt prefix based on feature's planning mode
|
||||||
|
*/
|
||||||
|
private getPlanningPromptPrefix(feature: Feature): string {
|
||||||
|
const mode = feature.planningMode || 'skip';
|
||||||
|
|
||||||
|
if (mode === 'skip') {
|
||||||
|
return ''; // No planning phase
|
||||||
|
}
|
||||||
|
|
||||||
|
const planningPrompt = PLANNING_PROMPTS[mode];
|
||||||
|
if (!planningPrompt) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return planningPrompt + '\n\n---\n\n## Feature Request\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract image paths from feature's imagePaths array
|
* Extract image paths from feature's imagePaths array
|
||||||
* Handles both string paths and objects with path property
|
* Handles both string paths and objects with path property
|
||||||
|
|||||||
1246
package-lock.json
generated
1246
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user