fix: RTL support for timelines and header dropdown
All checks were successful
Deploy / deploy (push) Successful in 11s

- InteractiveTimeline: dir="ltr" on scroll area, i18n for all strings
- ArtefactVersionTimeline: text-start, ms-11 logical properties
- Header dropdown: end-0 instead of right-0, text-start on menu items
- Added 11 new timeline i18n keys (en + ar)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-03-04 23:54:32 +03:00
parent fe509b65a9
commit 7dc7fbbbe2
5 changed files with 56 additions and 32 deletions

View File

@@ -19,7 +19,7 @@ export default function ArtefactVersionTimeline({ versions, activeVersionId, onS
<button <button
key={version.Id} key={version.Id}
onClick={() => onSelectVersion(version)} onClick={() => onSelectVersion(version)}
className={`w-full text-left p-3 rounded-lg border transition-colors ${ className={`w-full text-start p-3 rounded-lg border transition-colors ${
isActive isActive
? 'border-brand-primary bg-brand-primary/5' ? 'border-brand-primary bg-brand-primary/5'
: 'border-border hover:border-brand-primary/30 bg-surface hover:shadow-sm' : 'border-border hover:border-brand-primary/30 bg-surface hover:shadow-sm'
@@ -80,7 +80,7 @@ export default function ArtefactVersionTimeline({ versions, activeVersionId, onS
{/* Thumbnail for image artefacts */} {/* Thumbnail for image artefacts */}
{artefactType === 'design' && version.thumbnail && ( {artefactType === 'design' && version.thumbnail && (
<div className="mt-2 ml-11"> <div className="mt-2 ms-11">
<img <img
src={version.thumbnail} src={version.thumbnail}
alt={`Version ${version.version_number}`} alt={`Version ${version.version_number}`}

View File

@@ -122,7 +122,7 @@ export default function Header() {
}`}> }`}>
{getInitials(user?.name)} {getInitials(user?.name)}
</div> </div>
<div className="text-left hidden sm:block"> <div className="text-start hidden sm:block">
<p className="text-sm font-medium text-text-primary"> <p className="text-sm font-medium text-text-primary">
{user?.name || 'User'} {user?.name || 'User'}
</p> </p>
@@ -134,7 +134,7 @@ export default function Header() {
</button> </button>
{showDropdown && ( {showDropdown && (
<div className="absolute right-0 top-full mt-2 w-64 bg-white rounded-xl shadow-lg border border-border overflow-hidden animate-scale-in"> <div className="absolute end-0 top-full mt-2 w-64 bg-white rounded-xl shadow-lg border border-border overflow-hidden animate-scale-in">
{/* User info */} {/* User info */}
<div className="px-4 py-3 border-b border-border-light bg-surface-secondary"> <div className="px-4 py-3 border-b border-border-light bg-surface-secondary">
<p className="text-sm font-semibold text-text-primary">{user?.name}</p> <p className="text-sm font-semibold text-text-primary">{user?.name}</p>
@@ -153,7 +153,7 @@ export default function Header() {
setShowDropdown(false) setShowDropdown(false)
window.location.href = '/users' window.location.href = '/users'
}} }}
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-secondary transition-colors text-left" className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-secondary transition-colors text-start"
> >
<Shield className="w-4 h-4 text-text-tertiary" /> <Shield className="w-4 h-4 text-text-tertiary" />
<span className="text-sm text-text-primary">User Management</span> <span className="text-sm text-text-primary">User Management</span>
@@ -162,7 +162,7 @@ export default function Header() {
<button <button
onClick={openPasswordModal} onClick={openPasswordModal}
className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-secondary transition-colors text-left" className="w-full flex items-center gap-3 px-4 py-2.5 hover:bg-surface-secondary transition-colors text-start"
> >
<Lock className="w-4 h-4 text-text-tertiary" /> <Lock className="w-4 h-4 text-text-tertiary" />
<span className="text-sm text-text-primary">Change Password</span> <span className="text-sm text-text-primary">Change Password</span>

View File

@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, useCallback, useMemo } from 'react' import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
import { format, differenceInDays, startOfDay, addDays, isBefore, isAfter } from 'date-fns' import { format, differenceInDays, startOfDay, addDays, isBefore, isAfter } from 'date-fns'
import { Calendar, Rows3, Rows4 } from 'lucide-react' import { Calendar, Rows3, Rows4 } from 'lucide-react'
import { useLanguage } from '../i18n/LanguageContext'
const STATUS_COLORS = { const STATUS_COLORS = {
todo: 'bg-gray-500', todo: 'bg-gray-500',
@@ -33,9 +34,9 @@ const PRIORITY_BORDER = {
} }
const ZOOM_LEVELS = [ const ZOOM_LEVELS = [
{ key: 'month', label: 'Month', pxPerDay: 8 }, { key: 'month', i18n: 'timeline.month', pxPerDay: 8 },
{ key: 'week', label: 'Week', pxPerDay: 20 }, { key: 'week', i18n: 'timeline.week', pxPerDay: 20 },
{ key: 'day', label: 'Day', pxPerDay: 48 }, { key: 'day', i18n: 'timeline.day', pxPerDay: 48 },
] ]
const COLOR_PALETTE = [ const COLOR_PALETTE = [
@@ -50,6 +51,7 @@ function getInitials(name) {
} }
export default function InteractiveTimeline({ items = [], mapItem, onDateChange, onColorChange, onItemClick, readOnly = false }) { export default function InteractiveTimeline({ items = [], mapItem, onDateChange, onColorChange, onItemClick, readOnly = false }) {
const { t } = useLanguage()
const containerRef = useRef(null) const containerRef = useRef(null)
const didDragRef = useRef(false) const didDragRef = useRef(false)
const optimisticRef = useRef({}) // { [itemId]: { startDate, endDate } } — keeps bar in place until fresh data arrives const optimisticRef = useRef({}) // { [itemId]: { startDate, endDate } } — keeps bar in place until fresh data arrives
@@ -237,8 +239,8 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
return ( return (
<div className="bg-white rounded-xl border border-border py-16 text-center"> <div className="bg-white rounded-xl border border-border py-16 text-center">
<Calendar className="w-12 h-12 text-text-tertiary mx-auto mb-3" /> <Calendar className="w-12 h-12 text-text-tertiary mx-auto mb-3" />
<p className="text-text-secondary font-medium">No items to display</p> <p className="text-text-secondary font-medium">{t('timeline.noItems')}</p>
<p className="text-sm text-text-tertiary mt-1">Add items with dates to see the timeline</p> <p className="text-sm text-text-tertiary mt-1">{t('timeline.addItems')}</p>
</div> </div>
) )
} }
@@ -258,7 +260,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
: 'text-text-tertiary hover:text-text-primary hover:bg-surface-tertiary' : 'text-text-tertiary hover:text-text-primary hover:bg-surface-tertiary'
}`} }`}
> >
{z.label} {t(z.i18n)}
</button> </button>
))} ))}
</div> </div>
@@ -266,28 +268,28 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
<button <button
onClick={() => setBarMode(m => m === 'compact' ? 'expanded' : 'compact')} onClick={() => setBarMode(m => m === 'compact' ? 'expanded' : 'compact')}
className="flex items-center gap-1.5 px-3 py-1 text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-md transition-colors" className="flex items-center gap-1.5 px-3 py-1 text-xs font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-md transition-colors"
title={isExpanded ? 'Compact bars' : 'Expanded bars'} title={isExpanded ? t('timeline.compactBars') : t('timeline.expandedBars')}
> >
{isExpanded ? <Rows4 className="w-3.5 h-3.5" /> : <Rows3 className="w-3.5 h-3.5" />} {isExpanded ? <Rows4 className="w-3.5 h-3.5" /> : <Rows3 className="w-3.5 h-3.5" />}
{isExpanded ? 'Compact' : 'Expand'} {isExpanded ? t('timeline.compact') : t('timeline.expand')}
</button> </button>
<button <button
onClick={scrollToToday} onClick={scrollToToday}
className="flex items-center gap-1.5 px-3 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/10 rounded-md transition-colors" className="flex items-center gap-1.5 px-3 py-1 text-xs font-medium text-brand-primary hover:bg-brand-primary/10 rounded-md transition-colors"
> >
<Calendar className="w-3.5 h-3.5" /> <Calendar className="w-3.5 h-3.5" />
Today {t('timeline.today')}
</button> </button>
</div> </div>
</div> </div>
{/* Timeline */} {/* Timeline */}
<div ref={containerRef} className="overflow-x-auto relative" style={{ cursor: dragState ? 'grabbing' : undefined }}> <div ref={containerRef} dir="ltr" className="overflow-x-auto relative" style={{ cursor: dragState ? 'grabbing' : undefined }}>
<div style={{ minWidth: `${labelWidth + totalDays * pxPerDay}px` }}> <div style={{ minWidth: `${labelWidth + totalDays * pxPerDay}px` }}>
{/* Day header */} {/* Day header */}
<div className="flex sticky top-0 z-20 bg-white border-b border-border" style={{ height: headerHeight }}> <div className="flex sticky top-0 z-20 bg-white border-b border-border" style={{ height: headerHeight }}>
<div className="shrink-0 border-r border-border bg-surface-secondary flex items-center px-4 sticky left-0 z-30" style={{ width: labelWidth }}> <div className="shrink-0 border-e border-border bg-surface-secondary flex items-center px-4 sticky left-0 z-30" style={{ width: labelWidth }}>
<span className="text-xs font-semibold text-text-tertiary uppercase tracking-wider">Item</span> <span className="text-xs font-semibold text-text-tertiary uppercase tracking-wider">{t('timeline.item')}</span>
</div> </div>
<div className="flex relative"> <div className="flex relative">
{days.map((day, i) => { {days.map((day, i) => {
@@ -336,7 +338,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
> >
{/* Label column */} {/* Label column */}
<div <div
className={`shrink-0 border-r border-border flex ${isExpanded ? 'flex-col justify-center gap-1' : 'items-center gap-2'} px-3 overflow-hidden sticky left-0 z-10 bg-white group-hover/row:bg-surface-secondary/50`} className={`shrink-0 border-e border-border flex ${isExpanded ? 'flex-col justify-center gap-1' : 'items-center gap-2'} px-3 overflow-hidden sticky left-0 z-10 bg-white group-hover/row:bg-surface-secondary/50`}
style={{ width: labelWidth }} style={{ width: labelWidth }}
> >
{isExpanded ? ( {isExpanded ? (
@@ -351,7 +353,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
}} }}
className={`w-5 h-5 rounded-full border-2 border-white shadow-sm shrink-0 hover:scale-110 transition-transform ${!item.color ? (STATUS_COLORS[item.status] || 'bg-gray-400') : ''}`} className={`w-5 h-5 rounded-full border-2 border-white shadow-sm shrink-0 hover:scale-110 transition-transform ${!item.color ? (STATUS_COLORS[item.status] || 'bg-gray-400') : ''}`}
style={item.color ? { backgroundColor: item.color } : undefined} style={item.color ? { backgroundColor: item.color } : undefined}
title="Change color" title={t('timeline.changeColor')}
/> />
)} )}
{item.thumbnailUrl ? ( {item.thumbnailUrl ? (
@@ -387,7 +389,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
}} }}
className={`w-4 h-4 rounded-full border-2 border-white shadow-sm shrink-0 hover:scale-110 transition-transform ${!item.color ? (STATUS_COLORS[item.status] || 'bg-gray-400') : ''}`} className={`w-4 h-4 rounded-full border-2 border-white shadow-sm shrink-0 hover:scale-110 transition-transform ${!item.color ? (STATUS_COLORS[item.status] || 'bg-gray-400') : ''}`}
style={item.color ? { backgroundColor: item.color } : undefined} style={item.color ? { backgroundColor: item.color } : undefined}
title="Change color" title={t('timeline.changeColor')}
/> />
)} )}
{item.thumbnailUrl ? ( {item.thumbnailUrl ? (
@@ -414,7 +416,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
> >
{idx === 0 && ( {idx === 0 && (
<div className="absolute -top-0 left-1 text-[8px] font-bold text-red-500 bg-red-50 px-1 rounded whitespace-nowrap"> <div className="absolute -top-0 left-1 text-[8px] font-bold text-red-500 bg-red-50 px-1 rounded whitespace-nowrap">
Today {t('timeline.today')}
</div> </div>
)} )}
</div> </div>
@@ -477,7 +479,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
</span> </span>
)} )}
{width > 120 && item.status && ( {width > 120 && item.status && (
<span className="text-[9px] text-white/70 bg-white/15 px-1.5 py-0.5 rounded ml-auto shrink-0"> <span className="text-[9px] text-white/70 bg-white/15 px-1.5 py-0.5 rounded ms-auto shrink-0">
{item.status.replace(/_/g, ' ')} {item.status.replace(/_/g, ' ')}
</span> </span>
)} )}
@@ -493,7 +495,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
<span key={i} className="text-[8px] px-1 py-0.5 rounded bg-white/15 text-white/70 font-medium">{tag}</span> <span key={i} className="text-[8px] px-1 py-0.5 rounded bg-white/15 text-white/70 font-medium">{tag}</span>
))} ))}
{width > 140 && item.startDate && item.endDate && ( {width > 140 && item.startDate && item.endDate && (
<span className="text-[8px] text-white/50 ml-auto"> <span className="text-[8px] text-white/50 ms-auto">
{format(item.startDate, 'MMM d')} {format(item.endDate, 'MMM d')} {format(item.startDate, 'MMM d')} {format(item.endDate, 'MMM d')}
</span> </span>
)} )}
@@ -557,7 +559,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
}} }}
className="w-full text-[10px] text-text-tertiary hover:text-text-primary text-center py-1 hover:bg-surface-tertiary rounded transition-colors" className="w-full text-[10px] text-text-tertiary hover:text-text-primary text-center py-1 hover:bg-surface-tertiary rounded transition-colors"
> >
Reset to default {t('timeline.resetColor')}
</button> </button>
</div> </div>
)} )}
@@ -576,21 +578,21 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
<div className="font-semibold mb-1">{tooltip.item.label}</div> <div className="font-semibold mb-1">{tooltip.item.label}</div>
<div className="text-gray-300 space-y-0.5"> <div className="text-gray-300 space-y-0.5">
{tooltip.item.startDate && ( {tooltip.item.startDate && (
<div>Start: {format(tooltip.item.startDate, 'MMM d, yyyy')}</div> <div>{t('timeline.startDate')}: {format(tooltip.item.startDate, 'MMM d, yyyy')}</div>
)} )}
{tooltip.item.endDate && ( {tooltip.item.endDate && (
<div>End: {format(tooltip.item.endDate, 'MMM d, yyyy')}</div> <div>{t('timeline.endDate')}: {format(tooltip.item.endDate, 'MMM d, yyyy')}</div>
)} )}
{tooltip.item.assigneeName && ( {tooltip.item.assigneeName && (
<div>Assignee: {tooltip.item.assigneeName}</div> <div>{t('timeline.assignee')}: {tooltip.item.assigneeName}</div>
)} )}
{tooltip.item.status && ( {tooltip.item.status && (
<div>Status: {tooltip.item.status.replace(/_/g, ' ')}</div> <div>{t('timeline.status')}: {tooltip.item.status.replace(/_/g, ' ')}</div>
)} )}
</div> </div>
{!readOnly && onDateChange && ( {!readOnly && onDateChange && (
<div className="text-gray-400 mt-1 text-[10px] italic"> <div className="text-gray-400 mt-1 text-[10px] italic">
Drag to move · Drag edges to resize {t('timeline.dragToMove')} · {t('timeline.dragToResize')}
</div> </div>
)} )}
</div> </div>

