Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ const DashboardPage: React.FC = () => {
mobileOnly
/>

<div className="px-3 md:px-8 pb-24 md:pb-12">
<div className="px-3 pt-4 md:px-8 md:pt-8 pb-24 md:pb-12">
<div className="max-w-7xl mx-auto space-y-5 md:space-y-8">
{/* ── Desktop Hero (inside padded container for alignment) ── */}
<RevealItem index={0}>
Expand Down
79 changes: 46 additions & 33 deletions apps/web/src/components/global-command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,31 @@ export function GlobalCommandPalette() {
)

const commandSurfaceClass =
'[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'
'[&_[cmdk-group-heading]]:px-3 [&_[cmdk-group-heading]]:pt-2.5 [&_[cmdk-group-heading]]:pb-1 [&_[cmdk-group-heading]]:text-[10px] [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:uppercase [&_[cmdk-group-heading]]:tracking-[0.14em] [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-14 [&_[cmdk-input]]:text-base'

// Shared row treatment — gradient selection sweep + icon chip (Raycast-style).
const itemClass =
'group mx-1 flex items-center gap-3 rounded-xl px-2 py-2 transition-colors aria-selected:bg-gradient-to-r aria-selected:from-primary/[0.14] aria-selected:via-primary/[0.06] aria-selected:to-transparent aria-selected:ring-1 aria-selected:ring-inset aria-selected:ring-primary/20'
const chipClass =
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground ring-1 ring-inset ring-border/50 transition-colors group-aria-selected:bg-gradient-to-br group-aria-selected:from-primary/20 group-aria-selected:to-violet-500/10 group-aria-selected:text-primary group-aria-selected:ring-primary/20'

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
showCloseButton={false}
className={cn(
'overflow-hidden p-0 gap-0 max-w-2xl',
'max-h-[min(85vh,640px)] flex flex-col'
'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]'
)}
>
<DialogTitle className="sr-only">Search tools and pages</DialogTitle>
{/* Gradient hairline — the palette's signature accent */}
<div
aria-hidden
className="h-px w-full shrink-0 bg-gradient-to-r from-transparent via-primary/50 to-transparent"
/>
<Command
className={cn(
'rounded-none border-none bg-transparent shadow-none',
Expand All @@ -177,7 +190,7 @@ export function GlobalCommandPalette() {
<CommandInput
placeholder="Search tools and pages…"
aria-label="Search tools and pages"
className="focus-visible:outline-none"
className="focus-visible:outline-none placeholder:text-muted-foreground/60"
/>
<CommandList className="max-h-[min(60vh,480px)] overflow-y-auto">
<CommandEmpty>No results found.</CommandEmpty>
Expand All @@ -191,15 +204,17 @@ export function GlobalCommandPalette() {
key={`recent-${entry.url}`}
value={`recent ${entry.searchValue} ${entry.url}`.toLowerCase()}
onSelect={() => run(entry)}
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"
className={itemClass}
>
<ItemIcon className="mt-0.5 h-4 w-4 shrink-0 opacity-70" aria-hidden />
<span className={chipClass}>
<ItemIcon className="h-4 w-4" aria-hidden />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-medium leading-none">
{entry.title}
</span>
{entry.description ? (
<span className="line-clamp-2 text-xs text-muted-foreground">
<span className="line-clamp-1 text-xs text-muted-foreground">
{entry.description}
</span>
) : null}
Expand All @@ -221,24 +236,23 @@ export function GlobalCommandPalette() {
key={`pinned-${entry.url}`}
value={`pinned ${entry.searchValue} ${entry.url}`.toLowerCase()}
onSelect={() => run(entry)}
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"
className={itemClass}
>
<Star
className="mt-0.5 h-4 w-4 shrink-0 fill-primary text-primary"
aria-hidden
/>
<span className={chipClass}>
<ItemIcon className="h-4 w-4" aria-hidden />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-medium leading-none">
{entry.title}
</span>
{entry.description ? (
<span className="line-clamp-2 text-xs text-muted-foreground">
<span className="line-clamp-1 text-xs text-muted-foreground">
{entry.description}
</span>
) : null}
</span>
<ItemIcon
className="mt-0.5 h-3.5 w-3.5 shrink-0 opacity-40"
<Star
className="h-3.5 w-3.5 shrink-0 fill-primary text-primary"
aria-hidden
/>
</CommandItem>
Expand All @@ -259,18 +273,17 @@ export function GlobalCommandPalette() {
key={entry.url}
value={`${entry.searchValue} ${entry.url}`.toLowerCase()}
onSelect={() => run(entry)}
className="flex items-start gap-3 py-2.5 aria-selected:bg-accent"
className={itemClass}
>
<ItemIcon
className="mt-0.5 h-4 w-4 shrink-0 opacity-70"
aria-hidden
/>
<span className={chipClass}>
<ItemIcon className="h-4 w-4" aria-hidden />
</span>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="truncate font-medium leading-none">
{entry.title}
</span>
{entry.description ? (
<span className="line-clamp-2 text-xs text-muted-foreground">
<span className="line-clamp-1 text-xs text-muted-foreground">
{entry.description}
</span>
) : null}
Expand All @@ -282,29 +295,29 @@ export function GlobalCommandPalette() {
</React.Fragment>
))}
</CommandList>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 border-t border-border/60 px-4 py-3 text-[11px] text-muted-foreground">
<span>
<kbd className="pointer-events-none rounded border border-border bg-muted/80 px-1.5 py-0.5 font-mono">
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 border-t border-border/50 bg-muted/20 px-4 py-2.5 text-[11px] text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<kbd className="pointer-events-none rounded-md border border-border/60 bg-background/80 px-1.5 py-0.5 font-mono text-[10px] font-medium text-foreground/70 shadow-sm">
↑↓
</kbd>{' '}
</kbd>
navigate
</span>
<span>
<kbd className="pointer-events-none rounded border border-border bg-muted/80 px-1.5 py-0.5 font-mono">
<span className="inline-flex items-center gap-1.5">
<kbd className="pointer-events-none rounded-md border border-border/60 bg-background/80 px-1.5 py-0.5 font-mono text-[10px] font-medium text-foreground/70 shadow-sm">
</kbd>{' '}
</kbd>
open
</span>
<span>
<kbd className="pointer-events-none rounded border border-border bg-muted/80 px-1.5 py-0.5 font-mono">
<span className="inline-flex items-center gap-1.5">
<kbd className="pointer-events-none rounded-md border border-border/60 bg-background/80 px-1.5 py-0.5 font-mono text-[10px] font-medium text-foreground/70 shadow-sm">
esc
</kbd>{' '}
</kbd>
close
</span>
<span className="ml-auto">
<kbd className="pointer-events-none rounded border border-border bg-muted/80 px-1.5 py-0.5 font-mono">
<span className="ml-auto inline-flex items-center gap-1.5">
<kbd className="pointer-events-none rounded-md border border-border/60 bg-background/80 px-1.5 py-0.5 font-mono text-[10px] font-medium text-foreground/70 shadow-sm">
{modLabel}K
</kbd>{' '}
</kbd>
toggle
</span>
</div>
Expand Down
17 changes: 11 additions & 6 deletions apps/web/src/components/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { ModeToggle } from "@/components/modeToggle";
import { getToolMessageKey } from "@/lib/tool-i18n";
import { routeConfig } from "@/lib/route-config";
import { WorkspaceSwitcherDropdown } from "@/components/workspace-switcher-dropdown";
import { OrgSwitcherDropdown } from "@/components/org-switcher-dropdown";
import { WorkspaceQuickActions } from "@/components/workspace-quick-actions";
import { WorkspaceBreadcrumb } from "@/components/workspace-breadcrumb";
import { NotificationsBell } from "@/components/notifications-bell";

function openCommandPalette() {
Expand All @@ -27,7 +27,7 @@ function CommandTrigger() {
>
<Search className="h-4 w-4 shrink-0" />
<span className="flex-1 truncate text-left">Search or jump to…</span>
<kbd className="inline-flex items-center gap-0.5 rounded border border-border/60 bg-background/70 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
<kbd className="inline-flex items-center gap-0.5 rounded-md border border-border bg-background px-1.5 py-0.5 font-mono text-[11px] font-semibold text-foreground/80 shadow-sm">
⌘K
</kbd>
</button>
Expand Down Expand Up @@ -63,19 +63,23 @@ export function NavBar() {
const config = match?.[1];
const matchedRoute = match?.[0];

if (!config) {
// The dashboard needs no page title — the sidebar highlights it and the page
// greets the user. A "Dashboard" label beside the org switcher is noise.
if (!config || matchedRoute === '/dashboard') {
// Minimal bar everywhere with a sidebar so chrome (workspace, bell, theme)
// is reachable on /dashboard, /settings, etc. — not just /app routes.
return (
<header className="sticky top-0 z-20 hidden w-full border-b border-border/60 bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:block">
<div className="flex h-14 items-center justify-end gap-2 px-4">
<div className="flex h-14 items-center gap-2 px-4">
<div className="flex min-w-0 flex-1 items-center">
<OrgSwitcherDropdown />
</div>
<CommandTrigger />
<WorkspaceSwitcherDropdown />
<WorkspaceQuickActions />
<NotificationsBell />
<ModeToggle />
</div>
<WorkspaceBreadcrumb />
</header>
);
}
Expand All @@ -94,6 +98,8 @@ export function NavBar() {
<header className="sticky top-0 z-20 hidden w-full border-b border-border/60 bg-background/80 backdrop-blur supports-[backdrop-filter]:bg-background/60 md:block">
<div className="flex h-14 w-full items-center gap-4 px-4">
<div className="flex min-w-0 flex-1 items-center gap-2.5">
<OrgSwitcherDropdown />
<div className="h-5 w-px shrink-0 bg-border/60" aria-hidden />
<div className="shrink-0 rounded-lg bg-gradient-to-br from-primary/15 to-violet-500/10 p-1.5 ring-1 ring-inset ring-border/50">
<Icon className="h-4 w-4 text-primary" strokeWidth={2} />
</div>
Expand All @@ -116,7 +122,6 @@ export function NavBar() {
<ModeToggle />
</div>
</div>
<WorkspaceBreadcrumb />
</header>
);
}
4 changes: 2 additions & 2 deletions apps/web/src/components/org-switcher-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useActiveOrg, useWorkspaceStore } from "@/store/workspace-store"
import { CreateOrgDialog } from "@/components/create-org-dialog"

/**
* Organization switcher — top-left of the sidebar header, beside the brand.
* Organization switcher — lives in the top nav, beside the page title.
* Switching org cascades to its default workspace via `setActiveOrg`.
*/
export function OrgSwitcherDropdown() {
Expand All @@ -28,7 +28,7 @@ export function OrgSwitcherDropdown() {
<DropdownMenuTrigger asChild>
<button
type="button"
className="flex w-full items-center gap-2 rounded-lg border border-border/60 bg-muted/40 px-2.5 py-1.5 text-left text-sm font-medium text-foreground/90 transition-colors hover:border-primary/40 hover:bg-muted/60"
className="flex h-9 max-w-[13rem] items-center gap-2 rounded-lg border border-border/60 bg-muted/40 px-2.5 text-left text-sm font-medium text-foreground/90 transition-colors hover:border-primary/40 hover:bg-muted/60"
>
<Building2 className="h-4 w-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 truncate">{activeOrg.name}</span>
Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/components/sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import { NavGroup } from './nav-group'
import { NavUser } from './nav-user'
import { FeedbackDialog } from '@/components/feedback-dialog'
import { OrgSwitcherDropdown } from '@/components/org-switcher-dropdown'
import { Logo } from '../logo'
import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'
import { LayoutDashboard } from 'lucide-react'
Expand Down Expand Up @@ -144,9 +143,6 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<p className="text-[10px] text-muted-foreground/80 font-medium tracking-wider uppercase pl-0.5 whitespace-nowrap">Developer&apos;s Toolkit</p>
</div>
</div>
<div className="px-2 pt-1 group-data-[state=collapsed]:hidden">
<OrgSwitcherDropdown />
</div>
</SidebarHeader>
<SidebarContent className="mt-2 md:mt-0">
<SidebarMenu className="px-2 mb-1">
Expand Down
21 changes: 17 additions & 4 deletions apps/web/src/components/sidebar/client-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { SidebarProvider, useSidebar } from "@/components/ui/sidebar";
import { AppSidebar } from "./app-sidebar";
import { NavBar } from '@/components/nav-bar';
import { MobileNav } from '@/components/mobile-nav';
import { GlobalCommandPalette } from '@/components/global-command-palette';
import { TabBar } from '@/components/tab-bar/tab-bar';
import { useTabStore } from '@/store/tab-store';
import { isTabRoute } from '@/lib/route-config';
Expand All @@ -14,6 +13,12 @@ import { MigrationBanner } from '@/components/migration-banner';
import { MobileDesktopHint } from '@/components/mobile-desktop-hint';
import { WorkspaceSwitcherDropdown } from '@/components/workspace-switcher-dropdown';
import { NotificationsBell } from '@/components/notifications-bell';
import { useWorkspaceStore } from '@/store/workspace-store';
import { initWorkspaceScopeReset } from '@/lib/workspace-scope-reset';

// Reset workspace-scoped stores whenever the active workspace changes
// (module-level, mirrors workspace-store's own subscribeOnce pattern).
initWorkspaceScopeReset();

// Renders all open tool tabs simultaneously. The active tab is visible;
// inactive tabs use display:none to stay mounted (preserving their state).
Expand Down Expand Up @@ -59,6 +64,10 @@ function Layout({ children }: { children: React.ReactNode }) {
const { state } = useSidebar();
const pathname = usePathname();
const { tabs } = useTabStore();
// Keying tool content by workspace remounts every mounted tab (and page) on
// switch, so each tool refetches under the new scope instead of showing the
// previous workspace's data.
const activeWorkspaceId = useWorkspaceStore((s) => s.activeWorkspaceId);

// Open command palette for new tab via "+" button
const openCommandPalette = useCallback(() => {
Expand Down Expand Up @@ -104,9 +113,12 @@ function Layout({ children }: { children: React.ReactNode }) {
</div>

{inTabMode ? (
<TabContent />
<TabContent key={activeWorkspaceId ?? 'none'} />
) : (
<div className="z-10 flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div
key={activeWorkspaceId ?? 'none'}
className="z-10 flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden"
>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto">
{children}
</div>
Expand All @@ -116,7 +128,8 @@ function Layout({ children }: { children: React.ReactNode }) {
<MobileNav />
</main>
</div>
<GlobalCommandPalette />
{/* Command palette is mounted once globally in ClientShell (root layout) —
a second mount here caused stacked overlays needing two clicks to dismiss. */}
</div>
);
}
Expand Down
Loading