@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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/next.config.js +7 -0
  3. package/package.json +37 -0
  4. package/postcss.config.mjs +5 -0
  5. package/src/app/backlog/page.tsx +19 -0
  6. package/src/app/docs/page.tsx +7 -0
  7. package/src/app/globals.css +603 -0
  8. package/src/app/layout.tsx +43 -0
  9. package/src/app/page.tsx +16 -0
  10. package/src/app/providers.tsx +16 -0
  11. package/src/app/settings/page.tsx +194 -0
  12. package/src/components/BoardFilter.tsx +98 -0
  13. package/src/components/Header.tsx +21 -0
  14. package/src/components/PropertyItem.tsx +98 -0
  15. package/src/components/Sidebar.tsx +109 -0
  16. package/src/components/TaskCard.tsx +138 -0
  17. package/src/components/TaskCreateModal.tsx +243 -0
  18. package/src/components/TaskPanel.tsx +765 -0
  19. package/src/components/index.ts +7 -0
  20. package/src/components/ui/Badge.tsx +77 -0
  21. package/src/components/ui/Button.tsx +47 -0
  22. package/src/components/ui/Checkbox.tsx +52 -0
  23. package/src/components/ui/Dropdown.tsx +107 -0
  24. package/src/components/ui/Input.tsx +36 -0
  25. package/src/components/ui/Modal.tsx +79 -0
  26. package/src/components/ui/Textarea.tsx +21 -0
  27. package/src/components/ui/index.ts +7 -0
  28. package/src/hooks/useTasks.ts +119 -0
  29. package/src/lib/api-client.ts +24 -0
  30. package/src/lib/utils.ts +6 -0
  31. package/src/services/doc.service.ts +27 -0
  32. package/src/services/index.ts +3 -0
  33. package/src/services/sprint.service.ts +26 -0
  34. package/src/services/task.service.ts +75 -0
  35. package/src/views/Backlog.tsx +691 -0
  36. package/src/views/Board.tsx +306 -0
  37. package/src/views/Docs.tsx +625 -0
  38. 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
+ }
@@ -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
+ }