diff --git a/archon-ui-main/src/components/layout/MainLayout.tsx b/archon-ui-main/src/components/layout/MainLayout.tsx index 73fcc1de..ffa31792 100644 --- a/archon-ui-main/src/components/layout/MainLayout.tsx +++ b/archon-ui-main/src/components/layout/MainLayout.tsx @@ -129,11 +129,12 @@ export function MainLayout({ children, className }: MainLayoutProps) { }, [isBackendError, backendError, showToast]); return ( -
- Editing and uploading project documents is currently disabled while we migrate to a new storage system. - - {" "} - Please backup your existing project documents elsewhere as they will be lost when the migration is - complete. - -
-- Note: This only affects project-specific documents. Your knowledge base documents are safe and unaffected. -
-- Viewing {documents.length} document{documents.length !== 1 ? "s" : ""} -
+ {/* Left Sidebar - Document List */} ++ {documents.length} document{documents.length !== 1 ? "s" : ""} +
+ +{searchQuery ? "No documents found" : "No documents in this project"}
- {documents.length > 0 ? "Select a document to view" : "No documents available"} -
-+ {documents.length > 0 ? "Select a document to view" : "No documents available"} +
+- {new Date(document.updated_at || document.created_at || Date.now()).toLocaleDateString()} -
- - {/* ID Display Section - Always visible for active, hover for others */} -+ {new Date(document.updated_at || document.created_at || Date.now()).toLocaleDateString()} +
+ + {/* ID Display Section - Always visible for active, hover for others */} +No content available
; + returnNo content available
; } // Handle string content if (typeof document.content === "string") { return ( -{document.content}
+
+ {document.content}
+
+ ++); } @@ -38,78 +144,136 @@ export const DocumentViewer = ({ document }: DocumentViewerProps) => { // Handle text field if ("text" in document.content && typeof document.content.text === "string") { return ( -( + + ), + h2: ({ node, ...props }) => ( + + ), + h3: ({ node, ...props }) => ( + + ), + p: ({ node, ...props }) => ( + + ), + ul: ({ node, ...props }) => ( + + ), + ol: ({ node, ...props }) => ( +
+ ), + li: ({ node, ...props }) => , + code: ({ node, ...props }) => ( +
+ ), + pre: ({ node, ...props }) => ( + + ), + a: ({ node, ...props }) => , + blockquote: ({ node, ...props }) => ( + + ), + }} + > {document.content.markdown} - +- {document.content.text} -++); } - // Handle structured content (JSON) - return ( -+ {document.content.text} ++- {Object.entries(document.content).map(([key, value]) => ( --); diff --git a/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx b/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx index c1edb2d8..1c1e2e30 100644 --- a/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx +++ b/archon-ui-main/src/features/projects/tasks/components/KanbanColumn.tsx @@ -1,3 +1,4 @@ +import { Activity, CheckCircle2, Eye, ListTodo } from "lucide-react"; import { useRef } from "react"; import { useDrop } from "react-dnd"; import { cn } from "../../../ui/primitives/styles"; @@ -32,71 +33,96 @@ export const KanbanColumn = ({ }: KanbanColumnProps) => { const ref = useRef- {key.replace(/_/g, " ").charAt(0).toUpperCase() + key.replace(/_/g, " ").slice(1)} -
-- {typeof value === "string" ? ( -+ ); + } + + // Fallback: render JSON as formatted text + return ( +{value}
- ) : Array.isArray(value) ? ( -- {value.map((item, i) => ( -
- ) : ( -- - {typeof item === "object" ? JSON.stringify(item, null, 2) : String(item)} -
- ))} -- {JSON.stringify(value, null, 2)} -+ // Handle sections array (structured content) + if ("sections" in document.content && Array.isArray(document.content.sections)) { + return ( ++ {document.content.sections.map((section: any, index: number) => ( +- ))} + ))} ++ {section.heading && ( +-+ {section.heading} +
+ )} + {section.content && ( ++ {section.content} +)}+); }; return ( -+ {JSON.stringify(document.content, null, 2)} +- {/* Header */} -+); }; diff --git a/archon-ui-main/src/features/projects/documents/hooks/index.ts b/archon-ui-main/src/features/projects/documents/hooks/index.ts index 79042568..d21df27c 100644 --- a/archon-ui-main/src/features/projects/documents/hooks/index.ts +++ b/archon-ui-main/src/features/projects/documents/hooks/index.ts @@ -1,7 +1,7 @@ /** * Document Hooks * - * Read-only hooks for document display + * Hooks for document display and editing */ -export { useProjectDocuments } from "./useDocumentQueries"; +export { useCreateDocument, useDeleteDocument, useProjectDocuments, useUpdateDocument } from "./useDocumentQueries"; diff --git a/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts b/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts index 00c6eea6..9d4e9f4f 100644 --- a/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts +++ b/archon-ui-main/src/features/projects/documents/hooks/useDocumentQueries.ts @@ -1,6 +1,8 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { callAPIWithETag } from "../../../shared/api/apiClient"; import { DISABLED_QUERY_KEY, STALE_TIMES } from "../../../shared/config/queryPatterns"; -import { projectService } from "../../services"; +import { useToast } from "../../../shared/hooks/useToast"; +import { documentService } from "../services/documentService"; import type { ProjectDocument } from "../types"; // Query keys factory for documents @@ -14,18 +16,122 @@ export const documentKeys = { }; /** - * Get documents from project's docs JSONB field - * Read-only - no mutations + * Get documents for a project from Archon documents API */ export function useProjectDocuments(projectId: string | undefined) { return useQuery({ queryKey: projectId ? documentKeys.byProject(projectId) : DISABLED_QUERY_KEY, queryFn: async () => { if (!projectId) return []; - const project = await projectService.getProject(projectId); - return (project.docs || []) as ProjectDocument[]; + return await documentService.getDocumentsByProject(projectId); }, enabled: !!projectId, staleTime: STALE_TIMES.normal, }); } + +/** + * Get a single document by ID + */ +export function useProjectDocument(projectId: string | undefined, documentId: string | undefined) { + return useQuery({ + queryKey: projectId && documentId ? documentKeys.detail(projectId, documentId) : DISABLED_QUERY_KEY, + queryFn: async () => { + if (!projectId || !documentId) return null; + return await documentService.getDocument(projectId, documentId); + }, + enabled: !!(projectId && documentId), + staleTime: STALE_TIMES.normal, + }); +} + +// Type for document updates +export interface DocumentUpdateData { + documentId: string; + updates: { title?: string; content?: unknown; tags?: string[]; author?: string }; +} + +/** + * Update a project document + */ +export function useUpdateDocument(projectId: string) { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + return useMutation({ + mutationFn: async ({ documentId, updates }: DocumentUpdateData) => { + return await documentService.updateDocument(projectId, documentId, updates); + }, + + onSuccess: (_, variables) => { + // Invalidate documents list to refetch with new content + queryClient.invalidateQueries({ queryKey: documentKeys.byProject(projectId) }); + // Invalidate the specific document detail to update open viewers + queryClient.invalidateQueries({ queryKey: documentKeys.detail(projectId, variables.documentId) }); + showToast("Document updated successfully", "success"); + }, + + onError: (error: Error) => { + showToast(`Failed to update document: ${error.message}`, "error"); + }, + }); +} + +/** + * Create a new project document + */ +export function useCreateDocument(projectId: string) { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + return useMutation({ + mutationFn: async (document: { + title: string; + document_type: string; + content?: any; + tags?: string[]; + author?: string; + }) => { + const response = await callAPIWithETag<{ success: boolean; message: string; document: ProjectDocument }>( + `/api/projects/${projectId}/docs`, + { + method: "POST", + body: JSON.stringify(document), + }, + ); + return response.document; + }, + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: documentKeys.byProject(projectId) }); + showToast("Document created successfully", "success"); + }, + + onError: (error: Error) => { + showToast(`Failed to create document: ${error.message}`, "error"); + }, + }); +} + +/** + * Delete a project document + */ +export function useDeleteDocument(projectId: string) { + const queryClient = useQueryClient(); + const { showToast } = useToast(); + + return useMutation({ + mutationFn: async (documentId: string) => { + return await documentService.deleteDocument(projectId, documentId); + }, + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: documentKeys.byProject(projectId) }); + showToast("Document deleted successfully", "success"); + }, + + onError: (error: Error) => { + showToast(`Failed to delete document: ${error.message}`, "error"); + }, + }); +} diff --git a/archon-ui-main/src/features/projects/documents/services/documentService.ts b/archon-ui-main/src/features/projects/documents/services/documentService.ts new file mode 100644 index 00000000..c05c70e5 --- /dev/null +++ b/archon-ui-main/src/features/projects/documents/services/documentService.ts @@ -0,0 +1,70 @@ +/** + * Document Service + * Handles API calls for project documents via Archon MCP + */ + +import { callAPIWithETag } from "../../../shared/api/apiClient"; +import type { ProjectDocument } from "../types"; + +interface DocumentsResponse { + success: boolean; + documents: ProjectDocument[]; + count: number; + total: number; +} + +export const documentService = { + /** + * Get all documents for a project + */ + async getDocumentsByProject(projectId: string): Promise+ {/* Metadata Card - Blue glass with bottom edge-lit */} ++ - {/* Content */} - -- {document.tags && document.tags.length > 0 && ( -+ -{document.title}
-- Type: {document.document_type || "document"} • Last updated:{" "} - {new Date(document.updated_at).toLocaleDateString()} -
+{document.title}
++ + Type:{" "} + + {document.document_type || "document"} + + + {document.updated_at && ( + + Last updated: {new Date(document.updated_at).toLocaleDateString()} + + )} +- {document.tags.map((tag) => ( - - {tag} - - ))} -- )} -{renderContent()}+ {/* Content Card - Medium blur glass */} ++ ++ + {isEditMode ? ( +Content
++ {/* Save button - only show in edit mode with changes */} + {isEditMode && hasChanges && ( +++ + )} + {/* View/Edit toggle */} ++ + ++ ++ {isEditMode ? ( + + ) : ( + + )} + +{ + const response = await callAPIWithETag (`/api/projects/${projectId}/docs?include_content=true`); + return response.documents || []; + }, + + /** + * Get a single document by ID + */ + async getDocument(projectId: string, documentId: string): Promise { + const response = await callAPIWithETag<{ success: boolean; document: ProjectDocument }>( + `/api/projects/${projectId}/docs/${documentId}`, + ); + if (!response.document) { + throw new Error(`Document not found: ${documentId} in project ${projectId}`); + } + return response.document; + }, + + /** + * Update a document + */ + async updateDocument( + projectId: string, + documentId: string, + updates: { content?: unknown; title?: string; tags?: string[] }, + ): Promise { + const response = await callAPIWithETag<{ success: boolean; document: ProjectDocument }>( + `/api/projects/${projectId}/docs/${documentId}`, + { + method: "PUT", + body: JSON.stringify(updates), + }, + ); + if (!response.document) { + throw new Error(`Failed to update document: ${documentId} in project ${projectId}`); + } + return response.document; + }, + + /** + * Delete a document + */ + async deleteDocument(projectId: string, documentId: string): Promise { + await callAPIWithETag<{ success: boolean; message: string }>( + `/api/projects/${projectId}/docs/${documentId}`, + { + method: "DELETE", + }, + ); + }, +}; diff --git a/archon-ui-main/src/features/projects/tasks/TasksTab.tsx b/archon-ui-main/src/features/projects/tasks/TasksTab.tsx index d30da07a..284278e1 100644 --- a/archon-ui-main/src/features/projects/tasks/TasksTab.tsx +++ b/archon-ui-main/src/features/projects/tasks/TasksTab.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from "react"; import { DndProvider } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { DeleteConfirmModal } from "../../ui/components/DeleteConfirmModal"; -import { Button } from "../../ui/primitives"; +import { Button, Card } from "../../ui/primitives"; import { cn, glassmorphism } from "../../ui/primitives/styles"; import { TaskEditModal } from "./components/TaskEditModal"; import { useDeleteTask, useProjectTasks, useUpdateTask } from "./hooks"; @@ -260,18 +260,17 @@ const ViewControls = ({ viewMode, onViewChange, onAddTask }: ViewControlsProps) {/* View Toggle Controls with Glassmorphism */} - +onViewChange("table")} + aria-label="Switch to table view" + aria-pressed={viewMode === "table"} className={cn( "px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300", viewMode === "table" @@ -279,7 +278,7 @@ const ViewControls = ({ viewMode, onViewChange, onAddTask }: ViewControlsProps) : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300", )} > - -+ Table {viewMode === "table" && ( onViewChange("board")} + aria-label="Switch to board view" + aria-pressed={viewMode === "board"} className={cn( "px-5 py-2.5 flex items-center gap-2 relative transition-all duration-300", viewMode === "board" @@ -303,7 +304,7 @@ const ViewControls = ({ viewMode, onViewChange, onAddTask }: ViewControlsProps) : "text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-300", )} > -
+ Board {viewMode === "board" && ( )} (null); - const [{ isOver }, drop] = useDrop({ + const [, drop] = useDrop({ accept: ItemTypes.TASK, drop: (item: { id: string; status: Task["status"] }) => { if (item.status !== status) { onTaskMove(item.id, status); } }, - collect: (monitor) => ({ - isOver: !!monitor.isOver(), - }), }); drop(ref); + // Get icon and label based on status + const getStatusInfo = () => { + switch (status) { + case "todo": + return { + icon: , + label: "Todo", + color: "bg-pink-500/10 text-pink-600 dark:text-pink-400 border-pink-500/30", + }; + case "doing": + return { + icon: , + label: "Doing", + color: "bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/30", + }; + case "review": + return { + icon: , + label: "Review", + color: "bg-purple-500/10 text-purple-600 dark:text-purple-400 border-purple-500/30", + }; + case "done": + return { + icon: , + label: "Done", + color: "bg-green-500/10 text-green-600 dark:text-green-400 border-green-500/30", + }; + default: + return { + icon: , + label: "Todo", + color: "bg-gray-500/10 text-gray-600 dark:text-gray-400 border-gray-500/30", + }; + } + }; + + const statusInfo = getStatusInfo(); + return ( - - {/* Column Header with Glassmorphism */} --{title}
- {/* Column header glow effect */} ++ {/* Column Header - pill badge only */} +); diff --git a/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx b/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx index c8e09464..3772de0c 100644 --- a/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx +++ b/archon-ui-main/src/features/projects/tasks/components/TaskCard.tsx @@ -3,7 +3,9 @@ import type React from "react"; import { useCallback } from "react"; import { useDrag, useDrop } from "react-dnd"; import { isOptimistic } from "@/features/shared/utils/optimistic"; +import { Card } from "../../../ui/primitives"; import { OptimisticIndicator } from "../../../ui/primitives/OptimisticIndicator"; +import { cn } from "../../../ui/primitives/styles"; import { useTaskActions } from "../hooks"; import type { Assignee, Task, TaskPriority } from "../types"; import { getOrderColor, getOrderGlow, ItemTypes } from "../utils/task-styles"; @@ -120,48 +122,40 @@ export const TaskCard: React.FC+{/* Tasks Container */}++ {/* Colored underline */}+ {statusInfo.icon} + {statusInfo.label} + {tasks.length} ++- {tasks.length === 0 ? ( -No tasks- ) : ( - tasks.map((task, index) => ( -- )) - )} + {tasks.map((task, index) => ( + + ))} = ({ } }; - // Glassmorphism styling constants - const cardBaseStyles = - "bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30 border border-gray-200 dark:border-gray-700 rounded-lg backdrop-blur-md"; - const transitionStyles = "transition-all duration-200 ease-in-out"; - - // Subtle highlight effect for related tasks - const highlightGlow = isHighlighted ? "border-cyan-400/50 shadow-[0_0_8px_rgba(34,211,238,0.2)]" : ""; - - // Selection styling with glassmorphism - const selectionGlow = isSelected - ? "border-blue-500 shadow-[0_0_12px_rgba(59,130,246,0.4)] bg-blue-50/30 dark:bg-blue-900/20" - : ""; - - // Beautiful hover effect with glowing borders - const hoverEffectClasses = - "group-hover:border-cyan-400/70 dark:group-hover:border-cyan-500/50 group-hover:shadow-[0_0_15px_rgba(34,211,238,0.4)] dark:group-hover:shadow-[0_0_15px_rgba(34,211,238,0.6)]"; - return ( // biome-ignore lint/a11y/useSemanticElements: Drag-and-drop card with react-dnd - requires div for drag handle drag(drop(node))} - role="button" - tabIndex={0} - className={`w-full min-h-[140px] cursor-move relative ${isDragging ? "opacity-50 scale-90" : "scale-100 opacity-100"} ${transitionStyles} group`} + role="group" + className={cn( + "w-full min-h-[140px] cursor-move relative group", + "transition-all duration-200 ease-in-out", + isDragging ? "opacity-50 scale-90" : "scale-100 opacity-100", + )} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} onClick={handleTaskClick} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - if (onEdit) { - onEdit(task); - } - } - }} > -); }; diff --git a/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx b/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx index 3c6a8a08..28c91711 100644 --- a/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx +++ b/archon-ui-main/src/features/projects/tasks/utils/task-styles.tsx @@ -36,10 +36,10 @@ export const getAssigneeGlow = (assigneeName: Assignee) => { // Get color based on task priority/order export const getOrderColor = (order: number) => { - if (order <= 3) return "bg-rose-500"; - if (order <= 6) return "bg-orange-500"; - if (order <= 10) return "bg-blue-500"; - return "bg-green-500"; + if (order <= 3) return "bg-rose-500 dark:bg-rose-400"; + if (order <= 6) return "bg-orange-500 dark:bg-orange-400"; + if (order <= 10) return "bg-blue-500 dark:bg-blue-400"; + return "bg-green-500 dark:bg-green-400"; }; // Get glow effect based on task priority/order @@ -68,12 +68,12 @@ export const getColumnColor = (status: "todo" | "doing" | "review" | "done") => export const getColumnGlow = (status: "todo" | "doing" | "review" | "done") => { switch (status) { case "todo": - return "bg-gray-500/30"; + return "bg-gray-500/30 dark:bg-gray-400/40"; case "doing": - return "bg-blue-500/30 shadow-[0_0_10px_2px_rgba(59,130,246,0.2)]"; + return "bg-blue-500/30 dark:bg-blue-400/40 shadow-[0_0_10px_2px_rgba(59,130,246,0.2)] dark:shadow-[0_0_10px_2px_rgba(96,165,250,0.3)]"; case "review": - return "bg-purple-500/30 shadow-[0_0_10px_2px_rgba(168,85,247,0.2)]"; + return "bg-purple-500/30 dark:bg-purple-400/40 shadow-[0_0_10px_2px_rgba(168,85,247,0.2)] dark:shadow-[0_0_10px_2px_rgba(192,132,252,0.3)]"; case "done": - return "bg-green-500/30 shadow-[0_0_10px_2px_rgba(34,197,94,0.2)]"; + return "bg-green-500/30 dark:bg-green-400/40 shadow-[0_0_10px_2px_rgba(34,197,94,0.2)] dark:shadow-[0_0_10px_2px_rgba(74,222,128,0.3)]"; } }; diff --git a/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx b/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx index 35c7334f..5b005010 100644 --- a/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx +++ b/archon-ui-main/src/features/projects/tasks/views/BoardView.tsx @@ -37,7 +37,7 @@ export const BoardView = ({ return ({/* Priority indicator with beautiful glow */} {/* Content container with fixed padding */} @@ -186,7 +180,7 @@ export const TaskCard: React.FC+= ({ {/* Action buttons group */} - +-= ({ /> {/* Board Columns Grid */} -+{columns.map(({ status, title }) => (; + interface TableViewProps { tasks: Task[]; projectId: string; @@ -114,11 +121,9 @@ const DraggableRow = ({ drag(drop(node))} className={cn( - "group transition-all duration-200 cursor-move", - index % 2 === 0 ? "bg-white/50 dark:bg-black/50" : "bg-gray-50/80 dark:bg-gray-900/30", - "hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70", - "dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20", - "border-b border-gray-200 dark:border-gray-800", + "group transition-all duration-200 cursor-move border-b border-gray-200 dark:border-gray-800", + index % 2 === 0 ? rowVariants.even : rowVariants.odd, + rowVariants.hover, isDragging && "opacity-50 scale-105 shadow-lg", isOver && "bg-cyan-100/50 dark:bg-cyan-900/20 border-cyan-400", )} @@ -178,8 +183,8 @@ const DraggableRow = ({ - - + + Edit task @@ -192,8 +197,9 @@ const DraggableRow = ({ size="xs" onClick={handleComplete} className="h-7 w-7 p-0 text-green-600 hover:text-green-700" + aria-label="Mark task as complete" > -+ Mark as complete @@ -207,8 +213,9 @@ const DraggableRow = ({ onClick={handleDelete} className="h-7 w-7 p-0 text-red-600 hover:text-red-700" disabled={deleteTaskMutation.isPending} + aria-label="Delete task" > -+ Delete task @@ -255,7 +262,7 @@ export const TableView = ({-
+ Title Status diff --git a/archon-ui-main/src/features/projects/views/ProjectsView.tsx b/archon-ui-main/src/features/projects/views/ProjectsView.tsx index ceac8176..da1b3b65 100644 --- a/archon-ui-main/src/features/projects/views/ProjectsView.tsx +++ b/archon-ui-main/src/features/projects/views/ProjectsView.tsx @@ -1,10 +1,15 @@ import { useQueryClient } from "@tanstack/react-query"; import { motion } from "framer-motion"; +import { Activity, CheckCircle2, FileText, LayoutGrid, List, ListTodo, Pin } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useStaggeredEntrance } from "../../../hooks/useStaggeredEntrance"; +import { isOptimistic } from "../../shared/utils/optimistic"; import { DeleteConfirmModal } from "../../ui/components/DeleteConfirmModal"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../ui/primitives"; +import { OptimisticIndicator } from "../../ui/primitives/OptimisticIndicator"; +import { Button, PillNavigation, SelectableCard } from "../../ui/primitives"; +import { StatPill } from "../../ui/primitives/pill"; +import { cn } from "../../ui/primitives/styles"; import { NewProjectModal } from "../components/NewProjectModal"; import { ProjectHeader } from "../components/ProjectHeader"; import { ProjectList } from "../components/ProjectList"; @@ -44,6 +49,9 @@ export function ProjectsView({ className = "", "data-id": dataId }: ProjectsView // State management const [selectedProject, setSelectedProject] = useState(null); const [activeTab, setActiveTab] = useState("tasks"); + const [layoutMode, setLayoutMode] = useState<"horizontal" | "sidebar">("horizontal"); + const [sidebarExpanded, setSidebarExpanded] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [projectToDelete, setProjectToDelete] = useState<{ @@ -59,14 +67,20 @@ export function ProjectsView({ className = "", "data-id": dataId }: ProjectsView const updateProjectMutation = useUpdateProject(); const deleteProjectMutation = useDeleteProject(); - // Sort projects - pinned first, then alphabetically + // Sort and filter projects const sortedProjects = useMemo(() => { - return [...(projects as Project[])].sort((a, b) => { + // Filter by search query + const filtered = (projects as Project[]).filter((project) => + project.title.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // Sort: pinned first, then alphabetically + return filtered.sort((a, b) => { if (a.pinned && !b.pinned) return -1; if (!a.pinned && b.pinned) return 1; return a.title.localeCompare(b.title); }); - }, [projects]); + }, [projects, searchQuery]); // Handle project selection const handleProjectSelect = useCallback( @@ -165,51 +179,142 @@ export function ProjectsView({ className = "", "data-id": dataId }: ProjectsView initial="hidden" animate={isVisible ? "visible" : "hidden"} variants={containerVariants} - className={`max-w-full mx-auto ${className}`} + className={cn("max-w-full mx-auto", className)} data-id={dataId} > - setIsNewProjectModalOpen(true)} /> - - queryClient.invalidateQueries({ queryKey: projectKeys.lists() })} + setIsNewProjectModalOpen(true)} + layoutMode={layoutMode} + onLayoutModeChange={setLayoutMode} + searchQuery={searchQuery} + onSearchChange={setSearchQuery} /> - {/* Project Details Section */} - {selectedProject && ( - - - - + {layoutMode === "horizontal" ? ( + <> +- Docs - -- Tasks - -queryClient.invalidateQueries({ queryKey: projectKeys.lists() })} + /> - {/* Tab content */} - - {activeTab === "docs" && ( -- - )} - {activeTab === "tasks" && ( -- - - )} + {/* Project Details Section */} + {selectedProject && ( +- + {/* PillNavigation centered, View Toggle on right */} + + )} + > + ) : ( + /* Sidebar Mode */ ++ ++ + {/* Tab content */} +}, + { id: "tasks", label: "Tasks", icon: }, + ]} + activeSection={activeTab} + onSectionClick={(id) => setActiveTab(id as string)} + colorVariant="orange" + size="small" + showIcons={true} + showText={true} + hasSubmenus={false} + /> + + + {activeTab === "docs" &&+} + {activeTab === "tasks" && } + + {/* Left Sidebar - Collapsible Project List */} + {sidebarExpanded && ( +)} {/* Modals */} @@ -232,3 +337,77 @@ export function ProjectsView({ className = "", "data-id": dataId }: ProjectsView ); } + +// Sidebar Project Card - compact variant with StatPills +interface SidebarProjectCardProps { + project: Project; + isSelected: boolean; + taskCounts: { + todo: number; + doing: number; + review: number; + done: number; + }; + onSelect: () => void; +} + +const SidebarProjectCard: React.FC+- - + )} + + {/* Main Content Area - CRITICAL: min-w-0 prevents page expansion */} +++Projects
+setSidebarExpanded(false)} + className="px-2" + aria-label="Collapse sidebar" + aria-expanded={sidebarExpanded} + > + + ++ {sortedProjects.map((project) => ( +handleProjectSelect(project)} + /> + ))} + + {selectedProject && ( + <> + {/* Header with project name, tabs, view toggle inline */} +++ {!sidebarExpanded && ( ++ + {/* Tab Content */} +setSidebarExpanded(true)} + className="px-2 flex-shrink-0" + aria-label="Expand sidebar" + aria-expanded={sidebarExpanded} + > + + {selectedProject.title} + + )} + + {/* PillNavigation - ALWAYS CENTERED */} +++ +}, + { id: "tasks", label: "Tasks", icon: }, + ]} + activeSection={activeTab} + onSectionClick={(id) => setActiveTab(id as string)} + colorVariant="orange" + size="small" + showIcons={true} + showText={true} + hasSubmenus={false} + /> + + {activeTab === "docs" &&+ > + )} +} + {activeTab === "tasks" && } + = ({ project, isSelected, taskCounts, onSelect }) => { + const optimistic = isOptimistic(project); + + const getBackgroundClass = () => { + if (project.pinned) + return "bg-gradient-to-b from-purple-100/80 via-purple-50/30 to-purple-100/50 dark:from-purple-900/30 dark:via-purple-900/20 dark:to-purple-900/10"; + if (isSelected) + return "bg-gradient-to-b from-white/70 via-purple-50/20 to-white/50 dark:from-white/5 dark:via-purple-900/5 dark:to-black/20"; + return "bg-gradient-to-b from-white/80 to-white/60 dark:from-white/10 dark:to-black/30"; + }; + + return ( + + + ); +}; diff --git a/archon-ui-main/src/features/ui/primitives/index.ts b/archon-ui-main/src/features/ui/primitives/index.ts index e80760de..2d47e3b3 100644 --- a/archon-ui-main/src/features/ui/primitives/index.ts +++ b/archon-ui-main/src/features/ui/primitives/index.ts @@ -24,6 +24,7 @@ export * from "./grouped-card"; export * from "./input"; export * from "./inspector-dialog"; export * from "./pill"; +export * from "./pill-navigation"; export * from "./select"; export * from "./selectable-card"; // Export style utilities+ {/* Title */} ++++ + {/* Status Pills - horizontal layout with icons */} ++ {project.title} +
++ {project.pinned && ( +++ ++ )} ++ ++} /> + } + /> + } /> +