Files
sys-analysis-design/ba-output/e-commerce/03-specification/design/HIFI_US002-003_v1.0.html
2026-09-11 23:01:18 +07:00

469 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
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>