قاب محصول (ProductFrame)
قاب واحد همهٔ برنامههای دادهای پرتو — نوار بالا، منوی دادهمحور و ناحیهٔ محتوا؛ یک بار، در layout بخش واردشدهٔ برنامه
معرفی
ProductFrame قاب برنامهٔ همهٔ برنامههای دادهای پرتو است (لایهٔ برنامه؛ دامنهٔ سیستم طراحی): نوار
بالا با هویت محصول و اقدامهای سراسری، منوی ناوبری که از داده ساخته میشود، و ناحیهٔ محتوایی که صفحه در آن رندر
میشود.
تصمیم: هر محصول ProductFrame را دقیقاً یک بار، در layout بخش واردشدهٔ برنامه به کار میبرد
همهٔ صفحههایی که کاربرِ واردشده در آنها کار میکند زیر همین یک قاب رندر میشوند و هیچ صفحهای قاب خودش را نمیسازد.
در Next.js این همان layout ریشه است؛ اگر مسیرهایی بیرون از قاب دارید (ورود، منبع PDF، اسلاید)، layout گروه مسیری است
که صفحههای برنامه در آناند (مسیرهای بیرون از قاب). قاب دستساز از NavRail نسازید
(AppShell، Sidebar و AppBar در 5.0 حذف شدند).
قاعدههایی که قاب خودش اجرا میکند، و محصول هیچکدام را نمینویسد:
- یک ارتفاع برای نوار بالا (
--layout-header-height، 48 پیکسل). نوار بالا هیچوقت عنوان صفحه را نشان نمیدهد؛ عنوان،h1خود صفحه است که قالب صفحهاش آن را میسازد، و هر صفحه فقط یکی دارد. - انتهای نوار بالا یک ترتیب ثابت دارد: اقدامهای سراسری (
actions)، کلید تم و در آخر منوی حساب (user)، همه همقد. کلید تم یک جای ثابت دارد و قاب آن را میگذارد (themeToggle). - منو در سمت شروع خط (راست): بهطور پیشفرض نوار آیکونی 48 پیکسلی (
navMode="rail") که با اشارهگر یا فوکوس صفحهکلید روی صفحه باز میشود و صفحه جابهجا نمیشود، یا منوی برچسبدار 256 پیکسلی که باز شروع میشود (navMode="menu")، یا بدون منو و فقط نوار بالا (navMode="none"). دکمهٔ سنجاق پایین نوار («نوار ناوبری همیشه باز») آن را باز نگه میدارد و انتخاب کاربر در مرورگر میماند. روی موبایل منو پنلی است که از سمت شروع (راست) باز میشود. - همهٔ مقصدها لینک واقعیاند؛ مقصد فعال از
pathnameپیدا میشود وaria-current="page"میگیرد. - مقصدهای پرتعداد را در گروههای نامدارِ منوی اصلی مرتب کنید و ستون دوم را برای فیلترهای صفحه آزاد بگذارید؛ زیرصفحههای دارای جریان کار مستقل میتوانند ناوبری ثانویه داشته باشند. نقش ستون دوم میتواند بین صفحههای یک محصول عوض شود: یک صفحه ناوبری و صفحهای دیگر فیلتر. در همان صفحه ترکیب ناوبری بالا و فیلتر پایین ممنوع است؛ با ناوبری، فیلترها نوار افقی بالای محتوا و در عرض کم Sheet هستند؛ بدون آن، ستون آزاد میتواند فیلتر بگیرد. وجود فیلتر دلیل افزودن زیرمنو نیست.
- ناحیهٔ محتوا هیچ padding و هیچ سقف عرضی ندارد. عرض و حاشیه را قالب صفحه یک بار میدهد، پس حاشیهٔ دوبل دیگر ممکن نیست.
- پسزمینهٔ محتوا نقش
canvasدارد؛ سطح روشنترِ جدول، کارت و نمودار از آن جداست. منو و سربرگ رنگِ ناوبری خود را حفظ میکنند. در روشن، بوم خاکستری ملایم و سطوح محصور سفیدند؛ رنگهای تیره تغییر نمیکنند. - لندمارکهای
header،navوmain(وbannerبهصورت ناحیهٔ نامدار)، و پیوند «پرش به محتوای اصلی» بهعنوان اولین توقف Tab؛ همه به زبانlocaleقاب (فارسی، عربی یا انگلیسی). - چاپ: فقط خود صفحه، روان در هر چند برگی که لازم دارد، با پالت روشن، زیر سربرگ
printHeaderو بالای خطfooterدر هر برگ.
چه زمانی استفاده کنیم:
- در هر برنامهٔ دادهای پرتو، یک بار، دور همهٔ صفحههایی که کاربرِ واردشده در آنها کار میکند.
چه زمانی استفاده نکنیم:
- داخل یک صفحه یا دور یک بخش: قاب فقط در layout برنامه است. قاب تودرتو در محیط توسعه هشدار میدهد.
- برای ناوبری داخل یک صفحه (زبانهها، بخشها):
TabsیاPageNav. - برای مسیرهایی که بخشی از برنامهٔ واردشده نیستند (ورود، منبع PDF سرور، اسلاید، ویجت جاسازیشده، صفحهٔ اشتراک عمومی): بیرون از قاب (مسیرهای بیرون از قاب).
- برای سایت بازاریابی یا صفحهٔ فرود: قاب مال برنامههای دادهای است؛ آن سطحها فقط لایهٔ پایه و اجزا را به کار میبرند.
استفاده
ProductFrame فقط از مسیر جدای خودش وارد میشود. در Next.js App Router، layout یک Server Component است و
نمیتواند usePathname() را صدا بزند؛ پس layout یک جزء کلاینت کوچک را دور children رندر میکند:
// app/frame.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ProductFrame, type ProductFrameNavGroup } from '@partodata/ui/product-frame'
import { Icons } from '@partodata/ui/icons'
const nav: ProductFrameNavGroup[] = [
{
id: 'monitoring',
items: [
{ id: 'overview', label: 'نمای کلی', icon: <Icons.home />, href: '/' },
{ id: 'mentions', label: 'منشنها', icon: <Icons.messageCircle />, href: '/mentions' },
{ id: 'reports', label: 'گزارشها', icon: <Icons.fileText />, href: '/reports' },
],
},
{
id: 'admin',
label: 'مدیریت',
items: [
{
id: 'settings',
label: 'تنظیمات',
icon: <Icons.settings />,
href: '/settings',
children: [{ id: 'team', label: 'اعضا', href: '/settings/team' }],
},
],
},
]
export function Frame({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
return (
<ProductFrame product={{ name: 'پایش برند' }} nav={nav} pathname={pathname} linkComponent={Link}>
{children}
</ProductFrame>
)
}// app/layout.tsx
import { Frame } from './frame'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="fa" dir="rtl" className="dark" data-theme="dark" suppressHydrationWarning>
<body>
<Frame>{children}</Frame>
</body>
</html>
)
}هر صفحه یک قالب صفحه از @partodata/ui/templates است: قالب عرض و حاشیه،
عنوان (تنها h1، 48 پیکسل زیر نوار بالا)، ریتم، جای اقدامها و حالتها را تعیین میکند و صفحه فقط جایگاههایش را پر
میکند:
// app/mentions/page.tsx
'use client'
import { DataTable } from '@partodata/ui'
import { ListPage } from '@partodata/ui/templates'
const columns = [{ id: 'author', header: 'نویسنده', cell: (row: { author: string }) => row.author }]
export default function MentionsPage() {
return (
<ListPage title="منشنها" description="همهٔ منشنهای برند در شبکههای پایششده" state={{ status: 'ready' }}>
<DataTable columns={columns} data={[{ author: 'نگار' }]} />
</ListPage>
)
}نمونهٔ کامل صفحهٔ فهرست (جستوجو، فیلترها، حالتها و صفحهبندی) در ListPage است.
حالتها و انواع
منوی برچسبدار یا نوار آیکونی
navMode="rail" (پیشفرض) نوار آیکونی 48 پیکسلی است که با اشارهگر، روی صفحه و بدون جابهجا کردن آن، باز
میشود؛ ورود فوکوس صفحهکلید هم همین کار را میکند. اشارهگر و فوکوس دو دلیل جدا هستند: نوار وقتی جمع میشود که
هیچکدام داخلش نباشد. با خروج اشارهگر، نوار کمی بعد (150 میلیثانیه) جمع میشود تا رفتوبرگشت سریع چشمک نزند؛ اگر فوکوس
صفحهکلید داخلش باشد باز میماند. فوکوسی که کلیک موس روی یک مقصد جا میگذارد نوار را باز نگه نمیدارد، ولی Tab بعدی
دوباره بازش میکند. کاربر با دکمهٔ
سنجاق پایین نوار («نوار ناوبری همیشه باز») میتواند آن را باز نگه دارد؛ آنوقت منو عرض خودش را از صفحه میگیرد.
انتخاب در مرورگر میماند (اگر مرورگر ذخیره را ببندد، فقط فراموش میشود). navMode="menu" راه انصراف است: منوی
برچسبدار 256 پیکسلی که باز شروع میشود، برای محصولی که کاربرانش برچسبها را همیشه روی صفحه لازم دارند؛ همان
سنجاق آن را به نوار تبدیل میکند.
navMode تنها راه تعیین حالت پیشفرض است. navBehavior فقط همراه onNavBehaviorChange پذیرفته میشود و منو را
کنترلشده میکند (برای نمونهها و embed؛ چیزی ذخیره نمیشود). onNavBehaviorChange بهتنهایی فقط تغییر را گزارش میدهد
(مثلاً برای آمار) و انتخاب کاربر همچنان اعمال و ذخیره میشود.
منوی دوم (secondaryNav)
محصولی که هر بخشش مقصدهای خودش را دارد (صفحههای یک پروژه، بخشهای یک گزارش) آنها را در secondaryNav میدهد:
پنلی 240 پیکسلی بین منوی اصلی و صفحه، در سمت شروع، با لندمارک nav جدا به نام secondaryNavLabel (پیشفرض
«ناوبری بخش»). محتوا بخشهای SecondaryNav (SecondaryNavTitle، SecondaryNavItem، …) یا لینک است. نوار اصلی
وقتی باز میشود روی این پنل هم میآید و چیزی جابهجا نمیشود. روی موبایل منوی دوم داخل پنل منو، زیر مقصدهای
اصلی میآید. چاپ نمیشود و با chrome="none" نیست.
<ProductFrame
product={{ name: 'پایش برند' }}
nav={nav}
pathname={pathname}
secondaryNavLabel="کمپین تخفیف فصلی"
secondaryNav={
<>
<SecondaryNavTitle>کمپین تخفیف فصلی</SecondaryNavTitle>
<SecondaryNavItem asChild isActive>
<Link href="/campaigns/12">نمای کلی</Link>
</SecondaryNavItem>
<SecondaryNavItem asChild>
<Link href="/campaigns/12/mentions">منشنها</Link>
</SecondaryNavItem>
</>
}
>
{children}
</ProductFrame>سایدبار فیلتر در ستون دوم
نقش ستون دوم برای صفحهٔ فعلی تعیین میشود؛ لازم نیست در همهٔ صفحههای یک پنل ثابت باشد. بخشهای پرتعداد ممکن است این ستون را برای زیرصفحهها داشته باشند و صفحههای کمازدحام همان محصول برای فیلترها. در نبود secondaryNav همان صفحه، ستون سایدبار فیلتر ListPage، DashboardPage یا CustomPage است (filterPanel؛ از 7.13 جای پیشفرض همهٔ صفحهها) و همهچیزِ فیلترها داخل آن است: شمار و «پاک کردن همه»، چیپهای فعال و نشانگر هر بخش؛ نوار ابزار صفحه دکمهٔ «فیلترها» ندارد. ناوبری همان صفحه اولویت دارد: با secondaryNav دارای محتوا، فیلترها نوار افقی بالای محتوا با Popover بومی و Sheet موبایل هستند؛ زیر ناوبری یا در ستون عمودی سوم قرار نمیگیرند. Fragment خالی ستون را رزرو نمیکند.
عرض پیشفرض ستون فیلتر 15rem است؛ برای ستون پهنتر، روی قاب filterSidebarWidth="wide" بدهید: 18rem، یعنی 288 پیکسل با ریشهٔ 16 پیکسلی، که از منوی اصلی بازِ 16rem پهنتر است و نزدیک ستون فیلتر صفحهٔ Logs در Supabase (265 پیکسل). Prototype همین عرض را دارد. این گزینه عرض 15rem منوی ثانویه را تغییر نمیدهد.
اگر صفحه filterPanel={{ …, collapsible: true }} بدهد (اختیاری؛ در Prototype خاموش)، قاب ستون را برای کاربر جمعشدنی میکند: دکمهٔ «بستن فیلترها» در سر پنل و Ctrl+B (⌘B) آن را میبندند. ستون بسته یک نوار 40 پیکسلی (product-frame-page-sidebar-rail) جای خودش میگذارد که صفحه زبانهٔ «فیلترها» را با شمار فیلتر فعال در آن میگذارد؛ ستون از همان زبانه برمیگردد، هرگز از دکمهای در نوار ابزار. انتخاب کاربر در localStorage قاب میماند و فقط برای صفحههایی اثر دارد که خودشان collapsible خواستهاند؛ صفحهٔ بعدی بدون آن ستونش را دارد. عرض ستون (15rem یا 18rem) عوض نمیشود.
قاب فقط جا را قرض میدهد و وضعیت فیلترها در صفحه میماند. محاسبهٔ جاگیری از عرض انتخابشده استفاده میکند؛ روی موبایل، بدون قاب یا وقتی ستون کمتر از 40rem برای محتوا میگذارد، فیلترها نوار افقی بالای محتوا و زیر 36rem، Sheet بومی میشوند. با رفتن صفحه ستونِ اختصاصی فیلتر آزاد میشود.
فقط نوار بالا (navMode="none")
برای محصولی تکبخشی که کاربرانش در یک جا کار میکنند (یک فهرست، موارد آن و یک جریان ساخت): نه منو، نه دکمهٔ منو و نه
کنترل حالت منو؛ فقط نوار بالا و صفحه، و صفحه تمام عرض را میگیرد. چند مقصد دیگر این محصول (مدیریت کاربران،
هزینهها) موردهای منوی حساب (UserMenu در user) هستند که با مسیریاب جابهجا میشوند:
onSelect: () => router.push('/users') (useRouter از next/navigation) یا navigate('/users') (useNavigate در
react-router). این موردها پیوند نیستند: نه در زبانهٔ تازه باز میشوند و نه نشانیشان کپی میشود (href برای
مورد منوی حساب، از راه linkComponent قاب، کار بعدی است). قاعدهٔ تصمیم: محصولی که دو بخش یا بیشتر از خودش دارد، یا مقصد دیگری دارد ولی منوی حساب ندارد،
منو (menu) میگیرد؛ این تصمیمِ خود محصول است، نه کاربر به کاربر. none هیچ nav، navFooter، activeId یا جفت
حالت منو نمیپذیرد و menu و rail بدون nav پذیرفته نمیشوند (نوع هر دو را اجبار میکند).
const router = useRouter() // next/navigation
<ProductFrame
product={{ name: 'پایش پست' }}
navMode="none"
pathname={pathname}
linkComponent={Link}
user={
<UserMenu
user={user}
items={[
{ label: 'مدیریت کاربران', onSelect: () => router.push('/users') },
{ type: 'separator' },
{ label: 'خروج', onSelect: signOut, destructive: true },
]}
/>
}
>
{children}
</ProductFrame>منو از روی داده
- گروهها (
nav): هر گروه یکid، یکlabelاختیاری (عنوان گروه در منوی باز، و جداکننده روی نوار آیکونی) وitemsدارد. - مقصدها:
id،label،icon،href، و اختیاریbadge(عدد یا وضعیت کوتاه؛ روی نوار آیکونی یک نقطه) وdisabled. - یک سطح زیرمنو (
children): زیر مقصد والد، وقتی بخشِ آن فعال است و منو باز است، فهرست میشود. بخشی که صفحهٔ خودش را ندارد یک گروه است، نه یک مقصد با زیرمنو. - پایین منو (
navFooter): مقصدهایی مثل تنظیمات و راهنما، بالای کنترل حالت منو.
عنوان گروهها و ردیفهای زیرمنوی بخش فعال، در نوار بسته نیز فضای خود را نگه میدارند؛ عنوان با جداکننده جایگزین میشود و زیرمنو دیده نمیشود و در ترتیب Tab نیست. بنابراین هنگام بازشدن با اشارهگر، آیکون مقصد زیر دست جابهجا نمیشود. فاصلهٔ خالیِ زیر بخش فعال روی نوار، جای همان زیرمنو در منوی باز است.
مقصد فعال
pathname را بدهید (مثلاً usePathname()). مقصد فعال آن است که hrefش با مسیر، با بخشهای کامل مسیر،
جور باشد و بلندترین href برنده است: برای /reports/42 مقصد /reports فعال است، نه / و نه
/reports-archive. پرسوجو (?…)، # و / انتهایی نادیده گرفته میشوند. وقتی یک زیرمنو فعال است، والدش
برجسته و زیرمنو باز است. activeId صریح بر pathname برتری دارد.
اقدامهای سراسری و اعلان
actions کنترلهای سراسری انتهای نوار بالاست: جستوجو، اعلانها و راهنما. همه یک اندازه (sm، 30 پیکسل)
میگیرند و دکمهٔ فقطآیکون مربعی همقد ردیف است. کلید تم در actions نمیرود: قاب آن را در یک جای ثابت
میگذارد، با themeToggle: 'header' (پیشفرض) آخرین کنترل نوار بالا درست پیش از منوی حساب، 'user-menu' موردی
به نام «تم تاریک» داخل منوی حساب (بدون user به نوار بالا برمیگردد)، یا false. کلید فقط زیر ThemeProvider
از @partodata/ui/theme-toggle رندر میشود؛ محصولی با یک تم ثابت هیچ کلیدی نمیبیند. منوی حساب در
actions نمیرود: user آن را همیشه آخرین کنترل نوار بالا میگذارد، و UserMenu بدون size آنجا دایرهای
همقد همین ردیف است. اقدام اصلی یک صفحه («ساخت گزارش»، «هشدار جدید») هرگز در actions قاب نمیآید: مال همان صفحه
است (primaryAction نوارابزار یا سربرگ صفحه)، و دکمهٔ بدون variant در actions به default رندر میشود. banner
اعلانی تمامعرض بالای نوار بالاست (نگهداری، اختلال، ارتقا) و ناحیهای با نام «اطلاعیه» است.
«آخرین تغییرات» در actions یک برچسب آرام نسخه است، نه زنگولهٔ دوم: <WhatsNewBell variant="version" /> پیش از
زنگولهٔ اعلانها، که با کلیک پنل تغییرات را باز میکند و وقتی ریلیز دیدهنشده هست یک نقطه دارد
(آخرین تغییرات).
<ProductFrame
product={{ name: 'پایش برند' }}
nav={nav}
pathname={pathname}
linkComponent={Link}
actions={
<>
<WhatsNewBell variant="version" version="2.4" unseenCount={feed.announcedCount} onClick={openWhatsNew} />
<NotificationsButton />
</>
}
themeToggle="header"
user={<UserMenu user={{ name: 'سارا احمدی' }} items={accountItems} />}
>
{children}
</ProductFrame>ناحیهٔ محتوا
ناحیهٔ محتوا یک main با اسکرول خودش است، بدون padding و بدون سقف عرض. جزئی که بخواهد بداند صفحهٔ قاب است،
میتواند از useInProductFrame() بپرسد: فقط داخل صفحه (children) true است؛ در actions، user و banner، و
داخل هر پنجره یا پنل رویی (Dialog، Sheet، Drawer، Popover، منوها) که از صفحه باز شود، false است. برای
همین PageHeader داخل یک Sheet فاصلهٔ 48 پیکسلی صفحه را نمیگیرد.
عنوان سند، فوکوس و اسکرول در تغییر مسیر
از 7.9 قاب خودش کاری را میکند که یک برنامهٔ تکصفحهای معمولاً فراموش میکند (WCAG 2.4.2 سطح A، 2.4.3، 4.1.3) و محصول هیچ کدی برایش نمینویسد:
- عنوان تب:
document.titleهمیشه{عنوان صفحه}{ · titleContext} | {نام محصول}است. عنوان صفحه ازh1همان قالب میآید و اگر بعداً بارگذاری شود (نام یک مسئله) خودش بهروز میشود؛ بخش صفحه حداکثر 60 نویسه است.titleContextمکان را میگوید وقتی عنوان صفحه نمیگوید: منبع انتخابشده («اینستاگرام»)، پروژه. فقط متن ساده و ارقام ASCII. - فوکوس: پس از تغییر مسیر (نه query: فیلتر فوکوس را نمیپراند) فوکوس به
h1صفحهٔ تازه میرود، تا کاربر صفحهخوان از عنوان صفحهٔ تازه شروع کند نه از پیوندی که دیگر نیست. اگر فوکوس روی چیزی داخل محتوا باشد که میماند (پیوند یک تب، یک فیلد)، همانجا میماند و یک ناحیهٔaria-live="polite"عنوان تازه را اعلام میکند. بار نخست فوکوس جابهجا نمیشود. - اسکرول: ناحیهٔ محتوا اسکرول میشود، نه پنجره. صفحهٔ تازه از بالا شروع میشود، «برگشت» مرورگر به جای قبلی برمیگردد (در حافظه، بهازای هر نشانی)، و نشانی با
#hashبه همان عنصر میرود و فوکوسش میکند.useFrameScroll()ظرف اسکرول را بدونdata-slotمیدهد؛TableOfContentsهم همان را دنبال میکند.
<ProductFrame
product={{ name: 'پرتو' }}
nav={nav}
pathname={pathname}
titleContext={source.label} // «مسائل رصدشده · اینستاگرام | پرتو»
scroll={{ restore: true, key: location.key }} // key: شناسهٔ ورودی تاریخچه (react-router)؛ پیشفرض نشانی
>همه پیشفرض روشناند (پیش از 7.9 هیچ قابی عنوان نمیگذاشت): documentTitle={false} برای محصولی که عنوان را خودش میگذارد (فرادادهی فریمورک)، routeFocus={false} برای فوکوس، و scroll={false} برای اسکرولی که محصول خودش میکند.
خط پایانی (footer)
یک خط آرام در انتهای هر صفحه، زیر محتوای خود صفحه: نسخه یا شناسهٔ ساخت برنامه، متن مجوز یا حق نشر. فقط وقتی
شرح کار آن را بخواهد، و هرگز نسخه، شناسهٔ ساخت، سازمان یا تاریخی ساختگی: مقدارش از ساخت خود محصول میآید. در صفحهٔ
کوتاه به پایین ناحیهٔ محتوا میچسبد و در چاپ پایین هر برگ تکرار میشود. فقط متن و پیوند کوچک؛ مقصدهای ناوبری
جایشان navFooter است و دکمه هیچوقت اینجا نمیآید (قاعدهٔ lint parto/page-primary-action آن را میگیرد، و دکمهای
که برسد هرگز اصلی رندر نمیشود). با chrome="none" رندر نمیشود. خط پایانی بعد از محتوای صفحه است، پس محصولی که
فهرستهای اصلیاش بیپایان اسکرول میشوند، نسخه را در منوی حساب هم نشان میدهد.
<ProductFrame
product={{ name: 'پایش برند' }}
nav={nav}
// the product's own version, from its build (next.config: env.NEXT_PUBLIC_APP_VERSION)
footer={<span>نسخهٔ {process.env.NEXT_PUBLIC_APP_VERSION}</span>}
>
{children}
</ProductFrame>زبان (locale)
locale زبان صفحه است و یک بار، روی قاب داده میشود: fa (پیشفرض)، ar یا en. رشتههایی که خود قاب
میسازد — پیوند پرش، نام لندمارکها و ناحیهٔ اطلاعیه، دکمه و پنل منوی موبایل، و کنترل حالت منو — و رشتههای همهٔ
قالبهای صفحه (پیوند بازگشت، زبانهها، حالتهای بارگذاری و خطا، صفحهبندی، نوار ابزار) به همین زباناند: قالبی که
locale خودش را ندارد، زبان قاب را میگیرد. جزءهایی که برنامه در قاب، در جایهای نوار بالا و در صفحه میگذارد هم
همین را میگیرند، و پنجرهها، پنلها و منوهایی هم که از آنها باز میشوند: هر جزئی که locale خودش را دارد
(DataTable، SearchInput، FilterBar، PeriodSelector، DatePicker، DateRangePicker، ConfirmDialog، UserMenu در
user، SentimentBadge، نمودارها …) و برچسب پنهان دکمهٔ بستن Dialog و Sheet. قاب زبان را با زمینهٔ مشترک
page-locale@1 به همهٔ آنها میدهد؛ پس locale را روی هیچ قالب یا جزئی تکرار نکنید. locale خود یک جزء همچنان
برنده است (نقلقولی فارسی در صفحهٔ انگلیسی). DateRangePicker در ar و en تقویم میلادی دارد. <Toaster /> که در
layout ریشه کنار قاب است، زبان را از lang عنصر ریشه میخواند. جهت را locale تعیین
نمیکند: عنصر ریشه هر دو را دارد، <html lang={locale} dir={locale === 'en' ? 'ltr' : 'rtl'}>، و منو همیشه در سمت
شروع خط است. برچسب مقصدهای منو دادهٔ خود محصول است و به زبان همان صفحه نوشته میشود؛ badge عددی با رقم لاتین نوشته
میشود، پس محصول عربی یا انگلیسی عدد نشان را رشتهای میدهد که خودش قالببندی کرده است.
// app/[locale]/frame.tsx — a product with Persian and Arabic routes
<ProductFrame
product={{ name: 'رصد العلامة', href: `/${locale}` }}
nav={nav[locale]}
locale={locale}
pathname={pathname}
linkComponent={Link}
user={<UserMenu user={user} items={accountItems[locale]} />}
>
{children}
</ProductFrame>وقتی صفحهٔ اصلی محصول پیشوند زبان دارد (product.href برابر /fa)، همهٔ مسیرها زیر آناند؛ برای همین مقصدی که hrefش
همان صفحهٔ اصلی است فقط روی خود آن مسیر فعال است، نه روی هر مسیری که مقصد دیگری آن را نپوشانده است.
بدون نوار و منو (chrome="none")
برای همان مسیرِ یک صفحهٔ پایش وقتی روی نمایشگری بیکاربر دیده میشود (دیوار عملیات، تلویزیون، کیوسک): نه نوار
بالا، نه منو، نه پیوند پرش و نه خط پایانی. صفحه همان قالب خودش است، با همان عرض و حاشیه. banner میماند، چون
اطلاعیه برای کسی است که نمایشگر را میبیند.
یک سازوکار آن را روشن میکند: chrome={useKioskChrome()} در جزئی که قاب را رندر میکند، و نشانی ?kiosk=1 روی
نمایشگر (/live?kiosk=1). این قلاب نشانی را یک بار، وقتی قاب در مرورگر mount میشود، میخواند و تا زبانهٔ مرورگر در
برنامه است نگه میدارد (پیوندی که داخل نمای کیوسک دنبال شود نوار را برنمیگرداند). سرور و اولین render مرورگر full
میدهند، پس hydration جور است، و قاب بیآنکه صفحه دوباره mount شود به none میرود. قلاب هیچ hook مسیریابی به کار
نمیبرد، پس در layout ریشهٔ ایستای Next.js (جایی که useSearchParams بیمرز Suspense، next build را میشکند) و در
layout route در react-router یکسان کار میکند. نام پرسوجوی دیگر، مسیر جدا، متغیر محیطی یا ذخیرهٔ مرورگر برای آن
نسازید، و chrome="none" را هرگز ثابت ننویسید (قاعدهٔ lint parto/page-template آن را میگیرد).
قاعدهٔ تصمیم: مسیری که هیچوقت نوار و منوی قاب را ندارد (ورود، اسلاید، منبع PDF) اصلاً در قاب نیست؛ chrome
برای مسیری است که همان صفحهٔ برنامه است و گاهی بیکاربر نمایش داده میشود.
// app/frame.tsx
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { ProductFrame, useKioskChrome } from '@partodata/ui/product-frame'
export function Frame({ children }: { children: React.ReactNode }) {
return (
<ProductFrame product={product} nav={nav} chrome={useKioskChrome()} pathname={usePathname()} linkComponent={Link}>
{children}
</ProductFrame>
)
}چاپ
چاپ یک صفحه از برنامه، خود صفحه را چاپ میکند. قاب در چاپ:
- نوار بالا، منو،
bannerو پیوند پرش را چاپ نمیکند؛ - جعبهٔ ثابتارتفاع و اسکرول ناحیهٔ محتوا را برمیدارد، پس صفحه در هر چند برگی که لازم دارد جاری میشود و به یک صفحهٔ نمایش بریده نمیشود؛
- داخل صفحه هیچ جزء چسبان یا ثابتی (
sticky،fixed) روی محتوا نمینشیند و جعبهٔ اسکرول درونی جدول (stickyHeader) محتوایش را نمیبُرد؛ - هیچ چیز را پهنتر از برگ نمیگذارد: خانههای جدول در چاپ میان واژهها میشکنند (هیچ واژه یا عددی دو تکه نمیشود)، پس ستونهای آخر یک جدول پهن بیرون از برگ نمیمانند، و نموداری که روی صفحه پهنتر از برگ کشیده شده با همان تناسب به عرض برگ کوچک میشود (متنش هم کوچکتر) و کارتش همپای آن کوتاه میشود؛
- برگ را وسط یک کارت، نمودار، ردیف جدول یا تصویر، و درست بعد از یک عنوان نمیشکند؛
- همیشه با پالت روشن چاپ میکند (متن تیره روی کاغذ سفید، هر تمی که روی صفحه است) و رنگها را همانطور که دیده میشوند چاپ میکند؛ متن محورها، خطوط شبکه و برچسب برشهای نمودار دایرهای هم پالت روشن میگیرند؛
printHeaderرا، اگر داده باشید، بالای هر برگ وfooterرا پایین هر برگ تکرار میکند.
روی کاغذ، صفحه آنچه را دارد و دامنهاش را نشان میدهد، نه راه تغییر آن را:
| چاپ میشود | چاپ نمیشود |
|---|---|
عنوان، توضیح، meta و محتوای صفحه | دکمههای جایگاههای اقدام: سرِ صفحه، نوار ابزار، بخشها و کارتهای نمودار |
بازهٔ زمانی (period)، فقط گزینهٔ انتخابشده | گزینههای دیگر بازه |
جستوجویی که عبارت دارد، فیلتری که مقداری دارد، تراشههای فیلتر فعال (activeFilters) | جستوجوی خالی، فیلتر بیمقدار (بازهٔ تاریخ خالی هم) و دکمهٔ پاک کردن فیلترها |
| خط بازهٔ ردیفها («1 تا 25 از 60») | صفحهشمار، انتخاب اندازهٔ صفحه و نوار انتخاب ردیفها |
| زبانهٔ فعلی: نام بخشی که برگ نشان میدهد | زبانههای دیگر و پیوند بازگشت |
کنترلی که همهٔ اینها را خودتان حذف کنید وجود ندارد: کنترلهای جایگاههای قالب را قاب خودش کنار میگذارد. کنترلی که
صفحه بیرون از این جایگاهها خودش ساخته باشد (مثلاً در محتوای یک CustomPage) print:hidden میگیرد؛ ناحیهٔ چاپ جدا
هرگز ساخته نمیشود.
printHeader سربرگ برگههای چاپی محصول است: نشانه و نام محصول یا سازمان در ابتدا، و در انتها یک خط طبقهبندی یا
تاریخی که محصول خودش دارد (دورهٔ گزارش). فقط وقتی شرح کار سربرگ بخواهد؛ هرگز سازمان ساختگی و هرگز تاریخ امروز
(صفحهٔ ایستا آن را در زمان ساخت منجمد میکند و hydration ناجور میشود). روی صفحه دیده نمیشود. عنوان صفحه را در آن
تکرار نکنید؛ h1 صفحه همانطور که هست چاپ میشود، و دورهٔ گزارش جایش در خود صفحه است (meta یا period).
اقدام «چاپ» فقط روی صفحهای است که برای کاغذ معنا دارد: گزارشی (یک DetailPage یا DashboardPage) که جدولهایش
همهٔ ردیفها را دارند — بیصفحهبندی و بیvirtualize. فهرستی که صفحهبندی دارد فقط صفحهٔ فعلیاش را دارد و اقدام
«چاپ» نمیگیرد. اقدام «چاپ» یک اقدام ثانوی خود صفحه است (secondaryActions) که window.print() را صدا میزند؛ همان
دکمه خروجی PDF هم هست، چون پنجرهٔ چاپ مرورگر «ذخیره بهصورت PDF» دارد.
<ProductFrame
product={product}
nav={nav}
// the brief's letterhead: the product's name, and a classification line at the end — never today's date
printHeader={
<>
<span>پایش برند</span>
<span>ویژهٔ استفادهٔ داخلی</span>
</>
}
>
{children}
</ProductFrame>
// in the page: its period in the page itself, and its print action, secondary
<DetailPage
title="گزارش ماهانهٔ منشنها"
meta={<Badge variant="secondary">مهر 1405</Badge>}
secondaryActions={
<Button variant="default" onClick={() => window.print()}>
چاپ
</Button>
}
>
…
</DetailPage>دو مرز چاپ از مرورگر:
- نمودارها با JavaScript از تم صفحه رنگ میگیرند و به عرض صفحهٔ نمایش کشیده میشوند. در چاپ، متن محورها و شبکه
پالت روشن را میگیرد و نمودار پهن به عرض برگ کوچک میشود، اما رنگ سریها همان رنگی است که روی صفحه بود (از تم
تیره، کمی روشنتر) و نمودار با عرض برگ از نو کشیده نمیشود. نمودار حرارتی، ابر واژه و نمودار شبکه هم
همینطور کوچک میشوند و کارت نمودار همپای آن کوتاه میشود. چون کوچکشدن متن را هم ریز میکند (نمودار تمامعرضی
که روی صفحهٔ 1920 پیکسلی کشیده شده با متنی حدود نصف اندازه چاپ میشود)، نموداری که برای کاغذ است در
DashboardChartنیمعرض مینشیند (در برگ A4 تقریباً به اندازهٔ خودش چاپ میشود)، و محصولی که چاپهایش نمودار یا جدول پهن دارند در برگ افقی چاپ میکند (بند بعد). رنگ واژههای ابر واژه هم مثل رنگ سریها همان رنگ صفحه است. - جدول بسیار پهن: خانههای جدول فقط میان واژهها شکسته میشوند (هیچ واژه یا عددی دو تکه نمیشود)؛ جدولی که
واژههایش کنار هم در برگ عمودی جا نمیشوند، در برگ افقی چاپ میشود. این تصمیمِ
محصول برای همهٔ چاپهایش است: یک خط در CSS سراسری محصول،
@page { size: A4 landscape; }.
PDFی که سرور میسازد (سرویس تبدیل HTML به PDF) از صفحهٔ جدای بیرون از قاب ساخته میشود (مسیرهای بیرون از قاب).
مسیریابها: Next.js و react-router
linkComponent لینکی است که href میگیرد، و همهٔ لینکهای قاب و قالبهای صفحه از آن رد میشوند: منو، صفحهٔ اصلی
محصول، پیوند بازگشت و زبانههای قالبها. Link در Next.js همین را میگیرد و همانطور که هست داده میشود. Link در
react-router بهجای href، to میگیرد؛ آن را با createRouterLink یک بار، در سطح ماژول، تطبیق دهید (مؤلفهٔ
تازه در هر render همهٔ لینکها را از نو mount میکند). Link react-router که مستقیم داده شود خطای نوع است (همهٔ
لینکهایش به صفحهٔ فعلی اشاره میکردند). نشانیای که scheme دارد (https://…) لینک ساده میماند.
// src/frame.tsx — Vite + react-router
import { Link, Outlet, useLocation } from 'react-router-dom'
import { ProductFrame, createRouterLink } from '@partodata/ui/product-frame'
const FrameLink = createRouterLink(Link)
export function Frame() {
const { pathname } = useLocation()
return (
<ProductFrame product={{ name: 'پایش برند' }} nav={nav} pathname={pathname} linkComponent={FrameLink}>
<Outlet />
</ProductFrame>
)
}مسیرهای بیرون از قاب
قاب مال صفحههایی است که کاربرِ واردشده در آنها کار میکند. این مسیرها بیرون از آناند و layout خودشان را دارند:
| مسیر | چرا بیرون از قاب | چطور |
|---|---|---|
| ورود، ثبتنام، بازیابی رمز، پذیرفتن دعوت | کاربر هنوز وارد نشده و هیچ مقصد منو برایش باز نیست | گروه مسیر جدا، بدون قاب |
| منبع PDF که سرور میسازد (سرویس تبدیل HTML به PDF) | سندی برای کاغذ است، نه صفحهای برای کار | layout جدا با پالت روشن (data-theme="light" روی <html>)؛ کامپوننتهایی که سند لازم دارد (نمودار، جدول، BulletinViewer)، بدون قالب صفحه |
| اسلاید یا ارائهٔ تمامصفحه | بوم ثابت 16:9 است و هیچ ناوبری برنامه در آن نیست | گروه مسیر جدا |
| ویجت جاسازیشده، مینیاپ پیامرسان | میزبان (صفحهٔ دیگر یا پیامرسان) خودش قاب را دارد | گروه مسیر جدا؛ فقط صفحه |
| صفحهٔ اشتراک عمومی یک گزارش | خواننده کاربر برنامه نیست | گروه مسیر جدا |
چاپ صفحهای که کاربر در برنامه میبیند بیرون از قاب نیست: همان صفحه است و قاب خودش آن را برای چاپ آماده میکند
(چاپ). صفحهای که همان مسیر برنامه است و گاهی روی نمایشگر بیکاربر دیده میشود هم بیرون از قاب نیست:
chrome={useKioskChrome()}.
در Next.js App Router هر گروه، layout خودش را دارد و فقط گروه برنامه قاب را رندر میکند:
app/
├── layout.tsx ← <html>، <body>، ThemeProvider و Toaster؛ بدون قاب
├── (app)/
│ ├── layout.tsx ← <Frame>{children}</Frame>: ProductFrame، یک بار
│ ├── page.tsx
│ └── mentions/page.tsx
├── (auth)/login/page.tsx ← بدون قاب
└── (share)/r/[id]/page.tsxبا react-router هم همین است: قاب در یک layout route است (<Route element={<Frame />}>) و مسیرهای بیرون از قاب کنار آن
تعریف میشوند، نه زیر آن.
راهنمای استفاده
بکنید
ProductFrameرا یک بار در layout بخش واردشدهٔ برنامه بگذارید وpathnameوlinkComponentمسیریاب را به آن بدهید (Linkدر Next.js همانطور که هست؛Linkدر react-router باcreateRouterLink).- همهٔ
hrefها را از ریشه بنویسید (/reports) و به هر مقصد، درnav، زیرمنوها وnavFooter، شناسهٔ یکتا بدهید؛ در محیط توسعه، شناسهٔ تکراری،hrefنسبی وactiveIdناشناخته هشدار میدهند. - هر صفحه را یک قالب از
@partodata/ui/templatesکنید (جدول انتخاب قالب صفحه)؛ اقدام اصلی صفحه درprimaryActionهمان قالب است و قالب جایش را تعیین میکند. - مقصدها را به ترتیب کاربرد مرتب کنید و بخشهای مدیریتی را در یک گروه با عنوان جدا کنید.
navModeرا یک بار برای محصول انتخاب کنید: پیشفرضrailاست؛menuفقط وقتی برچسبها باید همیشه دیده شوند، وnoneوقتی شرح کار بگوید محصول تکبخشی است و منو ندارد.ThemeToggleرا درactionsیا جای دیگری از قاب نگذارید؛ جای کلید تم راthemeToggleتعیین میکند (قاعدهٔ lintparto/theme-toggle-placement).localeرا یک بار روی قاب بدهید؛ قالبهای صفحه و همهٔ جزءهای داخل قاب و صفحه از آن پیروی میکنند. عنصر ریشهlang={locale}وdir={locale === 'en' ? 'ltr' : 'rtl'}دارد (Toasterزبان را از آن میخواند).footer،printHeader، اقدام «چاپ» وchromeرا فقط وقتی بگذارید که شرح کار آنها را بخواهد؛chromeهمیشهchrome={useKioskChrome()}است.
نکنید
- عنوان صفحه را در نوار بالا نگذارید؛ قاب جایی برای آن ندارد و هر صفحه فقط یک
h1دارد. - منوی حساب را در
actionsنگذارید؛ جای آنuserاست. - اقدام اصلی یک صفحه را در
actionsقاب نگذارید؛ جای آنprimaryActionهمان صفحه است. - به ناحیهٔ محتوا padding یا
max-w-*اضافه نکنید و صفحه را باdivوp-6نپوشانید؛ قالب صفحه همین را یک بار انجام میدهد.PageContainerوPageHeaderرا هم خودتان در صفحه نگذارید: اجزاییاند که قالبها رویشان ساخته شدهاند. - منوی دوم (پنل کناری ثانویه) نسازید؛ یک سطح زیرمنو (
children) یا گروهها کافی است. - برای قاب برنامه
NavRailدستساز به کار نبرید. - صفحهٔ ورود، اسلاید یا منبع PDF سرور را داخل قاب نگذارید و قاب را برای آنها پنهان نکنید؛ آنها بیرون از layout قاباند.
- برای چاپ، قاب را با CSS خودتان پنهان نکنید و ناحیهٔ چاپ جدا نسازید؛ قاب خودش صفحه را برای چاپ آماده میکند.
- عنوان صفحه یا دکمه را در
printHeaderیاfooterنگذارید، و نسخه، شناسهٔ ساخت، سازمان یا تاریخی را که شرح کار نداده در آنها ننویسید. chrome="none"را ثابت ننویسید و برای نمای کیوسک پرسوجو، مسیر، متغیر محیطی یا prop خودتان را نسازید؛useKioskChrome()(قاعدهٔparto/page-templateهر دو را نشان میدهد).localeرا روی قالبها و جزءهای داخل صفحه تکرار نکنید؛ فقط صفحه یا جزئی که واقعاً زبان دیگری داردlocaleخودش را میگیرد.- جدول صفحه را در CSS یا آزمون خود با
main tableنگیرید: صفحه داخل جدول نمایشی قاب است؛[data-slot="table"]یا نقش جدول را بگیرید.
مهاجرت از AppShell
AppShell در 5.0 حذف شد. محصولی که هنوز رویش است، پیش از ارتقا به ^5 این جدول را دنبال کند. جای هر ویژگی آن:
AppShell | ProductFrame |
|---|---|
nav (فهرست تخت، href اختیاری) | nav: گروهها { id, label?, items }؛ هر مقصد href لازم دارد و میتواند badge و children داشته باشد |
activeId | pathname (ترجیحی) یا activeId |
onNavigate (نماهای وابسته به state) | ندارد: صفحهها لینکاند؛ مسیریاب به کار ببرید |
linkComponent، banner، contained | همانها |
brand، headerStart | product ({ name, logo, href }) |
header.actions، headerEnd | actions؛ منوی حساب در user؛ اقدام اصلی یک صفحه به primaryAction همان صفحه میرود |
header.title، header.subtitle، header.breadcrumbs | ندارد: عنوان و راه بازگشت صفحه (title، back) در قالب خود صفحهاند |
pageHeader | ندارد: سرِ صفحه را قالب هر صفحه میسازد |
navFooter (هر ReactNode) | navFooter: مقصدها (همان شکل items) |
secondaryPanel، secondaryResizable | زیرمنوی children؛ محتوای آزاد به خود صفحه یا یک Sheet میرود |
contentWidth | عرض قالب صفحه (پیشفرض قالب، یا width با نام) |
navBehavior، onNavBehaviorChange | navMode (rail پیشفرض، رفتار AppShell؛ menu انصراف)؛ جفت کنترلشده فقط برای نمونهها و embed |
headerSize، navSide، hideNavBehaviorToggle، className | ندارد: ارتفاع، سمت منو و کنترل حالت منو ثابتاند |
در صفحهها: <PageContainer inset="none" size="full"> و PageHeaderی که زیر AppShell لازم بود را با قالب همان صفحه
عوض کنید (ListPage، DetailPage، FormPage…؛ انتخاب قالب صفحه).
Props
ProductFrame
ProductFrameNavGroup
ProductFrameNavItem
ProductFrameNavItem همهٔ ویژگیهای ProductFrameNavLink را دارد، بهعلاوهٔ:
ProductFrameNavLink
ProductFrameProduct
createRouterLink
function createRouterLink(
Link: React.ElementType<{ to: string }>
): React.ForwardRefExoticComponent<ProductFrameLinkProps & React.RefAttributes<HTMLAnchorElement>>لینکی را که to میگیرد (Link در react-router) به linkComponent قاب تبدیل میکند، که href میگیرد
(ProductFrameLinkProps: href و ویژگیهای لینک). یک بار، در سطح ماژول، بسازید. نشانی با scheme لینک ساده میماند.
خروجی مؤلفهای با forwardRef است که بقیهٔ ویژگیها (aria-*، onClick، className) را به Link میدهد، پس کلیک
روی مقصد غیرفعال منو هنوز مسیر را عوض نمیکند.
useKioskChrome
function useKioskChrome(): 'full' | 'none'تنها راه تعیین chrome: وقتی نشانیای که زبانهٔ مرورگر برنامه را با آن باز کرده kiosk=1 دارد (/live?kiosk=1)،
none و در غیر این صورت full. یک بار، وقتی قاب در مرورگر mount میشود، خوانده میشود و تا زبانه در برنامه است
میماند. روی سرور و در اولین render مرورگر full است، پس hydration جور است و قاب بیآنکه صفحه دوباره mount شود جابهجا
میشود. hook مسیریابی به کار نمیبرد و در layout ریشهٔ ایستای Next.js و layout route در react-router کار میکند.
useFrameScroll
function useFrameScroll(): { ref: RefObject<HTMLElement | null>; scrollTo: (target: number | ScrollToOptions) => void }ظرفی که صفحهٔ قاب در آن اسکرول میشود (main قاب؛ پنجره هرگز اسکرول نمیشود، پس window.scrollTo، window.scrollY و شنوندهی scroll روی window در قاب کاری نمیکنند). ref فقط خواندنی است (به عنصری وصل نکنید). scrollTo فوری اسکرول میکند مگر behavior: 'smooth' بدهید؛ بیرون از قاب پنجره را اسکرول میکند.
useInProductFrame
useInProductFrame(): boolean — در صفحهای که ProductFrame رندر میکند (children آن) true است؛ در actions،
user، banner، footer، printHeader، داخل پنجرهها و پنلهای رویی و بیرون از قاب false است.
دسترسیپذیری
- اولین توقف Tab پیوند «پرش به محتوای اصلی» است؛ با فوکوس دیده میشود و فوکوس را به
mainمیبرد. همهٔ نامها و رشتههای قاب بهlocaleآناند. باchrome="none"نه نوار و منو هست و نه پیوند پرش؛ فقطmain(وbanner، اگر داده باشید). - عنوان تب با هر صفحه عوض میشود (2.4.2) و پس از تغییر مسیر فوکوس به
h1میرود یا عنوان در یک ناحیهٔaria-live="polite"اعلام میشود (2.4.3، 4.1.3)؛ حلقهٔ فوکوس رویh1رسم نمیشود چون فوکوس برنامهای است وtabindexهنگام خروج فوکوس برداشته میشود - لندمارکها:
headerبرای نوار بالا،navبا نام «ناوبری اصلی» برای منو (روی موبایل هم، داخل پنل)،mainبرای محتوا، وbannerناحیهای با نام «اطلاعیه». گروههای عنواندار منوrole="group"با همان عنوان دارند. - مقصد فعال
aria-current="page"دارد. روی نوار آیکونی جمعشده، برچسب هر مقصد نام دسترسپذیر و tooltip آن است، وbadgeدر نام لینک هم میآید. وقتی منو باز است tooltip باز نمیشود، پس برچسب دو بار خوانده نمیشود. - در حالت
rail، فوکوس صفحهکلید (:focus-visible) نوار را مثل اشارهگر باز میکند، پس برچسبها و زیرمنو بدون موس هم در دسترساند؛ تا فوکوس داخل نوار است، خروج اشارهگر آن را نمیبندد. سنجاق «نوار ناوبری همیشه باز» دکمهٔ تغییر وضعیت باaria-pressedاست، و کلید تم هم («تم تاریک»، فشرده در تم تاریک). - نوار بالا هیچ عنوانی (
h1تاh6) ندارد؛h1هر صفحه عنوان قالب همان صفحه است. footerداخلmainو بعد از صفحه است. سربرگ چاپ روی صفحه نمایش داده نمیشود و صفحهخوان آن را نمیخواند. صفحه، سربرگ و خط پایانی در یک جدول نمایشی (role="presentation") قرار دارند که روی صفحه هیچ جعبهای نمیسازد (display: contents) و فقط در چاپ جدول است، تا سربرگ و خط پایانی در هر برگ تکرار شوند. این جدول همیشه هست (با یا بیfooterوprintHeader)، تا تغییرchromeیا رسیدن خط پایانی صفحه را دوباره mount نکند؛ در درخت دسترسپذیری نیست، اما در CSS و آزمونها جدول صفحه را با[data-slot="table"]یا نقش جدول بگیرید، نهmain table.- روی موبایل منو یک پنل کناری با عنوان دسترسپذیر است. دکمهٔ «منوی ناوبری»
aria-expandedدارد؛ با باز شدن پنل فوکوس روی مقصد فعال (یا اولین مقصد) میرود، و باEscapeیا دنبال کردن یک مقصد، پنل بسته میشود و فوکوس به همان دکمه برمیگردد. کنترل حالت منو فقط روی دسکتاپ است.
کامپوننتهای مرتبط
- انتخاب قالب صفحه — هر صفحه داخل قاب یک قالب است (
ListPage،DetailPage،FormPage،SettingsPage،DashboardPage،UtilityPage،CustomPage). PageContainerوPageSection،PageHeaderوPageToolbar— اجزایی که قالبها رویشان ساخته شدهاند؛ مستقیم فقط داخل محتوای یکCustomPage.- هندسهٔ صفحه — ارتفاع نوار، عرض منو و حاشیهها از همین توکنها میآیند.
- دامنهٔ سیستم طراحی — قاب و قالبها لایهٔ برنامهاند، فقط برای برنامههای دادهای؛ سایت بازاریابی و
مسیرهای بیرون از قاب (منبع چاپ یا PDF، اسلاید) لایهٔ پایه و کامپوننتها را به کار میبرند؛ صفحهٔ ورود
AuthPageاست و نسخهٔ منشعب فقط لایهٔ پایه را. UserMenu— در قاب بدون منو (navMode="none")، مقصدهای دیگر محصول موردهای همین منواند (onSelectبا مسیریاب جابهجا میشود).