Everyone with an account on this site. Click a role to change it.
diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx
index cc57b55..d080c78 100644
--- a/app/(app)/layout.tsx
+++ b/app/(app)/layout.tsx
@@ -30,7 +30,11 @@ export default async function AppLayout({ children }: { children: React.ReactNod
-
+ {/* On small screens the three-column shell would crush the
+ board; give it a sensible minimum width and let the page
+ scroll horizontally instead, and drop the Scheduled panel
+ (auxiliary on a phone) until the viewport can hold it. */}
+
{children}
diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx
index 541c541..9b0d75d 100644
--- a/app/(auth)/layout.tsx
+++ b/app/(auth)/layout.tsx
@@ -1,7 +1,17 @@
+import { NotebookPen } from "lucide-react";
+
export default function AuthLayout({ children }: { children: React.ReactNode }) {
return (
-
-
{children}
+
+
+
+
+
+
+ Organize
+
+ {children}
+
);
}
diff --git a/app/globals.css b/app/globals.css
index 51f8627..1b5af8f 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -4,14 +4,16 @@
@import "@uiw/react-md-editor/markdown-editor.css";
@import "@uiw/react-markdown-preview/markdown.css";
-@custom-variant dark (&:is(.dark *));
+/* "dark:" must be true for every dark-surfaced theme, not just .dark --
+ Ocean is a second dark look, so both classes turn on dark utilities. */
+@custom-variant dark (&:is(.dark *, .ocean *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
- --font-mono: var(--font-geist-mono);
- --font-heading: var(--font-sans);
+ --font-mono: var(--font-mono);
+ --font-heading: var(--font-heading);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -41,6 +43,10 @@
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
+ /* One more surface: the "lane" behind a category's cards -- sits between
+ the page background and the card, so cards read as objects resting on a
+ tray instead of floating on the page. */
+ --color-lane: var(--lane);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
@@ -50,77 +56,173 @@
--radius-4xl: calc(var(--radius) * 2.6);
}
-:root {
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
+/* ---------------------------------------------------------------------------
+ Themes
+ The app ships four looks, chosen via the theme picker (next-themes):
+ default -- calm, cool neutral light theme (the safe, focused one)
+ sunset -- warm paper + persimmon accent, a cozier light theme
+ dark -- deep charcoal with an indigo accent
+ ocean -- deep blue-green night theme with a seafoam accent
+ Each is a complete token set; components only ever use tokens, so adding
+ a fifth theme later is just another block like these.
+--------------------------------------------------------------------------- */
+
+:root,
+.theme-default {
+ color-scheme: light;
+ --background: oklch(0.985 0.002 260);
+ --foreground: oklch(0.235 0.02 265);
--card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
+ --card-foreground: oklch(0.235 0.02 265);
--popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- /* Single calm accent (indigo/violet) for interactive chrome -- buttons,
- active nav, focus rings, links -- kept deliberately separate from the
- curated vibrant Group palette in lib/colors.ts so the group colors stay
- the only "loud" color on screen. */
- --primary: oklch(0.53 0.24 274);
+ --popover-foreground: oklch(0.235 0.02 265);
+ /* Single calm accent (indigo) for interactive chrome -- buttons, active
+ nav, focus rings -- kept deliberately separate from the curated vibrant
+ Group palette in lib/colors.ts so the group colors stay the only
+ "loud" color on screen. */
+ --primary: oklch(0.53 0.2 272);
--primary-foreground: oklch(0.98 0.01 274);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.94 0.03 274);
- --accent-foreground: oklch(0.33 0.16 274);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.53 0.24 274);
+ --secondary: oklch(0.955 0.004 260);
+ --secondary-foreground: oklch(0.31 0.02 265);
+ --muted: oklch(0.95 0.004 260);
+ --muted-foreground: oklch(0.52 0.02 265);
+ --accent: oklch(0.95 0.022 272);
+ --accent-foreground: oklch(0.35 0.12 272);
+ --destructive: oklch(0.58 0.21 27);
+ --border: oklch(0.905 0.004 260);
+ --input: oklch(0.885 0.004 260);
+ --ring: oklch(0.58 0.15 272);
+ --lane: oklch(0.95 0.005 260);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
- --radius: 0.625rem;
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.53 0.24 274);
+ --radius: 0.75rem;
+ --sidebar: oklch(0.975 0.003 260);
+ --sidebar-foreground: oklch(0.235 0.02 265);
+ --sidebar-primary: oklch(0.53 0.2 272);
--sidebar-primary-foreground: oklch(0.98 0.01 274);
- --sidebar-accent: oklch(0.94 0.03 274);
- --sidebar-accent-foreground: oklch(0.33 0.16 274);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.53 0.24 274);
+ --sidebar-accent: oklch(0.95 0.022 272);
+ --sidebar-accent-foreground: oklch(0.35 0.12 272);
+ --sidebar-border: oklch(0.905 0.004 260);
+ --sidebar-ring: oklch(0.58 0.15 272);
}
.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.73 0.17 276);
- --primary-foreground: oklch(0.17 0.05 274);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.3 0.08 274);
- --accent-foreground: oklch(0.9 0.04 274);
- --destructive: oklch(0.704 0.191 22.216);
+ color-scheme: dark;
+ --background: oklch(0.175 0.012 265);
+ --foreground: oklch(0.93 0.01 265);
+ --card: oklch(0.22 0.013 265);
+ --card-foreground: oklch(0.93 0.01 265);
+ --popover: oklch(0.23 0.014 265);
+ --popover-foreground: oklch(0.93 0.01 265);
+ --primary: oklch(0.7 0.14 272);
+ --primary-foreground: oklch(0.16 0.04 272);
+ --secondary: oklch(0.27 0.012 265);
+ --secondary-foreground: oklch(0.93 0.01 265);
+ --muted: oklch(0.26 0.013 265);
+ --muted-foreground: oklch(0.68 0.02 265);
+ --accent: oklch(0.31 0.04 272);
+ --accent-foreground: oklch(0.9 0.03 272);
+ --destructive: oklch(0.68 0.19 24);
--border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.73 0.17 276);
+ --input: oklch(1 0 0 / 14%);
+ --ring: oklch(0.68 0.14 272);
+ --lane: oklch(1 0 0 / 5%);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.73 0.17 276);
- --sidebar-primary-foreground: oklch(0.17 0.05 274);
- --sidebar-accent: oklch(0.3 0.08 274);
- --sidebar-accent-foreground: oklch(0.9 0.04 274);
+ --radius: 0.75rem;
+ --sidebar: oklch(0.205 0.013 265);
+ --sidebar-foreground: oklch(0.93 0.01 265);
+ --sidebar-primary: oklch(0.7 0.14 272);
+ --sidebar-primary-foreground: oklch(0.16 0.04 272);
+ --sidebar-accent: oklch(0.31 0.04 272);
+ --sidebar-accent-foreground: oklch(0.9 0.03 272);
--sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.73 0.17 276);
+ --sidebar-ring: oklch(0.68 0.14 272);
+}
+
+.theme-sunset {
+ color-scheme: light;
+ --background: oklch(0.968 0.016 75);
+ --foreground: oklch(0.27 0.032 52);
+ --card: oklch(0.988 0.008 80);
+ --card-foreground: oklch(0.27 0.032 52);
+ --popover: oklch(0.99 0.007 80);
+ --popover-foreground: oklch(0.27 0.032 52);
+ /* Persimmon -- warm enough to feel like part of the paper, saturated
+ enough to stay unmistakably "the accent". */
+ --primary: oklch(0.58 0.19 42);
+ --primary-foreground: oklch(0.98 0.01 60);
+ --secondary: oklch(0.945 0.02 72);
+ --secondary-foreground: oklch(0.32 0.05 50);
+ --muted: oklch(0.938 0.02 72);
+ --muted-foreground: oklch(0.54 0.045 58);
+ --accent: oklch(0.928 0.035 62);
+ --accent-foreground: oklch(0.36 0.11 44);
+ --destructive: oklch(0.57 0.21 25);
+ --border: oklch(0.89 0.022 72);
+ --input: oklch(0.865 0.024 72);
+ --ring: oklch(0.6 0.16 44);
+ --lane: oklch(0.942 0.018 72);
+ --chart-1: oklch(0.79 0.12 58);
+ --chart-2: oklch(0.66 0.14 48);
+ --chart-3: oklch(0.53 0.13 42);
+ --chart-4: oklch(0.42 0.1 52);
+ --chart-5: oklch(0.31 0.07 58);
+ /* Slightly rounder than the default theme -- a touch softer overall. */
+ --radius: 0.9rem;
+ --sidebar: oklch(0.958 0.013 75);
+ --sidebar-foreground: oklch(0.27 0.032 52);
+ --sidebar-primary: oklch(0.58 0.19 42);
+ --sidebar-primary-foreground: oklch(0.98 0.01 60);
+ --sidebar-accent: oklch(0.928 0.035 62);
+ --sidebar-accent-foreground: oklch(0.36 0.11 44);
+ --sidebar-border: oklch(0.89 0.022 72);
+ --sidebar-ring: oklch(0.6 0.16 44);
+}
+
+.ocean {
+ color-scheme: dark;
+ --background: oklch(0.2 0.035 224);
+ --foreground: oklch(0.92 0.02 215);
+ --card: oklch(0.245 0.04 220);
+ --card-foreground: oklch(0.92 0.02 215);
+ --popover: oklch(0.255 0.042 218);
+ --popover-foreground: oklch(0.92 0.02 215);
+ /* Seafoam -- a cool, luminous accent that reads "water at night" against
+ the deep blue-green surfaces without screaming. */
+ --primary: oklch(0.76 0.11 192);
+ --primary-foreground: oklch(0.21 0.05 200);
+ --secondary: oklch(0.295 0.042 222);
+ --secondary-foreground: oklch(0.92 0.02 215);
+ --muted: oklch(0.285 0.042 222);
+ --muted-foreground: oklch(0.7 0.03 215);
+ --accent: oklch(0.33 0.05 214);
+ --accent-foreground: oklch(0.9 0.03 192);
+ --destructive: oklch(0.7 0.17 25);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 14%);
+ --ring: oklch(0.72 0.11 192);
+ --lane: oklch(1 0 0 / 6%);
+ --chart-1: oklch(0.76 0.1 192);
+ --chart-2: oklch(0.63 0.11 200);
+ --chart-3: oklch(0.5 0.11 215);
+ --chart-4: oklch(0.4 0.1 228);
+ --chart-5: oklch(0.3 0.08 240);
+ --radius: 0.8rem;
+ --sidebar: oklch(0.225 0.042 220);
+ --sidebar-foreground: oklch(0.92 0.02 215);
+ --sidebar-primary: oklch(0.76 0.11 192);
+ --sidebar-primary-foreground: oklch(0.21 0.05 200);
+ --sidebar-accent: oklch(0.33 0.05 214);
+ --sidebar-accent-foreground: oklch(0.9 0.03 192);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.72 0.11 192);
}
@layer base {
@@ -129,8 +231,117 @@
}
body {
@apply bg-background text-foreground;
+ /* Gentle, calm type for a tool that sits on screen all day. */
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
+ letter-spacing: -0.006em;
}
- html {
- @apply font-sans;
+ /* Headings use the display face (see --font-heading / Figtree). */
+ h1,
+ h2,
+ h3,
+ h4 {
+ letter-spacing: -0.018em;
}
-}
\ No newline at end of file
+ /* Smooth, low-key hover/selection feel: fade color changes instead of
+ snapping. Components that set their own transition-* utility override
+ this (utilities win on specificity). */
+ *,
+ *::before,
+ *::after {
+ transition:
+ background-color 200ms ease,
+ border-color 200ms ease,
+ color 200ms ease,
+ fill 200ms ease,
+ stroke 200ms ease;
+ }
+ @media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ transition: none !important;
+ animation: none !important;
+ }
+ }
+ ::selection {
+ background: color-mix(in srgb, var(--ring) 28%, transparent);
+ }
+ /* Quiet scrollbars that match the theme. */
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: color-mix(in srgb, var(--foreground) 22%, transparent) transparent;
+ }
+ *::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+ }
+ *::-webkit-scrollbar-track,
+ *::-webkit-scrollbar-corner {
+ background: transparent;
+ }
+ *::-webkit-scrollbar-thumb {
+ background: color-mix(in srgb, var(--foreground) 20%, transparent);
+ border: 3px solid transparent;
+ border-radius: 999px;
+ background-clip: padding-box;
+ }
+ *::-webkit-scrollbar-thumb:hover {
+ background: color-mix(in srgb, var(--foreground) 32%, transparent);
+ background-clip: padding-box;
+ }
+}
+
+/* ---------------------------------------------------------------------------
+ Markdown editor / preview (third-party CSS) -- fit the theme's tokens and
+ the app's type scale so notes blend into dialogs instead of looking
+ pasted in.
+--------------------------------------------------------------------------- */
+.wmde-markdown {
+ font-family: var(--font-sans);
+ font-size: 0.875rem;
+ letter-spacing: -0.006em;
+}
+.w-md-editor {
+ --md-editor-font-family: var(--font-sans);
+ font-size: 0.875rem;
+ color: var(--foreground);
+ background: var(--card);
+ border-color: var(--border);
+ box-shadow: none;
+}
+.w-md-editor .w-md-editor-toolbar {
+ background: color-mix(in srgb, var(--foreground) 5%, transparent);
+ border-color: var(--border);
+}
+.w-md-editor-toolbar-item,
+.w-md-editor-toolbar-item-icon {
+ color: var(--muted-foreground);
+}
+.w-md-editor-toolbar-item-active,
+.w-md-editor-toolbar-item-active .w-md-editor-toolbar-item-icon {
+ color: var(--foreground);
+}
+.w-md-editor-text,
+.w-md-editor-text-pre,
+.w-md-editor-text-input {
+ color: var(--foreground);
+}
+.w-md-editor-text-input::placeholder {
+ color: var(--muted-foreground);
+}
+/* Dark-surfaced themes (Dark, Ocean) -- the editor's stock CSS is
+ light-only, so override it where we render with data-color-mode="dark". */
+[data-color-mode="dark"] .w-md-editor {
+ background: var(--card);
+ border-color: var(--border);
+}
+[data-color-mode="dark"] .w-md-editor .w-md-editor-toolbar {
+ background: color-mix(in srgb, var(--foreground) 7%, transparent);
+ border-color: var(--border);
+}
+[data-color-mode="dark"] .w-md-editor .w-md-editor-text,
+[data-color-mode="dark"] .w-md-editor .w-md-editor-text-pre,
+[data-color-mode="dark"] .w-md-editor .w-md-editor-text-input {
+ color: var(--foreground);
+ background: transparent;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 9e93ed8..1e41757 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata, Viewport } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
+import { Figtree, Inter, JetBrains_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "@/components/theme/theme-provider";
@@ -7,13 +7,23 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/sonner";
import { RegisterServiceWorker } from "@/components/pwa/register-sw";
-const geistSans = Geist({
- variable: "--font-geist-sans",
+/** UI type: Inter -- a neutral, highly legible humanist sans that reads
+ * cleanly at small sizes (to-do lists, tables, nav). */
+const inter = Inter({
+ variable: "--font-sans",
subsets: ["latin"],
});
-const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
+/** Display type: Figtree -- a slightly friendlier, rounder geometric sans
+ * for headings and wordmarks. Same family, one step of personality. */
+const figtree = Figtree({
+ variable: "--font-heading",
+ subsets: ["latin"],
+});
+
+/** Code: JetBrains Mono, used for the markdown editor's monospace bits. */
+const jetbrainsMono = JetBrains_Mono({
+ variable: "--font-mono",
subsets: ["latin"],
});
@@ -34,8 +44,8 @@ export const metadata: Metadata = {
export const viewport: Viewport = {
themeColor: [
- { media: "(prefers-color-scheme: light)", color: "#ffffff" },
- { media: "(prefers-color-scheme: dark)", color: "#171717" },
+ { media: "(prefers-color-scheme: light)", color: "#fafafa" },
+ { media: "(prefers-color-scheme: dark)", color: "#22252b" },
],
};
@@ -43,11 +53,21 @@ export default function RootLayout({ children }: LayoutProps<"/">) {
return (
-
+ {/* Four looks: Default (calm light), Sunset (warm light), Dark, Ocean
+ (deep blue-green) -- complete token sets in app/globals.css.
+ "system" resolves to Default/Dark by OS preference.
+ No-FOUC theme restore: runs before first paint, applies the
+ stored preference to (class + color-scheme). */}
+
+
{children}
diff --git a/components/admin/user-role-menu.tsx b/components/admin/user-role-menu.tsx
index da25a08..13ea6fb 100644
--- a/components/admin/user-role-menu.tsx
+++ b/components/admin/user-role-menu.tsx
@@ -49,12 +49,21 @@ export function UserRoleMenu({ userId, role }: { userId: string; role: Role }) {
trigger -- so the Badge
+ // (a ) sits inside a bare reset button instead of being
+ // the trigger itself.
+
}
/>
diff --git a/components/board/add-category-lane.tsx b/components/board/add-category-lane.tsx
index 98f1339..f920859 100644
--- a/components/board/add-category-lane.tsx
+++ b/components/board/add-category-lane.tsx
@@ -38,11 +38,13 @@ export function AddCategoryLane() {
-
- Add Category
+
+
+
+ Add lane
}
/>
diff --git a/components/board/add-group-popover.tsx b/components/board/add-group-popover.tsx
index 27a65ac..b994fc3 100644
--- a/components/board/add-group-popover.tsx
+++ b/components/board/add-group-popover.tsx
@@ -49,7 +49,10 @@ export function AddGroupPopover({ categoryId }: { categoryId: string }) {
+
diff --git a/components/board/category-lane.tsx b/components/board/category-lane.tsx
index 9bc6ca9..dd5caaf 100644
--- a/components/board/category-lane.tsx
+++ b/components/board/category-lane.tsx
@@ -5,7 +5,7 @@ import { useSortable } from "@dnd-kit/sortable";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { useDndContext, useDroppable } from "@dnd-kit/core";
import { CSS } from "@dnd-kit/utilities";
-import { GripVertical, MoreVertical, Trash2 } from "lucide-react";
+import { GripVertical, MoreVertical, Plus, Trash2 } from "lucide-react";
import { cn } from "@/lib/utils";
import {
@@ -86,6 +86,12 @@ export function CategoryLane({ category }: { category: CategoryDTO }) {
});
const groupIds = visibleGroups.map((g) => g.id);
+ // Small open-count for the header -- "what's still to do in this lane"
+ // at a glance, the number a user organizing their day actually wants.
+ const openTodos = category.groups.reduce(
+ (n, g) => n + g.todos.filter((t) => !t.completed).length,
+ 0
+ );
return (
<>
@@ -93,21 +99,34 @@ export function CategoryLane({ category }: { category: CategoryDTO }) {
ref={setNodeRef}
style={{ transform: CSS.Transform.toString(transform), transition }}
className={cn(
- "flex h-full w-72 shrink-0 flex-col rounded-xl bg-muted/40",
+ "flex h-full w-72 shrink-0 flex-col overflow-hidden rounded-2xl border bg-lane",
isDragging && "opacity-50"
)}
>
-
diff --git a/components/board/empty-state.tsx b/components/board/empty-state.tsx
index e4e2d10..370a0f4 100644
--- a/components/board/empty-state.tsx
+++ b/components/board/empty-state.tsx
@@ -2,15 +2,17 @@ import { LayoutDashboard } from "lucide-react";
export function EmptyState() {
return (
-
-
-
+
+
+
+
+
+
Your board is empty
+
+ Lanes hold your groups of notes and to-dos. Add your first lane to
+ start organizing.
+
-
Your board is empty
-
- Create your first category to get started — categories are the lanes that hold your
- groups of notes and to-dos.
-
);
}
diff --git a/components/board/group-ai-dialog.tsx b/components/board/group-ai-dialog.tsx
index e73559a..cc5ce74 100644
--- a/components/board/group-ai-dialog.tsx
+++ b/components/board/group-ai-dialog.tsx
@@ -1,11 +1,11 @@
"use client";
import { useState } from "react";
-import { useTheme } from "next-themes";
import { toast } from "sonner";
import { Mic, Send, Sparkles, Square } from "lucide-react";
import { cn } from "@/lib/utils";
+import { colorModeFromTheme } from "@/components/theme/use-dark-theme";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
@@ -31,8 +31,7 @@ export function GroupAiDialog({
onOpenChange: (open: boolean) => void;
}) {
const { saveNote } = useBoard();
- const { resolvedTheme } = useTheme();
- const colorMode = resolvedTheme === "dark" ? "dark" : "light";
+ const colorMode = colorModeFromTheme();
const [messages, setMessages] = useState([]);
const [input, setInput] = useState("");
diff --git a/components/board/group-card-overlay.tsx b/components/board/group-card-overlay.tsx
index 8f1cd10..105721e 100644
--- a/components/board/group-card-overlay.tsx
+++ b/components/board/group-card-overlay.tsx
@@ -1,9 +1,9 @@
"use client";
-import { useTheme } from "next-themes";
import { NotebookPen } from "lucide-react";
import { getGroupColor } from "@/lib/colors";
+import { isDarkTheme } from "@/components/theme/use-dark-theme";
import { cn } from "@/lib/utils";
import type { GroupDTO } from "@/types/board";
@@ -14,18 +14,15 @@ import type { GroupDTO } from "@/types/board";
* would register a second useSortable/useDraggable for the same id.
*/
export function GroupCardOverlay({ group }: { group: GroupDTO }) {
- const { resolvedTheme } = useTheme();
+ const isDark = isDarkTheme();
const color = getGroupColor(group.color);
- const isDark = resolvedTheme === "dark";
const borderColor = isDark ? color.dark : color.light;
- const backgroundColor = isDark
- ? `color-mix(in srgb, ${color.dark} 18%, var(--card))`
- : `color-mix(in srgb, ${color.light} 12%, var(--card))`;
+ const backgroundColor = `color-mix(in srgb, ${isDark ? color.softDark : color.soft} 85%, var(--card))`;
return (
{group.title}
diff --git a/components/board/group-card.tsx b/components/board/group-card.tsx
index 83ed246..61dda70 100644
--- a/components/board/group-card.tsx
+++ b/components/board/group-card.tsx
@@ -3,7 +3,6 @@
import { useState } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
-import { useTheme } from "next-themes";
import {
Archive,
ChevronRight,
@@ -19,6 +18,7 @@ import {
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { TodoCheckbox } from "@/components/board/todo-checkbox";
import { MouseFollowTooltip } from "@/components/board/mouse-follow-tooltip";
import {
@@ -29,6 +29,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { getBrighterColor, getComplementaryColor, getGroupColor } from "@/lib/colors";
+import { isDarkTheme } from "@/components/theme/use-dark-theme";
import { useBoard } from "@/components/board/board-context";
import { useBoardView } from "@/components/board/board-view-provider";
import { useHoldOnComplete } from "@/components/hold-on-complete";
@@ -90,7 +91,9 @@ export function GroupCard({ group }: { group: GroupDTO }) {
const { view } = useBoardView();
const { holds, beginHold, cancelHold } = useHoldOnComplete();
const compact = view === "compact";
- const { resolvedTheme } = useTheme();
+ // True for any theme whose surfaces are dark -- Dark and Ocean both use
+ // their `dark` color variant (see lib/colors.ts); light themes use `light`.
+ const isDark = isDarkTheme();
const [editingTodo, setEditingTodo] = useState(null);
const [editOpen, setEditOpen] = useState(false);
const [confirmOpen, setConfirmOpen] = useState(false);
@@ -119,15 +122,14 @@ export function GroupCard({ group }: { group: GroupDTO }) {
const isDropTarget = isOver && !isDragging;
const color = getGroupColor(group.color);
- const isDark = resolvedTheme === "dark";
const borderColor = isDark ? color.dark : color.light;
- // A subdued tint of the same stroke color, blended into the theme's own
- // card surface -- not a fixed pastel, so it automatically stays correct
- // if the neutral card token ever changes, and needs no separate
- // light/dark background palette to hand-tune.
- const backgroundColor = isDark
- ? `color-mix(in srgb, ${color.dark} 18%, var(--card))`
- : `color-mix(in srgb, ${color.light} 12%, var(--card))`;
+ // The card fill is a calm, hand-picked tint of the same stroke color
+ // (`soft` / `softDark` in lib/colors.ts), blended a little into the theme's
+ // own card surface so it sits naturally on any theme -- the 85% weight
+ // keeps the tint in charge while still letting a tinted theme surface show
+ // through. Dark themes use `softDark`: the same hue at a dark-surface
+ // lightness, instead of the near-white pastel of `soft`.
+ const backgroundColor = `color-mix(in srgb, ${isDark ? color.softDark : color.soft} 85%, var(--card))`;
// Opposing hue from the group's own color, for the progress pie below --
// so it reads as a distinct accent rather than blending into the border.
const pieAccentColor = getComplementaryColor(
@@ -191,7 +193,7 @@ export function GroupCard({ group }: { group: GroupDTO }) {
backgroundColor,
}}
className={cn(
- "relative rounded-xl border-[3px] p-3 shadow-sm transition-shadow",
+ "relative rounded-2xl border-[3px] p-3 shadow-sm transition-shadow",
"hover:-translate-y-0.5 hover:shadow-md",
isDragging && "opacity-50 shadow-lg",
isDropTarget && "ring-2 ring-primary ring-offset-2 ring-offset-background"
@@ -225,7 +227,9 @@ export function GroupCard({ group }: { group: GroupDTO }) {
)}
-
{group.title}
+
+ {group.title}
+
@@ -263,7 +267,7 @@ export function GroupCard({ group }: { group: GroupDTO }) {
{!isCollapsed && (
<>
-