# DS — Design System — e-commerce | | | |---|---| | **Version** | 1.1 | | **Date** | 2026-09-09 | | **Author** | BA/Design (skill ba-design, activity `ds`) | | **Status** | 🟡 Draft | | **Approved by** | Designer: — · PO: — *(dự án chưa có Designer riêng — PO ký thay, `DEC-01`)* | | **Source** | `00-index/UICONV_e-commerce_v1.0.md` v1.1 (§0, §1, §8, §11, §11.1) · brand guideline: chưa có — `e-commerce/docs/sections/07-giao-dien.md` §7.0 xác nhận "không có brand guideline cố định" · design system tham chiếu: chưa chọn (`OQ-022`) · `02-analysis/BR_CartCheckout_v1.0.md` §3 (vòng đời `CART`/`ORDER_SELLER`/`PAYMENT`, nguồn bảng badge §3 bên dưới) · `03-specification/WF_US002-003_v1.0.md` (§3.1.2 — nguồn các kiểu trình bày cần phủ ở §3) | | **Scope** | Toàn project — mọi `HIFI`, `FIGMA`, `DSPEC` có `PRODUCT = screen`. Lần chạy này lập bản đầu, dùng dữ liệu vào là US-002/US-003 (`SCR-04` Giỏ hàng) — token/component sẽ bồi đắp dần khi các US khác chạy `ba-design` | | **Profile** | `screen · greenfield · standard` | | **Tệp kèm** | `00-index/ds/tokens.json` (nguồn sự thật) · `ds/tokens.css` (sinh từ json) · `ds/components.html` (component sheet, mở bằng trình duyệt) | ## Change Log | Version | Date | Người sửa | Thay đổi | CR | |---|---|---|---|---| | 1.0 | 2026-09-08 | BA/Design (qua skill ba-design, activity `ds`) | Bản đầu — token placeholder trung tính của template (chưa có brand), §3 ánh xạ 12 kiểu trình bày dùng trong `WF_US002-003`, bảng badge từ `BR_CartCheckout` §3 (qua `UICONV` §11.1), tự chấm | — | | 1.1 | 2026-09-09 | BA/Design (qua skill ba-design, theo ghi chú người duyệt sau khi chạy `design-check.mjs`/`render-figma.sh`) | Nâng khối `ds-components` lên **v1.1** (thêm tiện ích `.ds-m-full` — sửa lỗi bố cục mobile của `HIFI_US002-003` §2.1); chép kết quả `design-check.mjs` nguyên văn vào §6 (T1/T2 đều ✅, 10/10 cặp tương phản đạt); cập nhật Tự chấm #1, #2 | — | > **Vai trò của tài liệu này.** `UICONV` nói *hành xử thế nào* (phân trang, vị trí toast, validate lúc nào); > tài liệu này nói *trông thế nào* (màu, chữ, khoảng cách, bo góc, đổ bóng, trạng thái hover/focus/disabled). > `HIFI` và `FIGMA` chỉ dùng token và component ở đây; muốn khác phải có dòng ở §8. > > **Thứ tự ưu tiên khi lệch:** Brand guideline của khách > Design system tham chiếu (nếu dự án đã chọn) > > `DS` này > `HIFI` từng US. Thiếu brand ⇒ giá trị ở đây là **đề xuất trung tính** chờ Designer/PO chốt — ghi `OQ`, không tự coi là đã duyệt. > ⚠️ **Ngoại lệ gate G2** — tài liệu này (và `HIFI`/`FIGMA`/`DSPEC` phụ thuộc nó) được sinh trong khi > Gate G2 (Solution sign-off) của module Giỏ hàng & Checkout **chưa được PO + Tech Lead ký chính thức** > (nối tiếp `DEC-02`, `DEC-05`, `DEC-06` — xem `00-index/DECISION_e-commerce.md`). Ngoại lệ đã được người > điều phối xác nhận rõ ràng lần nữa — xem `DEC-07`. Rủi ro riêng của `DS`: thấp hơn `SRS`/`AC` vì `DS` là > token cấp **project** (không phụ thuộc trực tiếp `BACKLOG`/`BR` của module) — nhưng bảng badge ở §3 tham > chiếu `BR_CartCheckout_v1.0.md` §3, nếu BR đổi khi G2 ký thật thì bảng đó phải rà lại. --- ## 0. Nguồn và độ tin cậy | | | |---|---| | **Brand guideline** | Chưa có — `e-commerce/docs/sections/07-giao-dien.md` §7.0 xác nhận rõ "không có brand guideline cố định". Mọi giá trị màu/chữ ở §1 là **placeholder trung tính của template `ba-design`**, không phải đề xuất nhận diện thương hiệu. `OQ-022` (đã mở ở `UICONV`, chưa đóng) | | **Design system tham chiếu** | Chưa chọn. `UICONV` §0 dẫn NFR-07: "sử dụng design system chuẩn (VD Material Design hoặc Ant Design)" nhưng ghi rõ "dev chọn, chưa chốt" → `OQ-022`, hỏi Tech Lead | | **Prototype đã có** | `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04 — mô tả **văn bản có cấu trúc**, không có mã màu/khoảng cách chính xác (không phải Figma/ảnh) — không trích được token từ đây, chỉ trích được bố cục (đã dùng ở `WF`) | | **Ai sở hữu** | BA/Design đề xuất · **Designer duyệt** visual (dự án không có Designer ⇒ **PO ký thay**, `DEC-01`) · PO duyệt giọng văn/nhận diện · Dev FE xác nhận khả năng hiện thực với stack đã chọn khi `OQ-022` được trả lời | | **Độ tin cậy** | 🔴 **Đề xuất trung tính, chưa có nguồn** — toàn bộ bảng màu/chữ/khoảng cách ở §1 là giá trị mặc định của `templates/tokens.json` trong bộ skill `ba-design`, không suy ra từ brand hay prototype (prototype chỉ có bố cục, không có visual). Mọi `HIFI`/`FIGMA` sinh từ `DS` này **mang cùng độ tin cậy 🔴** cho tới khi `OQ-022` (design system) và brand guideline được PO/Tech Lead xác nhận | --- ## 1. Token *Bảng này tóm tắt; giá trị chuẩn ở `ds/tokens.json`. Sửa ⇒ sửa json, sinh lại css, chạy `design-check.mjs`.* ### 1.1 Màu | Token | Giá trị | Dùng cho | Nguồn | |---|---|---|---| | `color.bg.canvas` | `#F6F7F9` | Nền trang | 🔴 Placeholder template — `OQ-022` | | `color.bg.surface` | `#FFFFFF` | Card, bảng, modal | 🔴 Placeholder template | | `color.bg.subtle` | `#EEF1F5` | Header bảng, vùng phụ | 🔴 Placeholder template | | `color.border.default` / `.strong` | `#D5DAE1` / `#767F8C` | Viền chia / viền ô nhập (F01) | 🔴 Placeholder template | | `color.text.primary` / `.secondary` / `.disabled` / `.inverse` | `#1B1F24` / `#5B6472` / `#9AA3AF` / `#FFFFFF` | Chữ chính (tên sản phẩm C04) / phụ (biến thể C05, hint) / vô hiệu (F01/C07 khi đang xử lý) / trên nền đậm (nhãn nút chính C11) | 🔴 Placeholder template | | `color.brand.primary` / `-hover` / `-subtle` | `#2456A6` / `#1C468A` / `#E7EEF9` | Nút chính (C11), liên kết, focus / hover / nền chọn | 🔴 Placeholder — chưa có màu thương hiệu thật | | `color.status.danger` / `success` / `warning` (+ `-subtle`) | `#B42318` / `#1E7B45` / `#B7791F` (+ nền nhạt) | Nút nguy hiểm (C07), banner lỗi (C12, E-CART-0003/0006), message lỗi F01 (E-CART-0002) | 🔴 Placeholder template | ### 1.2 Chữ | Token | Giá trị | Dùng cho | |---|---|---| | `font.family.base` | Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif | Toàn hệ thống — *system stack, giữ nguyên theo chỉ đạo (không có font brand)* | | `font.size.xs / sm / md / lg / xl / 2xl` | 12 / 14 / 16 / 20 / 24 / 32 px | Hint, nhãn phụ (C05, C08, C09) / body (C04, C06) / — / — / C01 (tiêu đề Giỏ hàng) / — | | `font.weight.regular / semibold` | 400 / 600 | Text thường / tiêu đề, nút, tên gian hàng (C02) | | `font.line-height.tight / normal` | 1.25 / 1.5 | Tiêu đề / body | ### 1.3 Khoảng cách, bo góc, đổ bóng, kích thước | Token | Giá trị | Ghi chú | |---|---|---| | `space.1 … 12` | 4 · 8 · 12 · 16 · 24 · 32 · 48 px | Thang 4px; không dùng số ngoài thang | | `radius.sm / md / pill` | 4 / 8 / 999 px | Nút, ô nhập (F01) / card (nhóm seller), modal / badge | | `shadow.sm / md` | — | Card nổi / modal xác nhận xoá | | `size.control.desktop / touch` | 36 / 44 px | Chiều cao nút (C07, C10, C11, C13), bộ đếm F01; **mobile ≥ 44px** (hit target) | | `size.icon` | 20 px | Icon inline (spinner, alert) | | `size.content-max` | 1200 px | 🔴 Placeholder — `UICONV` §1 chưa chốt số thật (`OQ-018`), giữ giá trị template cho tới khi có câu trả lời | | `breakpoint.tablet / desktop` | 768 / 1024 px | 🔴 Placeholder — `UICONV` §1 chưa chốt (`OQ-018`); `WF_US002-003` chỉ hiện thực desktop/mobile (không có công tắc tablet trong toolbar), `HIFI`/`FIGMA` lần này theo đúng phạm vi đó | ## 2. Thang chữ áp dụng | Cấp | Token size / weight / line-height | Dùng ở | |---|---|---| | H1 — tiêu đề màn hình | xl / semibold / tight | C01 "Giỏ hàng của bạn" | | H2 — tiêu đề vùng, modal | lg / semibold / tight | Tiêu đề modal xác nhận xoá | | H3 — tiêu đề card | md / semibold / tight | C02 tên gian hàng | | Body | sm / regular / normal | C04, C06, form | | Nhãn field, hint, header bảng | xs / semibold hoặc regular / normal | C05 (biến thể), C08/C09 (nhãn tạm tính/tổng) | ## 3. Từ vựng component — ánh xạ ba chiều *Cột 1 = UICONV §11 (BA) · cột 2 = lớp CSS trong `ds/components.html` (HIFI dùng) · cột 3 = component của design system tham chiếu (Dev FE dùng, để trống vì `OQ-022` chưa chốt). Bảng dưới phủ đủ mọi kiểu trình bày xuất hiện trong `WF_US002-003_v1.0.md` §3.1.2 (đánh dấu ✅ "dùng ở SCR-04") cộng các dòng còn lại của template (chưa dùng ở US này, giữ cho các US sau).* | Kiểu trình bày (UICONV §11 / SRS "Loại") | Lớp DS | Component DS tham chiếu | Dùng ở SCR-04 | Ghi chú | |---|---|---|---|---| | Tiêu đề cấp 1 | `

