سیستم طراحی — `packages/ui`
This content is not available in your language yet.
فاز ۳ پلن نسل سوم. اجزای رابط روی React Aria Components و توکنها از یک منبع. هر جزء در Storybook در چهار حالت × سه پوسته × دو جهت دیده و با axe اجرا میشود (۵.۸).
توکنها
Section titled “توکنها”- منبع:
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(:root)،lightو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خودِ رنگِ شفاف را نمیپذیرد. رنگی که آزمون را بیندازد وارد نمیشود.
جوهر روی کاغذ
Section titled “جوهر روی کاغذ”کاغذِ بوم، پلان، پلانِ کوچکِ تنظیمات، پیشنمای سازنده، نمادِ کتابخانه و بندانگشتیِ خانه در
هر پوسته روشن است (تیره #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 را برداشت) و فهرست آن را از خودِ پروژه میسازد: ردیفِ
قدیمی رنگِ پخته داشت.
روکش، پرده، سایه، لایه
Section titled “روکش، پرده، سایه، لایه”- هر نوارِ شناور روی بوم و صحنه —
.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 روی بوم هیچ نشانهای نداشت.
موادِ صحنه
Section titled “موادِ صحنه”رنگی که با پوسته عوض نمیشود توکن نیست و در 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
(رندر در پوستهٔ روشن گوشهٔ تیره دارد).
نگهبانهای CSS
Section titled “نگهبانهای CSS”- 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.
CSS Module ِ تیپدار (کیت)
Section titled “CSS Module ِ تیپدار (کیت)”هر *.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ندارد.
قراردادِ اجزا (۵.۱)
Section titled “قراردادِ اجزا (۵.۱)”هر جزءِ کیت — تازههای ۵.۲ از روزِ اول و اجزای پیشین از بخشِ دومِ فاز ۵ — همین است؛ آزمونِ تیپ
(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 ِ شکننده، با جاهای برنامه در همان کامیت):
- K2 —
NumberFieldبی برچسب کامپایل نمیشود؛ تنها جای «هر دو با هم» (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 | شمارندهٔ کوچک کنار برچسب (count)؛ fill ظرفِ ستونی را پر میکند و پنل پیمایش میخورد — بی آن اندازهٔ محتوا (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 را فعال نمیکند (پایین) |
اجزای تازه (۵.۲)
Section titled “اجزای تازه (۵.۲)”پیش از مهاجرتِ برنامه (۵.۵ و فاز ۶) آمدند و بیشترشان حالا در apps/web به کار رفتهاند؛ هنوز نه: Stack،
Inline، Grid، Separator، PanelHeader، StatusPill، Toolbar (فقط درونِ OverlayBar)، InlineEdit،
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 |
نمای دوپاره در دو جهت
Section titled “نمای دوپاره در دو جهت”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.tsdirرا مینشاند و زبانِ فروشگاه (که بهUiProviderمیرسد) پس از آن عوض میشود (state/ui.ts). نوارِ «جهت» ِ Storybook تا ۵.۲ فقطdirرا عوض میکرد و زبانِ React Ariafa-IRمیماند؛ حالا هر دو.
زبان در اجزای اپ
Section titled “زبان در اجزای اپ”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 بیرون.
هدفِ لمسی از توکن (۵.۷)
Section titled “هدفِ لمسی از توکن (۵.۷)”--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).
کنترل خام ممنوع
Section titled “کنترل خام ممنوع”قاعدهٔ 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) استایل میشوند، نه با کلاس دستی.
Storybook
Section titled “Storybook”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-rtl … workshop-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؛ گزینهٔ تهیِSelect←nullو گزینهٔ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، نهfilterkit-css.test.ts: بی filter؛ متن رویش در سه پوسته ≥ ۷٫۱۹ (بود ۵٫۲۳)K7 خطرِ منو با کلاس؛ جداکنندهٔ منوی راستکلیک همسطحِ گزینه؛ لنگرش در رندر؛ پنجرهٔ شناورش نام دارد overlays،context-menu: لنگر در لحظهٔ اندازهگیری «» بود، نه ۳۰۰×۲۰۰K8 نامِ ناحیهٔ اعلان و دکمهٔ بستن از کاتالوگ؛ گوشهٔ پایانِ خط toast.dom.test.tsx: «Notifications alt+T»؛ چپبهراستleftK9 EmptyStateباlive؛DialogباheadingLeveldisplay،overlaysK10 UiProvider/UiLocaleزبان را در رندر فعال نمیکنند؛ بی زبانِ فعال خطای روشن بهجای DOM ِ خالیprovider.dom.test.tsxK11 min-height: 0ِTabsفقط باfillkit-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ها را پردازش میکند.