fix: polish dashboard loading and interactions

This commit is contained in:
2026-08-20 22:59:07 +03:30
parent b599cdfba4
commit 05e4dcd123
7 changed files with 48 additions and 26 deletions

View File

@@ -132,6 +132,7 @@ The approved Persian-first baseline remains authoritative where the shipped arti
| Version | Date | Decision | | Version | Date | Decision |
| ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0.8.0 | 2026-08-20 | Increased light-theme skeleton contrast, constrained and animated chart tooltips, made default period choices explicitly applicable, hid collapsed-sidebar labels, and strengthened the separation between period type and exact-period controls. |
| 0.7.0 | 2026-08-20 | Made period changes confirm-on-selection, moved narrow-screen filters into a bottom sheet, enlarged collapsed navigation targets, simplified focus rings, and promoted the fee-share chart into a dedicated visual column. | | 0.7.0 | 2026-08-20 | Made period changes confirm-on-selection, moved narrow-screen filters into a bottom sheet, enlarged collapsed navigation targets, simplified focus rings, and promoted the fee-share chart into a dedicated visual column. |
| 0.6.0 | 2026-08-20 | Added full-span Jalali period selection, unified transaction status/amount controls, persistent skeleton states, custom RTL dropdowns, semantic peer/retry tones, and fee-share visualization; tightened the narrow navbar and 600px KPI breakpoint. | | 0.6.0 | 2026-08-20 | Added full-span Jalali period selection, unified transaction status/amount controls, persistent skeleton states, custom RTL dropdowns, semantic peer/retry tones, and fee-share visualization; tightened the narrow navbar and 600px KPI breakpoint. |
| 0.5.0 | 2026-08-20 | Reconciled the approved baseline with the shipped routed frontend and desktop, mobile, and 639px review captures; converted the file to canonical DESIGN.md format and added the design sidecar. | | 0.5.0 | 2026-08-20 | Reconciled the approved baseline with the shipped routed frontend and desktop, mobile, and 639px review captures; converted the file to canonical DESIGN.md format and added the design sidecar. |
@@ -288,7 +289,7 @@ Scope changes immediately replace data-dependent cards, tables, detail panels, a
### Payment-History Chart ### Payment-History Chart
The signature chart is a blue line with outlined points, a low-opacity blue area, and dashed neutral gridlines. It follows a written title and reporting-period chip, provides focusable points with spoken values, and animates its initial trace once over 900ms while reduced-motion collapses the effect. The signature chart is a blue line with outlined points, a low-opacity blue area, and dashed neutral gridlines. It follows a written title and reporting-period chip, provides focusable points with spoken values, and animates its initial trace once over 900ms while reduced-motion collapses the effect. Point tooltips use a short scale/fade transition, flip above or below according to vertical room, and clamp their center so the surface never leaves the chart wrapper.
### Transaction Table ### Transaction Table

View File

@@ -19,6 +19,7 @@
--text: #19191a; --text: #19191a;
--text-secondary: #3d3d42; --text-secondary: #3d3d42;
--muted: #f3f4f6; --muted: #f3f4f6;
--skeleton: #e1e3e8;
--border: #e4e4e7; --border: #e4e4e7;
--brand: #0a33ff; --brand: #0a33ff;
--brand-hover: #0828cc; --brand-hover: #0828cc;
@@ -87,6 +88,7 @@
--secondary: #27272a; --secondary: #27272a;
--secondary-foreground: #fafafa; --secondary-foreground: #fafafa;
--muted: #27272a; --muted: #27272a;
--skeleton: #323237;
--muted-foreground: #a1a1aa; --muted-foreground: #a1a1aa;
--accent: #202950; --accent: #202950;
--accent-foreground: #a9baff; --accent-foreground: #a9baff;
@@ -244,6 +246,7 @@ select {
--color-accent: var(--accent); --color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground); --color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted); --color-muted: var(--muted);
--color-skeleton: var(--skeleton);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);

View File

