469 lines
34 KiB
HTML
469 lines
34 KiB
HTML
<!--
|
||
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 <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>
|