mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
IDK
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import { GeistSans } from "geist/font/sans";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
import { Inter, JetBrains_Mono } from "next/font/google";
|
||||
import { Toaster } from "sonner";
|
||||
import "./globals.css";
|
||||
|
||||
// Inter font for clean theme
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// JetBrains Mono for clean theme
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-jetbrains-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Automaker - Autonomous AI Development Studio",
|
||||
description: "Build software autonomously with intelligent orchestration",
|
||||
@@ -16,7 +32,7 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} antialiased`}
|
||||
className={`${GeistSans.variable} ${GeistMono.variable} ${inter.variable} ${jetbrainsMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<Toaster richColors position="bottom-right" />
|
||||
|
||||
@@ -150,6 +150,7 @@ function HomeContent() {
|
||||
"cream",
|
||||
"sunset",
|
||||
"gray",
|
||||
"clean",
|
||||
];
|
||||
|
||||
// Remove all theme classes
|
||||
|
||||
@@ -1217,6 +1217,8 @@ export function Sidebar() {
|
||||
<aside
|
||||
className={cn(
|
||||
"flex-shrink-0 flex flex-col z-30 relative",
|
||||
// Clean theme sidebar-glass class
|
||||
"sidebar-glass",
|
||||
// Glass morphism background with gradient
|
||||
"bg-gradient-to-b from-sidebar/95 via-sidebar/85 to-sidebar/90 backdrop-blur-2xl",
|
||||
// Premium border with subtle glow
|
||||
@@ -1854,6 +1856,8 @@ export function Sidebar() {
|
||||
isActive
|
||||
? [
|
||||
// Active: Premium gradient with glow
|
||||
// Clean theme nav-active class
|
||||
"nav-active",
|
||||
"bg-gradient-to-r from-brand-500/20 via-brand-500/15 to-brand-600/10",
|
||||
"text-foreground font-medium",
|
||||
"border border-brand-500/30",
|
||||
@@ -1894,6 +1898,8 @@ export function Sidebar() {
|
||||
{item.shortcut && sidebarOpen && (
|
||||
<span
|
||||
className={cn(
|
||||
// Clean theme shortcut-badge class
|
||||
"shortcut-badge",
|
||||
"hidden lg:flex items-center justify-center min-w-5 h-5 px-1.5 text-[10px] font-mono rounded-md transition-all duration-200",
|
||||
isActive
|
||||
? "bg-brand-500/20 text-brand-400"
|
||||
@@ -1919,7 +1925,7 @@ export function Sidebar() {
|
||||
>
|
||||
{item.label}
|
||||
{item.shortcut && (
|
||||
<span className="ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
|
||||
<span className="shortcut-badge ml-2 px-1.5 py-0.5 bg-muted rounded text-[10px] font-mono text-muted-foreground">
|
||||
{formatShortcut(item.shortcut, true)}
|
||||
</span>
|
||||
)}
|
||||
@@ -2053,6 +2059,8 @@ export function Sidebar() {
|
||||
{!sidebarOpen && runningAgentsCount > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
// Clean theme running-agents-badge class
|
||||
"running-agents-badge",
|
||||
"absolute -top-1.5 -right-1.5 flex items-center justify-center",
|
||||
"min-w-4 h-4 px-1 text-[9px] font-bold rounded-full",
|
||||
"bg-brand-500 text-white shadow-sm",
|
||||
@@ -2076,6 +2084,8 @@ export function Sidebar() {
|
||||
{sidebarOpen && runningAgentsCount > 0 && (
|
||||
<span
|
||||
className={cn(
|
||||
// Clean theme running-agents-badge class
|
||||
"running-agents-badge",
|
||||
"hidden lg:flex items-center justify-center",
|
||||
"min-w-6 h-6 px-1.5 text-xs font-semibold rounded-full",
|
||||
"bg-brand-500 text-white shadow-sm",
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@dnd-kit/core";
|
||||
import { useAppStore, Feature } from "@/store/app-store";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
import { pathsEqual } from "@/lib/utils";
|
||||
import { pathsEqual, cn } from "@/lib/utils";
|
||||
import { BoardBackgroundModal } from "@/components/dialogs/board-background-modal";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { useAutoMode } from "@/hooks/use-auto-mode";
|
||||
@@ -76,6 +76,7 @@ export function BoardView() {
|
||||
setCurrentWorktree,
|
||||
getWorktrees,
|
||||
setWorktrees,
|
||||
getEffectiveTheme,
|
||||
} = useAppStore();
|
||||
const shortcuts = useKeyboardShortcutsConfig();
|
||||
const {
|
||||
@@ -521,6 +522,9 @@ export function BoardView() {
|
||||
[currentProject, setPendingPlanApproval]
|
||||
);
|
||||
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
if (!currentProject) {
|
||||
return (
|
||||
<div
|
||||
@@ -597,7 +601,7 @@ export function BoardView() {
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Search Bar Row */}
|
||||
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
||||
<div className={cn("flex items-center justify-between shrink-0", isCleanTheme ? "px-8 py-4" : "px-4 pt-4 pb-2")}>
|
||||
<BoardSearchBar
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { ImageIcon, Archive, Minimize2, Square, Maximize2 } from "lucide-react";
|
||||
import { ImageIcon, Archive, Minimize2, Square, Maximize2, History, Trash2, Layout } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
interface BoardControlsProps {
|
||||
isMounted: boolean;
|
||||
@@ -22,8 +23,41 @@ export function BoardControls({
|
||||
kanbanCardDetailLevel,
|
||||
onDetailLevelChange,
|
||||
}: BoardControlsProps) {
|
||||
const { getEffectiveTheme } = useAppStore();
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
if (isCleanTheme) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 ml-6">
|
||||
<button
|
||||
className="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition"
|
||||
onClick={onShowCompletedModal}
|
||||
>
|
||||
<History className="w-[18px] h-[18px]" />
|
||||
</button>
|
||||
<button className="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition">
|
||||
<Trash2 className="w-[18px] h-[18px]" />
|
||||
</button>
|
||||
<div className="w-px h-6 bg-white/10 mx-1"></div>
|
||||
<button
|
||||
className="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition"
|
||||
onClick={onShowBoardBackground}
|
||||
>
|
||||
<Maximize2 className="w-[18px] h-[18px]" />
|
||||
</button>
|
||||
<button
|
||||
className="p-2.5 glass rounded-xl text-slate-500 hover:text-white transition"
|
||||
onClick={() => onDetailLevelChange(kanbanCardDetailLevel === 'minimal' ? 'standard' : kanbanCardDetailLevel === 'standard' ? 'detailed' : 'minimal')}
|
||||
>
|
||||
<Layout className="w-[18px] h-[18px]" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-2 ml-4">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Play, StopCircle, Plus, Users } from "lucide-react";
|
||||
import { KeyboardShortcut } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
interface BoardHeaderProps {
|
||||
projectName: string;
|
||||
@@ -29,6 +30,59 @@ export function BoardHeader({
|
||||
addFeatureShortcut,
|
||||
isMounted,
|
||||
}: BoardHeaderProps) {
|
||||
const { getEffectiveTheme } = useAppStore();
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
if (isCleanTheme) {
|
||||
return (
|
||||
<header className="h-16 flex items-center justify-between px-8 border-b border-white/5 bg-[#0b101a]/40 backdrop-blur-md z-20 shrink-0">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white tracking-tight">Kanban Board</h2>
|
||||
<p className="text-[10px] text-slate-500 uppercase tracking-[0.2em] font-bold mono">
|
||||
{projectName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
{/* Concurrency Display (Visual only to match mockup for now, or interactive if needed) */}
|
||||
<div className="flex items-center bg-white/5 border border-white/10 rounded-full px-4 py-1.5 gap-3">
|
||||
<Users className="w-4 h-4 text-slate-500" />
|
||||
<div className="toggle-track">
|
||||
<div className="toggle-thumb"></div>
|
||||
</div>
|
||||
<span className="mono text-xs font-bold text-slate-400">{maxConcurrency}</span>
|
||||
</div>
|
||||
|
||||
{/* Auto Mode Button */}
|
||||
{isAutoModeRunning ? (
|
||||
<button
|
||||
className="flex items-center gap-2 glass px-5 py-2 rounded-xl text-xs font-bold hover:bg-white/10 transition text-rose-400 border-rose-500/30"
|
||||
onClick={onStopAutoMode}
|
||||
>
|
||||
<StopCircle className="w-3.5 h-3.5" /> Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="flex items-center gap-2 glass px-5 py-2 rounded-xl text-xs font-bold hover:bg-white/10 transition"
|
||||
onClick={onStartAutoMode}
|
||||
>
|
||||
<Play className="w-3.5 h-3.5 text-cyan-400 fill-cyan-400" /> Auto Mode
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Add Feature Button */}
|
||||
<button
|
||||
className="btn-cyan px-6 py-2 rounded-xl text-xs font-black flex items-center gap-2 shadow-lg shadow-cyan-500/20"
|
||||
onClick={onAddFeature}
|
||||
>
|
||||
<Plus className="w-4 h-4 stroke-[3.5px]" /> ADD FEATURE
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between p-4 border-b border-border bg-glass backdrop-blur-md">
|
||||
<div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Search, X, Loader2 } from "lucide-react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
interface BoardSearchBarProps {
|
||||
searchQuery: string;
|
||||
@@ -20,6 +21,9 @@ export function BoardSearchBar({
|
||||
currentProjectPath,
|
||||
}: BoardSearchBarProps) {
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const { getEffectiveTheme } = useAppStore();
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
// Focus search input when "/" is pressed
|
||||
useEffect(() => {
|
||||
@@ -39,6 +43,25 @@ export function BoardSearchBar({
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
if (isCleanTheme) {
|
||||
return (
|
||||
<div className="relative flex-1 max-w-2xl group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-cyan-400 transition-colors" />
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type="text"
|
||||
placeholder="Search features by keyword..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="w-full bg-white/5 border border-white/10 rounded-2xl py-2.5 pl-12 pr-12 text-sm focus:outline-none focus:border-cyan-500/50 transition-all mono"
|
||||
/>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2">
|
||||
<span className="shortcut-badge">/</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative max-w-md flex-1 flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
|
||||
@@ -58,6 +58,12 @@ import {
|
||||
Wand2,
|
||||
Archive,
|
||||
Lock,
|
||||
Target,
|
||||
Square,
|
||||
Terminal,
|
||||
RefreshCw,
|
||||
Layers,
|
||||
Edit3,
|
||||
} from "lucide-react";
|
||||
import { CountUpTimer } from "@/components/ui/count-up-timer";
|
||||
import { getElectronAPI } from "@/lib/electron";
|
||||
@@ -149,7 +155,9 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(() => Date.now());
|
||||
const { kanbanCardDetailLevel, enableDependencyBlocking, features, useWorktrees } = useAppStore();
|
||||
const { kanbanCardDetailLevel, enableDependencyBlocking, features, useWorktrees, getEffectiveTheme } = useAppStore();
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
// Calculate blocking dependencies (if feature is in backlog and has incomplete dependencies)
|
||||
const blockingDependencies = useMemo(() => {
|
||||
@@ -160,9 +168,9 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
}, [enableDependencyBlocking, feature, features]);
|
||||
|
||||
const showSteps =
|
||||
kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed";
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed";
|
||||
(kanbanCardDetailLevel === "standard" ||
|
||||
kanbanCardDetailLevel === "detailed") && !isCleanTheme; // Hide steps in clean theme
|
||||
const showAgentInfo = kanbanCardDetailLevel === "detailed" || isCleanTheme; // Always show model info in clean theme
|
||||
|
||||
const isJustFinished = useMemo(() => {
|
||||
if (
|
||||
@@ -291,17 +299,261 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
).borderColor = `color-mix(in oklch, var(--border) ${cardBorderOpacity}%, transparent)`;
|
||||
}
|
||||
|
||||
// CLEAN THEME IMPLEMENTATION
|
||||
if (isCleanTheme) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className={cn(
|
||||
"glass kanban-card flex flex-col gap-4 group relative",
|
||||
// Verified state
|
||||
feature.status === "verified" && "opacity-60 hover:opacity-100 transition-all",
|
||||
// Running card state
|
||||
isCurrentAutoTask && "border-cyan-500/40 bg-cyan-500/[0.08]",
|
||||
// Dragging state
|
||||
isDragging && "scale-105 shadow-xl shadow-black/20 opacity-50 z-50",
|
||||
!isDraggable && "cursor-default"
|
||||
)}
|
||||
onDoubleClick={onEdit}
|
||||
{...attributes}
|
||||
{...(isDraggable ? listeners : {})}
|
||||
>
|
||||
{/* Action Icons - Waiting/Verified (In Flow, First Child) */}
|
||||
{(feature.status === "waiting_approval" || feature.status === "verified") && (
|
||||
<div className={cn(
|
||||
"flex justify-end gap-3.5 transition-opacity",
|
||||
feature.status === "waiting_approval" ? "opacity-30 group-hover:opacity-100" : "opacity-20"
|
||||
)}>
|
||||
<Edit3
|
||||
className="w-4 h-4 cursor-pointer hover:text-white transition"
|
||||
onClick={(e) => { e.stopPropagation(); onEdit(); }}
|
||||
/>
|
||||
<Trash2
|
||||
className="w-4 h-4 cursor-pointer hover:text-rose-400 transition"
|
||||
onClick={(e) => { e.stopPropagation(); handleDeleteClick(e); }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Bar - Running State */}
|
||||
{isCurrentAutoTask && (
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
<div className="bg-orange-500/15 text-orange-400 text-[9px] px-2.5 py-1 rounded-lg border border-orange-500/20 flex items-center gap-1.5 font-black mono">
|
||||
<RefreshCw className="w-3 h-3" /> {formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||
</div>
|
||||
<div className="bg-slate-900/50 text-slate-500 text-[9px] px-2 py-1 rounded-lg border border-white/5 font-mono">
|
||||
{feature.startedAt ? (
|
||||
<CountUpTimer
|
||||
startedAt={feature.startedAt}
|
||||
className="text-inherit"
|
||||
/>
|
||||
) : "00:00"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top Bar - In Progress (Inactive) State */}
|
||||
{!isCurrentAutoTask && feature.status === "in_progress" && (
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="bg-orange-500/10 text-orange-400 text-[9px] px-2.5 py-1 rounded-lg border border-orange-500/10 flex items-center gap-1.5 font-bold mono">
|
||||
<RefreshCw className="w-3 h-3" /> {formatModelName(feature.model ?? DEFAULT_MODEL)}
|
||||
</div>
|
||||
{/* Duration if available - mocked for now as not in Feature type */}
|
||||
<div className="bg-slate-900/50 text-slate-500 text-[9px] px-2 py-1 rounded-lg border border-white/5 font-mono">
|
||||
00:07
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Icon - Top Right for Backlog (Absolute) */}
|
||||
{feature.status === "backlog" && (
|
||||
<div className="absolute top-5 right-6 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<Trash2
|
||||
className="w-4 h-4 text-slate-600 hover:text-red-400 cursor-pointer"
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
<p className={cn(
|
||||
"text-[13px] leading-relaxed font-medium line-clamp-3",
|
||||
isCurrentAutoTask ? "text-white font-semibold" : "text-slate-300",
|
||||
feature.status === "waiting_approval" && "italic",
|
||||
feature.status === "verified" && "line-through decoration-slate-600"
|
||||
)}>
|
||||
{feature.description || feature.summary || "No description"}
|
||||
</p>
|
||||
|
||||
{/* More link */}
|
||||
{(feature.description || "").length > 100 && (
|
||||
<div className="flex items-center gap-1 text-[10px] text-slate-500 -mt-1 cursor-pointer hover:text-slate-300">
|
||||
<ChevronDown className="w-3 h-3" /> More
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Backlog Info */}
|
||||
{feature.status === "backlog" && (
|
||||
<div className="text-[10px] font-bold text-cyan-400/80 mono flex items-center gap-1.5 uppercase tracking-tight">
|
||||
<Layers className="w-3.5 h-3.5" /> {feature.category || "General"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex gap-2 mt-auto">
|
||||
{/* Backlog Buttons */}
|
||||
{feature.status === "backlog" && (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onEdit(); }}
|
||||
className="flex-1 glass py-2.5 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:bg-white/10 transition"
|
||||
>
|
||||
<Edit3 className="w-3.5 h-3.5" /> Edit
|
||||
</button>
|
||||
{onImplement && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onImplement(); }}
|
||||
className="flex-1 bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/20 py-2.5 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 transition"
|
||||
>
|
||||
<Target className="w-3.5 h-3.5" /> Make
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* In Progress Buttons */}
|
||||
{feature.status === "in_progress" && (
|
||||
<>
|
||||
{onViewOutput && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onViewOutput(); }}
|
||||
className={cn(
|
||||
"flex-[4] py-3 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2",
|
||||
isCurrentAutoTask ? "btn-cyan font-black tracking-widest" : "bg-cyan-500/15 text-cyan-400 border border-cyan-500/20"
|
||||
)}
|
||||
>
|
||||
<Terminal className={cn("w-4 h-4", isCurrentAutoTask && "stroke-[2.5px]")} /> LOGS
|
||||
{agentInfo?.toolCallCount ? (
|
||||
<span className={cn("px-1.5 rounded ml-1", isCurrentAutoTask ? "bg-black/10" : "bg-cyan-500/10")}>{agentInfo.toolCallCount}</span>
|
||||
) : null}
|
||||
</button>
|
||||
)}
|
||||
{onForceStop && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onForceStop(); }}
|
||||
className={cn(
|
||||
"flex-1 rounded-xl flex items-center justify-center transition",
|
||||
isCurrentAutoTask ? "bg-rose-500 hover:bg-rose-600 text-white shadow-lg shadow-rose-500/20" : "bg-rose-500/20 text-rose-500/50 border border-rose-500/20"
|
||||
)}
|
||||
>
|
||||
<Square className="w-4 h-4 fill-current" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Waiting Buttons */}
|
||||
{feature.status === "waiting_approval" && (
|
||||
<>
|
||||
{onFollowUp && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFollowUp(); }}
|
||||
className="flex-1 glass border-white/10 py-3 rounded-xl text-[11px] font-bold flex items-center justify-center gap-2 hover:bg-white/10 transition"
|
||||
>
|
||||
<Wand2 className="w-4 h-4" /> Refine
|
||||
</button>
|
||||
)}
|
||||
{onCommit && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onCommit(); }}
|
||||
className="flex-1 btn-cyan py-3 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 tracking-widest"
|
||||
>
|
||||
<GitCommit className="w-4 h-4 stroke-[2.5px]" /> COMMIT
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Verified Buttons */}
|
||||
{feature.status === "verified" && (
|
||||
<>
|
||||
{onViewOutput && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onViewOutput(); }}
|
||||
className="px-7 glass border-white/10 py-3 rounded-xl text-[11px] font-bold text-slate-500 hover:text-slate-300 transition"
|
||||
>
|
||||
Logs
|
||||
</button>
|
||||
)}
|
||||
{onComplete && (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onComplete(); }}
|
||||
className="flex-1 bg-emerald-500/15 text-emerald-400 border border-emerald-500/20 py-3 rounded-xl text-[11px] font-black flex items-center justify-center gap-2 tracking-widest"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4 stroke-[2.5px]" /> COMPLETE
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<DeleteConfirmDialog
|
||||
open={isDeleteDialogOpen}
|
||||
onOpenChange={setIsDeleteDialogOpen}
|
||||
onConfirm={handleConfirmDelete}
|
||||
title="Delete Feature"
|
||||
description="Are you sure you want to delete this feature? This action cannot be undone."
|
||||
testId="delete-confirmation-dialog"
|
||||
confirmTestId="confirm-delete-button"
|
||||
/>
|
||||
|
||||
{/* Summary Modal - Reusing existing logic */}
|
||||
<Dialog open={isSummaryDialogOpen} onOpenChange={setIsSummaryDialogOpen}>
|
||||
<DialogContent
|
||||
className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col"
|
||||
data-testid={`summary-dialog-${feature.id}`}
|
||||
>
|
||||
{/* ... Existing dialog content ... */}
|
||||
<DialogHeader>
|
||||
<DialogTitle>Summary</DialogTitle>
|
||||
<DialogDescription>{feature.summary}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-card rounded-lg border border-border/50">
|
||||
<Markdown>
|
||||
{feature.summary ||
|
||||
summary ||
|
||||
agentInfo?.summary ||
|
||||
"No summary available"}
|
||||
</Markdown>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setIsSummaryDialogOpen(false)}>Close</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const cardElement = (
|
||||
<Card
|
||||
ref={setNodeRef}
|
||||
style={isCurrentAutoTask ? style : borderStyle}
|
||||
className={cn(
|
||||
// Clean theme kanban-card class
|
||||
"kanban-card",
|
||||
"cursor-grab active:cursor-grabbing relative kanban-card-content select-none",
|
||||
"transition-all duration-200 ease-out",
|
||||
// Premium shadow system
|
||||
"shadow-sm hover:shadow-md hover:shadow-black/10",
|
||||
// Subtle lift on hover
|
||||
"hover:-translate-y-0.5",
|
||||
// Running card state for clean theme
|
||||
isCurrentAutoTask && "is-running kanban-card-active",
|
||||
!isCurrentAutoTask &&
|
||||
cardBorderEnabled &&
|
||||
cardBorderOpacity === 100 &&
|
||||
@@ -725,7 +977,10 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
|
||||
{/* Model/Preset Info for Backlog Cards */}
|
||||
{showAgentInfo && feature.status === "backlog" && (
|
||||
<div className="mb-3 space-y-2 overflow-hidden">
|
||||
<div
|
||||
className="mb-3 space-y-2 overflow-hidden"
|
||||
style={isCleanTheme ? { order: 1 } : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-2 text-[11px] flex-wrap">
|
||||
<div className="flex items-center gap-1 text-[var(--status-info)]">
|
||||
<Cpu className="w-3 h-3" />
|
||||
@@ -747,7 +1002,10 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
|
||||
{/* Agent Info Panel */}
|
||||
{showAgentInfo && feature.status !== "backlog" && agentInfo && (
|
||||
<div className="mb-3 space-y-2 overflow-hidden">
|
||||
<div
|
||||
className="mb-3 space-y-2 overflow-hidden"
|
||||
style={isCleanTheme ? { order: 1 } : undefined}
|
||||
>
|
||||
{/* Model & Phase */}
|
||||
<div className="flex items-center gap-2 text-[11px] flex-wrap">
|
||||
<div className="flex items-center gap-1 text-[var(--status-info)]">
|
||||
@@ -880,7 +1138,10 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<div
|
||||
className="flex flex-wrap gap-1.5"
|
||||
style={isCleanTheme ? { order: 2 } : undefined}
|
||||
>
|
||||
{isCurrentAutoTask && (
|
||||
<>
|
||||
{/* Approve Plan button - PRIORITY: shows even when agent is "running" (paused for approval) */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { memo } from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactNode } from "react";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
|
||||
interface KanbanColumnProps {
|
||||
id: string;
|
||||
@@ -29,16 +30,127 @@ export const KanbanColumn = memo(function KanbanColumn({
|
||||
hideScrollbar = false,
|
||||
}: KanbanColumnProps) {
|
||||
const { setNodeRef, isOver } = useDroppable({ id });
|
||||
const { getEffectiveTheme } = useAppStore();
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
// Map column IDs to clean theme classes
|
||||
const getColumnClasses = () => {
|
||||
switch (id) {
|
||||
case "in_progress":
|
||||
return "col-in-progress";
|
||||
case "waiting_approval":
|
||||
return "col-waiting";
|
||||
case "verified":
|
||||
return "col-verified";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Map column IDs to status dot glow classes
|
||||
const getStatusDotClasses = () => {
|
||||
switch (id) {
|
||||
case "in_progress":
|
||||
return "status-dot-in-progress glow-cyan";
|
||||
case "waiting_approval":
|
||||
return "status-dot-waiting glow-orange";
|
||||
case "verified":
|
||||
return "status-dot-verified glow-green";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
// Clean theme column styles
|
||||
if (isCleanTheme) {
|
||||
const isBacklog = id === "backlog";
|
||||
|
||||
// Explicitly match mockup classes for status dots
|
||||
const getCleanStatusDotClass = () => {
|
||||
switch (id) {
|
||||
case "backlog":
|
||||
return "status-dot bg-slate-600";
|
||||
case "in_progress":
|
||||
return "status-dot bg-cyan-400 glow-cyan";
|
||||
case "waiting_approval":
|
||||
return "status-dot bg-orange-500 glow-orange";
|
||||
case "verified":
|
||||
return "status-dot bg-emerald-500 glow-green";
|
||||
default:
|
||||
return "status-dot bg-slate-600";
|
||||
}
|
||||
};
|
||||
|
||||
// Explicitly match mockup classes for badges
|
||||
const getBadgeClass = () => {
|
||||
switch (id) {
|
||||
case "in_progress":
|
||||
return "mono text-[10px] bg-cyan-500/10 px-2.5 py-0.5 rounded-full text-cyan-400 border border-cyan-500/20";
|
||||
case "verified":
|
||||
return "mono text-[10px] bg-emerald-500/10 px-2.5 py-0.5 rounded-full text-emerald-500 border border-emerald-500/20";
|
||||
case "backlog":
|
||||
case "waiting_approval":
|
||||
default:
|
||||
return "mono text-[10px] bg-white/5 px-2.5 py-0.5 rounded-full text-slate-500 border border-white/5";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"flex flex-col h-full w-80 gap-5",
|
||||
!isBacklog && "rounded-[2.5rem] p-3",
|
||||
getColumnClasses()
|
||||
)}
|
||||
data-testid={`kanban-column-${id}`}
|
||||
data-column-id={id}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-2 shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={getCleanStatusDotClass()} />
|
||||
<h3 className={cn(
|
||||
"text-[11px] font-black uppercase tracking-widest",
|
||||
id === "backlog" ? "text-slate-400" :
|
||||
id === "in_progress" ? "text-slate-200" : "text-slate-300"
|
||||
)}>
|
||||
{title}
|
||||
</h3>
|
||||
{headerAction}
|
||||
</div>
|
||||
|
||||
<span className={getBadgeClass()}>
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex-1 overflow-y-auto custom-scrollbar space-y-4",
|
||||
isBacklog ? "pr-2" : "pr-1",
|
||||
hideScrollbar && "scrollbar-hide"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={cn(
|
||||
"relative flex flex-col h-full rounded-xl transition-all duration-200 w-72",
|
||||
"relative flex flex-col h-full rounded-xl transition-all duration-200 w-72 clean:w-80",
|
||||
showBorder && "border border-border/60",
|
||||
isOver && "ring-2 ring-primary/30 ring-offset-1 ring-offset-background"
|
||||
isOver && "ring-2 ring-primary/30 ring-offset-1 ring-offset-background",
|
||||
getColumnClasses()
|
||||
)}
|
||||
data-testid={`kanban-column-${id}`}
|
||||
data-column-id={id}
|
||||
>
|
||||
{/* Background layer with opacity */}
|
||||
<div
|
||||
@@ -56,7 +168,7 @@ export const KanbanColumn = memo(function KanbanColumn({
|
||||
showBorder && "border-b border-border/40"
|
||||
)}
|
||||
>
|
||||
<div className={cn("w-2.5 h-2.5 rounded-full shrink-0", colorClass)} />
|
||||
<div className={cn("w-2.5 h-2.5 rounded-full shrink-0 status-dot", colorClass, getStatusDotClasses())} />
|
||||
<h3 className="font-semibold text-sm text-foreground/90 flex-1 tracking-tight">{title}</h3>
|
||||
{headerAction}
|
||||
<span className="text-xs font-medium text-muted-foreground/80 bg-muted/50 px-2 py-0.5 rounded-md tabular-nums">
|
||||
|
||||
@@ -13,10 +13,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { KanbanColumn, KanbanCard } from "./components";
|
||||
import { Feature } from "@/store/app-store";
|
||||
import { FastForward, Lightbulb, Trash2 } from "lucide-react";
|
||||
import { Feature, useAppStore } from "@/store/app-store";
|
||||
import { FastForward, Lightbulb, Trash2, GitBranch } from "lucide-react";
|
||||
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { COLUMNS, ColumnId } from "./constants";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface KanbanBoardProps {
|
||||
sensors: any;
|
||||
@@ -89,9 +90,16 @@ export function KanbanBoard({
|
||||
suggestionsCount,
|
||||
onDeleteAllVerified,
|
||||
}: KanbanBoardProps) {
|
||||
const { getEffectiveTheme } = useAppStore();
|
||||
const effectiveTheme = getEffectiveTheme();
|
||||
const isCleanTheme = effectiveTheme === "clean";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex-1 overflow-x-auto px-4 pb-4 relative"
|
||||
className={cn(
|
||||
"flex-1 overflow-x-auto relative",
|
||||
isCleanTheme ? "custom-scrollbar px-8 pb-8" : "px-4 pb-4"
|
||||
)}
|
||||
style={backgroundImageStyle}
|
||||
>
|
||||
<DndContext
|
||||
@@ -100,9 +108,91 @@ export function KanbanBoard({
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<div className="flex gap-5 h-full min-w-max py-1">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full min-w-max",
|
||||
isCleanTheme ? "gap-6 items-start" : "gap-5 py-1"
|
||||
)}
|
||||
>
|
||||
{COLUMNS.map((column) => {
|
||||
const columnFeatures = getColumnFeatures(column.id);
|
||||
|
||||
// Clean Theme Header Actions
|
||||
let headerAction;
|
||||
if (isCleanTheme) {
|
||||
if (column.id === "backlog") {
|
||||
headerAction = (
|
||||
<div className="flex items-center gap-1.5 opacity-40">
|
||||
<Lightbulb className="w-3.5 h-3.5 text-yellow-500" />
|
||||
<GitBranch className="w-3.5 h-3.5 text-cyan-400" />
|
||||
<span className="mono text-[9px] text-cyan-400 font-bold">Mabe 6</span>
|
||||
</div>
|
||||
);
|
||||
} else if (column.id === "verified" && columnFeatures.length > 0) {
|
||||
headerAction = (
|
||||
<button
|
||||
className="ml-2 text-[10px] text-rose-500 flex items-center gap-1 hover:underline font-black transition"
|
||||
onClick={onDeleteAllVerified}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" /> Delete All
|
||||
</button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Standard Theme Header Actions
|
||||
if (column.id === "verified" && columnFeatures.length > 0) {
|
||||
headerAction = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={onDeleteAllVerified}
|
||||
data-testid="delete-all-verified-button"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
Delete All
|
||||
</Button>
|
||||
);
|
||||
} else if (column.id === "backlog") {
|
||||
headerAction = (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10 relative"
|
||||
onClick={onShowSuggestions}
|
||||
title="Feature Suggestions"
|
||||
data-testid="feature-suggestions-button"
|
||||
>
|
||||
<Lightbulb className="w-3.5 h-3.5" />
|
||||
{suggestionsCount > 0 && (
|
||||
<span
|
||||
className="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-mono rounded-full bg-yellow-500 text-black flex items-center justify-center"
|
||||
data-testid="suggestions-count"
|
||||
>
|
||||
{suggestionsCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{columnFeatures.length > 0 && (
|
||||
<HotkeyButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
||||
onClick={onStartNextFeatures}
|
||||
hotkey={shortcuts.startNext}
|
||||
hotkeyActive={false}
|
||||
data-testid="start-next-button"
|
||||
>
|
||||
<FastForward className="w-3 h-3 mr-1" />
|
||||
Make
|
||||
</HotkeyButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<KanbanColumn
|
||||
key={column.id}
|
||||
@@ -113,56 +203,7 @@ export function KanbanBoard({
|
||||
opacity={backgroundSettings.columnOpacity}
|
||||
showBorder={backgroundSettings.columnBorderEnabled}
|
||||
hideScrollbar={backgroundSettings.hideScrollbar}
|
||||
headerAction={
|
||||
column.id === "verified" &&
|
||||
columnFeatures.length > 0 ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={onDeleteAllVerified}
|
||||
data-testid="delete-all-verified-button"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-1" />
|
||||
Delete All
|
||||
</Button>
|
||||
) : column.id === "backlog" ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0 text-yellow-500 hover:text-yellow-400 hover:bg-yellow-500/10 relative"
|
||||
onClick={onShowSuggestions}
|
||||
title="Feature Suggestions"
|
||||
data-testid="feature-suggestions-button"
|
||||
>
|
||||
<Lightbulb className="w-3.5 h-3.5" />
|
||||
{suggestionsCount > 0 && (
|
||||
<span
|
||||
className="absolute -top-1 -right-1 w-4 h-4 text-[9px] font-mono rounded-full bg-yellow-500 text-black flex items-center justify-center"
|
||||
data-testid="suggestions-count"
|
||||
>
|
||||
{suggestionsCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{columnFeatures.length > 0 && (
|
||||
<HotkeyButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 px-2 text-xs text-primary hover:text-primary hover:bg-primary/10"
|
||||
onClick={onStartNextFeatures}
|
||||
hotkey={shortcuts.startNext}
|
||||
hotkeyActive={false}
|
||||
data-testid="start-next-button"
|
||||
>
|
||||
<FastForward className="w-3 h-3 mr-1" />
|
||||
Make
|
||||
</HotkeyButton>
|
||||
)}
|
||||
</div>
|
||||
) : undefined
|
||||
}
|
||||
headerAction={headerAction}
|
||||
>
|
||||
<SortableContext
|
||||
items={columnFeatures.map((f) => f.id)}
|
||||
|
||||
@@ -33,7 +33,8 @@ export type Theme =
|
||||
| "red"
|
||||
| "cream"
|
||||
| "sunset"
|
||||
| "gray";
|
||||
| "gray"
|
||||
| "clean";
|
||||
|
||||
export type KanbanDetailLevel = "minimal" | "standard" | "detailed";
|
||||
|
||||
|
||||
@@ -431,6 +431,31 @@ const grayTheme: TerminalTheme = {
|
||||
brightWhite: "#e0e0e8",
|
||||
};
|
||||
|
||||
// Clean theme - Modern glassmorphism with cyan accents
|
||||
const cleanTheme: TerminalTheme = {
|
||||
background: "#0b101a",
|
||||
foreground: "#e2e8f0",
|
||||
cursor: "#22d3ee",
|
||||
cursorAccent: "#0b101a",
|
||||
selectionBackground: "#22d3ee33",
|
||||
black: "#0f1419",
|
||||
red: "#ef4444",
|
||||
green: "#10b981",
|
||||
yellow: "#f59e0b",
|
||||
blue: "#22d3ee",
|
||||
magenta: "#a78bfa",
|
||||
cyan: "#22d3ee",
|
||||
white: "#e2e8f0",
|
||||
brightBlack: "#4b5563",
|
||||
brightRed: "#f87171",
|
||||
brightGreen: "#34d399",
|
||||
brightYellow: "#fbbf24",
|
||||
brightBlue: "#67e8f9",
|
||||
brightMagenta: "#c4b5fd",
|
||||
brightCyan: "#67e8f9",
|
||||
brightWhite: "#f8fafc",
|
||||
};
|
||||
|
||||
// Theme mapping
|
||||
const terminalThemes: Record<ThemeMode, TerminalTheme> = {
|
||||
light: lightTheme,
|
||||
@@ -450,6 +475,7 @@ const terminalThemes: Record<ThemeMode, TerminalTheme> = {
|
||||
cream: creamTheme,
|
||||
sunset: sunsetTheme,
|
||||
gray: grayTheme,
|
||||
clean: cleanTheme,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Cat,
|
||||
CloudSun,
|
||||
Coffee,
|
||||
Droplets,
|
||||
Eclipse,
|
||||
Flame,
|
||||
Ghost,
|
||||
@@ -113,4 +114,10 @@ export const themeOptions: ReadonlyArray<ThemeOption> = [
|
||||
Icon: Square,
|
||||
testId: "gray-mode-button",
|
||||
},
|
||||
{
|
||||
value: "clean",
|
||||
label: "Clean",
|
||||
Icon: Droplets,
|
||||
testId: "clean-mode-button",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,7 +33,8 @@ export type ThemeMode =
|
||||
| "red"
|
||||
| "cream"
|
||||
| "sunset"
|
||||
| "gray";
|
||||
| "gray"
|
||||
| "clean";
|
||||
|
||||
export type KanbanCardDetailLevel = "minimal" | "standard" | "detailed";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user