@@ -100,7 +100,7 @@
<Sidebar.MenuButton <Sidebar.MenuButton
isActive={isActive(item.href)} isActive={isActive(item.href)}
tooltipContent={item.label} tooltipContent={item.label}
class="data-[active=true]:text-primary h-11 justify-start px-3 text-[13px] group-data-[collapsible=icon]:size-11! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:[&_svg]:size-[21px]!" class="data-[active=true]:text-primary h-11 justify-start px-3 text-[13px] group-data-[collapsible=icon]:size-11! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:[&_svg]:size-[21px]! group-data-[collapsible=icon]:[&>span]:hidden"
> >
{#snippet child({ props })} {#snippet child({ props })}
<a <a
@@ -109,7 +109,7 @@
aria-current={isActive(item.href) ? 'page' : undefined} aria-current={isActive(item.href) ? 'page' : undefined}
> >
<item.icon class="size-[19px]" strokeWidth={1.8} /> <item.icon class="size-[19px]" strokeWidth={1.8} />
<span>{item.label}</span> <span class="group-data-[collapsible=icon]:hidden">{item.label}</span>
</a> </a>
{/snippet} {/snippet}
</Sidebar.MenuButton> </Sidebar.MenuButton>
@@ -124,11 +124,11 @@
<Sidebar.MenuItem> <Sidebar.MenuItem>
<Sidebar.MenuButton <Sidebar.MenuButton
tooltipContent="خروج" tooltipContent="خروج"
class="h-11 px-3 group-data-[collapsible=icon]:size-11! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:[&_svg]:size-5!" class="h-11 px-3 group-data-[collapsible=icon]:size-11! group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:[&_svg]:size-5! group-data-[collapsible=icon]:[&>span]:hidden"
onclick={logout} onclick={logout}
> >
<LogOutIcon class="size-[18px]" /> <LogOutIcon class="size-[18px]" />
<span>خروج از حساب</span> <span class="group-data-[collapsible=icon]:hidden">خروج از حساب</span>
</Sidebar.MenuButton> </Sidebar.MenuButton>
</Sidebar.MenuItem> </Sidebar.MenuItem>
</Sidebar.Menu> </Sidebar.Menu>

View File

@@ -52,7 +52,7 @@
</script> </script>
{#snippet periodControls(mobile: boolean)} {#snippet periodControls(mobile: boolean)}
<div class={mobile ? 'grid gap-4' : 'flex min-w-0 items-center gap-3'}> <div class={mobile ? 'grid gap-4' : 'flex min-w-0 items-center gap-5'}>
<div <div
class={mobile ? 'grid grid-cols-2 gap-2' : 'flex max-w-full items-center gap-1 overflow-x-auto'} class={mobile ? 'grid grid-cols-2 gap-2' : 'flex max-w-full items-center gap-1 overflow-x-auto'}
aria-label="بازه گزارش" aria-label="بازه گزارش"
@@ -82,6 +82,7 @@
onselect={applyPeriod} onselect={applyPeriod}
/> />
{:else} {:else}
<div class="border-border border-s ps-5">
<PeriodPicker <PeriodPicker
periods={dashboard.periods} periods={dashboard.periods}
range={pickerRange} range={pickerRange}
@@ -89,6 +90,7 @@
bind:open={desktopPickerOpen} bind:open={desktopPickerOpen}
onselect={applyPeriod} onselect={applyPeriod}
/> />
</div>
{/if} {/if}
</div> </div>
{/snippet} {/snippet}

View File

@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { scale } from 'svelte/transition';
import type { components } from '$lib/api/schema'; import type { components } from '$lib/api/schema';
import { formatMoney, formatValue } from '$lib/format'; import { formatMoney, formatValue } from '$lib/format';
@@ -26,6 +28,14 @@
new Date(`${value}T12:00:00Z`), new Date(`${value}T12:00:00Z`),
); );
} }
function tooltipPosition(point: (typeof positions)[number]) {
const horizontalPercent = (point.x / width) * 100;
const verticalPercent = (point.y / height) * 100;
const placeBelow = verticalPercent < 34;
const top = placeBelow ? verticalPercent + 5 : verticalPercent - 5;
return `left:clamp(5.75rem, ${horizontalPercent}%, calc(100% - 5.75rem)); top:${top}%; transform:translate(-50%, ${placeBelow ? '0' : '-100%'});`;
}
</script> </script>
<div class="relative min-h-[260px] w-full overflow-hidden" dir="ltr"> <div class="relative min-h-[260px] w-full overflow-hidden" dir="ltr">
@@ -89,14 +99,21 @@
</svg> </svg>
{#if active !== null && positions[active]} {#if active !== null && positions[active]}
<div <div
class="pointer-events-none absolute z-10 w-44 -translate-x-1/2 rounded-md bg-zinc-950 p-3 text-right text-xs leading-6 text-white shadow-xl" class="pointer-events-none absolute z-10 w-44 transition-[left,top,transform] duration-150 ease-out motion-reduce:transition-none"
style={`left:${(positions[active].x / width) * 100}%; top:${Math.max(0, (positions[active].y / height) * 100 - 8)}%`} style={tooltipPosition(positions[active])}
dir="rtl" dir="rtl"
>
<div
class="rounded-md bg-zinc-950 p-3 text-right text-xs leading-6 text-white shadow-xl"
transition:scale={{ duration: 150, start: 0.96 }}
> >
<strong class="block">{persianDate(positions[active].localDate)}</strong> <strong class="block">{persianDate(positions[active].localDate)}</strong>
<span class="block text-white/75">{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span> <span class="block text-white/75"
>{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span
>
<span class="block text-white/75">{formatMoney(positions[active].bankPaidAmountRial)}</span> <span class="block text-white/75">{formatMoney(positions[active].bankPaidAmountRial)}</span>
</div> </div>
</div>
{/if} {/if}
{/if} {/if}
</div> </div>

View File

@@ -55,9 +55,8 @@
.filter((period) => period.periodType === periodTypeForRange(range) && period.isComplete) .filter((period) => period.periodType === periodTypeForRange(range) && period.isComplete)
.sort((left, right) => right.localStartAt.localeCompare(left.localStartAt)), .sort((left, right) => right.localStartAt.localeCompare(left.localStartAt)),
); );
const selectedPeriod = $derived( const explicitPeriod = $derived(matchingPeriods.find((period) => period.periodId === periodId));
matchingPeriods.find((period) => period.periodId === periodId) ?? matchingPeriods[0], const selectedPeriod = $derived(explicitPeriod ?? matchingPeriods[0]);
);
const calendarGrid = $derived( const calendarGrid = $derived(
!selectedYear || (range !== 'day' && range !== 'week') !selectedYear || (range !== 'day' && range !== 'week')
? [] ? []
@@ -168,8 +167,8 @@
<span>{range === 'month' ? 'ماه' : 'فصل'}</span> <span>{range === 'month' ? 'ماه' : 'فصل'}</span>
<Select.Root <Select.Root
type="single" type="single"
value={offered.some((period) => period.periodId === selectedPeriod?.periodId) value={explicitPeriod && offered.some((period) => period.periodId === explicitPeriod.periodId)
? selectedPeriod?.periodId ? explicitPeriod.periodId
: ''} : ''}
onValueChange={(value) => { onValueChange={(value) => {
if (namedOptions.some((option) => option.period?.periodId === value)) choose(value); if (namedOptions.some((option) => option.period?.periodId === value)) choose(value);
@@ -182,8 +181,8 @@
> >
<Select.Trigger class="min-h-11 w-full cursor-pointer rounded-md px-3 text-sm sm:min-h-9"> <Select.Trigger class="min-h-11 w-full cursor-pointer rounded-md px-3 text-sm sm:min-h-9">
<span data-slot="select-value"> <span data-slot="select-value">
{offered.find((period) => period.periodId === selectedPeriod?.periodId) {explicitPeriod && offered.find((period) => period.periodId === explicitPeriod.periodId)
? shortPeriodLabel(selectedPeriod) ? shortPeriodLabel(explicitPeriod)
: range === 'month' : range === 'month'
? 'انتخاب ماه' ? 'انتخاب ماه'
: 'انتخاب فصل'} : 'انتخاب فصل'}

View File

@@ -12,6 +12,6 @@
<div <div
bind:this={ref} bind:this={ref}
data-slot="skeleton" data-slot="skeleton"
class={cn('bg-muted animate-pulse rounded-md', className)} class={cn('bg-skeleton ring-border/70 animate-pulse rounded-md ring-1', className)}
{...restProps} {...restProps}
></div> ></div>