# Portfolio Site

`06 · portfolio-site · Open source`

الموقع الذي تقرأه. React · Vite · TypeScript مع CSS قائم على tokens، محتوى EN/RU/AR typed، مرايا Markdown، telemetry تراعي الخصوصية، وnavigation واعية بالroute.

**النطاق:** Solo · مستمر  
**الدور:** Engineering portfolio · 2025–2026

---

## السياق

> ست حالات لا يستطيع recruiter فتحها. وموقع واحد يستطيع.

من أصل ستة مشاريع معروضة، خمسة خلف نطاق مغلق أو NDA — client work، internal tooling، وR&D. لا يستطيع recruiter فتحها أو diff history. الموقع نفسه هو artifact العام الوحيد الذي يستطيع reviewer تدقيقه فورا: DevTools، network، CSS، view-source. كل تفصيل يراه المستخدم هو أيضا قرار يراه المهندس.

القيد مزدوج: يجب أن يقرأه recruiter خلال خمس ثوان — cards ثابتة الشكل، spec-sheets mono، ASCII diagrams. ويجب أن يصمد أمام senior eye بطيئة — token-first design، i18n مخصص، view transitions، وHero ember field لا يسخن iPhones.

## حقائق

| | |
|---|---|
| **النطاق** | Solo · مستمر |
| **الأسطح** | Home + 6 cases · EN/RU/AR · dark/light · 3 palettes |
| **المصدر** | مستودع GitHub عام · CV PDF · أزرار copy-as-Markdown |
| **التقنيات** | React 18 · Vite · TypeScript · CSS variables |
| **المحتوى** | Typed dictionaries · public/private content trees · مرايا Markdown مولدة |
| **القياس** | أحداث دون ملفات تعريف ارتباط إلى same-origin · dashboard للمدير فقط |
| **الحركة** | View transitions · يحترم prefers-reduced-motion |
| **الحالة** | Open source · live |

## المعمارية

### شجرة providers + routing

```text
<ThemeProvider>           // src/theme/ThemeContext.tsx
  <LangProvider>          // src/i18n/LangContext.tsx
    <BrowserRouter>
      <Loader />
      <ScrollToHash />
      <AnalyticsRouteTracker />
      <Routes>
        <Route "/"            → <HomePage />
        <Route "/cases/:slug" → <CaseRoute /> → <ProjectDetailPage />
        <Route "*"            → <HomePage />
      </Routes>
    </BrowserRouter>
  </LangProvider>
</ThemeProvider>
```

**ترتيب providers مهم.** ThemeProvider في الخارج حتى تكتب palette على `<html data-palette>` قبل أن يقرأ أي child متغيرات اللون. LangProvider بعده حتى يكون `useT()` متاحا في كل مكان. Loader وscroll restoration وanalytics داخل router لأنها تحتاج routed content أو `useLocation()`.

**مساران وظيفيان + fallback.** `/` يعرض home، و`/cases/:slug` يعرض case study، و`*` يعود إلى home للروابط غير المعروفة. slug whitelist يعيش في `src/config/cases.ts` وتستهلكه Nav وProjectDetailPage؛ server analytics لديه allowlists خاصة به.

**لا code-splitting بعد.** المساران يشحنان في bundle واحد. Lighthouse ما زال أخضر؛ يعاد النظر عندما يظهر route ثالث.

### سلسلة حلّ tokens

```text
 1  ThemeContext        theme = 'dark'  →  palette = 'ochre'
        │
        ▼  writes data-attrs on <html>
 2  <html data-theme="dark" data-palette="ochre">
        │
        ▼  tokens.css matches via attribute selectors
 3  [data-theme="dark"]                       → --bg --fg --line
    [data-palette="ochre"][data-theme="dark"] → --accent --accent-soft
        │
        ▼  CSS modules imported by styles.css consume via var()
 4  .hero h1 { color: var(--fg) }
    .chip    { background: color-mix(in oklab, var(--bg) 10%, …) }
```

**Theme يحدد palette.** الاشتقاق deterministic: dark → ochre وlight → electric. كلا `data-theme` و`data-palette` يكتبان على `<html>`. CSS يبدل عبر attribute selectors؛ صفر branching في JS لكل عنصر.

