From e171b6a0491d8fdac5ea592e6e778225d9fd7cf4 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sun, 11 Jan 2026 12:03:52 +0100 Subject: [PATCH] feat: add empty state card component and integrate AI suggestion functionality - Introduced the EmptyStateCard component to display contextual guidance when columns are empty. - Enhanced the KanbanBoard and BoardView components to utilize the new EmptyStateCard for improved user experience. - Added AI suggestion functionality to the empty state configuration, allowing users to generate ideas directly from the backlog column. - Updated constants to define empty state configurations for various column types. --- apps/ui/src/components/views/board-view.tsx | 2 + .../components/empty-state-card.tsx | 217 ++++++++++++++++++ .../views/board-view/components/index.ts | 1 + .../components/views/board-view/constants.ts | 87 +++++++ .../views/board-view/kanban-board.tsx | 31 ++- 5 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 apps/ui/src/components/views/board-view/components/empty-state-card.tsx diff --git a/apps/ui/src/components/views/board-view.tsx b/apps/ui/src/components/views/board-view.tsx index 2b1e3591..e325d165 100644 --- a/apps/ui/src/components/views/board-view.tsx +++ b/apps/ui/src/components/views/board-view.tsx @@ -1287,6 +1287,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..eff9a9be --- /dev/null +++ b/apps/ui/src/components/views/board-view/components/empty-state-card.tsx @@ -0,0 +1,217 @@ +import { memo, useState } 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, + X, + Eye, + EyeOff, +} 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, + columnTitle, + addFeatureShortcut, + isFilteredEmpty = false, + isReadOnly = false, + onAiSuggest, + opacity = 100, + glassmorphism = true, + customConfig, +}: EmptyStateCardProps) { + const [isDismissed, setIsDismissed] = useState(false); + const [isMinimized, setIsMinimized] = useState(false); + + // Get base config and merge with custom overrides + const baseConfig = getEmptyStateConfig(columnId); + const config: EmptyStateConfig = { ...baseConfig, ...customConfig }; + + // Handle dismissal + if (isDismissed) { + return null; + } + + const IconComponent = ICON_MAP[config.icon]; + const showActions = !isReadOnly && !isFilteredEmpty; + const showShortcut = columnId === 'backlog' && addFeatureShortcut && showActions; + + // Minimized state - compact indicator + if (isMinimized) { + return ( + + ); + } + + // Action button handler + const handlePrimaryAction = () => { + if (!config.primaryAction) return; + if (config.primaryAction.actionType === 'ai-suggest') { + onAiSuggest?.(); + } + }; + + return ( +
+ {/* Background with opacity */} +
+ + {/* Dismiss/Minimize controls */} +
+ + +
+ +
+ {/* Header with icon */} +
+
+ +
+
+

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

+

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

+
+
+ + {/* Keyboard shortcut hint */} + {showShortcut && ( +
+ + {config.shortcutHint || 'Press'} + + + {formatShortcut(addFeatureShortcut, true)} + + to add a feature +
+ )} + + {/* Example card preview */} + {config.exampleCard && ( +
+
+ {config.exampleCard.category} +
+
+ {config.exampleCard.title} +
+
+ )} + + {/* Action buttons */} + {showActions && config.primaryAction && config.primaryAction.actionType !== 'none' && ( + + )} + + {/* 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..5faf4d84 100644 --- a/apps/ui/src/components/views/board-view/constants.ts +++ b/apps/ui/src/components/views/board-view/constants.ts @@ -3,6 +3,93 @@ 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'; + }; + exampleCard?: { + title: string; + category: string; + }; +} + +/** + * 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: 'ai-suggest', + }, + exampleCard: { + title: 'User Authentication', + category: 'Core Feature', + }, + }, + in_progress: { + title: 'Nothing in Progress', + description: 'Drag a feature from the backlog here or click implement to start working on it.', + icon: 'play', + exampleCard: { + title: 'Implementing feature...', + category: 'In Development', + }, + }, + waiting_approval: { + title: 'No Items Awaiting Approval', + description: 'Features will appear here after implementation is complete and need your review.', + icon: 'clock', + exampleCard: { + title: 'Ready for Review', + category: 'Completed', + }, + }, + verified: { + title: 'No Verified Features', + description: 'Approved features will appear here. They can then be completed and archived.', + icon: 'check', + exampleCard: { + title: 'Approved & Ready', + category: 'Verified', + }, + }, + // Pipeline step default configuration + pipeline_default: { + title: 'Pipeline Step Empty', + description: 'Features will flow through this step during the automated pipeline process.', + icon: 'sparkles', + exampleCard: { + title: 'Processing...', + category: 'Pipeline', + }, + }, +}; + +/** + * 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.backlog; +} + 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;