fix: RTL support for timelines and header dropdown
All checks were successful
Deploy / deploy (push) Successful in 11s
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:
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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": "النقاش",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user