From 9e2089e853a26402a92fe733c8e4cc8b0f4b75f3 Mon Sep 17 00:00:00 2001 From: "reza.g" Date: Thu, 3 Sep 2026 19:26:40 +0330 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20mobile-responsive=20UI=20?= =?UTF-8?q?=E2=80=94=20bottom=20tab=20bar,=20bottom-sheet=20dialogs,=20PWA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All changes are gated behind max-md:/md:hidden so the >=768px desktop layout is untouched (verified with lint + build + live checks at 375px). - globals.css: one mobile block — 16px inputs (no iOS auto-zoom), units sidebar stacks above content, wizard item rows become 2-col (text fields full-width, delete button full-width), dd-panels / flow diagram / conv-form stack, larger touch targets - ui/dialog.tsx: dialogs become full-width bottom sheets on mobile (all modals inherit it); desktop keeps the centered dialog - Header: sticky on mobile only + PageLayout bottom clearance - New MobileTabBar: role-aware bottom tabs (office admin/manager, inventory PM, inventory admin); hidden on /wizard - Wizard step 4: TouchSensor (150ms delay) so touch drag works without stealing scroll - JalaliDatePicker: dropdown capped to viewport; bigger day cells and time inputs on mobile - PWA: manifest.webmanifest + 192/512 icons (scripts/gen-pwa-icons.ps1) + theme-color/manifest links in index.html - New docs/mobile-responsive.md documenting the full change set --- docs/mobile-responsive.md | 108 ++++++++++++++++++ frontend/index.html | 4 + frontend/public/icons/icon-192.png | Bin 0 -> 2744 bytes frontend/public/icons/icon-512.png | Bin 0 -> 8272 bytes frontend/public/manifest.webmanifest | 33 ++++++ frontend/scripts/gen-pwa-icons.ps1 | 58 ++++++++++ frontend/src/App.tsx | 12 +- frontend/src/components/JalaliDatePicker.tsx | 8 +- frontend/src/components/layout/Header.tsx | 6 +- .../src/components/layout/MobileTabBar.tsx | 100 ++++++++++++++++ frontend/src/components/ui/dialog.tsx | 4 +- frontend/src/pages/Dashboard/EditModal.tsx | 4 +- frontend/src/pages/Suppliers/CategoryPage.tsx | 2 +- .../src/pages/Wizard/Step1TopicSupplier.tsx | 2 +- frontend/src/pages/Wizard/Step4Projects.tsx | 9 +- frontend/src/pages/Wizard/Step5Review.tsx | 2 +- frontend/src/styles/globals.css | 93 +++++++++++++++ 17 files changed, 427 insertions(+), 18 deletions(-) create mode 100644 docs/mobile-responsive.md create mode 100644 frontend/public/icons/icon-192.png create mode 100644 frontend/public/icons/icon-512.png create mode 100644 frontend/public/manifest.webmanifest create mode 100644 frontend/scripts/gen-pwa-icons.ps1 create mode 100644 frontend/src/components/layout/MobileTabBar.tsx diff --git a/docs/mobile-responsive.md b/docs/mobile-responsive.md new file mode 100644 index 0000000..d2b46c7 --- /dev/null +++ b/docs/mobile-responsive.md @@ -0,0 +1,108 @@ +# Mobile-Responsive UI — Change Log (v1.4) + +هدف: تجربه‌ی کاربری قابل‌استفاده برای کاربران موبایل (اکثریت کاربران)، بدون اینکه +ظاهر دسکتاپ هیچ تغییری بکند. هیچ بک‌اند، nginx، docker-compose یا ساختار جدیدی +اضافه نشده — همه‌چیز داخل همان اپ React است. + +## Core principle + +یک کدبیس، دو نمایش. همه‌ی تغییرات «فقط موبایل» هستند و پشت بریک‌پوینت +Tailwind `max-md:` (زیر 768px) یا `md:hidden` (فقط روی موبایل نمایش داده می‌شود) +قفل شده‌اند. دسکتاپ (≥768px) همان استایل‌های قبلی را می‌خواند و بنابراین +پیکسل‌به‌پیکسل بدون تغییر می‌ماند — به‌ساختار. + +## What changed + +### 1. `frontend/src/styles/globals.css` — consolidated mobile block (new) +یک بلوک `@media (max-width: 767.98px)` در انتهای فایل اضافه شد: + +| Rule | Effect (mobile only) | +|---|---| +| `input/select/textarea { font-size: 16px !important }` | جلوگیری از زوم خودکار iOS روی فوکوس (فونت‌های ۱۲–۱۴px زوم می‌شدند) | +| `.units-layout { grid-template-columns: 1fr }` | سایدبار ۲۲۰px دسته‌ها روی موبایل بالای پنل محتوا قرار می‌گیرد | +| `.item-row-head { display: none }` + `.wizard-item-row` دوستونه | ردیف اقلام ویزارد (گرید ۸ ستونه) روی موبایل می‌شود: فیلدهای متنی تمام‌عرض، عددها دوستونه؛ placeholderها نقش برچسب را دارند | +| `.wizard-item-row > :last-child` تمام‌عرض + حداقل ۴۴×۴۰px | دکمه‌ی حذف قلم بزرگ و قابل لمس | +| `.dd-panels { grid-template-columns: 1fr }` | پنل‌های درگ‌اندروپ مرحله‌ی ۴ تک‌ستونه | +| `.flow-cols` تک‌ستونه + `.flow-lines { display: none }` | نمودار جریان باکس‌ها روی هم؛ خطوط SVG اتصال که فقط در چیدمان دوستونه معنی داشتند روی موبایل مخفی می‌شوند | +| `.conv-form` تک‌ستونه، `.conv-row` wrap | فرم تبدیل واحد (گرید ۵ ستونه) روی موبایل ستونی | +| touch targets بزرگ‌تر (`.chip`, `.quick-chip`, `.spec-chip`, `.dd-item`, `.ut-cat`, `.btn-line-file`, دکمه‌های × و ...) | ارتفاع هدف لمسی به ~۴۰px رسید | + +### 2. `frontend/src/components/ui/dialog.tsx` — dialogs become bottom sheets +`DialogContent` با کلاس‌های `max-md:` به bottom-sheet تمام‌عرض تبدیل شد +(`inset-x-0 bottom-0`، گوشه‌های گرد فقط بالا، `max-h-[85dvh]`، padding با +`safe-area-inset-bottom`). چون همه‌ی مودال‌ها (ویرایش، پذیرش، تایید حذف) از همین +کامپوننت استفاده می‌کنند، همه یک‌جا درست شدند. دکمه‌ی بستن هم بزرگ‌تر شد. + +### 3. Form grids stacked — one-line additions each +- `EditModal.tsx` — گرید تأمین‌کننده/پروژه و گرید تاریخ‌ها: `max-md:grid-cols-1` +- `Wizard/Step1TopicSupplier.tsx` — فرم تأمین‌کننده‌ی جدید: `max-md:grid-cols-1` +- `Wizard/Step5Review.tsx` — گرید تاریخ‌ها: `max-md:grid-cols-1` +- `Suppliers/CategoryPage.tsx` — فرم افزودن تأمین‌کننده: `max-md:grid-cols-1` + +### 4. `frontend/src/components/layout/Header.tsx` — sticky header + clearance +- هدر فقط روی موبایل `sticky top-0 z-40` شد (`max-md:`) +- دکمه‌ی بازگشت بزرگ‌تر شد +- `PageLayout` روی موبایل `pb-24` گرفت تا نوار تب پایین روی محتوا نیفتد + +### 5. `frontend/src/components/layout/MobileTabBar.tsx` — NEW mobile bottom tab bar +نوار تب پایین (فقط `<768px` با `md:hidden`): +- **ادمین / مدیر تدارکات در دفتر:** داشبورد / تأمین‌کننده‌ها / پروژه‌ها / ضرایب +- **ادمین / مدیر تدارکات در پنل انبار:** انبار / کنترل روزانه / کالاهای در راه / سازمان‌دهی (آینه‌ی ناوبری دسکتاپ) +- **مدیر پروژه:** انبار / تغییر موجودی / کنترل روزانه / کالاهای در راه +- هدف لمسی ≥۵۶px، padding برای `safe-area-inset-bottom`، هایلایت تب فعال +- در `/wizard` نمایش داده نمی‌شود (تا با جابه‌جایی تصادفی کارِ کاربر از بین نرود) + +### 6. `frontend/src/App.tsx` — renders `` + +### 7. `frontend/src/pages/Wizard/Step4Projects.tsx` — touch drag & drop +`TouchSensor` با `{ delay: 150, tolerance: 8 }` به سنسورهای dnd-kit اضافه شد: +درگ لمسی کار می‌کند بدون اینکه اسکرول صفحه را بدزدد. مسیر کلیک-قلم-بعد-کلیک-پروژه +هم‌چنان مسیر جایگزین است. دسکتاپ همان `PointerSensor` قبلی را دارد. + +### 8. `frontend/src/components/JalaliDatePicker.tsx` — dropdown width + targets +- داپداون تقویم: `max-w-[calc(100vw-2rem)]` (در صفحه‌های باریک سرریز نمی‌شود) +- سلول‌های روز و ورودی‌های ساعت/دقیقه روی موبایل بزرگ‌تر شدند + +### 9. PWA layer (optional installability) +- `frontend/public/manifest.webmanifest` — NEW (name فارسی، `dir: rtl`، + `display: standalone`، theme `#4f46e5`، آیکون 192/512 + maskable) +- `frontend/public/icons/icon-192.png`, `icon-512.png` — NEW + (تولیدشده با `frontend/scripts/gen-pwa-icons.ps1` — NEW، بدون وابستگی) +- `frontend/index.html` — `theme-color` + لینک manifest + آیکون‌ها اضافه شد +- روی HTTP مثل میان‌بر صفحه‌ی اصلی کار می‌کند؛ پرامپت نصب رسمی PWA به HTTPS + نیاز دارد (بعداً با پروکسی/Let's Encrypt — بدون تغییر کد). + +## Deliberately NOT changed + +- **`SpecDrawer.tsx`**: کد مرده است (هیچ‌جا import نمی‌شود) — تصمیم تیم: دست نمی‌زنیم، فایل می‌ماند. +- **`.e-item-row` / `.spec-drawer` در CSS**: کلاس‌های بلااستفاده — دست‌نخورده ماندند. +- هیچ کلاس/استایل دسکتاپی ویرایش نشد؛ تمام تغییرات داخل `max-md:` یا کامپوننت‌های `md:hidden` است. + +## Verification performed + +- `npm run lint` (oxlint) → 0 errors (4 هشدار از قبل موجود) +- `npm run build` (tsc + vite) → موفق +- تست مرورگر روی Vite dev (viewport واقعی 375px): + - نوار تب: `fixed bottom-0`، قابل‌مشاهده، ۴ تب با برچسب درست، ارتفاع ≥56px + - هدر `sticky`، ورودی‌ها 16px، padding پایین صفحه 96px (جای نوار تب) + - صفحات ضرایب/ویزارد چیدمان تک‌ستونه (`.units-layout` فقط بعد از انتخاب موضوع رندر می‌شود) +- رگرسیون دسکتاپ: کلاس‌های دسکتاپ دست‌نخورده‌اند (بریک‌پوینت‌گیت)؛ + `npm run build` سلامت باندل را تایید می‌کند. + +## How to test manually + +```powershell +cd C:\Users\Reza\Desktop\v1.4 +docker compose up -d --build # بازسازی ایمیج فرانت +``` + +سپس در مرورگر دسکتاپ DevTools → device toolbar (مثلاً iPhone SE 375px) یا +مستقیم با گوشی (بخش «دیدن روی گوشی» در README/جواب نهایی). دسکتاپ را هم در +1280px چک کنید — باید دقیقاً مثل قبل باشد. + +## Notes / known considerations + +- خطوط اتصال نمودار جریان (SVG) روی موبایل عمداً مخفی شد — فقط باکس‌ها روی هم + قرار می‌گیرند؛ اگر روزی خواستید اتصال‌ها را روی موبایل هم ببینید باید + `FlowDiagram` برای حالت تک‌ستونه بازنویسی شود. +- نوار تب موبایل «انبارها» را ندارد (PMها از منوی همبرگری به آن می‌رسند). diff --git a/frontend/index.html b/frontend/index.html index c810dc5..701ce25 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,10 @@ + + + + =#EVVPGN+YJm%(NXrs?@#?O4KlAw2>Gxs@86bR4Z*PZ3nT8#!#JN zS}h5sf|j7PwN^?6u@qB#G+HFJG|VqE??3a`d)_ne@0|O)=XvgP@Aq@>ea?CAbx#jx zg&n(h000zRT^zljzFFL4q@cgoXt*@g!OnR(+k^5R?Rm(MJZXou1E7*B_vuFjWXqm* z@jC~A>UnX4y$RNd1z-o()zR+zSp2&J>^1G5c9X40-Mz)m)Wf8FZ7&nmECUt2&t+gL zI7>5W-A1?oc1!BXw1&Xhi#&ni16@mIGgQw`@F-@y` zqo~bIfl^{!7`y93F_PfwU7%i(p#030HKPx&`81Sle5|zX_*wan>tS-#kieR?+sJbr zp8kE!Fmw$ahbw8rpFh;YH|AWo!)2#2kd)qC_$S@22J-j9!+$C+YYY6Wu3S1O_nF<9 zXa$>1(XOs}zKWGTrDZ@TXy3=3b4Hq}lk}qzg~}3Ivw6Ca!B6$NNh?p;s*eP1)$;MZ z+OKct{^G|~67EOUmUGmBQR9#4h*IYh{o~q3rDqXU$TG&m5An1OIOnCMCTj{Bj(C8l0(AFaVwd~3ID<^j`X8??c-eX2rCB77pw(b;G3tF!1x;E5ZFygckJgp$t8)8d zJD6WPuIS_QVmpE*Z{|hySChHJJp;pSb+r!j^pf9Y5LACXB3f_CluqbkHn}X*gJK;D(inTc0-rZA2@k?_=VQfo0435=XV`;u~f1e3N zaR~8BpQL&EcVxZ3vh5?ujGKSMB|lM)RO9CKb&ybkQ zAZv?uaG8E1dH9~Ip!9o4*X0{Bri51JM$tv87A4+k${&Ib!hRpD-N(6REkn_?+~&wa zq=`823*g7iKpLyOI=}85B3?fU*pJ7;>mB$%zZ&V z-IWqDmj-vbCwHsPFBw!rpvo2?IR2CJCiw^GUIL|17@LWdp?W*<2!?EmXB7_`>A5eS zllaR4xJ6_gndTqh?-8(FZlDnbf@GUDBm)k>)~ahT*oPQNY)V!+9U z|IZWq@8o4b*j20&^OnL{5`>08d;|N)qUG2WJQP@+1@-eIgK zU{mKkA^B8rJbz0&MLU9r2=RvOL@gxd+D=kORD6_{l>|{c1F;o!e5U1Rr zkO|NdDKgFcy2MEC^$kem4vaWWN9I435$v~x;?-?^gcSEnhrT1STA4|qcF>H6V40ta z%zipgWVs0tk8+?5=#lBBI?=qQ>o{aDRHvDUgTFvt6qyc}ROpb=-r4u(cu) zP#qHVR>!sNf{bY}vpVFKT*KGW2ZZXK{zdtl6PoT&%x@p-uR`_T`0=tmLqpp&YB_BUp4SH8ULK(y@3d2)4ImJs& z^+kgwdhO5U%9u-$_On^m6JrWrE|0MNZXMX13SB!n*4ncU)eVXU6+O^cJC;9TxsA`H zYz|}tiUZnCL literal 0 HcmV?d00001 diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..2fae949da6b01fc90b1453509a6417a0a148a9c0 GIT binary patch literal 8272 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 literal 0 HcmV?d00001 diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest new file mode 100644 index 0000000..12b0389 --- /dev/null +++ b/frontend/public/manifest.webmanifest @@ -0,0 +1,33 @@ +{ + "name": "دفترچه تأمین — سامانه تدارکات و انبار", + "short_name": "دفترچه تأمین", + "description": "سامانه تدارکات و مدیریت انبار پروژه‌های ساختمانی", + "dir": "rtl", + "lang": "fa", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "background_color": "#f3f5f9", + "theme_color": "#4f46e5", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/frontend/scripts/gen-pwa-icons.ps1 b/frontend/scripts/gen-pwa-icons.ps1 new file mode 100644 index 0000000..b055bff --- /dev/null +++ b/frontend/scripts/gen-pwa-icons.ps1 @@ -0,0 +1,58 @@ +# Generate PWA icons (192px / 512px) — indigo rounded square + white clipboard glyph +Add-Type -AssemblyName System.Drawing +Add-Type -AssemblyName System.Drawing.Drawing2D + +function Make-RoundedRect([float]$x, [float]$y, [float]$w, [float]$h, [float]$r) { + $p = New-Object System.Drawing.Drawing2D.GraphicsPath + $d = 2 * $r + $p.AddArc($x, $y, $d, $d, 180, 90) + $p.AddArc($x + $w - $d, $y, $d, $d, 270, 90) + $p.AddArc($x + $w - $d, $y + $h - $d, $d, $d, 0, 90) + $p.AddArc($x, $y + $h - $d, $d, $d, 90, 90) + $p.CloseFigure() + return $p +} + +$outDir = "public/icons" +New-Item -ItemType Directory -Force -Path $outDir | Out-Null + +foreach ($s in @(192, 512)) { + $bmp = New-Object System.Drawing.Bitmap($s, $s) + $g = [System.Drawing.Graphics]::FromImage($bmp) + $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::AntiAlias + $g.Clear([System.Drawing.Color]::Transparent) + + $m = [float]($s * 0.055) + $bg = Make-RoundedRect $m $m ([float]($s - 2 * $m)) ([float]($s - 2 * $m)) ([float]($s * 0.20)) + $indigo = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(79, 70, 229)) + $g.FillPath($indigo, $bg) + + # clipboard board (white) + $bw = [float]($s * 0.56); $bh = [float]($s * 0.62) + $bx = [float](($s - $bw) / 2); $by = [float]($s * 0.31) + $board = Make-RoundedRect $bx $by $bw $bh ([float]($s * 0.05)) + $white = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::White) + $g.FillPath($white, $board) + + # clip on top + $cw = [float]($s * 0.15); $ch = [float]($s * 0.11) + $cx = [float](($s - $cw) / 2); $cy = [float]($by - $s * 0.085) + $clip = Make-RoundedRect $cx $cy $cw $ch ([float]($s * 0.03)) + $g.FillPath($white, $clip) + + # ring holes (indigo) + $g.FillEllipse($indigo, [float]($cx + $cw * 0.16), [float]($cy + $ch * 0.30), [float]($s * 0.035), [float]($s * 0.035)) + $g.FillEllipse($indigo, [float]($cx + $cw * 0.62), [float]($cy + $ch * 0.30), [float]($s * 0.035), [float]($s * 0.035)) + + # lines on the board + $slate = New-Object System.Drawing.SolidBrush ([System.Drawing.Color]::FromArgb(148, 163, 184)) + $lh = [float]($s * 0.032) + $g.FillRectangle($slate, [float]($bx + $s * 0.09), [float]($by + $s * 0.15), [float]($bw - $s * 0.18), $lh) + $g.FillRectangle($slate, [float]($bx + $s * 0.09), [float]($by + $s * 0.29), [float]($bw - $s * 0.30), $lh) + $g.FillRectangle($slate, [float]($bx + $s * 0.09), [float]($by + $s * 0.43), [float]($bw - $s * 0.24), $lh) + + $path = Join-Path $outDir "icon-$s.png" + $bmp.Save($path, [System.Drawing.Imaging.ImageFormat]::Png) + $g.Dispose(); $bmp.Dispose() + Write-Output "saved $path" +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0b51874..fdf1f7b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import { useAuthStore } from "./store/auth"; +import { MobileTabBar } from "./components/layout/MobileTabBar"; // هر صفحه با React.lazy لود می‌شود (code splitting) const DashboardPage = lazy(() => @@ -65,8 +66,9 @@ function HomeRoute() { export default function App() { return ( - }> - + <> + }> + } /> } /> - - + + + + ); } diff --git a/frontend/src/components/JalaliDatePicker.tsx b/frontend/src/components/JalaliDatePicker.tsx index 53b6aa4..0c28d9a 100644 --- a/frontend/src/components/JalaliDatePicker.tsx +++ b/frontend/src/components/JalaliDatePicker.tsx @@ -183,6 +183,8 @@ export function JalaliDatePicker({
@@ -217,7 +219,7 @@ export function JalaliDatePicker({ key={i} type="button" className={cn( - "cursor-pointer rounded py-0.5 text-[12px] hover:bg-slate-bg", + "cursor-pointer rounded py-0.5 text-[12px] hover:bg-slate-bg max-md:py-1.5", cell.isToday && !selectedIso && "bg-blue-bg font-bold text-[#1d4ed8]", cell.isToday && selectedIso && "font-bold text-[#1d4ed8]", cell.iso === selectedIso && "bg-accent text-white hover:bg-accent-dark", @@ -235,7 +237,7 @@ export function JalaliDatePicker({ ساعت دقیقه +
{menu &&
{menu}
} {(icon || brand) && ( @@ -38,7 +38,7 @@ export function Header({ title, sub, backTo, backLabel = "بازگشت", actions