style(timesheet): add loading skeleton and soften dark surfaces

This commit is contained in:
2026-04-29 02:05:53 +03:30
parent cb4a7ae118
commit e60a4c9ab4
2 changed files with 128 additions and 37 deletions

View File

@@ -271,7 +271,7 @@ export default function TimesheetFilterBar({
); );
return ( return (
<div className="rounded-lg border border-slate-200 bg-white p-2.5 shadow-sm dark:border-slate-800 dark:bg-slate-900"> <div className="rounded-lg border border-slate-200 bg-white p-2.5 shadow-sm dark:border-slate-700 dark:bg-slate-900/95">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative min-w-0 flex-1"> <div className="relative min-w-0 flex-1">
@@ -332,7 +332,7 @@ export default function TimesheetFilterBar({
</div> </div>
{isExpanded && ( {isExpanded && (
<div className="border-t border-slate-200 pt-2 dark:border-slate-800"> <div className="border-t border-slate-200 pt-2 dark:border-slate-700">
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.2fr)]"> <div className="grid gap-2 md:grid-cols-2 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,0.9fr)_minmax(0,1fr)_minmax(0,1fr)_minmax(0,1.2fr)]">
<MiniFilterBlock icon={<CalendarRange className="h-3.5 w-3.5" />} label={labels?.customFrom || "From date"}> <MiniFilterBlock icon={<CalendarRange className="h-3.5 w-3.5" />} label={labels?.customFrom || "From date"}>
<JalaliDatePicker <JalaliDatePicker

View File

@@ -1583,7 +1583,7 @@ function RecordedEntryCard({
<div <div
ref={rowRef} ref={rowRef}
onBlurCapture={handleBlurCapture} onBlurCapture={handleBlurCapture}
className="border-b border-slate-200 bg-white px-4 py-4 dark:border-slate-800 dark:bg-slate-950" className="border-b border-slate-200 bg-white px-4 py-4 dark:border-slate-700 dark:bg-slate-900/95"
> >
<div className="space-y-4"> <div className="space-y-4">
<EntryEditorFields <EntryEditorFields
@@ -1598,7 +1598,7 @@ function RecordedEntryCard({
portalOwnerId={editorOwnerId} portalOwnerId={editorOwnerId}
/> />
<div className="flex items-center justify-between gap-3 border-t border-slate-200 pt-3 dark:border-slate-800"> <div className="flex items-center justify-between gap-3 border-t border-slate-200 pt-3 dark:border-slate-700">
<div className="text-sm text-slate-500 dark:text-slate-400"> <div className="text-sm text-slate-500 dark:text-slate-400">
{formatDateTime(entry.start_time, lang)} {formatDateTime(entry.start_time, lang)}
</div> </div>
@@ -1634,7 +1634,7 @@ function RecordedEntryCard({
} }
return ( return (
<div ref={rowRef} onBlurCapture={handleBlurCapture} className="border-b border-slate-200 bg-white px-4 py-4 dark:border-slate-800 dark:bg-slate-950"> <div ref={rowRef} onBlurCapture={handleBlurCapture} className="border-b border-slate-200 bg-white px-4 py-4 dark:border-slate-700 dark:bg-slate-900/95">
<div className="flex min-w-0 items-center"> <div className="flex min-w-0 items-center">
<EntryEditorFields <EntryEditorFields
state={draft} state={draft}
@@ -1649,20 +1649,20 @@ function RecordedEntryCard({
portalOwnerId={editorOwnerId} portalOwnerId={editorOwnerId}
/> />
<div className="flex h-12 shrink-0 items-center border-s border-slate-200 px-5 text-sm font-semibold text-slate-700 dark:border-slate-800 dark:text-slate-200"> <div className="flex h-12 shrink-0 items-center border-s border-slate-200 px-5 text-sm font-semibold text-slate-700 dark:border-slate-700 dark:text-slate-200">
{formatDuration(entry)} {formatDuration(entry)}
</div> </div>
<button <button
type="button" type="button"
onClick={() => onRestart(entry)} onClick={() => onRestart(entry)}
className="inline-flex h-12 w-10 shrink-0 items-center justify-center border-s border-slate-200 bg-transparent text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700 dark:border-slate-800 dark:text-slate-500 dark:hover:bg-slate-900 dark:hover:text-white" className="inline-flex h-12 w-10 shrink-0 items-center justify-center border-s border-slate-200 bg-transparent text-slate-400 transition-colors hover:bg-slate-50 hover:text-slate-700 dark:border-slate-700 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-white"
title="Start from this entry" title="Start from this entry"
> >
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
</button> </button>
<div className="border-s border-slate-200 dark:border-slate-800"> <div className="border-s border-slate-200 dark:border-slate-700">
<DeleteEntryButton onDelete={() => onDelete(entry)} /> <DeleteEntryButton onDelete={() => onDelete(entry)} />
</div> </div>
</div> </div>
@@ -1791,7 +1791,7 @@ function MobileRecordedEntryCard({
}; };
return ( return (
<div ref={wrapperRef} className="relative overflow-hidden border-b border-slate-200 bg-slate-100/70 dark:border-slate-800 dark:bg-slate-900/70 xl:hidden"> <div ref={wrapperRef} className="relative overflow-hidden border-b border-slate-200 bg-slate-100/70 dark:border-slate-700 dark:bg-slate-800/70 xl:hidden">
<div className="pointer-events-none absolute inset-y-0 left-0 flex w-24 items-center justify-start bg-emerald-500/12 ps-4 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300"> <div className="pointer-events-none absolute inset-y-0 left-0 flex w-24 items-center justify-start bg-emerald-500/12 ps-4 text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300">
<Play className="h-4 w-4" /> <Play className="h-4 w-4" />
</div> </div>
@@ -1800,7 +1800,7 @@ function MobileRecordedEntryCard({
</div> </div>
<div <div
className="relative bg-white px-4 py-5 transition-transform duration-150 ease-out dark:bg-slate-950" className="relative bg-white px-4 py-5 transition-transform duration-150 ease-out dark:bg-slate-900/95"
style={{ transform: `translateX(${swipeOffset}px)` }} style={{ transform: `translateX(${swipeOffset}px)` }}
onTouchStart={handleTouchStart} onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove} onTouchMove={handleTouchMove}
@@ -1917,6 +1917,97 @@ function MobileRecordedEntryCard({
); );
} }
function TimesheetSkeleton({ loadingLabel }: { loadingLabel: string }) {
return (
<div className="flex flex-1 flex-col gap-4 animate-pulse">
<div className="hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-slate-900/95 xl:block">
<div className="flex items-center gap-2 px-3 py-3">
<div className="h-12 flex-1 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-12 w-52 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-12 w-48 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-12 w-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-12 w-28 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-12 w-12 rounded-lg bg-slate-200 dark:bg-slate-800" />
</div>
</div>
<div className="rounded-xl border border-slate-200 bg-white p-3 shadow-sm dark:border-slate-700 dark:bg-slate-900/95 xl:hidden">
<div className="space-y-3">
<div className="h-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-2">
<div className="h-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-10 w-28 rounded-lg bg-slate-200 dark:bg-slate-800" />
</div>
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<div className="h-9 w-28 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-9 w-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
</div>
<div className="flex items-center gap-2">
<div className="h-10 w-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="h-10 w-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
</div>
</div>
</div>
</div>
<div className="rounded-lg border border-slate-200 bg-white p-2.5 shadow-sm dark:border-slate-700 dark:bg-slate-900/95">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="h-9 flex-1 rounded-md bg-slate-200 dark:bg-slate-800" />
<div className="h-9 w-9 rounded-md bg-slate-200 dark:bg-slate-800" />
<div className="h-9 w-9 rounded-md bg-slate-200 dark:bg-slate-800" />
</div>
<div className="flex flex-wrap gap-2">
<div className="h-6 w-24 rounded-full bg-slate-200 dark:bg-slate-800" />
<div className="h-6 w-28 rounded-full bg-slate-200 dark:bg-slate-800" />
<div className="h-6 w-20 rounded-full bg-slate-200 dark:bg-slate-800" />
</div>
</div>
</div>
<div className="mb-1 flex items-center gap-3 text-slate-500 dark:text-slate-400">
<span className="h-2.5 w-2.5 rounded-full bg-sky-500" />
<span className="text-sm font-medium">{loadingLabel}</span>
</div>
<div className="space-y-4">
{[0, 1].map((weekIndex) => (
<div key={weekIndex} className="space-y-2">
<div className="flex items-center justify-between px-1">
<div className="h-4 w-40 rounded-full bg-slate-200 dark:bg-slate-800" />
<div className="h-4 w-28 rounded-full bg-slate-200 dark:bg-slate-800" />
</div>
{[0, 1].map((dayIndex) => (
<div
key={dayIndex}
className="overflow-hidden border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-slate-900/95"
>
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-100/80 px-4 py-2 dark:border-slate-700 dark:bg-slate-800/85">
<div className="h-3 w-24 rounded-full bg-slate-200 dark:bg-slate-700" />
<div className="h-3 w-20 rounded-full bg-slate-200 dark:bg-slate-700" />
</div>
<div className="space-y-px bg-slate-200/80 dark:bg-slate-700/70">
{[0, 1].map((entryIndex) => (
<div key={entryIndex} className="bg-white px-4 py-4 dark:bg-slate-900/95">
<div className="flex items-center gap-4">
<div className="h-10 flex-1 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="hidden h-10 w-28 rounded-lg bg-slate-200 dark:bg-slate-800 md:block" />
<div className="h-10 w-10 rounded-lg bg-slate-200 dark:bg-slate-800" />
<div className="hidden h-10 w-20 rounded-lg bg-slate-200 dark:bg-slate-800 lg:block" />
</div>
</div>
))}
</div>
</div>
))}
</div>
))}
</div>
</div>
);
}
export default function Timesheet() { export default function Timesheet() {
const { t, lang } = useTranslation(); const { t, lang } = useTranslation();
const { activeWorkspace } = useWorkspace(); const { activeWorkspace } = useWorkspace();
@@ -2484,7 +2575,7 @@ export default function Timesheet() {
<div <div
ref={desktopTimerRef} ref={desktopTimerRef}
onBlurCapture={handleTimerBlurCapture} onBlurCapture={handleTimerBlurCapture}
className="mb-4 hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950 xl:block" className="mb-4 hidden rounded-xl border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-slate-900/95 xl:block"
> >
<div className="flex min-w-0 items-center gap-2 px-3 py-3"> <div className="flex min-w-0 items-center gap-2 px-3 py-3">
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -2593,7 +2684,7 @@ export default function Timesheet() {
<div <div
ref={mobileTimerRef} ref={mobileTimerRef}
onBlurCapture={handleTimerBlurCapture} onBlurCapture={handleTimerBlurCapture}
className="mb-4 rounded-xl border border-slate-200 bg-white p-3 shadow-sm dark:border-slate-800 dark:bg-slate-950 xl:hidden" className="mb-4 rounded-xl border border-slate-200 bg-white p-3 shadow-sm dark:border-slate-700 dark:bg-slate-900/95 xl:hidden"
> >
<div className="space-y-3"> <div className="space-y-3">
<Input <Input
@@ -2729,7 +2820,7 @@ export default function Timesheet() {
</div> </div>
{isLoading ? ( {isLoading ? (
<div className="flex justify-center p-12 text-slate-500">{t.loading || "Loading..."}</div> <TimesheetSkeleton loadingLabel={t.loading || "Loading..."} />
) : ( ) : (
<InfiniteScroll <InfiniteScroll
className="flex flex-1 flex-col" className="flex flex-1 flex-col"
@@ -2750,8 +2841,8 @@ export default function Timesheet() {
</div> </div>
{week.days.map((day) => ( {week.days.map((day) => (
<div key={day.key} className="border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-950"> <div key={day.key} className="border border-slate-200 bg-white shadow-sm dark:border-slate-700 dark:bg-slate-900/95">
<div className="flex items-center justify-between border-b border-slate-200 bg-slate-100/80 px-4 py-2 dark:border-slate-800 dark:bg-slate-900"> <div className="flex items-center justify-between border-b border-slate-200 bg-slate-100/80 px-4 py-2 dark:border-slate-700 dark:bg-slate-800/85">
<p className="text-xs font-medium text-slate-500 dark:text-slate-400"> <p className="text-xs font-medium text-slate-500 dark:text-slate-400">
{formatDayLabel(new Date(`${day.date}T00:00:00`), lang)} {formatDayLabel(new Date(`${day.date}T00:00:00`), lang)}
</p> </p>
@@ -2796,7 +2887,7 @@ export default function Timesheet() {
))} ))}
{groupedHistory.length === 0 && ( {groupedHistory.length === 0 && (
<div className="flex flex-col items-center justify-center border-2 border-dashed border-slate-200 py-16 text-slate-500 dark:border-slate-800 dark:text-slate-400"> <div className="flex flex-col items-center justify-center border-2 border-dashed border-slate-200 bg-white/60 py-16 text-slate-500 dark:border-slate-700 dark:bg-slate-900/60 dark:text-slate-400">
<Clock3 className="mb-3 h-10 w-10" /> <Clock3 className="mb-3 h-10 w-10" />
<p>{t.timesheet?.emptyState || "No time entries found"}</p> <p>{t.timesheet?.emptyState || "No time entries found"}</p>
</div> </div>
@@ -2853,7 +2944,7 @@ export default function Timesheet() {
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400"> <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.deleteConfirmMessage || t.timesheet?.deleteConfirmMessage || "Are you sure you want to delete this time entry?"} {extendedTimesheet.deleteConfirmMessage || t.timesheet?.deleteConfirmMessage || "Are you sure you want to delete this time entry?"}
</p> </p>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60"> <div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/60">
<p className="font-medium text-slate-900 dark:text-white"> <p className="font-medium text-slate-900 dark:text-white">
{deleteModal.entry.description || t.timesheet?.emptyDescription || "No description"} {deleteModal.entry.description || t.timesheet?.emptyDescription || "No description"}
</p> </p>
@@ -2887,7 +2978,7 @@ export default function Timesheet() {
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400"> <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.restartConfirmMessage || t.timesheet?.restartConfirmMessage || "Start a new running timer from this entry?"} {extendedTimesheet.restartConfirmMessage || t.timesheet?.restartConfirmMessage || "Start a new running timer from this entry?"}
</p> </p>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60"> <div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/60">
<p className="font-medium text-slate-900 dark:text-white"> <p className="font-medium text-slate-900 dark:text-white">
{restartModal.entry.description || t.timesheet?.emptyDescription || "No description"} {restartModal.entry.description || t.timesheet?.emptyDescription || "No description"}
</p> </p>
@@ -2921,7 +3012,7 @@ export default function Timesheet() {
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400"> <p className="text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{extendedTimesheet.discardConfirmMessage || t.timesheet?.discardConfirmMessage || "Are you sure you want to discard this running timer?"} {extendedTimesheet.discardConfirmMessage || t.timesheet?.discardConfirmMessage || "Are you sure you want to discard this running timer?"}
</p> </p>
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/60"> <div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 dark:border-slate-700 dark:bg-slate-800/60">
<p className="font-medium text-slate-900 dark:text-white"> <p className="font-medium text-slate-900 dark:text-white">
{discardTimerModal.entry.description || t.timesheet?.emptyDescription || "No description"} {discardTimerModal.entry.description || t.timesheet?.emptyDescription || "No description"}
</p> </p>