diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 0ef50729..2ad6560c 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1289,6 +1289,8 @@ export function BoardView() { selectedFeatureIds={selectedFeatureIds} onToggleFeatureSelection={toggleFeatureSelection} onToggleSelectionMode={toggleSelectionMode} + isDragging={activeFeature !== null} + onAiSuggest={() => setShowPlanDialog(true)} /> diff --git a/apps/ui/src/components/views/board-view/components/empty-state-card.tsx b/apps/ui/src/components/views/board-view/components/empty-state-card.tsx new file mode 100644 index 00000000..30ccdefc --- /dev/null +++ b/apps/ui/src/components/views/board-view/components/empty-state-card.tsx @@ -0,0 +1,120 @@ +import { memo } from 'react'; +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Kbd } from '@/components/ui/kbd'; +import { formatShortcut } from '@/store/app-store'; +import { getEmptyStateConfig, type EmptyStateConfig } from '../constants'; +import { Lightbulb, Play, Clock, CheckCircle2, Sparkles, Wand2 } from 'lucide-react'; + +const ICON_MAP = { + lightbulb: Lightbulb, + play: Play, + clock: Clock, + check: CheckCircle2, + sparkles: Sparkles, +} as const; + +interface EmptyStateCardProps { + columnId: string; + columnTitle?: string; + /** Keyboard shortcut for adding features (from settings) */ + addFeatureShortcut?: string; + /** Whether the column is empty due to active filters */ + isFilteredEmpty?: boolean; + /** Whether we're in read-only mode (hide actions) */ + isReadOnly?: boolean; + /** Called when user clicks "Use AI Suggestions" */ + onAiSuggest?: () => void; + /** Card opacity (matches board settings) */ + opacity?: number; + /** Enable glassmorphism effect */ + glassmorphism?: boolean; + /** Custom config override for pipeline steps */ + customConfig?: Partial; +} + +export const EmptyStateCard = memo(function EmptyStateCard({ + columnId, + addFeatureShortcut, + isFilteredEmpty = false, + isReadOnly = false, + onAiSuggest, + customConfig, +}: EmptyStateCardProps) { + // Get base config and merge with custom overrides + const baseConfig = getEmptyStateConfig(columnId); + const config: EmptyStateConfig = { ...baseConfig, ...customConfig }; + + const IconComponent = ICON_MAP[config.icon]; + const showActions = !isReadOnly && !isFilteredEmpty; + const showShortcut = columnId === 'backlog' && addFeatureShortcut && showActions; + + // Action button handler + const handlePrimaryAction = () => { + if (!config.primaryAction) return; + if (config.primaryAction.actionType === 'ai-suggest') { + onAiSuggest?.(); + } + }; + + return ( +
+ {/* Icon */} +
+ +
+ + {/* Title */} +

+ {isFilteredEmpty ? 'No Matching Items' : config.title} +

+ + {/* Description */} +

+ {isFilteredEmpty ? 'No features match your current filters.' : config.description} +

+ + {/* Keyboard shortcut hint for backlog */} + {showShortcut && ( +
+ Press + + {formatShortcut(addFeatureShortcut, true)} + + to add +
+ )} + + {/* AI Suggest action for backlog */} + {showActions && config.primaryAction && config.primaryAction.actionType === 'ai-suggest' && ( + + )} + + {/* Filtered empty state hint */} + {isFilteredEmpty && ( +

+ Clear filters to see all items +

+ )} +
+ ); +}); diff --git a/apps/ui/src/components/views/board-view/components/index.ts b/apps/ui/src/components/views/board-view/components/index.ts index 514e407d..0288031d 100644 --- a/apps/ui/src/components/views/board-view/components/index.ts +++ b/apps/ui/src/components/views/board-view/components/index.ts @@ -1,3 +1,4 @@ export { KanbanCard } from './kanban-card/kanban-card'; export { KanbanColumn } from './kanban-column'; export { SelectionActionBar } from './selection-action-bar'; +export { EmptyStateCard } from './empty-state-card'; diff --git a/apps/ui/src/components/views/board-view/constants.ts b/apps/ui/src/components/views/board-view/constants.ts index 9302ea01..fda19ebf 100644 --- a/apps/ui/src/components/views/board-view/constants.ts +++ b/apps/ui/src/components/views/board-view/constants.ts @@ -3,6 +3,69 @@ import type { PipelineConfig, FeatureStatusWithPipeline } from '@automaker/types export type ColumnId = Feature['status']; +/** + * Empty state configuration for each column type + */ +export interface EmptyStateConfig { + title: string; + description: string; + icon: 'lightbulb' | 'play' | 'clock' | 'check' | 'sparkles'; + shortcutKey?: string; // Keyboard shortcut label (e.g., 'N', 'A') + shortcutHint?: string; // Human-readable shortcut hint + primaryAction?: { + label: string; + actionType: 'ai-suggest' | 'none'; + }; +} + +/** + * Default empty state configurations per column type + */ +export const EMPTY_STATE_CONFIGS: Record = { + backlog: { + title: 'Ready for Ideas', + description: + 'Add your first feature idea to get started using the button below, or let AI help generate ideas.', + icon: 'lightbulb', + shortcutHint: 'Press', + primaryAction: { + label: 'Use AI Suggestions', + actionType: 'none', + }, + }, + in_progress: { + title: 'Nothing in Progress', + description: 'Drag a feature from the backlog here or click implement to start working on it.', + icon: 'play', + }, + waiting_approval: { + title: 'No Items Awaiting Approval', + description: 'Features will appear here after implementation is complete and need your review.', + icon: 'clock', + }, + verified: { + title: 'No Verified Features', + description: 'Approved features will appear here. They can then be completed and archived.', + icon: 'check', + }, + // Pipeline step default configuration + pipeline_default: { + title: 'Pipeline Step Empty', + description: 'Features will flow through this step during the automated pipeline process.', + icon: 'sparkles', + }, +}; + +/** + * Get empty state config for a column, with fallback for pipeline columns + */ +export function getEmptyStateConfig(columnId: string): EmptyStateConfig { + if (columnId.startsWith('pipeline_')) { + return EMPTY_STATE_CONFIGS.pipeline_default; + } + return EMPTY_STATE_CONFIGS[columnId] || EMPTY_STATE_CONFIGS.default; +} + export interface Column { id: FeatureStatusWithPipeline; title: string; diff --git a/apps/ui/src/components/views/board-view/kanban-board.tsx b/apps/ui/src/components/views/board-view/kanban-board.tsx index 4601a70c..e6b229ee 100644 --- a/apps/ui/src/components/views/board-view/kanban-board.tsx +++ b/apps/ui/src/components/views/board-view/kanban-board.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { DndContext, DragOverlay } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { Button } from '@/components/ui/button'; -import { KanbanColumn, KanbanCard } from './components'; +import { KanbanColumn, KanbanCard, EmptyStateCard } from './components'; import { Feature, useAppStore, formatShortcut } from '@/store/app-store'; import { Archive, Settings2, CheckSquare, GripVertical, Plus } from 'lucide-react'; import { useResponsiveKanban } from '@/hooks/use-responsive-kanban'; @@ -51,6 +51,12 @@ interface KanbanBoardProps { selectedFeatureIds?: Set; onToggleFeatureSelection?: (featureId: string) => void; onToggleSelectionMode?: () => void; + // Empty state action props + onAiSuggest?: () => void; + /** Whether currently dragging (hides empty states during drag) */ + isDragging?: boolean; + /** Whether the board is in read-only mode */ + isReadOnly?: boolean; } export function KanbanBoard({ @@ -86,6 +92,9 @@ export function KanbanBoard({ selectedFeatureIds = new Set(), onToggleFeatureSelection, onToggleSelectionMode, + onAiSuggest, + isDragging = false, + isReadOnly = false, }: KanbanBoardProps) { // Generate columns including pipeline steps const columns = useMemo(() => getColumnsWithPipeline(pipelineConfig), [pipelineConfig]); @@ -211,6 +220,26 @@ export function KanbanBoard({ items={columnFeatures.map((f) => f.id)} strategy={verticalListSortingStrategy} > + {/* Empty state card when column has no features */} + {columnFeatures.length === 0 && !isDragging && ( + + )} {columnFeatures.map((feature, index) => { // Calculate shortcut key for in-progress cards (first 10 get 1-9, 0) let shortcutKey: string | undefined;