# Design System — ZarinPal Merchant Analytics - Status: Approved baseline — implementation not yet authorized - Version: 0.3.3 - Last updated: 2026-08-20 - Direction: Persian-first RTL ## Design intent Match the recognizable product language of ZarinPal without copying inaccessible proprietary assets: calm financial confidence, generous whitespace, crisp white surfaces, strong blue actions, restrained yellow accents, dark ink sections, rounded geometry, and dense data presented in plain language. The authenticated panel screenshots are the primary behavioral reference: fixed right navigation on desktop, right-side drawer on narrow screens, light gray workspace, white cards, compact outlined controls, blue active states, and direct numerical summaries. The public-site screenshots inform larger radius, dark promotional surfaces, and spacing—but the dashboard prioritizes speed and legibility over marketing spectacle. ## Foundations ### Color tokens | Token | Value | Use | |---|---:|---| | `--color-brand-600` | `#0A33FF` | Primary actions, active navigation, links, selected chart series | | `--color-brand-700` | `#0828CC` | Hover/pressed brand state | | `--color-accent-400` | `#FFD400` | Small ZarinPal-like accent, highlights; never body text on white | | `--color-ink-950` | `#19191A` | Primary text, dark panels | | `--color-ink-700` | `#3D3D42` | Secondary strong text | | `--color-ink-500` | `#6B6B73` | Supporting text | | `--color-canvas` | `#F1F1F3` | Application background | | `--color-surface` | `#FFFFFF` | Cards, drawer, dialogs | | `--color-surface-muted` | `#F7F7F8` | Nested panels, table headers | | `--color-border` | `#E3E3E7` | Dividers and neutral outlines | | `--color-success-600` | `#248061` | Success states and positive semantic data | | `--color-warning-600` | `#A86700` | Warnings; paired with text/icon, never color alone | | `--color-danger-500` | `#FF5C5D` | Errors and destructive actions | | `--color-info-surface` | `#E7F2FF` | Informational callouts | All text/background pairs must meet WCAG 2.2 AA contrast. Chart palettes require patterns, labels, or shapes in addition to hue. ### Theme contract Light and dark modes are first-class variants of the same semantic token system. Components consume role tokens rather than raw palette values; dark mode is not a color inversion or an after-launch skin. | Semantic role | Light | Dark | Notes | |---|---:|---:|---| | `--color-canvas` | `#F1F1F3` | `#0F0F10` | Application workspace | | `--color-surface` | `#FFFFFF` | `#19191A` | Cards, drawers, dialogs | | `--color-surface-muted` | `#F7F7F8` | `#28282A` | Nested panels and table headers | | `--color-text-primary` | `#19191A` | `#FFFFFF` | Primary content | | `--color-text-secondary` | `#3D3D42` | `#D1D1D6` | Supporting content | | `--color-text-muted` | `#6B6B73` | `#A7A7AF` | Metadata; contrast must be verified at actual size | | `--color-border` | `#E3E3E7` | `#3A3A3D` | Dividers and neutral outlines | | `--color-brand-action` | `#0A33FF` | `#90AEF4` | Primary action and active state | | `--color-on-brand` | `#FFFFFF` | `#0A0A0A` | Content on brand action | | `--color-focus-ring` | `#0A33FF` | `#90AEF4` | Keyboard focus | | `--color-semantic-success` | `#248061` | `#6FD6AE` | Positive values with text/icon support | | `--color-semantic-warning` | `#8A5300` | `#F3BE5B` | Caution and incomplete verification | | `--color-semantic-danger` | `#C93637` | `#FF8A8B` | Error and destructive meaning | | `--color-semantic-info` | `#0A33FF` | `#90AEF4` | Informational emphasis | - The initial theme follows `prefers-color-scheme` unless the user has explicitly selected light or dark. The explicit choice is persisted locally because it is presentation-only and non-sensitive. - Theme selection is applied before first paint to avoid a light/dark flash. The toggle remains keyboard and screen-reader operable and exposes its current state. - Semantic success, warning, danger, information, chart, hover, pressed, disabled, skeleton, scrim, and elevation tokens have separately tested light/dark values before delivery. - Every data series maintains at least 3:1 non-text contrast against its chart surface. Text maintains WCAG 2.2 AA, and status/trend meaning never depends on color alone. ### Typography - Required Persian UI family: licensed **Iran Yekan X Professional** (`IRANYekanX`), matching the current ZarinPal panel's `IRANYekanXVFaNumVF` variable font. - Required asset: the Persian-number (`FaNum`) variable webfont in WOFF2, covering the `wght` axis from 100–1000. The product uses 400 for body/value text, 500 for labels and controls, 600 for emphasis, and 700 for section/page headings. No separate static weights are required when the variable file is supplied. - The purchased font is a Git-tracked project asset. Keep `static/fonts/IRANYekanXVFaNumVF.woff2` and its required adjacent `static/fonts/FontLicense.txt` in the frontend repository so local, CI, and Docker builds use the same reviewed bytes without downloading from ZarinPal or another CDN. - The tracked font is the only approved primary font asset for implementation. Do not replace, rename, subset, regenerate, or add other files from the purchased package without updating this specification and confirming license compliance. - Runtime fallback while the licensed asset is unavailable: `Tahoma`, system sans-serif. The fallback is for resilience, not the approved visual baseline. - Evidence: inspection of the current [ZarinPal panel](https://next.zarinpal.com/beta/panel) found `IRANYekanXVFaNumVF` declared for weights 100–1000. The licensed [Iran Yekan X product page](https://fontiran.com/fonts/iranyekan) identifies the professional package as the option containing all weights, the variable font, Persian-number assets, and WOFF/WOFF2 webfonts. - Use tabular numerals for tables/KPIs where supported. - Body: 14–16 px, line-height 1.7–1.9 for Persian. - Labels/meta: 12–14 px; never below 12 px. - Section heading: 20–24 px, weight 700. - Page heading: 28–32 px desktop, 24–28 px mobile, weight 700–800. - Avoid ultralight weights and excessive bolding. Keep Persian punctuation and half-spaces correct. ### Spacing, shape, elevation - Base spacing unit: 4 px; common sequence `4, 8, 12, 16, 24, 32, 48, 64`. - Control height: 40 px desktop; interactive touch target at least 44×44 px on touch layouts. - Card radius: 12 px for dense dashboard cards; 16–20 px for feature/empty-state surfaces. - Button/input radius: 8–10 px; pill radius only for compact statuses and segmented controls. - Card border is preferred over shadow. Default elevation is subtle (`0 1px 3px rgb(25 25 26 / 0.08)`); overlays may use stronger elevation. - Content width is bounded for scanning; tables/charts may use the full available dashboard column. ## Layout and responsiveness ### Desktop (≥ 1024 px) - Fixed right sidebar, approximately 248–272 px wide. - Top utility bar remains visually quiet. - Main content uses a centered, fluid container with 24–32 px gutters. - Overview uses a 12-column grid. Primary analytics get 8 columns; secondary summaries get 4 where useful. - Do not create large empty regions merely to mimic the reference screenshot; use the density required by real content. ### Tablet (768–1023 px) - Sidebar becomes an overlay drawer from the right. - Two-column cards collapse according to content, not arbitrary device labels. - Tables may scroll horizontally only after priority columns and responsive alternatives are considered. ### Mobile (< 768 px) - Single-column content, 16 px outer gutter. - Right-side navigation drawer traps focus, closes with Escape/backdrop, restores focus to its trigger, and locks background scroll. - KPI pairs stack; charts remain at least 240 px tall and expose an accessible data table/summary. - Sticky actions must not cover content or browser safe areas. ## Navigation and information architecture Initial authenticated routes: 1. ورود (login) 2. پیشخوان (overview) 3. عملکرد پرداخت (success, retries, failures, timing) 4. کارمزد و درآمد (real merchant fee amounts and rates in Rial) 5. مقایسه با هم‌گروه‌ها (anonymized category/cohort benchmark) 6. کیفیت داده / درباره شاخص‌ها (definitions, freshness, limitations) 7. حساب کاربری / خروج Navigation labels should describe merchant tasks, not internal database concepts. Active location is conveyed by color plus a bar/background and `aria-current`. ## Core component contracts - **App shell:** sidebar/drawer, utility header, page title, merchant identity, analytics progress/status. First access idempotently starts a run, then opens its authenticated SSE stream; the page reveals overview, payment performance, fees, and peer comparison independently as each becomes ready. - **Theme control:** accessible light/dark toggle in the utility area, initialized from the persisted choice or system preference and applied without a first-paint flash. - **Metric card:** label, formatted value/unit, comparison and period, semantic trend, definition link, independent queued/calculating/ready/failed states. - **Chart card:** title, period/filter controls, concise takeaway, plot, legend, accessible tabular alternative, provenance link, and section-local loading/error behavior. - **Insight card:** deterministic finding, numeric evidence, suggested merchant action, confidence/limitation; AI-generated copy is visibly labeled later. - **Data table:** semantic headers, server pagination/sorting, row actions named for screen readers, responsive priority, empty/error states. - **Analytics export:** eligible merchant-owned detail views expose an explicit CSV export action that applies the current approved filters and clearly states period, unit, generation state, and failure/retry behavior. Aggregate-only cards and peer data are not exported unless separately specified. - **Filter bar:** URL-backed state, explicit apply/reset behavior, and approved daily, weekly, Jalali-month, and Persian-season periods with unambiguous comparison baselines. - **Status badge:** text plus icon/shape; color reinforces but never carries meaning alone. - **Feedback:** inline field errors, page-level error boundary, skeleton only when layout is known, retry action for recoverable failures. - **Rate-limited state:** explain when an analytics start, stream, or manual retry is temporarily limited; preserve ready content, honor the server `Retry-After`, and never run a client-side retry loop. ## Data visualization - Lead with a written takeaway and exact KPI; charts support the conclusion rather than decorate it. - Default comparisons: current period vs previous equivalent period and merchant vs eligible merchants in the same category and selected period. Peer cards show the merchant-weighted cohort median and the active merchant's performance percentile; each peer contributes one value regardless of transaction volume. A higher displayed percentile always means better, so processing-time and fee-rate ranks are direction-adjusted. Peer values appear only when at least 10 other merchants and 1,000 combined peer sessions contribute; otherwise show a clear insufficient-data state. - Always show unit, timezone/calendar, exact reporting period, comparison baseline, sample size, freshness, and missing-data caveat where material. - Success/failure colors remain semantic and consistent. Never use a rainbow categorical palette for ordered values. - Tooltips are keyboard reachable and not the sole location of important values. - Charts have a textual summary and downloadable/visible data table when practical. - Zero, unavailable, suppressed, and not-yet-calculated are visually and linguistically distinct. - A deterministic insight card appears only with at least 100 relevant sessions and a qualifying change: at least 3 percentage points for rates or 20 percent for amounts/counts. Smaller comparisons remain visible in metrics without being promoted into an action claim. - A ready section appears immediately without layout-shifting or blocking on unfinished siblings. Overall progress remains understandable without relying on animation. - Source statuses `Paid` and `Verified` both count as successful bank payments because funds were deducted. Keep them visually distinguishable: `Paid` means merchant verification is incomplete and is an actionable gap; `Verified` is finalized. - Present `fee_amount_rial` as the merchant's real fee, summed once per session. Never multiply it by retry attempts or label it as obfuscated/relative-only in the product. - First load selects the latest complete Jalali month containing data and compares only with the immediately previous equivalent period. An absent baseline has an explicit unavailable state. - Overview leads with bank-paid amount, bank-paid session count, success rate, and verification completion. Retry, timing, and fee modules follow in that order of detail. - Drill-down tables for unverified-paid, failed, and retried sessions are read-only. They never reveal payer tokens or peer rows; `NoAttempt` and `InBank` are stage labels, not invented failure causes. ## Motion - Functional motion only: drawer/dialog transitions, focus/selection feedback, and short chart/filter state changes. - Typical duration 120–220 ms; enter may be slightly slower than exit. - Prefer opacity and transform. Avoid layout-heavy animation and dashboard-wide scroll reveals. - Honor `prefers-reduced-motion` by removing nonessential movement. - No third-party scroll-reveal library is planned for the authenticated panel. Reconsider only for a separately scoped marketing surface. ## Persian, direction, and formatting - Root UI is `lang="fa" dir="rtl"`. - Technical identifiers, URLs, hashes, and mixed Latin values use isolated LTR spans (`dir="ltr"`) to prevent bidi corruption. - Merchant-facing numbers, percentages, chart axes, counts, dates, and monetary values use Persian digits through locale-aware formatting (`fa-IR`). Technical identifiers and machine-facing values retain their canonical Latin representation inside isolated LTR spans. - Display money in **Rial only**, with an explicit `ریال` unit. Do not silently convert to Toman or mix the two units. - Use `Intl.NumberFormat`/`Intl.DateTimeFormat`. Display merchant-facing dates in the Jalali calendar and `Asia/Tehran`; API instants remain ISO-8601 UTC. - Do not concatenate translated fragments. Write complete Persian strings with correct نیم‌فاصله. - Authentication errors must not reveal whether a username exists. ## Accessibility and interaction - WCAG 2.2 AA target. - Visible `:focus-visible` ring on every interactive element. - Semantic buttons/links/forms before ARIA; icons have accessible names or are hidden when decorative. - All functionality is keyboard operable. Dialogs/drawers manage focus correctly. - Validation is associated with fields and announced; asynchronous updates use restrained live regions. - Loading indicators expose status text; avoid endless skeletons. - Destructive or consequential actions require clear confirmation and recovery where possible. ## Current decisions and change log | Version | Date | Decision | |---|---|---| | 0.3.3 | 2026-08-20 | Replaced merchant/cohort revision freshness language with the approved immutable completed-import dataset snapshot. This changes trace labels only; cache age still does not imply stale data. | | 0.3.2 | 2026-08-20 | Made the licensed Iran Yekan X FaNum variable WOFF2 and its required license file Git-tracked frontend assets, eliminating a separate private build-injection path. | | 0.3.1 | 2026-08-20 | Verified the purchased Iran Yekan X Professional FaNum variable assets and required a private, reproducible injection path for public repositories. Added merchant-owned detail CSV export as an MVP component contract. | | 0.3.0 | 2026-08-20 | Approved Iran Yekan X Professional FaNum variable typography, Persian digits, Rial-only values, and first-class light/dark semantic themes. Added the deterministic insight eligibility threshold. | | 0.2.4 | 2026-08-20 | Fixed the static-demo default period, comparison fallback, overview KPI priority, and safe read-only drill-down scope; added accurate `NoAttempt`/`InBank` stage language from the CTO data guide. | | 0.2.3 | 2026-08-20 | Kept authentication UX intentionally demo-sized: generic and throttled username/password login now, with a clean replacement boundary for ZarinPal SSO after acceptance. | | 0.2.2 | 2026-08-20 | Defined backend-owned session UX: authentication tokens remain inaccessible to the frontend, and expiry/revocation returns the user to login with a generic, accessible message. | | 0.2.1 | 2026-08-20 | Defined same-category peer cards as an equal-per-merchant median plus direction-aware percentile. This prevents the dataset's largest merchants from dominating what should represent a typical peer. | | 0.2.0 | 2026-08-20 | Corrected payment lifecycle semantics from CTO guidance: `Paid` and `Verified` are bank-payment successes, `Paid` remains unverified, and session-level fees are real Rial amounts. | | 0.1.9 | 2026-08-20 | Added rate-limit UX, 15-second SSE heartbeat/recovery, one-hour replay, and failed-section-only manual retry. | | 0.1.8 | 2026-08-20 | Fixed the four parallel section ids, 30-day cache, idempotent start/SSE flow, and section timeout/retry behavior. | | 0.1.7 | 2026-08-20 | Moved analytics results to high-TTL Redis and defined partial, parallel section delivery through authenticated SSE. | | 0.1.6 | 2026-08-20 | Replaced time-based analytics freshness with lazy first-access calculation and revision-based merchant/cohort cache states; defined Saturday–Friday weeks. | | 0.1.5 | 2026-08-20 | Removed holiday/event reporting; limited period controls to daily, weekly, Jalali-month, and Persian-season views. | | 0.1.2 | 2026-08-20 | Added the insufficient-data state and visibility threshold for anonymized peer comparisons. | | 0.1.1 | 2026-08-20 | Fixed merchant-facing time presentation to the Jalali calendar in `Asia/Tehran`, with explicit UTC API instants. | | 0.1.0 | 2026-08-20 | Established ZarinPal-inspired RTL foundation from supplied public/panel screenshots; fixed SvelteKit/Svelte/TypeScript/Tailwind stack; defined responsive shell, core components, accessibility, chart, and motion rules. | Any material token/component/interaction change increments this version and adds a row describing why it changed. A surprising or difficult-to-reverse change also receives an ADR. ## Open design decisions before implementation planning None. The approved licensed font and its license file are already tracked at the specified frontend paths.