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
key={version.Id}
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
? 'border-brand-primary bg-brand-primary/5'
: '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 */}
{artefactType === 'design' && version.thumbnail && (
<div className="mt-2 ml-11">
<div className="mt-2 ms-11">
<img
src={version.thumbnail}
alt={`Version ${version.version_number}`}

View File

@@ -122,7 +122,7 @@ export default function Header() {
}`}>
{getInitials(user?.name)}
</div>
<div className="text-left hidden sm:block">
<div className="text-start hidden sm:block">
<p className="text-sm font-medium text-text-primary">
{user?.name || 'User'}
</p>
@@ -134,7 +134,7 @@ export default function Header() {
</button>
{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 */}
<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>
@@ -153,7 +153,7 @@ export default function Header() {
setShowDropdown(false)
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" />
<span className="text-sm text-text-primary">User Management</span>
@@ -162,7 +162,7 @@ export default function Header() {
<button
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" />
<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 { format, differenceInDays, startOfDay, addDays, isBefore, isAfter } from 'date-fns'
import { Calendar, Rows3, Rows4 } from 'lucide-react'
import { useLanguage } from '../i18n/LanguageContext'
const STATUS_COLORS = {
todo: 'bg-gray-500',
@@ -33,9 +34,9 @@ const PRIORITY_BORDER = {
}
const ZOOM_LEVELS = [
{ key: 'month', label: 'Month', pxPerDay: 8 },
{ key: 'week', label: 'Week', pxPerDay: 20 },
{ key: 'day', label: 'Day', pxPerDay: 48 },
{ key: 'month', i18n: 'timeline.month', pxPerDay: 8 },
{ key: 'week', i18n: 'timeline.week', pxPerDay: 20 },
{ key: 'day', i18n: 'timeline.day', pxPerDay: 48 },
]
const COLOR_PALETTE = [
@@ -50,6 +51,7 @@ function getInitials(name) {
}
export default function InteractiveTimeline({ items = [], mapItem, onDateChange, onColorChange, onItemClick, readOnly = false }) {
const { t } = useLanguage()
const containerRef = useRef(null)
const didDragRef = useRef(false)
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 (
<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" />
<p className="text-text-secondary font-medium">No items to display</p>
<p className="text-sm text-text-tertiary mt-1">Add items with dates to see the timeline</p>
<p className="text-text-secondary font-medium">{t('timeline.noItems')}</p>
<p className="text-sm text-text-tertiary mt-1">{t('timeline.addItems')}</p>
</div>
)
}
@@ -258,7 +260,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
: 'text-text-tertiary hover:text-text-primary hover:bg-surface-tertiary'
}`}
>
{z.label}
{t(z.i18n)}
</button>
))}
</div>
@@ -266,28 +268,28 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
<button
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"
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 ? 'Compact' : 'Expand'}
{isExpanded ? t('timeline.compact') : t('timeline.expand')}
</button>
<button
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"
>
<Calendar className="w-3.5 h-3.5" />
Today
{t('timeline.today')}
</button>
</div>
</div>
{/* 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` }}>
{/* Day header */}
<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 }}>
<span className="text-xs font-semibold text-text-tertiary uppercase tracking-wider">Item</span>
<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">{t('timeline.item')}</span>
</div>
<div className="flex relative">
{days.map((day, i) => {
@@ -336,7 +338,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
>
{/* Label column */}
<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 }}
>
{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') : ''}`}
style={item.color ? { backgroundColor: item.color } : undefined}
title="Change color"
title={t('timeline.changeColor')}
/>
)}
{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') : ''}`}
style={item.color ? { backgroundColor: item.color } : undefined}
title="Change color"
title={t('timeline.changeColor')}
/>
)}
{item.thumbnailUrl ? (
@@ -414,7 +416,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
>
{idx === 0 && (
<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>
@@ -477,7 +479,7 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
</span>
)}
{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, ' ')}
</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>
))}
{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')}
</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"
>
Reset to default
{t('timeline.resetColor')}
</button>
</div>
)}
@@ -576,21 +578,21 @@ export default function InteractiveTimeline({ items = [], mapItem, onDateChange,
<div className="font-semibold mb-1">{tooltip.item.label}</div>
<div className="text-gray-300 space-y-0.5">
{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 && (
<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 && (
<div>Assignee: {tooltip.item.assigneeName}</div>
<div>{t('timeline.assignee')}: {tooltip.item.assigneeName}</div>
)}
{tooltip.item.status && (
<div>Status: {tooltip.item.status.replace(/_/g, ' ')}</div>
<div>{t('timeline.status')}: {tooltip.item.status.replace(/_/g, ' ')}</div>
)}
</div>
{!readOnly && onDateChange && (
<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>

View File

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

View File

@@ -351,13 +351,24 @@
"timeline.day": "Day",
"timeline.week": "Week",
"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.dragToResize": "Drag edges to resize",
"timeline.noItems": "No items to display",
"timeline.addItems": "Add items with dates to see the timeline",
"timeline.tracks": "Tracks",
"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.platformsLinks": "Platforms & Links",
"posts.discussion": "Discussion",