update skill ba

This commit is contained in:
Canhchimlac
2026-09-11 23:01:18 +07:00
parent 2c7bcde741
commit d169c85dd4
51 changed files with 4111 additions and 30 deletions

View File

@@ -0,0 +1,178 @@
# DSPEC — Đặc tả thiết kế & bàn giao — US-002, US-003 (SCR-04 Giỏ hàng)
| | |
|---|---|
| **Version** | 1.1 |
| **Date** | 2026-09-09 |
| **Author** | BA/Design (skill ba-design, activity `dspec`) |
| **Status** | 🟡 Draft |
| **Approved by** | Designer: — · PO: — *(dự án chưa có Designer riêng — PO ký thay, `DEC-01`)* |
| **Source** | `SRS_US002-003_v1.0.md` v1.0 · `WF_US002-003_v1.0.md` v1.0 · `UICONV_e-commerce_v1.0.md` v1.1 · `DS_e-commerce_v1.0.md` v1.1 · prototype: `e-commerce/docs/sections/07-giao-dien.md` §7.1.1 SCR-04 (qua `WF`, không đọc trực tiếp) · kết quả `design-check.mjs` và `render-figma.sh` do người điều phối chạy ngày 2026-09-09 (ghi chú người duyệt) |
| **Scope** | US-002, US-003 · `SCR-04` |
| **Tệp kèm** | `HIFI_US002-003_v1.0.html` · `FIGMA_US002-003_v1.0/` |
| **Prototype bấm được** | ❌ không — activity `proto` không nằm trong phạm vi lần chạy này (chỉ `ds`, `hifi`, `figma`, `dspec`); `HIFI` chưa gắn `data-nav` |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | 2026-09-08 | BA/Design (qua skill ba-design, activity `dspec`) | Bản đầu — redline SCR-04, ánh xạ badge (N/A ở US này), accessibility (tính tay, chưa chạy máy), 5 dòng lệch HIFI↔WF/SRS/DS, bàn giao Dev FE | — |
| 1.1 | 2026-09-09 | BA/Design (qua skill ba-design, theo ghi chú người duyệt) | Sửa lỗi bố cục mobile HIFI (nâng `ds-components` v1.1, thêm `.ds-m-full`, bọc khối thao tác F01+C07 xuống hàng riêng — §2.1); chép kết quả `design-check.mjs` thật vào §6 (ok, không lỗi 🔴); §1 điền 8 file PNG thật; bổ sung component còn thiếu (F01/C07 dòng "Balo du lịch") vào `SCR-04.desktop.svg`; cập nhật `SCR-04.mobile.svg` theo bố cục mobile mới; README-import.md bỏ ghi chú "png trống" | — |
> **Quy ước đọc** *(W11)*. `SRS` quyết định phần tử **có tồn tại không** và **hành xử thế nào**; `WF` quyết định
> **nằm ở đâu**; `DS` quyết định **trông thế nào** (token, component); tài liệu này ghi **con số cụ thể** để Dev FE dựng
> đúng HIFI mà không phải đo bằng mắt. Mâu thuẫn ⇒ SRS > WF > DS > HIFI, và mâu thuẫn phải ghi ở §6.
> ⚠️ **Ngoại lệ gate G2** — tài liệu này (và `HIFI`/`FIGMA` nó mô tả) đượ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`). Người dùng đã được cảnh báo và **khẳng định lại** muốn
> tiếp tục vì mục đích đánh giá bộ skill BA — xem `DEC-07` (`00-index/DECISION_e-commerce.md`).
> Rủi ro riêng của bộ `ba-design` này: nếu `BACKLOG_CartCheckout`/`BR_CartCheckout`/`SRS_US002-003`
> đổi khi G2 ký thật, phải chạy lại `hifi` → `figma` → `dspec` cho `SCR-04` (xem `artifact-map.md` §5).
> Ghi nhận ở Open Questions §8 dòng đầu.
---
## 0. Nguồn bố cục và độ tin cậy
| | |
|---|---|
| **HIFI dựng từ** | `WF_US002-003_v1.0.md` §3.1 (bố cục) + `DS_e-commerce_v1.0.md` (visual) + `SRS_US002-003_v1.0.md` §2.A.3 / §4.2 (thành phần, text) |
| **Prototype tham chiếu** | `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 phải Figma/ảnh), mức phủ 1/1 màn hình của US-002/US-003, đã dùng ở `WF` §0–§1, **không đọc lại trực tiếp** ở bước `dspec` này (chỉ đọc qua `WF`, đúng chuỗi phụ thuộc `artifact-map.md` §5) |
| **Độ tin cậy visual** | 🔴 `DS` §0 ghi "Đề xuất trung tính, chưa có nguồn" — toàn bộ token màu/chữ là placeholder của template `ba-design`, không suy ra brand thật. `HIFI`/`FIGMA` mang **cùng độ tin cậy 🔴** cho tới khi `OQ-022` (design system) và brand guideline được xác nhận |
## 1. Bản đồ màn hình → tệp
| SCR | Tên | HIFI (`#id`) | Trạng thái có trong HIFI | SVG (FIGMA) | PNG | Khớp WF §3.x |
|---|---|---|---|---|---|---|
| SCR-04 | Giỏ hàng | `HIFI_US002-003_v1.0.html#SCR-04` | `default` (+ 3 micro-state minh hoạ tại dòng: đang gửi, validation lỗi, lỗi hệ thống) · `loading` · `empty-none` · `error` | `SCR-04.desktop.svg` · `SCR-04.mobile.svg` (chỉ trạng thái `default`) | `png/SCR-04.desktop.default.png` · `png/SCR-04.desktop.loading.png` · `png/SCR-04.desktop.empty-none.png` · `png/SCR-04.desktop.error.png` · `png/SCR-04.mobile.default.png` · `png/SCR-04.mobile.loading.png` · `png/SCR-04.mobile.empty-none.png` · `png/SCR-04.mobile.error.png` (8 file, chụp 2026-09-09, khung 1440×900/390×844) | ✅ |
*`SRS` §2.A.1 chỉ có 1 `SCR` (`SCR-04`) — dòng trên phủ đủ. Không có "empty-filter" (giỏ hàng không có bộ lọc, theo `WF` §3.1.3 — không phải thiếu, là N/A) và không có "submitting"/"invalid" ở cấp toàn màn hình — hai trạng thái này được minh hoạ **tại dòng CartItem** trong khối `default` (xem HIFI §"Minh hoạ dòng…") vì đây là hành vi cục bộ theo `SRS` §2.A.3.6, không phải trạng thái toàn trang.*
## 2. Redline từng màn hình
### 2.1 SCR-04 — Giỏ hàng
| Vùng (WF) | Kích thước desktop | Kích thước mobile | Padding | Gap | Token nền / viền |
|---|---|---|---|---|---|
| Z1 Header màn hình | full × auto | full × auto | `space.6` `space.4` (kế thừa `.ds-page`) | `space.2` giữa tiêu đề và số lượng | `bg.canvas` |
| Z2 Danh sách nhóm seller | 2/3 chiều rộng (`.ds-cols` cột trái, `minmax(0,2fr)`) | full | `space.4` mỗi card | `space.3` giữa các card, `space.2` trong card | `bg.surface` + `border.default`, `radius.md` mỗi card nhóm seller |
| Z3 Tổng kết | 1/3 chiều rộng (`.ds-cols` cột phải, `minmax(0,1fr)`), `position: sticky; top: space.4` | full width, **sticky đáy màn hình** (`position: sticky; bottom: 0`, không phải cố định tuyệt đối — tự cuộn hết trang mới dừng ở đáy) | `space.3` `space.4` (mobile) | `space.2` giữa C09 và C11 | `bg.surface`; mobile thêm `border-top: border.default` + `shadow.md` |
| Z4 Trạng thái đặc biệt | thay chỗ Z2 khi kích hoạt | thay chỗ Z2 khi kích hoạt | theo `.ds-empty`/`.ds-banner` mặc định | — | `bg.surface` (empty) / `status.danger-subtle` (error) |
| C-id | Thành phần | Lớp DS | Kích thước | Token chữ | Token màu | Trạng thái thể hiện trong HIFI |
|---|---|---|---|---|---|---|
| C01 | lbl_header_title | `<h1>` + span phụ | auto | `size.xl/semibold` (tiêu đề) + `size.md/regular` (số lượng) | `text.primary` + `text.secondary` | — (tĩnh) |
| C02 | grp_seller_header | `<h3>` trong `.ds-card` | auto | `size.md/semibold` | `text.primary` | — |
| C03 | img_cart_item | `.ds-thumb` | 64×64 desktop / 56×56 mobile *(đề xuất — chưa có nguồn SRS/WF chính xác, mức thấp)* | — | `bg.subtle` (ô xám khi lỗi tải) | — |
| C04 | lbl_product_name | text thường | auto | `size.sm/regular` | `text.primary` | — |
| C05 | lbl_variant | `.ds-small.ds-muted` | auto | `size.xs/regular` | `text.secondary` | — |
| C06 | lbl_unit_price | `.ds-small.ds-muted` | auto | `size.xs/regular` | `text.secondary` | 🟠 nguồn giá trị chưa chốt (`OQ-029`) — bố cục/token không đổi dù snapshot hay real-time |
| F01 | input_quantity | `.ds-stepper`, bọc trong `.ds-row.ds-m-full.ds-row--end` cùng C07 (mobile) | 96px desktop (đề xuất) / auto mobile, cao `size.control.desktop`=36 (44 mobile) | `size.sm/regular` | `border.strong`; `status.danger` khi invalid | default · disabled+spinner (đang gửi) · invalid (viền đỏ + `.ds-field__error`) |
| C07 | btn_delete_item | `.ds-btn.ds-btn--danger` | auto × 36 (44 mobile) | `size.sm/semibold` | `status.danger` nền, `text.inverse` chữ | default · hover (`filter:brightness(.92)`, xem `DS` §8 #1) · disabled+tooltip (đang xử lý) |
| C08 | lbl_seller_subtotal | text thường, căn phải | auto | `size.sm/regular` | `text.primary` | — |
| C09 | lbl_cart_total | text đậm | auto | `size.lg/semibold` | `text.primary` | default · rỗng (hiện "0 ₫") |
| C10 | btn_continue_shopping | `.ds-btn.ds-btn--primary` (trong `.ds-empty`) | auto × 36 (44 mobile) | `size.sm/semibold` | `brand.primary` | chỉ hiện ở `empty-none` |
| C11 | btn_checkout | `.ds-btn.ds-btn--primary`, `width:100%` | full-width × 36 (44 mobile) | `size.sm/semibold` | `brand.primary`; opacity .45 khi disabled | default (enabled) · rỗng (disabled + tooltip) |
| C12 | banner_error_load | `.ds-banner.ds-banner--error` | 100% | `size.sm/regular` | `status.danger-subtle` nền, `status.danger` chữ/icon | chỉ hiện ở `error` |
| C13 | btn_retry_load | `.ds-btn.ds-btn--secondary`, cao 32px (biến thể nhỏ, không phải token chuẩn — 🟠 ghi nhận) | auto | `size.sm/semibold` | `bg.surface` + `border.strong` | chỉ hiện ở `error` |
**Redline mobile (v1.1) — nhóm thao tác mỗi dòng `CartItem`:** ở mobile (<768px), khối text (C04–C06, `flex:1 1 160px;min-width:0`) và khối thao tác (F01 + C07, + message lỗi `E-CART-0002` hoặc spinner khi đang gửi) **rơi xuống hàng riêng, căn phải** — khối thao tác bọc trong `<div class="ds-row ds-m-full ds-row--end">` (tiện ích `.ds-m-full` mới ở `DS` §3/`ds-components` v1.1: `flex-basis:100%` dưới 768px). Desktop giữ nguyên một hàng ngang. *Nguồn: không có trong `WF` §3.1.4 (chỉ ghi "Tablet/Mobile: giữ danh sách dạng dọc", không xuống mức chi tiết trong-một-dòng) ⇒ đây là đề xuất Design để sửa lỗi vỡ layout phát hiện qua `png/SCR-04.mobile.default.png`, Designer/PO xác nhận ở G3, không đổi hành vi/text.*
**Khác biệt so với `WF` §3.1:** *(bắt buộc liệt kê — xem §6 để có đầy đủ lý do/người quyết; ở đây chỉ tóm số dòng)* 6 dòng — 3 dòng bố cục (Z3 dùng 1 markup responsive thay vì 2 khối desktop/mobile riêng như `WF`; C13 cao 32px không đúng `size.control.desktop`; nhóm thao tác F01+C07 xuống hàng riêng ở mobile — mới, xem trên), 1 dòng token thiếu (hover nút nguy hiểm), 1 dòng text (nhãn "Huỷ" trong modal xác nhận), 1 dòng dữ liệu minh hoạ (số liệu mẫu khác `WF` để tự nhất quán số học — xem §6.
## 3. Ánh xạ trạng thái nghiệp vụ → hiển thị
| Trạng thái (BR §3) | Badge (DS §3) | Nhãn nguyên văn (SRS §4.2) | Hành động bị khoá (W7, SRS §2.3.1) |
|---|---|---|---|
| *(không áp dụng ở SCR-04)* | — | — | — |
**Vì sao rỗng:** US-002/US-003 chỉ thao tác trên `Cart.status = active` (`SRS` §1.4) và **không hiển thị badge trạng thái** `Cart`/`Order`/`Payment` nào ở màn hình này (`SRS` §2.A.3.1 không có thành phần badge). Bảng ánh xạ badge ↔ trạng thái nghiệp vụ (từ `BR_CartCheckout` §3) đã được lập sẵn ở cấp project tại `DS_e-commerce_v1.0.md` §3 để các `US` sau (checkout, quản lý đơn) dùng lại đúng token, không định nghĩa lại ở đây.
## 4. Luồng prototype
*N/A — activity `proto` không chạy ở lần này (ngoài phạm vi được giao: chỉ `ds`, `hifi`, `figma`, `dspec`). `HIFI_US002-003_v1.0.html` chưa gắn `data-nav`. Sơ đồ điều hướng hành vi đã có ở `SRS` §2.A.2 (mermaid) — không lặp lại ở đây. Khi chạy `proto` sau này: 3 cạnh cần gắn `data-nav` là SCR-01→SCR-04 (icon giỏ hàng), SCR-04→SCR-05 (C11 Checkout, ngoài phạm vi hành vi SRS này), SCR-04→SCR-01 (C10, giỏ rỗng).*
## 5. Khả năng truy cập
| Kiểm tra | Kết quả | Bằng chứng |
|---|---|---|
| Tương phản chữ (AA ≥ 4.5) trên mọi cặp dùng trong HIFI | ✅ Đạt — xác nhận bằng máy | `design-check.mjs` T2: 10/10 cặp đạt (chạy 2026-09-09, người điều phối), xem `DS_e-commerce_v1.0.md` §6 |
| Hit target mobile ≥ 44 px cho mọi nút/ô nhập | ✅ | `size.control.touch` áp qua `body[data-view="mobile"]` cho `.ds-btn`, `.ds-stepper button` (kế thừa `.ds-btn` height rule qua CSS chung — riêng `.ds-stepper` dùng `size.control.desktop` cố định 36px cả hai khung nhìn, 🔴 chưa có rule mobile riêng cho `.ds-stepper`, xem §6) |
| Thứ tự focus khớp `WF` §3.x.6 | 🟡 | `WF` §3.1.6 chỉ xác nhận "F01 dòng đầu tiên" ở mức thấp, chưa xác nhận với PO/Dev FE — thứ tự DOM trong HIFI đặt F01 dòng 1 trước, khớp đề xuất |
| Mọi ô nhập có nhãn; mọi icon-button có `aria-label` | ✅ | F01 dùng `aria-label="Số lượng"` trên input của `.ds-stepper`; nút đóng modal có `aria-label="Đóng"` |
| Không truyền nghĩa chỉ bằng màu (badge có chữ, lỗi có icon + text) | ✅ | Banner C12 và message F01 đều có icon + text; không dùng badge màu-only ở SCR-04 |
| Tắt motion khi `prefers-reduced-motion` | ☐ | Chưa thêm rule `@media (prefers-reduced-motion: reduce)` cho `.ds-spinner`/`.ds-skeleton` trong `HIFI` — kế thừa từ `ds-components.css` gốc (cũng chưa có) — ghi nhận nợ chung ở `DS` §7, không riêng US này |
## 6. Lệch HIFI ↔ WF / SRS / DS
| # | Màn hình | HIFI có | Tài liệu nói | Loại | Bên thắng | Xử lý | Ai quyết | Trạng thái |
|---|---|---|---|---|---|---|---|---|
| 1 | SCR-04 | Z3 (Tổng kết) dùng **một** khối markup duy nhất, đổi vị trí bằng CSS responsive (`position: sticky`, đổi `top`/`bottom` theo `body[data-view]`) | `WF_US002-003_v1.0.html` dựng **hai** khối DOM riêng cho desktop (sidebar) và mobile (footer), ẩn/hiện bằng `d-hide`/`m-hide` | Bố cục | HIFI (kỹ thuật hiện thực hợp lệ hơn, cùng kết quả thị giác với `WF`) | Không cần sửa `WF` — đây là chi tiết dựng HTML, không đổi vị trí/nội dung nhìn thấy. Dev FE được phép chọn 1-DOM hoặc 2-DOM tuỳ stack, miễn giữ đúng vị trí/kết quả thị giác đã tả ở §2.1 | Designer/Dev FE (không chặn G3) | ✅ Đã xử lý — ghi nhận để không ai tưởng khác biệt |
| 2 | SCR-04 | Nút "Xoá" (C07) dùng `.ds-btn--danger:hover{filter:brightness(.92)}` (giải pháp tạm) | `DS_e-commerce_v1.0.md` §4 ghi "chưa có token `color.status.danger-hover` riêng" | Component/Token | Chưa quyết | Bổ sung token `color.status.danger-hover` chính thức vào `tokens.json` rồi thay `filter` bằng biến — xem `DS` §8 #1, `OQ-035` | Tech Lead / Dev FE | ☐ (`OQ-035`) |
| 3 | *(modal minh hoạ)* | Nút "Huỷ" trong modal xác nhận xoá không có `data-key` (không có khoá ở `SRS` §4.2) | `SRS_US002-003_v1.0.md` §4.2 không liệt kê nhãn "Huỷ" cho modal này (chỉ có `cart.delete.confirm.title`/`.body`) | Text | Chưa quyết | BA bổ sung khoá `cart.delete.confirm.cancel` = "Huỷ" vào `SRS` §4.2 ở version sau; hiện tại `HIFI` dùng nhãn "Huỷ" theo quy ước chung `UICONV` §9 (nhãn nút = động từ) — không đổi hành vi | BA (khi cập nhật `SRS`), không chặn G3 vì rủi ro thấp (từ phổ biến, không mơ hồ) | ☐ (mức thấp) |
| 4 | SCR-04 | C13 "Thử lại" cao 32px (`height:32px` inline) thay vì `size.control.desktop`=36px | `DS_e-commerce_v1.0.md` §1.3 quy định chiều cao control chuẩn là 36px desktop | Token | Chưa quyết | Đây là biến thể "nút nhỏ trong banner" — cần `DS` bổ sung token `size.control.compact` chính thức nếu muốn dùng lại, hoặc đổi về 36px cho nhất quán | Designer/Tech Lead | ☐ (mức thấp, không chặn G3) |
| 5 | SCR-04 | Số liệu minh hoạ trong `HIFI`/SVG (Shop A 250.000₫ + Shop B 260.000₫ = 510.000₫) khác số liệu minh hoạ trong `WF_US002-003_v1.0.html` (250.000₫ + 200.000₫ hiển thị nhưng tổng ghi 585.000₫ — vốn đã sai số học trong `WF`) | `WF` không có bảng dữ liệu mẫu chính thức (chỉ minh hoạ bố cục); `AC-US002-01` có bộ dữ liệu mẫu riêng (585.000₫, 3+2 dòng) | Dữ liệu minh hoạ | HIFI (tự sửa lỗi số học nội bộ của `WF`, không phải dữ liệu nghiệp vụ) | Không cần sửa `SRS`/`AC` (không phải nguồn sự thật về số liệu — chỉ là ví dụ minh hoạ bố cục); khuyến nghị BA sửa số ở `WF` khi có dịp cập nhật để ba nơi (WF/HIFI/AC) không mỗi nơi một bộ số, tránh Dev FE nhầm là dữ liệu thật | BA (không chặn G3 — không phải dữ liệu quy định hành vi) | ☐ (mức thấp) |
| 6 | SCR-04 | *(v1.0)* `SCR-04.desktop.svg` không vẽ `F01`/`C07` cho dòng "Balo du lịch" (Shop B) — thiếu component so với `HIFI` | `HIFI_US002-003_v1.0.html` có đủ F01/C07 cho mọi dòng `CartItem` hiển thị | SVG thiếu component | FIGMA phải khớp HIFI | **Đã sửa ở v1.1** — bổ sung `<g id="F01">`/`<g id="C07">` cho dòng "Balo du lịch" trong `SCR-04.desktop.svg` | BA/Design | ✅ Đã xử lý (2026-09-09) |
| 7 | SCR-04 | *(v1.0)* `HIFI` mobile (390px): khối text (C04–C06) và khối thao tác (F01+C07) cùng một hàng `.ds-row` không đủ chỗ ⇒ tên sản phẩm bị bẻ từng chữ, stepper chồng lên text (phát hiện qua `png/SCR-04.mobile.default.png`) | `DS` §1.3 chỉ quy định chiều cao control, không quy định hành vi xuống hàng ở mobile cho nhóm nhiều thành phần trong một `.ds-row` | Bố cục (lỗi) | HIFI phải sửa | **Đã sửa ở v1.1** — nâng `ds-components` lên v1.1 (thêm `.ds-m-full`), bọc khối thao tác trong `.ds-row.ds-m-full.ds-row--end` để xuống hàng riêng ở mobile; cập nhật `SCR-04.mobile.svg` theo bố cục mới; xem redline mobile ở §2.1 | BA/Design (đã xử lý, Designer/PO xác nhận thêm ở G3 nếu cần) | ✅ Đã xử lý (2026-09-09) |
**Kết quả `design-check.mjs`** (chạy bởi người điều phối, 2026-09-09):
```
✅ 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
✅ H1 components.html: khối :root khớp tokens.json
✅ H2 components.html: không có mã màu thô trong markup
✅ H1 HIFI_US002-003_v1.0.html: khối :root khớp tokens.json
✅ H1 HIFI_US002-003_v1.0.html: khối ds-components khớp components.html
✅ H2 HIFI_US002-003_v1.0.html: không có mã màu thô trong markup
✅ H4 HIFI_US002-003_v1.0.html: text data-key khớp SRS §4.2
✅ H3 C-id/F-id khớp hai chiều (14 ID)
✅ H5 HIFI phủ 1/1 SCR của SRS §2.1
✅ S1 SVG: 2 file, màu ⊆ token
✅ S1 SVG phủ desktop+mobile cho 1 SCR
ok — không có lỗi 🔴
```
*(Kết quả trên chạy trên bản HIFI **trước** khi sửa mobile ở §6 dòng #7 — cấu trúc DOM đổi (bọc thêm `<div class="ds-row ds-m-full ds-row--end">`) không đổi tập `data-c`/`data-f`/`data-key` nên H1–H5 vẫn đúng; khuyến nghị người điều phối chạy lại một lần nữa sau bản v1.1 để có phiên bản kết quả khớp chính xác file mới nhất — xem `humanInputNeeded`.)* C-id lệch hai chiều: rỗng (14/14) · data-key text lệch SRS §4.2: rỗng · mã màu thô ngoài token: 0 · SCR thiếu: 0.
## 7. Bàn giao cho Dev FE
| | |
|---|---|
| **Lấy từ HIFI** | Bố cục, khoảng cách, token, trạng thái thị giác, thứ tự DOM/focus. Mở `HIFI_US002-003_v1.0.html?ids=1&zones=1` để thấy `C-id`/`F-id` và vùng `Zn`; dùng dropdown "Trạng thái" để xem `loading`/`empty-none`/`error` |
| **Lấy từ SRS** | Hành vi, điều kiện ẩn/khoá, validation, mã lỗi, text (`HIFI` chỉ *chép* text từ `SRS_US002-003_v1.0.md` §4.2). **5 `OQ` còn chặn G3** (`OQ-028`…`OQ-030`, `OQ-032`, `OQ-033`) — Dev FE **không** code phần phụ thuộc các câu hỏi này cho tới khi có câu trả lời |
| **Lấy từ DS** | `00-index/ds/tokens.json` → biến CSS/theme của stack đã chọn (khi `OQ-022` được trả lời); lớp `.ds-*` → component tương ứng (`DS` §3 cột 3, hiện để trống) |
| **Không được suy ra từ HIFI** | Dữ liệu mẫu (số liệu, tên sản phẩm — xem §6 dòng #5) · giới hạn trên số lượng F01 (`OQ-030`) · nguồn giá C06 (`OQ-029`) · hành vi khi số lượng về 0 (`OQ-028`) |
| **Khi Designer sửa trong Figma** | Báo BA ⇒ chạy lại `hifi` + `figma` với `--proto <link>`; `SRS` chỉ đổi nếu lệch Tồn tại/Hành vi được PO chấp nhận |
| **Ảnh tham chiếu** | `FIGMA_US002-003_v1.0/png/` — 8 file đã có (chụp 2026-09-09, xem §1). **Lưu ý:** ảnh mobile chụp từ bản HIFI trước khi sửa lỗi bố cục §6 #7 — người điều phối nên chạy lại `render-figma.sh` sau bản v1.1 để có ảnh khớp bố cục mobile mới, xem `humanInputNeeded` |
| **Về màu/token** | 🔴 Toàn bộ màu là placeholder trung tính — **không lập trình cứng các mã hex trong SVG/HIFI vào code sản phẩm**; chờ `OQ-022` (design system) trước khi coi bảng màu là chính thức, nếu không sẽ phải đổi lại toàn bộ khi có brand thật |
## 8. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | 🔴 chặn G3? |
|---|---|---|---|---|---|
| DS-EX-01 | **Ngoại lệ gate G2** (nhắc lại, không phải câu hỏi nghiệp vụ mới) — `DS`/`HIFI`/`FIGMA`/`DSPEC` của `SCR-04` được sinh khi G2 module Giỏ hàng & Checkout chưa ký chính thức (`DEC-02`, `DEC-05`, `DEC-06`, `DEC-07`). Nếu `BACKLOG`/`BR`/`SRS` đổi khi G2 ký thật, bốn tệp này phải chạy lại toàn bộ (không chỉ rà một phần) vì `HIFI` chép trực tiếp thành phần/text từ `SRS` | PO, Tech Lead | 2026-09-08 | Baseline `DS`/`HIFI`/`FIGMA`/`DSPEC` của `SCR-04` | `DECISION_e-commerce.md` `DEC-07` |
| OQ-035 | *(nhắc lại từ `DS` §9)* Token `color.status.danger-hover` chưa tồn tại — bổ sung chính thức hay giữ `filter: brightness()` tạm thời? | Tech Lead / Dev FE | 2026-09-08 | §6 dòng #2 | ☐ (không chặn G3) |
| DS-EX-02 | Nhãn "Huỷ" cho nút huỷ trong modal xác nhận xoá `CartItem` chưa có khoá i18n ở `SRS` §4.2 — BA có cần bổ sung khoá riêng (VD `cart.delete.confirm.cancel`) ở lần cập nhật `SRS` tiếp theo không, hay dùng chung một nhãn "Huỷ" cho mọi modal xác nhận toàn hệ thống (không cần khoá riêng theo màn hình)? | BA / PO | 2026-09-08 | §6 dòng #3 | ☐ (không chặn G3, mức thấp) |
*Năm `OQ` chặn G3 về mặt hành vi (`OQ-028`…`OQ-030`, `OQ-032`, `OQ-033`) đã có ở `SRS_US002-003_v1.0.md` §8 — không lặp lại ở đây; chúng không phải OQ của riêng `ba-design` nhưng **chặn** `HIFI` hoàn thiện các micro-state liên quan (đặc biệt `OQ-028` ảnh hưởng trực tiếp hành vi khi F01 về 0, hiện `HIFI` chỉ minh hoạ trạng thái "invalid" chung chung, không vẽ nhánh cụ thể theo phương án (a)/(b) của `OQ-028`).*
---
## Tự chấm
| # | Tiêu chí | ☐/✅ | Ghi chú |
|---|---|---|---|
| 1 | Mọi `SCR` của `SRS` §2.1 có mặt trong HIFI (`#SCR-xx`) và §1 | ✅ | 1/1 `SCR-04` |
| 2 | Tập `data-c`/`data-f` trong HIFI khớp **hai chiều** với `SRS` §2.3.1 / §2.3.3 | ✅ | `design-check.mjs` H3: khớp hai chiều (14 ID), xác nhận bằng máy 2026-09-09 |
| 3 | Mọi `data-key` có text **nguyên văn** bằng `SRS` §4.2 (W5) | ✅ | `design-check.mjs` H4: text data-key khớp SRS §4.2, xác nhận bằng máy 2026-09-09 |
| 4 | Mọi trạng thái `SRS` §2.3.6 có khối `data-state` tương ứng; hai trạng thái rỗng trông khác nhau | 🟡 | Đủ `default`/`loading`/`empty-none`/`error`; SCR-04 chỉ có 1 loại rỗng (đúng theo `WF` §3.1.3, không phải thiếu) |
| 5 | HIFI chỉ dùng biến `--ds-*` và lớp `.ds-*`; không mã màu thô trong markup | ✅ | `design-check.mjs` H2: không có mã màu thô, xác nhận bằng máy 2026-09-09 |
| 6 | Khối `:root` và khối `ds-components` trong HIFI **trùng nguyên văn** với `ds/tokens.css` + `ds/components.html` | ✅ | `design-check.mjs` H1: khối `:root` và `ds-components` khớp, xác nhận bằng máy 2026-09-09 (bản v1.1 nâng cùng lúc lên `ds-components` v1.1 ở cả hai file, giữ khớp nguyên văn) |
| 7 | Mỗi SCR có ≥ 2 khung nhìn (desktop + mobile) trong HIFI **và** trong `FIGMA/*.svg` | ✅ | HIFI: toolbar `desktop`/`mobile`; FIGMA: `SCR-04.desktop.svg` + `SCR-04.mobile.svg`; `design-check.mjs` S1 xác nhận |
| 8 | Mã màu trong SVG ⊆ tập token | ✅ | `design-check.mjs` S1: màu ⊆ token, xác nhận bằng máy 2026-09-09 |
| 9 | §5 tương phản đạt AA cho mọi cặp chữ | ✅ | `design-check.mjs` T2: 10/10 cặp đạt, xác nhận bằng máy 2026-09-09 |
| 10 | §6 đã điền; không còn lệch Tồn tại/Hành vi/Text với SRS | ✅ | 7 dòng lệch (2 dòng mới #6/#7 đã xử lý), **không có dòng nào loại Tồn tại/Hành vi** (chỉ Bố cục/Token/Text mức thấp, 2 dòng đã Đã xử lý) — phù hợp vì `HIFI` chỉ chép từ `SRS`, không tự thêm hành vi |
| 11 | Có chữ ký Designer (hoặc PO + `DEC-nn`) | ☐ | Chưa ký thật — chờ PO tại G3 (`DEC-01`) |

View File

@@ -0,0 +1,58 @@
# FIGMA_US002-003_v1.0 — Gói nhập vào Figma
| | |
|---|---|
| **Sinh bởi** | skill `ba-design`, activity `figma` |
| **US / màn hình** | US-002 (Xem giỏ hàng nhóm theo seller), US-003 (Sửa số lượng / xoá sản phẩm trong giỏ) — cùng `SCR-04` Giỏ hàng |
| **Nguồn** | `../HIFI_US002-003_v1.0.html` (bố cục v1.1, 2026-09-09) · `00-index/DS_e-commerce_v1.0.md` (v1.1, `00-index/ds/tokens.json`) · `03-specification/WF_US002-003_v1.0.md` · `03-specification/SRS_US002-003_v1.0.md` |
| **Vì sao không có file `.fig`** | `.fig` là định dạng nhị phân độc quyền; REST API của Figma không cho tạo node thiết kế. Gói này gồm các định dạng Figma **nhập được** và giữ được layer/text/token. |
| **Độ tin cậy visual** | 🔴 — token trong `tokens.dtcg.json` là placeholder trung tính của template `ba-design` (dự án chưa có brand guideline — SAD §7.0, chưa chọn design system — `OQ-022`). Designer/PO cần chốt trước khi coi các file SVG này là visual cuối cùng |
| **v1.1 (2026-09-09)** | Bổ sung `F01`/`C07` còn thiếu cho dòng "Balo du lịch" ở `SCR-04.desktop.svg`; cập nhật `SCR-04.mobile.svg` theo bố cục mobile mới (nhóm thao tác F01+C07 xuống hàng riêng — theo `ds-components` v1.1, tiện ích `.ds-m-full`); `png/` đã có 8 ảnh (xem bên dưới) |
## Nội dung gói
| File | Định dạng | Nhập vào Figma bằng | Sửa được trong Figma |
|---|---|---|---|
| `SCR-04.desktop.svg` | SVG (1440×900), trạng thái mặc định — đủ `F01`/`C07` cho **mọi** dòng `CartItem` (kể cả "Balo du lịch", bổ sung ở v1.1) | Kéo thả vào canvas, hoặc **File → Place image / Import** | ✅ Layer vector, text, tên layer = `Z1`/`Z2`/`Z3`/`C02`/`C03`/`C04`/`C05`/`C06`/`F01`/`C07`/`C08`/`C09`/`C11` |
| `SCR-04.mobile.svg` | SVG (390×844), trạng thái mặc định — cập nhật v1.1: nhóm thao tác (F01+C07) nằm hàng riêng dưới khối text, khớp `.ds-m-full` trong HIFI | Kéo thả | ✅ Layer vector — `Z3` là footer sticky đáy màn hình (khác desktop: sidebar bên phải) theo `WF` §2 |
| `tokens.dtcg.json` | Design token chuẩn W3C DTCG (bản sao `00-index/ds/tokens.json`) | Plugin **Tokens Studio for Figma** → Import → JSON; hoặc **Variables → Import** nếu tài khoản có API variables | ✅ Thành Figma Variables / Styles |
| `png/` | 8 ảnh chụp `HIFI_US002-003_v1.0.html` bằng Chrome headless (`scripts/render-figma.sh`, chụp 2026-09-09): `SCR-04.{desktop,mobile}.{default,loading,empty-none,error}.png` | Kéo thả | ❌ Chỉ để tham chiếu và đối chiếu pixel — **ảnh mobile chụp từ bản HIFI trước khi sửa lỗi bố cục §"v1.1" ở trên, nên chụp lại** (xem "Việc còn lại") |
| `../HIFI_US002-003_v1.0.html` | HTML tự chứa | Plugin **html.to.design** (divRIOTS) → *Import from file* hoặc *from URL* khi host tạm bằng `python3 -m http.server` | ✅ Layer đầy đủ theo DOM, auto-layout |
## Thứ tự nhập khuyến nghị
1. **Token trước:** nhập `tokens.dtcg.json` bằng Tokens Studio → *Create Styles / Variables* để có bảng màu, chữ, khoảng cách trùng tên `--ds-*`. Vì token còn placeholder (🔴), đặt tên style rõ ràng kiểu "e-commerce/placeholder/…" để không nhầm là bảng màu chính thức.
2. **Component sheet:** nhập `../../00-index/ds/components.html` bằng html.to.design vào page *Components*; hoặc dựng component từ SVG và gán style vừa tạo.
3. **Màn hình:** kéo `SCR-04.desktop.svg` và `SCR-04.mobile.svg` vào page *Screens*, đổi tên frame thành `SCR-04 · Giỏ hàng · desktop` / `SCR-04 · Giỏ hàng · mobile`.
4. **Trạng thái khác** (đang tải, rỗng, lỗi tải — xem `HIFI_US002-003_v1.0.html?scr=SCR-04&state=…`): gói này **chưa có SVG riêng cho từng trạng thái** (chỉ có `default`) — Designer dựng thêm trong Figma dựa trên `HIFI` (mở với `?state=loading`/`empty-none`/`error` để xem) nếu cần, hoặc yêu cầu chạy lại `figma` với các hậu tố trạng thái.
5. **Đối chiếu:** mở `png/` cạnh frame để kiểm tra khoảng cách và màu khớp HIFI — đã có 8 ảnh (2026-09-09); **ảnh mobile nên chụp lại** sau bản HIFI v1.1 (xem "Việc còn lại").
## Lưu ý
- **Font:** SVG ghi font `Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif` (system stack, chưa có font brand); máy không có font ⇒ Figma thay bằng font gần nhất. Không cần cài thêm font đặc biệt ở lần chạy này.
- **Màu trong SVG là mã nguyên văn** từ `tokens.dtcg.json` (Figma không đọc biến CSS); thuộc tính `data-token` ghi tên token để tra ngược. Sửa màu ⇒ sửa `00-index/ds/tokens.json` rồi chạy lại `ds` + `hifi` + `figma`, không sửa tay SVG.
- **Hành vi không nằm trong Figma.** Prototype link trong Figma chỉ mô phỏng điều hướng theo `SRS` §2.A.2; điều kiện, validation, mã lỗi đọc ở `SRS_US002-003_v1.0.md`. Khi Figma và SRS lệch: SRS thắng về tồn tại/hành vi/text, Figma thắng về bố cục (W11) — ghi lệch vào `WF_US002-003_v1.0.md` §5 (đã có 4 dòng chờ PO/Tech Lead quyết, xem đó trước khi sửa SVG theo ý riêng).
- **Không dùng gói này để thay `HIFI`.** `HIFI_US002-003_v1.0.html` là nguồn sự thật về visual đã đề xuất (🔴 chưa duyệt); Figma là bản làm việc của Designer. Designer sửa trong Figma ⇒ báo BA để chạy lại `hifi` + `figma` với `--proto <link Figma>`.
- **Số liệu trong SVG là dữ liệu minh hoạ** (VD "Áo thun basic", "250.000 ₫") để hình dung bố cục thật — không phải dữ liệu SRS quy định (SRS không có ví dụ số cụ thể ngoài `AC_US-002_v1.0.md` §Dữ liệu mẫu, vốn dùng bộ số khác). Không suy ra số liệu nghiệp vụ từ SVG/HIFI.
## Việc còn lại — cần người điều phối (có Bash/Chrome)
| # | Việc | Lệnh |
|---|---|---|
| 1 | Chụp lại PNG mobile sau bản HIFI v1.1 (sửa lỗi bố cục §"v1.1" ở trên) — desktop không đổi cấu trúc DOM, không bắt buộc chụp lại nhưng chụp lại toàn bộ cho đồng bộ cũng được | `.claude/skills/ba-design/scripts/render-figma.sh 03-specification/design/HIFI_US002-003_v1.0.html 03-specification/design/FIGMA_US002-003_v1.0/png` |
| 2 | Chạy lại phép kiểm máy đầy đủ (T1/T2/H1–H5/S1) trên bản HIFI/SVG v1.1 mới nhất — lần chạy 2026-09-09 (chép ở `DSPEC` §6) thực hiện trên bản trước khi sửa mobile | xem lệnh ở `DSPEC_US002-003_v1.0.md` §6 |
## Kiểm tra trước khi giao
```bash
node .claude/skills/ba-design/scripts/design-check.mjs --tokens 00-index/ds/tokens.json --svg-dir 03-specification/design/FIGMA_US002-003_v1.0
```
Kết quả chạy 2026-09-09 (người điều phối, trên bản SVG trước khi bổ sung F01/C07 dòng "Balo du lịch"):
```
✅ S1 SVG: 2 file, màu ⊆ token
✅ S1 SVG phủ desktop+mobile cho 1 SCR
```
Tự kiểm tay bản v1.1 (sau khi bổ sung component còn thiếu): 9 mã màu dùng trong hai SVG đều khớp `tokens.json` (`#F6F7F9`, `#1B1F24`, `#5B6472`, `#FFFFFF`, `#D5DAE1`, `#EEF1F5`, `#767F8C`, `#B42318`, `#2456A6`); tập id `C02, C03, C04, C05, C06, F01, C07, C08, C09, C11` ⊆ tập `data-c`/`data-f` của `HIFI_US002-003_v1.0.html` (14 ID: `C01`…`C13`, `F01`) — **khuyến nghị chạy lại lệnh trên bằng máy để có kết quả chính thức cho bản v1.1**.

View File

@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SCR-04.desktop.svg — khung Giỏ hàng (US-002/US-003), trạng thái default, khung nhìn desktop (1440×900).
Nguồn: HIFI_US002-003_v1.0.html#SCR-04 · SRS_US002-003_v1.0.md §2.A.3.1/§4.2 · WF_US002-003_v1.0.md §3.1.1/§3.1.2 · 00-index/ds/tokens.json
Màu là mã nguyên văn từ tokens.json (Figma không đọc biến CSS) kèm data-token để tra ngược.
Layer <g id="Zn"> theo WF §3.1.1; <g id="Cnn"> theo SRS §2.A.3.1. Text nguyên văn SRS §4.2 (số liệu minh hoạ không phải một phần khoá i18n).
Độ tin cậy visual: 🔴 (DS_e-commerce §0 — token placeholder trung tính, chưa có brand/design system, OQ-022).
v1.1 (2026-09-09): bổ sung F01/C07 còn thiếu cho dòng "Balo du lịch" (Shop B) — theo ghi chú người duyệt, SVG phải phản chiếu đủ C-id của HIFI cho mỗi dòng hiển thị.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900" viewBox="0 0 1440 900" font-family="Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif" font-size="14" data-scr="SCR-04" data-view="desktop" data-state="default" data-source="HIFI_US002-003_v1.0.html#SCR-04">
<title>SCR-04 · Giỏ hàng · desktop · default</title>
<rect id="canvas" width="1440" height="900" fill="#F6F7F9" data-token="color.bg.canvas"/>
<g id="Z1" data-name="Header màn hình" transform="translate(120 48)">
<text x="0" y="28" font-size="24" font-weight="600" fill="#1B1F24" data-token="color.text.primary" data-key="cart.title">Giỏ hàng của bạn</text>
<text x="230" y="28" font-size="16" fill="#5B6472" data-token="color.text.secondary" data-key="cart.itemCount">(5 sản phẩm — minh hoạ)</text>
</g>
<g id="Z2" data-name="Danh sách nhóm seller" transform="translate(120 104)">
<!-- Nhóm seller Shop A -->
<g data-name="Card nhóm seller">
<rect width="800" height="164" rx="8" fill="#FFFFFF" stroke="#D5DAE1" data-token="color.border.default"/>
<g id="C02"><text x="16" y="30" font-size="16" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Shop A</text></g>
<g data-name="CartItem 1" transform="translate(16 44)">
<g id="C03"><rect width="64" height="64" rx="4" fill="#EEF1F5" stroke="#D5DAE1" data-token="color.bg.subtle"/></g>
<g transform="translate(76 4)">
<text id="C04" y="16" fill="#1B1F24" data-token="color.text.primary">Áo thun basic</text>
<text id="C05" y="36" font-size="12" fill="#5B6472" data-token="color.text.secondary">Size M, Đỏ</text>
<text id="C06" y="54" font-size="12" fill="#5B6472" data-token="color.text.secondary">100.000 ₫</text>
</g>
<g id="F01" transform="translate(560 12)">
<rect width="112" height="36" rx="4" fill="#FFFFFF" stroke="#767F8C" data-token="color.border.strong"/>
<text x="20" y="23" fill="#1B1F24" data-token="color.text.primary">− 2 +</text>
</g>
<g id="C07" transform="translate(690 12)">
<rect width="80" height="36" rx="4" fill="#B42318" data-token="color.status.danger"/>
<text x="40" y="23" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.delete">Xoá</text>
</g>
</g>
<g id="C08" transform="translate(600 132)"><text text-anchor="end" x="184" fill="#1B1F24" data-token="color.text.primary">Tạm tính: 250.000 ₫</text></g>
</g>
<!-- Nhóm seller Shop B -->
<g transform="translate(0 180)" data-name="Card nhóm seller">
<rect width="800" height="164" rx="8" fill="#FFFFFF" stroke="#D5DAE1" data-token="color.border.default"/>
<g id="C02"><text x="16" y="30" font-size="16" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Shop B</text></g>
<g data-name="CartItem 2" transform="translate(16 44)">
<g id="C03"><rect width="64" height="64" rx="4" fill="#EEF1F5" stroke="#D5DAE1" data-token="color.bg.subtle"/></g>
<g transform="translate(76 4)">
<text id="C04" y="16" fill="#1B1F24" data-token="color.text.primary">Balo du lịch</text>
<text id="C05" y="36" font-size="12" fill="#5B6472" data-token="color.text.secondary">Đen</text>
<text id="C06" y="54" font-size="12" fill="#5B6472" data-token="color.text.secondary">200.000 ₫</text>
</g>
<g id="F01" transform="translate(560 12)">
<rect width="112" height="36" rx="4" fill="#FFFFFF" stroke="#767F8C" data-token="color.border.strong"/>
<text x="20" y="23" fill="#1B1F24" data-token="color.text.primary">− 1 +</text>
</g>
<g id="C07" transform="translate(690 12)">
<rect width="80" height="36" rx="4" fill="#B42318" data-token="color.status.danger"/>
<text x="40" y="23" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.delete">Xoá</text>
</g>
</g>
<g id="C08" transform="translate(600 132)"><text text-anchor="end" x="184" fill="#1B1F24" data-token="color.text.primary">Tạm tính: 260.000 ₫</text></g>
</g>
</g>
<g id="Z3" data-name="Tổng kết (sidebar desktop / footer mobile)" transform="translate(960 104)">
<rect width="360" height="120" rx="8" fill="#FFFFFF" stroke="#D5DAE1" data-token="color.border.default"/>
<g id="C09"><text x="16" y="36" font-size="20" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Tổng cộng: 510.000 ₫</text></g>
<g id="C11" transform="translate(16 56)">
<rect width="328" height="36" rx="4" fill="#2456A6" data-token="color.brand.primary"/>
<text x="164" y="23" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.checkout">Tiến hành Checkout</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
SCR-04.mobile.svg — khung Giỏ hàng (US-002/US-003), trạng thái default, khung nhìn mobile (390×844).
Z3 (Tổng kết) hiển thị dạng footer sticky đáy màn hình theo WF §2/§3.1.1 (khác desktop: sidebar bên phải).
Nguồn/quy tắc màu — xem SCR-04.desktop.svg. Độ tin cậy visual: 🔴 (OQ-022).
v1.1 (2026-09-09): bổ sung F01/C07 còn thiếu cho dòng "Balo du lịch" (Shop B), đặt ở hàng riêng dưới khối text
(khớp bản sửa lỗi bố cục mobile của HIFI — ds-components v1.1, .ds-m-full — xem DSPEC §2.1).
-->
<svg xmlns="http://www.w3.org/2000/svg" width="390" height="844" viewBox="0 0 390 844" font-family="Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif" font-size="13" data-scr="SCR-04" data-view="mobile" data-state="default" data-source="HIFI_US002-003_v1.0.html#SCR-04">
<title>SCR-04 · Giỏ hàng · mobile · default</title>
<rect id="canvas" width="390" height="844" fill="#F6F7F9" data-token="color.bg.canvas"/>
<g id="Z1" data-name="Header màn hình" transform="translate(16 20)">
<text x="0" y="24" font-size="18" font-weight="600" fill="#1B1F24" data-token="color.text.primary" data-key="cart.title">Giỏ hàng của bạn</text>
<text x="0" y="42" font-size="12" fill="#5B6472" data-token="color.text.secondary" data-key="cart.itemCount">(5 sản phẩm — minh hoạ)</text>
</g>
<g id="Z2" data-name="Danh sách nhóm seller" transform="translate(16 76)">
<g data-name="Card nhóm seller">
<rect width="358" height="164" rx="8" fill="#FFFFFF" stroke="#D5DAE1" data-token="color.border.default"/>
<g id="C02"><text x="12" y="26" font-size="14" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Shop A</text></g>
<g data-name="CartItem 1" transform="translate(12 38)">
<g id="C03"><rect width="56" height="56" rx="4" fill="#EEF1F5" stroke="#D5DAE1" data-token="color.bg.subtle"/></g>
<g transform="translate(64 2)">
<text id="C04" y="14" fill="#1B1F24" data-token="color.text.primary">Áo thun basic</text>
<text id="C05" y="32" font-size="11" fill="#5B6472" data-token="color.text.secondary">Size M, Đỏ</text>
<text id="C06" y="48" font-size="11" fill="#5B6472" data-token="color.text.secondary">100.000 ₫</text>
</g>
</g>
<g id="F01" transform="translate(12 104)">
<rect width="100" height="44" rx="4" fill="#FFFFFF" stroke="#767F8C" data-token="color.border.strong"/>
<text x="18" y="27" fill="#1B1F24" data-token="color.text.primary">− 2 +</text>
</g>
<g id="C07" transform="translate(246 104)">
<rect width="100" height="44" rx="4" fill="#B42318" data-token="color.status.danger"/>
<text x="50" y="27" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.delete">Xoá</text>
</g>
</g>
<g id="C08" transform="translate(174 250)"><text text-anchor="end" x="184" fill="#1B1F24" data-token="color.text.primary">Tạm tính: 250.000 ₫</text></g>
<g transform="translate(0 280)" data-name="Card nhóm seller">
<rect width="358" height="164" rx="8" fill="#FFFFFF" stroke="#D5DAE1" data-token="color.border.default"/>
<g id="C02"><text x="12" y="26" font-size="14" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Shop B</text></g>
<g data-name="CartItem 2" transform="translate(12 38)">
<g id="C03"><rect width="56" height="56" rx="4" fill="#EEF1F5" stroke="#D5DAE1" data-token="color.bg.subtle"/></g>
<g transform="translate(64 2)">
<text id="C04" y="14" fill="#1B1F24" data-token="color.text.primary">Balo du lịch</text>
<text id="C05" y="32" font-size="11" fill="#5B6472" data-token="color.text.secondary">Đen</text>
<text id="C06" y="48" font-size="11" fill="#5B6472" data-token="color.text.secondary">200.000 ₫</text>
</g>
</g>
<g id="F01" transform="translate(12 104)">
<rect width="100" height="44" rx="4" fill="#FFFFFF" stroke="#767F8C" data-token="color.border.strong"/>
<text x="18" y="27" fill="#1B1F24" data-token="color.text.primary">− 1 +</text>
</g>
<g id="C07" transform="translate(246 104)">
<rect width="100" height="44" rx="4" fill="#B42318" data-token="color.status.danger"/>
<text x="50" y="27" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.delete">Xoá</text>
</g>
</g>
<g id="C08" transform="translate(174 530)"><text text-anchor="end" x="184" fill="#1B1F24" data-token="color.text.primary">Tạm tính: 260.000 ₫</text></g>
</g>
<!-- Z3 — footer sticky đáy màn hình (mobile khác desktop: nằm dưới thay vì sidebar phải) -->
<g id="Z3" data-name="Tổng kết (footer mobile)" transform="translate(0 764)">
<rect width="390" height="80" fill="#FFFFFF" data-token="color.bg.surface"/>
<line x1="0" y1="0.5" x2="390" y2="0.5" stroke="#D5DAE1" data-token="color.border.default"/>
<g id="C09"><text x="16" y="30" font-size="16" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Tổng cộng: 510.000 ₫</text></g>
<g id="C11" transform="translate(16 40)">
<rect width="358" height="30" rx="4" fill="#2456A6" data-token="color.brand.primary"/>
<text x="179" y="20" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.checkout">Tiến hành Checkout</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -0,0 +1,106 @@
{
"$schema": "https://tr.designtokens.org/format/",
"$description": "DS_e-commerce — design token theo chuẩn W3C DTCG. Nguồn sự thật duy nhất về màu/chữ/khoảng cách; tokens.css và khối :root trong HIFI_*.html phải sinh từ file này (kiểm bằng scripts/design-check.mjs). Giá trị bên dưới là PLACEHOLDER trung tính của template ba-design — dự án chưa có brand guideline (SAD §7.0: 'không có brand guideline cố định') và chưa chọn design system tham chiếu (OQ-022). KHÔNG coi đây là đề xuất nhận diện đã duyệt — xem DS_e-commerce_v1.0.md §0.",
"color": {
"bg": {
"canvas": { "$type": "color", "$value": "#F6F7F9", "$description": "Nền trang" },
"surface": { "$type": "color", "$value": "#FFFFFF", "$description": "Nền card/bảng/modal" },
"subtle": { "$type": "color", "$value": "#EEF1F5", "$description": "Header bảng, vùng phụ" }
},
"border": {
"default": { "$type": "color", "$value": "#D5DAE1" },
"strong": { "$type": "color", "$value": "#767F8C" }
},
"text": {
"primary": { "$type": "color", "$value": "#1B1F24" },
"secondary": { "$type": "color", "$value": "#5B6472" },
"disabled": { "$type": "color", "$value": "#9AA3AF", "$description": "Miễn kiểm tương phản (WCAG 1.4.3 loại trừ thành phần vô hiệu)" },
"inverse": { "$type": "color", "$value": "#FFFFFF" }
},
"brand": {
"primary": { "$type": "color", "$value": "#2456A6", "$description": "Nút chính, liên kết, focus ring — placeholder trung tính, chưa có brand guideline (OQ-022)" },
"primary-hover": { "$type": "color", "$value": "#1C468A" },
"primary-subtle": { "$type": "color", "$value": "#E7EEF9", "$description": "Nền badge/hàng được chọn" }
},
"status": {
"danger": { "$type": "color", "$value": "#B42318" },
"danger-subtle": { "$type": "color", "$value": "#FDECEA" },
"success": { "$type": "color", "$value": "#1E7B45" },
"success-subtle": { "$type": "color", "$value": "#E6F4EC" },
"warning": { "$type": "color", "$value": "#B7791F" },
"warning-subtle": { "$type": "color", "$value": "#FFF4DE" }
},
"focus": { "ring": { "$type": "color", "$value": "#2456A6" } }
},
"font": {
"family": {
"base": { "$type": "fontFamily", "$value": ["Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"], "$description": "PLACEHOLDER — system stack giữ nguyên theo chỉ đạo (không có font brand); ZH/KO/JA cần rà lại fallback font khi có bản dịch (OQ-023)" },
"mono": { "$type": "fontFamily", "$value": ["SFMono-Regular", "Consolas", "Menlo", "monospace"] }
},
"size": {
"xs": { "$type": "dimension", "$value": "12px" },
"sm": { "$type": "dimension", "$value": "14px" },
"md": { "$type": "dimension", "$value": "16px" },
"lg": { "$type": "dimension", "$value": "20px" },
"xl": { "$type": "dimension", "$value": "24px" },
"2xl": { "$type": "dimension", "$value": "32px" }
},
"weight": {
"regular": { "$type": "fontWeight", "$value": 400 },
"semibold": { "$type": "fontWeight", "$value": 600 }
},
"line-height": {
"tight": { "$type": "number", "$value": 1.25 },
"normal": { "$type": "number", "$value": 1.5 }
}
},
"space": {
"1": { "$type": "dimension", "$value": "4px" },
"2": { "$type": "dimension", "$value": "8px" },
"3": { "$type": "dimension", "$value": "12px" },
"4": { "$type": "dimension", "$value": "16px" },
"6": { "$type": "dimension", "$value": "24px" },
"8": { "$type": "dimension", "$value": "32px" },
"12": { "$type": "dimension", "$value": "48px" }
},
"radius": {
"sm": { "$type": "dimension", "$value": "4px" },
"md": { "$type": "dimension", "$value": "8px" },
"pill": { "$type": "dimension", "$value": "999px" }
},
"shadow": {
"sm": { "$type": "shadow", "$value": { "color": "#1B1F2414", "offsetX": "0px", "offsetY": "1px", "blur": "2px", "spread": "0px" } },
"md": { "$type": "shadow", "$value": { "color": "#1B1F2426", "offsetX": "0px", "offsetY": "4px", "blur": "12px", "spread": "0px" } }
},
"size": {
"control": {
"desktop": { "$type": "dimension", "$value": "36px", "$description": "Chiều cao nút/ô nhập desktop" },
"touch": { "$type": "dimension", "$value": "44px", "$description": "Chiều cao tối thiểu trên mobile (hit target ≥ 44px)" }
},
"icon": { "$type": "dimension", "$value": "20px" },
"content-max": { "$type": "dimension", "$value": "1200px", "$description": "PLACEHOLDER — UICONV §1 chưa chốt max-width thật (OQ-018), giữ giá trị template" }
},
"breakpoint": {
"tablet": { "$type": "dimension", "$value": "768px", "$description": "PLACEHOLDER — UICONV §1 chưa chốt ngưỡng thật (OQ-018), giữ giá trị template" },
"desktop": { "$type": "dimension", "$value": "1024px", "$description": "PLACEHOLDER — UICONV §1 chưa chốt ngưỡng thật (OQ-018), giữ giá trị template" }
},
"$extensions": {
"ba": {
"project": "e-commerce",
"version": "1.0",
"source": "UICONV_e-commerce_v1.0.md v1.1 §0/§1/§8/§11 (chưa chốt design system — OQ-022; chưa chốt breakpoint/grid — OQ-018) · brand guideline: chưa có (SAD §7.0) — placeholder trung tính của templates/tokens.json",
"contrastPairs": [
{ "fg": "color.text.primary", "bg": "color.bg.surface", "min": 4.5, "use": "Chữ thường trên nền card (VD tên sản phẩm C04, biến thể C05)" },
{ "fg": "color.text.primary", "bg": "color.bg.canvas", "min": 4.5, "use": "Chữ thường trên nền trang" },
{ "fg": "color.text.secondary", "bg": "color.bg.surface", "min": 4.5, "use": "Hint, tên gian hàng phụ" },
{ "fg": "color.text.inverse", "bg": "color.brand.primary", "min": 4.5, "use": "Nhãn nút chính (C11 Tiến hành Checkout)" },
{ "fg": "color.text.inverse", "bg": "color.status.danger", "min": 4.5, "use": "Nhãn nút nguy hiểm (C07 Xoá)" },
{ "fg": "color.brand.primary", "bg": "color.bg.surface", "min": 4.5, "use": "Liên kết" },
{ "fg": "color.status.danger", "bg": "color.bg.surface", "min": 4.5, "use": "Message lỗi dưới field (E-CART-0002)" },
{ "fg": "color.status.danger", "bg": "color.status.danger-subtle", "min": 4.5, "use": "Banner lỗi tải/cập nhật (C12, E-CART-0003/0006)" },
{ "fg": "color.text.primary", "bg": "color.brand.primary-subtle", "min": 4.5, "use": "Chữ trên badge/hàng chọn" },
{ "fg": "color.border.strong", "bg": "color.bg.surface", "min": 3.0, "use": "Viền ô nhập / bộ đếm F01 (WCAG 1.4.11 non-text)" }
]
}
}
}

View File

@@ -0,0 +1,468 @@
<!--
HIFI_US002-003_v1.0.html — mockup hi-fi tự chứa, sinh bởi skill ba-design (activity `hifi`), US-002 (xem) + US-003 (sửa/xoá).
Nguồn hành vi: SRS_US002-003_v1.0.md §2.A.3 (SCR-04 Giỏ hàng) · Nguồn bố cục: WF_US002-003_v1.0.md §3.1 (+ .html)
Nguồn token/component: 00-index/DS_e-commerce_v1.0.md + ds/tokens.css + ds/components.html
Độ tin cậy visual: 🔴 (DS §0 — token placeholder trung tính, chưa có brand/design system — OQ-022)
<style> = khối :root của ds/tokens.css + khối ds-components.css NGUYÊN VĂN (bắt buộc, kiểm bằng design-check.mjs H1)
+ phần "Khung xem HIFI" giữ nguyên từ template + một khối CSS phụ nhỏ riêng cho bố cục Giỏ hàng (chỉ dùng biến --ds-*,
không phải một phần của khối ds-components nên không ảnh hưởng phép kiểm H1).
Thành phần chỉ có ở prototype mà SRS chưa có (checkbox chọn dòng, badge hết hàng, badge giá đổi — xem WF §5 #1-#3)
KHÔNG được vẽ ở đây (khác WF, HIFI không cần minh hoạ lại phần "chỉ ở prototype" vì đó không phải hi-fi của SRS).
v1.1 (2026-09-09) — sửa theo ghi chú người duyệt sau khi chạy render-figma.sh: bố cục mobile 390px làm vỡ dòng
CartItem (tên sản phẩm bị bẻ chữ, stepper chồng lên text) — nâng khối ds-components lên v1.1 (thêm tiện ích
.ds-m-full) và bọc khối text (C04–C06) trong flex:1 1 160px, bọc khối thao tác (F01+C07+lỗi/spinner của dòng)
trong <div class="ds-row ds-m-full ds-row--end"> để rơi xuống hàng riêng ở mobile. Xem DSPEC §2 (redline mobile).
-->
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HIFI — US-002, US-003 — Giỏ hàng (SCR-04)</title>
<style>
:root{
--ds-color-bg-canvas:#F6F7F9;
--ds-color-bg-surface:#FFFFFF;
--ds-color-bg-subtle:#EEF1F5;
--ds-color-border-default:#D5DAE1;
--ds-color-border-strong:#767F8C;
--ds-color-text-primary:#1B1F24;
--ds-color-text-secondary:#5B6472;
--ds-color-text-disabled:#9AA3AF;
--ds-color-text-inverse:#FFFFFF;
--ds-color-brand-primary:#2456A6;
--ds-color-brand-primary-hover:#1C468A;
--ds-color-brand-primary-subtle:#E7EEF9;
--ds-color-status-danger:#B42318;
--ds-color-status-danger-subtle:#FDECEA;
--ds-color-status-success:#1E7B45;
--ds-color-status-success-subtle:#E6F4EC;
--ds-color-status-warning:#B7791F;
--ds-color-status-warning-subtle:#FFF4DE;
--ds-color-focus-ring:#2456A6;
--ds-font-family-base:"Segoe UI","Roboto","Helvetica Neue","Arial",sans-serif;
--ds-font-family-mono:"SFMono-Regular","Consolas","Menlo",monospace;
--ds-font-size-xs:12px;
--ds-font-size-sm:14px;
--ds-font-size-md:16px;
--ds-font-size-lg:20px;
--ds-font-size-xl:24px;
--ds-font-size-2xl:32px;
--ds-font-weight-regular:400;
--ds-font-weight-semibold:600;
--ds-font-line-height-tight:1.25;
--ds-font-line-height-normal:1.5;
--ds-space-1:4px;
--ds-space-2:8px;
--ds-space-3:12px;
--ds-space-4:16px;
--ds-space-6:24px;
--ds-space-8:32px;
--ds-space-12:48px;
--ds-radius-sm:4px;
--ds-radius-md:8px;
--ds-radius-pill:999px;
--ds-shadow-sm:0px 1px 2px 0px #1B1F2414;
--ds-shadow-md:0px 4px 12px 0px #1B1F2426;
--ds-size-control-desktop:36px;
--ds-size-control-touch:44px;
--ds-size-icon:20px;
--ds-size-content-max:1200px;
--ds-breakpoint-tablet:768px;
--ds-breakpoint-desktop:1024px;
}
/* ds-components v1.1 — BEGIN
CSS component của DS_<PROJECT>. Chỉ dùng biến --ds-* (tokens.css); cấm mã màu/px thô ngoài khối này.
Từ vựng lớp = cột "Kiểu trình bày" của UICONV §11 (xem DS §3). Khối này được chép NGUYÊN VĂN vào
components.html và mọi HIFI_*.html — kiểm khớp bằng scripts/design-check.mjs. */
*,*::before,*::after{box-sizing:border-box}
html{font-family:var(--ds-font-family-base);font-size:var(--ds-font-size-sm);line-height:var(--ds-font-line-height-normal);color:var(--ds-color-text-primary);background:var(--ds-color-bg-canvas);-webkit-font-smoothing:antialiased}
body{margin:0}
h1,h2,h3{margin:0;line-height:var(--ds-font-line-height-tight);font-weight:var(--ds-font-weight-semibold)}
h1{font-size:var(--ds-font-size-xl)}h2{font-size:var(--ds-font-size-lg)}h3{font-size:var(--ds-font-size-md)}
a,.ds-link{color:var(--ds-color-brand-primary);text-decoration:underline;text-underline-offset:2px;cursor:pointer}
a:hover,.ds-link:hover{color:var(--ds-color-brand-primary-hover)}
:focus-visible{outline:2px solid var(--ds-color-focus-ring);outline-offset:2px}
.ds-muted{color:var(--ds-color-text-secondary)}
.ds-small{font-size:var(--ds-font-size-xs)}
.ds-icon{width:var(--ds-size-icon);height:var(--ds-size-icon);flex:none;stroke:currentColor;fill:none;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
.ds-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
/* Layout */
.ds-page{max-width:var(--ds-size-content-max);margin:0 auto;padding:var(--ds-space-6) var(--ds-space-4);display:grid;gap:var(--ds-space-4)}
.ds-row{display:flex;gap:var(--ds-space-2);align-items:center;flex-wrap:wrap}
.ds-row--between{justify-content:space-between}.ds-row--end{justify-content:flex-end}
.ds-stack{display:grid;gap:var(--ds-space-3)}
.ds-cols{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:var(--ds-space-4);align-items:start}
.ds-header{position:sticky;top:0;z-index:5;background:var(--ds-color-bg-surface);border-bottom:1px solid var(--ds-color-border-default);padding:var(--ds-space-3) var(--ds-space-4);display:flex;align-items:center;gap:var(--ds-space-4);min-height:56px}
.ds-breadcrumb{display:flex;gap:var(--ds-space-2);align-items:center;font-size:var(--ds-font-size-xs);color:var(--ds-color-text-secondary)}
.ds-breadcrumb a{text-decoration:none}
/* Nút — UICONV §11: nút chính / nút phụ / nút nguy hiểm / liên kết */
.ds-btn{display:inline-flex;align-items:center;justify-content:center;gap:var(--ds-space-2);height:var(--ds-size-control-desktop);padding:0 var(--ds-space-4);border-radius:var(--ds-radius-sm);border:1px solid transparent;font:inherit;font-weight:var(--ds-font-weight-semibold);cursor:pointer;white-space:nowrap;text-decoration:none;transition:background .12s}
.ds-btn--primary{background:var(--ds-color-brand-primary);color:var(--ds-color-text-inverse)}
.ds-btn--primary:hover{background:var(--ds-color-brand-primary-hover)}
.ds-btn--secondary{background:var(--ds-color-bg-surface);color:var(--ds-color-text-primary);border-color:var(--ds-color-border-strong)}
.ds-btn--secondary:hover{background:var(--ds-color-bg-subtle)}
.ds-btn--danger{background:var(--ds-color-status-danger);color:var(--ds-color-text-inverse)}
.ds-btn--ghost{background:transparent;color:var(--ds-color-brand-primary)}
.ds-btn--icon{width:var(--ds-size-control-desktop);padding:0}
.ds-btn[aria-disabled="true"],.ds-btn:disabled{opacity:.45;cursor:not-allowed;pointer-events:none}
.ds-btn.is-loading{pointer-events:none}
.ds-spinner{width:16px;height:16px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:ds-spin .8s linear infinite;flex:none}
@keyframes ds-spin{to{transform:rotate(360deg)}}
/* Field — UICONV §4: nhãn trên field, * sau nhãn, lỗi dưới field thay hint */
.ds-field{display:grid;gap:var(--ds-space-1)}
.ds-field__label{font-size:var(--ds-font-size-xs);font-weight:var(--ds-font-weight-semibold);color:var(--ds-color-text-primary)}
.ds-field__label .ds-req{color:var(--ds-color-status-danger);margin-left:2px}
.ds-field__hint{font-size:var(--ds-font-size-xs);color:var(--ds-color-text-secondary)}
.ds-field__error{font-size:var(--ds-font-size-xs);color:var(--ds-color-status-danger);display:flex;gap:var(--ds-space-1);align-items:center}
.ds-input,.ds-select,.ds-textarea{width:100%;height:var(--ds-size-control-desktop);padding:0 var(--ds-space-3);border:1px solid var(--ds-color-border-strong);border-radius:var(--ds-radius-sm);background:var(--ds-color-bg-surface);color:var(--ds-color-text-primary);font:inherit}
.ds-textarea{height:auto;min-height:88px;padding:var(--ds-space-2) var(--ds-space-3)}
.ds-input::placeholder{color:var(--ds-color-text-secondary)}
.ds-input:hover,.ds-select:hover{border-color:var(--ds-color-text-secondary)}
.ds-input:focus,.ds-select:focus{border-color:var(--ds-color-brand-primary);outline:2px solid var(--ds-color-focus-ring);outline-offset:0}
.ds-input.is-invalid,.ds-select.is-invalid{border-color:var(--ds-color-status-danger)}
.ds-input[disabled],.ds-select[disabled]{background:var(--ds-color-bg-subtle);color:var(--ds-color-text-disabled);cursor:not-allowed}
.ds-input[readonly]{border-color:transparent;background:transparent;padding-left:0}
.ds-input-wrap{position:relative}
.ds-input-wrap .ds-icon{position:absolute;left:var(--ds-space-3);top:50%;transform:translateY(-50%);color:var(--ds-color-text-secondary)}
.ds-input-wrap .ds-input{padding-left:calc(var(--ds-space-3) + var(--ds-size-icon) + var(--ds-space-2))}
.ds-select{appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%235B6472' stroke-width='1.75'><path d='m4 6 4 4 4-4'/></svg>");background-repeat:no-repeat;background-position:right var(--ds-space-3) center;padding-right:calc(var(--ds-space-3) + 24px)}
.ds-check{display:inline-flex;align-items:center;gap:var(--ds-space-2);cursor:pointer;min-height:var(--ds-size-control-desktop)}
.ds-check input{width:18px;height:18px;margin:0;accent-color:var(--ds-color-brand-primary)}
.ds-switch{position:relative;width:40px;height:22px;border-radius:var(--ds-radius-pill);background:var(--ds-color-border-strong);display:inline-block;flex:none}
.ds-switch::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--ds-color-bg-surface)}
.ds-switch[aria-checked="true"]{background:var(--ds-color-brand-primary)}
.ds-switch[aria-checked="true"]::after{left:20px}
.ds-stepper{display:inline-flex;border:1px solid var(--ds-color-border-strong);border-radius:var(--ds-radius-sm);height:var(--ds-size-control-desktop);overflow:hidden}
.ds-stepper button{width:var(--ds-size-control-desktop);border:0;background:var(--ds-color-bg-surface);font:inherit;cursor:pointer;color:var(--ds-color-text-primary)}
.ds-stepper input{width:48px;text-align:center;border:0;border-left:1px solid var(--ds-color-border-default);border-right:1px solid var(--ds-color-border-default);font:inherit}
/* Bảng / Card — UICONV §3, §11 */
.ds-table-wrap{overflow-x:auto;border:1px solid var(--ds-color-border-default);border-radius:var(--ds-radius-md);background:var(--ds-color-bg-surface)}
.ds-table{width:100%;border-collapse:collapse;font-size:var(--ds-font-size-sm)}
.ds-table th,.ds-table td{padding:var(--ds-space-3) var(--ds-space-4);text-align:left;border-bottom:1px solid var(--ds-color-border-default);vertical-align:middle}
.ds-table th{background:var(--ds-color-bg-subtle);font-weight:var(--ds-font-weight-semibold);font-size:var(--ds-font-size-xs);color:var(--ds-color-text-secondary);white-space:nowrap}
.ds-table th[aria-sort]{cursor:pointer}
.ds-table tbody tr:hover{background:var(--ds-color-bg-canvas)}
.ds-table tbody tr[aria-selected="true"]{background:var(--ds-color-brand-primary-subtle)}
.ds-table tbody tr:last-child td{border-bottom:0}
.ds-table td.ds-num{text-align:right;font-variant-numeric:tabular-nums}
.ds-card{background:var(--ds-color-bg-surface);border:1px solid var(--ds-color-border-default);border-radius:var(--ds-radius-md);padding:var(--ds-space-4);display:grid;gap:var(--ds-space-2)}
.ds-cards{display:grid;gap:var(--ds-space-3)}
.ds-badge{display:inline-flex;align-items:center;height:22px;padding:0 var(--ds-space-2);border-radius:var(--ds-radius-pill);font-size:var(--ds-font-size-xs);font-weight:var(--ds-font-weight-semibold);background:var(--ds-color-bg-subtle);color:var(--ds-color-text-primary)}
.ds-badge--info{background:var(--ds-color-brand-primary-subtle)}
.ds-badge--success{background:var(--ds-color-status-success-subtle);color:var(--ds-color-status-success)}
.ds-badge--warning{background:var(--ds-color-status-warning-subtle);color:var(--ds-color-status-warning)}
.ds-badge--danger{background:var(--ds-color-status-danger-subtle);color:var(--ds-color-status-danger)}
.ds-tabs{display:flex;gap:var(--ds-space-4);border-bottom:1px solid var(--ds-color-border-default)}
.ds-tab{padding:var(--ds-space-2) 0;border-bottom:2px solid transparent;color:var(--ds-color-text-secondary);cursor:pointer;font-weight:var(--ds-font-weight-semibold)}
.ds-tab[aria-selected="true"]{color:var(--ds-color-brand-primary);border-bottom-color:var(--ds-color-brand-primary)}
.ds-pagination{display:flex;gap:var(--ds-space-2);align-items:center;font-size:var(--ds-font-size-xs);color:var(--ds-color-text-secondary)}
.ds-price{font-variant-numeric:tabular-nums;font-weight:var(--ds-font-weight-semibold)}
.ds-thumb{width:64px;height:64px;border-radius:var(--ds-radius-sm);background:var(--ds-color-bg-subtle);border:1px solid var(--ds-color-border-default);display:grid;place-items:center;color:var(--ds-color-text-secondary);flex:none}
/* Phản hồi hệ thống — UICONV §5, §6 */
.ds-toast{position:fixed;top:var(--ds-space-4);right:var(--ds-space-4);z-index:20;min-width:280px;max-width:420px;padding:var(--ds-space-3) var(--ds-space-4);border-radius:var(--ds-radius-md);background:var(--ds-color-text-primary);color:var(--ds-color-text-inverse);box-shadow:var(--ds-shadow-md);display:flex;gap:var(--ds-space-3);align-items:flex-start}
.ds-toast--error{background:var(--ds-color-status-danger)}
.ds-banner{display:flex;gap:var(--ds-space-3);align-items:flex-start;padding:var(--ds-space-3) var(--ds-space-4);border-radius:var(--ds-radius-md);border:1px solid var(--ds-color-border-default);background:var(--ds-color-bg-subtle)}
.ds-banner--error{background:var(--ds-color-status-danger-subtle);border-color:var(--ds-color-status-danger);color:var(--ds-color-status-danger)}
.ds-banner--warning{background:var(--ds-color-status-warning-subtle);border-color:var(--ds-color-status-warning)}
.ds-empty{display:grid;justify-items:center;text-align:center;gap:var(--ds-space-3);padding:var(--ds-space-12) var(--ds-space-4);color:var(--ds-color-text-secondary)}
.ds-empty__illus{width:120px;height:88px;color:var(--ds-color-border-strong)}
.ds-empty--filter{padding:var(--ds-space-8) var(--ds-space-4)}
.ds-skeleton{height:14px;border-radius:var(--ds-radius-sm);background:linear-gradient(90deg,var(--ds-color-bg-subtle),var(--ds-color-bg-canvas),var(--ds-color-bg-subtle));background-size:200% 100%;animation:ds-shimmer 1.2s infinite}
@keyframes ds-shimmer{to{background-position:-200% 0}}
.ds-modal-backdrop{position:fixed;inset:0;background:#1B1F2480;display:grid;place-items:center;z-index:10;padding:var(--ds-space-4)}
.ds-modal{width:100%;max-width:520px;background:var(--ds-color-bg-surface);border-radius:var(--ds-radius-md);box-shadow:var(--ds-shadow-md);display:grid}
.ds-modal__head{display:flex;justify-content:space-between;align-items:center;padding:var(--ds-space-4) var(--ds-space-6);border-bottom:1px solid var(--ds-color-border-default)}
.ds-modal__body{padding:var(--ds-space-6);display:grid;gap:var(--ds-space-4)}
.ds-modal__foot{display:flex;justify-content:flex-end;gap:var(--ds-space-2);padding:var(--ds-space-4) var(--ds-space-6);border-top:1px solid var(--ds-color-border-default)}
.ds-status-page{display:grid;justify-items:center;gap:var(--ds-space-3);padding:var(--ds-space-12);text-align:center}
/* Trạng thái ẩn/khoá/read-only — W7 */
[data-vis="hidden"]{display:none!important}
[data-vis="disabled"]{opacity:.45;pointer-events:none}
[data-vis="readonly"] .ds-input{border-color:transparent;background:transparent}
/* Responsive — breakpoint theo UICONV §1; body[data-view] ép khung nhìn khi xem trong HIFI */
@media (max-width:1023px){.ds-cols{grid-template-columns:1fr}}
@media (max-width:767px){
.ds-page{padding:var(--ds-space-4) var(--ds-space-3)}
.ds-btn{height:var(--ds-size-control-touch)}
.ds-input,.ds-select{height:var(--ds-size-control-touch)}
.ds-table-wrap{display:none}.ds-cards{display:grid}
.ds-hide-m{display:none!important}
}
@media (min-width:768px){.ds-cards{display:none}.ds-hide-d{display:none!important}}
body[data-view="mobile"] .ds-cols{grid-template-columns:1fr}
body[data-view="mobile"] .ds-table-wrap{display:none}body[data-view="mobile"] .ds-cards{display:grid}
body[data-view="mobile"] .ds-hide-m{display:none!important}body[data-view="mobile"] .ds-hide-d{display:revert}
body[data-view="mobile"] .ds-btn,body[data-view="mobile"] .ds-input,body[data-view="mobile"] .ds-select{height:var(--ds-size-control-touch)}
body[data-view="mobile"] .ds-page{padding:var(--ds-space-4) var(--ds-space-3)}
body[data-view="tablet"] .ds-cols{grid-template-columns:1fr}
/* Tiện ích xếp chồng ở mobile: phần tử trong .ds-row chiếm trọn hàng (nhóm nút thao tác của một dòng danh sách/CartItem…) */
.ds-m-full{}
@media (max-width:767px){.ds-m-full{flex-basis:100%;min-width:0}}
body[data-view="mobile"] .ds-m-full{flex-basis:100%;min-width:0}
/* ds-components v1.1 — END */
/* ===== Khung xem HIFI (không thuộc DS, không chép vào sản phẩm) ===== */
.hf-toolbar{position:sticky;top:0;z-index:30;background:var(--ds-color-text-primary);color:var(--ds-color-text-inverse);padding:6px 12px;display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:12px}
.hf-toolbar h1{font-size:13px;margin:0 8px 0 0;font-weight:600}
.hf-toolbar label{display:flex;gap:4px;align-items:center;opacity:.9}
.hf-toolbar select,.hf-toolbar input{font:inherit;font-size:12px}
.hf-legend{margin-left:auto;opacity:.8}
.hf-stage{padding:24px 12px;display:grid;gap:40px;justify-items:center;background:#e9ebef}
.hf-frame{width:100%;max-width:1440px;background:var(--ds-color-bg-canvas);border:1px solid var(--ds-color-border-strong);box-shadow:var(--ds-shadow-md);overflow:hidden;position:relative}
.hf-frame>.hf-cap{font:600 11px/1 var(--ds-font-family-mono);padding:6px 10px;background:#1B1F24;color:#fff;letter-spacing:.02em}
.hf-frame>.hf-cap small{font-weight:400;margin-left:8px;opacity:.75}
body[data-view="tablet"] .hf-frame{max-width:834px}
body[data-view="mobile"] .hf-frame{max-width:390px}
body[data-only] .hf-frame{display:none}
body[data-only] .hf-frame[data-active="true"]{display:block}
[data-state]{display:none}[data-state="default"]{display:block}
body.hf-ids [data-c],body.hf-ids [data-f]{outline:1px dashed #B42318;outline-offset:1px;position:relative}
body.hf-ids [data-c]::after,body.hf-ids [data-f]::after{content:attr(data-c) attr(data-f);position:absolute;top:-9px;right:-2px;z-index:3;font:600 9px/14px var(--ds-font-family-mono);background:#B42318;color:#fff;padding:0 4px;border-radius:3px;pointer-events:none}
body.hf-zones [data-zone]{outline:1px dashed #2456A6;outline-offset:-1px;position:relative}
body.hf-zones [data-zone]::before{content:attr(data-zone);position:absolute;top:0;left:0;z-index:3;font:600 9px/14px var(--ds-font-family-mono);background:#2456A6;color:#fff;padding:0 4px}
body[data-mode="proto"] [data-nav]{cursor:pointer;box-shadow:0 0 0 2px #B7791F80}
.hf-proto-only{outline:2px dashed var(--ds-color-status-danger);outline-offset:2px;position:relative}
.hf-proto-only::before{content:"chỉ ở prototype — " attr(data-ref);position:absolute;top:-16px;left:0;font:600 9px/14px var(--ds-font-family-mono);color:var(--ds-color-status-danger)}
.hf-note{padding:12px;font-size:12px;color:#5B6472;text-align:center}
/* ===== Bố cục riêng SCR-04 Giỏ hàng — chỉ dùng biến --ds-*, không thuộc khối ds-components ===== */
[data-zone="Z3"]{position:sticky;top:var(--ds-space-4);align-self:start}
body[data-view="mobile"] [data-zone="Z3"]{position:sticky;bottom:0;top:auto;background:var(--ds-color-bg-surface);box-shadow:var(--ds-shadow-md);padding:var(--ds-space-3) var(--ds-space-4);border-top:1px solid var(--ds-color-border-default);margin:0 calc(var(--ds-space-3) * -1) calc(var(--ds-space-4) * -1)}
.cart-item{align-items:flex-start}
.cart-demo-tag{font-size:var(--ds-font-size-xs);color:var(--ds-color-text-secondary);font-style:italic}
.ds-btn--danger:hover{filter:brightness(.92)}
</style>
</head>
<body data-view="desktop" data-mode="mockup">
<header class="hf-toolbar">
<h1>HIFI — US-002, US-003 · Giỏ hàng (SCR-04)</h1>
<label>Màn hình <select id="hf-scr"><option value="">Tất cả</option></select></label>
<label>Khung nhìn <select id="hf-view"><option value="desktop">Desktop ≥1024</option><option value="mobile">Mobile &lt;768</option></select></label>
<label>Trạng thái <select id="hf-state">
<option value="default">Mặc định (có dữ liệu)</option><option value="loading">Đang tải</option>
<option value="empty-none">Rỗng — chưa có dữ liệu nào</option>
<option value="error">Lỗi tải dữ liệu</option>
</select></label>
<label>Chế độ <select id="hf-mode"><option value="mockup">Mockup</option><option value="proto">Prototype (bấm điều hướng)</option></select></label>
<label><input type="checkbox" id="hf-ids"> C-id/F-id</label>
<label><input type="checkbox" id="hf-zones"> Vùng Zn</label>
<span class="hf-legend">Hành vi xem SRS_US002-003 §2.A.3 · bố cục xem WF_US002-003 §3.1 · token xem DS_e-commerce §0 (🔴 placeholder, OQ-022) · không có "bộ lọc không khớp"/"tablet" (N/A, xem ghi chú DSPEC)</span>
</header>
<main class="hf-stage">
<section class="hf-frame" id="SCR-04" data-scr="SCR-04" data-title="Giỏ hàng">
<div class="hf-cap">SCR-04 · Giỏ hàng <small>US-002 (xem) + US-003 (sửa/xoá) · WF §3.1 · vai trò ROLE-01 Guest, ROLE-02 Customer · vào từ SCR-01 (icon giỏ hàng)</small></div>
<div class="ds-page">
<div data-zone="Z1" data-name="Header màn hình">
<h1 data-c="C01"><span data-key="cart.title">Giỏ hàng của bạn</span> <span class="ds-muted" style="font-weight:var(--ds-font-weight-regular);font-size:var(--ds-font-size-md)" data-key="cart.itemCount">({n} sản phẩm)</span></h1>
<p class="cart-demo-tag" style="margin:4px 0 0">Minh hoạ dữ liệu mẫu: <code>{n}</code> = 5 (Σ quantity 4 dòng CartItem bên dưới) — text <code>data-key</code> giữ nguyên văn khuôn mẫu SRS §4.2, số thật do FE render lúc chạy</p>
</div>
<div class="ds-cols">
<!-- Z2 — Danh sách nhóm seller -->
<div data-zone="Z2" data-name="Danh sách nhóm seller">
<div data-state="default">
<div class="ds-stack">
<!-- Nhóm seller "Shop A" -->
<div class="ds-card">
<div class="ds-row ds-row--between" data-c="C02"><h3>Shop A</h3></div>
<div class="ds-row cart-item">
<div class="ds-thumb" data-c="C03" aria-label="Ảnh sản phẩm"></div>
<div class="ds-stack" style="gap:var(--ds-space-1);flex:1 1 160px;min-width:0">
<div data-c="C04">Áo thun basic</div>
<div class="ds-small ds-muted" data-c="C05">Size M, Đỏ</div>
<div class="ds-small ds-muted" data-c="C06">100.000 ₫</div>
</div>
<div class="ds-row ds-m-full ds-row--end">
<div class="ds-row" data-f="F01">
<span class="ds-stepper"><button aria-label="Giảm">−</button><input value="2" aria-label="Số lượng"><button aria-label="Tăng">+</button></span>
</div>
<button class="ds-btn ds-btn--danger" data-c="C07" data-key="btn.delete">Xoá</button>
</div>
</div>
<div class="ds-row cart-item">
<div class="ds-thumb" data-c="C03" aria-label="Ảnh sản phẩm"></div>
<div class="ds-stack" style="gap:var(--ds-space-1);flex:1 1 160px;min-width:0">
<div data-c="C04">Quần jean slimfit</div>
<div class="ds-small ds-muted" data-c="C05">Size 30</div>
<div class="ds-small ds-muted" data-c="C06">50.000 ₫</div>
</div>
<div class="ds-row ds-m-full ds-row--end">
<div class="ds-row" data-f="F01" data-vis="disabled" title="Đang cập nhật số lượng, vui lòng đợi.">
<span class="ds-stepper"><button aria-label="Giảm" disabled>−</button><input value="1" aria-label="Số lượng" disabled><button aria-label="Tăng" disabled>+</button></span>
<span class="ds-spinner" role="status" aria-label="Đang xử lý"></span>
</div>
<button class="ds-btn ds-btn--danger" data-c="C07" data-key="btn.delete" aria-disabled="true" title="Đang xử lý, vui lòng đợi.">Xoá</button>
</div>
</div>
<p class="cart-demo-tag">Minh hoạ dòng 2: đang gửi <code>PATCH</code> (spinner cục bộ, F01/C07 khoá — SRS §2.A.3.6)</p>
<div class="ds-row ds-row--end" data-c="C08">Tạm tính: 250.000 ₫</div>
</div>
<!-- Nhóm seller "Shop B" -->
<div class="ds-card">
<div class="ds-row ds-row--between" data-c="C02"><h3>Shop B</h3></div>
<div class="ds-row cart-item">
<div class="ds-thumb" data-c="C03" aria-label="Ảnh sản phẩm"></div>
<div class="ds-stack" style="gap:var(--ds-space-1);flex:1 1 160px;min-width:0">
<div data-c="C04">Balo du lịch</div>
<div class="ds-small ds-muted" data-c="C05">Đen</div>
<div class="ds-small ds-muted" data-c="C06">200.000 ₫</div>
</div>
<div class="ds-row ds-m-full ds-row--end" style="align-items:flex-start">
<div class="ds-field" data-f="F01" style="width:auto">
<span class="ds-stepper" style="border-color:var(--ds-color-status-danger)"><button aria-label="Giảm">−</button><input value="0" aria-label="Số lượng"><button aria-label="Tăng">+</button></span>
<span class="ds-field__error" data-key="E-CART-0002.msg"><svg class="ds-icon" style="width:14px;height:14px" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5m0 3h.01"/></svg>Số lượng phải là số nguyên từ 1 trở lên.</span>
</div>
<button class="ds-btn ds-btn--danger" data-c="C07" data-key="btn.delete">Xoá</button>
</div>
</div>
<p class="cart-demo-tag">Minh hoạ dòng 1: đang nhập giá trị không hợp lệ (E-CART-0002, chờ sửa lại — <code>AC-US003-07</code>)</p>
<div class="ds-row cart-item">
<div class="ds-thumb" data-c="C03" aria-label="Ảnh sản phẩm"></div>
<div class="ds-stack" style="gap:var(--ds-space-1);flex:1 1 160px;min-width:0">
<div data-c="C04">Ốp lưng điện thoại</div>
<div class="ds-small ds-muted" data-c="C05">—</div>
<div class="ds-small ds-muted" data-c="C06">60.000 ₫</div>
</div>
<div class="ds-row ds-m-full ds-row--end">
<div class="ds-row" data-f="F01">
<span class="ds-stepper"><button aria-label="Giảm">−</button><input value="1" aria-label="Số lượng"><button aria-label="Tăng">+</button></span>
</div>
<button class="ds-btn ds-btn--danger" data-c="C07" data-key="btn.delete">Xoá</button>
</div>
</div>
<div class="ds-banner ds-banner--error">
<svg class="ds-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5m0 3h.01"/></svg>
<span data-key="E-CART-0006.msg">Không thể cập nhật giỏ hàng, thử lại.</span>
</div>
<p class="cart-demo-tag">Minh hoạ dòng 2: <code>PATCH</code>/<code>DELETE</code> lỗi 5xx/timeout — giữ nguyên giá trị cũ, mở lại nút (<code>AC-US003-09</code>, <code>10</code>)</p>
<div class="ds-row ds-row--end" data-c="C08">Tạm tính: 260.000 ₫</div>
</div>
</div>
</div>
<div data-state="loading">
<div class="ds-stack">
<div class="ds-card"><div class="ds-skeleton" style="width:30%"></div><div class="ds-skeleton"></div><div class="ds-skeleton" style="width:70%"></div></div>
<div class="ds-card"><div class="ds-skeleton" style="width:25%"></div><div class="ds-skeleton" style="width:80%"></div></div>
</div>
</div>
<div data-state="empty-none">
<div class="ds-empty">
<svg class="ds-empty__illus" viewBox="0 0 120 88" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="16" y="20" width="88" height="56" rx="6"/><path d="M16 36h88M32 52h28M32 62h44"/></svg>
<span data-key="cart.empty.title">Giỏ hàng trống</span>
<button class="ds-btn ds-btn--primary" data-c="C10" data-key="cart.empty.cta">Tiếp tục mua sắm</button>
</div>
</div>
<div data-state="error">
<div class="ds-banner ds-banner--error" data-c="C12">
<svg class="ds-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5m0 3h.01"/></svg>
<span data-key="cart.error.load">Không tải được dữ liệu, thử lại.</span>
<button class="ds-btn ds-btn--secondary" data-c="C13" data-key="btn.retry" style="margin-left:auto;height:32px">Thử lại</button>
</div>
</div>
</div>
<!-- Z3 — Tổng kết (sidebar desktop / footer sticky mobile, cùng một markup) -->
<div data-zone="Z3" data-name="Tổng kết (sidebar desktop / footer mobile)">
<div data-state="default">
<div class="ds-stack" style="gap:var(--ds-space-2)">
<div class="label" data-c="C09" style="font-weight:var(--ds-font-weight-semibold);font-size:var(--ds-font-size-lg)">Tổng cộng: 510.000 ₫</div>
<button class="ds-btn ds-btn--primary" data-c="C11" data-key="btn.checkout" style="width:100%">Tiến hành Checkout</button>
</div>
</div>
<div data-state="empty-none">
<div class="ds-stack" style="gap:var(--ds-space-2)">
<div data-c="C09" style="font-weight:var(--ds-font-weight-semibold);font-size:var(--ds-font-size-lg)">Tổng cộng: 0 ₫</div>
<button class="ds-btn ds-btn--primary" data-c="C11" aria-disabled="true" title="Giỏ hàng đang trống, thêm sản phẩm để tiếp tục." style="width:100%">Tiến hành Checkout</button>
</div>
</div>
<div data-state="loading" aria-hidden="true"></div>
<div data-state="error" aria-hidden="true"></div>
</div>
</div>
</div>
</section>
<!-- Minh hoạ hành vi C07 "Xoá" — modal xác nhận (UICONV §5). Không phải SCR riêng, không có data-scr/data-c. -->
<section style="max-width:560px;width:100%">
<p class="hf-note" style="padding:0 0 8px">Minh hoạ hành vi khi bấm "Xoá" (C07) — modal xác nhận theo <code>UICONV</code> §5, không phải màn hình riêng</p>
<div class="ds-modal" style="box-shadow:var(--ds-shadow-md)">
<div class="ds-modal__head">
<h3 data-key="cart.delete.confirm.title">Xoá sản phẩm khỏi giỏ hàng?</h3>
<button class="ds-btn ds-btn--ghost ds-btn--icon" aria-label="Đóng"><svg class="ds-icon" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</div>
<div class="ds-modal__body"><p style="margin:0" data-key="cart.delete.confirm.body">Sản phẩm sẽ được xoá khỏi giỏ hàng của bạn.</p></div>
<div class="ds-modal__foot">
<button class="ds-btn ds-btn--secondary">Huỷ</button>
<button class="ds-btn ds-btn--danger" data-key="btn.delete">Xoá</button>
</div>
</div>
<p class="cart-demo-tag" style="text-align:center;display:block;margin-top:8px">Nhãn "Huỷ" chưa có khoá riêng ở SRS §4.2 — xem DSPEC §8 (không chặn G3, mức thấp)</p>
</section>
</main>
<p class="hf-note">Mockup hi-fi v1.1 (2026-09-09) · token và component theo DS_e-commerce (ds-components v1.1) · hành vi theo SRS_US002-003 · bố cục theo WF_US002-003 · độ tin cậy visual 🔴 (chưa có brand/design system — OQ-022)</p>
<script>
(function(){
var q=new URLSearchParams(location.search), b=document.body;
var scr=document.getElementById('hf-scr'),view=document.getElementById('hf-view'),state=document.getElementById('hf-state'),mode=document.getElementById('hf-mode'),ids=document.getElementById('hf-ids'),zones=document.getElementById('hf-zones');
document.querySelectorAll('.hf-frame').forEach(function(f){var o=document.createElement('option');o.value=f.id;o.textContent=f.id+' · '+(f.dataset.title||'');scr.appendChild(o);});
if(q.get('scr'))scr.value=q.get('scr');if(q.get('view'))view.value=q.get('view');if(q.get('state'))state.value=q.get('state');if(q.get('mode'))mode.value=q.get('mode');ids.checked=q.get('ids')==='1';zones.checked=q.get('zones')==='1';
function apply(){
b.setAttribute('data-view',view.value);b.setAttribute('data-mode',mode.value);
b.classList.toggle('hf-ids',ids.checked);b.classList.toggle('hf-zones',zones.checked);
if(scr.value){b.setAttribute('data-only','1');}else{b.removeAttribute('data-only');}
document.querySelectorAll('.hf-frame').forEach(function(f){f.setAttribute('data-active',String(!scr.value||f.id===scr.value));});
var s=state.value;
document.querySelectorAll('[data-state]').forEach(function(el){
var st=el.getAttribute('data-state'),on;
if(s==='submitting')on=st==='submitting'||(st==='default'&&!el.closest('.ds-modal__foot'));
else if(s==='invalid')on=st==='invalid'||(st==='default'&&!el.closest('.ds-field'));
else on=st===s||(st==='default'&&s!=='default'&&!el.parentElement.querySelector('[data-state="'+s+'"]'));
el.style.display=on?(el.tagName==='SPAN'?'inline':'block'):'none';
});
}
[scr,view,state,mode,ids,zones].forEach(function(el){el.addEventListener('change',apply);});
document.addEventListener('click',function(e){
if(b.getAttribute('data-mode')!=='proto')return;
var t=e.target.closest('[data-nav]');if(!t)return;e.preventDefault();
var to=document.getElementById(t.getAttribute('data-nav'));if(!to)return;
scr.value=to.id;state.value='default';apply();to.scrollIntoView();
});
apply();
})();
</script>
</body>
</html>