diff --git a/apps/web/components/kanban-board-grid.tsx b/apps/web/components/kanban-board-grid.tsx index dc30d297bb..178542fee2 100644 --- a/apps/web/components/kanban-board-grid.tsx +++ b/apps/web/components/kanban-board-grid.tsx @@ -1,16 +1,24 @@ 'use client'; +import { useEffect, useMemo, useRef } from 'react'; import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, PointerSensor, + TouchSensor, useSensor, useSensors, } from '@dnd-kit/core'; import { KanbanColumn, WorkflowStep } from './kanban-column'; import { KanbanCardPreview, Task } from './kanban-card'; +import { MobileColumnTabs } from './kanban/mobile-column-tabs'; +import { SwipeableColumns } from './kanban/swipeable-columns'; +import { MobileDropTargets } from './kanban/mobile-drop-targets'; +import { MobileFab } from './kanban/mobile-fab'; +import { useResponsiveBreakpoint } from '@/hooks/use-responsive-breakpoint'; +import { useAppStore } from '@/components/state-provider'; export type KanbanBoardGridProps = { columns: WorkflowStep[]; @@ -19,12 +27,14 @@ export type KanbanBoardGridProps = { onOpenTask: (task: Task) => void; onEditTask: (task: Task) => void; onDeleteTask: (task: Task) => void; + onMoveTask?: (task: Task, targetColumnId: string) => void; onDragStart: (event: DragStartEvent) => void; onDragEnd: (event: DragEndEvent) => void; onDragCancel: () => void; activeTask: Task | null; showMaximizeButton?: boolean; deletingTaskId?: string | null; + onCreateTask?: () => void; }; export function KanbanBoardGrid({ @@ -34,18 +44,31 @@ export function KanbanBoardGrid({ onOpenTask, onEditTask, onDeleteTask, + onMoveTask, onDragStart, onDragEnd, onDragCancel, activeTask, showMaximizeButton, deletingTaskId, + onCreateTask, }: KanbanBoardGridProps) { + const { isMobile, isTablet } = useResponsiveBreakpoint(); + const activeColumnIndex = useAppStore((state) => state.mobileKanban.activeColumnIndex); + const setActiveColumnIndex = useAppStore((state) => state.setMobileKanbanColumnIndex); + + // Use TouchSensor with delay for mobile (long-press to drag) const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8, }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, }) ); @@ -56,6 +79,134 @@ export function KanbanBoardGrid({ .sort((a, b) => (a.position ?? 0) - (b.position ?? 0)); }; + // Calculate task counts per column for tabs + const taskCounts = useMemo(() => { + const counts: Record = {}; + for (const column of columns) { + counts[column.id] = tasks.filter((task) => task.workflowStepId === column.id).length; + } + return counts; + }, [columns, tasks]); + + // On mobile, select the first column with tasks on initial load + const hasInitializedRef = useRef(false); + useEffect(() => { + if (!isMobile || hasInitializedRef.current || columns.length === 0) return; + + const firstColumnWithTasks = columns.findIndex( + (column) => taskCounts[column.id] > 0 + ); + + if (firstColumnWithTasks !== -1 && firstColumnWithTasks !== activeColumnIndex) { + setActiveColumnIndex(firstColumnWithTasks); + } + hasInitializedRef.current = true; + }, [isMobile, columns, taskCounts, activeColumnIndex, setActiveColumnIndex]); + + // Get current column ID for mobile drop targets + const currentColumnId = columns[activeColumnIndex]?.id ?? null; + + const renderEmptyState = () => ( +
+ No boards available yet. +
+ ); + + // Mobile layout: Single column with swipeable tabs + if (isMobile) { + return ( + +
+ {columns.length === 0 ? ( + renderEmptyState() + ) : ( + <> + + + + + )} +
+ {onCreateTask && ( + + )} + + {activeTask ? : null} + +
+ ); + } + + // Tablet layout: Two columns with horizontal scroll + if (isTablet) { + return ( + +
+ {columns.length === 0 ? ( + renderEmptyState() + ) : ( +
+ {columns.map((column) => ( +
+ +
+ ))} +
+ )} +
+ + {activeTask ? : null} + +
+ ); + } + + // Desktop layout: Original grid return (
{columns.length === 0 ? ( -
- No boards available yet. -
+ renderEmptyState() ) : (
diff --git a/apps/web/components/kanban-board.tsx b/apps/web/components/kanban-board.tsx index 50b275d256..53453bab48 100644 --- a/apps/web/components/kanban-board.tsx +++ b/apps/web/components/kanban-board.tsx @@ -144,7 +144,7 @@ export function KanbanBoard({ onPreviewTask, onOpenTask }: KanbanBoardProps = {} ); // Drag and drop with workflow automation callback - const { activeTask, handleDragStart, handleDragEnd, handleDragCancel } = useDragAndDrop({ + const { activeTask, handleDragStart, handleDragEnd, handleDragCancel, moveTaskToColumn } = useDragAndDrop({ visibleTasks: visibleTasksWithSessions, onWorkflowAutomation: handleWorkflowAutomation, onMoveError: handleMoveError, @@ -289,12 +289,14 @@ export function KanbanBoard({ onPreviewTask, onOpenTask }: KanbanBoardProps = {} onOpenTask={handleOpenTask} onEditTask={handleEdit} onDeleteTask={handleDelete} + onMoveTask={moveTaskToColumn} onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragCancel={handleDragCancel} activeTask={activeTask} showMaximizeButton={enablePreviewOnClick} deletingTaskId={deletingTaskId} + onCreateTask={handleCreate} />
); diff --git a/apps/web/components/kanban-card.tsx b/apps/web/components/kanban-card.tsx index 6faf9b067e..accd8fe0e3 100644 --- a/apps/web/components/kanban-card.tsx +++ b/apps/web/components/kanban-card.tsx @@ -11,6 +11,10 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, + DropdownMenuPortal, } from '@kandev/ui/dropdown-menu'; import type { TaskState, Repository } from '@/lib/types/http'; import { cn, getRepositoryDisplayName } from '@/lib/utils'; @@ -32,6 +36,12 @@ export interface Task { reviewStatus?: 'pending' | 'approved' | 'changes_requested' | 'rejected' | null; } +export interface WorkflowColumn { + id: string; + title: string; + color: string; +} + interface KanbanCardProps { task: Task; repositoryName?: string | null; @@ -39,6 +49,8 @@ interface KanbanCardProps { onEdit?: (task: Task) => void; onDelete?: (task: Task) => void; onOpenFullPage?: (task: Task) => void; + onMove?: (task: Task, targetColumnId: string) => void; + columns?: WorkflowColumn[]; showMaximizeButton?: boolean; isDeleting?: boolean; } @@ -102,7 +114,7 @@ function KanbanCardLayout({ task, repositoryName, className }: KanbanCardProps & ); } -export function KanbanCard({ task, repositoryName, onClick, onEdit, onDelete, onOpenFullPage, showMaximizeButton = true, isDeleting }: KanbanCardProps) { +export function KanbanCard({ task, repositoryName, onClick, onEdit, onDelete, onOpenFullPage, onMove, columns, showMaximizeButton = true, isDeleting }: KanbanCardProps) { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id: task.id, }); @@ -181,6 +193,35 @@ export function KanbanCard({ task, repositoryName, onClick, onEdit, onDelete, on > Edit + {columns && columns.length > 1 && onMove && ( + + event.stopPropagation()} + onPointerDown={(event) => event.stopPropagation()} + > + Move to + + + + {columns + .filter((col) => col.id !== task.workflowStepId) + .map((col) => ( + { + event.stopPropagation(); + onMove(task, col.id); + }} + > +
+ {col.title} + + ))} + + + + )} { diff --git a/apps/web/components/kanban-column.tsx b/apps/web/components/kanban-column.tsx index 0760f87f97..8c9d6f6a9f 100644 --- a/apps/web/components/kanban-column.tsx +++ b/apps/web/components/kanban-column.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { useDroppable } from '@dnd-kit/core'; -import { KanbanCard, Task } from './kanban-card'; +import { KanbanCard, Task, WorkflowColumn } from './kanban-card'; import { Badge } from '@kandev/ui/badge'; import { cn, getRepositoryDisplayName } from '@/lib/utils'; import { useAppStore } from '@/components/state-provider'; @@ -22,11 +22,14 @@ interface KanbanColumnProps { onOpenTask: (task: Task) => void; onEditTask: (task: Task) => void; onDeleteTask: (task: Task) => void; + onMoveTask?: (task: Task, targetColumnId: string) => void; + columns?: WorkflowColumn[]; showMaximizeButton?: boolean; deletingTaskId?: string | null; + hideHeader?: boolean; } -export function KanbanColumn({ column, tasks, onPreviewTask, onOpenTask, onEditTask, onDeleteTask, showMaximizeButton, deletingTaskId }: KanbanColumnProps) { +export function KanbanColumn({ column, tasks, onPreviewTask, onOpenTask, onEditTask, onDeleteTask, onMoveTask, columns, showMaximizeButton, deletingTaskId, hideHeader = false }: KanbanColumnProps) { const { setNodeRef, isOver } = useDroppable({ id: column.id, }); @@ -54,15 +57,17 @@ export function KanbanColumn({ column, tasks, onPreviewTask, onOpenTask, onEditT )} > {/* Column Header */} -
-
-
-

{column.title}

- - {tasks.length} - + {!hideHeader && ( +
+
+
+

{column.title}

+ + {tasks.length} + +
-
+ )} {/* Tasks */}
@@ -75,6 +80,8 @@ export function KanbanColumn({ column, tasks, onPreviewTask, onOpenTask, onEditT onOpenFullPage={onOpenTask} onEdit={onEditTask} onDelete={onDeleteTask} + onMove={onMoveTask} + columns={columns} showMaximizeButton={showMaximizeButton} isDeleting={deletingTaskId === task.id} /> diff --git a/apps/web/components/kanban/kanban-header-mobile.tsx b/apps/web/components/kanban/kanban-header-mobile.tsx new file mode 100644 index 0000000000..0227751122 --- /dev/null +++ b/apps/web/components/kanban/kanban-header-mobile.tsx @@ -0,0 +1,54 @@ +'use client'; + +import Link from 'next/link'; +import { Button } from '@kandev/ui/button'; +import { IconMenu2 } from '@tabler/icons-react'; +import { MobileMenuSheet } from './mobile-menu-sheet'; +import { useAppStore } from '@/components/state-provider'; + +type KanbanHeaderMobileProps = { + workspaceId?: string; + currentPage?: 'kanban' | 'tasks'; + searchQuery?: string; + onSearchChange?: (query: string) => void; + isSearchLoading?: boolean; +}; + +export function KanbanHeaderMobile({ + workspaceId, + currentPage = 'kanban', + searchQuery = '', + onSearchChange, + isSearchLoading = false, +}: KanbanHeaderMobileProps) { + const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen); + const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen); + + return ( + <> +
+ + KanDev + + +
+ + + ); +} diff --git a/apps/web/components/kanban/kanban-header.tsx b/apps/web/components/kanban/kanban-header.tsx index c72debc43e..abbdcb9747 100644 --- a/apps/web/components/kanban/kanban-header.tsx +++ b/apps/web/components/kanban/kanban-header.tsx @@ -5,10 +5,14 @@ import { useRouter } from 'next/navigation'; import { Button } from '@kandev/ui/button'; import { ToggleGroup, ToggleGroupItem } from '@kandev/ui/toggle-group'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@kandev/ui/tooltip'; -import { IconPlus, IconSettings, IconList, IconLayoutKanban } from '@tabler/icons-react'; +import { IconPlus, IconSettings, IconList, IconLayoutKanban, IconMenu2 } from '@tabler/icons-react'; import { KanbanDisplayDropdown } from '../kanban-display-dropdown'; import { TaskSearchInput } from './task-search-input'; +import { KanbanHeaderMobile } from './kanban-header-mobile'; +import { MobileMenuSheet } from './mobile-menu-sheet'; import { linkToTasks } from '@/lib/links'; +import { useResponsiveBreakpoint } from '@/hooks/use-responsive-breakpoint'; +import { useAppStore } from '@/components/state-provider'; type KanbanHeaderProps = { onCreateTask: () => void; @@ -21,6 +25,9 @@ type KanbanHeaderProps = { export function KanbanHeader({ onCreateTask, workspaceId, currentPage = 'kanban', searchQuery = '', onSearchChange, isSearchLoading = false }: KanbanHeaderProps) { const router = useRouter(); + const { isMobile, isTablet } = useResponsiveBreakpoint(); + const isMenuOpen = useAppStore((state) => state.mobileKanban.isMenuOpen); + const setMenuOpen = useAppStore((state) => state.setMobileKanbanMenuOpen); const handleViewChange = (value: string) => { if (value === 'list' && currentPage !== 'tasks') { @@ -30,6 +37,102 @@ export function KanbanHeader({ onCreateTask, workspaceId, currentPage = 'kanban' } }; + // Mobile header: Logo and Hamburger menu (Add task is a FAB) + if (isMobile) { + return ( + + ); + } + + // Tablet header: Compact with overflow menu + if (isTablet) { + return ( + <> +
+ + KanDev + + {onSearchChange && ( + + )} +
+ + + + + + + + + + + Kanban + + + + + + + + + + List + + + + + +
+
+ + + ); + } + + // Desktop header: Full layout return (
diff --git a/apps/web/components/kanban/mobile-column-tabs.tsx b/apps/web/components/kanban/mobile-column-tabs.tsx new file mode 100644 index 0000000000..ab7741db7b --- /dev/null +++ b/apps/web/components/kanban/mobile-column-tabs.tsx @@ -0,0 +1,70 @@ +'use client'; + +import { useRef, useEffect } from 'react'; +import { cn } from '@/lib/utils'; +import { Badge } from '@kandev/ui/badge'; +import type { WorkflowStep } from '../kanban-column'; + +type MobileColumnTabsProps = { + columns: WorkflowStep[]; + activeIndex: number; + taskCounts: Record; + onColumnChange: (index: number) => void; +}; + +export function MobileColumnTabs({ + columns, + activeIndex, + taskCounts, + onColumnChange, +}: MobileColumnTabsProps) { + const tabsRef = useRef(null); + const activeTabRef = useRef(null); + + // Scroll active tab into view when it changes + useEffect(() => { + if (activeTabRef.current && tabsRef.current) { + const tabRect = activeTabRef.current.getBoundingClientRect(); + const containerRect = tabsRef.current.getBoundingClientRect(); + + const isTabVisible = + tabRect.left >= containerRect.left && tabRect.right <= containerRect.right; + + if (!isTabVisible) { + activeTabRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'center', + }); + } + } + }, [activeIndex]); + + return ( +
+ {columns.map((column, index) => ( + + ))} +
+ ); +} diff --git a/apps/web/components/kanban/mobile-drop-targets.tsx b/apps/web/components/kanban/mobile-drop-targets.tsx new file mode 100644 index 0000000000..9f542edccc --- /dev/null +++ b/apps/web/components/kanban/mobile-drop-targets.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { useDroppable } from '@dnd-kit/core'; +import { cn } from '@/lib/utils'; +import type { WorkflowStep } from '../kanban-column'; + +type MobileDropTargetProps = { + column: WorkflowStep; + isCurrentColumn: boolean; +}; + +function MobileDropTarget({ column, isCurrentColumn }: MobileDropTargetProps) { + const { setNodeRef, isOver } = useDroppable({ + id: column.id, + }); + + return ( +
+
+ {column.title} +
+ ); +} + +type MobileDropTargetsProps = { + columns: WorkflowStep[]; + currentColumnId: string | null; + isDragging: boolean; +}; + +export function MobileDropTargets({ + columns, + currentColumnId, + isDragging, +}: MobileDropTargetsProps) { + if (!isDragging) return null; + + return ( +
+
+ {columns.map((column) => ( + + ))} +
+

+ Drop on a column to move task +

+
+ ); +} diff --git a/apps/web/components/kanban/mobile-fab.tsx b/apps/web/components/kanban/mobile-fab.tsx new file mode 100644 index 0000000000..a4e164529e --- /dev/null +++ b/apps/web/components/kanban/mobile-fab.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { Button } from '@kandev/ui/button'; +import { IconPlus } from '@tabler/icons-react'; +import { cn } from '@/lib/utils'; + +type MobileFabProps = { + onClick: () => void; + isDragging?: boolean; +}; + +export function MobileFab({ onClick, isDragging = false }: MobileFabProps) { + return ( + + ); +} diff --git a/apps/web/components/kanban/mobile-menu-sheet.tsx b/apps/web/components/kanban/mobile-menu-sheet.tsx new file mode 100644 index 0000000000..965e6882b4 --- /dev/null +++ b/apps/web/components/kanban/mobile-menu-sheet.tsx @@ -0,0 +1,220 @@ +'use client'; + +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@kandev/ui/sheet'; +import { Button } from '@kandev/ui/button'; +import { Checkbox } from '@kandev/ui/checkbox'; +import { Badge } from '@kandev/ui/badge'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@kandev/ui/select'; +import { ToggleGroup, ToggleGroupItem } from '@kandev/ui/toggle-group'; +import { IconSettings, IconList, IconLayoutKanban } from '@tabler/icons-react'; +import { TaskSearchInput } from './task-search-input'; +import { useKanbanDisplaySettings } from '@/hooks/use-kanban-display-settings'; +import { linkToTasks } from '@/lib/links'; +import type { Workspace, Repository } from '@/lib/types/http'; +import type { BoardState } from '@/lib/state/slices'; + +type MobileMenuSheetProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + workspaceId?: string; + currentPage?: 'kanban' | 'tasks'; + searchQuery?: string; + onSearchChange?: (query: string) => void; + isSearchLoading?: boolean; +}; + +export function MobileMenuSheet({ + open, + onOpenChange, + workspaceId, + currentPage = 'kanban', + searchQuery = '', + onSearchChange, + isSearchLoading = false, +}: MobileMenuSheetProps) { + const router = useRouter(); + const { + workspaces, + boards, + activeWorkspaceId, + activeBoardId, + repositories, + repositoriesLoading, + allRepositoriesSelected, + selectedRepositoryId, + enablePreviewOnClick, + onWorkspaceChange, + onBoardChange, + onRepositoryChange, + onTogglePreviewOnClick, + } = useKanbanDisplaySettings(); + + const repositoryValue = allRepositoriesSelected ? 'all' : selectedRepositoryId ?? 'all'; + + const handleViewChange = (value: string) => { + if (value === 'list' && currentPage !== 'tasks') { + router.push(linkToTasks(workspaceId)); + onOpenChange(false); + } else if (value === 'kanban' && currentPage !== 'kanban') { + router.push('/'); + onOpenChange(false); + } + }; + + return ( + + + + Menu + +
+ {onSearchChange && ( +
+ + +
+ )} + +
+ + + + + Kanban + + + + List + + +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + onOpenChange(false)}> + + +
+
+
+ ); +} diff --git a/apps/web/components/kanban/swipeable-columns.tsx b/apps/web/components/kanban/swipeable-columns.tsx new file mode 100644 index 0000000000..5269ec34a9 --- /dev/null +++ b/apps/web/components/kanban/swipeable-columns.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { useEffect, useCallback } from 'react'; +import useEmblaCarousel from 'embla-carousel-react'; +import { KanbanColumn, WorkflowStep } from '../kanban-column'; +import { Task } from '../kanban-card'; + +type SwipeableColumnsProps = { + columns: WorkflowStep[]; + tasks: Task[]; + activeIndex: number; + onIndexChange: (index: number) => void; + onPreviewTask: (task: Task) => void; + onOpenTask: (task: Task) => void; + onEditTask: (task: Task) => void; + onDeleteTask: (task: Task) => void; + onMoveTask?: (task: Task, targetColumnId: string) => void; + showMaximizeButton?: boolean; + deletingTaskId?: string | null; +}; + +export function SwipeableColumns({ + columns, + tasks, + activeIndex, + onIndexChange, + onPreviewTask, + onOpenTask, + onEditTask, + onDeleteTask, + onMoveTask, + showMaximizeButton, + deletingTaskId, +}: SwipeableColumnsProps) { + const [emblaRef, emblaApi] = useEmblaCarousel({ + align: 'start', + containScroll: 'trimSnaps', + watchDrag: true, + }); + + const getTasksForColumn = useCallback( + (columnId: string) => { + return tasks + .filter((task) => task.workflowStepId === columnId) + .map((task) => ({ ...task, position: task.position ?? 0 })) + .sort((a, b) => (a.position ?? 0) - (b.position ?? 0)); + }, + [tasks] + ); + + // Sync carousel position with external activeIndex + useEffect(() => { + if (emblaApi && emblaApi.selectedScrollSnap() !== activeIndex) { + emblaApi.scrollTo(activeIndex); + } + }, [emblaApi, activeIndex]); + + // Update external activeIndex when user swipes + useEffect(() => { + if (!emblaApi) return; + + const onSelect = () => { + const selectedIndex = emblaApi.selectedScrollSnap(); + if (selectedIndex !== activeIndex) { + onIndexChange(selectedIndex); + } + }; + + emblaApi.on('select', onSelect); + return () => { + emblaApi.off('select', onSelect); + }; + }, [emblaApi, activeIndex, onIndexChange]); + + return ( +
+
+ {columns.map((column) => ( +
+ +
+ ))} +
+
+ ); +} diff --git a/apps/web/components/kanban/task-search-input.tsx b/apps/web/components/kanban/task-search-input.tsx index a12c48c1e6..8699f61b45 100644 --- a/apps/web/components/kanban/task-search-input.tsx +++ b/apps/web/components/kanban/task-search-input.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { Input } from '@kandev/ui/input'; import { IconSearch, IconX, IconLoader2 } from '@tabler/icons-react'; +import { cn } from '@/lib/utils'; interface TaskSearchInputProps { value: string; @@ -10,6 +11,7 @@ interface TaskSearchInputProps { placeholder?: string; debounceMs?: number; isLoading?: boolean; + className?: string; } export function TaskSearchInput({ @@ -18,6 +20,7 @@ export function TaskSearchInput({ placeholder = 'Search tasks...', debounceMs = 300, isLoading = false, + className, }: TaskSearchInputProps) { const [localValue, setLocalValue] = useState(value); const timeoutRef = useRef(null); @@ -63,7 +66,7 @@ export function TaskSearchInput({ }, []); return ( -
+
{isLoading ? ( ) : ( diff --git a/apps/web/components/settings/settings-app-sidebar.tsx b/apps/web/components/settings/settings-app-sidebar.tsx index 1bf8ff79c6..4446773d4a 100644 --- a/apps/web/components/settings/settings-app-sidebar.tsx +++ b/apps/web/components/settings/settings-app-sidebar.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect } from 'react'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { @@ -25,6 +26,7 @@ import { SidebarMenuSubItem, SidebarMenuSubButton, SidebarHeader, + useSidebar, } from '@kandev/ui/sidebar'; import { ScrollArea } from '@kandev/ui/scroll-area'; import { useAppStore } from '@/components/state-provider'; @@ -33,12 +35,20 @@ import type { Workspace, Agent, AgentProfile, Environment, Executor } from '@/li export function SettingsAppSidebar() { const pathname = usePathname(); + const { setOpenMobile, isMobile } = useSidebar(); const workspaces = useAppStore((state) => state.workspaces.items); const environments = useAppStore((state) => state.environments.items); const executors = useAppStore((state) => state.executors.items); const agents = useAppStore((state) => state.settingsAgents.items); useAvailableAgents(); + // Close mobile sidebar when navigating to a new page + useEffect(() => { + if (isMobile) { + setOpenMobile(false); + } + }, [pathname, isMobile, setOpenMobile]); + return ( @@ -46,12 +56,8 @@ export function SettingsAppSidebar() { -
- K -
- KanDev.ai - Settings + Kandev
@@ -64,193 +70,193 @@ export function SettingsAppSidebar() { Settings - {/* General */} - - - - - General - - - - - - - - Notifications - - - - - - - - Editors - - - - - - - {/* Workspaces */} - - - - - Workspaces - - - {workspaces.length > 0 && ( + {/* General */} + + + + + General + + - {workspaces.map((workspace: Workspace) => { - const workspacePath = `/settings/workspace/${workspace.id}`; - const boardsPath = `${workspacePath}/boards`; - const repositoriesPath = `${workspacePath}/repositories`; - - return ( - - - - {workspace.name} - - - - - - - Repositories - - - - - - - Boards - - - - - - ); - })} + + + + + Notifications + + + + + + + + Editors + + + - )} - + + + {/* Workspaces */} + + + + + Workspaces + + + {workspaces.length > 0 && ( + + {workspaces.map((workspace: Workspace) => { + const workspacePath = `/settings/workspace/${workspace.id}`; + const boardsPath = `${workspacePath}/boards`; + const repositoriesPath = `${workspacePath}/repositories`; - {/* Agents */} - - - - - Agents - - - {agents.length > 0 && ( - - {agents.flatMap((agent: Agent) => - agent.profiles.map((profile: AgentProfile) => { - const encodedAgent = encodeURIComponent(agent.name); - const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`; - const agentLabel = profile.agent_display_name || agent.name; return ( - - - - - {agentLabel} • {profile.name} - + + + + {workspace.name} + + + + + Repositories + + + + + + + Boards + + + + ); - }) - )} - - )} - + })} + + )} + - {/* Prompts */} - - - - - Prompts - - - + {/* Agents */} + + + + + Agents + + + {agents.length > 0 && ( + + {agents.flatMap((agent: Agent) => + agent.profiles.map((profile: AgentProfile) => { + const encodedAgent = encodeURIComponent(agent.name); + const profilePath = `/settings/agents/${encodedAgent}/profiles/${profile.id}`; + const agentLabel = profile.agent_display_name || agent.name; + return ( + + + + + {agentLabel} • {profile.name} + + + + + ); + }) + )} + + )} + - {/* Environments */} - - - - - Environments - - - {environments.length > 0 && ( - - {environments.map((env: Environment) => ( - - - - {env.name} - - - - ))} - - )} - + {/* Prompts */} + + + + + Prompts + + + - {/* Executors */} - - - - - Executors - - - {executors.filter((executor: Executor) => executor.type !== 'remote_docker').length > 0 && ( - - {executors - .filter((executor: Executor) => executor.type !== 'remote_docker') - .map((executor: Executor) => ( - + {/* Environments */} + + + + + Environments + + + {environments.length > 0 && ( + + {environments.map((env: Environment) => ( + - - {executor.name} + + {env.name} ))} - - )} - + + )} + + + {/* Executors */} + + + + + Executors + + + {executors.filter((executor: Executor) => executor.type !== 'remote_docker').length > 0 && ( + + {executors + .filter((executor: Executor) => executor.type !== 'remote_docker') + .map((executor: Executor) => ( + + + + {executor.name} + + + + ))} + + )} + diff --git a/apps/web/components/settings/settings-layout-client.tsx b/apps/web/components/settings/settings-layout-client.tsx index 9c95e6ee09..4161407eb7 100644 --- a/apps/web/components/settings/settings-layout-client.tsx +++ b/apps/web/components/settings/settings-layout-client.tsx @@ -8,7 +8,7 @@ import { BreadcrumbLink, BreadcrumbList, } from '@kandev/ui/breadcrumb'; -import { SidebarInset, SidebarProvider } from '@kandev/ui/sidebar'; +import { SidebarInset, SidebarProvider, SidebarTrigger } from '@kandev/ui/sidebar'; import { SettingsAppSidebar } from '@/components/settings/settings-app-sidebar'; import { TooltipProvider } from '@kandev/ui/tooltip'; @@ -24,7 +24,8 @@ export function SettingsLayoutClient({ children }: { children: React.ReactNode }
-
+
+ diff --git a/apps/web/components/task-create-dialog.tsx b/apps/web/components/task-create-dialog.tsx index e6d59d52af..7ae4a31834 100644 --- a/apps/web/components/task-create-dialog.tsx +++ b/apps/web/components/task-create-dialog.tsx @@ -929,7 +929,7 @@ export function TaskCreateDialog({ return ( - + {isSessionMode ? 'Create New Session' : submitLabel === 'Create' ? 'Create Task' : 'Edit Task'} @@ -949,7 +949,7 @@ export function TaskCreateDialog({ descriptionValueRef={descriptionInputRef} /> {!isSessionMode && ( -
+
)}
- + {!isSessionMode && (
@@ -1193,13 +1193,14 @@ export function TaskCreateDialog({
)} -