Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 8 additions & 0 deletions apps/web/src/App.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ vi.mock('./components/ui/tooltip', () => ({
TooltipProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

vi.mock('./components/ui/direction', () => ({
DirectionProvider: ({ children }: { children: React.ReactNode }) => (
<div 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 +52,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 +64,7 @@ describe('App shell layout behavior', () => {
render(<App />);

expect(screen.queryByTestId('marketing-layout')).toBeNull();
screen.getByTestId('dashboard-direction-provider');
screen.getByText('upload-data-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