From b6b1690fdd4bc3fc6f47416e2b08ce1f2ca3d68e Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Tue, 25 Aug 2026 13:06:53 -0600 Subject: [PATCH] Redesign UI theming, typography, and board polish - Replace next-themes with a custom ThemeProvider supporting four themes (Default, Sunset, Dark, Ocean) since next-themes' no-FOUC script tag breaks under React 19 dev warnings - Swap Geist fonts for Inter/Figtree/JetBrains Mono for a friendlier display face and add a shared isDarkTheme/colorModeFromTheme helper so components stop reading next-themes directly - Add per-color "soft"/"softDark" tints in lib/colors.ts so group cards carry a calm hue-tinted fill instead of a generic gray - Restyle nav, board lanes, group cards, empty states, and page headers with heading font, tighter spacing, and rounded-2xl surfaces - Add lane "open to-dos" counts, board summary stats, and a quick "+ To-do" action targeting the first available group - Hide the Scheduled panel below sm and give the app shell a minimum width with horizontal scroll instead of crushing the board - Fix DropdownMenuLabel to self-wrap in a Menu.Group (Base UI requires the context) and wrap the admin role Badge trigger in a native button --- app/(app)/admin/page.tsx | 6 +- app/(app)/layout.tsx | 6 +- app/(auth)/layout.tsx | 14 +- app/globals.css | 327 +++++++++++++++++---- app/layout.tsx | 38 ++- components/admin/user-role-menu.tsx | 19 +- components/board/add-category-lane.tsx | 8 +- components/board/add-group-popover.tsx | 5 +- components/board/category-lane.tsx | 36 ++- components/board/color-swatch-picker.tsx | 5 +- components/board/empty-state.tsx | 18 +- components/board/group-ai-dialog.tsx | 5 +- components/board/group-card-overlay.tsx | 11 +- components/board/group-card.tsx | 70 +++-- components/board/kanban-board.tsx | 68 ++++- components/board/mouse-follow-tooltip.tsx | 5 +- components/board/notes-dialog.tsx | 5 +- components/board/status-update-dialog.tsx | 6 +- components/board/todo-create-dialog.tsx | 5 +- components/board/todo-create-popover.tsx | 7 +- components/board/todo-edit-dialog.tsx | 5 +- components/chat/chat-page-client.tsx | 25 +- components/nav/projects-nav-section.tsx | 2 +- components/nav/side-nav-item.tsx | 2 +- components/nav/side-nav.tsx | 38 +-- components/projects/project-card.tsx | 4 +- components/projects/projects-list-view.tsx | 34 ++- components/scheduled/scheduled-panel.tsx | 12 +- components/theme/theme-provider.tsx | 153 +++++++++- components/theme/theme-toggle.tsx | 72 ++++- components/ui/dropdown-menu.tsx | 23 +- components/ui/sonner.tsx | 10 +- lib/colors.ts | 37 ++- 33 files changed, 828 insertions(+), 253 deletions(-) diff --git a/app/(app)/admin/page.tsx b/app/(app)/admin/page.tsx index 09c4866..48e3ce9 100644 --- a/app/(app)/admin/page.tsx +++ b/app/(app)/admin/page.tsx @@ -37,9 +37,9 @@ export default async function AdminPage() { return (
-
-

Admin

-

+

+

Admin

+

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). */} +