beads-kanban-ui 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +16 -222
  2. package/package.json +18 -55
  3. package/.designs/beads-kanban-ui-bj0.md +0 -73
  4. package/.designs/beads-kanban-ui-qxq.md +0 -144
  5. package/.designs/epic-support.md +0 -282
  6. package/.env.local.example +0 -2
  7. package/.eslintrc.json +0 -3
  8. package/.gitattributes +0 -3
  9. package/.github/workflows/release.yml +0 -123
  10. package/.history/README_20260121193710.md +0 -227
  11. package/.history/README_20260121193918.md +0 -227
  12. package/.history/README_20260121193921.md +0 -227
  13. package/.history/README_20260121193933.md +0 -227
  14. package/.history/README_20260121193934.md +0 -227
  15. package/.history/README_20260121193944.md +0 -227
  16. package/.history/README_20260121193953.md +0 -227
  17. package/.history/src/app/page_20260121133429.tsx +0 -134
  18. package/.history/src/app/page_20260121133928.tsx +0 -134
  19. package/.history/src/app/page_20260121144850.tsx +0 -138
  20. package/.history/src/app/page_20260121144854.tsx +0 -138
  21. package/.history/src/app/page_20260121144858.tsx +0 -138
  22. package/.history/src/app/page_20260121144902.tsx +0 -138
  23. package/.history/src/app/page_20260121144906.tsx +0 -138
  24. package/.history/src/app/page_20260121144911.tsx +0 -138
  25. package/.history/src/app/page_20260121144928.tsx +0 -138
  26. package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
  27. package/.playwright-mcp/beams-test.png +0 -0
  28. package/.playwright-mcp/card-verification.png +0 -0
  29. package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
  30. package/.playwright-mcp/dialog-width-test.png +0 -0
  31. package/.playwright-mcp/homepage.png +0 -0
  32. package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
  33. package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
  34. package/.playwright-mcp/morphing-dialog-open.png +0 -0
  35. package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
  36. package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
  37. package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
  38. package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
  39. package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
  40. package/.playwright-mcp/screenshot-after-click.png +0 -0
  41. package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
  42. package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
  43. package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
  44. package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
  45. package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
  46. package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
  47. package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
  48. package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
  49. package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
  50. package/Screenshots/bead-detail.png +0 -0
  51. package/Screenshots/dashboard.png +0 -0
  52. package/Screenshots/kanban-board.png +0 -0
  53. package/components.json +0 -27
  54. package/logo/logo.svg +0 -1
  55. package/next.config.js +0 -9
  56. package/npm/README.md +0 -37
  57. package/npm/package.json +0 -20
  58. package/postcss.config.js +0 -6
  59. package/public/logo.svg +0 -1
  60. package/restart.sh +0 -5
  61. package/server/Cargo.lock +0 -1685
  62. package/server/Cargo.toml +0 -24
  63. package/server/src/db.rs +0 -570
  64. package/server/src/main.rs +0 -141
  65. package/server/src/routes/beads.rs +0 -413
  66. package/server/src/routes/cli.rs +0 -150
  67. package/server/src/routes/fs.rs +0 -360
  68. package/server/src/routes/git.rs +0 -169
  69. package/server/src/routes/mod.rs +0 -107
  70. package/server/src/routes/projects.rs +0 -177
  71. package/server/src/routes/watch.rs +0 -211
  72. package/src/app/globals.css +0 -101
  73. package/src/app/layout.tsx +0 -36
  74. package/src/app/page.tsx +0 -348
  75. package/src/app/project/kanban-board.tsx +0 -356
  76. package/src/app/project/page.tsx +0 -18
  77. package/src/app/settings/page.tsx +0 -224
  78. package/src/components/Beams.css +0 -5
  79. package/src/components/Beams.jsx +0 -307
  80. package/src/components/Galaxy.css +0 -5
  81. package/src/components/Galaxy.jsx +0 -333
  82. package/src/components/activity-timeline.tsx +0 -172
  83. package/src/components/add-project-dialog.tsx +0 -219
  84. package/src/components/bead-card.tsx +0 -196
  85. package/src/components/bead-detail.tsx +0 -306
  86. package/src/components/color-picker.tsx +0 -101
  87. package/src/components/comment-input.tsx +0 -155
  88. package/src/components/comment-list.tsx +0 -147
  89. package/src/components/dependency-badge.tsx +0 -106
  90. package/src/components/design-doc-dialog.tsx +0 -58
  91. package/src/components/design-doc-preview.tsx +0 -97
  92. package/src/components/design-doc-viewer.tsx +0 -199
  93. package/src/components/editable-project-name.tsx +0 -178
  94. package/src/components/epic-card.tsx +0 -263
  95. package/src/components/folder-browser.tsx +0 -273
  96. package/src/components/footer.tsx +0 -27
  97. package/src/components/kanban/default.tsx +0 -184
  98. package/src/components/kanban-column.tsx +0 -167
  99. package/src/components/project-card.tsx +0 -191
  100. package/src/components/quick-filter-bar.tsx +0 -279
  101. package/src/components/scan-directory-dialog.tsx +0 -368
  102. package/src/components/status-donut.tsx +0 -197
  103. package/src/components/subtask-list.tsx +0 -128
  104. package/src/components/tag-picker.tsx +0 -252
  105. package/src/components/ui/.gitkeep +0 -0
  106. package/src/components/ui/alert-dialog.tsx +0 -141
  107. package/src/components/ui/avatar.tsx +0 -67
  108. package/src/components/ui/badge.tsx +0 -230
  109. package/src/components/ui/button.tsx +0 -433
  110. package/src/components/ui/card/index.tsx +0 -24
  111. package/src/components/ui/card/roiui-card.module.css +0 -197
  112. package/src/components/ui/card/roiui-card.tsx +0 -154
  113. package/src/components/ui/card/shadcn-card.tsx +0 -76
  114. package/src/components/ui/chart.tsx +0 -369
  115. package/src/components/ui/dialog.tsx +0 -122
  116. package/src/components/ui/dropdown-menu.tsx +0 -201
  117. package/src/components/ui/input.tsx +0 -22
  118. package/src/components/ui/kanban.tsx +0 -522
  119. package/src/components/ui/morphing-dialog.tsx +0 -457
  120. package/src/components/ui/popover.tsx +0 -33
  121. package/src/components/ui/progress.tsx +0 -28
  122. package/src/components/ui/scroll-area.tsx +0 -48
  123. package/src/components/ui/select.tsx +0 -159
  124. package/src/components/ui/separator.tsx +0 -31
  125. package/src/components/ui/sheet.tsx +0 -142
  126. package/src/components/ui/skeleton.tsx +0 -15
  127. package/src/components/ui/toast.tsx +0 -129
  128. package/src/components/ui/toaster.tsx +0 -35
  129. package/src/components/ui/tooltip.tsx +0 -30
  130. package/src/hooks/.gitkeep +0 -0
  131. package/src/hooks/use-bead-filters.ts +0 -261
  132. package/src/hooks/use-beads.ts +0 -162
  133. package/src/hooks/use-branch-statuses.ts +0 -161
  134. package/src/hooks/use-epics.ts +0 -173
  135. package/src/hooks/use-file-watcher.ts +0 -111
  136. package/src/hooks/use-keyboard-navigation.ts +0 -282
  137. package/src/hooks/use-project.ts +0 -61
  138. package/src/hooks/use-projects.ts +0 -93
  139. package/src/hooks/use-toast.ts +0 -194
  140. package/src/hooks/useClickOutside.tsx +0 -26
  141. package/src/lib/.gitkeep +0 -0
  142. package/src/lib/api.ts +0 -186
  143. package/src/lib/beads-parser.ts +0 -252
  144. package/src/lib/cli.ts +0 -193
  145. package/src/lib/db.ts +0 -145
  146. package/src/lib/design-doc.ts +0 -74
  147. package/src/lib/epic-parser.ts +0 -242
  148. package/src/lib/git.ts +0 -102
  149. package/src/lib/utils.ts +0 -12
  150. package/src/types/index.ts +0 -107
  151. package/tailwind.config.ts +0 -85
  152. package/tsconfig.json +0 -26
  153. /package/{npm/bin → bin}/cli.js +0 -0
  154. /package/{npm/scripts → scripts}/postinstall.js +0 -0
