diff --git a/apps/desktop-ui/DESIGN.md b/apps/desktop-ui/DESIGN.md new file mode 100644 index 00000000..f85a045e --- /dev/null +++ b/apps/desktop-ui/DESIGN.md @@ -0,0 +1,62 @@ +# DESIGN — MyDevTools Desktop ("Pro Instrument") + +The desktop app is a **pro instrument**, not a web dashboard. Reference the things +that make Figma, Linear, Raycast, and Adobe apps feel native and expensive: +custom window chrome, dense information, one confident accent, keyboard-first +navigation, and motion that only ever conveys state. + +Register: **product** (design serves the task). The tool disappears into the work. + +## Anti-references (what it must NOT look like) +- shadcn/Vercel starter dark mode (neutral zinc). The current dark theme IS this — replace it. +- A 247px white sidebar + uniform card grid + `0.5rem` radius everywhere (SaaS template silhouette). +- User-selectable accent colors (blue/purple/green picker). One brand accent only. +- A browser-style OS title bar sitting above web content. + +## Theme +- **Dark is the signature and default.** One genuinely-refined **Light** exists (manual toggle). No `system`, no color picker. +- Graphite blue-black surfaces (hue ~225, low chroma), stepped by lightness — never pure black, never zinc-neutral. + +## Color (HSL tokens, dark) +| Role | Token | Value | Use | +|---|---|---|---| +| App base | `--background` | `225 14% 8%` | behind panels, titlebar | +| Panel | `--surface-1` / `--card` | `225 13% 11%` | content surface | +| Raised | `--surface-2` | `225 12% 13%` | toolbars, tab strip | +| Hover/input | `--surface-3` | `225 11% 16%` | inputs, hovered rows | +| Ink | `--foreground` | `220 16% 96%` | primary text | +| Muted ink | `--muted-foreground` | `222 10% 64%` | labels (≥4.5:1) | +| Hairline | `--border` | `225 9% 20%` | 1px separators | +| **Accent** | `--primary` / `--ring` | `235 84% 67%` | **electric indigo — selection + primary action + state only, never decoration** | + +- Per-**category** accent colors stay (they help navigate 80 tools). The single brand accent is separate. +- Semantic: destructive red kept; success/warning/info to be added as tokens when needed. +- Mono (`--font-geist-mono`) for all values: hashes, JSON, timestamps, byte counts. Values feel like data. + +## Typography +- One family: **Geist Sans** (UI) + **Geist Mono** (data). No display pairing. +- Fixed rem scale, tight ratio (~1.2). UI is dense: ~13px controls, 12px secondary labels, 11px micro. Reading prose 14px, capped 65–75ch. +- Headings tighten letter-spacing slightly; never fluid/clamp in-app. + +## Shape, density, elevation +- `--radius: 0.375rem` (6px) base; controls tighter. No 8px-everywhere. +- Dense spacing on a 4px grid. Rely on **surface steps + hairline borders**, not shadows. One soft shadow reserved for popovers / the ⌘K palette. + +## Chrome architecture +- **Custom unified titlebar+toolbar** (Tauri, decorations off): inset macOS traffic lights, app mark, current-tool breadcrumb, ⌘K trigger, actions. No OS strip. +- **Left nav**: dense sidebar (~220px) collapsible to a ~52px icon rail. Not a white 247px web sidebar. +- **⌘K command palette**: the primary way to move across 80 tools. + +## Tabs (Figma-style) +- **Not pills.** Square tabs, softly rounded **top** corners only (`rounded-t`, square bottom). +- Tabs sit on a raised strip (`--surface-2`) and align to its bottom edge. +- **Dividers** (1px) between inactive tabs; suppressed next to the active tab. +- **Active tab is welded to the content**: its background matches the content surface and it breaks the strip's bottom border (the seam), with a 2px accent bar on top. Clear top-strip / content separation. +- Monochrome tool icon (accent-tinted when active) — no random rainbow chips. + +## Motion +- 120–200ms, ease-out (quart/expo). State only: selection, panel open, palette, feedback. No page-load choreography, no decorative motion. +- Every animation has a `prefers-reduced-motion` fallback. + +## Component contract +Every interactive element ships default/hover/focus/active/disabled/loading. Skeletons over spinners. Empty states teach. Same button/control vocabulary everywhere. diff --git a/apps/desktop-ui/src/app/globals.css b/apps/desktop-ui/src/app/globals.css index 76fed569..88d4cd27 100644 --- a/apps/desktop-ui/src/app/globals.css +++ b/apps/desktop-ui/src/app/globals.css @@ -38,7 +38,7 @@ body { --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; - --radius: 0.5rem; + --radius: 0.375rem; --surface-1: 0 0% 100%; --surface-2: 225 24% 97.5%; --surface-3: 225 22% 94.5%; @@ -61,144 +61,50 @@ body { } .dark { - /* Minimalist dark theme - shadcn neutral zinc defaults */ - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - /* Chart colors remain balanced */ - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --card: 240 10% 5.5%; - --surface-1: 240 8% 7%; - --surface-2: 240 7% 9%; - --surface-3: 240 6% 12%; - /* Sidebar aligned to the deck blue-black theme (was a desaturated grey) */ - --sidebar-background: 240 10% 5%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 0 0% 98%; - --sidebar-primary-foreground: 240 5.9% 10%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 240 4.9% 83.9%; - } - - /* Color Theme Overrides - Light Mode */ - :root.blue { - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 0 0% 98%; - --ring: 217.2 91.2% 59.8%; - --sidebar-primary: 217.2 91.2% 59.8%; - } - - :root.purple { - --primary: 258.3 89.5% 66.3%; - --primary-foreground: 0 0% 98%; - --ring: 258.3 89.5% 66.3%; - --sidebar-primary: 258.3 89.5% 66.3%; - } - - :root.green { - --primary: 142.1 76.2% 36.3%; - --primary-foreground: 0 0% 98%; - --ring: 142.1 76.2% 36.3%; - --sidebar-primary: 142.1 76.2% 36.3%; - } - - :root.orange { - --primary: 24.6 95% 53.1%; - --primary-foreground: 0 0% 98%; - --ring: 24.6 95% 53.1%; - --sidebar-primary: 24.6 95% 53.1%; - } - - :root.red { - --primary: 0 72.2% 50.6%; - --primary-foreground: 0 0% 98%; - --ring: 0 72.2% 50.6%; - --sidebar-primary: 0 72.2% 50.6%; - } - - :root.pink { - --primary: 340.7 82.1% 52.7%; - --primary-foreground: 0 0% 98%; - --ring: 340.7 82.1% 52.7%; - --sidebar-primary: 340.7 82.1% 52.7%; - } - - :root.cyan { - --primary: 199.1 89.1% 48.2%; - --primary-foreground: 0 0% 98%; - --ring: 199.1 89.1% 48.2%; - --sidebar-primary: 199.1 89.1% 48.2%; - } - - :root.indigo { - --primary: 239.4 83.9% 66.7%; - --primary-foreground: 0 0% 98%; - --ring: 239.4 83.9% 66.7%; - --sidebar-primary: 239.4 83.9% 66.7%; - } - - /* Color Theme Overrides - Dark Mode */ - .dark.blue { - --primary: 217.2 91.2% 59.8%; - --sidebar-primary: 217.2 91.2% 59.8%; - } - - .dark.purple { - --primary: 258.3 89.5% 66.3%; - --sidebar-primary: 258.3 89.5% 66.3%; - } - - .dark.green { - --primary: 142.1 70.6% 45.3%; - --sidebar-primary: 142.1 70.6% 45.3%; - } - - .dark.orange { - --primary: 24.6 95% 53.1%; - --sidebar-primary: 24.6 95% 53.1%; - } - - .dark.red { - --primary: 0 72.2% 50.6%; - --sidebar-primary: 0 72.2% 50.6%; - } - - .dark.pink { - --primary: 340.7 82.1% 52.7%; - --sidebar-primary: 340.7 82.1% 52.7%; - } - - .dark.cyan { - --primary: 199.1 89.1% 48.2%; - --sidebar-primary: 199.1 89.1% 48.2%; + /* Pro Instrument — graphite blue-black (hue ~225, low chroma). Surfaces + step by lightness; one electric-indigo accent; hairline borders. */ + --background: 225 14% 8%; /* app base behind panels + titlebar */ + --foreground: 220 16% 96%; + --card: 225 13% 11%; /* = surface-1 (content panels) */ + --card-foreground: 220 16% 96%; + --popover: 225 14% 13%; /* menus / ⌘K palette, slightly lifted */ + --popover-foreground: 220 16% 96%; + --primary: 235 84% 67%; /* signature accent — electric indigo */ + --primary-foreground: 0 0% 100%; + --secondary: 225 10% 16%; + --secondary-foreground: 220 14% 92%; + --muted: 225 10% 15%; + --muted-foreground: 222 10% 64%; /* ≥4.5:1 on surfaces */ + --accent: 225 11% 18%; /* hover tint (neutral, not brand) */ + --accent-foreground: 220 16% 96%; + --destructive: 0 72% 57%; + --destructive-foreground: 0 0% 100%; + --border: 225 9% 20%; /* hairline */ + --input: 225 9% 22%; + --ring: 235 84% 67%; + /* Chart / category palette (kept vivid for data + category accents) */ + --chart-1: 235 84% 67%; + --chart-2: 190 80% 55%; + --chart-3: 160 60% 50%; + --chart-4: 40 90% 62%; + --chart-5: 330 75% 62%; + --surface-1: 225 13% 11%; + --surface-2: 225 12% 13%; + --surface-3: 225 11% 16%; + --sidebar-background: 225 14% 9%; + --sidebar-foreground: 222 11% 72%; + --sidebar-primary: 235 84% 67%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 225 12% 15%; + --sidebar-accent-foreground: 220 16% 96%; + --sidebar-border: 225 9% 18%; + --sidebar-ring: 235 84% 67%; } - .dark.indigo { - --primary: 239.4 83.9% 66.7%; - --sidebar-primary: 239.4 83.9% 66.7%; - } + /* Pro Instrument commits to ONE brand accent (electric indigo, set above). + The user-selectable accent-color overrides were removed intentionally — + multiple brand colors dilute identity and read as a web-app template. + Per-category tool accents live in CATEGORY_ACCENT, not here. */ } /* Custom Scrollbar for Sidebar */ diff --git a/apps/desktop-ui/src/app/layout.tsx b/apps/desktop-ui/src/app/layout.tsx index 390f2741..69fd690b 100644 --- a/apps/desktop-ui/src/app/layout.tsx +++ b/apps/desktop-ui/src/app/layout.tsx @@ -118,8 +118,8 @@ export default async function RootLayout({ diff --git a/apps/desktop-ui/src/components/dashboard/dashboard-hero.tsx b/apps/desktop-ui/src/components/dashboard/dashboard-hero.tsx index 1054da6e..befde774 100644 --- a/apps/desktop-ui/src/components/dashboard/dashboard-hero.tsx +++ b/apps/desktop-ui/src/components/dashboard/dashboard-hero.tsx @@ -48,7 +48,7 @@ function Stat({ 'flex h-7 w-7 shrink-0 items-center justify-center rounded-lg ring-1 ring-inset ring-border/50', isZero ? 'bg-muted text-muted-foreground' - : 'bg-gradient-to-br from-primary/15 to-violet-500/10 text-primary', + : 'bg-primary/[0.12] text-primary', )} > @@ -120,10 +120,10 @@ export function DashboardHero({

