۰۳ — جهتگیری UI (Premium, Minimal)
مکمل 02-mvp-requirements.md — فقط لایه ظاهری/فرانت.
نام موقت پروژه: TakeEat — عنصر امضای بصری «مُهر» (seal) میتواند حتی با تغییر نام برند بماند.
۰. درس از رقبای دبی
تمایز بصری = گیفتباکس نه سطل تخفیف. Peekabox با برندهای پریمیوم (Carrefour، Pret، Krispy Kreme) این را ثابت کرده.
۰.۵ — TGTG benchmark (Too Good To Go)
مرجع UX برای لیست و جزئیات باکس. قبل/بعد هر تغییر UI موبایل (390×844) side-by-side با TGTG چک شود.
الگوهای TGTG که adopt کردیم
| بخش | TGTG | TakeEat (فعلی) |
|---|---|---|
| لیست | کارت compact، چند آیتم روی صفحه | horizontal card 96×96 + 3 خط info |
| جزئیات | full-screen page + back | /box/[id] full page + sticky back header |
| footer رزرو | price + CTA در یک row | stickyCompact + CTAButton md |
| hierarchy | نام → فاصله/زمان → قیمت | همان — بدون rating در لیست |
Target metrics
| Metric | هدف |
|---|---|
| کارت visible روی iPhone 390×844 | ≥ 2.5 |
| footer رزرو (step detail) | ≤ 72px |
| info layers per list card | 3–4 |
| detail view | full page (نه bottom sheet) |
چکلیست ۱۰ آیتمی (با TGTG)
- چند کارت بدون scroll دیده میشود؟
- scroll لیست روان است؟ (بدون nested scroll)
- tap → full page با back واضح؟
- footer رزرو جا نمیگیرد بیش از حد؟
- قیمت یک focal point در کارت؟
- urgency («آخرین») فقط یکبار؟
- shadow minimal — border-first؟
- header home یک band (نه دو)؟
- checkout step هنوز readable؟
- RTL و اعداد fa-IR درست؟
۱. چیزهایی که عمداً رد شد
| ❌ رد شده | دلیل |
|---|---|
| کرم + سریف + ترکوتا | کلیشه AI design |
| مشکی + نئون | کلیشه دوم |
| بج «٪ تخفیف» روی عکس | حس junk-food |
| پارالاکس، confetti، جشن روی هر اکشن | حس template |
| چند فونت/چند radius | بینظمی |
۲. Design tokens
رنگ
| Token | Hex | CSS var | نقش |
|---|---|---|---|
| Clay | #EFEAE1 | --color-clay | پسزمینه صفحات |
| Surface | #FBF9F5 | --color-surface | کارتها |
| Ink | #21241F | --color-ink | متن اصلی |
| Ink Muted | #6E6A60 | --color-ink-muted | زمان، فاصله |
| Saffron | #B5651D | --color-saffron | قیمت، CTA، مُهر |
| Firouzeh | #1F5C56 | --color-firouzeh | تایید vendor، success |
| Plum | #4A2740 | --color-plum | فقط «آخرین باکس» |
| Border | #DCD5C8 | — | بوردر ۰.۵px کارت |
| Device frame (mockup) | #2B2B29 | — | قاب موبایل در mockup |
قانون: حداکثر یک نقطه Saffron per صفحه. Firouzeh/Plum معنادار، نه تزئینی.
الهام: سفال/کاشی ایرانی — گلرنگ خاکی + زعفرانی + فیروزه کدر.
تایپوگرافی
| نقش | فونت | استفاده |
|---|---|---|
| Body/UI | Vazirmatn (variable) | همهجا |
| Display | Estedad (variable) | عنوان صفحه، قیمت بزرگ جزئیات — با احتیاط |
| Pickup code | Vazirmatn + font-feature-settings: "tnum" | اعداد tabular — نه mono جدا |
اعداد: پیشنهاد Western (0123) برای قیمت/زمان/کد — حس fintech مدرن. تصمیم برندی → 11-open-decisions.md.
Spacing / Radius / Shadow
| Token | مقدار |
|---|---|
| Base unit | 4px (Tailwind default) |
| Gap کارتها | 12–16px |
| Padding کارت | 14–16px |
| Radius کارت | 18px (یکسان همهجا) |
| Shadow | عملاً صفر — عمق با border #DCD5C8 |
| Shadow exception | modal / bottom sheet — خیلی نرم |
Motion
| لحظه | رفتار | مدت |
|---|---|---|
| باز کردن باکس (Container Transform) | کارت expand → کل صفحه | 500ms |
| بستن مودال | محتوا fade، container shrink به کارت | 500ms |
| تایید رزرو (مُهر) | دایره چرخان → seal مینشیند | ~600ms |
| بقیه | fade/slide | 150–200ms |
| Card press | scale(0.98) | 150ms |
| reduced-motion | prefers-reduced-motion: reduce | — |
ممنوع: پارالاکس، confetti، bounce زیاد، transition-* CSS روی element های layoutId.
۳.۵ — Container Transform Animation (پیادهسازی شده)
این امضای بصری اصلی اپ است — وقتی باکس tap میشود، خودِ کارت expand میشود و کل صفحه را میگیرد و تبدیل به صفحه محصول میشود.
معماری
(client)/layout.tsx
└── LayoutGroup ← framer-motion: همه layoutId ها در یک group
├── {children} ← صفحه خانه با لیست کارتها
└── AnimatePresence ← wraps @modal slot — بدون mode="wait"
└── @modal/(.)box/[id]/page.tsx ← intercepting route
└── BoxDetail isModal={true}قوانین پیادهسازی
- فقط یک
layoutIdروی container بیرونی در هر دوSurpriseBoxCardوBoxDetail - هیچ nested
layoutId— عکس، title، price، icon — هیچکدام layoutId نمیگیرند AnimatePresenceبدونmode="wait"— source و target باید لحظهای همزمان در DOM باشند- بدون CSS
transition-*روی element دارای layoutId (conflict با framer-motion) - Easing:
[0.32, 0.72, 0, 1]+duration: 0.5(iOS-style deceleration)
معماری scroll — حیاتی
layoutId element نباید scroll container باشد:
{/* Shape shell — layoutId، بدون overflow-y */}
<motion.div layoutId="box-{id}" className="fixed inset-0 z-50 bg-surface">
{/* Scroll container — از inset-0 ابعاد میگیرد، نه از height انیمیتشده parent */}
<div className="absolute inset-0 overflow-y-auto overscroll-contain bg-clay">
{/* محتوا بعد از expand ظاهر میشود */}
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ delay: 0.32, duration: 0.22 }}>
...
</motion.div>
</div>
</motion.div>اگر overflow-y-auto روی layoutId element باشد → height انیمیتشده Motion به scroll calculation نشت میکند → فضای سفید پایین صفحه.
عکس در حین انیمیشن
عکس هیچوقت width/height props تغییرپذیر نمیگیرد:
{/* ✅ */}
<div style={{ height: 260 }}>
<img style={{ width: "100%", height: "100%", objectFit: "cover" }} />
</div>
{/* ❌ */}
<img width={400} height={176} /> {/* این در کارت یه سایز داره، تو دیتیل سایز دیگه */}جریان انیمیشن
- مشتری کلیک میکند
- Next.js Intercepting Route صفحه را intercept میکند
BoxDetail isModalبا همانlayoutIdدر DOM ظاهر میشود- Framer Motion: کارت morph → کل صفحه (500ms، iOS easing)
- محتوا بعد از 320ms با fade ظاهر میشود
- URL به
/box/[id]تغییر میکند — دکمه back گوشی کار میکند - هنگام برگشت: محتوا fade out، container shrink به کارت
۳. عنصر امضا — «مُهر» (Seal)
SVG: دایره + نقطه/حلقه داخلی (مهر موم).
| محل | رفتار |
|---|---|
| Loading | چرخش آرام |
| Confirm reservation | مهر «مینشیند» |
| Pickup ticket | کد داخل مهر — بلیط مهرشده |
۴. کامپوننتها
| Component | Spec |
|---|---|
SurpriseBoxCard | عکس 16:9، radius 18px بالا؛ بدون بج درصد؛ قیمت Saffron + strikethrough؛ تگ «آخرین باکس» فقط if quantity_remaining <= 1 |
VendorBadge | نقطه Firouzeh 6px کنار نام — فقط approved + سابقه خوب |
PickupTicket | fullscreen؛ مهر بزرگ وسط؛ کد؛ آدرس؛ بازه؛ دکمه «مسیر را نشان بده» |
CTAButton | Saffron fill؛ «رزرو میکنم»؛ active scale 0.98 |
EmptyState | «هنوز باکسی ایندور و بر نیست. فردا همین ساعت دوباره سر بزن» |
BottomNav | ۳ آیتم: خانه، جستجو، پروفایل — active = Saffron |
Mockup sample data
| Vendor | فاصله | Pickup | قیمت | ارزش |
|---|---|---|---|---|
| کافه نیلوفر | 800 متر | 20:00–21:00 | 65,000 | |
| نان و کیک پرنیان | 1.2 km | 21:30–22:00 | 85,000 |
۵. لحن copy (microcopy)
| موقعیت | ❌ | ✅ |
|---|---|---|
| CTA | ثبت سفارش | رزرو میکنم |
| بعد پرداخت | تبریک! 🎉 | رزرو شد. مهرتون آمادهست |
| کممانده | عجله کنید! | آخرین باکس |
| لیست خالی | نتیجهای یافت نشد | هنوز باکسی ایندور و بر نیست… |
| خطای پرداخت | خطا!! | پرداخت انجام نشد. باکستون هنوز نگهداشته شده… |
Subtitle home: «باکسهای نزدیک شما»
۶. RTL / فارسی
dir="rtl"روی<html>- Tailwind v4.3.x — logical properties:
ps-*,pe-*,ms-*,me-*,inset-s-*— نهleft/right - یک فونت برای فارسی + لاتin
- آیکن: Lucide/Tabler — فقط directional ها
rtl:rotate-180
۷. استک فرانت
| لایه | انتخاب | نسخه |
|---|---|---|
| Framework | Next.js App Router | 16.2.x (Jun 2026 stable) |
| React | — | 19 |
| Bundler | Turbopack | default |
| Node | — | 20+ |
| CSS | Tailwind | v4.3.x — @theme در globals.css |
| Components | shadcn/ui | theme سفارشی (نه zinc) |
| Motion | Framer Motion | مُهر + card press |
| Icons | Lucide یا Tabler | |
| PWA | manifest + SW | add-to-homescreen |
create-next-app
npx create-next-app@latest TakeEat-client --typescript --tailwind --eslint --app --src-dir --turbopackglobals.css @theme
@import "tailwindcss";
@theme {
--color-clay: #EFEAE1;
--color-surface: #FBF9F5;
--color-ink: #21241F;
--color-ink-muted: #6E6A60;
--color-saffron: #B5651D;
--color-firouzeh: #1F5C56;
--color-plum: #4A2740;
--font-body: "Vazirmatn", sans-serif;
--font-display: "Estedad", sans-serif;
}next/font
import { Vazirmatn, Estedad } from "next/font/google";
const vazirmatn = Vazirmatn({ subsets: ["arabic"], variable: "--font-body" });
const estedad = Estedad({ subsets: ["arabic"], variable: "--font-display" });۸. Quality checklist (قبل از demo)
- [ ] تست روی موبایل واقعی
- [ ] focus state روی input/button
- [ ]
prefers-reduced-motionبرای مُهر - [ ] محتوای فارسی واقعی (نه Lorem)
- [ ] WCAG contrast: Ink/Clay، Saffron/Surface