**Token file + CSS modules، بلا Tailwind.** `tokens.css` هو مصدر الحقيقة للألوان والمسافات والخطوط والزوايا. `styles.css` مجرد entry يستورد base وhome وmedia وcase-study وruntime. نظام tokens يقوم بدور Tailwind config، والselectors تقوم بدور utility classes.

**oklch + color-mix للخلفيات.** استخدام كثيف لـ `color-mix(in oklab, var(--bg) X%, transparent)` للطبقات الملوّنة — hero chips، case-study glows، diagram backdrops. يتجنب opacity hacks على اللون الخطأ.

### Loader + مراحل دخول الواجهة

```text
T=0          T=400 ms                       T=900 ms
│            │                              │
▼            ▼                              ▼
pulse    →   rush                       →   gone
hairline     two whiteish blurred           loader unmounted
opacity      pulses run from 15% / 85%      hero/about reveal
0.22-0.7     toward center (600 ms)         (staggered)

at T+400 ms of rush:
  html.is-loading removed (was set synchronously by inline <script> in index.html)
    → hero-fx + about-body cascade through enter-fade / enter-up keyframes
      (stagger T=0..1550 ms across hero / row-top / about / contact)
    → loader opacity fades 1→0 between T=400-900 ms
    → pulses arrive at center while loader is already mid-fade

prefers-reduced-motion: rush skipped, embers off, staggered entrances off
```

**is-loading متزامن.** `index.html` يحتوي script صغير يضع `html.is-loading` قبل تحميل React. عناصر hero/about تكون `opacity: 0` أثناء وجوده. النتيجة صفر FOUC بلا flash من `useEffect`.

**Hairline مطابق للdivider الحقيقي.** Loader يقيس `#home.getBoundingClientRect().bottom` ويكتب `--loader-line-y` حتى يطابق موضعه divider الحقيقي بين Hero وAbout. عند fade ينتقل الخط الأبيض بصريا إلى divider الموجود في نفس Y.

**الضوء يبدأ أثناء rush، بلا flash.** نسخة سابقة كان فيها flash مركزي. الآن `is-loading` يزال عند T+400ms، يبدأ دخول hero، ويتلاشى loader بين T=400–900ms. `prefers-reduced-motion` يتجاوز rush بالكامل.

## قرارات هندسية رئيسية

### 01 · Tokens لا Tailwind — token file + split CSS modules

**القرار.** tokens.css يعرف colors/spacing/type/radii عبر custom properties؛ selectors في base/home/media/case-study/runtime. لا utility framework ولا CSS-in-JS.

**لماذا.** موقع ذو design language صارم يتلوث بضوضاء utility classes. tokens تفعل ما يفعله Tailwind config، والselectors تبقى grep-friendly.

**الكلفة.** أي heading size جديد يعني token جديد. split يزيد الملفات ويتطلب discipline في import order.

### 02 · Content trees متوازية لا key store

**القرار.** useT() يعيد Content object كامل لكل لغة. public tree committed و.private real tree؛ select-content يكتب active.ts.

**لماذا.** لهذا الحجم، t.hero.name أوضح من flat keys. TypeScript يعطي parity وautocomplete.

**الكلفة.** إضافة field تكسر كل اللغات حتى تملأها؛ plurals/interpolation تحتاج hand-roll.

### 03 · توأم Markdown لكل صفحة، لا HTML scraping

**القرار.** كل صفحة لها sibling .md، مع llms.txt وllms-full.txt؛ نفس serializers تغذي copy-as-Markdown.

**لماذا.** AI/search agents يقتبسون Markdown أنظف من React HTML، والمشاريع الخاصة تحتاج artifact نصي ثابت.

**الكلفة.** الملفات المولدة build artifacts ويجب إبقاء serializers متزامنة.

### 04 · Hero embers — CSS particles لا SVG filter + SMIL

**القرار.** 26 spans تتحرك بـ transform/opacity فقط مع box-shadow glow.

**لماذا.** SVG filters المتحركة على iOS Safari CPU-bound وتسخن الهاتف؛ CSS transform stays cool.