- + {dashboardGreeting(t)}

-

+

{user?.displayName ? t('welcomeBackNamed', { name: user.displayName.split(' ')[0] }) : t('welcomeBack')} diff --git a/apps/desktop-ui/src/components/dashboard/dashboard-tool-card.tsx b/apps/desktop-ui/src/components/dashboard/dashboard-tool-card.tsx index bdc678f0..6f286555 100644 --- a/apps/desktop-ui/src/components/dashboard/dashboard-tool-card.tsx +++ b/apps/desktop-ui/src/components/dashboard/dashboard-tool-card.tsx @@ -95,7 +95,7 @@ export const ToolCard = React.memo(function ToolCard({ @@ -169,16 +169,12 @@ export function GlobalCommandPalette() { className={cn( 'overflow-hidden p-0 gap-0 max-w-2xl', 'max-h-[min(85vh,640px)] flex flex-col', - 'rounded-2xl border-border/60 bg-popover/90 backdrop-blur-2xl', - 'shadow-2xl shadow-primary/10 ring-1 ring-inset ring-white/[0.04]' + 'rounded-xl border-border bg-popover/95 backdrop-blur-xl', + 'shadow-2xl shadow-black/40' )} > Search tools and pages - {/* Gradient hairline — the palette's signature accent */} -
+
+
+ + {/* Draggable spacer — the breathing room in the middle */} +
+ + {/* Right cluster */} +
+ + +
+ + + + + + + + + + + {isLoggedIn ? ( + <> +
+

{displayName}

+ {user.email ? ( +

{user.email}

+ ) : null} +
+ + + + Profile + + + + + Settings + + + + + Help + + + + void signOut()} + className={cn('cursor-pointer gap-2.5 text-destructive focus:text-destructive')} + > + Sign out + + + ) : ( + + + Sign in + + + )} +
+
+
+ + ) +} diff --git a/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx b/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx index 183479e0..9e3ffd01 100644 --- a/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx +++ b/apps/desktop-ui/src/components/sidebar/app-sidebar.tsx @@ -1,6 +1,4 @@ 'use client' -import { useEffect, useState } from 'react' -import Image from 'next/image' import Link from 'next/link' import { usePathname } from 'next/navigation' import { motion } from 'framer-motion' @@ -9,133 +7,28 @@ import { Sidebar, SidebarContent, SidebarFooter, - SidebarHeader, SidebarRail, } from '@/components/ui/sidebar' import { NavGroup } from './nav-group' -import { NavUser } from './nav-user' import { FeedbackDialog } from '@/components/feedback-dialog' -import { Logo } from '../logo' import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar' import { LayoutDashboard, Sparkles } from 'lucide-react' -import { onAuthStateChanged, signOut as firebaseSignOut } from 'firebase/auth' -import { auth } from '../../database/firebase' -import { useRouter } from 'next/navigation' -import { usePasswordStore } from '@/store/password-store' -import { useEnvironmentManagerStore } from '@/store/environment-manager-store' -import { useMasterKeyStore } from '@/store/master-key-store' -import { useUserKeypairStore } from '@/store/user-keypair-store' -import { clearMasterKey } from '@/lib/key-storage' -import { logoutBackendSession } from '@/lib/backend-auth' import { usePinnedToolsForActiveWorkspace } from '@/store/pinned-tools-store' -import { useWorkspaceStore, useActiveWorkspace } from '@/store/workspace-store' +import { useActiveWorkspace } from '@/store/workspace-store' import { IconPin } from '@tabler/icons-react' import { buildPinnedNavItems } from './app-sidebar.helpers' export function AppSidebar({ ...props }: React.ComponentProps) { - const { clearPasswords } = usePasswordStore() - const { clearSets } = useEnvironmentManagerStore() - const { clearKey: clearMasterKeyStore } = useMasterKeyStore() - const { clear: clearUserKeypair } = useUserKeypairStore() const pinnedTools = usePinnedToolsForActiveWorkspace() - const clearWorkspaceStore = useWorkspaceStore((s) => s.clear) const activeWs = useActiveWorkspace() const pinnedNavItems = buildPinnedNavItems(pinnedTools, activeWs) - const [user, setUser] = useState({ - name: '', - email: '', - avatar: '' - }) - - const router = useRouter(); const pathname = usePathname(); const isDashboardActive = pathname === '/dashboard'; - // ... - - const handleSignOut = async () => { - try { - clearPasswords() // clear decrypted passwords from memory - clearSets() // clear decrypted environment sets from memory - clearMasterKeyStore() // clear global master key in-memory state - clearUserKeypair() // clear workspace keypair from memory - clearWorkspaceStore() // clear workspace selection on sign-out - - // Clear password-manager vault key from IndexedDB - if (typeof window !== 'undefined' && window.indexedDB) { - await new Promise((resolve) => { - const req = window.indexedDB.open("PasswordManagerDB", 1) - req.onsuccess = (e: any) => { - const db = e.target.result - if (db.objectStoreNames.contains("keys")) { - const tx = db.transaction("keys", "readwrite") - tx.objectStore("keys").delete("vaultKey") - tx.oncomplete = () => resolve() - tx.onerror = () => resolve() - } else { - resolve() - } - } - req.onerror = () => resolve() - }) - } - - // Clear global master key from IndexedDB - await clearMasterKey() - - await logoutBackendSession() - await firebaseSignOut(auth); - router.push('/login'); - } catch (error) { - console.error('Error signing out:', error); - } - }; - - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (user) => { - if (user) { - setUser({ - name: user.displayName || '', - email: user.email || '', - avatar: user.photoURL || '' - }) - } - }) - - return () => unsubscribe() - }, []) - return ( - -
router.push('/dashboard')} - > -
- -
-
-
-
- MyDevTools - MyDevTools -
-
-

