Skip to content

Commit 9a420ef

Browse files
feat: add Notion-like Todo view with push-to-board, brand gradient, and dev proxy
- New /todos route: quick-add drafts with drag-and-drop reorder, inline editing, priority cycling, bulk select, and push-to-board (localStorage-persisted via Zustand) - Brand alignment: gradient wordmark logo matching engram.sensai.co.in landing page - IssuePanel: added Chat button to navigate to /task/:id from board - Vite dev proxy: /api and /ws passthrough for local development - New files: useTodoStore.ts, TodoView.tsx, checkbox.tsx (shadcn/radix) - Modified: App.tsx, AppBar.tsx, IssuePanel.tsx, index.css, vite.config.ts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7ac9709 commit 9a420ef

8 files changed

Lines changed: 480 additions & 8 deletions

File tree

engram-bridge/engram_bridge/channels/web-ui/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ChatView } from "@/views/ChatView";
99
import { BoardView } from "@/views/BoardView";
1010
import { TaskChatView } from "@/views/TaskChatView";
1111
import { MemoryView } from "@/views/MemoryView";
12+
import { TodoView } from "@/views/TodoView";
1213
import { CommandBar } from "@/components/dialogs/CommandBar";
1314
import { SettingsDialog } from "@/components/dialogs/SettingsDialog";
1415
import { IssuePanel } from "@/components/issue/IssuePanel";
@@ -29,6 +30,7 @@ function AppInner() {
2930

3031
<Routes>
3132
<Route path="/" element={<ChatView />} />
33+
<Route path="/todos" element={<TodoView />} />
3234
<Route path="/board" element={<BoardView />} />
3335
<Route path="/task/:taskId" element={<TaskChatView />} />
3436
<Route path="/memory" element={<MemoryView />} />

engram-bridge/engram_bridge/channels/web-ui/src/components/issue/IssuePanel.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useCallback, useEffect, useRef } from "react";
2-
import { X, Trash2, Plus, Send } from "lucide-react";
2+
import { useNavigate } from "react-router-dom";
3+
import { X, Trash2, Plus, Send, MessageSquare } from "lucide-react";
34
import { Button } from "@/components/ui/button";
45
import { Input } from "@/components/ui/input";
56
import { Textarea } from "@/components/ui/textarea";
@@ -33,6 +34,7 @@ export function IssuePanel({ issue, createMode, defaultStatusId, onClose, onIssu
3334
const [newAssignee, setNewAssignee] = useState("");
3435
const [saving, setSaving] = useState(false);
3536

37+
const navigate = useNavigate();
3638
const titleRef = useRef<HTMLTextAreaElement>(null);
3739

3840
useEffect(() => {
@@ -149,6 +151,18 @@ export function IssuePanel({ issue, createMode, defaultStatusId, onClose, onIssu
149151
)}
150152
</div>
151153
<div className="flex items-center gap-1">
154+
{!createMode && issue && (
155+
<Button
156+
variant="ghost"
157+
size="sm"
158+
onClick={() => { onClose(); navigate(`/task/${issue.id}`); }}
159+
className="h-7 gap-1 px-2 text-xs text-muted-foreground hover:text-foreground"
160+
title="Open task chat"
161+
>
162+
<MessageSquare className="h-3.5 w-3.5" />
163+
Chat
164+
</Button>
165+
)}
152166
{!createMode && issue && (
153167
<Button variant="ghost" size="sm" onClick={handleDelete} className="h-7 w-7 p-0 text-destructive hover:text-destructive">
154168
<Trash2 className="h-3.5 w-3.5" />

engram-bridge/engram_bridge/channels/web-ui/src/components/layout/AppBar.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { useLocation, useNavigate } from "react-router-dom";
22
import {
3-
Brain,
43
Wifi,
54
WifiOff,
65
Loader2,
76
MessageSquare,
7+
ListTodo,
88
LayoutDashboard,
99
Database,
1010
ChevronDown,
@@ -22,6 +22,7 @@ interface Props {
2222

2323
const NAV_TABS = [
2424
{ path: "/", label: "Chat", icon: MessageSquare },
25+
{ path: "/todos", label: "Todos", icon: ListTodo },
2526
{ path: "/board", label: "Board", icon: LayoutDashboard },
2627
{ path: "/memory", label: "Memory", icon: Database },
2728
] as const;
@@ -53,12 +54,18 @@ export function AppBar({ onOpenSettings }: Props) {
5354
<header className="flex items-center justify-between px-4 py-2.5 border-b border-border bg-background">
5455
<div className="flex items-center gap-4">
5556
{/* Logo */}
56-
<div className="flex items-center gap-2.5">
57-
<div className="w-7 h-7 rounded-lg flex items-center justify-center bg-primary">
58-
<Brain className="h-4 w-4 text-primary-foreground" />
59-
</div>
60-
<h1 className="text-sm font-semibold tracking-tight">engram</h1>
61-
</div>
57+
<a
58+
href="/"
59+
onClick={(e) => { e.preventDefault(); navigate("/"); }}
60+
className="flex items-center hover:opacity-80 transition-opacity"
61+
>
62+
<span
63+
className="engram-gradient-text font-bold tracking-[-0.025em]"
64+
style={{ fontSize: "1.35rem", lineHeight: "1.5rem", fontFamily: "'Space Grotesk', sans-serif" }}
65+
>
66+
engram
67+
</span>
68+
</a>
6269

6370
{/* Project selector */}
6471
{currentProject && (
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import * as React from "react"
2+
import { CheckIcon } from "lucide-react"
3+
import { Checkbox as CheckboxPrimitive } from "radix-ui"
4+
5+
import { cn } from "@/lib/utils"
6+
7+
function Checkbox({
8+
className,
9+
...props
10+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
11+
return (
12+
<CheckboxPrimitive.Root
13+
data-slot="checkbox"
14+
className={cn(
15+
"peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
16+
className
17+
)}
18+
{...props}
19+
>
20+
<CheckboxPrimitive.Indicator
21+
data-slot="checkbox-indicator"
22+
className="flex items-center justify-center text-current transition-none"
23+
>
24+
<CheckIcon className="size-3.5" />
25+
</CheckboxPrimitive.Indicator>
26+
</CheckboxPrimitive.Root>
27+
)
28+
}
29+
30+
export { Checkbox }

engram-bridge/engram_bridge/channels/web-ui/src/index.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap");
12
@import "tailwindcss";
23
@import "tw-animate-css";
34
@import "shadcn/tailwind.css";
@@ -114,3 +115,15 @@
114115
.thinking-dot:nth-child(1) { animation: pulse-dots 1.4s infinite 0s; }
115116
.thinking-dot:nth-child(2) { animation: pulse-dots 1.4s infinite 0.2s; }
116117
.thinking-dot:nth-child(3) { animation: pulse-dots 1.4s infinite 0.4s; }
118+
119+
/* Engram brand gradient (matches landing page) */
120+
.engram-gradient-text {
121+
background: linear-gradient(135deg, #e8722a 0%, #e85d45 30%, #d4607a 60%, #ff8a2b 100%);
122+
-webkit-background-clip: text;
123+
-webkit-text-fill-color: transparent;
124+
background-clip: text;
125+
}
126+
127+
.engram-gradient-bg {
128+
background: linear-gradient(135deg, #e8722a 0%, #e85d45 30%, #d4607a 60%, #ff8a2b 100%);
129+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { create } from "zustand";
2+
import { persist } from "zustand/middleware";
3+
import type { Priority } from "@/types";
4+
5+
export interface TodoDraft {
6+
id: string;
7+
title: string;
8+
priority: Priority;
9+
selected: boolean;
10+
createdAt: string;
11+
}
12+
13+
interface TodoState {
14+
drafts: TodoDraft[];
15+
addDraft: (title: string) => void;
16+
updateDraftTitle: (id: string, title: string) => void;
17+
updateDraftPriority: (id: string, priority: Priority) => void;
18+
removeDraft: (id: string) => void;
19+
toggleSelect: (id: string) => void;
20+
selectAll: () => void;
21+
deselectAll: () => void;
22+
reorder: (fromIndex: number, toIndex: number) => void;
23+
removeDrafts: (ids: string[]) => void;
24+
}
25+
26+
export const useTodoStore = create<TodoState>()(
27+
persist(
28+
(set) => ({
29+
drafts: [],
30+
31+
addDraft: (title) =>
32+
set((s) => ({
33+
drafts: [
34+
...s.drafts,
35+
{
36+
id: crypto.randomUUID(),
37+
title,
38+
priority: "medium" as Priority,
39+
selected: false,
40+
createdAt: new Date().toISOString(),
41+
},
42+
],
43+
})),
44+
45+
updateDraftTitle: (id, title) =>
46+
set((s) => ({
47+
drafts: s.drafts.map((d) => (d.id === id ? { ...d, title } : d)),
48+
})),
49+
50+
updateDraftPriority: (id, priority) =>
51+
set((s) => ({
52+
drafts: s.drafts.map((d) => (d.id === id ? { ...d, priority } : d)),
53+
})),
54+
55+
removeDraft: (id) =>
56+
set((s) => ({
57+
drafts: s.drafts.filter((d) => d.id !== id),
58+
})),
59+
60+
toggleSelect: (id) =>
61+
set((s) => ({
62+
drafts: s.drafts.map((d) =>
63+
d.id === id ? { ...d, selected: !d.selected } : d,
64+
),
65+
})),
66+
67+
selectAll: () =>
68+
set((s) => ({
69+
drafts: s.drafts.map((d) => ({ ...d, selected: true })),
70+
})),
71+
72+
deselectAll: () =>
73+
set((s) => ({
74+
drafts: s.drafts.map((d) => ({ ...d, selected: false })),
75+
})),
76+
77+
reorder: (fromIndex, toIndex) =>
78+
set((s) => {
79+
const drafts = [...s.drafts];
80+
const [moved] = drafts.splice(fromIndex, 1);
81+
drafts.splice(toIndex, 0, moved);
82+
return { drafts };
83+
}),
84+
85+
removeDrafts: (ids) =>
86+
set((s) => ({
87+
drafts: s.drafts.filter((d) => !ids.includes(d.id)),
88+
})),
89+
}),
90+
{
91+
name: "engram-todos",
92+
},
93+
),
94+
);

0 commit comments

Comments
 (0)