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
7 changes: 6 additions & 1 deletion apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { TrieRouter } from 'hono/router/trie-router';

import { Layout } from './components/layout/Layout';
import { RouterProvider, RouteView, useLocation } from './components/router/HonoRouter';
import { DirectionProvider } from './components/ui/direction';
import { TooltipProvider } from './components/ui/tooltip';
import { LandingPage } from './pages/LandingPage';
import { ContactPage } from './pages/ContactPage';
Expand Down Expand Up @@ -41,7 +42,11 @@ function AppShell() {
const route = <RouteView router={router} />;

if (path === '/dashboard' || path.startsWith('/dashboard/')) {
return route;
return (
<DirectionProvider dir="rtl" direction="rtl">
{route}
</DirectionProvider>
);
}

return <Layout>{route}</Layout>;
Expand Down
34 changes: 34 additions & 0 deletions apps/web/src/App.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ vi.mock('./components/ui/tooltip', () => ({
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

vi.mock('./components/ui/direction', () => ({
DirectionProvider: ({
children,
dir,
direction,
}: {
children: React.ReactNode;
dir?: string;
direction?: string;
}) => (
<div data-dir={dir} data-direction={direction} data-testid="dashboard-direction-provider">
{children}
</div>
),
}));
Comment thread
HusseinBaraja marked this conversation as resolved.

vi.mock('./pages/LandingPage', () => ({
LandingPage: () => <div>landing-page</div>,
}));
Expand Down Expand Up @@ -46,6 +62,7 @@ describe('App shell layout behavior', () => {
render(<App />);

screen.getByTestId('marketing-layout');
expect(screen.queryByTestId('dashboard-direction-provider')).toBeNull();
screen.getByText('contact-page');
});

Expand All @@ -57,6 +74,23 @@ describe('App shell layout behavior', () => {
render(<App />);

expect(screen.queryByTestId('marketing-layout')).toBeNull();
const directionProvider = screen.getByTestId('dashboard-direction-provider');
expect(directionProvider.getAttribute('data-dir')).toBe('rtl');
expect(directionProvider.getAttribute('data-direction')).toBe('rtl');
screen.getByText('upload-data-page');
});

it('does not wrap the dashboard root in marketing layout', async () => {
setupGsapMocks();
window.history.replaceState({}, '', '/dashboard');
const { default: App } = await import('./App');

render(<App />);

expect(screen.queryByTestId('marketing-layout')).toBeNull();
const directionProvider = screen.getByTestId('dashboard-direction-provider');
expect(directionProvider.getAttribute('data-dir')).toBe('rtl');
expect(directionProvider.getAttribute('data-direction')).toBe('rtl');
screen.getByText('dashboard-page');
});
});
12 changes: 6 additions & 6 deletions apps/web/src/components/dashboard/DashboardShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ function DashboardSidebar({ activePath }: { activePath: string }) {
};

return (
<Sidebar side="right" collapsible="icon" className="top-[var(--header-height)] h-[calc(100svh-var(--header-height))] border-l border-[#dde4e0] shadow-[-2px_0_10px_rgba(22,35,29,0.05)]">
<Sidebar dir="rtl" side="right" collapsible="icon" className="top-[var(--header-height)] h-[calc(100svh-var(--header-height))] border-s border-[#dde4e0] shadow-[-2px_0_10px_rgba(22,35,29,0.05)]">
<SidebarContent className="relative bg-white px-3 py-6 group-data-[icon-layout=collapsed]:px-0">
<ScrollArea
onScrollCapture={handleNavigationScroll}
className="min-h-0 flex-1 **:data-[slot=scroll-area-scrollbar]:left-0 **:data-[slot=scroll-area-scrollbar]:right-auto **:data-[slot=scroll-area-viewport]:pl-3 group-data-[icon-layout=collapsed]:**:data-[slot=scroll-area-viewport]:pl-0"
className="min-h-0 flex-1 **:data-[slot=scroll-area-scrollbar]:start-0 **:data-[slot=scroll-area-scrollbar]:end-auto **:data-[slot=scroll-area-viewport]:ps-3 group-data-[icon-layout=collapsed]:**:data-[slot=scroll-area-viewport]:ps-0"
>
<SidebarGroup className="group-data-[icon-layout=collapsed]:p-0">
<SidebarGroupContent>
Expand All @@ -83,10 +83,10 @@ function DashboardSidebar({ activePath }: { activePath: string }) {
href={href}
data-placeholder={isPlaceholder ? 'true' : undefined}
onClick={isPlaceholder ? (event) => event.preventDefault() : undefined}
className="grid grid-cols-[minmax(0,1fr)_1.25rem] items-center gap-3 group-data-[icon-layout=collapsed]:grid-cols-1 group-data-[icon-layout=collapsed]:justify-items-center group-data-[icon-layout=collapsed]:gap-0"
className="grid grid-cols-[1.25rem_minmax(0,1fr)] items-center gap-3 [&>span:last-child]:overflow-visible [&>span:last-child]:text-wrap [&>span:last-child]:whitespace-normal group-data-[icon-layout=collapsed]:grid-cols-1 group-data-[icon-layout=collapsed]:justify-items-center group-data-[icon-layout=collapsed]:gap-0"
>
<span className="min-w-0 overflow-hidden text-right leading-6 wrap-break-word hyphens-auto group-data-[icon-layout=collapsed]:hidden">{label}</span>
<Icon className="justify-self-end stroke-[1.9] group-data-[icon-layout=collapsed]:justify-self-center" />
<Icon className="justify-self-start stroke-[1.9] group-data-[icon-layout=collapsed]:justify-self-center" />
<span className="min-w-0 overflow-visible text-start leading-6 text-wrap wrap-break-word hyphens-auto group-data-[icon-layout=collapsed]:hidden">{label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
Expand Down Expand Up @@ -127,7 +127,7 @@ export function DashboardShell({ activePath, children }: { activePath: string; c
</Avatar>
<Button variant="ghost" size="icon-lg" className="relative text-[#202825] hover:bg-[#ecf5ef]" aria-label="الإشعارات">
<Bell />
<span className="absolute right-2 top-2 size-2.5 rounded-full bg-[#09844a]" />
<span className="absolute end-2 top-2 size-2.5 rounded-full bg-[#09844a]" />
</Button>
<div className="hidden h-8 w-px bg-[#d8dddc] sm:block" />
<nav className="hidden items-center gap-3 text-sm font-semibold sm:flex">
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/components/ui/direction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from "react"
import { Direction } from "radix-ui"

function DirectionProvider({
dir,
direction,
children,
}: React.ComponentProps<typeof Direction.DirectionProvider> & {
direction?: React.ComponentProps<typeof Direction.DirectionProvider>["dir"]
}) {
return (
<Direction.DirectionProvider dir={direction ?? dir}>
{children}
</Direction.DirectionProvider>
)
}

const useDirection = Direction.useDirection

export { DirectionProvider, useDirection }
7 changes: 5 additions & 2 deletions apps/web/src/components/ui/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ function Sidebar({
return (
<div
data-slot="sidebar"
dir={dir}
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
className
Expand Down Expand Up @@ -245,6 +246,7 @@ function Sidebar({
data-variant={variant}
data-side={side}
data-slot="sidebar"
dir={dir}
>
{/* This is what handles the sidebar gap on desktop */}
<div
Expand All @@ -262,6 +264,7 @@ function Sidebar({
<div
data-slot="sidebar-container"
data-side={side}
dir={dir}
className={cn(
// duration-200 must stay in sync with SIDEBAR_COLLAPSE_DURATION_MS.
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
Expand Down Expand Up @@ -323,9 +326,9 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
onClick={toggleSidebar}
title="Toggle Sidebar"
className={cn(
"absolute inset-y-0 z-20 hidden w-7 items-center justify-center overflow-hidden border-sidebar-rail-border bg-sidebar-rail/55 text-sidebar-rail-foreground outline-none backdrop-blur-[1px] transition-[background-color,border-color,box-shadow] duration-200 ease-out hover:bg-sidebar-rail-hover hover:shadow-[var(--sidebar-rail-shadow)] focus-visible:ring-2 focus-visible:ring-sidebar-rail-ring/35 sm:flex group-data-[side=left]:-right-7 group-data-[side=left]:border-r group-data-[side=right]:left-0 group-data-[side=right]:-translate-x-full group-data-[side=right]:border-l",
"absolute inset-y-0 z-20 hidden w-7 items-center justify-center overflow-hidden border-sidebar-rail-border bg-sidebar-rail/55 text-sidebar-rail-foreground outline-none backdrop-blur-[1px] transition-[background-color,border-color,box-shadow] duration-200 ease-out hover:bg-sidebar-rail-hover hover:shadow-[var(--sidebar-rail-shadow)] focus-visible:ring-2 focus-visible:ring-sidebar-rail-ring/35 sm:flex group-data-[side=left]:-right-7 group-data-[side=left]:border-e group-data-[side=right]:left-0 group-data-[side=right]:-translate-x-full group-data-[side=right]:border-s group-data-[side=right]:border-l",
"cursor-pointer",
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:bg-sidebar/70",
"group-data-[collapsible=offcanvas]:translate-x-0 rtl:group-data-[collapsible=offcanvas]:-translate-x-0 group-data-[collapsible=offcanvas]:bg-sidebar/70",
"[[data-side=left][data-collapsible=offcanvas]_&]:-inset-e-7",
"[[data-side=right][data-collapsible=offcanvas]_&]:-inset-s-7",
Comment thread
HusseinBaraja marked this conversation as resolved.
className
Expand Down
22 changes: 16 additions & 6 deletions apps/web/src/pages/DashboardPage.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,20 @@ describe('DashboardPage', () => {
const scrollArea = container.querySelector('[data-slot="scroll-area"]');
const overflowShadow = container.querySelector('[data-testid="sidebar-bottom-overflow-shadow"]');

expect(scrollArea?.getAttribute('class')).toContain('**:data-[slot=scroll-area-scrollbar]:left-0');
expect(scrollArea?.getAttribute('class')).toContain('**:data-[slot=scroll-area-scrollbar]:right-auto');
expect(scrollArea?.getAttribute('class')).toContain('**:data-[slot=scroll-area-viewport]:pl-3');
expect(scrollArea?.getAttribute('class')).toContain('**:data-[slot=scroll-area-scrollbar]:start-0');
expect(scrollArea?.getAttribute('class')).toContain('**:data-[slot=scroll-area-scrollbar]:end-auto');
expect(scrollArea?.getAttribute('class')).toContain('**:data-[slot=scroll-area-viewport]:ps-3');
expect(overflowShadow?.getAttribute('class')).toContain('pointer-events-none');
expect(overflowShadow?.getAttribute('class')).toContain('bottom-0');
expect(overflowShadow?.getAttribute('class')).toContain('transition-opacity');
expect(navLink?.getAttribute('class')).toContain('grid-cols-[minmax(0,1fr)_1.25rem]');
expect(aiLabel.getAttribute('class')).toContain('overflow-hidden');
expect(navLink?.getAttribute('class')).toContain('grid-cols-[1.25rem_minmax(0,1fr)]');
expect(navLink?.getAttribute('class')).toContain('[&>span:last-child]:whitespace-normal');
expect(navLink?.getAttribute('class')).toContain('[&>span:last-child]:text-wrap');
expect(navLink?.getAttribute('class')).toContain('[&>span:last-child]:overflow-visible');
expect(aiLabel.getAttribute('class')).toContain('overflow-visible');
expect(aiLabel.getAttribute('class')).toContain('text-wrap');
expect(aiLabel.getAttribute('class')).toContain('wrap-break-word');
expect(aiLabel.getAttribute('class')).toContain('text-start');
expect(aiLabel.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:hidden');
});

Expand All @@ -92,7 +97,7 @@ describe('DashboardPage', () => {
const navIcon = navLink?.querySelector('svg');

expect(sidebarContent?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:px-0');
expect(scrollArea?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:**:data-[slot=scroll-area-viewport]:pl-0');
expect(scrollArea?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:**:data-[slot=scroll-area-viewport]:ps-0');
expect(sidebarGroup?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:p-0');
expect(navButton?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:size-12');
expect(navButton?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:p-3');
Expand All @@ -106,6 +111,8 @@ describe('DashboardPage', () => {
expect(navItem?.getAttribute('class')).not.toContain('group-data-[collapsible=icon]:justify-end');
expect(navLink?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:grid-cols-1');
expect(navLink?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:justify-items-center');
expect(navIcon?.nextElementSibling?.textContent).toBe('لوحة التحكم');
expect(navIcon?.getAttribute('class')).toContain('justify-self-start');
expect(navIcon?.getAttribute('class')).toContain('group-data-[icon-layout=collapsed]:justify-self-center');
expect(navIcon?.getAttribute('class')).toContain('stroke-[1.9]');
});
Expand Down Expand Up @@ -189,6 +196,7 @@ describe('DashboardPage', () => {
expect(header?.getAttribute('class')).toContain('h-[var(--header-height)]');
expect(sidebar?.getAttribute('class')).toContain('top-[var(--header-height)]');
expect(sidebar?.getAttribute('class')).toContain('h-[calc(100svh-var(--header-height))]');
expect(sidebar?.getAttribute('dir')).toBe('rtl');
expect(inset?.getAttribute('class')).toContain('pt-[var(--header-height)]');
});

Expand All @@ -211,9 +219,11 @@ describe('DashboardPage', () => {
expect(rail.getAttribute('class')).toContain('cursor-pointer');
expect(rail.getAttribute('class')).not.toContain('cursor-e-resize');
expect(rail.getAttribute('class')).not.toContain('cursor-w-resize');
expect(rail.getAttribute('class')).toContain('group-data-[side=right]:border-s');
expect(rail.getAttribute('class')).toContain('group-data-[side=right]:border-l');
expect(rail.getAttribute('class')).toContain('group-data-[side=right]:left-0');
expect(rail.getAttribute('class')).toContain('group-data-[side=right]:-translate-x-full');
expect(rail.getAttribute('class')).not.toContain('rtl:group-data-[side=right]:translate-x-full');
expect(rail.getAttribute('class')).toContain('items-center');
expect(rail.getAttribute('class')).toContain('justify-center');
expect(railIconShell?.getAttribute('class')).toContain('size-7');
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pages/UploadDataPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function UploadDataPage() {
<div className="flex size-10 items-center justify-center rounded-lg bg-[#edf7f1] text-[#0d7c47]">
<FileSpreadsheet />
</div>
<div className="min-w-0 text-right">
<div className="min-w-0 text-end">
<p className="truncate font-bold text-[#1c2521]">{uploadedFile.name}</p>
<p className="text-sm text-[#68736f]">{uploadedFile.date}</p>
</div>
Expand Down