@locusai/web 0.1.0
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/LICENSE +21 -0
- package/next.config.js +7 -0
- package/package.json +37 -0
- package/postcss.config.mjs +5 -0
- package/src/app/backlog/page.tsx +19 -0
- package/src/app/docs/page.tsx +7 -0
- package/src/app/globals.css +603 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/page.tsx +16 -0
- package/src/app/providers.tsx +16 -0
- package/src/app/settings/page.tsx +194 -0
- package/src/components/BoardFilter.tsx +98 -0
- package/src/components/Header.tsx +21 -0
- package/src/components/PropertyItem.tsx +98 -0
- package/src/components/Sidebar.tsx +109 -0
- package/src/components/TaskCard.tsx +138 -0
- package/src/components/TaskCreateModal.tsx +243 -0
- package/src/components/TaskPanel.tsx +765 -0
- package/src/components/index.ts +7 -0
- package/src/components/ui/Badge.tsx +77 -0
- package/src/components/ui/Button.tsx +47 -0
- package/src/components/ui/Checkbox.tsx +52 -0
- package/src/components/ui/Dropdown.tsx +107 -0
- package/src/components/ui/Input.tsx +36 -0
- package/src/components/ui/Modal.tsx +79 -0
- package/src/components/ui/Textarea.tsx +21 -0
- package/src/components/ui/index.ts +7 -0
- package/src/hooks/useTasks.ts +119 -0
- package/src/lib/api-client.ts +24 -0
- package/src/lib/utils.ts +6 -0
- package/src/services/doc.service.ts +27 -0
- package/src/services/index.ts +3 -0
- package/src/services/sprint.service.ts +26 -0
- package/src/services/task.service.ts +75 -0
- package/src/views/Backlog.tsx +691 -0
- package/src/views/Board.tsx +306 -0
- package/src/views/Docs.tsx +625 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Roboto } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
import { Header } from "@/components/Header";
|
|
5
|
+
import { Sidebar } from "@/components/Sidebar";
|
|
6
|
+
import { Providers } from "./providers";
|
|
7
|
+
|
|
8
|
+
const roboto = Roboto({
|
|
9
|
+
weight: ["300", "400", "500", "700"],
|
|
10
|
+
subsets: ["latin"],
|
|
11
|
+
display: "swap",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = {
|
|
15
|
+
title: "Locus | Engineering Workspace",
|
|
16
|
+
description:
|
|
17
|
+
"Modernized task management and documentation for engineering teams.",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html lang="en" className="dark">
|
|
27
|
+
<body className={`${roboto.className} antialiased`}>
|
|
28
|
+
<Providers>
|
|
29
|
+
<div className="flex h-screen overflow-hidden bg-background">
|
|
30
|
+
<Sidebar />
|
|
31
|
+
|
|
32
|
+
<main className="flex-1 overflow-auto bg-background p-8">
|
|
33
|
+
<div className="max-w-[1440px] mx-auto">
|
|
34
|
+
<Header />
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
</main>
|
|
38
|
+
</div>
|
|
39
|
+
</Providers>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
|
42
|
+
);
|
|
43
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import dynamic from "next/dynamic";
|
|
4
|
+
|
|
5
|
+
const Board = dynamic(() => import("@/views/Board").then((mod) => mod.Board), {
|
|
6
|
+
ssr: false,
|
|
7
|
+
loading: () => (
|
|
8
|
+
<div className="flex-1 flex items-center justify-center">
|
|
9
|
+
<div className="animate-pulse text-muted-foreground">Loading...</div>
|
|
10
|
+
</div>
|
|
11
|
+
),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export default function Home() {
|
|
15
|
+
return <Board />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
4
|
+
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
|
|
7
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const [queryClient] = useState(() => new QueryClient({}));
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<QueryClientProvider client={queryClient}>
|
|
12
|
+
{children}
|
|
13
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
14
|
+
</QueryClientProvider>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Bell,
|
|
5
|
+
Globe,
|
|
6
|
+
Moon,
|
|
7
|
+
Palette,
|
|
8
|
+
Shield,
|
|
9
|
+
Sun,
|
|
10
|
+
User,
|
|
11
|
+
Zap,
|
|
12
|
+
} from "lucide-react";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { Button } from "@/components/ui";
|
|
15
|
+
|
|
16
|
+
interface SettingItemProps {
|
|
17
|
+
icon: React.ReactNode;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function SettingItem({ icon, title, description, children }: SettingItemProps) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex items-start justify-between p-4 rounded-xl hover:bg-secondary/30 transition-colors">
|
|
26
|
+
<div className="flex gap-4">
|
|
27
|
+
<div className="w-10 h-10 rounded-xl bg-secondary flex items-center justify-center text-muted-foreground shrink-0">
|
|
28
|
+
{icon}
|
|
29
|
+
</div>
|
|
30
|
+
<div>
|
|
31
|
+
<h4 className="font-medium text-foreground">{title}</h4>
|
|
32
|
+
<p className="text-sm text-muted-foreground mt-0.5">{description}</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="shrink-0">{children}</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function Toggle({
|
|
41
|
+
checked,
|
|
42
|
+
onChange,
|
|
43
|
+
}: {
|
|
44
|
+
checked: boolean;
|
|
45
|
+
onChange: (checked: boolean) => void;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
role="switch"
|
|
51
|
+
aria-checked={checked}
|
|
52
|
+
onClick={() => onChange(!checked)}
|
|
53
|
+
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 ${
|
|
54
|
+
checked ? "bg-primary" : "bg-secondary"
|
|
55
|
+
}`}
|
|
56
|
+
>
|
|
57
|
+
<span
|
|
58
|
+
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition ${
|
|
59
|
+
checked ? "translate-x-5" : "translate-x-0"
|
|
60
|
+
}`}
|
|
61
|
+
/>
|
|
62
|
+
</button>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default function SettingsPage() {
|
|
67
|
+
const [darkMode, setDarkMode] = useState(true);
|
|
68
|
+
const [notifications, setNotifications] = useState(true);
|
|
69
|
+
const [autoRefresh, setAutoRefresh] = useState(true);
|
|
70
|
+
const [compactMode, setCompactMode] = useState(false);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="max-w-3xl">
|
|
74
|
+
<div className="mb-8">
|
|
75
|
+
<h1 className="text-2xl font-bold tracking-tight text-foreground">
|
|
76
|
+
Settings
|
|
77
|
+
</h1>
|
|
78
|
+
<p className="text-muted-foreground mt-1">
|
|
79
|
+
Manage your workspace preferences and configuration.
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* Appearance Section */}
|
|
84
|
+
<div className="mb-8">
|
|
85
|
+
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-4 px-4">
|
|
86
|
+
Appearance
|
|
87
|
+
</h3>
|
|
88
|
+
<div className="bg-card border border-border/50 rounded-2xl overflow-hidden divide-y divide-border/50">
|
|
89
|
+
<SettingItem
|
|
90
|
+
icon={darkMode ? <Moon size={18} /> : <Sun size={18} />}
|
|
91
|
+
title="Dark Mode"
|
|
92
|
+
description="Toggle between light and dark theme"
|
|
93
|
+
>
|
|
94
|
+
<Toggle checked={darkMode} onChange={setDarkMode} />
|
|
95
|
+
</SettingItem>
|
|
96
|
+
<SettingItem
|
|
97
|
+
icon={<Palette size={18} />}
|
|
98
|
+
title="Accent Color"
|
|
99
|
+
description="Choose your preferred accent color"
|
|
100
|
+
>
|
|
101
|
+
<div className="flex gap-2">
|
|
102
|
+
{["#6366f1", "#8b5cf6", "#06b6d4", "#10b981", "#f59e0b"].map(
|
|
103
|
+
(color) => (
|
|
104
|
+
<button
|
|
105
|
+
key={color}
|
|
106
|
+
className="w-6 h-6 rounded-full border-2 border-transparent hover:border-foreground/30 transition-colors"
|
|
107
|
+
style={{ backgroundColor: color }}
|
|
108
|
+
/>
|
|
109
|
+
)
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</SettingItem>
|
|
113
|
+
<SettingItem
|
|
114
|
+
icon={<Zap size={18} />}
|
|
115
|
+
title="Compact Mode"
|
|
116
|
+
description="Reduce spacing for more content visibility"
|
|
117
|
+
>
|
|
118
|
+
<Toggle checked={compactMode} onChange={setCompactMode} />
|
|
119
|
+
</SettingItem>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Notifications Section */}
|
|
124
|
+
<div className="mb-8">
|
|
125
|
+
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-4 px-4">
|
|
126
|
+
Notifications
|
|
127
|
+
</h3>
|
|
128
|
+
<div className="bg-card border border-border/50 rounded-2xl overflow-hidden divide-y divide-border/50">
|
|
129
|
+
<SettingItem
|
|
130
|
+
icon={<Bell size={18} />}
|
|
131
|
+
title="Push Notifications"
|
|
132
|
+
description="Receive notifications for task updates"
|
|
133
|
+
>
|
|
134
|
+
<Toggle checked={notifications} onChange={setNotifications} />
|
|
135
|
+
</SettingItem>
|
|
136
|
+
<SettingItem
|
|
137
|
+
icon={<Globe size={18} />}
|
|
138
|
+
title="Auto Refresh"
|
|
139
|
+
description="Automatically refresh board data"
|
|
140
|
+
>
|
|
141
|
+
<Toggle checked={autoRefresh} onChange={setAutoRefresh} />
|
|
142
|
+
</SettingItem>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Account Section */}
|
|
147
|
+
<div className="mb-8">
|
|
148
|
+
<h3 className="text-xs font-bold uppercase tracking-widest text-muted-foreground mb-4 px-4">
|
|
149
|
+
Account
|
|
150
|
+
</h3>
|
|
151
|
+
<div className="bg-card border border-border/50 rounded-2xl overflow-hidden divide-y divide-border/50">
|
|
152
|
+
<SettingItem
|
|
153
|
+
icon={<User size={18} />}
|
|
154
|
+
title="Profile"
|
|
155
|
+
description="Update your personal information"
|
|
156
|
+
>
|
|
157
|
+
<Button variant="secondary" size="sm">
|
|
158
|
+
Edit
|
|
159
|
+
</Button>
|
|
160
|
+
</SettingItem>
|
|
161
|
+
<SettingItem
|
|
162
|
+
icon={<Shield size={18} />}
|
|
163
|
+
title="Security"
|
|
164
|
+
description="Manage authentication settings"
|
|
165
|
+
>
|
|
166
|
+
<Button variant="secondary" size="sm">
|
|
167
|
+
Configure
|
|
168
|
+
</Button>
|
|
169
|
+
</SettingItem>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Danger Zone */}
|
|
174
|
+
<div>
|
|
175
|
+
<h3 className="text-xs font-bold uppercase tracking-widest text-destructive mb-4 px-4">
|
|
176
|
+
Danger Zone
|
|
177
|
+
</h3>
|
|
178
|
+
<div className="bg-destructive/5 border border-destructive/20 rounded-2xl p-6">
|
|
179
|
+
<div className="flex items-center justify-between">
|
|
180
|
+
<div>
|
|
181
|
+
<h4 className="font-medium text-foreground">Reset Workspace</h4>
|
|
182
|
+
<p className="text-sm text-muted-foreground mt-0.5">
|
|
183
|
+
Delete all tasks, documents, and settings
|
|
184
|
+
</p>
|
|
185
|
+
</div>
|
|
186
|
+
<Button variant="danger" size="sm">
|
|
187
|
+
Reset
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AssigneeRole, TaskPriority } from "@locusai/shared";
|
|
4
|
+
import { Search, X } from "lucide-react";
|
|
5
|
+
import { Button, Dropdown, Input } from "@/components/ui";
|
|
6
|
+
|
|
7
|
+
interface BoardFilterProps {
|
|
8
|
+
searchQuery: string;
|
|
9
|
+
onSearchChange: (query: string) => void;
|
|
10
|
+
priorityFilter: TaskPriority | "ALL";
|
|
11
|
+
onPriorityChange: (priority: TaskPriority | "ALL") => void;
|
|
12
|
+
assigneeFilter: AssigneeRole | "ALL";
|
|
13
|
+
onAssigneeChange: (assignee: AssigneeRole | "ALL") => void;
|
|
14
|
+
onClearFilters: () => void;
|
|
15
|
+
hasActiveFilters: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PRIORITY_FILTER_OPTIONS = [
|
|
19
|
+
{ value: "ALL" as const, label: "All Priorities" },
|
|
20
|
+
{ value: TaskPriority.LOW, label: "Low" },
|
|
21
|
+
{ value: TaskPriority.MEDIUM, label: "Medium" },
|
|
22
|
+
{ value: TaskPriority.HIGH, label: "High" },
|
|
23
|
+
{ value: TaskPriority.CRITICAL, label: "Critical" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const ASSIGNEE_FILTER_OPTIONS = [
|
|
27
|
+
{ value: "ALL" as const, label: "All Assignees" },
|
|
28
|
+
...Object.values(AssigneeRole).map((role) => ({
|
|
29
|
+
value: role,
|
|
30
|
+
label: role.charAt(0) + role.slice(1).toLowerCase(),
|
|
31
|
+
})),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function BoardFilter({
|
|
35
|
+
searchQuery,
|
|
36
|
+
onSearchChange,
|
|
37
|
+
priorityFilter,
|
|
38
|
+
onPriorityChange,
|
|
39
|
+
assigneeFilter,
|
|
40
|
+
onAssigneeChange,
|
|
41
|
+
onClearFilters,
|
|
42
|
+
hasActiveFilters,
|
|
43
|
+
}: BoardFilterProps) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="flex flex-wrap items-end gap-4 mb-8 bg-card p-4 rounded-xl border shadow-sm">
|
|
46
|
+
<div className="flex-1 min-w-[300px]">
|
|
47
|
+
<Input
|
|
48
|
+
value={searchQuery}
|
|
49
|
+
onChange={(e) => onSearchChange(e.target.value)}
|
|
50
|
+
placeholder="Filter by title or ID..."
|
|
51
|
+
className="search-input h-10"
|
|
52
|
+
icon={<Search size={16} />}
|
|
53
|
+
rightElement={
|
|
54
|
+
searchQuery ? (
|
|
55
|
+
<button
|
|
56
|
+
className="hover:text-destructive transition-colors"
|
|
57
|
+
onClick={() => onSearchChange("")}
|
|
58
|
+
>
|
|
59
|
+
<X size={14} />
|
|
60
|
+
</button>
|
|
61
|
+
) : undefined
|
|
62
|
+
}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<div className="flex gap-4">
|
|
67
|
+
<div className="w-[180px]">
|
|
68
|
+
<Dropdown
|
|
69
|
+
label="Priority Filter"
|
|
70
|
+
value={priorityFilter}
|
|
71
|
+
onChange={onPriorityChange}
|
|
72
|
+
options={PRIORITY_FILTER_OPTIONS}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="w-[180px]">
|
|
76
|
+
<Dropdown
|
|
77
|
+
label="Assignee Filter"
|
|
78
|
+
value={assigneeFilter}
|
|
79
|
+
onChange={onAssigneeChange}
|
|
80
|
+
options={ASSIGNEE_FILTER_OPTIONS}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{hasActiveFilters && (
|
|
86
|
+
<Button
|
|
87
|
+
variant="ghost"
|
|
88
|
+
size="sm"
|
|
89
|
+
className="text-muted-foreground hover:text-destructive h-10 px-4"
|
|
90
|
+
onClick={onClearFilters}
|
|
91
|
+
>
|
|
92
|
+
<X size={14} className="mr-2" />
|
|
93
|
+
Clear All
|
|
94
|
+
</Button>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Search } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export function Header() {
|
|
6
|
+
return (
|
|
7
|
+
<header className="flex items-center mb-6 py-3">
|
|
8
|
+
<div className="relative flex-1 max-w-md group">
|
|
9
|
+
<Search
|
|
10
|
+
size={16}
|
|
11
|
+
className="absolute left-3.5 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-primary transition-colors"
|
|
12
|
+
/>
|
|
13
|
+
<input
|
|
14
|
+
type="text"
|
|
15
|
+
placeholder="Search tasks, docs... (⌘K)"
|
|
16
|
+
className="w-full bg-secondary/40 border border-border/50 rounded-lg pl-10 pr-4 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/70 outline-none transition-all focus:border-primary/50 focus:ring-2 focus:ring-primary/20 focus:bg-background hover:bg-secondary/60"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
</header>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Check, Edit2, X } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Button } from "./ui/Button";
|
|
6
|
+
import { Dropdown } from "./ui/Dropdown";
|
|
7
|
+
import { Input } from "./ui/Input";
|
|
8
|
+
|
|
9
|
+
export interface PropertyItemProps {
|
|
10
|
+
label: string;
|
|
11
|
+
value: string | number;
|
|
12
|
+
onEdit: (newValue: string) => void;
|
|
13
|
+
options?: string[];
|
|
14
|
+
type?: "text" | "date" | "dropdown";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function PropertyItem({
|
|
18
|
+
label,
|
|
19
|
+
value,
|
|
20
|
+
onEdit,
|
|
21
|
+
options,
|
|
22
|
+
type = "text",
|
|
23
|
+
}: PropertyItemProps) {
|
|
24
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
25
|
+
const [editValue, setEditValue] = useState(String(value));
|
|
26
|
+
|
|
27
|
+
const handleSave = () => {
|
|
28
|
+
onEdit(editValue);
|
|
29
|
+
setIsEditing(false);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleCancel = () => {
|
|
33
|
+
setEditValue(String(value));
|
|
34
|
+
setIsEditing(false);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex flex-col gap-1.5 py-3 first:pt-0 last:pb-0">
|
|
39
|
+
<span className="text-[10px] items-center font-bold uppercase tracking-widest text-muted-foreground">
|
|
40
|
+
{label}
|
|
41
|
+
</span>
|
|
42
|
+
<div className="relative group min-h-[32px] flex items-center">
|
|
43
|
+
{isEditing ? (
|
|
44
|
+
<div className="flex items-center gap-2 w-full">
|
|
45
|
+
<div className="flex-1">
|
|
46
|
+
{type === "dropdown" && options ? (
|
|
47
|
+
<Dropdown
|
|
48
|
+
value={editValue}
|
|
49
|
+
options={options.map((o) => ({ value: o, label: o }))}
|
|
50
|
+
onChange={(v) => setEditValue(v)}
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<Input
|
|
54
|
+
type={type}
|
|
55
|
+
value={editValue}
|
|
56
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
57
|
+
autoFocus
|
|
58
|
+
className="h-8 py-1"
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
63
|
+
<Button
|
|
64
|
+
size="icon"
|
|
65
|
+
variant="ghost"
|
|
66
|
+
className="h-8 w-8"
|
|
67
|
+
onClick={handleSave}
|
|
68
|
+
>
|
|
69
|
+
<Check size={14} />
|
|
70
|
+
</Button>
|
|
71
|
+
<Button
|
|
72
|
+
size="icon"
|
|
73
|
+
variant="ghost"
|
|
74
|
+
className="h-8 w-8"
|
|
75
|
+
onClick={handleCancel}
|
|
76
|
+
>
|
|
77
|
+
<X size={14} />
|
|
78
|
+
</Button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
) : (
|
|
82
|
+
<div
|
|
83
|
+
className="flex items-center justify-between gap-2 w-full cursor-pointer hover:bg-secondary/50 px-2 -mx-2 py-1 rounded-md transition-colors"
|
|
84
|
+
onClick={() => setIsEditing(true)}
|
|
85
|
+
>
|
|
86
|
+
<span className="text-sm font-medium text-foreground truncate">
|
|
87
|
+
{value || "None"}
|
|
88
|
+
</span>
|
|
89
|
+
<Edit2
|
|
90
|
+
size={12}
|
|
91
|
+
className="text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChevronRight,
|
|
5
|
+
Command,
|
|
6
|
+
FileText,
|
|
7
|
+
LayoutDashboard,
|
|
8
|
+
List,
|
|
9
|
+
Settings,
|
|
10
|
+
} from "lucide-react";
|
|
11
|
+
import Link from "next/link";
|
|
12
|
+
import { usePathname } from "next/navigation";
|
|
13
|
+
import { cn } from "@/lib/utils";
|
|
14
|
+
|
|
15
|
+
export function Sidebar() {
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
|
|
18
|
+
const menuItems = [
|
|
19
|
+
{
|
|
20
|
+
href: "/",
|
|
21
|
+
label: "Board",
|
|
22
|
+
icon: LayoutDashboard,
|
|
23
|
+
description: "Manage tasks",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
href: "/backlog", // Added Backlog item
|
|
27
|
+
label: "Backlog",
|
|
28
|
+
icon: List,
|
|
29
|
+
description: "Manage backlog items",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
href: "/docs",
|
|
33
|
+
label: "Library",
|
|
34
|
+
icon: FileText,
|
|
35
|
+
description: "Documents",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<aside className="w-[260px] flex flex-col border-r border-border/50 bg-card/50 backdrop-blur-sm h-full">
|
|
41
|
+
{/* Logo */}
|
|
42
|
+
<div className="flex items-center gap-3 p-5 border-b border-border/50">
|
|
43
|
+
<div className="bg-linear-to-br from-primary to-primary/70 text-primary-foreground p-2 rounded-xl shadow-lg shadow-primary/20">
|
|
44
|
+
<Command size={20} />
|
|
45
|
+
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<h2 className="text-lg font-bold tracking-tight text-foreground">
|
|
48
|
+
Locus
|
|
49
|
+
</h2>
|
|
50
|
+
<p className="text-[10px] text-muted-foreground font-medium">
|
|
51
|
+
Engineering Workspace
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Navigation */}
|
|
57
|
+
<div className="flex-1 p-4">
|
|
58
|
+
<div className="text-[10px] uppercase font-bold tracking-widest text-muted-foreground/70 mb-3 px-3">
|
|
59
|
+
Navigation
|
|
60
|
+
</div>
|
|
61
|
+
<nav className="space-y-1">
|
|
62
|
+
{menuItems.map((item) => {
|
|
63
|
+
const Icon = item.icon;
|
|
64
|
+
const isActive = pathname === item.href;
|
|
65
|
+
return (
|
|
66
|
+
<Link
|
|
67
|
+
key={item.href}
|
|
68
|
+
href={item.href}
|
|
69
|
+
className={cn(
|
|
70
|
+
"group flex items-center gap-3 px-3 py-2.5 text-sm font-medium rounded-xl transition-all",
|
|
71
|
+
isActive
|
|
72
|
+
? "bg-primary text-primary-foreground shadow-md shadow-primary/20"
|
|
73
|
+
: "text-muted-foreground hover:bg-secondary hover:text-foreground"
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<Icon
|
|
77
|
+
size={18}
|
|
78
|
+
className={
|
|
79
|
+
isActive ? "" : "group-hover:scale-110 transition-transform"
|
|
80
|
+
}
|
|
81
|
+
/>
|
|
82
|
+
<div className="flex-1">
|
|
83
|
+
<span className="block">{item.label}</span>
|
|
84
|
+
</div>
|
|
85
|
+
{isActive && <ChevronRight size={14} className="opacity-70" />}
|
|
86
|
+
</Link>
|
|
87
|
+
);
|
|
88
|
+
})}
|
|
89
|
+
</nav>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Footer */}
|
|
93
|
+
<div className="p-4 border-t border-border/50">
|
|
94
|
+
<Link
|
|
95
|
+
href="/settings"
|
|
96
|
+
className={cn(
|
|
97
|
+
"flex items-center gap-3 w-full px-3 py-2.5 text-sm font-medium rounded-xl transition-all",
|
|
98
|
+
pathname === "/settings"
|
|
99
|
+
? "bg-secondary text-foreground"
|
|
100
|
+
: "text-muted-foreground hover:bg-secondary/60 hover:text-foreground"
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<Settings size={18} />
|
|
104
|
+
<span>Settings</span>
|
|
105
|
+
</Link>
|
|
106
|
+
</div>
|
|
107
|
+
</aside>
|
|
108
|
+
);
|
|
109
|
+
}
|