**الكلفة.** فقدان warble طفيف.

### 05 · ASCII diagrams في <pre> لا Mermaid

**القرار.** الرسوم template literals في content وتعرض داخل pre؛ image diagrams variant أولي أيضا.

**لماذا.** 200KB JS لرسوم قليلة لا يستحق. ASCII mono يقرأ كartifact هندسي واضح.

**الكلفة.** hand-drafted ولا zoom تفاعلي.

### 06 · Telemetry privacy-first لا public analytics theater

**القرار.** events صغيرة إلى same-origin /api/track عبر sendBeacon، visitor hash daily-salted، dashboard admin-only.

**لماذا.** public counters على portfolio قليل الزيارات reverse signal. proof الحقيقي هو design قابل للتدقيق.

**الكلفة.** server surface وتشغيل SQLite/backups/salt rotation.

### 07 · Video provider sticky + single-player rule

**القرار.** Mirror toggle global sticky في localStorage، ومخزن single current player يمنع صوتين معا، فوق LiteYouTube facade.

**لماذا.** اختيار provider تفضيل site-wide، وsingle-player يمنع autoplay bursts عند provider swap.

**الكلفة.** لا cross-tab storage sync وRuTube لا يحفظ position reliably.

### 08 · Public/private content trees عبر generated barrel

**القرار.** public sanitized و.private real يشاركان Content type؛ scripts explicit تكتب active.ts، وCI يمنع leakage.

**لماذا.** inspectable public repo بدون تسريب private copy. barrel يمنع runtime branching وrepo drift.

**الكلفة.** fresh clone يحتاج script قبل active.ts؛ private tree يجب أن يوازي public shape.

## التقنيات

| | |
|---|---|
| **Frontend** | React 18 · Vite · TypeScript · react-router-dom |
| **Styles** | CSS custom properties · tokens.css + 5 domain CSS modules · oklch palette · بلا Tailwind/UI library |
| **Content** | Custom i18n context · typed dictionaries · مرايا Markdown مولدة · بلا react-i18next |
| **Motion/video** | View transitions API · CSS @keyframes · LiteYouTube facade · sticky provider toggle · single-player rule |
| **Telemetry** | Same-origin /api/track · route-aware dwell · daily-salted visitor hash · admin-only dashboard |
| **Scale** | ~8.5K LOC TS/TSX incl. content · ~2.5K LOC CSS · ~100 TS modules · 7 CSS files · 6 cases · 3 languages |

## الدروس والحالة

### ما سأحمله للأمام

- Token-first discipline — heading size جديد يعني token جديد، لا clamp inline.
- Content trees public/private عبر active.ts — paragraphs تبقى في سياقها وTS يمسك missing keys.
- is-loading class synchronous في index.html — صفر FOUC.
- CSS transform + opacity لأي ambient animation — درس iOS Safari من embers.
- تقسيم styles.css إلى modules domain بعد استقرار الصفحة — debug أسهل وVite يجمعها production.
- مرايا Markdown كagent layer أولي — نفس typed content يخدم البشر والوكلاء.

### ما سأغيره

- react-router-dom لمسارين فقط overkill صادق؛ لكن ScrollToHash يحتاج useLocation والتكلفة صغيرة.
- image-diagram variant أضيف بعد ASCII convention؛ يحتاج guard/lint للـ imageCols ratios.
- frontend slug registry في config، لكن server لديه قوائم موازية؛ cleanup التالي shared JSON.

Open source · live · ongoing. مصدر GitHub عام، مرايا Markdown مولدة، dictionaries typed بثلاث لغات، telemetry privacy-first، وثيمات dark/light.

---

المصدر: https://ilyadev.xyz/cases/portfolio-site (HTML) · /cases/portfolio-site.ar.md (هذا الملف)
السابق: 05 — macOS VPN · توجيه لكل تطبيق → https://ilyadev.xyz/cases/macos-vpn.ar.md
الفهرس: https://ilyadev.xyz/llms-ar.txt — قائمة دراسات الحالة الكاملة
المؤلف: إليا كازانتسيف — https://ilyadev.xyz/index.ar.md