View File

@@ -351,13 +351,24 @@
"timeline.day": "يوم", "timeline.day": "يوم",
"timeline.week": "أسبوع", "timeline.week": "أسبوع",
"timeline.today": "اليوم", "timeline.today": "اليوم",
"timeline.startDate": "تاريخ البدء", "timeline.startDate": "البداية",
"timeline.endDate": "النهاية",
"timeline.assignee": "المُكلّف",
"timeline.status": "الحالة",
"timeline.dragToMove": "اسحب للنقل", "timeline.dragToMove": "اسحب للنقل",
"timeline.dragToResize": "اسحب الحواف لتغيير الحجم", "timeline.dragToResize": "اسحب الحواف لتغيير الحجم",
"timeline.noItems": "لا توجد عناصر للعرض", "timeline.noItems": "لا توجد عناصر للعرض",
"timeline.addItems": "أضف عناصر بتواريخ لعرض الجدول الزمني", "timeline.addItems": "أضف عناصر بتواريخ لعرض الجدول الزمني",
"timeline.tracks": "المسارات", "timeline.tracks": "المسارات",
"timeline.timeline": "الجدول الزمني", "timeline.timeline": "الجدول الزمني",
"timeline.item": "العنصر",
"timeline.month": "شهر",
"timeline.compact": "مضغوط",
"timeline.expand": "موسّع",
"timeline.resetColor": "إعادة إلى الافتراضي",
"timeline.changeColor": "تغيير اللون",
"timeline.compactBars": "أشرطة مضغوطة",
"timeline.expandedBars": "أشرطة موسّعة",
"posts.details": "التفاصيل", "posts.details": "التفاصيل",
"posts.platformsLinks": "المنصات والروابط", "posts.platformsLinks": "المنصات والروابط",
"posts.discussion": "النقاش", "posts.discussion": "النقاش",

