Skip to content

Commit 07e91bc

Browse files
committed
feat: task creation and task list improvements
1 parent 416bf92 commit 07e91bc

File tree

13 files changed

+1024
-134
lines changed

13 files changed

+1024
-134
lines changed

.claude/CLAUDE.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,34 @@
1-
- Use Radix UI/theme for styling. Prefer prestyled components and primitives over HTML elements.
2-
- Use Tailwind CSS for styling.
3-
- Use zustand for state management, not React context.
1+
# Guidelines
2+
3+
- Use Radix UI/theme for styling. Prefer prestyled components over HTML elements.
4+
- Use `Kbd` for key hints. Fallback to Tailwind CSS when Radix lacks props.
5+
- Use zustand for state management.
6+
- Prefer creating separate, reusable components over large monolithic components.
7+
8+
## Layout Components
9+
10+
- **Box**: Fundamental layout component for spacing, sizing, and responsive display
11+
- **Flex**: Box + flexbox properties for axis-based organization
12+
- **Grid**: Box + grid properties for column/row layouts
13+
- **Section**: Consistent vertical spacing for page sections
14+
- **Container**: Consistent max-width for content
15+
16+
## Space Scale
17+
18+
Spacing props accept "1"-"9" (4px-64px in increments) or any valid CSS value.
19+
20+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
21+
|---|---|---|---|---|---|---|---|---|
22+
| 4px | 8px | 12px | 16px | 24px | 32px | 40px | 48px | 64px |
23+
24+
## Layout Props
25+
26+
All props support responsive object values (e.g., `{{ sm: '6', lg: '9' }}`).
27+
28+
- **Padding**: `p`, `px`, `py`, `pt`, `pr`, `pb`, `pl`
29+
- **Margin**: `m`, `mx`, `my`, `mt`, `mr`, `mb`, `ml` (uses space scale or CSS values)
30+
- **Width**: `width`, `minWidth`, `maxWidth`
31+
- **Height**: `height`, `minHeight`, `maxHeight`
32+
- **Position**: `position`, `inset`, `top`, `right`, `bottom`, `left` (offset values use space scale)
33+
- **Flex child**: `flexBasis`, `flexShrink`, `flexGrow`
34+
- **Grid child**: `gridArea`, `gridColumn`, `gridColumnStart`, `gridColumnEnd`, `gridRow`, `gridRowStart`, `gridRowEnd`

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
"build:electron": "electron-builder",
2121
"preview": "vite preview",
2222
"typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.web.json --noEmit",
23-
"lint": "biome check --fix",
24-
"format": "biome format --fix",
25-
"check": "pnpm run lint && pnpm run typecheck",
23+
"lint:write": "biome check --write --unsafe",
24+
"format:write": "biome format --write --unsafe",
25+
"check:write": "pnpm run lint:write && pnpm run typecheck",
2626
"generate-client": "tsx scripts/update-openapi-client.ts"
2727
},
2828
"keywords": [

src/api/posthogClient.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { RepositoryConfig } from "@shared/types";
12
import { buildApiFetcher } from "./fetcher";
23
import { createApiClient, type Schemas } from "./generated";
34

@@ -94,6 +95,22 @@ export class PostHogAPIClient {
9495
return data;
9596
}
9697

98+
async deleteTask(taskId: string) {
99+
const teamId = await this.getTeamId();
100+
await this.api.delete(`/api/projects/{project_id}/tasks/{id}/`, {
101+
path: { project_id: teamId.toString(), id: taskId },
102+
});
103+
}
104+
105+
async duplicateTask(taskId: string) {
106+
const task = await this.getTask(taskId);
107+
return this.createTask(
108+
`${task.title} (copy)`,
109+
task.description,
110+
task.repository_config as RepositoryConfig | undefined,
111+
);
112+
}
113+
97114
async runTask(taskId: string) {
98115
// TODO: Pull this out and handle local and API calls
99116
const teamId = await this.getTeamId();

src/renderer/components/MainLayout.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { useTabStore } from "../stores/tabStore";
66
import { CommandMenu } from "./command";
77
import { StatusBar } from "./StatusBar";
88
import { TabBar } from "./TabBar";
9+
import { TaskCreate } from "./TaskCreate";
910
import { TaskDetail } from "./TaskDetail";
1011
import { TaskList } from "./TaskList";
1112
import { WorkflowView } from "./WorkflowView";
1213

1314
export function MainLayout() {
1415
const { activeTabId, tabs, createTab, setActiveTab } = useTabStore();
1516
const [commandMenuOpen, setCommandMenuOpen] = useState(false);
17+
const [taskCreateOpen, setTaskCreateOpen] = useState(false);
1618

1719
useHotkeys("mod+k", () => setCommandMenuOpen((prev) => !prev), {
1820
enabled: !commandMenuOpen,
@@ -23,6 +25,7 @@ export function MainLayout() {
2325
useHotkeys("mod+p", () => setCommandMenuOpen((prev) => !prev), {
2426
enabled: !commandMenuOpen,
2527
});
28+
useHotkeys("mod+n", () => setTaskCreateOpen(true));
2629

2730
const handleSelectTask = (task: Task) => {
2831
// Check if task is already open in a tab
@@ -54,7 +57,10 @@ export function MainLayout() {
5457

5558
<Box flexGrow="1" overflow="hidden">
5659
{activeTab?.type === "task-list" && (
57-
<TaskList onSelectTask={handleSelectTask} />
60+
<TaskList
61+
onSelectTask={handleSelectTask}
62+
onNewTask={() => setTaskCreateOpen(true)}
63+
/>
5864
)}
5965

6066
{activeTab?.type === "task-detail" && activeTab.data ? (
@@ -68,7 +74,12 @@ export function MainLayout() {
6874

6975
<StatusBar />
7076

71-
<CommandMenu open={commandMenuOpen} onOpenChange={setCommandMenuOpen} />
77+
<CommandMenu
78+
open={commandMenuOpen}
79+
onOpenChange={setCommandMenuOpen}
80+
onCreateTask={() => setTaskCreateOpen(true)}
81+
/>
82+
<TaskCreate open={taskCreateOpen} onOpenChange={setTaskCreateOpen} />
7283
</Flex>
7384
);
7485
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Button, Code, Flex, Kbd } from "@radix-ui/themes";
2+
import type { ReactNode } from "react";
3+
4+
interface ShortcutCardProps {
5+
icon: ReactNode;
6+
title: string;
7+
keys: string[];
8+
}
9+
10+
export function ShortcutCard({ icon, title, keys }: ShortcutCardProps) {
11+
return (
12+
<Button size="2" variant="surface" color="gray">
13+
<Flex direction="column" gap="2" width="200px">
14+
<Flex align="center" justify="between" gap="2">
15+
<Flex align="center" gap="2">
16+
{icon}
17+
<Code variant="ghost">{title}</Code>
18+
</Flex>
19+
20+
<Flex align="center" gap="2">
21+
{keys.map((key) => (
22+
<Kbd size="1" key={key}>
23+
{key}
24+
</Kbd>
25+
))}
26+
</Flex>
27+
</Flex>
28+
</Flex>
29+
</Button>
30+
);
31+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import {
2+
Cross2Icon,
3+
EnterFullScreenIcon,
4+
ExitFullScreenIcon,
5+
} from "@radix-ui/react-icons";
6+
import {
7+
Button,
8+
Dialog,
9+
Flex,
10+
IconButton,
11+
Switch,
12+
Text,
13+
TextArea,
14+
} from "@radix-ui/themes";
15+
import { useRef, useState } from "react";
16+
import { useHotkeys } from "react-hotkeys-hook";
17+
import { useTabStore } from "../stores/tabStore";
18+
import { useTaskStore } from "../stores/taskStore";
19+
20+
interface TaskCreateProps {
21+
open: boolean;
22+
onOpenChange: (open: boolean) => void;
23+
}
24+
25+
export function TaskCreate({ open, onOpenChange }: TaskCreateProps) {
26+
const { createTask, isLoading } = useTaskStore();
27+
const { createTab } = useTabStore();
28+
const [title, setTitle] = useState("");
29+
const [description, setDescription] = useState("");
30+
const [isExpanded, setIsExpanded] = useState(false);
31+
const [createMore, setCreateMore] = useState(false);
32+
const titleRef = useRef<HTMLTextAreaElement>(null);
33+
const descriptionRef = useRef<HTMLTextAreaElement>(null);
34+
35+
const handleCreate = async () => {
36+
if (!title.trim() || !description.trim()) return;
37+
38+
const newTask = await createTask(title, description);
39+
if (newTask) {
40+
createTab({
41+
type: "task-detail",
42+
title: newTask.title,
43+
data: newTask,
44+
});
45+
setTitle("");
46+
setDescription("");
47+
if (!createMore) {
48+
onOpenChange(false);
49+
}
50+
}
51+
};
52+
53+
useHotkeys("mod+enter", handleCreate, {
54+
enabled: open,
55+
enableOnFormTags: true,
56+
});
57+
58+
return (
59+
<Dialog.Root open={open} onOpenChange={onOpenChange}>
60+
<Dialog.Content
61+
maxHeight={isExpanded ? "90vh" : "600px"}
62+
height={isExpanded ? "90vh" : "auto"}
63+
>
64+
<Flex direction="column" height="100%">
65+
<Flex justify="between" align="center">
66+
<Dialog.Title className="mb-0" size="2">
67+
New Task
68+
</Dialog.Title>
69+
<Flex gap="2">
70+
<IconButton
71+
size="1"
72+
variant="ghost"
73+
color="gray"
74+
onClick={() => setIsExpanded(!isExpanded)}
75+
>
76+
{isExpanded ? <ExitFullScreenIcon /> : <EnterFullScreenIcon />}
77+
</IconButton>
78+
<Dialog.Close>
79+
<IconButton size="1" variant="ghost" color="gray">
80+
<Cross2Icon />
81+
</IconButton>
82+
</Dialog.Close>
83+
</Flex>
84+
</Flex>
85+
86+
<Flex direction="column" gap="4" mt="4" flexGrow="1">
87+
<Flex direction="column" gap="2">
88+
<TextArea
89+
ref={titleRef}
90+
value={title}
91+
onChange={(e) => {
92+
setTitle(e.target.value);
93+
if (titleRef.current && !isExpanded) {
94+
titleRef.current.style.height = "auto";
95+
titleRef.current.style.height = `${titleRef.current.scrollHeight}px`;
96+
}
97+
}}
98+
placeholder="Task title..."
99+
size="3"
100+
autoFocus
101+
rows={1}
102+
style={{
103+
resize: "none",
104+
overflow: "hidden",
105+
minHeight: "auto",
106+
}}
107+
/>
108+
</Flex>
109+
110+
<Flex
111+
direction="column"
112+
gap="2"
113+
flexGrow="1"
114+
style={{ minHeight: 0 }}
115+
>
116+
<TextArea
117+
ref={descriptionRef}
118+
value={description}
119+
onChange={(e) => {
120+
setDescription(e.target.value);
121+
if (descriptionRef.current && !isExpanded) {
122+
descriptionRef.current.style.height = "auto";
123+
descriptionRef.current.style.height = `${descriptionRef.current.scrollHeight}px`;
124+
}
125+
}}
126+
placeholder="Add description..."
127+
size="3"
128+
rows={3}
129+
style={{
130+
resize: "none",
131+
overflow: isExpanded ? "auto" : "hidden",
132+
minHeight: "auto",
133+
height: isExpanded ? "100%" : "auto",
134+
}}
135+
/>
136+
</Flex>
137+
138+
<Flex gap="3" justify="end" align="end">
139+
<Text as="label" size="1" style={{ cursor: "pointer" }}>
140+
<Flex gap="2" align="center" mb="2">
141+
<Switch
142+
checked={createMore}
143+
onCheckedChange={setCreateMore}
144+
size="1"
145+
/>
146+
<Text size="1" color="gray" className="select-none">
147+
Create more
148+
</Text>
149+
</Flex>
150+
</Text>
151+
<Button
152+
variant="classic"
153+
onClick={handleCreate}
154+
disabled={!title.trim() || !description.trim() || isLoading}
155+
>
156+
{isLoading ? "Creating..." : "Create task"}
157+
</Button>
158+
</Flex>
159+
</Flex>
160+
</Flex>
161+
</Dialog.Content>
162+
</Dialog.Root>
163+
);
164+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Badge, Box, Flex, Text } from "@radix-ui/themes";
2+
import type { ForwardedRef } from "react";
3+
import { forwardRef } from "react";
4+
5+
interface TaskDragPreviewProps {
6+
status: string;
7+
title: string;
8+
}
9+
10+
export const TaskDragPreview = forwardRef(
11+
(
12+
{ status, title }: TaskDragPreviewProps,
13+
ref: ForwardedRef<HTMLDivElement>,
14+
) => {
15+
return (
16+
<Box
17+
ref={ref}
18+
position="fixed"
19+
style={{
20+
// Painful hack to position it out of the screen, hiding doesn't work
21+
top: "-10000px",
22+
left: "-10000px",
23+
pointerEvents: "none",
24+
}}
25+
>
26+
<Flex
27+
gap="2"
28+
align="center"
29+
p="2"
30+
className="border border-gray-6 bg-panel-solid font-mono"
31+
style={{ borderRadius: "var(--radius-2)" }}
32+
>
33+
<Badge color={status === "Backlog" ? "gray" : undefined} size="1">
34+
{status}
35+
</Badge>
36+
<Text size="2">{title}</Text>
37+
</Flex>
38+
</Box>
39+
);
40+
},
41+
);
42+
43+
TaskDragPreview.displayName = "TaskDragPreview";

0 commit comments

Comments
 (0)