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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user