From 46f02ba03bf71fee415af31d0a1bfc7d8dab1f88 Mon Sep 17 00:00:00 2001 From: anuphilipp3 Date: Thu, 25 Jun 2026 16:12:03 +0530 Subject: [PATCH 1/2] Dashboard revamp --- apps/web/src/app/dashboard/page.tsx | 169 +++-- apps/web/src/app/globals.css | 203 ++++-- apps/web/src/app/profile/page.tsx | 16 +- apps/web/src/app/settings/page.tsx | 40 +- .../web/src/components/app-loading-screen.tsx | 39 ++ .../app-tools/client-only-tool-loaders.tsx | 7 +- apps/web/src/components/branded-toaster.tsx | 32 + apps/web/src/components/client-shell.tsx | 2 +- .../dashboard/activity/activity-log-panel.tsx | 12 +- .../dashboard/dashboard-featured.tsx | 54 ++ .../components/dashboard/dashboard-hero.tsx | 75 ++- .../dashboard/dashboard-login-cta.tsx | 8 +- .../dashboard/dashboard-pinned-section.tsx | 60 +- .../dashboard/dashboard-recent-tools.tsx | 36 +- .../components/dashboard/dashboard-reveal.tsx | 37 ++ .../dashboard/dashboard-search-bar.tsx | 10 +- .../dashboard/dashboard-section-header.tsx | 57 ++ .../dashboard/dashboard-tool-card.tsx | 45 +- .../dashboard/dashboard-tool-grid.tsx | 77 +-- .../dashboard/dashboard-whats-new.tsx | 18 +- .../src/components/ensure-backend-session.tsx | 9 +- .../src/components/global-command-palette.tsx | 12 +- .../graphql-formatter-layout.tsx | 2 +- .../json-formatter/json-formatter-layout.tsx | 12 +- .../lorem-ipsum/lorem-ipsum-layout.tsx | 2 +- .../src/components/master-password-gate.tsx | 17 +- apps/web/src/components/mobile-nav.tsx | 6 +- .../mock-data-generator-layout.tsx | 2 +- apps/web/src/components/nav-bar.tsx | 49 +- apps/web/src/components/onboarding-modal.tsx | 600 +++++++++++++++++- apps/web/src/components/require-auth.tsx | 9 +- .../secret-api-key-generator-layout.tsx | 2 +- .../src/components/sidebar/app-sidebar.tsx | 31 +- apps/web/src/components/sidebar/nav-group.tsx | 23 +- .../sql-formatter/sql-formatter-layout.tsx | 2 +- .../ssh-key-generator-layout.tsx | 2 +- apps/web/src/components/ui/button.tsx | 10 +- apps/web/src/components/ui/card.tsx | 2 +- apps/web/src/components/ui/dialog.tsx | 2 +- apps/web/src/components/ui/dropdown-menu.tsx | 12 +- apps/web/src/components/ui/input.tsx | 2 +- apps/web/src/components/ui/popover.tsx | 2 +- apps/web/src/components/ui/select.tsx | 6 +- apps/web/src/components/ui/tabs.tsx | 4 +- apps/web/src/components/ui/tooltip.tsx | 2 +- .../uuid-generator/uuid-generator-layout.tsx | 2 +- .../yaml-formatter/yaml-formatter-layout.tsx | 2 +- apps/web/src/hooks/use-count-up.ts | 36 ++ 48 files changed, 1513 insertions(+), 346 deletions(-) create mode 100644 apps/web/src/components/app-loading-screen.tsx create mode 100644 apps/web/src/components/branded-toaster.tsx create mode 100644 apps/web/src/components/dashboard/dashboard-featured.tsx create mode 100644 apps/web/src/components/dashboard/dashboard-reveal.tsx create mode 100644 apps/web/src/components/dashboard/dashboard-section-header.tsx diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index dea12ed1..3aebbfcd 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useMemo } from 'react' import dynamic from 'next/dynamic' import Link from 'next/link' -import { BarChart3, LayoutGrid } from 'lucide-react' +import { ArrowLeft, ArrowRight, BarChart3, LayoutGrid } from 'lucide-react' import { useTranslations } from 'next-intl' import { sidebarData } from '@/components/sidebar/data/sidebar-data' import { usePinnedToolsStore } from '@/store/pinned-tools-store' @@ -30,6 +30,9 @@ import { DashboardRecentTools } from '@/components/dashboard/dashboard-recent-to import { ActivityLogDrawer } from '@/components/dashboard/activity/activity-log-drawer' import { DashboardLoginCta } from '@/components/dashboard/dashboard-login-cta' import { DashboardToolGrid } from '@/components/dashboard/dashboard-tool-grid' +import { DashboardFeatured } from '@/components/dashboard/dashboard-featured' +import { RevealItem } from '@/components/dashboard/dashboard-reveal' +import { AppLoadingScreen } from '@/components/app-loading-screen' // Lazy-load the analytics panel — it's behind a tab click, not in the critical path @@ -66,6 +69,8 @@ const DashboardPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState('') const [filterGroup, setFilterGroup] = useState(null) const [showFilters, setShowFilters] = useState(false) + // Home-screen model: 'home' shows a curated launchpad; 'all' opens the full catalog. + const [view, setView] = useState<'home' | 'all'>('home') const totalTools = useMemo(() => getTotalToolCount(), []) @@ -240,21 +245,20 @@ const DashboardPage: React.FC = () => { }, []) if (loading) { - return ( -
-
-
-
-
-
- ) + return } const toolCardProps = { user, isPinned, togglePin } + // Home-screen model: curated launchpad by default; the full catalog only shows + // when the user searches, filters, or explicitly opens "Browse all". + const showCurated = view === 'home' && !searchQuery && !filterGroup + const isBrowsing = !!searchQuery || !!filterGroup || view === 'all' + return ( -
+
+
{/* ── Mobile Sticky Header (outside padded container for full-bleed) ── */} {
{/* ── Desktop Hero (inside padded container for alignment) ── */} - + + +
- + {tTabs('apps')} @@ -306,41 +312,100 @@ const DashboardPage: React.FC = () => { - {/* ── Pinned Tools ── */} - - - {/* ── What's New ── */} - - - {/* ── Recently Used ── */} - - - {/* ── Login CTA (non-auth) ── */} - + {/* ── Curated home (launchpad) ── */} + {showCurated && ( + <> + {/* Pinned — your go-to tools */} + + + + + {/* Recently used — jump back in */} + + + + + {/* Suggested / popular — discovery */} + + + + + {/* What's new */} + + + + + {/* Login CTA (non-auth) */} + + + + + {/* Browse-all entry — the door to the full catalog */} + + + + + )} - {/* ── All Tools Grid ── */} - + {/* ── Full catalog (searching, filtering, or "Browse all") ── */} + {isBrowsing && ( + + {view === 'all' && !searchQuery && !filterGroup && ( + + )} + + + )} diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css index cade53e4..34ad86ec 100644 --- a/apps/web/src/app/globals.css +++ b/apps/web/src/app/globals.css @@ -13,42 +13,43 @@ body { @layer base { :root { - --background: 40 18% 93%; - --foreground: 220 14% 14%; - --card: 40 14% 95%; - --card-foreground: 220 14% 14%; - --popover: 40 14% 95%; - --popover-foreground: 220 14% 14%; - --primary: 220 14% 16%; - --primary-foreground: 40 18% 93%; - --secondary: 40 10% 89%; - --secondary-foreground: 220 14% 14%; - --muted: 40 10% 89%; - --muted-foreground: 220 12% 34%; - --accent: 40 10% 89%; - --accent-foreground: 220 14% 14%; + /* Light "deck-in-daylight": crisp white panels lifting off a soft cool canvas */ + --background: 225 24% 96.5%; + --foreground: 222 32% 11%; + --card: 0 0% 100%; + --card-foreground: 222 32% 11%; + --popover: 0 0% 100%; + --popover-foreground: 222 32% 11%; + --primary: 222 47% 16%; + --primary-foreground: 0 0% 100%; + --secondary: 225 22% 93.5%; + --secondary-foreground: 222 30% 16%; + --muted: 225 22% 93.5%; + --muted-foreground: 222 14% 42%; + --accent: 225 24% 93%; + --accent-foreground: 222 30% 16%; --destructive: 0 72% 51%; - --destructive-foreground: 40 18% 93%; - --border: 40 10% 83%; - --input: 40 10% 83%; - --ring: 220 14% 16%; + --destructive-foreground: 0 0% 100%; + --border: 222 18% 88%; + --input: 222 18% 88%; + --ring: 222 47% 16%; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; --radius: 0.5rem; - --surface-1: 40 14% 91%; - --surface-2: 40 12% 87%; - --surface-3: 40 10% 83%; - --sidebar-background: 40 14% 91%; - --sidebar-foreground: 220 10% 24%; - --sidebar-primary: 220 14% 16%; - --sidebar-primary-foreground: 40 18% 93%; - --sidebar-accent: 40 10% 89%; - --sidebar-accent-foreground: 220 14% 14%; - --sidebar-border: 40 10% 83%; - --sidebar-ring: 220 14% 16%; + --surface-1: 0 0% 100%; + --surface-2: 225 24% 97.5%; + --surface-3: 225 22% 94.5%; + --sidebar-background: 0 0% 100%; + --sidebar-foreground: 222 18% 28%; + --sidebar-primary: 222 47% 16%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 225 26% 94.5%; + --sidebar-accent-foreground: 222 32% 14%; + --sidebar-border: 222 18% 90%; + --sidebar-ring: 222 47% 16%; --sidebar-width: 247px; --main-content-width: calc(100% - var(--sidebar-width)); @@ -90,8 +91,8 @@ body { --surface-1: 240 8% 7%; --surface-2: 240 7% 9%; --surface-3: 240 6% 12%; - /* Sidebar with neutral dark theme */ - --sidebar-background: 240 5.9% 10%; + /* 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%; @@ -459,14 +460,6 @@ body { border-color: hsl(var(--primary) / 0.2); } - /* Clean gradient backgrounds to remove blue tint */ - .dark .bg-gradient-to-br { - background-image: linear-gradient(135deg, - hsl(var(--background)), - hsl(var(--background)), - hsl(var(--muted))); - } - /* Standard neutral background for body */ .dark body { background: hsl(var(--background)); @@ -811,6 +804,76 @@ body { box-shadow: 0 0 20px hsl(var(--primary) / 0.1); } + /* Dashboard greeting — brand gradient both themes (deep, legible stops in light) */ + .dash-greeting { + background: linear-gradient(100deg, #4338ca 0%, #7c3aed 52%, #0e7490 100%); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + } + .dark .dash-greeting { + background: var(--mdt-grad); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + } + + /* Ambient top glow for the dashboard surface (theme-aware, decorative) */ + .dash-ambient { + position: absolute; + inset: 0 0 auto 0; + height: 26rem; + pointer-events: none; + background: + radial-gradient(58% 100% at 12% 0%, rgba(99, 102, 241, 0.16), transparent 68%), + radial-gradient(50% 100% at 50% 0%, rgba(124, 58, 237, 0.10), transparent 70%), + radial-gradient(58% 100% at 90% 0%, rgba(34, 211, 238, 0.12), transparent 70%); + } + .dark .dash-ambient { + background: + radial-gradient(60% 100% at 18% 0%, hsl(var(--primary) / 0.16), transparent 70%), + radial-gradient(55% 100% at 88% 0%, rgba(79, 208, 230, 0.12), transparent 70%); + } + + /* Tool card gradient sheen revealed on hover (transform/opacity only) */ + .dash-card-sheen::before { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient(130deg, hsl(var(--primary) / 0.55), transparent 40%, transparent 60%, rgba(79, 208, 230, 0.45)); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity var(--dur-2, 0.2s) var(--ease-out-quart, ease); + } + .group:hover .dash-card-sheen::before { + opacity: 1; + } + /* Cursor-anchored glow — illuminates the card under the pointer (set via --mx/--my). */ + .dash-card-sheen::after { + content: ''; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: radial-gradient( + 240px circle at var(--mx, 50%) var(--my, 50%), + hsl(var(--primary) / 0.13), + rgba(79, 208, 230, 0.06) 35%, + transparent 60% + ); + opacity: 0; + transition: opacity var(--dur-2, 0.2s) var(--ease-out-quart, ease); + } + .group:hover .dash-card-sheen::after { + opacity: 1; + } + .dark .stats-glow { box-shadow: 0 0 30px hsl(var(--primary) / 0.15); } @@ -1359,6 +1422,16 @@ body { /* Disable animations on low-end devices */ @media (prefers-reduced-motion: reduce) { + /* Universal reset — honor the OS-level "reduce motion" preference everywhere */ + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + .fab-pulse, .animate-float, .animate-float-delayed, @@ -1629,6 +1702,58 @@ html { overflow-x: hidden; } } @keyframes mdt-spin { to { --mdt-angle: 360deg; } } +/* ── Branded app loading screen (theme-aware) ───────────── */ +.mdt-loading__aura { + position: absolute; inset: 0; pointer-events: none; + background: + radial-gradient(40rem 28rem at 50% 42%, hsl(var(--primary) / 0.10), transparent 70%), + radial-gradient(26rem 26rem at 66% 66%, rgba(154, 92, 242, 0.10), transparent 70%); +} +.mdt-loading__ring { + position: absolute; width: 8.5rem; height: 8.5rem; border-radius: 50%; + background: conic-gradient(from var(--mdt-angle), transparent 0 58%, #5b63f0 74%, #9a5cf2 86%, #4fd0e6 95%, transparent 100%); + -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3px)); + mask: radial-gradient(farthest-side, transparent calc(100% - 4px), #000 calc(100% - 3px)); + animation: mdt-spin 1.4s linear infinite; + filter: drop-shadow(0 0 10px rgba(122, 107, 245, 0.35)); +} +.mdt-loading__bar { + position: relative; display: block; width: 11rem; height: 3px; border-radius: 999px; + overflow: hidden; background: hsl(var(--foreground) / 0.08); +} +.mdt-loading__bar > span { + position: absolute; inset: 0; width: 40%; border-radius: inherit; + background: var(--mdt-grad); + animation: mdt-loading-slide 1.25s cubic-bezier(0.65, 0, 0.35, 1) infinite; +} +@keyframes mdt-loading-slide { + 0% { transform: translateX(-110%); } + 100% { transform: translateX(360%); } +} +.mdt-loading__dots::after { + content: "…"; display: inline-block; width: 1.2ch; text-align: left; + animation: mdt-loading-dots 1.4s steps(1, end) infinite; +} +@keyframes mdt-loading-dots { + 0% { content: ""; } 33% { content: "·"; } 66% { content: "··"; } 100% { content: "···"; } +} +@media (prefers-reduced-motion: reduce) { + .mdt-loading__ring { animation-duration: 3s; } + .mdt-loading__bar > span { animation: mdt-loading-pulse 1.6s ease-in-out infinite; width: 100%; } + .mdt-loading__dots::after { animation: none; content: "…"; } +} +@keyframes mdt-loading-pulse { 0%, 100% { opacity: .35; } 50% { opacity: 1; } } + +/* Compact branded spinner for per-tool lazy loading */ +.mdt-tool-spinner { + width: 2.25rem; height: 2.25rem; border-radius: 50%; + background: conic-gradient(from var(--mdt-angle), transparent 0 55%, #5b63f0 74%, #9a5cf2 88%, #4fd0e6 96%, transparent 100%); + -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2px)); + mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2px)); + animation: mdt-spin 1.1s linear infinite; +} +@media (prefers-reduced-motion: reduce) { .mdt-tool-spinner { animation-duration: 2.4s; } } + /* scroll-progress beam */ .mdt-progress { position: fixed; top: 0; left: 0; right: 0; height: 2px; transform-origin: 0 50%; background: var(--mdt-grad); z-index: 60; } diff --git a/apps/web/src/app/profile/page.tsx b/apps/web/src/app/profile/page.tsx index f732366e..90141c09 100644 --- a/apps/web/src/app/profile/page.tsx +++ b/apps/web/src/app/profile/page.tsx @@ -252,9 +252,13 @@ export default function ProfilePage() {
{/* ── Page header ── */}
-
-

Your Profile

-

Manage your identity and developer portfolio.

+
+

+ + Developer identity +

+

Your Profile

+

Manage your identity and developer portfolio.

diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index e33e8a49..75d93715 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -52,22 +52,30 @@ export default function SettingsPage() { } return ( -
-
-
-

{t('title')}

-

{t('subtitle')}

-
+
+
+

+ + Workspace +

+

{t('title')}

+

{t('subtitle')}

+<<<<<<< Updated upstream +======= + +>>>>>>> Stashed changes - - + + + + {t('tools.title')} @@ -125,10 +133,12 @@ export default function SettingsPage() { - + - - + + + + {t('appearance.title')} @@ -192,10 +202,12 @@ export default function SettingsPage() { - + - - + + + + {t('language.title')} diff --git a/apps/web/src/components/app-loading-screen.tsx b/apps/web/src/components/app-loading-screen.tsx new file mode 100644 index 00000000..9816973c --- /dev/null +++ b/apps/web/src/components/app-loading-screen.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Logo } from "@/components/logo"; + +/** + * Branded full-screen loading state for the authenticated shell. + * Theme-aware (works in light and dark) — a spinning deck-gradient ring + * orbiting the logo mark, with an indeterminate progress beam. + */ +export function AppLoadingScreen({ label = "Preparing your workspace" }: { label?: string }) { + return ( +
+
+ +
+ +
+ +
+
+ +
+

+ {label} + +

+ + + +
+ + Loading +
+ ); +} diff --git a/apps/web/src/components/app-tools/client-only-tool-loaders.tsx b/apps/web/src/components/app-tools/client-only-tool-loaders.tsx index 71f1ab7c..91ee2f87 100644 --- a/apps/web/src/components/app-tools/client-only-tool-loaders.tsx +++ b/apps/web/src/components/app-tools/client-only-tool-loaders.tsx @@ -3,8 +3,11 @@ import dynamic from 'next/dynamic' const toolLoading = () => ( -
-
+
+
+ + Loading tool… +
) diff --git a/apps/web/src/components/branded-toaster.tsx b/apps/web/src/components/branded-toaster.tsx new file mode 100644 index 00000000..529abc8f --- /dev/null +++ b/apps/web/src/components/branded-toaster.tsx @@ -0,0 +1,32 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as SonnerToaster } from "sonner" + +/** + * App-wide toast styling. Theme-aware (syncs with next-themes) and matched to + * the overlay design language — rounded-xl, soft border, blur, layered shadow. + */ +export function BrandedToaster() { + const { resolvedTheme } = useTheme() + + return ( + + ) +} diff --git a/apps/web/src/components/client-shell.tsx b/apps/web/src/components/client-shell.tsx index 205435e6..e97a878d 100644 --- a/apps/web/src/components/client-shell.tsx +++ b/apps/web/src/components/client-shell.tsx @@ -20,7 +20,7 @@ const ClientSpeedInsights = dynamic( { ssr: false } ) const ClientToaster = dynamic( - () => import('sonner').then((m) => m.Toaster), + () => import('@/components/branded-toaster').then((m) => m.BrandedToaster), { ssr: false } ) diff --git a/apps/web/src/components/dashboard/activity/activity-log-panel.tsx b/apps/web/src/components/dashboard/activity/activity-log-panel.tsx index c471e6c2..b609db35 100644 --- a/apps/web/src/components/dashboard/activity/activity-log-panel.tsx +++ b/apps/web/src/components/dashboard/activity/activity-log-panel.tsx @@ -216,7 +216,7 @@ export function ActivityLogPanel({ embedded = false }: { embedded?: boolean }) { className={cn( 'h-7 cursor-pointer rounded-full border px-2.5 text-xs font-medium transition-colors', active - ? 'border-primary bg-primary text-primary-foreground' + ? 'border-transparent bg-gradient-to-r from-primary to-violet-500 text-primary-foreground shadow-sm' : 'border-border bg-card text-muted-foreground hover:bg-muted/50', )} aria-pressed={active} @@ -396,9 +396,13 @@ export function ActivityLogPanel({ embedded = false }: { embedded?: boolean }) { return (
-
-

{t('title')}

-

{t('subtitle')}

+
+

+ + Audit trail +

+

{t('title')}

+

{t('subtitle')}

{filters} diff --git a/apps/web/src/components/dashboard/dashboard-featured.tsx b/apps/web/src/components/dashboard/dashboard-featured.tsx new file mode 100644 index 00000000..3ff17b8b --- /dev/null +++ b/apps/web/src/components/dashboard/dashboard-featured.tsx @@ -0,0 +1,54 @@ +'use client' + +import React from 'react' +import { TrendingUp } from 'lucide-react' +import { useTranslations } from 'next-intl' +import { type RenderToolItem, type ToolCardProps } from './types' +import { ToolCard } from './dashboard-tool-card' +import { DashboardSectionHeader } from './dashboard-section-header' + +const FEATURED_ACCENT = { bg: 'bg-primary/15', text: 'text-primary' } + +interface DashboardFeaturedProps { + items: RenderToolItem[] + toolCardProps: Omit + searchQuery: string + filterGroup: string | null +} + +/** + * "Popular tools" featured panel — a bento-style highlighted starting point. + * Hidden while searching/filtering (the grid below handles those states). + */ +export function DashboardFeatured({ + items, + toolCardProps, + searchQuery, + filterGroup, +}: DashboardFeaturedProps) { + const t = useTranslations('Dashboard') + + if (items.length === 0 || searchQuery || filterGroup) return null + + return ( +
+ +
+ {items.map((item, index) => ( + + ))} +
+
+ ) +} diff --git a/apps/web/src/components/dashboard/dashboard-hero.tsx b/apps/web/src/components/dashboard/dashboard-hero.tsx index 70146402..1054da6e 100644 --- a/apps/web/src/components/dashboard/dashboard-hero.tsx +++ b/apps/web/src/components/dashboard/dashboard-hero.tsx @@ -5,6 +5,8 @@ import Link from 'next/link' import { Layers, Zap, Pin, Clock, History } from 'lucide-react' import { useTranslations } from 'next-intl' import { dashboardGreeting } from './types' +import { useCountUp } from '@/hooks/use-count-up' +import { cn } from '@/lib/utils' interface DashboardHeroProps { user: { displayName?: string | null } | null @@ -27,8 +29,8 @@ interface DashboardHeroProps { * must be outside the padded container for full-bleed, while the * desktop hero sits inside it for proper alignment). */ -/** Compact KPI stat tile for the dashboard hero. */ -function KpiCard({ +/** A single count-up stat in the hero strip. */ +function Stat({ icon: Icon, label, value, @@ -37,15 +39,29 @@ function KpiCard({ label: string value: number }) { + const display = useCountUp(value) + const isZero = value === 0 return ( -
-
- -
-
-

{label}

-

{value}

-
+
+ + + + + {display} + + {label}
) } @@ -101,26 +117,27 @@ export function DashboardHero({ {/* ── Desktop Header Section ──────────────────────────────────────── */} {!mobileOnly && ( -
-
-
-

- {dashboardGreeting(t)} -

-

- {user?.displayName - ? t('welcomeBackNamed', { name: user.displayName.split(' ')[0] }) - : t('welcomeBack')} -

-

{t('tagline')}

-
+
+
+

+ + {dashboardGreeting(t)} +

+

+ {user?.displayName + ? t('welcomeBackNamed', { name: user.displayName.split(' ')[0] }) + : t('welcomeBack')} +

+

{t('tagline')}

+
- {/* Quick Stats — KPI row */} -
- - - -
+ {/* Quick stats — inline strip with count-up */} +
+ + + + +
)} diff --git a/apps/web/src/components/dashboard/dashboard-login-cta.tsx b/apps/web/src/components/dashboard/dashboard-login-cta.tsx index a1f2f5e3..871fa177 100644 --- a/apps/web/src/components/dashboard/dashboard-login-cta.tsx +++ b/apps/web/src/components/dashboard/dashboard-login-cta.tsx @@ -40,11 +40,11 @@ export function DashboardLoginCta({ user, searchQuery }: DashboardLoginCtaProps) ))}
{/* Overlay CTA */} -
-
- +
+
+
-

+

{t('loginCta.description')}

diff --git a/apps/web/src/components/dashboard/dashboard-tool-grid.tsx b/apps/web/src/components/dashboard/dashboard-tool-grid.tsx index 4b1ab152..0b2450dc 100644 --- a/apps/web/src/components/dashboard/dashboard-tool-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-tool-grid.tsx @@ -1,7 +1,8 @@ 'use client' import React from 'react' -import { Sparkles } from 'lucide-react' +import { motion } from 'framer-motion' +import { Sparkles, SearchX } from 'lucide-react' import { useTranslations } from 'next-intl' import { type RenderGroup, @@ -13,6 +14,7 @@ import { categoryAccent, } from './types' import { ToolCard } from './dashboard-tool-card' +import { DashboardSectionHeader } from './dashboard-section-header' interface DashboardToolGridProps { filteredGroups: RenderGroup[] @@ -41,49 +43,47 @@ export function DashboardToolGrid({ const accent = categoryAccent(group.title) return (
-
-
-
- {group.icon ? ( - - ) : ( - - )} -
-

{groupDisplayTitle(group.title, t)}

- - {group.items.reduce( - (acc, item) => acc + (item.items ? item.items.length : 1), - 0, - )} - -
-
+ acc + (item.items ? item.items.length : 1), + 0, + )} + />
{group.items.map((item: any, itemIndex: number) => ( {/* Render search result items (flattened) */} {item.originalId && ( - + + + )} {/* Render nested items directly in the grid (only when not searching) */} {item.items && item.items.map((subItem: ToolItem, subIndex: number) => ( - + layout="position" + className="h-full" + > + + ))} ))} @@ -94,9 +94,14 @@ export function DashboardToolGrid({ {/* No results state */} {(searchQuery || filterGroup) && filteredGroups.length === 0 && (
-
-

{t('noResults')}

-

{t('noResultsHint')}

+
+
+ +
+
+

{t('noResults')}

+

{t('noResultsHint')}

+
{popularItems.length > 0 && (
diff --git a/apps/web/src/components/dashboard/dashboard-whats-new.tsx b/apps/web/src/components/dashboard/dashboard-whats-new.tsx index 82b27af3..bf2eaf0e 100644 --- a/apps/web/src/components/dashboard/dashboard-whats-new.tsx +++ b/apps/web/src/components/dashboard/dashboard-whats-new.tsx @@ -5,6 +5,9 @@ import { Wand2 } from 'lucide-react' import { useTranslations } from 'next-intl' import { type RenderToolItem, type ToolCardProps } from './types' import { ToolCard, HScrollFade } from './dashboard-tool-card' +import { DashboardSectionHeader } from './dashboard-section-header' + +const WHATS_NEW_ACCENT = { bg: 'bg-amber-500/15', text: 'text-amber-600 dark:text-amber-400' } interface DashboardWhatsNewProps { whatsNewItems: RenderToolItem[] @@ -29,15 +32,12 @@ export function DashboardWhatsNew({ return (
-
-
- -
-

{t('sections.whatsNew')}

- - {whatsNewItems.length} - -
+ {/* Mobile: horizontal scroll */}
diff --git a/apps/web/src/components/ensure-backend-session.tsx b/apps/web/src/components/ensure-backend-session.tsx index 7721b251..85e1590d 100644 --- a/apps/web/src/components/ensure-backend-session.tsx +++ b/apps/web/src/components/ensure-backend-session.tsx @@ -2,8 +2,8 @@ import { useEffect, useState } from "react" import type { User } from "firebase/auth" -import { Loader2 } from "lucide-react" import { ensureBackendSession } from "@/lib/backend-auth" +import { AppLoadingScreen } from "@/components/app-loading-screen" type Props = { user: User | null @@ -51,12 +51,7 @@ export function EnsureBackendSession({ user, children }: Props) { }, [user]) if (user && !ready) { - return ( -
- -

Loading…

-
- ) + return } return <>{children} diff --git a/apps/web/src/components/global-command-palette.tsx b/apps/web/src/components/global-command-palette.tsx index 78ef5481..baea1c23 100644 --- a/apps/web/src/components/global-command-palette.tsx +++ b/apps/web/src/components/global-command-palette.tsx @@ -174,7 +174,11 @@ export function GlobalCommandPalette() { shouldFilter loop > - + No results found. {recentEntries.length > 0 && ( @@ -187,7 +191,7 @@ export function GlobalCommandPalette() { key={`recent-${entry.url}`} value={`recent ${entry.searchValue} ${entry.url}`.toLowerCase()} onSelect={() => run(entry)} - className="flex items-start gap-3 py-2.5 aria-selected:bg-accent" + className="mx-1 flex items-start gap-3 rounded-lg py-2.5 transition-colors aria-selected:bg-gradient-to-r aria-selected:from-primary/[0.12] aria-selected:to-primary/[0.04] aria-selected:ring-1 aria-selected:ring-inset aria-selected:ring-primary/15" > @@ -217,7 +221,7 @@ export function GlobalCommandPalette() { key={`pinned-${entry.url}`} value={`pinned ${entry.searchValue} ${entry.url}`.toLowerCase()} onSelect={() => run(entry)} - className="flex items-start gap-3 py-2.5 aria-selected:bg-accent" + className="mx-1 flex items-start gap-3 rounded-lg py-2.5 transition-colors aria-selected:bg-gradient-to-r aria-selected:from-primary/[0.12] aria-selected:to-primary/[0.04] aria-selected:ring-1 aria-selected:ring-inset aria-selected:ring-primary/15" > ))} -
+
↑↓ diff --git a/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx b/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx index 4435f6dc..71b4cdca 100644 --- a/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx +++ b/apps/web/src/components/graphql-formatter/graphql-formatter-layout.tsx @@ -151,7 +151,7 @@ export function GraphqlFormatterLayout() {
- diff --git a/apps/web/src/components/json-formatter/json-formatter-layout.tsx b/apps/web/src/components/json-formatter/json-formatter-layout.tsx index 4bcf4b39..96359b84 100644 --- a/apps/web/src/components/json-formatter/json-formatter-layout.tsx +++ b/apps/web/src/components/json-formatter/json-formatter-layout.tsx @@ -289,11 +289,11 @@ export function JsonFormatterLayout() { const renderPaneToolbar = (pane: PaneKey) => { const state = pane === 'left' ? leftPane : rightPane return ( -
+
handlePaneNameChange(pane, event.target.value)} - className="h-8 w-full sm:w-[170px] rounded-md border border-input bg-background px-2.5 text-xs text-foreground outline-none ring-offset-background transition-colors focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" + className="h-8 w-full sm:w-[170px] rounded-md border border-input bg-background px-2.5 text-xs font-medium text-foreground outline-none transition-[color,border-color,box-shadow] duration-150 hover:border-border focus-visible:border-primary/50 focus-visible:ring-2 focus-visible:ring-primary/20" aria-label={pane === 'left' ? t('leftDocNameLabel') : t('rightDocNameLabel')} /> - @@ -323,7 +323,7 @@ export function JsonFormatterLayout() {
-
+
@@ -367,7 +367,7 @@ export function JsonFormatterLayout() { {isMobile ? ( /* Mobile: single full-height pane, toggled by tabs */ -
+
{renderPaneToolbar(activePane)}
{activePane === 'left' ? ( @@ -398,7 +398,7 @@ export function JsonFormatterLayout() { /* Desktop: side-by-side resizable panes */
diff --git a/apps/web/src/components/lorem-ipsum/lorem-ipsum-layout.tsx b/apps/web/src/components/lorem-ipsum/lorem-ipsum-layout.tsx index cabbc3fd..4a9ed376 100644 --- a/apps/web/src/components/lorem-ipsum/lorem-ipsum-layout.tsx +++ b/apps/web/src/components/lorem-ipsum/lorem-ipsum-layout.tsx @@ -130,7 +130,7 @@ export function LoremIpsumLayout() {
- diff --git a/apps/web/src/components/master-password-gate.tsx b/apps/web/src/components/master-password-gate.tsx index 35fde7cb..0f896711 100644 --- a/apps/web/src/components/master-password-gate.tsx +++ b/apps/web/src/components/master-password-gate.tsx @@ -294,6 +294,16 @@ export function MasterPasswordGate() { if (showBackupCodes) e.preventDefault() }} > + {/* Premium branded top accent (deck signature) */} +
+
+ {/* Hidden accessible title/description for screen readers */} @@ -405,10 +415,10 @@ export function MasterPasswordGate() { />
{mode === "loading" ? ( @@ -676,6 +686,7 @@ export function MasterPasswordGate() {
- diff --git a/apps/web/src/components/nav-bar.tsx b/apps/web/src/components/nav-bar.tsx index dd906347..11500722 100644 --- a/apps/web/src/components/nav-bar.tsx +++ b/apps/web/src/components/nav-bar.tsx @@ -2,10 +2,43 @@ import { usePathname } from "next/navigation"; import { useTranslations, useMessages } from "next-intl"; +import { Search } from "lucide-react"; import { ModeToggle } from "@/components/modeToggle"; import { getToolMessageKey } from "@/lib/tool-i18n"; import { routeConfig } from "@/lib/route-config"; +function openCommandPalette() { + document.dispatchEvent(new CustomEvent("open-command-palette")); +} + +/** Global ⌘K launcher — keeps the command-center pattern on every app page. */ +function CommandTrigger() { + return ( + <> + + + + ); +} + /** Strip HTML-like tags and ICU {variable} placeholders from rich-text subtitles. */ function cleanSubtitle(raw: string): string { return raw @@ -29,8 +62,9 @@ export function NavBar() { if (!config) { if (pathname.startsWith("/app")) { return ( -
-
+
+
+
@@ -50,10 +84,10 @@ export function NavBar() { const subtitle = rawSubtitle ? cleanSubtitle(rawSubtitle) : undefined; return ( -
-
-
-
+
+
+
+
@@ -67,7 +101,8 @@ export function NavBar() { )}
-
+ +
diff --git a/apps/web/src/components/onboarding-modal.tsx b/apps/web/src/components/onboarding-modal.tsx index 4ffb73f8..bdfd7b9b 100644 --- a/apps/web/src/components/onboarding-modal.tsx +++ b/apps/web/src/components/onboarding-modal.tsx @@ -8,10 +8,101 @@ import { Progress } from "@/components/ui/progress" import { completeOnboarding } from "@/lib/onboarding-api" import { useToolVisibilityStore } from "@/store/tool-visibility-store" import { patchUserPreferences } from "@/lib/user-preferences-api" +<<<<<<< Updated upstream import { IconArrowRight, IconCheck, IconX } from "@tabler/icons-react" import { WelcomeStep } from "./onboarding-modal/welcome-step" import { TourStep, TOUR_SLIDES } from "./onboarding-modal/tour-step" import { ToolSelectionStep, buildCategories } from "./onboarding-modal/tool-selection" +======= +import { + IconRocket, + IconShield, + IconBraces, + IconArrowsExchange, + IconWand, + IconNetwork, + IconServer, + IconPalette, + IconDeviceGamepad2, + IconCheck, + IconArrowRight, + IconLayout, + IconCommand, + IconPin, + IconX, + IconChevronDown as IconChevron, +} from "@tabler/icons-react" +import { sidebarData } from "@/components/sidebar/data/sidebar-data" +import type { NavLink, NavCollapsible } from "@/components/sidebar/types" +import MdtAurora from "@/components/mdt-aurora" + +// ── Tool category icons by title ────────────────────────────────────────────── + +const CATEGORY_ICONS: Record = { + Productivity: IconRocket, + Security: IconShield, + Formatters: IconBraces, + Converters: IconArrowsExchange, + Generators: IconWand, + "Network & API": IconNetwork, + Database: IconServer, + "Media & Design": IconPalette, + "Break Room": IconDeviceGamepad2, +} + +const CATEGORY_COLORS: Record = { + Productivity: "from-violet-500/20 to-violet-600/10 border-violet-500/30", + Security: "from-green-500/20 to-green-600/10 border-green-500/30", + Formatters: "from-blue-500/20 to-blue-600/10 border-blue-500/30", + Converters: "from-cyan-500/20 to-cyan-600/10 border-cyan-500/30", + Generators: "from-amber-500/20 to-amber-600/10 border-amber-500/30", + "Network & API": "from-orange-500/20 to-orange-600/10 border-orange-500/30", + Database: "from-red-500/20 to-red-600/10 border-red-500/30", + "Media & Design": "from-pink-500/20 to-pink-600/10 border-pink-500/30", + "Break Room": "from-emerald-500/20 to-emerald-600/10 border-emerald-500/30", +} + +const CATEGORY_ICON_COLORS: Record = { + Productivity: "text-violet-400", + Security: "text-green-400", + Formatters: "text-blue-400", + Converters: "text-cyan-400", + Generators: "text-amber-400", + "Network & API": "text-orange-400", + Database: "text-red-400", + "Media & Design": "text-pink-400", + "Break Room": "text-emerald-400", +} + +// ── Tour slides ─────────────────────────────────────────────────────────────── + +const TOUR_SLIDES = [ + { + icon: IconLayout, + iconColor: "text-violet-400", + bg: "from-violet-500/10 to-transparent", + title: "Your workspace, your way", + description: + "Every tool lives in the sidebar. Open multiple tools in tabs — they stay open until you close them. Your layout is restored on every visit.", + }, + { + icon: IconCommand, + iconColor: "text-blue-400", + bg: "from-blue-500/10 to-transparent", + title: "Command palette", + description: + "Press ⌘K (or Ctrl+K) to instantly search and launch any tool, jump to settings, or switch tabs — without touching the mouse.", + }, + { + icon: IconPin, + iconColor: "text-amber-400", + bg: "from-amber-500/10 to-transparent", + title: "Pin your favourites", + description: + "Right-click any tool in the sidebar to pin it to the top. Pinned tools are synced across all your devices.", + }, +] +>>>>>>> Stashed changes // ── Step indicators ─────────────────────────────────────────────────────────── @@ -26,6 +117,439 @@ function StepDot({ active, done }: { active: boolean; done: boolean }) { ) } +<<<<<<< Updated upstream +======= +// ── Step 0: Welcome ─────────────────────────────────────────────────────────── + +const WELCOME_HIGHLIGHTS = [ + { icon: IconBraces, label: "60+ tools" }, + { icon: IconCommand, label: "⌘K to launch" }, + { icon: IconShield, label: "Encrypted sync" }, + { icon: IconLayout, label: "Yours, instantly" }, +] + +function WelcomeStep() { + return ( +
+ + Welcome aboard + + + + Your entire dev toolkit,{" "} + one tab away. + + + + Let's take 30 seconds to build your workspace and learn the two shortcuts you'll use + every day. + + + + {WELCOME_HIGHLIGHTS.map(({ icon: Icon, label }) => ( +
+ + {label} +
+ ))} +
+
+ ) +} + +// ── Step 1: Tool selection ──────────────────────────────────────────────────── + +/** Flip this to switch between selection UIs without touching anything else. */ +const TOOL_SELECTION_MODE: "tags" | "checkboxes" = "tags" + +interface ToolEntry { + title: string + url: string + icon?: React.ElementType +} + +interface Category { + title: string + tools: ToolEntry[] +} + +function buildCategories(): Category[] { + return sidebarData.navGroups.map((g) => ({ + title: g.title, + tools: g.items.flatMap((item) => { + if (!("items" in item)) { + const link = item as NavLink + return [{ title: link.title, url: String(link.url), icon: link.icon }] + } + return (item as NavCollapsible).items.map((s) => ({ + title: s.title, + url: String(s.url), + icon: s.icon, + })) + }), + })) +} + +interface SelectionProps { + selectedUrls: Set + onToggleTool: (url: string) => void + onToggleCategory: (urls: string[], selected: boolean) => void +} + +// ── Tags UI ─────────────────────────────────────────────────────────────────── + +function TagsToolSelection({ selectedUrls, onToggleTool, onToggleCategory }: SelectionProps) { + const categories = React.useMemo(() => buildCategories(), []) + const allUrls = React.useMemo(() => categories.flatMap((c) => c.tools.map((t) => t.url)), [categories]) + const allTools = React.useMemo(() => categories.flatMap((c) => c.tools), [categories]) + const selectedTools = allTools.filter((t) => selectedUrls.has(t.url)) + + return ( +
+
+

Step 2 · Build your workspace

+

Pick the tools you'll use

+

+ They get pinned to your sidebar — watch it fill as you choose. Change anytime in Settings. +

+
+ +
+ {/* Picker */} + +
+ {categories.map((cat) => { + const urls = cat.tools.map((t) => t.url) + const allSelected = urls.every((u) => selectedUrls.has(u)) + const Icon = CATEGORY_ICONS[cat.title] ?? IconWand + const iconColor = CATEGORY_ICON_COLORS[cat.title] ?? "text-primary" + + return ( +
+
+ + + {cat.title} + +
+ +
+ +
+ {cat.tools.map((tool) => { + const selected = selectedUrls.has(tool.url) + const ToolIcon = tool.icon + return ( + onToggleTool(tool.url)} + whileTap={{ scale: 0.94 }} + className={cn( + "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-all duration-150", + selected + ? "border-transparent bg-gradient-to-r from-[#5b63f0] via-[#9a5cf2] to-[#4fd0e6] text-[#0a0b12] shadow-sm shadow-violet-500/25" + : "border-border bg-muted/40 text-muted-foreground hover:border-primary/40 hover:bg-muted/70 hover:text-foreground" + )} + > + {ToolIcon && } + {tool.title} + + ) + })} +
+
+ ) + })} +
+ + + {/* Live sidebar preview */} + +
+ +
+

+ {selectedUrls.size} tools enabled +

+
+ + · + +
+
+
+ ) +} + +// ── Checkboxes UI ───────────────────────────────────────────────────────────── + +function CheckboxCategoryRow({ + cat, + selectedUrls, + onToggleTool, + onToggleCategory, +}: SelectionProps & { cat: Category }) { + const [expanded, setExpanded] = useState(false) + const urls = cat.tools.map((t) => t.url) + const selectedCount = urls.filter((u) => selectedUrls.has(u)).length + const allSelected = selectedCount === urls.length + const someSelected = selectedCount > 0 && selectedCount < urls.length + const Icon = CATEGORY_ICONS[cat.title] ?? IconWand + const iconColor = CATEGORY_ICON_COLORS[cat.title] ?? "text-primary" + const color = CATEGORY_COLORS[cat.title] ?? "from-primary/20 to-primary/10 border-primary/30" + + return ( +
+
+ + + +
+ + + {expanded && ( + +
+ {cat.tools.map((tool) => { + const ToolIcon = tool.icon + const checked = selectedUrls.has(tool.url) + return ( + + ) + })} +
+
+ )} +
+
+ ) +} + +function CheckboxesToolSelection({ selectedUrls, onToggleTool, onToggleCategory }: SelectionProps) { + const categories = React.useMemo(() => buildCategories(), []) + const allUrls = React.useMemo(() => categories.flatMap((c) => c.tools.map((t) => t.url)), [categories]) + + return ( +
+
+

Pick your tools

+

+ Toggle categories or expand to select individual tools. Change anytime in Settings. +

+
+ + +
+ {categories.map((cat) => ( + + ))} +
+
+ +
+

+ {selectedUrls.size} tools enabled +

+
+ + · + +
+
+
+ ) +} + +// ── Dispatcher ──────────────────────────────────────────────────────────────── + +function ToolSelectionStep(props: SelectionProps) { + if (TOOL_SELECTION_MODE === "checkboxes") return + return +} + +// ── Step 2: Tour ────────────────────────────────────────────────────────────── + +function TourStep({ slideIndex, onSlide }: { slideIndex: number; onSlide: (i: number) => void }) { + const slide = TOUR_SLIDES[slideIndex]! + + return ( +
+
+

Step 3 · The shortcuts you'll live in

+

Quick tour

+
+ +
+ + + + + +
+

{slide.title}

+

{slide.description}

+
+
+
+
+ + {/* slide dots */} +
+ {TOUR_SLIDES.map((_, i) => ( +
+
+ ) +} +>>>>>>> Stashed changes // ── Root modal ──────────────────────────────────────────────────────────────── @@ -39,6 +563,7 @@ export function OnboardingModal({ onComplete }: OnboardingModalProps) { const [step, setStep] = useState(0) const [tourSlide, setTourSlide] = useState(0) const [completing, setCompleting] = useState(false) + const [celebrating, setCelebrating] = useState(false) const setEnabledTools = useToolVisibilityStore((s) => s.setEnabledTools) @@ -96,10 +621,12 @@ export function OnboardingModal({ onComplete }: OnboardingModalProps) { ]) } catch { // non-blocking — user can still proceed - } finally { - setCompleting(false) - onComplete() } + // Payoff reveal, then drop them onto their personalized home. + setCompleting(false) + setCelebrating(true) + await new Promise((r) => setTimeout(r, 1600)) + onComplete() } const handleBack = () => { @@ -124,14 +651,32 @@ export function OnboardingModal({ onComplete }: OnboardingModalProps) { : "Continue" return ( -
+
+ {/* Deck backdrop */} +
+ +
+
+
+
+ + + + + + + + + +
+ {/* Header bar */}
@@ -212,7 +757,7 @@ export function OnboardingModal({ onComplete }: OnboardingModalProps) { size="sm" onClick={handleBack} disabled={step === 0 && tourSlide === 0} - className="text-muted-foreground" + className="text-foreground/70 hover:text-foreground hover:bg-white/5" > Back @@ -222,8 +767,9 @@ export function OnboardingModal({ onComplete }: OnboardingModalProps) { disabled={completing} size="sm" className={cn( - "gap-2 transition-all", - isLastAction && "bg-primary hover:bg-primary/90 shadow-md shadow-primary/25" + "gap-2 rounded-full px-5 font-semibold transition-all", + "bg-gradient-to-r from-[#5b63f0] via-[#9a5cf2] to-[#4fd0e6] text-[#0a0b12] hover:brightness-110", + isLastAction && "shadow-lg shadow-violet-500/30" )} > {nextLabel} @@ -231,6 +777,44 @@ export function OnboardingModal({ onComplete }: OnboardingModalProps) { {isLastAction && !completing && }
+ + {/* Payoff reveal */} + + {celebrating && ( + + + + + +

+ Your workspace is ready +

+

+ {selectedUrls.size} tools pinned. Press{" "} + + ⌘K + {" "} + anytime to launch. +

+
+
+ )} +
) diff --git a/apps/web/src/components/require-auth.tsx b/apps/web/src/components/require-auth.tsx index cc5f8c67..d28929be 100644 --- a/apps/web/src/components/require-auth.tsx +++ b/apps/web/src/components/require-auth.tsx @@ -4,7 +4,7 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import useAuth from "@/utils/useAuth"; import { EnsureBackendSession } from "@/components/ensure-backend-session"; -import { Loader2 } from "lucide-react"; +import { AppLoadingScreen } from "@/components/app-loading-screen"; export function RequireAuth({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(false); @@ -18,12 +18,7 @@ export function RequireAuth({ children }: { children: React.ReactNode }) { }, [loading, user, router]); if (loading || !user) { - return ( -
- -

Loading…

-
- ); + return ; } return ( diff --git a/apps/web/src/components/secret-api-key-generator/secret-api-key-generator-layout.tsx b/apps/web/src/components/secret-api-key-generator/secret-api-key-generator-layout.tsx index a9196051..92daed98 100644 --- a/apps/web/src/components/secret-api-key-generator/secret-api-key-generator-layout.tsx +++ b/apps/web/src/components/secret-api-key-generator/secret-api-key-generator-layout.tsx @@ -213,7 +213,7 @@ export function SecretApiKeyGeneratorLayout() {
- diff --git a/apps/web/src/components/sidebar/app-sidebar.tsx b/apps/web/src/components/sidebar/app-sidebar.tsx index 3088323c..3f4a56f4 100644 --- a/apps/web/src/components/sidebar/app-sidebar.tsx +++ b/apps/web/src/components/sidebar/app-sidebar.tsx @@ -1,6 +1,10 @@ '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' +import { cn } from '@/lib/utils' import { Sidebar, SidebarContent, @@ -55,6 +59,8 @@ export function AppSidebar({ ...props }: React.ComponentProps) { }) const router = useRouter(); + const pathname = usePathname(); + const isDashboardActive = pathname === '/dashboard'; // ... @@ -145,9 +151,28 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - router.push('/dashboard')} tooltip="Dashboard"> - - Dashboard + + + {isDashboardActive && ( + + + + )} + + Dashboard + diff --git a/apps/web/src/components/sidebar/nav-group.tsx b/apps/web/src/components/sidebar/nav-group.tsx index 548f1df8..33a34bfd 100644 --- a/apps/web/src/components/sidebar/nav-group.tsx +++ b/apps/web/src/components/sidebar/nav-group.tsx @@ -178,11 +178,11 @@ export function NavGroup({ title, titleKey, items, collapsible, icon: Icon, hidd return ( - + {Icon && ( - + {groupTitle} @@ -235,12 +235,23 @@ const SidebarMenuLink = ({ {isActive && ( + > + + + )} + {item.icon && ( + )} - {item.icon && } - {displayTitle} + + {displayTitle} + {item.badge && {item.badge}} diff --git a/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx b/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx index 1a97fe40..00aa87af 100644 --- a/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx +++ b/apps/web/src/components/sql-formatter/sql-formatter-layout.tsx @@ -138,7 +138,7 @@ export function SqlFormatterLayout() {
- diff --git a/apps/web/src/components/ssh-key-generator/ssh-key-generator-layout.tsx b/apps/web/src/components/ssh-key-generator/ssh-key-generator-layout.tsx index af328c50..e789951f 100644 --- a/apps/web/src/components/ssh-key-generator/ssh-key-generator-layout.tsx +++ b/apps/web/src/components/ssh-key-generator/ssh-key-generator-layout.tsx @@ -334,7 +334,7 @@ export function SshKeyGeneratorLayout() {
- diff --git a/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx b/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx index 1253413a..c4bc539e 100644 --- a/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx +++ b/apps/web/src/components/yaml-formatter/yaml-formatter-layout.tsx @@ -130,7 +130,7 @@ export function YamlFormatterLayout() {
- diff --git a/apps/web/src/hooks/use-count-up.ts b/apps/web/src/hooks/use-count-up.ts index 2f050c83..fbe195fa 100644 --- a/apps/web/src/hooks/use-count-up.ts +++ b/apps/web/src/hooks/use-count-up.ts @@ -1,5 +1,6 @@ 'use client' +<<<<<<< Updated upstream import { useEffect, useRef, useState } from 'react' export function useCountUp(target: number, duration = 550): number { @@ -21,4 +22,39 @@ export function useCountUp(target: number, duration = 550): number { return () => cancelAnimationFrame(raf) }, [target, duration]) return count +======= +import { useEffect, useState } from 'react' + +/** + * Animate a number from 0 → target with an ease-out curve on mount/target change. + * Under prefers-reduced-motion the duration collapses to 0 so it lands on the + * target on the first frame (no misleading lingering "0"). + */ +export function useCountUp(target: number, durationMs = 900): number { + const [value, setValue] = useState(0) + + useEffect(() => { + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches + const duration = reduce ? 0 : durationMs + let raf = 0 + let start: number | null = null + + const tick = (now: number) => { + if (start === null) start = now + const progress = duration <= 0 ? 1 : Math.min(1, (now - start) / duration) + const eased = 1 - Math.pow(1 - progress, 3) // easeOutCubic + setValue(Math.round(target * eased)) + if (progress < 1) raf = requestAnimationFrame(tick) + } + raf = requestAnimationFrame(tick) + // Safety net: if rAF is throttled (backgrounded/inactive tab), still land on target. + const fallback = setTimeout(() => setValue(target), duration + 120) + return () => { + cancelAnimationFrame(raf) + clearTimeout(fallback) + } + }, [target, durationMs]) + + return value +>>>>>>> Stashed changes } From 3c2f74300a37acf096868ae9e2b11e7f128a8b71 Mon Sep 17 00:00:00 2001 From: anuphilipp3 Date: Thu, 25 Jun 2026 16:30:14 +0530 Subject: [PATCH 2/2] fix: resolve committed merge conflict markers blocking the build - onboarding-modal: keep the immersive-deck redesign over the older modular sub-module version; restore dropped ScrollArea import - settings: merge upstream PasskeySection with the redesigned card styling - use-count-up: keep the reduced-motion + throttled-tab fallback variant Resolves the conflict markers committed in 46f02ba that failed the Vercel/turbopack build. --- apps/web/src/app/settings/page.tsx | 4 ---- apps/web/src/components/onboarding-modal.tsx | 11 +-------- apps/web/src/hooks/use-count-up.ts | 24 -------------------- 3 files changed, 1 insertion(+), 38 deletions(-) diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 75d93715..3a55f17c 100644 --- a/apps/web/src/app/settings/page.tsx +++ b/apps/web/src/app/settings/page.tsx @@ -64,13 +64,9 @@ export default function SettingsPage() {
-<<<<<<< Updated upstream - -======= ->>>>>>> Stashed changes diff --git a/apps/web/src/components/onboarding-modal.tsx b/apps/web/src/components/onboarding-modal.tsx index bdfd7b9b..284f24eb 100644 --- a/apps/web/src/components/onboarding-modal.tsx +++ b/apps/web/src/components/onboarding-modal.tsx @@ -8,12 +8,6 @@ import { Progress } from "@/components/ui/progress" import { completeOnboarding } from "@/lib/onboarding-api" import { useToolVisibilityStore } from "@/store/tool-visibility-store" import { patchUserPreferences } from "@/lib/user-preferences-api" -<<<<<<< Updated upstream -import { IconArrowRight, IconCheck, IconX } from "@tabler/icons-react" -import { WelcomeStep } from "./onboarding-modal/welcome-step" -import { TourStep, TOUR_SLIDES } from "./onboarding-modal/tour-step" -import { ToolSelectionStep, buildCategories } from "./onboarding-modal/tool-selection" -======= import { IconRocket, IconShield, @@ -35,6 +29,7 @@ import { import { sidebarData } from "@/components/sidebar/data/sidebar-data" import type { NavLink, NavCollapsible } from "@/components/sidebar/types" import MdtAurora from "@/components/mdt-aurora" +import { ScrollArea } from "@/components/ui/scroll-area" // ── Tool category icons by title ────────────────────────────────────────────── @@ -102,7 +97,6 @@ const TOUR_SLIDES = [ "Right-click any tool in the sidebar to pin it to the top. Pinned tools are synced across all your devices.", }, ] ->>>>>>> Stashed changes // ── Step indicators ─────────────────────────────────────────────────────────── @@ -117,8 +111,6 @@ function StepDot({ active, done }: { active: boolean; done: boolean }) { ) } -<<<<<<< Updated upstream -======= // ── Step 0: Welcome ─────────────────────────────────────────────────────────── const WELCOME_HIGHLIGHTS = [ @@ -549,7 +541,6 @@ function TourStep({ slideIndex, onSlide }: { slideIndex: number; onSlide: (i: nu
) } ->>>>>>> Stashed changes // ── Root modal ──────────────────────────────────────────────────────────────── diff --git a/apps/web/src/hooks/use-count-up.ts b/apps/web/src/hooks/use-count-up.ts index fbe195fa..b94d609d 100644 --- a/apps/web/src/hooks/use-count-up.ts +++ b/apps/web/src/hooks/use-count-up.ts @@ -1,28 +1,5 @@ 'use client' -<<<<<<< Updated upstream -import { useEffect, useRef, useState } from 'react' - -export function useCountUp(target: number, duration = 550): number { - const [count, setCount] = useState(0) - const prev = useRef(0) - useEffect(() => { - const from = prev.current - prev.current = target - if (target === from) return - const start = Date.now() - let raf: number - const tick = () => { - const t = Math.min((Date.now() - start) / duration, 1) - const eased = 1 - Math.pow(1 - t, 3) // ease-out-cubic - setCount(Math.round(from + (target - from) * eased)) - if (t < 1) raf = requestAnimationFrame(tick) - } - raf = requestAnimationFrame(tick) - return () => cancelAnimationFrame(raf) - }, [target, duration]) - return count -======= import { useEffect, useState } from 'react' /** @@ -56,5 +33,4 @@ export function useCountUp(target: number, durationMs = 900): number { }, [target, durationMs]) return value ->>>>>>> Stashed changes }