diff --git a/apps/storybook/stories/AppShell.stories.tsx b/apps/storybook/stories/AppShell.stories.tsx new file mode 100644 index 0000000000..e7273a7be2 --- /dev/null +++ b/apps/storybook/stories/AppShell.stories.tsx @@ -0,0 +1,99 @@ +import type { Meta, StoryObj } from '@storybook/nextjs'; +import { BreadcrumbItem, BreadcrumbPage } from '@/components/ui/breadcrumb'; +import { Button } from '@/components/ui/button'; +import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; +import { AppShellSkipLink } from '@/components/AppShellSkipLink'; +import AdminPage from '@/app/admin/components/AdminPage'; +import { AppSidebarView } from '@/app/admin/components/AppSidebar'; + +const meta: Meta = { + title: 'Components/Layout/App Shell', + parameters: { + layout: 'fullscreen', + nextjs: { + appDirectory: true, + navigation: { + pathname: '/admin/users', + query: {}, + }, + }, + }, +}; + +export default meta; +type Story = StoryObj; + +function AdminUsersPreview() { + return ( +
+
+

Users

+

+ Admin page content begins below the canonical 56px topbar. +

+
+
+ User + Status + Credits +
+ {[ + ['Jean du Plessis', 'Active', '$142.20'], + ['Avery Stone', 'Trial', '$18.00'], + ['Morgan Lee', 'Blocked', '$0.00'], + ].map(row => ( +
+ {row[0]} + {row[1]} + {row[2]} +
+ ))} +
+ ); +} + +function AdminShellPreview() { + return ( + + +
+ +
Build preview
+
+ +
+ + Users + + } + buttons={} + > + + +
+
+
+
+ ); +} + +export const AdminShell: Story = { + render: () => , +}; + +export const SkipLinkFocused: Story = { + render: () => , + play: async () => { + document.querySelector('a[href="#main-content"]')?.focus(); + }, +}; diff --git a/apps/storybook/stories/OrganizationSwitcher.stories.tsx b/apps/storybook/stories/OrganizationSwitcher.stories.tsx index b43fa485a4..f3d2b6f9b6 100644 --- a/apps/storybook/stories/OrganizationSwitcher.stories.tsx +++ b/apps/storybook/stories/OrganizationSwitcher.stories.tsx @@ -26,6 +26,11 @@ const organizations: OrganizationSwitcherOrganization[] = [ organizationName: 'Cloud Platform', role: 'member', }, + { + organizationId: 'org-long', + organizationName: '[seed:cost-insights] Northstar Labs', + role: 'owner', + }, ]; function OrganizationSwitcherStory({ @@ -71,6 +76,12 @@ export const OrganizationSelected: Story = { }, }; +export const LongOrganizationSelected: Story = { + args: { + organizationId: 'org-long', + }, +}; + export const Loading: Story = { args: { isPending: true, diff --git a/apps/storybook/stories/Sidebar.stories.tsx b/apps/storybook/stories/Sidebar.stories.tsx index cd8d2c5a91..f848978f1f 100644 --- a/apps/storybook/stories/Sidebar.stories.tsx +++ b/apps/storybook/stories/Sidebar.stories.tsx @@ -40,6 +40,7 @@ import { SidebarRail, SidebarTrigger, } from '@/components/ui/sidebar'; +import { AppShellSkipLink } from '@/components/AppShellSkipLink'; import { OrganizationSwitcherView } from '@/app/(app)/components/OrganizationSwitcher'; import SidebarMenuList from '@/app/(app)/components/SidebarMenuList'; import SidebarUserFooter from '@/app/(app)/components/SidebarUserFooter'; @@ -292,7 +293,7 @@ function StoryOrganizationSwitcher() { function StoryTopbar({ title }: { title: string }) { return ( -
+
@@ -307,7 +308,9 @@ function StoryInset({ title, children }: { title: string; children: ReactNode }) return ( -
{children}
+
+ {children} +
); } @@ -315,6 +318,7 @@ function StoryInset({ title, children }: { title: string; children: ReactNode }) function AppSidebarShell({ children }: { children: ReactNode }) { return ( +
{children} diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts index 9a928b15f2..34d9d6b8a5 100644 --- a/apps/web/playwright.config.ts +++ b/apps/web/playwright.config.ts @@ -1,4 +1,25 @@ import { defineConfig, devices } from '@playwright/test'; +import { config as loadEnv } from 'dotenv'; + +const e2eEnvKeys = new Set(); + +function loadE2eEnvFile(path: string, override = false) { + const result = loadEnv({ path, override }); + for (const key of Object.keys(result.parsed ?? {})) { + e2eEnvKeys.add(key); + } +} + +loadE2eEnvFile('.env'); +loadE2eEnvFile('.env.test', true); +loadE2eEnvFile('.env.test.local', true); + +const e2eEnv = Object.fromEntries( + [...e2eEnvKeys].flatMap(key => { + const value = process.env[key]; + return value === undefined ? [] : [[key, value]]; + }) +); const port = process.env.PORT ? Number(process.env.PORT) : 3000; // Use localhost instead of 127.0.0.1 to match cookie domain @@ -53,17 +74,22 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { // Always use dev mode for Playwright tests - never production - command: `dotenvx run --convention=nextjs -- pnpm next dev -p ${port}`, + command: `pnpm run copy:swagger-ui-assets && pnpm next dev -p ${port}`, url: baseURL, reuseExistingServer: !process.env.CI, timeout: 120_000, stdout: 'ignore', stderr: 'pipe', env: { + ...e2eEnv, // Always use development mode for Playwright tests NODE_ENV: 'development', DEBUG_SHOW_DEV_UI: 'true', // Enable fake login + APP_URL_OVERRIDE: baseURL, + NEXTAUTH_URL: baseURL, PORT: String(port), + VERCEL_ENV: '', + VERCEL_TARGET_ENV: '', }, }, }); diff --git a/apps/web/src/app/(app)/account-deleted/page.tsx b/apps/web/src/app/(app)/account-deleted/page.tsx index 6441db7072..17a4336642 100644 --- a/apps/web/src/app/(app)/account-deleted/page.tsx +++ b/apps/web/src/app/(app)/account-deleted/page.tsx @@ -4,7 +4,7 @@ import { LinkButton } from '@/components/Button'; export default function AccountDeletedPage() { return ( -
+
@@ -27,6 +27,6 @@ export default function AccountDeletedPage() {
-
+
); } diff --git a/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx b/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx index ae45076ae7..0c3ac15ac7 100644 --- a/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx +++ b/apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Sidebar, SidebarContent, SidebarHeader } from '@/components/ui/sidebar'; +import { Sidebar, SidebarContent, SidebarHeader, SidebarRail } from '@/components/ui/sidebar'; import { useUser } from '@/hooks/useUser'; import { Bot, @@ -419,6 +419,7 @@ export default function OrganizationAppSidebar({ + ); } diff --git a/apps/web/src/app/(app)/components/OrganizationSwitcher.tsx b/apps/web/src/app/(app)/components/OrganizationSwitcher.tsx index 0a4c6eeca4..ca3150b6b3 100644 --- a/apps/web/src/app/(app)/components/OrganizationSwitcher.tsx +++ b/apps/web/src/app/(app)/components/OrganizationSwitcher.tsx @@ -33,15 +33,20 @@ type OrganizationSwitcherViewProps = { }; const triggerClassName = - 'h-auto min-h-12 w-full justify-between rounded-lg border border-border bg-transparent px-3 py-1.5 text-left hover:border-border-strong hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'; + 'h-auto min-h-12 w-full justify-between gap-2 rounded-lg border border-border bg-transparent px-3 py-1.5 text-left hover:border-border-strong hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'; const menuItemClassName = 'flex min-h-12 cursor-pointer items-center rounded-md border border-transparent px-3 py-1.5 hover:border-border hover:bg-accent hover:text-accent-foreground'; const selectedMenuItemClassName = 'border-border bg-surface-selected text-foreground'; -const switcherTitleClassName = 'text-foreground text-sm leading-4 font-semibold'; -const switcherSubtitleClassName = 'text-muted-foreground text-xs leading-4'; +const switcherTextClassName = 'flex min-w-0 flex-1 flex-col items-start gap-0.5'; +const switcherRowClassName = 'flex w-full min-w-0 items-center justify-between gap-2'; +const switcherTitleClassName = + 'text-foreground max-w-full truncate text-sm leading-4 font-semibold'; +const switcherSubtitleClassName = 'text-muted-foreground max-w-full truncate text-xs leading-4'; +const switcherIconClassName = 'text-muted-foreground h-4 w-4 shrink-0'; +const selectedIconClassName = 'text-primary h-4 w-4 shrink-0'; export default function OrganizationSwitcher({ organizationId = null }: OrganizationSwitcherProps) { const trpc = useTRPC(); @@ -102,11 +107,11 @@ export function OrganizationSwitcherView({ return (
); @@ -122,7 +127,7 @@ export function OrganizationSwitcherView({ -
-
+
+
{org.organizationName}
{getRoleLabel(org.role)}
{organizationId === org.organizationId && ( - + )}
@@ -168,12 +173,12 @@ export function OrganizationSwitcherView({ onClick={() => onOrganizationSwitch(null)} className={cn(menuItemClassName, !organizationId && selectedMenuItemClassName)} > -
-
+
+
Personal
Personal Workspace
- {!organizationId && } + {!organizationId && }
diff --git a/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx b/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx index 7a57a915eb..b0dd9da90a 100644 --- a/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx +++ b/apps/web/src/app/(app)/components/PersonalAppSidebar.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Sidebar, SidebarContent, SidebarHeader } from '@/components/ui/sidebar'; +import { Sidebar, SidebarContent, SidebarHeader, SidebarRail } from '@/components/ui/sidebar'; import { useUser } from '@/hooks/useUser'; import { useKiloClawNavState } from '@/hooks/useKiloClaw'; import { useState } from 'react'; @@ -385,6 +385,7 @@ export default function PersonalAppSidebar(props: React.ComponentProps} + ); } diff --git a/apps/web/src/app/(app)/components/SidebarMenuList.tsx b/apps/web/src/app/(app)/components/SidebarMenuList.tsx index c8a06be4ef..a72a346891 100644 --- a/apps/web/src/app/(app)/components/SidebarMenuList.tsx +++ b/apps/web/src/app/(app)/components/SidebarMenuList.tsx @@ -92,7 +92,12 @@ export default function SidebarMenuList({ isActive={isActive} size={item.subtitle ? 'lg' : 'default'} > - + {content} @@ -103,6 +108,7 @@ export default function SidebarMenuList({ isActive={isActive} size={item.subtitle ? 'lg' : 'default'} className={cn('cursor-pointer', buttonClassName)} + aria-current={isActive ? 'page' : undefined} > {content} diff --git a/apps/web/src/app/(app)/layout.tsx b/apps/web/src/app/(app)/layout.tsx index 22b5385e66..059bed3257 100644 --- a/apps/web/src/app/(app)/layout.tsx +++ b/apps/web/src/app/(app)/layout.tsx @@ -5,6 +5,7 @@ import { RoleTestingProvider } from '@/contexts/RoleTestingContext'; import { PageTitleProvider } from '@/contexts/PageTitleContext'; import { EventServiceProvider } from '@/contexts/EventServiceContext'; import { AdminOmnibox } from '@/components/admin-omnibox'; +import { AppShellSkipLink } from '@/components/AppShellSkipLink'; import { PrefetchedOrganizations } from './components/PrefetchedOrganizations'; import { PlatformPresenceMount } from './components/PlatformPresenceMount'; export default function AppLayout({ children }: { children: React.ReactNode }) { @@ -15,11 +16,14 @@ export default function AppLayout({ children }: { children: React.ReactNode }) { +
-
{children}
+
+ {children} +
diff --git a/apps/web/src/app/admin/components/AdminPage.tsx b/apps/web/src/app/admin/components/AdminPage.tsx index 5dd0ec67bf..eff01548c1 100644 --- a/apps/web/src/app/admin/components/AdminPage.tsx +++ b/apps/web/src/app/admin/components/AdminPage.tsx @@ -19,7 +19,7 @@ export default function AdminPage({ }) { return ( <> -
+
diff --git a/apps/web/src/app/admin/components/AppSidebar.tsx b/apps/web/src/app/admin/components/AppSidebar.tsx index 0a014eb205..9b26b600cf 100644 --- a/apps/web/src/app/admin/components/AppSidebar.tsx +++ b/apps/web/src/app/admin/components/AppSidebar.tsx @@ -30,6 +30,7 @@ import { Route, } from 'lucide-react'; import { useSession } from 'next-auth/react'; +import { usePathname } from 'next/navigation'; import type { Session } from 'next-auth'; import KiloCrabIcon from '@/components/KiloCrabIcon'; @@ -49,6 +50,7 @@ import { } from '@/components/ui/sidebar'; import Link from 'next/link'; import { useTRPC } from '@/lib/trpc/utils'; +import { cn } from '@/lib/utils'; type MenuItem = { title: (session: Session | null) => string; @@ -269,25 +271,45 @@ const menuSections: MenuSection[] = [ }, ]; -export function AppSidebar({ +const adminMenuUrls = menuSections.flatMap(section => section.items.map(item => item.url)); + +function isAdminMenuItemActive(pathname: string, itemUrl: string) { + const matchesPrefix = pathname === itemUrl || pathname.startsWith(itemUrl + '/'); + if (!matchesPrefix) return false; + + return !adminMenuUrls.some( + url => + url !== itemUrl && + url.length > itemUrl.length && + (pathname === url || pathname.startsWith(url + '/')) + ); +} + +type AppSidebarViewProps = { + children: React.ReactNode; + pathname: string; + session: Session | null; + pendingDisputesCount?: number; +} & React.ComponentProps; + +export function AppSidebarView({ children, + pathname, + session, + pendingDisputesCount = 0, ...props -}: { children: React.ReactNode } & React.ComponentProps) { - const session = useSession(); - const trpc = useTRPC(); - const disputesSummaryQuery = useQuery({ - ...trpc.admin.disputes.summary.queryOptions(), - staleTime: DISPUTES_SUMMARY_STALE_TIME_MS, - }); - const pendingDisputesCount = disputesSummaryQuery.data?.pendingCount ?? 0; - +}: AppSidebarViewProps) { return ( - - + +
K
@@ -307,28 +329,35 @@ export function AppSidebar({ {section.label} - {section.items.map(item => ( - - 0 - ? 'pr-10' - : undefined - } - > - - {item.icon(session.data)} - {item.title(session.data)} - - - {item.url === '/admin/disputes' && pendingDisputesCount > 0 ? ( - - {pendingDisputesCount} - - ) : null} - - ))} + {section.items.map(item => { + const isActive = isAdminMenuItemActive(pathname, item.url); + + return ( + + 0 && 'pr-10' + )} + > + + {item.icon(session)} + {item.title(session)} + + + {item.url === '/admin/disputes' && pendingDisputesCount > 0 ? ( + + {pendingDisputesCount} + + ) : null} + + ); + })} @@ -341,3 +370,28 @@ export function AppSidebar({
); } + +export function AppSidebar({ + children, + ...props +}: { children: React.ReactNode } & React.ComponentProps) { + const session = useSession(); + const pathname = usePathname(); + const trpc = useTRPC(); + const disputesSummaryQuery = useQuery({ + ...trpc.admin.disputes.summary.queryOptions(), + staleTime: DISPUTES_SUMMARY_STALE_TIME_MS, + }); + const pendingDisputesCount = disputesSummaryQuery.data?.pendingCount ?? 0; + + return ( + + {children} + + ); +} diff --git a/apps/web/src/app/admin/layout.tsx b/apps/web/src/app/admin/layout.tsx index a915e1488b..04a0ad9332 100644 --- a/apps/web/src/app/admin/layout.tsx +++ b/apps/web/src/app/admin/layout.tsx @@ -4,6 +4,7 @@ import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; import { AppSidebar } from './components/AppSidebar'; import { Toaster } from '@/components/ui/sonner'; import { BuildInfo } from '@/app/admin/components/BuildInfo'; +import { AppShellSkipLink } from '@/components/AppShellSkipLink'; export default async function AdminLayout({ children }: { children: React.ReactNode }) { const { user: currentUser } = await getUserFromAuth({ adminOnly: true }); @@ -15,11 +16,16 @@ export default async function AdminLayout({ children }: { children: React.ReactN return (
+ {/* Need to pass BuildInfo as children from a server component to make it have access to the right variables */} - {children} + +
+ {children} +
+
diff --git a/apps/web/src/components/AppShellSkipLink.tsx b/apps/web/src/components/AppShellSkipLink.tsx new file mode 100644 index 0000000000..b6c0314797 --- /dev/null +++ b/apps/web/src/components/AppShellSkipLink.tsx @@ -0,0 +1,10 @@ +export function AppShellSkipLink() { + return ( + + Skip to main content + + ); +} diff --git a/apps/web/src/components/gastown/GastownTownSidebar.tsx b/apps/web/src/components/gastown/GastownTownSidebar.tsx index f2e986d6ab..a7ced1f997 100644 --- a/apps/web/src/components/gastown/GastownTownSidebar.tsx +++ b/apps/web/src/components/gastown/GastownTownSidebar.tsx @@ -15,6 +15,7 @@ import { SidebarMenuItem, SidebarMenuButton, SidebarFooter, + SidebarRail, } from '@/components/ui/sidebar'; import { ArrowLeft, @@ -136,7 +137,11 @@ export function GastownTownSidebar({ isActive={isActive(item.url)} data-onboarding-target={item.onboardingTarget} > - + {item.title} @@ -169,7 +174,11 @@ export function GastownTownSidebar({ > - +
{rig.name.charAt(0).toUpperCase()}
@@ -191,7 +200,11 @@ export function GastownTownSidebar({ - + Settings @@ -199,6 +212,7 @@ export function GastownTownSidebar({ + ); } diff --git a/apps/web/src/components/payment/CreditPurchaseOptions.tsx b/apps/web/src/components/payment/CreditPurchaseOptions.tsx index 0a5f354fea..f9ade73d1f 100644 --- a/apps/web/src/components/payment/CreditPurchaseOptions.tsx +++ b/apps/web/src/components/payment/CreditPurchaseOptions.tsx @@ -70,9 +70,6 @@ export default function CreditPurchaseOptions({ const [isDialogOpen, setIsDialogOpen] = useState(false); const [customAmountError, setCustomAmountError] = useState(''); const [showValidationError, setShowValidationError] = useState(false); - const [isHighlighted, setIsHighlighted] = useState(false); - const [animatingButton, setAnimatingButton] = useState(null); - const [rippleOrigin, setRippleOrigin] = useState({ x: 50, y: 50 }); const validateCustomAmount = (value: string) => { if (!value.trim()) { @@ -145,210 +142,136 @@ export default function CreditPurchaseOptions({ } }; - const handleButtonMouseEnter = (e: React.MouseEvent, buttonId: string) => { - if (!animatingButton && !submitting) { - const rect = e.currentTarget.getBoundingClientRect(); - const x = ((e.clientX - rect.left) / rect.width) * 100; - const y = ((e.clientY - rect.top) / rect.height) * 100; - - setRippleOrigin({ x, y }); - setAnimatingButton(buttonId); - setTimeout(() => setAnimatingButton(null), 600); - } - }; - const cardTitle = showOrganizationWarning ? 'Buy Personal Credits' : 'Buy Credits'; return ( - <> - - - - - {cardTitle} - - - - {showFirstPurchasePromo && ( -
setIsHighlighted(true)} - onMouseLeave={() => setIsHighlighted(false)} - className="mb-4 cursor-pointer" - > - -
- )} - - {showOrganizationWarning && ( -
- -
- )} + + + + + {cardTitle} + + + + {showFirstPurchasePromo && ( +
+ +
+ )} -
-
- {purchaseAmounts.map(amount => { - const totalAmount = showFirstPurchasePromo - ? amount + FIRST_TOPUP_BONUS_AMOUNT - : amount; + {showOrganizationWarning && ( +
+ +
+ )} - const buttonText = showFirstPurchasePromo - ? `Buy $${amount}, get $${totalAmount}` - : `$${amount}`; +
+
+ {purchaseAmounts.map(amount => { + const totalAmount = showFirstPurchasePromo + ? amount + FIRST_TOPUP_BONUS_AMOUNT + : amount; - return ( -
setSubmitting(true)} - > - -
- ); - })} + const buttonText = showFirstPurchasePromo + ? `Buy $${amount}, get $${totalAmount}` + : `$${amount}`; - - - - - - - Custom Credit Amount - - Enter the amount of credits you want to purchase. - {showFirstPurchasePromo && ( - <> - - 🎉 You'll receive an extra ${FIRST_TOPUP_BONUS_AMOUNT} on your first - purchase! - -

- Free promotional credits expire in{' '} - {Math.ceil(PROMO_CREDIT_EXPIRY_HRS / 24)} days. -

- - )} -
-
- -
-
-
- -
- handleCustomAmountChange(e.target.value)} - placeholder="Enter amount" - className="col-span-3" - /> - {showValidationError && customAmountError && ( -

{customAmountError}

- )} -
-
-

- {customAmount && - !showValidationError && - showFirstPurchasePromo && - !isNaN(parseFloat(customAmount)) && - parseFloat(customAmount) >= MINIMUM_TOP_UP_AMOUNT ? ( - - You'll receive $ - {Math.floor(parseFloat(customAmount)) + FIRST_TOPUP_BONUS_AMOUNT} in - total credits - - ) : ( - `Minimum amount is ${formatDollars(MINIMUM_TOP_UP_AMOUNT)}` - )} + + ); + })} + +

+ + + + + + Custom Credit Amount + + Enter the amount of credits you want to purchase. + {showFirstPurchasePromo && ( + <> + + 🎉 You'll receive an extra ${FIRST_TOPUP_BONUS_AMOUNT} on your first + purchase! + +

+ Free promotional credits expire in{' '} + {Math.ceil(PROMO_CREDIT_EXPIRY_HRS / 24)} days.

+ + )} +
+
+
+
+
+
+ +
+ handleCustomAmountChange(e.target.value)} + placeholder="Enter amount" + className="col-span-3" + /> + {showValidationError && customAmountError && ( +

{customAmountError}

+ )} +
+

+ {customAmount && + !showValidationError && + showFirstPurchasePromo && + !isNaN(parseFloat(customAmount)) && + parseFloat(customAmount) >= MINIMUM_TOP_UP_AMOUNT ? ( + + You'll receive $ + {Math.floor(parseFloat(customAmount)) + FIRST_TOPUP_BONUS_AMOUNT} in + total credits + + ) : ( + `Minimum amount is ${formatDollars(MINIMUM_TOP_UP_AMOUNT)}` + )} +

- - - - - -
-
+
+ + + + +
+
- - - - +
+ + ); } diff --git a/apps/web/src/components/ui/design-primitives.test.ts b/apps/web/src/components/ui/design-primitives.test.ts index 8045899215..48460393b8 100644 --- a/apps/web/src/components/ui/design-primitives.test.ts +++ b/apps/web/src/components/ui/design-primitives.test.ts @@ -152,25 +152,25 @@ describe('design primitive defaults', () => { expectClasses(tableCellClassName, ['px-3', 'py-3']); }); - it('uses primary-tinted active sidebar rows without yellow text', () => { + it('uses selected-surface active sidebar rows without yellow text', () => { const menuButtonClassName = sidebarMenuButtonVariants(); expectClasses(menuButtonClassName, [ 'hover:bg-sidebar-accent', - 'data-[active=true]:bg-primary/10', + 'data-[active=true]:bg-surface-selected', 'data-[active=true]:text-sidebar-accent-foreground', ]); - expect(menuButtonClassName).not.toContain('data-[active=true]:bg-surface-selected'); + expect(menuButtonClassName).not.toContain('data-[active=true]:bg-primary/10'); expect(menuButtonClassName).not.toContain('data-[active=true]:bg-sidebar-accent'); expect(menuButtonClassName).not.toContain('data-[active=true]:shadow'); expect(menuButtonClassName).not.toContain('data-[active=true]:text-primary'); expectClasses(sidebarMenuSubButtonClassName, [ 'hover:bg-sidebar-accent', - 'data-[active=true]:bg-primary/10', + 'data-[active=true]:bg-surface-selected', 'data-[active=true]:text-sidebar-accent-foreground', ]); - expect(sidebarMenuSubButtonClassName).not.toContain('data-[active=true]:bg-surface-selected'); + expect(sidebarMenuSubButtonClassName).not.toContain('data-[active=true]:bg-primary/10'); expect(sidebarMenuSubButtonClassName).not.toContain('data-[active=true]:bg-sidebar-accent'); expect(sidebarMenuSubButtonClassName).not.toContain('data-[active=true]:shadow'); expect(sidebarMenuSubButtonClassName).not.toContain('data-[active=true]:text-primary'); diff --git a/apps/web/src/components/ui/primitive-classnames.ts b/apps/web/src/components/ui/primitive-classnames.ts index 8ca51dc8aa..6bb76415d6 100644 --- a/apps/web/src/components/ui/primitive-classnames.ts +++ b/apps/web/src/components/ui/primitive-classnames.ts @@ -53,10 +53,10 @@ export const tableCellClassName = 'px-3 py-3 align-middle'; export const tableCaptionClassName = 'type-body text-muted-foreground mt-4'; export const sidebarMenuSubButtonClassName = - 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-surface-selected active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 data-[active=true]:bg-primary/10 data-[active=true]:text-sidebar-accent-foreground'; + 'text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-surface-selected active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 data-[active=true]:bg-surface-selected data-[active=true]:text-sidebar-accent-foreground'; export const sidebarMenuButtonVariants = cva( - 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-surface-selected active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-primary/10 data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', + 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-surface-selected active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-surface-selected data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', { variants: { variant: { diff --git a/apps/web/src/components/ui/sidebar.tsx b/apps/web/src/components/ui/sidebar.tsx index 84ed978df0..6381ff4902 100644 --- a/apps/web/src/components/ui/sidebar.tsx +++ b/apps/web/src/components/ui/sidebar.tsx @@ -267,11 +267,12 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps { onClick?.(event); toggleSidebar(); @@ -289,13 +290,15 @@ function SidebarRail({ className, ...props }: React.ComponentProps<'button'>) { return (