s.completed).length / task.subtasks.length) * 100}%` }}
@@ -658,7 +647,6 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
)}
- {/* Subtask list */}
{task.subtasks?.length > 0 && (
{task.subtasks.map((subtask) => (
@@ -675,7 +663,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
className={`w-5 h-5 rounded border-2 flex items-center justify-center shrink-0 transition ${
subtask.completed
? "bg-green-500 border-green-500 text-white"
- : "border-gray-300 hover:border-amber-400"
+ : "border-gray-300 dark:border-gray-600 hover:border-amber-400 dark:hover:border-amber-500"
}`}
>
{subtask.completed && (
@@ -684,7 +672,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
)}
-
+
{subtask.title}
{hasToken && (
@@ -697,7 +685,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
console.error("Failed to delete subtask:", e);
}
}}
- className="opacity-0 group-hover:opacity-100 text-gray-300 hover:text-red-400 transition p-0.5"
+ className="opacity-0 group-hover:opacity-100 text-gray-300 dark:text-gray-600 hover:text-red-400 dark:hover:text-red-400 transition p-0.5"
title="Remove subtask"
>
)}
- {/* Add subtask input */}
{hasToken && (
setNewSubtaskTitle(e.target.value)}
placeholder="Add a subtask..."
- className="flex-1 text-sm border border-gray-200 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-200 focus:border-amber-300"
+ className="flex-1 text-sm border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-amber-200 dark:focus:ring-amber-800 focus:border-amber-300 dark:focus:border-amber-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500"
onKeyDown={async (e) => {
if (e.key === "Enter" && newSubtaskTitle.trim()) {
setAddingSubtask(true);
@@ -760,13 +747,12 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
{/* Progress Notes */}
-
+
Progress Notes {task.progressNotes?.length > 0 && (
- ({task.progressNotes.length})
+ ({task.progressNotes.length})
)}
- {/* Add note input */}
{hasToken && (
@@ -775,7 +761,7 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
onChange={(e) => setNoteText(e.target.value)}
placeholder="Add a progress note..."
rows={2}
- className="flex-1 text-sm border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-amber-200 focus:border-amber-300 resize-y min-h-[40px] max-h-32"
+ className="flex-1 text-sm border border-gray-200 dark:border-gray-700 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-amber-200 dark:focus:ring-amber-800 focus:border-amber-300 dark:focus:border-amber-700 resize-y min-h-[40px] max-h-32 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder:text-gray-400 dark:placeholder:text-gray-500"
onKeyDown={(e) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
@@ -810,12 +796,12 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
{addingNote ? "..." : "Add"}
-
โ+Enter to submit
+
โ+Enter to submit
)}
{!task.progressNotes || task.progressNotes.length === 0 ? (
-
+
No progress notes yet
) : (
@@ -825,24 +811,21 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
.reverse()
.map((note, i) => (
- {/* Timeline line */}
{i < task.progressNotes.length - 1 && (
-
+
)}
- {/* Timeline dot */}
- {/* Content */}
-
{note.note}
-
{formatTimestamp(note.timestamp)} ยท {timeAgo(note.timestamp)}
+
{note.note}
+
{formatTimestamp(note.timestamp)} ยท {timeAgo(note.timestamp)}
))}
@@ -853,20 +836,20 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
{/* Save / Cancel Bar */}
{hasToken && isDirty && (
-
-
Unsaved changes
+
+
Unsaved changes
@@ -876,8 +859,8 @@ export function TaskDetailPanel({ task, onClose, onStatusChange, onTaskUpdated,
{/* Actions Footer */}
{hasToken && actions.length > 0 && (
-
-
Actions
+
+
Actions
{actions.map((action) => (
) : (
-
-
Delete this task?
+
+ Delete this task?
)}
- {/* Task ID - click to copy */}
>
diff --git a/frontend/src/hooks/useTheme.tsx b/frontend/src/hooks/useTheme.tsx
new file mode 100644
index 0000000..7a08b63
--- /dev/null
+++ b/frontend/src/hooks/useTheme.tsx
@@ -0,0 +1,66 @@
+import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
+
+type Theme = "light" | "dark" | "system";
+
+interface ThemeContextType {
+ theme: Theme;
+ resolved: "light" | "dark";
+ setTheme: (t: Theme) => void;
+}
+
+const ThemeContext = createContext
({
+ theme: "system",
+ resolved: "light",
+ setTheme: () => {},
+});
+
+function getSystemTheme(): "light" | "dark" {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
+}
+
+function resolveTheme(theme: Theme): "light" | "dark" {
+ return theme === "system" ? getSystemTheme() : theme;
+}
+
+export function ThemeProvider({ children }: { children: ReactNode }) {
+ const [theme, setThemeState] = useState(() => {
+ const stored = localStorage.getItem("hammer-theme");
+ return (stored === "light" || stored === "dark" || stored === "system") ? stored : "system";
+ });
+ const [resolved, setResolved] = useState<"light" | "dark">(() => resolveTheme(theme));
+
+ const setTheme = (t: Theme) => {
+ setThemeState(t);
+ localStorage.setItem("hammer-theme", t);
+ };
+
+ // Apply class to
+ useEffect(() => {
+ const r = resolveTheme(theme);
+ setResolved(r);
+ document.documentElement.classList.toggle("dark", r === "dark");
+ }, [theme]);
+
+ // Listen for system theme changes
+ useEffect(() => {
+ if (theme !== "system") return;
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
+ const handler = () => {
+ const r = resolveTheme("system");
+ setResolved(r);
+ document.documentElement.classList.toggle("dark", r === "dark");
+ };
+ mq.addEventListener("change", handler);
+ return () => mq.removeEventListener("change", handler);
+ }, [theme]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTheme() {
+ return useContext(ThemeContext);
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index fb4e853..4a846dc 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,5 +1,7 @@
@import "tailwindcss";
+@custom-variant dark (&:where(.dark, .dark *));
+
@keyframes slide-in-right {
from {
transform: translateX(100%);
diff --git a/frontend/src/pages/ActivityPage.tsx b/frontend/src/pages/ActivityPage.tsx
index ebde861..653f140 100644
--- a/frontend/src/pages/ActivityPage.tsx
+++ b/frontend/src/pages/ActivityPage.tsx
@@ -50,7 +50,6 @@ export function ActivityPage() {
return items;
}, [tasks]);
- // Group by day
const groupedActivity = useMemo(() => {
const filtered =
filter === "all"
@@ -77,7 +76,7 @@ export function ActivityPage() {
if (loading && tasks.length === 0) {
return (
-
+
Loading activity...
);
@@ -85,18 +84,18 @@ export function ActivityPage() {
return (
-
+
-
๐ Activity Log
-
+
๐ Activity Log
+
{allActivity.length} updates across {tasks.length} tasks