@@ -1,191 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { useRouter } from "next/navigation";
5
- import { StatusDonut } from "@/components/status-donut";
6
- import { RoiuiCard } from "@/components/ui/card";
7
- import { Badge } from "@/components/ui/badge";
8
- import { Button } from "@/components/ui/button";
9
- import {
10
- DropdownMenu,
11
- DropdownMenuContent,
12
- DropdownMenuItem,
13
- DropdownMenuTrigger,
14
- } from "@/components/ui/dropdown-menu";
15
- import { TagPicker } from "@/components/tag-picker";
16
- import { useToast } from "@/hooks/use-toast";
17
- import * as api from "@/lib/api";
18
- import { ExternalLink, Code, FolderOpen, Loader2 } from "lucide-react";
19
- import type { Tag } from "@/lib/db";
20
- import type { BeadCounts } from "@/types";
21
-
22
- /**
23
- * Converts kebab-case, snake_case, camelCase to Title Case with spaces
24
- */
25
- function formatProjectName(name: string): string {
26
- return name
27
- .replace(/[-_]/g, ' ') // Replace hyphens and underscores with spaces
28
- .replace(/([a-z])([A-Z])/g, '$1 $2') // Add space before capitals in camelCase
29
- .replace(/\b\w/g, c => c.toUpperCase()); // Capitalize first letter of each word
30
- }
31
-
32
- interface ProjectCardProps {
33
- id: string;
34
- name: string;
35
- path: string;
36
- tags: Tag[];
37
- beadCounts?: BeadCounts;
38
- onTagsChange?: (tags: Tag[]) => void;
39
- }
40
-
41
- export function ProjectCard({
42
- id,
43
- name,
44
- path,
45
- tags,
46
- beadCounts = { open: 0, in_progress: 0, inreview: 0, closed: 0 },
47
- onTagsChange,
48
- }: ProjectCardProps) {
49
- const router = useRouter();
50
- const [isOpening, setIsOpening] = useState<string | null>(null);
51
- const { toast } = useToast();
52
-
53
- const handleOpenExternal = async (target: 'vscode' | 'cursor' | 'finder', e: React.MouseEvent) => {
54
- e.stopPropagation();
55
- setIsOpening(target);
56
-
57
- try {
58
- await api.fs.openExternal(path, target);
59
- toast({
60
- title: "Opening project",
61
- description: target === 'finder'
62
- ? "Opening in Finder..."
63
- : `Opening in ${target === 'vscode' ? 'VS Code' : 'Cursor'}...`,
64
- });
65
- } catch (err) {
66
- console.error("Error opening project:", err);
67
- toast({
68
- title: "Failed to open",
69
- description: err instanceof Error ? err.message : "Could not open the project. Make sure the application is installed.",
70
- variant: "destructive",
71
- });
72
- } finally {
73
- setIsOpening(null);
74
- }
75
- };
76
-
77
- const handleCardClick = () => {
78
- router.push(`/project?id=${id}`);
79
- };
80
-
81
- return (
82
- <RoiuiCard
83
- className="cursor-pointer flex flex-col min-h-[155px]"
84
- onClick={handleCardClick}
85
- role="link"
86
- tabIndex={0}
87
- aria-label={`View ${formatProjectName(name)} project`}
88
- onKeyDown={(e) => {
89
- if (e.key === "Enter" || e.key === " ") {
90
- e.preventDefault();
91
- handleCardClick();
92
- }
93
- }}
94
- >
95
- {/* Top row: Donut left, Tags right */}
96
- <div className="flex items-start justify-between">
97
- <StatusDonut beadCounts={beadCounts} size={36} />
98
- <div
99
- className="flex flex-wrap items-center gap-1.5"
100
- onClick={(e) => e.stopPropagation()}
101
- onKeyDown={(e) => e.stopPropagation()}
102
- >
103
- {tags.map((tag) => (
104
- <Badge
105
- key={tag.id}
106
- variant="secondary"
107
- size="sm"
108
- style={{
109
- backgroundColor: `${tag.color}20`,
110
- color: tag.color,
111
- borderColor: tag.color,
112
- }}
113
- >
114
- {tag.name}
115
- </Badge>
116
- ))}
117
- {onTagsChange && (
118
- <TagPicker
119
- projectId={id}
120
- projectTags={tags}
121
- onTagsChange={onTagsChange}
122
- />
123
- )}
124
- </div>
125
- </div>
126
-
127
- {/* Middle: Title (grows to fill space) */}
128
- <div className="flex-1 flex items-center">
129
- <h3 className="text-xl font-medium text-balance font-project-name">
130
- {formatProjectName(name)}
131
- </h3>
132
- </div>
133
-
134
- {/* Bottom row: Path left, Open In button right (aligned) */}
135
- <div className="flex items-center justify-between gap-2">
136
- <p className="text-sm text-zinc-500 truncate min-w-0 flex-1" title={path}>
137
- {path}
138
- </p>
139
- <DropdownMenu>
140
- <DropdownMenuTrigger asChild>
141
- <Button
142
- variant="ghost"
143
- size="sm"
144
- mode="icon"
145
- className="shrink-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
146
- aria-label="Open in external application"
147
- onClick={(e) => e.stopPropagation()}
148
- >
149
- <ExternalLink className="h-4 w-4" />
150
- </Button>
151
- </DropdownMenuTrigger>
152
- <DropdownMenuContent align="end">
153
- <DropdownMenuItem
154
- onClick={(e) => handleOpenExternal('vscode', e)}
155
- disabled={isOpening !== null}
156
- >
157
- {isOpening === 'vscode' ? (
158
- <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
159
- ) : (
160
- <Code className="h-4 w-4" aria-hidden="true" />
161
- )}
162
- VS Code
163
- </DropdownMenuItem>
164
- <DropdownMenuItem
165
- onClick={(e) => handleOpenExternal('cursor', e)}
166
- disabled={isOpening !== null}
167
- >
168
- {isOpening === 'cursor' ? (
169
- <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
170
- ) : (
171
- <Code className="h-4 w-4" aria-hidden="true" />
172
- )}
173
- Cursor
174
- </DropdownMenuItem>
175
- <DropdownMenuItem
176
- onClick={(e) => handleOpenExternal('finder', e)}
177
- disabled={isOpening !== null}
178
- >
179
- {isOpening === 'finder' ? (
180
- <Loader2 className="h-4 w-4 animate-spin" aria-hidden="true" />
181
- ) : (
182
- <FolderOpen className="h-4 w-4" aria-hidden="true" />
183
- )}
184
- Finder
185
- </DropdownMenuItem>
186
- </DropdownMenuContent>
187
- </DropdownMenu>
188
- </div>
189
- </RoiuiCard>
190
- );
191
- }
@@ -1,279 +0,0 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import { Search, X, ArrowUpDown, SlidersHorizontal } from 'lucide-react';
5
- import { Button } from '@/components/ui/button';
6
- import { Input } from '@/components/ui/input';
7
- import {
8
- DropdownMenu,
9
- DropdownMenuTrigger,
10
- DropdownMenuContent,
11
- DropdownMenuLabel,
12
- DropdownMenuSeparator,
13
- DropdownMenuCheckboxItem,
14
- DropdownMenuItem,
15
- } from '@/components/ui/dropdown-menu';
16
- import { cn } from '@/lib/utils';
17
- import type { BeadStatus } from '@/types';
18
-
19
- type TypeFilter = 'all' | 'epics' | 'tasks';
20
- type SortField = 'ticket_number' | 'created_at';
21
- type SortDirection = 'asc' | 'desc';
22
-
23
- interface QuickFilterBarProps {
24
- /** Issue type filter: all, epics, or tasks */
25
- typeFilter: TypeFilter;
26
- /** Callback when type filter changes */
27
- onTypeFilterChange: (type: TypeFilter) => void;
28
- /** Whether to show only today's active items */
29
- todayOnly: boolean;
30
- /** Callback when today's active toggle changes */
31
- onTodayOnlyChange: (value: boolean) => void;
32
- /** Field to sort by */
33
- sortField: SortField;
34
- /** Sort direction */
35
- sortDirection: SortDirection;
36
- /** Callback when sort changes */
37
- onSortChange: (field: SortField, direction: SortDirection) => void;
38
- /** Search query */
39
- search: string;
40
- /** Callback when search changes */
41
- onSearchChange: (value: string) => void;
42
- /** Ref for the search input (keyboard navigation) */
43
- searchInputRef?: React.RefObject<HTMLInputElement>;
44
- /** Active status filters */
45
- statuses: BeadStatus[];
46
- /** Callback when status filter toggles */
47
- onStatusToggle: (status: BeadStatus) => void;
48
- /** Active owner filters */
49
- owners: string[];
50
- /** Callback when owner filter toggles */
51
- onOwnerToggle: (owner: string) => void;
52
- /** List of available owners */
53
- availableOwners: string[];
54
- /** Callback to clear all filters */
55
- onClearFilters: () => void;
56
- /** Whether any filters are active */
57
- hasActiveFilters: boolean;
58
- }
59
-
60
- const TYPE_OPTIONS: { value: TypeFilter; label: string }[] = [
61
- { value: 'all', label: 'All' },
62
- { value: 'epics', label: 'Epics' },
63
- { value: 'tasks', label: 'Tasks' },
64
- ];
65
-
66
- const SORT_OPTIONS: { value: string; label: string; field: SortField; direction: SortDirection }[] = [
67
- { value: 'ticket_number_desc', label: 'Ticket # (Newest)', field: 'ticket_number', direction: 'desc' },
68
- { value: 'ticket_number_asc', label: 'Ticket # (Oldest)', field: 'ticket_number', direction: 'asc' },
69
- { value: 'created_at_desc', label: 'Updated (Newest)', field: 'created_at', direction: 'desc' },
70
- { value: 'created_at_asc', label: 'Updated (Oldest)', field: 'created_at', direction: 'asc' },
71
- ];
72
-
73
- const STATUS_OPTIONS: { value: BeadStatus; label: string }[] = [
74
- { value: 'open', label: 'Open' },
75
- { value: 'in_progress', label: 'In Progress' },
76
- { value: 'inreview', label: 'In Review' },
77
- { value: 'closed', label: 'Closed' },
78
- ];
79
-
80
- /**
81
- * QuickFilterBar provides quick access to common filter and sort operations
82
- * for the kanban board. Displays below the header as a horizontal bar.
83
- */
84
- export function QuickFilterBar({
85
- typeFilter,
86
- onTypeFilterChange,
87
- todayOnly,
88
- onTodayOnlyChange,
89
- sortField,
90
- sortDirection,
91
- onSortChange,
92
- search,
93
- onSearchChange,
94
- searchInputRef,
95
- statuses,
96
- onStatusToggle,
97
- owners,
98
- onOwnerToggle,
99
- availableOwners,
100
- onClearFilters,
101
- hasActiveFilters,
102
- }: QuickFilterBarProps) {
103
- const currentSortValue = `${sortField}_${sortDirection}`;
104
-
105
- const handleSortOptionSelect = (value: string) => {
106
- const option = SORT_OPTIONS.find((opt) => opt.value === value);
107
- if (option) {
108
- onSortChange(option.field, option.direction);
109
- }
110
- };
111
-
112
- return (
113
- <div
114
- role="toolbar"
115
- aria-label="Quick filters"
116
- className="flex items-center gap-3 bg-zinc-900/50 border border-zinc-800 rounded-lg px-3 py-2"
117
- >
118
- {/* Search Input */}
119
- <div className="relative">
120
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-zinc-500" aria-hidden="true" />
121
- <Input
122
- ref={searchInputRef}
123
- type="text"
124
- aria-label="Search beads"
125
- placeholder="Search… (/)"
126
- value={search}
127
- onChange={(e) => onSearchChange(e.target.value)}
128
- className="pl-8 pr-8 w-[180px] h-8 bg-zinc-800/50 border-zinc-700 text-zinc-100 placeholder:text-zinc-500"
129
- />
130
- {search && (
131
- <button
132
- type="button"
133
- onClick={() => onSearchChange('')}
134
- className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 -m-1.5 text-zinc-500 hover:text-zinc-300"
135
- aria-label="Clear search"
136
- >
137
- <X className="h-3.5 w-3.5" />
138
- </button>
139
- )}
140
- </div>
141
-
142
- {/* Type Filter - Segmented Control */}
143
- <div
144
- role="radiogroup"
145
- aria-label="Filter by issue type"
146
- className="flex items-center bg-zinc-800/50 rounded-md p-0.5"
147
- >
148
- {TYPE_OPTIONS.map((option) => (
149
- <button
150
- key={option.value}
151
- type="button"
152
- role="radio"
153
- aria-checked={typeFilter === option.value}
154
- onClick={() => onTypeFilterChange(option.value)}
155
- className={cn(
156
- 'px-3 py-1.5 text-sm font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900',
157
- typeFilter === option.value
158
- ? 'bg-zinc-700 text-zinc-100'
159
- : 'bg-transparent text-zinc-300 hover:text-zinc-200'
160
- )}
161
- >
162
- {option.label}
163
- </button>
164
- ))}
165
- </div>
166
-
167
- {/* Today's Active Toggle */}
168
- <Button
169
- variant="outline"
170
- size="sm"
171
- onClick={() => onTodayOnlyChange(!todayOnly)}
172
- aria-pressed={todayOnly}
173
- className={cn(
174
- 'h-8 transition-colors',
175
- todayOnly
176
- ? 'bg-purple-500/20 text-purple-400 border-purple-500/30 hover:bg-purple-500/30'
177
- : 'border-zinc-700 text-zinc-400 hover:text-zinc-200 hover:border-zinc-600'
178
- )}
179
- >
180
- Today
181
- </Button>
182
-
183
- {/* Spacer to push sort and filter to the right */}
184
- <div className="flex-1" />
185
-
186
- {/* Sort Icon Menu */}
187
- <DropdownMenu>
188
- <DropdownMenuTrigger asChild>
189
- <Button
190
- variant="ghost"
191
- size="sm"
192
- className="h-8 px-2 text-zinc-400 hover:text-zinc-100"
193
- aria-label="Sort options"
194
- >
195
- <ArrowUpDown className="h-4 w-4" />
196
- </Button>
197
- </DropdownMenuTrigger>
198
- <DropdownMenuContent align="end" className="bg-zinc-900 border-zinc-800">
199
- <DropdownMenuLabel className="text-zinc-400">Sort by</DropdownMenuLabel>
200
- <DropdownMenuSeparator className="bg-zinc-800" />
201
- {SORT_OPTIONS.map((option) => (
202
- <DropdownMenuCheckboxItem
203
- key={option.value}
204
- checked={currentSortValue === option.value}
205
- onCheckedChange={() => handleSortOptionSelect(option.value)}
206
- className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100"
207
- >
208
- {option.label}
209
- </DropdownMenuCheckboxItem>
210
- ))}
211
- </DropdownMenuContent>
212
- </DropdownMenu>
213
-
214
- {/* Filter Icon Menu */}
215
- <DropdownMenu>
216
- <DropdownMenuTrigger asChild>
217
- <Button
218
- variant="ghost"
219
- size="sm"
220
- className={cn(
221
- 'h-8 px-2',
222
- hasActiveFilters ? 'text-purple-400' : 'text-zinc-400 hover:text-zinc-100'
223
- )}
224
- aria-label="Filter options"
225
- >
226
- <SlidersHorizontal className="h-4 w-4" />
227
- {hasActiveFilters && <span className="ml-1 text-xs" aria-hidden="true">•</span>}
228
- </Button>
229
- </DropdownMenuTrigger>
230
- <DropdownMenuContent align="end" className="w-56 bg-zinc-900 border-zinc-800">
231
- <DropdownMenuLabel className="text-zinc-400">Status</DropdownMenuLabel>
232
- <DropdownMenuSeparator className="bg-zinc-800" />
233
- {STATUS_OPTIONS.map((option) => (
234
- <DropdownMenuCheckboxItem
235
- key={option.value}
236
- checked={statuses.includes(option.value)}
237
- onCheckedChange={() => onStatusToggle(option.value)}
238
- className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100"
239
- >
240
- {option.label}
241
- </DropdownMenuCheckboxItem>
242
- ))}
243
-
244
- {availableOwners.length > 0 && (
245
- <>
246
- <DropdownMenuSeparator className="bg-zinc-800" />
247
- <DropdownMenuLabel className="text-zinc-400">Owner</DropdownMenuLabel>
248
- <DropdownMenuSeparator className="bg-zinc-800" />
249
- {availableOwners.map((owner) => (
250
- <DropdownMenuCheckboxItem
251
- key={owner}
252
- checked={owners.includes(owner)}
253
- onCheckedChange={() => onOwnerToggle(owner)}
254
- className="text-zinc-200 focus:bg-zinc-800 focus:text-zinc-100"
255
- >
256
- {owner}
257
- </DropdownMenuCheckboxItem>
258
- ))}
259
- </>
260
- )}
261
-
262
- {hasActiveFilters && (
263
- <>
264
- <DropdownMenuSeparator className="bg-zinc-800" />
265
- <DropdownMenuItem
266
- onClick={onClearFilters}
267
- className="text-red-400 focus:bg-zinc-800 focus:text-red-400"
268
- >
269
- Clear filters
270
- </DropdownMenuItem>
271
- </>
272
- )}
273
- </DropdownMenuContent>
274
- </DropdownMenu>
275
- </div>
276
- );
277
- }
278
-
279
- export type { QuickFilterBarProps, TypeFilter, SortField, SortDirection };