Files
sys-analysis-design/.claude/skills/ba-design/templates/hifi.html
2026-09-11 23:01:18 +07:00

359 lines
31 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_<US-id>_v1.0.html — mockup hi-fi tự chứa, sinh bởi skill ba-design (activity hifi / proto).
QUY TẮC SINH FILE NÀY:
· <style> = khối :root của 00-index/ds/tokens.css + khối ds-components.css NGUYÊN VĂN (kiểm bằng scripts/design-check.mjs) + phần "Khung xem HIFI" giữ nguyên.
· Mỗi màn hình SCR-xx của SRS §2.1 → một <section class="hf-frame" id="SCR-xx" data-scr data-title>, bố cục theo WF §3.x.1 (mỗi vùng Zn → data-zone="Zn" data-name).
· Mỗi thành phần SRS §2.3.1 → data-c="Cnn"; mỗi field SRS §2.3.3 → data-f="Fnn". Tập ID phải KHỚP HAI CHIỀU với SRS.
· Mọi chuỗi hiển thị → data-key="<khoá i18n SRS §4.2>" và text NGUYÊN VĂN theo SRS (W5). Message lỗi → data-key="E-<DOMAIN>-nnnn".
· Trạng thái WF §3.x.3 / SRS §2.3.6 → khối data-state="default|loading|empty-none|empty-filter|error|submitting|invalid" trong cùng vùng.
· Ẩn/khoá/read-only (W7) → data-vis="hidden|disabled|readonly" kèm title nêu lý do khi disabled.
· Điều hướng theo SRS §2.2 → data-nav="SCR-yy" (chỉ có tác dụng ở chế độ Prototype).
· Thành phần CHỈ CÓ Ở PROTOTYPE (WF §5 loại Tồn tại chưa quyết) → bọc <span class="hf-proto-only" data-ref="WF §5 #n">, KHÔNG gán data-c.
· Chỉ dùng lớp .ds-* và biến --ds-*. Cấm mã màu/px thô trong markup (trừ width cột theo SRS §2.3.2). Icon = inline SVG, không emoji.
· Chụp ảnh từng trạng thái bằng Chrome headless: scripts/render-figma.sh (dùng query ?scr=&view=&state=).
-->
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HIFI — <US-id> — <Tên US></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}
</style>
</head>
<body data-view="desktop" data-mode="mockup">
<header class="hf-toolbar">
<h1>HIFI — &lt;US-id&gt; · &lt;Tên US&gt;</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="tablet">Tablet 768–1023</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="empty-filter">Rỗng — bộ lọc không khớp</option>
<option value="error">Lỗi tải dữ liệu</option><option value="submitting">Đang gửi</option><option value="invalid">Validation lỗi</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 · bố cục xem WF · token xem DS · URL: ?scr=SCR-01&amp;view=mobile&amp;state=empty-none&amp;ids=1</span>
</header>
<main class="hf-stage">
<!-- ============ SCR-01 (mẫu: danh sách) — mỗi SCR một .hf-frame, id = SCR-id, data-c/data-f/data-key khớp SRS, data-zone khớp WF ============ -->
<section class="hf-frame" id="SCR-01" data-scr="SCR-01" data-title="Danh sách cửa hàng">
<div class="hf-cap">SCR-01 · Danh sách cửa hàng <small>WF §3.1 · vai trò ROLE-01, ROLE-02 · vào từ Menu</small></div>
<header class="ds-header" data-zone="Z0" data-name="Header toàn site">
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="menu"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
<b>Tên hệ thống</b>
<nav class="ds-breadcrumb ds-hide-m"><a data-key="nav.home">Trang chủ</a><span>/</span><span data-key="scr01.title">Danh sách cửa hàng</span></nav>
<span style="margin-left:auto" class="ds-row"><svg class="ds-icon" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span>
</header>
<div class="ds-page">
<div class="ds-row ds-row--between" data-zone="Z1" data-name="Header màn hình">
<h1 data-c="C01" data-key="scr01.title">Danh sách cửa hàng</h1>
<button class="ds-btn ds-btn--primary" data-c="C02" data-key="btn.create" data-nav="SCR-03"><svg class="ds-icon" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>Tạo mới</button>
</div>
<div class="ds-row" data-zone="Z2" data-name="Thanh lọc">
<div class="ds-input-wrap" style="width:320px;max-width:100%"><svg class="ds-icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input class="ds-input" data-c="C03" data-key="scr01.search.ph" placeholder="Nhập mã hoặc tên…"></div>
<select class="ds-select ds-hide-m" style="width:200px" data-c="C04" data-key="scr01.filter.status"><option>Trạng thái: Tất cả</option></select>
<button class="ds-btn ds-btn--secondary ds-hide-d" data-key="btn.filter"><svg class="ds-icon" viewBox="0 0 24 24"><path d="M3 5h18l-7 8v6l-4-2v-4z"/></svg>Lọc</button>
</div>
<div data-zone="Z3" data-name="Dữ liệu">
<div data-state="default">
<div class="ds-table-wrap"><table class="ds-table" data-c="C05">
<thead><tr><th aria-sort="ascending">Mã ▲</th><th>Tên</th><th>Trạng thái</th><th>Ngày tạo</th><th></th></tr></thead>
<tbody>
<tr><td>STR-001</td><td>Cửa hàng A</td><td><span class="ds-badge">Nháp</span></td><td>30/08/2026</td><td><a class="ds-link" data-nav="SCR-02" data-key="btn.detail">Chi tiết</a></td></tr>
<tr><td>STR-002</td><td>Cửa hàng B</td><td><span class="ds-badge ds-badge--success">Đã duyệt</span></td><td>29/08/2026</td><td><a class="ds-link" data-nav="SCR-02" data-key="btn.detail">Chi tiết</a></td></tr>
</tbody></table></div>
<div class="ds-cards" data-c="C05">
<div class="ds-card" data-nav="SCR-02"><div class="ds-row ds-row--between"><b>STR-001</b><span class="ds-badge">Nháp</span></div><div>Cửa hàng A</div><div class="ds-small ds-muted">30/08/2026</div></div>
<div class="ds-card" data-nav="SCR-02"><div class="ds-row ds-row--between"><b>STR-002</b><span class="ds-badge ds-badge--success">Đã duyệt</span></div><div>Cửa hàng B</div><div class="ds-small ds-muted">29/08/2026</div></div>
</div>
</div>
<div data-state="loading"><div class="ds-card"><div class="ds-skeleton"></div><div class="ds-skeleton" style="width:85%"></div><div class="ds-skeleton" style="width:92%"></div><div class="ds-skeleton" style="width:70%"></div></div></div>
<div data-state="empty-none"><div class="ds-card"><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="empty.none">Chưa có cửa hàng nào. Tạo cửa hàng đầu tiên?</span><button class="ds-btn ds-btn--primary" data-key="btn.create" data-nav="SCR-03">Tạo mới</button></div></div></div>
<div data-state="empty-filter"><div class="ds-card"><div class="ds-empty ds-empty--filter"><span data-key="empty.filter">Không tìm thấy kết quả phù hợp với bộ lọc.</span><button class="ds-btn ds-btn--secondary" data-key="btn.clearFilter">Xoá lọc</button></div></div></div>
<div data-state="error"><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="error.load">Không tải được dữ liệu.</span><button class="ds-btn ds-btn--secondary" style="margin-left:auto;height:32px" data-key="btn.retry">Thử lại</button></div></div>
</div>
<div class="ds-row ds-row--between" data-zone="Z4" data-name="Phân trang">
<span class="hf-proto-only" data-ref="WF §5 #1"><button class="ds-btn ds-btn--secondary">Xuất Excel</button></span>
<div class="ds-pagination" data-c="C06"><button class="ds-btn ds-btn--secondary ds-btn--icon" aria-label="Trang trước">‹</button><span>1 / 7</span><button class="ds-btn ds-btn--secondary ds-btn--icon" aria-label="Trang sau">›</button><select class="ds-select ds-hide-m" style="width:110px;height:32px"><option>20 dòng</option></select></div>
</div>
</div>
</section>
<!-- ============ SCR-03 (mẫu: form trong modal) ============ -->
<section class="hf-frame" id="SCR-03" data-scr="SCR-03" data-title="Tạo cửa hàng">
<div class="hf-cap">SCR-03 · Tạo cửa hàng <small>Modal · mở từ C02 · WF §3.3</small></div>
<div style="min-height:560px;position:relative">
<div class="ds-modal-backdrop" style="position:absolute">
<div class="ds-modal" role="dialog" aria-labelledby="scr03-title">
<div class="ds-modal__head" data-zone="Z1" data-name="Tiêu đề modal"><h3 id="scr03-title" data-c="C01" data-key="scr03.title">Tạo cửa hàng</h3><button class="ds-btn ds-btn--ghost ds-btn--icon" data-c="C08" data-key="btn.close" data-nav="SCR-01" 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" data-zone="Z2" data-name="Form">
<label class="ds-field"><span class="ds-field__label" data-key="f01.label">Mã <b class="ds-req">*</b></span>
<span data-state="default"><input class="ds-input" data-f="F01" data-key="f01.ph" placeholder="3–20 ký tự, A–Z 0–9 -"><span class="ds-field__hint" data-key="f01.hint">Không trùng với mã đã có</span></span>
<span data-state="invalid"><input class="ds-input is-invalid" data-f="F01" value="ab"><span class="ds-field__error" data-key="E-XXX-0001">Mã phải có từ 3 đến 20 ký tự.</span></span>
</label>
<label class="ds-field"><span class="ds-field__label" data-key="f02.label">Loại <b class="ds-req">*</b></span><select class="ds-select" data-f="F02"><option>Thường</option></select></label>
<label class="ds-field"><span class="ds-field__label" data-key="f03.label">Tên</span><input class="ds-input" data-f="F03" data-key="f03.ph" placeholder="Tên hiển thị"></label>
</div>
<div class="ds-modal__foot" data-zone="Z3" data-name="Hành động">
<button class="ds-btn ds-btn--secondary" data-c="C07" data-key="btn.cancel" data-nav="SCR-01">Huỷ</button>
<span data-state="default" style="display:inline"><button class="ds-btn ds-btn--primary" data-c="C09" data-key="btn.save">Lưu</button></span>
<span data-state="submitting"><button class="ds-btn ds-btn--primary is-loading" data-c="C09" aria-disabled="true"><span class="ds-spinner"></span>Đang lưu…</button></span>
</div>
</div>
</div>
</div>
</section>
</main>
<p class="hf-note">Mockup hi-fi · token và component theo DS_&lt;PROJECT&gt; · hành vi theo SRS_&lt;US-id&gt; · bố cục theo WF_&lt;US-id&gt; · khung đứt đỏ = chỉ có ở prototype, chưa có trong SRS (WF §5)</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>