View File

@@ -351,13 +351,24 @@
"timeline.day": "Day", "timeline.day": "Day",
"timeline.week": "Week", "timeline.week": "Week",
"timeline.today": "Today", "timeline.today": "Today",
"timeline.startDate": "Start Date", "timeline.startDate": "Start",
"timeline.endDate": "End",
"timeline.assignee": "Assignee",
"timeline.status": "Status",
"timeline.dragToMove": "Drag to move", "timeline.dragToMove": "Drag to move",
"timeline.dragToResize": "Drag edges to resize", "timeline.dragToResize": "Drag edges to resize",
"timeline.noItems": "No items to display", "timeline.noItems": "No items to display",
"timeline.addItems": "Add items with dates to see the timeline", "timeline.addItems": "Add items with dates to see the timeline",
"timeline.tracks": "Tracks", "timeline.tracks": "Tracks",
"timeline.timeline": "Timeline", "timeline.timeline": "Timeline",
"timeline.item": "Item",
"timeline.month": "Month",
"timeline.compact": "Compact",
"timeline.expand": "Expand",
"timeline.resetColor": "Reset to default",
"timeline.changeColor": "Change color",
"timeline.compactBars": "Compact bars",
"timeline.expandedBars": "Expanded bars",
"posts.details": "Details", "posts.details": "Details",
"posts.platformsLinks": "Platforms & Links", "posts.platformsLinks": "Platforms & Links",
"posts.discussion": "Discussion", "posts.discussion": "Discussion",