` (mặc định `ds-components.css`) | chưa chọn (`OQ-022`) | ✅ | C01 | | Nhãn | text thường / `.ds-muted`, `.ds-small` cho phụ | chưa chọn (`OQ-022`) | ✅ | C02 (tên gian hàng, ``/H3), C04 (tên SP), C05 (biến thể), C06 (đơn giá), C08 (tạm tính nhóm), C09 (tổng cộng) | | Ảnh | `.ds-thumb` | chưa chọn (`OQ-022`) | ✅ | C03 — ô xám khi lỗi tải (theo `UICONV` §1) | | Nút chính | `.ds-btn.ds-btn--primary` | chưa chọn (`OQ-022`) | ✅ | C11 "Tiến hành Checkout" — một nút chính mỗi màn hình | | Nút phụ | `.ds-btn.ds-btn--secondary` | chưa chọn (`OQ-022`) | ✅ | C10 "Tiếp tục mua sắm", C13 "Thử lại", nút "Huỷ" trong modal xác nhận xoá | | Nút nguy hiểm | `.ds-btn.ds-btn--danger` | chưa chọn (`OQ-022`) | ✅ | C07 "Xoá" + nút xác nhận trong modal — luôn kèm modal xác nhận (`UICONV` §5) | | Liên kết | `.ds-link` | chưa chọn (`OQ-022`) | ❌ | Không dùng ở SCR-04 (§1.5 dòng 7 của SRS: không điều hướng từ dòng CartItem) | | Ô nhập / Ô nhập số | `.ds-field` + `.ds-input` / `.ds-stepper` | chưa chọn (`OQ-022`) | ✅ | F01 — bộ đếm +/- (`.ds-stepper`), có ô nhập tay giữa bộ đếm | | Chọn 1 (≤7) / (>7) | `.ds-check` radio / `.ds-select` | chưa chọn (`OQ-022`) | ❌ | Chưa dùng ở SCR-04 | | Chọn nhiều | `.ds-check` checkbox | chưa chọn (`OQ-022`) | ❌ | Checkbox chọn dòng chỉ có ở **prototype**, chưa vào SRS (`WF §5 #1`, `OQ-031`) — **không** vẽ trong HIFI với `data-c` | | Công tắc | `.ds-switch` | chưa chọn (`OQ-022`) | ❌ | Chưa dùng ở SCR-04 | | Bộ đếm số lượng | `.ds-stepper` | chưa chọn (`OQ-022`) | ✅ | F01 (trùng dòng Ô nhập số) | | Bảng / Card | `.ds-table-wrap` / `.ds-cards > .ds-card` | chưa chọn (`OQ-022`) | ✅ (Card) | Mỗi nhóm seller (Z2) và khối tổng kết (Z3) dùng `.ds-card`; không dùng `.ds-table` (Guest/Customer không có bảng dữ liệu — `UICONV` §1) | | Badge trạng thái | `.ds-badge[--info/--success/--warning/--danger]` | chưa chọn (`OQ-022`) | ❌ | **Không** hiển thị badge trạng thái `Cart`/`Order`/`Payment` ở SCR-04 — US-002/US-003 chỉ thao tác trên `Cart.status=active` (SRS §1.4), badge để dành cho US khác (checkout, quản lý đơn) — xem bảng ánh xạ bên dưới | | Tab | `.ds-tabs > .ds-tab` | chưa chọn (`OQ-022`) | ❌ | Chưa dùng ở SCR-04 | | Modal / Drawer / Bottom-sheet | `.ds-modal-backdrop > .ds-modal` | chưa chọn (`OQ-022`) | ✅ (Modal) | Xác nhận xoá `CartItem` (hành vi của C07, theo `UICONV` §5 mặc định — `WF §5 #5`, đã xử lý) | | Toast | `.ds-toast[--error]` | chưa chọn (`OQ-022`) | ❌ | `UICONV` §5/§11 chưa chốt vị trí/thời lượng (`OQ-020`) — SCR-04 không dùng toast, cập nhật C08/C09 tại chỗ (`WF` §3.1.5) | | Banner trong vùng | `.ds-banner[--error/--warning]` | chưa chọn (`OQ-022`) | ✅ | C12 (lỗi tải `GET /v1/cart`), banner tại dòng cho `E-CART-0003`/`0006` | | Skeleton | `.ds-skeleton` | chưa chọn (`OQ-022`) | ✅ | Trạng thái "Đang tải lần đầu" (SRS §2.A.3.6) | | Trạng thái rỗng (2 loại) | `.ds-empty` (có minh hoạ) / `.ds-empty.ds-empty--filter` (không) | chưa chọn (`OQ-022`) | 🟠 (chỉ 1/2 loại) | SCR-04 chỉ có loại "chưa có dữ liệu" (`.ds-empty`, C10) — **không có** loại "bộ lọc không khớp" (giỏ hàng không có bộ lọc, đã ghi ở `WF` §3.1.3, không phải thiếu sót) | | Trang trạng thái | `.ds-status-page` | chưa chọn (`OQ-022`) | ❌ | Chưa dùng ở SCR-04 (E-CART-0005 hiển thị "trang trạng thái" theo SRS §4.1 nhưng nằm ngoài SCR-04 — điều hướng sang trang khác) | | `LanguageSwitcher` | *(chưa có class riêng — dự án chưa dựng)* | chưa chọn (`OQ-022`) | ❌ | Thuộc header toàn site (SCR-01), không thuộc SCR-04 | | `CurrencyToggle` / `PriceDisplay` | *(chưa có class riêng — dự án chưa dựng)* | chưa chọn (`OQ-022`) | 🟠 | C06/C08/C09 hiển thị VND theo `UICONV` §8 (`#,##0 ₫`) — chưa có giá quy đổi tham khảo ở SCR-04 (SRS không yêu cầu), `PriceDisplay` dùng nguyên `.ds-price` | **Ánh xạ trạng thái nghiệp vụ → badge** *(từ `BR_CartCheckout_v1.0.md` §3, qua `UICONV` §11.1 — chưa có US nào ở SCR-04 hiển thị các badge này; giữ ở đây để `HIFI` của US-004 trở đi dùng lại đúng token, không định nghĩa lại)*: | Trạng thái (BR) | Biến thể badge | Nhãn nguyên văn (SRS §4.2 — chờ US tương ứng) | Nhóm ý nghĩa màu (UICONV §11.1) | |---|---|---|---| | `CART.active` | `.ds-badge` (mặc định, trung tính) | "Đang hoạt động" | Trung tính | | `CART.converted` | `.ds-badge--success` | "Đã chuyển thành đơn" | Thành công | | `CART.abandoned` | `.ds-badge` (mặc định) hoặc `.ds-badge--warning` *(chưa chốt — trung tính/cảnh báo nhẹ theo `UICONV`, để Dev FE/Designer chọn khi US dùng tới)* | "Bỏ quên" | Trung tính/cảnh báo nhẹ | | `ORDER_SELLER.pending` *(khởi tạo)* | `.ds-badge--warning` | "Chờ thanh toán" | Cảnh báo | | `ORDER_SELLER.confirmed` *(khởi tạo)* | `.ds-badge--success` | "Đã xác nhận" | Thành công | | `ORDER_SELLER.cancelled` *(khởi tạo)* | `.ds-badge--danger` | "Đã huỷ" | Lỗi/trung tính | | `PAYMENT.pending` | `.ds-badge--warning` | "Chờ xử lý" | Cảnh báo | | `PAYMENT.success` | `.ds-badge--success` | "Thành công" | Thành công | | `PAYMENT.failed` | `.ds-badge--danger` | "Thất bại" | Lỗi | **Tiện ích bố cục mới (v1.1):** `.ds-m-full` — ở mobile (`body[data-view="mobile"]` hoặc `max-width:767px`), phần tử mang lớp này chiếm trọn hàng (`flex-basis:100%`). Dùng để nhóm nút thao tác (VD F01 + nút Xoá của một dòng `CartItem`) rơi xuống hàng riêng thay vì chồng lên khối text khi hàng `.ds-row` không đủ chỗ — xem `HIFI_US002-003_v1.0.html` §Minh hoạ dòng CartItem và `DSPEC_US002-003_v1.0.md` §2.1. Không phải một "kiểu trình bày" theo UICONV §11 (không cần dòng riêng trong bảng trên) — là tiện ích kỹ thuật chung của `ds-components.css`. 🔴 Bảng trên **chưa đủ để US ngoài SCR-04 dùng trực tiếp** — vòng đời đầy đủ `ORDER_SELLER` sau `confirmed` (packed/shipped/delivered/returned…) chưa có badge tương ứng (ngoài phạm vi module Giỏ hàng & Checkout, xem `UICONV` §11.1). Không có màu hex cụ thể theo nhóm ý nghĩa (chỉ có biến thể lớp) vì chưa chọn design system — `OQ-022`. ## 4. Trạng thái component *Mỗi ô ghi thay đổi thị giác (token nào đổi). "—" = không có trạng thái đó. Ô trống = chưa thiết kế.* | Component | Default | Hover | Focus | Disabled | Loading | Error / Invalid | Selected | |---|---|---|---|---|---|---|---| | Nút chính (C11) | `brand.primary` | `brand.primary-hover` | ring 2px `focus.ring` | opacity .45 + **tooltip lý do** (giỏ 0 CartItem) | — | — | — | | Nút phụ (C10, C13) | surface + `border.strong` | `bg.subtle` | ring | opacity .45 + tooltip | spinner (C13 khi đang thử lại — không bắt buộc theo SRS, tuỳ Dev FE) | — | — | | Nút nguy hiểm (C07) | `status.danger` | tối hơn *(chưa có token `-hover` riêng cho danger — 🔴 để trống, DS §9)* | ring | opacity .45 + tooltip ("đang xử lý") | spinner (khi DELETE đang chạy) | — | — | | Bộ đếm F01 | `border.strong` | `text.secondary` | `brand.primary` + ring | `bg.subtle`, chữ disabled (khi đang gửi PATCH) | spinner cục bộ tại dòng (theo SRS §2.A.3.6) | `status.danger` + message dưới (E-CART-0002) | — | | Card nhóm seller (Z2) | surface + `border.default` | — | — | — | skeleton (khi loading) | banner lỗi tại dòng (E-CART-0003/0006) | — | | Banner (C12) | `status.danger-subtle` + `status.danger` | — | — | — | — | luôn ở trạng thái lỗi khi hiện | — | | Modal xác nhận xoá | — | — | focus trap trong modal (chưa xác nhận cụ thể — mức thấp) | — | nút "Xoá" trong modal chuyển `is-loading` khi đang gửi | — | — | ## 5. Icon | | | |---|---| | **Bộ icon** | Inline SVG tự vẽ theo mẫu `components.html` (lưới 24, stroke 1.75) — chưa chọn bộ icon thư viện ngoài (`OQ-022`) | | **Quy cách** | Lưới 24, stroke 1.75, `stroke-linecap: round`, hiển thị 20px (`size.icon`), màu `currentColor` | | **Cấm** | Emoji, ký hiệu ✓✕▲ dạng chữ thay icon (trừ mũi tên phân trang `‹ ›`), icon tô đặc trộn với icon nét | | **Danh sách dùng ở SCR-04** | trash (nút Xoá C07 — tuỳ chọn kèm nhãn chữ), alert (banner lỗi C12, message F01), close (đóng modal), spinner (đang gửi) | | **Danh sách chung dự án** | search · plus · close · chevron-down · back · check · alert · trash · filter · cart · user · menu — *(bổ sung theo WF các US sau)* | ## 6. Tương phản và khả năng truy cập *Kết quả chạy `node scripts/design-check.mjs --tokens ds/tokens.json --css ds/tokens.css --components ds/components.html` — **đã chạy bởi người điều phối ngày 2026-09-09**, chép nguyên văn:* ``` ✅ T0 tokens.json: 49 token, 17 màu ✅ T1 tokens.css khớp tokens.json ✅ T2 Tương phản: 10/10 cặp đạt color.text.primary / color.bg.surface = 16.56 (≥4.5) color.text.primary / color.bg.canvas = 15.45 (≥4.5) color.text.secondary / color.bg.surface = 5.98 (≥4.5) color.text.inverse / color.brand.primary = 7.1 (≥4.5) color.text.inverse / color.status.danger = 6.57 (≥4.5) color.brand.primary / color.bg.surface = 7.1 (≥4.5) color.status.danger / color.bg.surface = 6.57 (≥4.5) color.status.danger / color.status.danger-subtle = 5.75 (≥4.5) color.text.primary / color.brand.primary-subtle = 14.19 (≥4.5) color.border.strong / color.bg.surface = 4.05 (≥3) ✅ H1 components.html: khối :root khớp tokens.json ✅ H2 components.html: không có mã màu thô trong markup ok — không có lỗi 🔴 ``` | Cặp (fg / bg) | Dùng cho | Tỷ lệ (design-check) | Ngưỡng | ☐/✅ | |---|---|---|---|---| | `text.primary` / `bg.surface` | Chữ thường (C04, C05, C06) | 16.56 | 4.5 | ✅ | | `text.primary` / `bg.canvas` | Chữ thường trên nền trang | 15.45 | 4.5 | ✅ | | `text.secondary` / `bg.surface` | Hint, chữ phụ | 5.98 | 4.5 | ✅ | | `text.inverse` / `brand.primary` | Nhãn nút chính (C11) | 7.1 | 4.5 | ✅ | | `text.inverse` / `status.danger` | Nhãn nút nguy hiểm (C07) | 6.57 | 4.5 | ✅ | | `brand.primary` / `bg.surface` | Liên kết | 7.1 | 4.5 | ✅ | | `status.danger` / `bg.surface` | Message lỗi (E-CART-0002 dưới F01) | 6.57 | 4.5 | ✅ | | `status.danger` / `status.danger-subtle` | Banner lỗi (C12, E-CART-0003/0006) | 5.75 | 4.5 | ✅ | | `text.primary` / `brand.primary-subtle` | Chữ trên badge/hàng chọn | 14.19 | 4.5 | ✅ | | `border.strong` / `bg.surface` | Viền F01 (non-text) | 4.05 | 3.0 | ✅ | ✅ **T1/T2 đã xác nhận bằng máy** (2026-09-09) — 10/10 cặp tương phản đạt, `tokens.css` khớp `tokens.json`. Kết quả trên là nguồn sự thật cho §6, thay cho bảng tính tay ở v1.0. | Quy tắc | Giá trị | Nguồn | |---|---|---| | Hit target tối thiểu trên mobile | 44 × 44 px | WCAG 2.5.8 / `size.control.touch` — áp dụng cho F01 (+/-), C07, C10, C11, C13 ở mobile | | Focus luôn thấy được | ring 2px `focus.ring`, offset 2px | WCAG 2.4.7 | | Không truyền nghĩa chỉ bằng màu | Banner lỗi luôn có icon + text (C12); không có badge màu-only ở SCR-04 | WCAG 1.4.1 | | Mọi ô nhập có nhãn | F01 có `aria-label="Số lượng"` (không có `