fix: polish dashboard loading and interactions
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,13 +82,15 @@
|
|||||||
onselect={applyPeriod}
|
onselect={applyPeriod}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<PeriodPicker
|
<div class="border-border border-s ps-5">
|
||||||
periods={dashboard.periods}
|
<PeriodPicker
|
||||||
range={pickerRange}
|
periods={dashboard.periods}
|
||||||
periodId={pickerRange === filters.range ? filters.period : ''}
|
range={pickerRange}
|
||||||
bind:open={desktopPickerOpen}
|
periodId={pickerRange === filters.range ? filters.period : ''}
|
||||||
onselect={applyPeriod}
|
bind:open={desktopPickerOpen}
|
||||||
/>
|
onselect={applyPeriod}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -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,13 +99,20 @@
|
|||||||
</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"
|
||||||
>
|
>
|
||||||
<strong class="block">{persianDate(positions[active].localDate)}</strong>
|
<div
|
||||||
<span class="block text-white/75">{formatValue(positions[active].bankPaidSessions)} پرداخت موفق</span>
|
class="rounded-md bg-zinc-950 p-3 text-right text-xs leading-6 text-white shadow-xl"
|
||||||
<span class="block text-white/75">{formatMoney(positions[active].bankPaidAmountRial)}</span>
|
transition:scale={{ duration: 150, start: 0.96 }}
|
||||||
|
>
|
||||||
|
<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">{formatMoney(positions[active].bankPaidAmountRial)}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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'
|
||||||
? 'انتخاب ماه'
|
? 'انتخاب ماه'
|
||||||
: 'انتخاب فصل'}
|
: 'انتخاب فصل'}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user