Skip to content

سیستم طراحی — `packages/ui`

This content is not available in your language yet.

فاز ۳ پلن نسل سوم. اجزای رابط روی React Aria Components و توکن‌ها از یک منبع. هر جزء در Storybook در چهار حالت × سه پوسته × دو جهت دیده و با axe اجرا می‌شود (۵.۸).

  • منبع: packages/ui/src/themes.ts (رنگ و سایهٔ هر پوسته) و tokens.ts (مقیاس‌ها، نردبانِ لایه، رنگِ بی‌پوسته). پروندهٔ tokens.css از این دو تولید می‌شود (pnpm format:tokens؛ pnpm verify ناهماهنگی را می‌گیرد). دستی ویرایش نکن. هیچ رنگ، لایه یا اندازه‌ای بیرون از این دو نیست — نگهبان‌ها پایین‌تر.
  • رنگِ هر پوسته (THEMES، از مقیاسِ خنثی و کهربایی ده‌پلهٔ NEUTRAL/AMBER): زمینه، پنل، خط، hover/active؛ متن در سه نقش — --text، --text-secondary و --text-label (برچسبِ ریز، پرکنتراست‌تر از متنِ دوم چون ریز است؛ نام‌های کهنهٔ --muted/--dim نقش را برعکس می‌گفتند و css-vars.mjs ردشان می‌کند)؛ --accent با --accent-hover و --accent-ink؛ خطر/هشدار/اطلاع/درست؛ یک --focus برای کیت و اپ.
  • روی بوم و صحنه: --surface-overlay (پنلِ ۸۸٪ برای هر نوارِ شناور)، --scrim (پشتِ پنجره)، --elevation-1..3 (سایهٔ هر پوسته: سیاه در تیره، جوهرِ کم‌رنگ در روشن)، --paper و --paper-wall (کاغذ و رویهٔ دیوار) و خانوادهٔ --ink-* (جوهر روی کاغذ، جدولِ پایین)، --scene-top/--scene-bottom (زمینهٔ صحنهٔ سه‌بعدی).
  • بی‌پوسته (زمینه‌شان پوسته نیست): --media-mark/--media-mark-core (نشانه روی عکسِ کاربر و صحنه)، --media-backdrop (سیاهِ پشتِ دوربین)، --print-desk/--print-paper (پیش‌نمای چاپ — کاغذ در هر پوسته سفید است).
  • نردبانِ لایه: --z-overlay-bar ۵ · --z-menu ۲۰ · --z-modal ۴۰ · --z-drag ۶۰، و --z-toast ۱۰۰٬۱۰۰ که تنها پلهٔ بیرونِ نردبان است (پرتالِ React Aria، بالاتر). هیچ z-index ِ عددی بیرون از این.
  • رنگِ نمودار: --chart-1..6 — شش سری، هر کدام ≥ ۳ روی هر سطح و هر دوتای پیاپی در روشنی هم متفاوت (چاپِ سیاه‌وسفیدِ کارگاه، و کسی که سرخ و سبز را جدا نمی‌کند). شبکه و محور همان --line و --line-2 اند.
  • فاصله ۴/۸/۱۲/۱۶/۲۴/۳۲/۴۸ (--space-1..7)، شعاع ۴/۶/۱۰/۱۴ (--radius-sm..xl) و --radius-pill (دو سرِ گرد برای هر ارتفاعی — نشان، شمارنده، شیار؛ به‌جای نیمِ ارتفاعِ هر کدام)، حرکت ۱۲۰/۲۰۰/۳۲۰ میلی‌ثانیه با یک منحنی که با prefers-reduced-motion صفر می‌شود.
  • قلم نسبت به --font-base: --font-xs..display = پایه −۲، −۱، ۰، +۲، +۶، +۱۰، +۱۸؛ با پایهٔ ۱۴ همان ۱۲…۳۲ و در کارگاه (پایهٔ ۱۶) همه دو پیکسل درشت‌تر. مقیاس در هر بلوکِ پوسته تکرار می‌شود: متغیرِ var() ‌دار روی عنصری حساب می‌شود که تعریفش آنجاست و [data-theme] ِ تودرتو بی تکرار مقیاسِ :root را به ارث می‌برد. زیرِ ۱۲ نیست: e2e/theme.spec.ts هر متنِ دیدنیِ رابط را در تیره (≥ ۱۲) و کارگاه (≥ ۱۴) می‌سنجد — پیش از فاز ۴، ۸۵ و ۷۵ متن زیرِ این مرز بودند چون styles.css پیکسلِ ثابت داشت.
  • متنِ نقشه (--drawing-text-rule|dim|note|live|plan-note|plan-label = ۴۰/۴۴/۴۸/۵۴/۱۰۰/۱۱۰) به واحدِ خودِ نقشه است — میلی‌مترِ دیوار، نه پیکسل — پس با پوسته و --font-base عوض نمی‌شود: برچسبِ ارتفاعِ ردیف و بازشو، عددِ درجا، یادداشتِ مانع و پلانِ اتاق. عددِ خط‌کش و زنجیرهٔ اندازه، راهنمای چفت، حبابِ اندازهٔ زنده و عددِ نشانِ دستیار از ۷.۱ روی روکشِ فضای صفحه به پیکسل‌اند (wall-canvas/ScreenOverlay.tsx، PX در screen-layout.ts).
  • سه پوسته: dark (:rootlight و workshop ([data-theme='…'])، هر کدام با color-scheme ِ خودش — نوار پیمایش و کنترلِ بومی پیش از JS هم رنگِ درست دارند. کارگاه روشن و پرکنتراست است با قلم پایهٔ ۱۶ و هدف لمسی ۴۸ (--font-base، --touch). انتخاب با applyTheme('system' | …)؛ ذخیره در localStorage (darzsaz.theme). پیش‌فرض «هماهنگ با سیستم» — تا فاز ۴ ِ نسل پنجم تیره بود، چون رنگِ بیرون از توکن در روشن متنِ تیره روی نوارِ تیره می‌ساخت؛ حالا axe هر مسیر را در تیره و روشن بی نقض می‌بیند.
  • پیش از رنگ: prePaintScript() (theme.ts) همان تصمیمِ applyTheme(readThemeChoice()) است به‌صورتِ اسکریپتِ درون‌خطیِ index.html، پیش از هر CSS و JS (vite.config.ts، با هشِ CSP)؛ پیش‌تر پوسته در افکتِ UiProvider می‌نشست و کاربرِ روشن در هر بار باز کردن صفحهٔ تیره می‌دید. test/theme.dom.test.tsx برای هر مقدارِ ذخیره‌شده برابریِ اسکریپت و TS را می‌سنجد و e2e/theme.spec.ts صفحه را با اسکریپتِ برنامهٔ نگه‌داشته. theme-color دو تاست (برای هر طرحِ رنگِ سیستم، از توکن) و applyTheme هر دو را به پوستهٔ انتخابی می‌برد؛ رنگِ مانیفست THEMES.dark.bg.
  • ارقام هم‌عرض (font-variant-numeric: tabular-nums) فقط روی جدول، فیلد عددی و عددِ زنده — نه روی ریشه، که متن روان را باز می‌کرد. ارقام فارسیِ پیش‌فرض هم‌عرض نیستند و عددی که زنده عوض می‌شود می‌رقصد. فونت: docs/reference/fonts.md.
  • جزء کیت چیدمانِ بیرونی تحمیل نمی‌کند. پهنا را ظرف تعیین می‌کند (Slider دیگر width: 100% ندارد).
  • لایه‌های آبشار reset, tokens, kit, app (۶.۱۰، apps/web/vite/css.ts): هر CSS در زمانِ ساخت به لایهٔ جایش می‌رود — packages/ui/src/tokens.css در tokens، ماژول‌های کیت در kit، هر CSS ِ apps/web/src در app؛ بازنشانیِ styles.css (* ، button، حلقهٔ تمرکز، حرکتِ کمتر) @layer reset ِ صریح دارد و styles.css جز آن هیچ کلاسی ندارد (۹۰۶ ← ۷۱ خط): هر کلاس در ماژولِ کنارِ جزءِ خودش، کمکیِ مشترک (sectionLabel، fieldHint، card، empty، grow، vr، «صحنه + ستونِ کناری») در styles/util.module.css. نامِ کلاسِ رشته‌ای بی‌صدا به جایی نمی‌رسید — چهار "sectionLabel sectionTight" هرگز تنگ نشدند — و test/global-classes.test.ts هر رشته‌ای را که در styles.css نیست می‌گیرد؛ قلابِ e2e نقش و نام یا data-* است (data-unit-id، data-scene، data-scene-tools، data-status). ترتیب را <style> ِ سربرگِ index.html پیش از هر <link> اعلان می‌کند. پیش‌تر CSS ِ کیت در تکهٔ تنبل بعد از CSS ِ برنامه می‌آمد و className با همان ویژگی‌مندی در بیلد می‌باخت (شیارِ «برش» ِ نوار سه‌بعدی ۱۷ پیکسل)؛ حالا برنامه هر ترتیبِ تکه‌ای بر کیت می‌چربد. !important ِ برنامه در لایه برعکس می‌شود (مهمِ لایهٔ پایین‌تر برنده است) — برای بردن بر کیت لازم نیست. نامِ کلاسِ ماژول _<محلی>_<هشِ مسیر> است (نه شمارهٔ سطر): CSS ۲۱٫۴ ← ۲۰٫۵ کیلوبایت gzip. قاعدهٔ برنامه روی جزء کیت یک کلاس است: سلکتورهای دوکلاسی (.parts .part، .wrap .cell) و !important ِ ماژول‌ها که برای بردن از CSS ِ دیرتر-بارِ کیت بودند برداشته شدند (!important ۲۰ ← ۵: چهار در «حرکتِ کمتر» ِ بازنشانی و یکی در چاپِ report). رقیبِ هم‌لایه با ویژگی‌مندیِ صفر حل می‌شود، نه با ترتیبِ تکه‌ها: پایه‌های util.module.css (:where(.sectionLabel)، :where(.card)، …) بر گونه‌ها و کلاسِ ماژولِ هر جزء می‌بازند.
  • متن Checkbox یک جعبه است (.text): برچسب و توضیحش با هم می‌پیچند و div ِ راهنما خطِ خودش را می‌گیرد. پیش از این دو آیتمِ فلکسِ کنار هم بودند.
  • کنتراست آزمون دارد (test/contrast.test.ts)، هر جفت روی زمینهٔ واقعی‌اش: متن و رنگ‌های معنایی روی هر شش سطح ≥ ۴٫۵؛ برچسب ریز روی سطح ثابت ≥ ۷؛ متن روی دکمهٔ رنگی، آرام و زیر اشاره‌گر ≥ ۴٫۵؛ نشان‌های info/ok/warn ≥ ۴٫۵؛ حلقهٔ تمرکز روی هر سطحِ ثابت ≥ ۳؛ متن روی --surface-overlay ِ ترکیب‌شده با سیاه و سفیدِ مطلق (صحنه هر رنگی می‌تواند باشد) ≥ ۴٫۵؛ جوهرِ متن روی کاغذ و رویهٔ دیوار ≥ ۴٫۵ و جوهرِ خط ≥ ۳. رنگِ نیمه‌شفاف با composite روی زمینه نشانده می‌شود؛ luminance خودِ رنگِ شفاف را نمی‌پذیرد. رنگی که آزمون را بیندازد وارد نمی‌شود.

کاغذِ بوم، پلان، پلانِ کوچکِ تنظیمات، پیش‌نمای سازنده، نمادِ کتابخانه و بندانگشتیِ خانه در هر پوسته روشن است (تیره #efece4، روشن و کارگاه سفید)، پس نقشه جوهرِ خودش را دارد، نه رنگِ رابط: کهرباییِ --accent ِ پوستهٔ تیره روی کاغذ ۱٫۸ می‌شد. رنگ از کلاسِ CSS (ماژول‌های wall-canvas/drawing.module.css، wall-canvas/screen-overlay.module.css، wall-plan.module.css، unit-shapes.module.css و plan-canvas.module.css)، اندازه و ضخامتِ خط در خودِ SVG.

توکن کجا کمینه روی کاغذ و دیوار
--paper-wall رویهٔ دیوار، کفِ پلان، خانهٔ سازنده
--ink پلاکِ حبابِ اندازهٔ زنده (با متنِ --paper) ۴٫۵
--ink-strong کف، پاخور، دیوارِ پلان، طولِ کلِ دیوار، برچسبِ پلان ۴٫۵
--ink-dim زنجیرهٔ اندازه، طولِ دیوار در پلانِ کوچک، دستگیرهٔ نماد ۴٫۵
--ink-label عددِ خط‌کش، «سقف»، عرضِ یونیت، برچسبِ یونیتِ قفل، نشانِ بیننده ۴٫۵
--ink-rule خط‌کش، خطِ سقف، دورِ یونیتِ پلان و نماد، صفحهٔ کار ۳ (خط)
--ink-grid شبکه و هاشورِ جای خالی دیده‌شدنی، کمتر از خط
--ink-info بازشو، خطِ ارتفاعِ ردیف، مانعِ عادی، نشانِ اطلاع ۴٫۵
--ink-danger مانعِ «در دسترس بماند»، سرریز از دیوار، نشانِ خطا ۴٫۵
--ink-selection یونیت و دیوارِ انتخاب‌شده، دستگیره، راهنما، کادرِ انتخاب، نشانِ هشدار ۴٫۵
--ink-door درِ پلان ۳ (خط)

نشانِ دستیار عددِ --paper روی دایرهٔ جوهری است — همان جفتِ آزموده. بندانگشتیِ خانه دیگر در پایگاه نیست (ارتقای نسخهٔ ۴ ِ Dexie ستونِ thumbnail را برداشت) و فهرست آن را از خودِ پروژه می‌سازد: ردیفِ قدیمی رنگِ پخته داشت.

  • هر نوارِ شناور روی بوم و صحنه — .view3dBar (OverlayBar ِ کیت)، .sceneTools، نوارِ نما (view-controls.module.css) و نوارِ مانع (wall-canvas/obstacle-bar.module.css) — --surface-overlay با --elevation-2 و blur است؛ .photoHint هم --surface-overlay است، بی سایه و blur. پیش‌تر سه تایش زمینهٔ تیرهٔ سخت‌کد داشت و در پوستهٔ روشن متنِ تیره رویش ۱٫۰۵ می‌شد.
  • پردهٔ Dialog ِ کیت (همهٔ پنجره‌های اپ از ۵.۵) --scrim است و لایه‌اش --z-modal. تور Popover ِ کیت کنارِ هدفش است (۶.۸) و پرده ندارد.
  • منوی «نمایش» ِ نوارِ ابزارهای سه‌بعدی (SceneTools) Menu ِ کیت است (۶.۴)، پاپ‌اورِ React Aria با لایهٔ خودش؛ منوی دست‌سازِ پیشین لایه‌اش درونِ لایهٔ نوار بود. امروز هیچ CSS ی در اپ و کیت --z-menu نمی‌خواند.
  • فهرست‌های React Aria لایهٔ خودشان را دارند (zIndex: 100000 ِ درون‌خطی از useOverlayPosition، روی پرتالِ document.body) — عددی که نردبانِ برنامه به آن نمی‌رسد. اعلان باید روی منوی باز خوانده شود (پذیرشِ ۴٫۸)، پس --z-toast تنها پلهٔ بیرونِ نردبان است: ۱۰۰٬۱۰۰، درست بالای همان پرتال. story ِ «اعلان روی منوی باز» در مرورگرِ واقعی با elementFromPoint می‌سنجدش.
  • حلقهٔ تمرکز همه‌جا --focus. روی بوم حلقه روی قابِ کاغذ است (.canvas:has(svg[role='application']:focus-visible)): outline ِ خودِ SVG را overflow: hidden ِ قاب می‌بُرید و Tab روی بوم هیچ نشانه‌ای نداشت.

رنگی که با پوسته عوض نمی‌شود توکن نیست و در apps/web/src/scene/materials.ts است، هر مقدار با دلیلش: جنس و نورِ سه‌بعدی (استیلِ هود در پوستهٔ روشن همان استیل است)، پیش‌تنظیمِ رنگِ دیوار و کف که در پروژه ذخیره می‌شود، جنسِ ناشناخته، و زمینه و ویگنتِ رندرِ آفلاین. نشانه روی صحنه و عکس MEDIA ِ توکن‌هاست. روکشِ پیش‌فرضِ اتاق DEFAULT_FINISHES ِ هسته.

زمینهٔ صحنه دو گرادیان در lib/env.ts است. نمای زنده sceneBackdrop(theme) از sceneTop/sceneBottom ِ پوستهٔ نشستهappliedTheme() و onThemeApplied() ِ کیت همان صفتی را می‌خوانند که applyTheme می‌نویسد، پس «هماهنگ با سیستم» و صفحهٔ کارگاه هم (Studio با useSyncExternalStore). رندرِ آفلاین backdropTexture() با RENDER_BACKDROP، در هر پوسته همان: عکسی است که بیرون می‌رود. بافت از نگاشتِ ACES می‌گذرد و پیکسل عینِ توکن نیست — گوشهٔ بالای صحنهٔ بی‌دیوار: تیره (۷۴، ۸۵، ۹۴)، روشن (۲۱۴، ۲۱۳، ۲۱۱)، کارگاه (۲۱۹، ۲۱۹، ۲۱۹)؛ پیش از فاز ۴ در هر سه همان تیره (درخشندگی ۰٫۰۸۱). گرادیانِ CSS زیرِ بومِ شفاف عینِ توکن می‌داد ولی نمای تیره را از رندر جدا می‌کرد. آزمون: e2e/theme.spec.ts (تعویضِ پوسته از منو) و e2e/render.spec.ts (رندر در پوستهٔ روشن گوشهٔ تیره دارد).

  • Stylelint (stylelint.config.mjs، pnpm lint:css) روی هر CSS ِ اپ، کیت، Storybook، سایت مستندات و برگهٔ چاپی:
    • جهت: ویژگی و مقدارِ فیزیکیِ محورِ خط (left، margin-right، text-align: right، float) با stylelint-use-logical، و کوتاه‌نویسِ چهارمقداری (padding: 0 8px 0 12px) که راست و چپ را جدا می‌دهد. محورِ بلوک و پهنا آزادند: هر دو زبان افقی‌اند.
    • رنگ فقط از توکن: declaration-strict-value برای رنگ، زمینه، fill و stroke؛ color-no-hex، color-named و function-disallowed-list برای رنگ در هر ویژگیِ دیگر (سایه، گرادیان، کوتاه‌نویسِ حاشیه).
    • z-index، اندازهٔ قلم، فاصله و شعاع فقط var(--*) یا تابع (calc())؛ استثنا 0/auto در margin و padding، 0 در gap، 0/50% در شعاع و inherit در قلم — در z-index هیچ؛ !important ممنوع. قلم و شعاعِ خام در اپ و کیت صفر است؛ از فاصله فقط آنچه درست روی پله بود توکن شد. بقیه (۸۲ اعلان در اپ، کیت و Storybook، بیشترشان ۶، ۱۰ و ۱۴ پیکسل — درست میانِ دو پله؛ ۹۱ ِ دیگرِ این قاعده در sheet.css ِ برگهٔ چاپی) در فهرستِ پایه مانده: گرد کردنشان تصمیمِ تراکم است و آرت‌بوردهای فاز ۶ همین عددها را دارند.
    • خاموش‌کردنِ قاعده فقط با -- دلیل؛ خاموش‌کردنی که دیگر چیزی را خاموش نمی‌کند خطاست.
  • فهرستِ پایه stylelint-suppressions.json (سازوکارِ خودِ Stylelint): آنچه روزِ آمدن سر می‌پیچید، به تفکیکِ پرونده و قاعده. scripts/css-lint.mjs سه چیز را هم می‌سنجد: رشد نکند، کهنه نماند (درست شد ← node scripts/css-lint.mjs --update)، و جمعِ هر قاعده از main بزرگ‌تر نباشد. آزمون‌ها: scripts/test/stylelint-config.test.mjs، css-lint.test.mjs.
  • برگهٔ چاپی پروندهٔ packages/report/src/sheet.css است؛ sheet-css.ts از آن ساخته می‌شود (pnpm format:report-css) و test/sheet-css.test.ts ناهماهنگی را می‌گیرد. ?raw ِ Vite نشد: rolldown ِ خط فرمان بارش نمی‌کند.
  • scripts/css-vars.mjs می‌ماند: Stylelint هر پرونده را جدا می‌بیند و نمی‌داند متغیری که var() می‌خواند جای دیگری تعریف شده یا نه. نامِ بازنشسته (--dim، --muted، --shadow-N) را هم، حتی تعریف‌شده، رد می‌کند.
  • رنگ در TS/TSX: قاعدهٔ no-restricted-syntax (rawColors در eslint.config.js) هر رشته‌ای که hex یا تابعِ رنگ دارد را در اپ و کیت می‌گیرد — صفتِ JSX، شرط، style، قالبِ SVG، addColorStop. استثنا فقط packages/ui/src/{themes,tokens}.ts و apps/web/src/scene/materials.ts. آزمون: scripts/test/eslint-colors.test.mjs.

هر *.module.css ِ کیت یک .d.ts ِ تولیدی کنارش دارد با یک صادره برای هر کلاس (typed-css-modules، pnpm --filter @darzsaz/ui css:types) و جزء آن را با نامِ ایستا می‌خواند: import * as styles from './button.module.css' و styles.btn.

خطا که می‌گیرد پیش از این
کلاسِ ناموجود (s.chipx) TypeScript (TS2551) اعلانِ کلیِ *.module.css: هر نام string
ماژولِ بی .d.ts TypeScript (TS2307) — اعلانِ کلی برداشته شد بی‌صدا
کلاسِ بی‌مصرف knip، با cssModuleExports در knip.config.js دیده نمی‌شد
.d.ts ِ کهنه test/css-modules.test.ts (همان DtsCreator)
دسترسیِ پویا (s[variant]) همان آزمون، روی درختِ نحو
  • دسترسیِ پویا ممنوع است، چون knip آن را «کلِ ماژول مصرف‌شده» می‌خواند: کلاسِ ساختگیِ بی‌مصرف در button.module.css با styles[variant] پنهان ماند و همان در tabs.module.css گرفته شد. نقشهٔ صریح (VARIANT در Button.tsx، TONE در Display.tsx).
  • نامِ @keyframes هم صادره است (CSS Modules محلی‌اش می‌کند) ولی مصرفش animation ِ همان پرونده است؛ کامپایلرِ knip آن را @public می‌کند.
  • Vitest ماژولِ CSS ِ پردازش‌نشده را با export default new Proxy(…) عوض می‌کند که صادرهٔ نام‌دار ندارد: s.btn در آزمون undefined بود و هیچ آزمونی نمی‌افتاد. css.include در vitest.config.ts ِ کیت؛ برنامهٔ وب با مهاجرتِ ماژول‌هایش همان را می‌خواهد.
  • .d.ts در .prettierignore است: خروجیِ ابزار یک سطرِ خالیِ پایانی بیشتر دارد و Prettier ِ قلابِ pre-commit آن را از خروجیِ ابزار جدا می‌کرد — آزمونِ تازگی همان لحظه قرمز.
  • کلاسِ مرده در کیت: صفر. روزِ آمدنِ نگهبان هر ۸۴ کلاسِ هشت ماژول مصرف داشت؛ پس از اجزای ۵.۲ ۲۱۹ کلاس و ۷ نامِ @keyframes در ۲۲ ماژول، باز صفر. ۱۱ کلاسِ مردهٔ پلن (K25) در apps/web است و با تیپ‌دار شدنِ ماژول‌های برنامه دیده می‌شود — امروز هیچ‌یک از ماژول‌های apps/web .d.ts ندارد.

هر جزءِ کیت — تازه‌های ۵.۲ از روزِ اول و اجزای پیشین از بخشِ دومِ فاز ۵ — همین است؛ آزمونِ تیپ (test/labels.test.tsx) هر دو دسته را می‌سنجد.

چه قرارداد
برچسب label همیشه دیدنی، aria-label صریح؛ تیپ Labelled یکی از دو را الزام می‌کند و هر دو با هم را رد (test/labels.test.tsx). Checkbox/Switch: متنِ کنارِ کادر children است (ChildLabelled) — برچسب خودِ ناحیهٔ کلیک است، قراردادِ React Aria
نامِ نادیدنی Tabs، Menu، ContextMenu، DataTable، Popover، Badge: aria-label (K1) — پیش‌تر label بود که در پنج جزءِ دیگر برچسبِ دیدنی است
عنوانِ ظرف title (دیدنی و عنوانِ سند) با headingLevel؛ نوار (Toolbar، OverlayBar) نامِ دیدنی ندارد و aria-label ش اجباری است
خاموش isDisabled روی جزء و روی گزینه (Option، MenuEntry، TabItem، RadioOption، ToggleOption، OverlayBarItem، SwatchOption) — نه disabled (K3)
گونه variant = primary · secondary · quiet · danger؛ جزئی که همه را ندارد زیرمجموعه می‌گیرد (ToggleButton: secondary/quiet)
اندازه size = sm · md · lg از --touch (−۸، ۰، +۸)، هر کدام با کفِ --hit (پایین)
لحن tone برای نشان (StatusPill، Badge): neutral · info · ok · warn · danger
مقدار تکی value/onChange، مجموعه selectedKeys/onSelectionChange (ToggleButtonGroup ِ چندانتخابی، DisclosureGroup با expandedKeys)
ref روی عنصرِ نقش‌دار در همهٔ اجزا (K5): دکمه، کادر (NumberField، TextField، ComboBox، ورودیِ Checkbox/Switch/Slider)، دکمهٔ Select، tablist، menu، جدول، dialog؛ InlineEdit روی دکمهٔ حالتِ نمایش، FileTrigger روی دکمه نه ورودیِ پنهان. Slider فقط RefObject (inputRef ِ React Aria تابع نمی‌پذیرد)
تهی NumberField: value: number | null و onChange(null) برای کادرِ خالی — NaN ِ React Aria بیرون نمی‌آید. Select: emptyLabel گزینهٔ تهیِ اول می‌سازد و تیپِ onChange را string | null می‌کند؛ بی آن null فقط جای‌نماست (۵.۳، K4)
className روی جعبهٔ بیرونی؛ کیت چیدمانِ بیرونی تحمیل نمی‌کند (بالا). Switch و Checkbox آن را روی ردیفِ کلیک‌خوردنیِ درونِ فیلد می‌گذارند
متنِ مالِ کیت از کاتالوگ با شناسهٔ ui.* («انصراف»، «پاک کردن جست‌وجو»، «ابزارهای بیشتر») — هرگز رشتهٔ ثابت
رنگ و فاصله فقط توکن؛ رنگِ دادهٔ برنامه (نمونهٔ ColorSwatchPicker) درون‌خطی از React Aria، نه از کیت
دسترسیِ CSS نامِ ایستا از ماژولِ تیپ‌دار (s.quiet)، نقشهٔ صریح برای گونه — نه s[variant] (بالا)
سطحِ عنوانِ نو پیش‌فرضِ PanelHeader، ConfirmDialog و PromptDialog سطحِ ۲ است؛ Dialog ِ پیشین تا مهاجرت ۳ می‌ماند و Disclosure (درونِ بازرس) ۳

Button: نام‌های قرارداد، size="lg" و ref. نام‌های پیشینِ default و ghost با مهاجرتِ هر ۲۸ جای برنامه (quiet) برداشته شدند.

بخشِ دوم (API ِ شکننده، با جاهای برنامه در همان کامیت):

  • K2NumberField بی برچسب کامپایل نمی‌شود؛ تنها جای «هر دو با هم» (aria-label ِ «ارتفاع طبقهٔ N» کنارِ برچسبِ دیدنیِ «طبقهٔ N» در InteriorTab) برداشته شد و نام همان برچسبِ دیدنی است.
  • K3 — تغییرِ نامِ disabled به isDisabled در شیءِ ساخته‌شده با گسترش (...) یا تیپِ دست‌نوشته خطای تیپ نمی‌دهد: TypeScript فیلدِ اضافه را فقط در شیءِ تحت‌اللفظیِ تازه می‌گیرد، و گزینهٔ خاموش بی‌صدا روشن می‌شد. دو جا با جست‌وجو پیدا شد، نه با tsc: entryOf ِ app/commands.ts (فرمانِ خاموشِ منوی بالا) و overflowGroups ِ OverlayBar — حالا هر دو تیپِ MenuEntry را برمی‌گردانند.
  • ۵.۳ — ۲۹ نگهبانِ Number.isFinite و ۷ NaN ِ نشانه در ۱۶ پرونده (از ۳۰ و ۷؛ یکی تجزیه‌گرِ کاتالوگ است) به v !== null (که تیپ الزامش را می‌گیرد) و null رسیدند؛ کلیدهای ساختگیِ ' none' (SelectField، HardwareFields) به emptyLabel. ' unchanged' ِ cost/VariantsCard.tsx هنوز کلیدِ ساختگی است: پرونده ترجمه شده ولی به emptyLabel نرسیده.
  • onCommit ِ NumberField: پایانِ ویرایش با Enter یا بیرون رفتن، حتی بی تغییر، با مقدارِ ثبت‌شده. Enter روی keyup: onKeyDown ِ ما پیش از میان‌برِ Enter ِ React Aria (که ثبت می‌کند) اجرا می‌شد و مقدارِ پیش از تایپ را می‌داد.
  • چرخِ موس پیش‌فرض خاموش (isWheelDisabled، K12)؛ false ِ صریح روشنش می‌کند.
  • جای واحد با @container (بخشِ آخرِ ۵.۳): ریشهٔ کادرِ عددی ظرف است؛ پهن‌تر از 13em واحد درونِ کادر با زمینهٔ --panel-2 (متن رویش ≥ ۸٫۶۱ در سه پوسته)، باریک‌تر در ردیفِ برچسب (رونوشتِ aria-hidden) و واحدِ کادر فقط از دید پنهان (نه display: none، تا صفحه‌خوان یک بار بخواندش). container-type اندازهٔ ذاتی را صفر می‌کرد (دلیلِ نیامدنش)؛ contain-intrinsic-inline-size: 14em جایش را پر می‌کند. بی برچسبِ دیدنی واحد همیشه درونِ کادر. آزمون: number-unit.dom، kit-css؛ پذیرشِ «درز داخلی» ≥ ۴۸ در بازرسِ ۲۶۸: apps/web/e2e/number-unit.spec.ts (پروژهٔ app). مصرفِ ref و onCommit در برنامه با ۵.۵.
جزء روی نکته
Button، IconButton، SplitButton RAC Button گونه‌های قرارداد و سه اندازه (بالا)، ref؛ برچسب IconButton اجباری
NumberField RAC NumberField رقم فارسی از fa-IR؛ «۵۰۰» و «500» هر دو؛ تعهد در blur/Enter؛ پسوند واحد؛ مهار بازه؛ پله؛ null برای خالی؛ onCommit؛ چرخِ موس خاموش
TextField RAC TextField تک‌خطی یا multiline
Select، ComboBox RAC Select/ComboBox value/onChange رشته‌ای؛ گزینه‌ها { id, label, description?, isDisabled? }؛ emptyLabel برای گزینهٔ تهی (null)
Tabs RAC Tabs شمارندهٔ کوچک کنار برچسب (countfill ظرفِ ستونی را پر می‌کند و پنل پیمایش می‌خورد — بی آن اندازهٔ محتوا (K11)
Dialog RAC Modal/Dialog پنجرهٔ وسطِ صفحه؛ تلهٔ تمرکز، Escape، بازگشت تمرکز و پنهان‌کردن بیرون از React Aria؛ headingLevel (K9)؛ role="alertdialog"؛ ref به عنصرِ پنجره
Sheet RAC Modal/Dialog پنلی که از لبه می‌آید: from="start" پنلِ کناری، from="bottom" برگهٔ گوشی؛ size همان یک اندازه در راستای لبه؛ همان سربرگ و بدنه و پایینِ Dialog (۴٫۸)
NavRail، Sidebar یک فهرستِ جاها در دو شکل: ریلِ باریک (آیکون و یک واژه) و پنلِ پهن (گروه، سر و پا)؛ جای کنونی aria-current="page"، جایی که نشانی دارد پیوندِ واقعی است (۴٫۸)
Popover، Menu، Tooltip RAC منو با گروه، جداکننده، میان‌بر، گزینهٔ خطرناک، و گروه حالت‌دار (selection: رادیویی/چک‌باکس با aria-checked)
Switch، Checkbox، RadioGroup، Slider RAC *Field + *Button API تازهٔ ۱٫۲۱ (نه Switch منسوخ)
Chip گونهٔ اطمینان قیمت: market/relative/estimate
Badge، Kbd
EmptyState level ِ عنوان؛ live (ناحیهٔ زنده) پیش‌فرض همه‌جا جز level={1} که خودِ صفحه است (K9)
DataTable RAC Table ستونِ تیپ‌دار (value مقایسه می‌شود، render کشیده می‌شود)، مرتب‌سازی با Intl.Collator ِ زبانِ جاری، خالی/در حالِ بار در ناحیهٔ زنده، virtual برای هزاران ردیف (۴٫۸)
DateField، DatePicker RAC DateField/Calendar روز را YYYY-MM-DD ِ میلادی نگه می‌دارد و به تقویمِ زبان نشان می‌دهد (فارسی: جلالی)؛ بخش‌بخش، با تقویمِ بازشو (۴٫۸)
RollingNumber عددی که با تغییر می‌غلتد: بالا برای بیشتر (قرمز)، پایین برای کمتر (سبز) — شمارِ ورقِ نوارِ پایین (امضای ۴٫۴)
Toaster/toast RAC ToastRegion گوشهٔ پایینِ پایانِ خط (inset-inline-end)؛ نامِ ناحیه و دکمهٔ بستن از کاتالوگ (K8)؛ لحن، شرح و یک اقدام
ContextMenu RAC Popover + Menu منوی راست‌کلیک روی لنگرِ نامرئی در at؛ گزینه‌ها MenuEntry ِ Menu؛ جای لنگر در رندر (K7)
UiProvider، UiLocale RAC + Lingui زبان و جهت (پارامتر، نه ثابت)، پوسته، ناحیهٔ اعلان — ریشهٔ هر اپ؛ زبانِ Lingui را فعال نمی‌کند (پایین)

پیش از مهاجرتِ برنامه (۵.۵ و فاز ۶) آمدند و بیشترشان حالا در apps/web به کار رفته‌اند؛ هنوز نه: Stack، Inline، Grid، Separator، PanelHeader، StatusPill، Toolbar (فقط درونِ OverlayBarInlineEdit، Stepper، ProgressBar، Skeleton و SearchField. هر کدام story در پروندهٔ خانواده‌اش (چهار حالت) و آزمونِ DOM دارد؛ همه جز CommandList در گالریِ axe (stories/GalleryParts.tsx) هم هستند.

جزء روی نکته آزمون
Stack، Inline، Grid فاصله فقط پلهٔ توکن (gap ۰–۷)؛ as برای معنای سند (ul، section)؛ ستونِ Grid همیشه minmax(0, 1fr) یا کمینهٔ بُریده با min() layout.dom
Heading RAC Heading سطح و اندازه دو تصمیمِ جدا؛ پیش‌فرضِ اندازه از سطح layout.dom
Separator، VisuallyHidden RAC جداکنندهٔ عمودی در نوار؛ متنِ فقط برای صفحه‌خوان layout.dom
Icon، icons lucide فقط lucide (K21)؛ اندازه از قلم (1em)؛ بی نام تزئینی، با aria-label نقشِ img؛ icons به نامِ کار (جانشینِ icons.tsx و نویسه‌ها، ۵.۹)؛ flipInRtl برای back layout.dom، icons.dom
PanelHeader عنوان (h2)، خطِ دوم، کارها، sticky؛ div نه header — نشانه‌ها مالِ برنامه layout.dom
StatusPill tone با نقطه یا آیکون؛ ناحیهٔ زنده فقط با live (S7)؛ متن نمی‌بُرد (S15)؛ متن روی ته‌رنگ ≥ ۹٫۸۹ و نقطه ≥ ۵٫۳۵ در سه پوسته layout.dom، kit-css
Toolbar RAC Toolbar یک ایستِ Tab، کلیدهای جهت (در راست‌به‌چپ برعکس)، Home/End؛ aria-label اجباری toggle.dom
ToggleButton، ToggleButtonGroup RAC کلید با aria-pressed؛ گروهِ تکی radiogroup با aria-checked، بی انتخابِ خالی و بی رویدادِ تکراری روی گزینهٔ روشن؛ چندتایی toolbar؛ iconOnly با نام و راهنما؛ گروهِ قائم فهرستی به پهنای ستون است و برچسبش می‌پیچد، افقی یک‌خطی می‌ماند toggle.dom
SplitView react-resizable-panels جداکنندهٔ separator با aria-valuenow، کلید، Home/End، Enter؛ سهمِ درصدی فقط پس از کارِ کاربر؛ هدفِ کشیدن ۲۴ / ۴۴ (لمس) — پایین split-view.dom
Disclosure، DisclosureGroup RAC title با summary ِ یک‌خطی (فقط بسته) و badge؛ چند بخشِ باز؛ expandedKeys برای ماندگاری disclosure.dom
FileTrigger RAC FileTrigger دکمهٔ کیت، نه <label> دورِ ورودیِ پنهان (K18)؛ انصراف از پنجرهٔ سامانه صدا نمی‌زند actions.dom
ConfirmDialog، PromptDialog Dialog ِ کیت جای confirm/prompt (K17): alertdialog، تمرکزِ آغاز روی کارِ امن، کلیکِ بیرون نمی‌بندد؛ فرمِ واقعی، خالی خطا، از نو با هر باز شدن confirm.dom
InlineEdit RAC TextField دکمه با نامِ «برچسب + مقدار» ← کادر؛ Enter ثبت، Escape انصراف، بیرون رفتن ثبت بی دزدیدنِ تمرکز؛ خالی ثبت نمی‌شود actions.dom
Stepper RAC NumberField − و + در دو سو، ↑/↓، رقمِ زبان؛ چرخِ موس خاموش (K12) actions.dom
OverlayBar Toolbar + Menu ِ کیت روکشِ --surface-overlay؛ اولویت‌دار: آنچه در پهنا جا نشود از آخر به منوی ⋯ (ResizeObserver روی ریل و رونوشتِ پنهان) (S4) overlay-bar.dom
ProgressBar، Skeleton RAC ProgressBar درصد به رقمِ زبان، نامعین بی aria-valuenow؛ جای‌نگار همیشه پنهان از صفحه‌خوان، پویانمایی با prefers-reduced-motion ایستا inputs-extra.dom
ColorSwatchPicker RAC ColorSwatchPicker هر نمونه نامِ رنگ («گچیِ گرم») نه hex؛ رنگ همان رشتهٔ برنامه برمی‌گردد (نه #E4DFD3 ِ React Aria)؛ isDisabled روی فهرست و نمونه inputs-extra.dom
SearchField RAC SearchField searchbox، Escape پاک می‌کند، دکمهٔ پاک کردن با نام، Enter ← onSubmit inputs-extra.dom
CommandList RAC Autocomplete کادر + فهرستِ بخش‌دار با تمرکزِ مجازی (پالت فرمان)؛ تایپ اولین گزینه را برمی‌گزیند، Enter اجرا؛ پالایش و رتبه با برنامه؛ شناسهٔ گزینه جدا از شناسهٔ بخش (جای cmdk) command-list.dom

react-resizable-panels ۴٫۱۲٫۴ (MIT، بی وابستگی، ۱۵٬۲۹۵ بایتِ gzip) جهت نمی‌شناسد: پنل‌ها را هنگامِ ثبت با offsetLeft مرتب می‌کند و از آن پس کشیدن و کلید را فیزیکی می‌خواند. سنجیده با Playwright در کرومِ واقعی روی «نمای سه‌پاره» ِ Storybook ِ ساخته‌شده (۱۰۰۰×۵۰۰؛ امروز درونِ story ِ «چیدمان») — jsdom چیدمان ندارد و split-view.dom فقط ترتیبِ ثبت را با offsetLeft ِ ساختگی می‌سنجد؛ آزمونِ ماندگارِ مرورگر با WorkspaceLayout آمد (۶.۱): apps/web/e2e/layout.spec.ts:

رفتار راست‌به‌چپ چپ‌به‌راست
پنلِ اصلیِ جداکنندهٔ اول (aria-controls) بوم (دیداریِ چپ)، نام «بوم» کتابخانه، نام «کتابخانه»
← یک بار جداکننده ۴۷ پیکسل به چپ ۴۷ پیکسل به چپ
کشیدنِ ۶۰ پیکسل به چپ ۶۰ پیکسل به چپ ۶۰ پیکسل به چپ
Enter، دوباره Enter کتابخانه ۲۰۲ ← ۰ ← ۲۰۲ (پیش‌تر هیچ) ۱۷۸ ← ۰ ← ۱۷۸
تعویضِ جهت بی بارگذاری همان اعدادِ بارگذاریِ چپ‌به‌راست
  • key={direction}: بی ثبتِ دوباره، پس از تعویضِ زبان مدلِ کتابخانه آینه‌وار می‌ماند (aria-controls به پنلِ کهنه، کلید برعکس). گروه با تعویضِ زبان یک بار از نو سوار می‌شود.
  • نامِ جداکننده نامِ پنلِ اصلی است (الگوی window splitter ِ APG)، پس در راست‌به‌چپ نامِ پنلِ پس از آن در DOM.
  • Enter ِ کتابخانه فقط پنلِ اصلی را جمع می‌کند و در راست‌به‌چپ کتابخانهٔ جمع‌شدنیِ سمتِ شروع اصلی نیست. کیت Enter را در فازِ capture می‌گیرد و پنلِ جمع‌شدنیِ کنارِ جداکننده (اول سمتِ شروع) را با API ِ دستوریِ پنل جمع یا باز می‌کند؛ onLayoutChange آن را کارِ کاربر می‌شمارد.
  • جهتِ سند باید پیش از سوار شدنِ گروه با زبان یکی باشد — در برنامه activateLocale ِ lib/locale.ts dir را می‌نشاند و زبانِ فروشگاه (که به UiProvider می‌رسد) پس از آن عوض می‌شود (state/ui.ts). نوارِ «جهت» ِ Storybook تا ۵.۲ فقط dir را عوض می‌کرد و زبانِ React Aria fa-IR می‌ماند؛ حالا هر دو.

UiProvider هر دو ارائه‌دهندهٔ زبان را سوار می‌کند؛ جزءِ اپ متن را از useLingui() ِ @lingui/react و عدد و تاریخ را از useFmt() (apps/web/src/lib/useFmt.ts) می‌خواند، نه از i18n ِ سراسری یا قالب‌گرِ هسته — وگرنه زیرِ memo پس از تعویضِ زبان متنِ قبلی می‌ماند. قاعده و دلیل: i18n.md؛ لینت (no-restricted-imports) واردکردنِ i18n ِ سراسری را در apps/web/src/**/*.tsx می‌گیرد؛ قالب‌گرِ بی‌زبانِ هسته دیگر نیست (formatter(locale) ِ @darzsaz/i18n). اجزای خودِ کیت (Dialog، ConfirmDialog، Select، NumberField، Stepper، SearchField، InlineEdit، OverlayBar، Display، Toast) هنوز سراسری را می‌خوانند.

برچسب: یا دیدنی یا aria-label

Section titled “برچسب: یا دیدنی یا aria-label”

تیپ Labelled الزام می‌کند هر کنترل یکی از دو را داشته باشد — در زمان کامپایل، نه در آزمون axe. پیش از این label اجباری بود، که برعکس مشکل داشت: کنترل‌هایی که برچسبشان جای دیگری است (لغزندهٔ برش مقطعی، جست‌وجوی قطعه) ناچار <input> خام می‌ماندند و از kit بیرون.

--hit کمینهٔ هدف است: ۲۴ با موس، ۴۴ روی (pointer: coarse)، ۴۸ در کارگاه در هر اشاره‌گر (POINTER_HIT و THEME_METRICS.hit در tokens.ts). هر اندازهٔ تعاملیِ کیت max(اندازهٔ دیداری، var(--hit)) است — دکمه، کلید و گروهش، کادر و دکمه‌های +/−، فهرست، جست‌وجو، پله، بازشو، تب، چک‌باکس و لغزنده، ویرایشِ درجا، گزینهٔ منو، نمونهٔ رنگ (۳۴ اعلان). پیش‌تر کوچک‌ترین هدف با موس و لمس ۱۸ و در کارگاه ۳۴ بود (گزینهٔ sm ِ گروهِ کلید) و ۴۴ ِ لمسی فقط زیرِ ۷۶۸ پیکسل پهنا با قاعدهٔ برنامه؛ پهنا اشاره‌گر را نمی‌گوید. آزمون: test/touch-target.test.ts هر اعلان را با توکنِ هر پوسته × اشاره‌گر ارزیابی می‌کند. قاعدهٔ min-height: 44px ِ برنامه (زیرِ ۷۶۸) فقط روی برچسبِ «ورود از پرونده» ِ کاتالوگ مانده (.fileButton ِ catalog.module.css، بازماندهٔ .btn ِ سراسری).

از کیت بگیر: لینتِ ضدِ برگشت (۵.۶، ۵.۹)

Section titled “از کیت بگیر: لینتِ ضدِ برگشت (۵.۶، ۵.۹)”

شش قاعدهٔ kit/* در eslint.config.js (هر ممنوعه قاعدهٔ جدا، تا فهرستِ پایه هر کدام را جدا بشمارد)، پیامِ همه «از کیت بگیر: …». آنچه روزِ روشن شدن بود در scripts/lint-baseline.json شمرده شد (ستونِ آخر) و به صفر رسید؛ حالا بی استثنا‌اند: pnpm lint:baseline هر مدخلِ kit/* را در فهرستِ پایه رد می‌کند، قاعدهٔ کیتِ فردا را هم (i18n.md، «بی استثنا»):

قاعده چه کجا فهرستِ پایه
kit/no-raw-button <button> ِ خام (بازیگرِ SVG ِ بوم <g role="button"> است و گرفته نمی‌شود) TSX ِ اپ و کیت ۱۱۳ در ۴۶ پرونده
kit/no-title-attribute صفتِ title روی عنصرِ DOM (title ِ جزءِ کیت عنوانِ دیدنی است) TSX ِ اپ و کیت ۲۴ در ۱۵
kit/no-role-tab role="tab" TSX ِ اپ و کیت ۳ در ۳
kit/no-native-dialog confirm/prompt/alert ِ مرورگر (نامِ محلی نه) اپ و کیت ۳ در ۳
kit/no-glyph-icon ✓ ⚠️ ▾ ⤢ ▲ ← … در متن، و × + − ِ آغازِ محتوای JSX (× ِ میانِ دو عدد نه) اپ و کیت ۲۶ در ۱۹
kit/no-icons-module واردکردنِ components/icons.tsx اپ ۳ در ۳

تنها موردِ کیت (title ِ Chip) همان‌جا درست شد: درجهٔ اطمینان متنِ پنهان برای صفحه‌خوان است. آزمون: scripts/test/eslint-kit.test.mjs، scripts/test/lint-baseline.test.mjs.

موجِ ۳ (صفحه‌ها و پنجره‌های بیرون از ویرایشگر): همهٔ پرونده‌های خانه، راهنما، هزینه، کارگاه، کاتالوگ، تنظیمات، مرکز چاپ و نوار بالا بی استثنا شدند — فهرستِ پایه no-raw-button ۶۶ ← ۲۵، no-title-attribute ۹ ← ۶، no-role-tab ۲ ← ۰، no-native-dialog ۲ ← ۱، no-glyph-icon ۲۱ ← ۱۴، no-icons-module ۲ ← ۱. امروز فهرستِ پایه تهی است ({}): هر شش قاعدهٔ kit/* بی هیچ موردِ کنارگذاشته، و components/icons.tsx پاک شده است. title ِ <iframe> از قاعده بیرون است: نامِ دسترس‌پذیرِ قاب است (jsx-a11y/iframe-has-title)، نه راهنمای شناور. تأییدِ خروجی با خطای ایمنی ConfirmDialog ِ components/SafetyConfirm.tsx است (فروشگاهِ کوچک، چون فرمان‌ها رندر نمی‌کنند؛ danger، تمرکز روی «انصراف»). تبِ کارگاه Tabs ِ کیت زیرِ سرتیتر است، نه نوارِ دست‌سازِ تهِ سند که بی پیمایشِ body دست‌نیافتنی بود (docs/reference/workshop.md).

قاعدهٔ no-restricted-syntax در eslint.config.js جلوی <select>، <input> و <textarea> خام در apps/web را می‌گیرد. مسئله ظاهر نیست: کنترل خام از React Aria رد می‌شود و با آن ناوبری صفحه‌کلید یکسان، data-focus-visible، رفتار درست راست‌به‌چپ و پوستهٔ کارگاه (قلم ۱۶، هدف لمسی ۴۸).

سه استثنا با دلیلِ کنارِ خودشان: یک type="file" (catalog/CatalogPanel.tsx) و یک type="color" (scene/FinishesPanel.tsx) — انتخابگرِ سامانه؛ جزء وبی جایشان را نمی‌گیرد — و ویرایشِ درجای نام پروژه (app/ProjectName.tsx). FileTrigger ِ کیت جای ورودیِ پرونده را در PhotoCalibrator و cost/PriceBooks.tsx گرفت.

هر جزء CSS Module ِ کنارِ خودش را دارد (برخی مشترک‌اند: field.module.css، overlay.module.css) و فقط از توکن‌ها رنگ می‌گیرد. حالت‌ها با صفت‌های دادهٔ React Aria (data-hovered، data-pressed، data-selected، data-focus-visible، data-invalid) استایل می‌شوند، نه با کلاس دستی.

pnpm storybook (پورت ۶۰۰۶). نوار بالا پوسته (تیره/روشن/کارگاه) و جهت (RTL/LTR) را عوض می‌کند — جهت با زبانِ React Aria (fa-IR/en-US)، نه فقط dir ِ سند — و کنترلِ «state» حالت را. افزونهٔ a11y نقض را همان‌جا نشان می‌دهد و افزونهٔ vitest پنلِ آزمون را. ساخت ایستا: pnpm --filter @darzsaz/ui storybook:build.

story در هر خانواده، چهار حالت (۵.۸). هر پروندهٔ stories/*.stories.tsx یک خانوادهٔ کیت است و Normal، Invalid، Disabled و Pending را صادر می‌کند؛ حالت arg است (stories/state.ts)، پوسته و جهت global:

پرونده (عنوان) اجزا خطا / خاموش / در انتظار
Buttons (دکمه و نوار) Button، IconButton، SplitButton، FileTrigger، Tooltip (باز)، Toolbar، ToggleButton(Group)، OverlayBar گونهٔ خطر / isDisabled / isPending ِ React Aria
Fields (کادر) TextField، NumberField، SearchField، Stepper، InlineEdit، ColorSwatchPicker isInvalid با پیام / isDisabled / فقط‌خواندنی زیرِ aria-busy و نوارِ نامعین
Choices (انتخاب) Select، ComboBox، Checkbox، RadioGroup، Switch، Slider همان؛ Select ِ در انتظار خاموش با توضیح (فقط‌خواندنی ندارد)
Display (نمایش) Badge، Chip، Kbd، StatusPill، Heading، Icon، Separator، VisuallyHidden، EmptyState لحنِ خطر و «پایگاه باز نشد» / کارِ خاموش / ناحیهٔ زندهٔ «در حال ذخیره»
Feedback (بازخورد) ProgressBar، Skeleton، toast اعلانِ هر حالت (باز، در body)؛ نوارِ ایستاده / نامعین و جای‌نگار
Collections (فهرست و جدول) Tabs، DataTable، CommandList ردیفِ خطر و «چیزی پیدا نشد» / بی انتخاب و گزینهٔ خاموش / جای‌نگار؛ «جدولِ خالی» و «پنج هزار ردیف» (کمتر از ۱۰۰ ردیف در سند)
Layout (چیدمان) Stack، Inline، Grid، PanelHeader، Disclosure(Group)، SplitView کادرِ نامعتبر / گروهِ خاموش / جای‌نگار در پنل‌ها
Dialogs (پنجره)، Menus (منو) PromptDialog، ConfirmDialog، Dialog، Sheet؛ Menu، ContextMenu، Popover هر حالت یک پنجرهٔ باز — مودالِ دوم بقیه را aria-hidden می‌کرد؛ دو برگه (کناری و گوشی) و «اعلان روی منوی باز»
All (همه/گالری) stories/Gallery.tsx — همه کنار هم همان چهار حالت

اجرا: pnpm --filter @darzsaz/ui test:stories (vitest.stories.config.ts) — در pnpm verify و کارِ storybook ِ CI. @storybook/addon-vitest ۱۰٫۶٫۰ (در packages/ui) روی Vitest ِ مرورگر: @vitest/browser-playwright ۴٫۱٫۱۱ و playwright ۱٫۶۳٫۰ ِ هم‌نسخهٔ @playwright/test در ریشه، کنارِ خودِ vitest — همتای اختیاریِ vitest از ریشه حل می‌شود و در قفل‌پرونده یک نمونه ماند. افزونهٔ a11y با a11y: { test: 'error' } ِ .storybook/preview.tsx: هر نقضِ axe روی کلِ body — پنجره، منو، راهنما و اعلانِ باز هم، و کنتراست که jsdom نمی‌دید — آزمون را می‌اندازد. ۱۰ پرونده × ۴ حالت = ۴۰ story × ۶ پروژهٔ Vitest (dark-rtlworkshop-ltr، هر کدام initialGlobals ِ خودش) = ۲۴۰ اجرا؛ ۸۱ ثانیه روی دستگاهِ توسعه زیرِ بارِ ۱۰ تا ۴۰. تصمیم‌ها، هر کدام با دلیل در پیکربندی:

  • پروژه برای هر پوسته و جهت، نه رونوشتِ story؛ .storybook/vitest.matrix.ts پس از هر story می‌سنجد سند همان پوسته و جهت را داشت — بی initialGlobals ۲۰ از ۲۴ اجرای «دکمه و نوار» افتاد (همه جز dark-rtl).
  • پیکربندیِ جدا: pnpm -r test و pnpm coverage مرورگر نمی‌خواهند.
  • مرورگر: در CI کرومیومِ تصویرِ mcr.microsoft.com/playwright:v1.63.0-noble، روی دستگاهِ توسعه کرومِ نصب‌شده (همان سیاستِ apps/web/playwright.config.ts).
  • reducedMotion: 'reduce': پویانماییِ باز شدنِ منو صفر می‌شود و axe حالتِ پایانی را می‌بیند؛ زیرِ بار یک منوی خاموش هنوز opacity ِ آغاز را داشت و toBeVisible افتاد.
  • مهلتِ ۶۰ ثانیه: اولین story ِ هر پرونده واردکردنِ ماژول‌ها را هم می‌پردازد و شش پروژه هم‌زمان‌اند؛ زیرِ بار ۴۸ از ۲۴۰ اجرا (همه اولین story) از ۱۵ ثانیهٔ پیش‌فرض گذشت، با ۶۰ ثانیه ۲۴۰ از ۲۴۰. isolate: false هم امتحان شد (همان ۲۴۰، ۸۴ ثانیه) و نماند: سودی در زمان نداشت و بی جداسازی هر پرونده حالتِ پروندهٔ پیشین (اعلانِ باز، صفتِ سند) را هم می‌توانست ببیند.
  • گزارشِ استفاده خاموش (core.disableTelemetry): storybookTest در هر اجرا روشنش می‌کرد.

نقضِ یافته. ToggleButtonGroup ِ چندانتخابی درونِ Toolbar: useToolbar ِ React Aria ۱٫۲۱ نقش را group می‌کند ولی aria-orientation را نگه می‌دارد — axe «aria-allowed-attr» در هر ۲۴ اجرا. برنامه این ترکیب را ندارد؛ story آن را کنارِ نوار گذاشت. تا اصلاحِ React Aria، گروهِ چندانتخابی بیرون از Toolbar. VisuallyHidden ِ گالری درونِ <p> یک div بود (HTML ِ نامعتبر، خطای کنسولِ React) — elementType="span".

پوشش: test/stories.test.ts می‌گیرد اگر جزئی از بارول در هیچ story نیاید (تا ۵.۸: SplitButton، CommandList، Checkbox، RadioGroup، VisuallyHidden) یا پرونده‌ای یکی از چهار حالت را نداشته باشد. story تازه: همان چهار صادره، و جزءِ تازه در پروندهٔ خانواده‌اش.

  • test/contrast.test.ts توکن‌ها.

  • test/a11y.dom.test.tsx: گالری همهٔ اجزا (stories/Gallery.tsx) با axe در jsdom، عادی و در حالت خطا — صفر نقض (قاعدهٔ کنتراست رنگ در jsdom خاموش است؛ آن را آزمون توکن‌ها می‌سنجد). a11y-matrix.dom همان گالری در ۲۴ ترکیب — لایهٔ تندِ pnpm -r test و pre-push؛ لایهٔ مرورگر با کنتراست test:stories است (بالا).

  • test/fields.dom.test.tsx، test/overlays.dom.test.tsx: رفتار (تایپ «۵۰۰» با صفحه‌کلید فارسی، مهار بازه، پله، انتخاب، تب، پنجره، منو با صفحه‌کلید).

  • test/number-select.dom.test.tsx (۵.۳): چرخِ موس روی کادرِ فعال مقدار را عوض نمی‌کند (و با isWheelDisabled={false} همان رویداد ۶۰۰ ← ۶۱۰ می‌کند)؛ کادرِ خالی null؛ onCommit با Enter و بیرون رفتن؛ ref؛ گزینهٔ تهیِ Selectnull و گزینهٔ isDisabled.

  • اجزای ۵.۲: layout، toggle، split-view، disclosure، actions، confirm، overlay-bar، inputs-extra، icons و command-list (*.dom.test.tsx) — کلید، نام، حالت، خاموش، ref؛ test/labels.test.tsx تیپِ برچسب را با @ts-expect-error می‌سنجد (سنجه tsc -p tsconfig.test.json، نه Vitest) و هر عنصرِ ردشده با یک برچسب کامپایل می‌شود. کمکی‌های DOM (تمرکز، ردیفِ ساختگیِ offsetLeft) در test/dom.ts؛ ariaDisabled ِ نبوده در jsdom در test/setup.ts.

  • درونِ کیت (فاز ۵ ِ نسل پنجم، ۵.۴) — هر کدام آزمونی که پیش از اصلاح افتاد:

    # چه آزمون
    K6 شناور و فشردهٔ دکمهٔ اصلی و خطر با --accent-hover و color-mix به سوی --text، نه filter kit-css.test.ts: بی filter؛ متن رویش در سه پوسته ≥ ۷٫۱۹ (بود ۵٫۲۳)
    K7 خطرِ منو با کلاس؛ جداکنندهٔ منوی راست‌کلیک هم‌سطحِ گزینه؛ لنگرش در رندر؛ پنجرهٔ شناورش نام دارد overlays، context-menu: لنگر در لحظهٔ اندازه‌گیری «» بود، نه ۳۰۰×۲۰۰
    K8 نامِ ناحیهٔ اعلان و دکمهٔ بستن از کاتالوگ؛ گوشهٔ پایانِ خط toast.dom.test.tsx: «Notifications alt+T»؛ چپ‌به‌راست left
    K9 EmptyState با live؛ Dialog با headingLevel display، overlays
    K10 UiProvider/UiLocale زبان را در رندر فعال نمی‌کنند؛ بی زبانِ فعال خطای روشن به‌جای DOM ِ خالی provider.dom.test.tsx
    K11 min-height: 0 ِ Tabs فقط با fill kit-css.test.ts، fields.dom.test.tsx
    گالری با منوی خطر و جداکنندهٔ باز و منوی راست‌کلیکِ باز a11y.dom.test.tsx: aria-dialog-name
  • زبان پیش از اولین رندر. کیت زبانِ Lingui را فعال نمی‌کند: برنامه در main.tsx، آزمون در test/setup.ts، Storybook در .storybook/preview.tsx. UiProvider بی زبانِ فعال با پیامِ «no active Lingui locale» می‌افتد — پیش از این یا در حینِ رندر فعالش می‌کرد، یا (بی آن) I18nProvider ِ Lingui بی‌صدا هیچ رندر نمی‌کرد.

pnpm --filter @darzsaz/ui build = tsc -p tsconfig.build.json + کپی CSS به dist (scripts/copy-css.mjs). اپ وب، Storybook و آزمون کیت را از src می‌خوانند (شرطِ @darzsaz/source در exports)؛ Vite خودش CSS Module‌ها را پردازش می‌کند.