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.
- package/README.md +16 -222
- package/package.json +18 -55
- package/.designs/beads-kanban-ui-bj0.md +0 -73
- package/.designs/beads-kanban-ui-qxq.md +0 -144
- package/.designs/epic-support.md +0 -282
- package/.env.local.example +0 -2
- package/.eslintrc.json +0 -3
- package/.gitattributes +0 -3
- package/.github/workflows/release.yml +0 -123
- package/.history/README_20260121193710.md +0 -227
- package/.history/README_20260121193918.md +0 -227
- package/.history/README_20260121193921.md +0 -227
- package/.history/README_20260121193933.md +0 -227
- package/.history/README_20260121193934.md +0 -227
- package/.history/README_20260121193944.md +0 -227
- package/.history/README_20260121193953.md +0 -227
- package/.history/src/app/page_20260121133429.tsx +0 -134
- package/.history/src/app/page_20260121133928.tsx +0 -134
- package/.history/src/app/page_20260121144850.tsx +0 -138
- package/.history/src/app/page_20260121144854.tsx +0 -138
- package/.history/src/app/page_20260121144858.tsx +0 -138
- package/.history/src/app/page_20260121144902.tsx +0 -138
- package/.history/src/app/page_20260121144906.tsx +0 -138
- package/.history/src/app/page_20260121144911.tsx +0 -138
- package/.history/src/app/page_20260121144928.tsx +0 -138
- package/.playwright-mcp/.playwright-mcp/morphing-dialog-wheel-scroll-fix.png +0 -0
- package/.playwright-mcp/beams-test.png +0 -0
- package/.playwright-mcp/card-verification.png +0 -0
- package/.playwright-mcp/design-doc-dialog-fix-verification.png +0 -0
- package/.playwright-mcp/dialog-width-test.png +0 -0
- package/.playwright-mcp/homepage.png +0 -0
- package/.playwright-mcp/morphing-dialog-expanded.png +0 -0
- package/.playwright-mcp/morphing-dialog-fixes-final.png +0 -0
- package/.playwright-mcp/morphing-dialog-open.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-08-31-529Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-09-23-431Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-28-773Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-10-47-432Z.png +0 -0
- package/.playwright-mcp/page-2026-01-21T14-11-12-350Z.png +0 -0
- package/.playwright-mcp/screenshot-after-click.png +0 -0
- package/.playwright-mcp/screenshot-after-dialog-click.png +0 -0
- package/.playwright-mcp/sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/test-1-sheet-open-with-overlay.png +0 -0
- package/.playwright-mcp/test-2-morphing-dialog-with-overlay.png +0 -0
- package/.playwright-mcp/test-3-sheet-open-dark-overlay.png +0 -0
- package/.playwright-mcp/test-4-morphing-dialog-with-dark-overlay.png +0 -0
- package/.playwright-mcp/test-5-morphing-dialog-scrolled.png +0 -0
- package/.playwright-mcp/test-6-sheet-restored-after-dialog-close.png +0 -0
- package/.playwright-mcp/wheel-scroll-fixed.png +0 -0
- package/Screenshots/bead-detail.png +0 -0
- package/Screenshots/dashboard.png +0 -0
- package/Screenshots/kanban-board.png +0 -0
- package/components.json +0 -27
- package/logo/logo.svg +0 -1
- package/next.config.js +0 -9
- package/npm/README.md +0 -37
- package/npm/package.json +0 -20
- package/postcss.config.js +0 -6
- package/public/logo.svg +0 -1
- package/restart.sh +0 -5
- package/server/Cargo.lock +0 -1685
- package/server/Cargo.toml +0 -24
- package/server/src/db.rs +0 -570
- package/server/src/main.rs +0 -141
- package/server/src/routes/beads.rs +0 -413
- package/server/src/routes/cli.rs +0 -150
- package/server/src/routes/fs.rs +0 -360
- package/server/src/routes/git.rs +0 -169
- package/server/src/routes/mod.rs +0 -107
- package/server/src/routes/projects.rs +0 -177
- package/server/src/routes/watch.rs +0 -211
- package/src/app/globals.css +0 -101
- package/src/app/layout.tsx +0 -36
- package/src/app/page.tsx +0 -348
- package/src/app/project/kanban-board.tsx +0 -356
- package/src/app/project/page.tsx +0 -18
- package/src/app/settings/page.tsx +0 -224
- package/src/components/Beams.css +0 -5
- package/src/components/Beams.jsx +0 -307
- package/src/components/Galaxy.css +0 -5
- package/src/components/Galaxy.jsx +0 -333
- package/src/components/activity-timeline.tsx +0 -172
- package/src/components/add-project-dialog.tsx +0 -219
- package/src/components/bead-card.tsx +0 -196
- package/src/components/bead-detail.tsx +0 -306
- package/src/components/color-picker.tsx +0 -101
- package/src/components/comment-input.tsx +0 -155
- package/src/components/comment-list.tsx +0 -147
- package/src/components/dependency-badge.tsx +0 -106
- package/src/components/design-doc-dialog.tsx +0 -58
- package/src/components/design-doc-preview.tsx +0 -97
- package/src/components/design-doc-viewer.tsx +0 -199
- package/src/components/editable-project-name.tsx +0 -178
- package/src/components/epic-card.tsx +0 -263
- package/src/components/folder-browser.tsx +0 -273
- package/src/components/footer.tsx +0 -27
- package/src/components/kanban/default.tsx +0 -184
- package/src/components/kanban-column.tsx +0 -167
- package/src/components/project-card.tsx +0 -191
- package/src/components/quick-filter-bar.tsx +0 -279
- package/src/components/scan-directory-dialog.tsx +0 -368
- package/src/components/status-donut.tsx +0 -197
- package/src/components/subtask-list.tsx +0 -128
- package/src/components/tag-picker.tsx +0 -252
- package/src/components/ui/.gitkeep +0 -0
- package/src/components/ui/alert-dialog.tsx +0 -141
- package/src/components/ui/avatar.tsx +0 -67
- package/src/components/ui/badge.tsx +0 -230
- package/src/components/ui/button.tsx +0 -433
- package/src/components/ui/card/index.tsx +0 -24
- package/src/components/ui/card/roiui-card.module.css +0 -197
- package/src/components/ui/card/roiui-card.tsx +0 -154
- package/src/components/ui/card/shadcn-card.tsx +0 -76
- package/src/components/ui/chart.tsx +0 -369
- package/src/components/ui/dialog.tsx +0 -122
- package/src/components/ui/dropdown-menu.tsx +0 -201
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/kanban.tsx +0 -522
- package/src/components/ui/morphing-dialog.tsx +0 -457
- package/src/components/ui/popover.tsx +0 -33
- package/src/components/ui/progress.tsx +0 -28
- package/src/components/ui/scroll-area.tsx +0 -48
- package/src/components/ui/select.tsx +0 -159
- package/src/components/ui/separator.tsx +0 -31
- package/src/components/ui/sheet.tsx +0 -142
- package/src/components/ui/skeleton.tsx +0 -15
- package/src/components/ui/toast.tsx +0 -129
- package/src/components/ui/toaster.tsx +0 -35
- package/src/components/ui/tooltip.tsx +0 -30
- package/src/hooks/.gitkeep +0 -0
- package/src/hooks/use-bead-filters.ts +0 -261
- package/src/hooks/use-beads.ts +0 -162
- package/src/hooks/use-branch-statuses.ts +0 -161
- package/src/hooks/use-epics.ts +0 -173
- package/src/hooks/use-file-watcher.ts +0 -111
- package/src/hooks/use-keyboard-navigation.ts +0 -282
- package/src/hooks/use-project.ts +0 -61
- package/src/hooks/use-projects.ts +0 -93
- package/src/hooks/use-toast.ts +0 -194
- package/src/hooks/useClickOutside.tsx +0 -26
- package/src/lib/.gitkeep +0 -0
- package/src/lib/api.ts +0 -186
- package/src/lib/beads-parser.ts +0 -252
- package/src/lib/cli.ts +0 -193
- package/src/lib/db.ts +0 -145
- package/src/lib/design-doc.ts +0 -74
- package/src/lib/epic-parser.ts +0 -242
- package/src/lib/git.ts +0 -102
- package/src/lib/utils.ts +0 -12
- package/src/types/index.ts +0 -107
- package/tailwind.config.ts +0 -85
- package/tsconfig.json +0 -26
- /package/{npm/bin → bin}/cli.js +0 -0
- /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 };
|