359 lines
31 KiB
HTML
359 lines
31 KiB
HTML
<!--
|
||
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 — <US-id> · <Tên US></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 <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&view=mobile&state=empty-none&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_<PROJECT> · hành vi theo SRS_<US-id> · bố cục theo WF_<US-id> · 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>
|