Developer's Toolkit

-
-
-
+ {/* Brand lockup moved to the unified TopBar (components/shell/top-bar.tsx). + The sidebar is now pure navigation. */} @@ -152,10 +45,10 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {isDashboardActive && ( - + )} @@ -187,7 +80,6 @@ export function AppSidebar({ ...props }: React.ComponentProps) { -
diff --git a/apps/desktop-ui/src/components/sidebar/client-layout.tsx b/apps/desktop-ui/src/components/sidebar/client-layout.tsx index 5a2edee4..14367310 100644 --- a/apps/desktop-ui/src/components/sidebar/client-layout.tsx +++ b/apps/desktop-ui/src/components/sidebar/client-layout.tsx @@ -6,6 +6,7 @@ import { AppSidebar } from "./app-sidebar"; import { NavBar } from '@/components/nav-bar'; import { MobileNav } from '@/components/mobile-nav'; import { TabBar } from '@/components/tab-bar/tab-bar'; +import { TopBar } from '@/components/shell/top-bar'; import { useTabStore } from '@/store/tab-store'; import { isTabRoute } from '@/lib/route-config'; import { getTabComponent, isRegisteredTab } from '@/lib/tab-registry'; @@ -77,10 +78,14 @@ function Layout({ children }: { children: React.ReactNode }) { return (
-
+ + {/* transform establishes a containing block so the sidebar's `fixed` + positioning is bounded to this row (below the top bar) instead of + the viewport top — otherwise it overlaps the top bar / logo. */} +