update skill ba

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

View File

@@ -0,0 +1,303 @@
<!--
components.html — component sheet của DS_<PROJECT>, sinh bởi skill ba-design (activity ds). Tự chứa, mở bằng trình duyệt.
QUY TẮC: <style> = tokens.css (khối :root) + ds-components.css NGUYÊN VĂN. Mỗi component có data-component và data-uiconv (khớp UICONV §11).
Đủ trạng thái: default · hover (mô tả) · focus · disabled (+ tooltip lý do) · loading · error. Không mã màu thô ngoài biến --ds-*.
Kiểm: node .claude/skills/ba-design/scripts/design-check.mjs --tokens tokens.json --css tokens.css --html components.html
-->
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DS — <PROJECT> — Components</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 */
</style>
</head>
<body>
<header class="ds-header">
<h1 style="font-size:var(--ds-font-size-md)">DS_&lt;PROJECT&gt; · Component sheet v1.0</h1>
<span class="ds-muted ds-small">Mỗi component = một dòng UICONV §11 · đủ trạng thái default / hover / focus / disabled / loading / error · chỉ dùng biến <code>--ds-*</code></span>
</header>
<main class="ds-page">
<section class="ds-card" data-component="Nút" data-uiconv="Nút chính · Nút phụ · Nút nguy hiểm · Liên kết">
<h2>Nút</h2>
<div class="ds-row">
<button class="ds-btn ds-btn--primary">Tạo mới</button>
<button class="ds-btn ds-btn--secondary">Huỷ</button>
<button class="ds-btn ds-btn--danger">Xoá</button>
<button class="ds-btn ds-btn--ghost">Xoá lọc</button>
<a class="ds-link" href="#">Chi tiết</a>
</div>
<div class="ds-row">
<button class="ds-btn ds-btn--primary" aria-disabled="true" title="Chọn ít nhất 1 dòng để bật">Disabled + tooltip lý do (UICONV §7)</button>
<button class="ds-btn ds-btn--primary is-loading"><span class="ds-spinner"></span>Đang lưu…</button>
<button class="ds-btn ds-btn--secondary 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>
<p class="ds-small ds-muted">Hover: nền tối hơn một bậc (<code>--ds-color-brand-primary-hover</code>). Focus: viền 2px <code>--ds-color-focus-ring</code>. Chiều cao 36px desktop / 44px mobile.</p>
</section>
<section class="ds-card" data-component="Field" data-uiconv="Ô nhập · Ô nhập số · Chọn 1 · Chọn nhiều · Chọn ngày · Công tắc">
<h2>Field</h2>
<div class="ds-cols" style="grid-template-columns:1fr 1fr">
<label class="ds-field"><span class="ds-field__label">Mã <b class="ds-req">*</b></span><input class="ds-input" placeholder="3–20 ký tự, A–Z 0–9 -"><span class="ds-field__hint">Không trùng với mã đã có</span></label>
<label class="ds-field"><span class="ds-field__label">Mã <b class="ds-req">*</b></span><input class="ds-input is-invalid" value="ab"><span class="ds-field__error"><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>Mã phải có từ 3 đến 20 ký tự.</span></label>
<label class="ds-field"><span class="ds-field__label">Loại</span><select class="ds-select"><option>Thường</option></select></label>
<label class="ds-field"><span class="ds-field__label">Tên (read-only)</span><input class="ds-input" readonly value="Cửa hàng A"></label>
<label class="ds-field"><span class="ds-field__label">Tên (disabled)</span><input class="ds-input" disabled value="Cửa hàng A"></label>
<div class="ds-field"><span class="ds-field__label">Tìm kiếm</span><div class="ds-input-wrap"><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" placeholder="Nhập mã hoặc tên…"></div></div>
</div>
<div class="ds-row">
<label class="ds-check"><input type="checkbox" checked> Chọn nhiều</label>
<label class="ds-check"><input type="radio" name="r" checked> Chọn 1 (≤ 7 lựa chọn)</label>
<span class="ds-row"><span class="ds-switch" role="switch" aria-checked="true"></span> Công tắc</span>
<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>
</section>
<section class="ds-card" data-component="Bảng / Card" data-uiconv="Bảng · Card · Badge trạng thái">
<h2>Bảng (desktop) → Card (mobile)</h2>
<div class="ds-table-wrap"><table class="ds-table">
<thead><tr><th aria-sort="ascending">Mã ▲</th><th>Tên</th><th>Trạng thái</th><th class="ds-num">Số tiền</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 class="ds-num ds-price">1.234.567 ₫</td><td><a class="ds-link">Chi tiết</a></td></tr>
<tr aria-selected="true"><td>STR-002</td><td>Cửa hàng B</td><td><span class="ds-badge ds-badge--success">Đã duyệt</span></td><td class="ds-num ds-price">890.000 ₫</td><td><a class="ds-link">Chi tiết</a></td></tr>
<tr><td>STR-003</td><td>Cửa hàng C</td><td><span class="ds-badge ds-badge--danger">Từ chối</span></td><td class="ds-num ds-price">—</td><td><a class="ds-link">Chi tiết</a></td></tr>
</tbody></table></div>
<div class="ds-cards" style="display:grid"><div class="ds-card"><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>
<div class="ds-row"><span class="ds-badge">Mặc định</span><span class="ds-badge ds-badge--info">Thông tin</span><span class="ds-badge ds-badge--success">Thành công</span><span class="ds-badge ds-badge--warning">Cảnh báo</span><span class="ds-badge ds-badge--danger">Lỗi</span></div>
<div class="ds-row ds-row--between"><div class="ds-tabs"><span class="ds-tab" aria-selected="true">Tất cả</span><span class="ds-tab">Nháp</span><span class="ds-tab">Đã duyệt</span></div><div class="ds-pagination"><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></div></div>
</section>
<section class="ds-card" data-component="Trạng thái chuẩn" data-uiconv="Skeleton · Toast · Banner · Trang trạng thái">
<h2>Trạng thái chuẩn (UICONV §6) — hai trạng thái rỗng phải trông khác nhau</h2>
<div class="ds-cols" style="grid-template-columns:1fr 1fr">
<div class="ds-card"><b class="ds-small">Đang tải</b><div class="ds-skeleton"></div><div class="ds-skeleton" style="width:85%"></div><div class="ds-skeleton" style="width:60%"></div></div>
<div class="ds-card"><b class="ds-small">Lỗi tải (banner trong vùng)</b><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>Không tải được dữ liệu.</span><button class="ds-btn ds-btn--secondary" style="margin-left:auto;height:32px">Thử lại</button></div></div>
<div class="ds-card"><b class="ds-small">Rỗng — chưa có dữ liệu (minh hoạ + nút chính)</b><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>Chưa có cửa hàng nào.</span><button class="ds-btn ds-btn--primary">Tạo mới</button></div></div>
<div class="ds-card"><b class="ds-small">Rỗng — bộ lọc không khớp (không minh hoạ + nút phụ)</b><div class="ds-empty ds-empty--filter"><span>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">Xoá lọc</button></div></div>
</div>
<div class="ds-row">
<div class="ds-toast" style="position:static"><svg class="ds-icon" viewBox="0 0 24 24"><path d="m5 12 4 4L19 6"/></svg><span>Đã lưu cửa hàng STR-004.</span><button class="ds-btn ds-btn--ghost ds-btn--icon" aria-label="Đóng" style="color:inherit;height:24px;width:24px;margin-left:auto">✕</button></div>
<div class="ds-toast ds-toast--error" style="position:static"><svg class="ds-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5m0 3h.01"/></svg><span>Không lưu được. Thử lại sau.</span></div>
</div>
</section>
<section class="ds-card" data-component="Modal" data-uiconv="Modal · Drawer · Bottom-sheet">
<h2>Modal xác nhận (UICONV §5) — nút chính bên phải, hành động nguy hiểm dùng nút nguy hiểm</h2>
<div class="ds-modal" style="box-shadow:var(--ds-shadow-sm)">
<div class="ds-modal__head"><h3>Xoá cửa hàng STR-001?</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">Hành động này không hoàn tác được.</p><label class="ds-field"><span class="ds-field__label">Lý do <b class="ds-req">*</b></span><textarea class="ds-textarea" placeholder="Tối đa 200 ký tự"></textarea></label></div>
<div class="ds-modal__foot"><button class="ds-btn ds-btn--secondary">Huỷ</button><button class="ds-btn ds-btn--danger">Xoá</button></div>
</div>
</section>
<section class="ds-card" data-component="Icon" data-uiconv="—">
<h2>Icon — inline SVG 24 grid, stroke 1.75, không emoji</h2>
<div class="ds-row" style="gap:var(--ds-space-4)">
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="search"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="plus"><path d="M12 5v14M5 12h14"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="close"><path d="M6 6l12 12M18 6 6 18"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="chevron-down"><path d="m6 9 6 6 6-6"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="back"><path d="M19 12H5m7-7-7 7 7 7"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="check"><path d="m5 12 4 4L19 6"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="alert"><circle cx="12" cy="12" r="9"/><path d="M12 8v5m0 3h.01"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="trash"><path d="M4 7h16M9 7V4h6v3m-8 0 1 13h8l1-13"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="filter"><path d="M3 5h18l-7 8v6l-4-2v-4z"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="cart"><path d="M3 4h2l2.4 11h11.2L21 7H6"/><circle cx="9" cy="19" r="1.5"/><circle cx="17" cy="19" r="1.5"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="user"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="menu"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
</div>
</section>
</main>
</body>
</html>

View File

@@ -0,0 +1,128 @@
# DSPEC — Đặc tả thiết kế & bàn giao — <US-id>
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | YYYY-MM-DD |
| **Author** | <BA/Design> (skill ba-design, activity `dspec`) |
| **Status** | 🟡 Draft |
| **Approved by** | Designer: — · PO: — |
| **Source** | SRS_<US> v1.0 · WF_<US> v1.0 · UICONV_<PROJECT> v1.0 · DS_<PROJECT> v1.0 · prototype: <đường dẫn / "không có"> |
| **Scope** | US-0nn · SCR-01, SCR-02, … |
| **Tệp kèm** | `HIFI_<US>_v1.0.html` · `FIGMA_<US>_v1.0/` |
| **Prototype bấm được** | ✅ có (`HIFI…html?mode=proto`) / ❌ không |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | | | Bản đầu | — |
> **Quy ước đọc** *(W11)*. `SRS` quyết định phần tử **có tồn tại không** và **hành xử thế nào**; `WF` quyết định
> **nằm ở đâu**; `DS` quyết định **trông thế nào** (token, component); tài liệu này ghi **con số cụ thể** để Dev FE dựng
> đúng HIFI mà không phải đo bằng mắt. Mâu thuẫn ⇒ SRS > WF > DS > HIFI, và mâu thuẫn phải ghi ở §6.
---
## 0. Nguồn bố cục và độ tin cậy
| | |
|---|---|
| **HIFI dựng từ** | `WF` §3 (bố cục) + `DS` (visual) + `SRS` §2.3 / §4.2 (thành phần, text) |
| **Prototype tham chiếu** | *(Figma/HTML/SAD §7 · phiên bản · mức phủ n/m màn hình — theo `WF` §0)* |
| **Độ tin cậy visual** | 🟢 DS có brand thật · 🟡 DS suy từ prototype · 🔴 DS placeholder trung tính (`DS` §0) — *HIFI mang độ tin cậy của DS* |
## 1. Bản đồ màn hình → tệp
| SCR | Tên | HIFI (`#id`) | Trạng thái có trong HIFI | SVG (FIGMA) | PNG | Khớp WF §3.x |
|---|---|---|---|---|---|---|
| SCR-01 | | `HIFI_<US>_v1.0.html#SCR-01` | default · loading · empty-none · empty-filter · error | `SCR-01.desktop.svg` · `SCR-01.mobile.svg` | `png/SCR-01.desktop.default.png` … | ✅ |
*Mọi `SCR` của `SRS` §2.1 phải có một dòng. Trạng thái phải khớp `SRS` §2.3.6 / `WF` §3.x.3 — thiếu trạng thái nào ghi rõ và lý do.*
## 2. Redline từng màn hình
### 2.1 SCR-01 — <Tên màn hình>
| Vùng (WF) | Kích thước desktop | Kích thước mobile | Padding | Gap | Token nền / viền |
|---|---|---|---|---|---|
| Z1 Header màn hình | full × auto | full × auto | `space.6` `space.4` | `space.2` | `bg.canvas` |
| Z3 Dữ liệu | full | full | 0 | — | `bg.surface` + `border.default`, `radius.md` |
| C-id | Thành phần | Lớp DS | Kích thước | Token chữ | Token màu | Trạng thái thể hiện trong HIFI |
|---|---|---|---|---|---|---|
| C01 | lbl_title | h1 | auto | `size.xl / semibold` | `text.primary` | — |
| C02 | btn_create | `.ds-btn--primary` | auto × 36 (44 mobile) | `size.sm / semibold` | `brand.primary` | hover · focus · disabled(+tooltip) |
**Khác biệt so với `WF` §3.1:** *(không có / liệt kê — mọi khác biệt về vị trí phải có lý do và Designer duyệt)*
### 2.2 SCR-02 — …
*(lặp 2.1)*
## 3. Ánh xạ trạng thái nghiệp vụ → hiển thị
| Trạng thái (BR §3) | Badge (DS §3) | Nhãn nguyên văn (SRS §4.2) | Hành động bị khoá (W7, SRS §2.3.1) |
|---|---|---|---|
## 4. Luồng prototype *(chỉ khi có prototype bấm được)*
*Mỗi cạnh khớp `SRS` §2.2; cạnh có trong HIFI mà SRS không có ⇒ §6.*
| Từ | Thành phần (`data-nav`) | Đến | Có trong SRS §2.2 |
|---|---|---|---|
| SCR-01 | C02 btn_create | SCR-03 | ✅ |
## 5. Khả năng truy cập
| Kiểm tra | Kết quả | Bằng chứng |
|---|---|---|
| Tương phản chữ (AA ≥ 4.5) trên mọi cặp dùng trong HIFI | ☐/✅ | `design-check.mjs` T2 |
| Hit target mobile ≥ 44 px cho mọi nút/ô nhập | | `size.control.touch` áp qua `body[data-view="mobile"]` |
| Thứ tự focus khớp `WF` §3.x.6 | | thứ tự DOM trong HIFI |
| Mọi ô nhập có nhãn; mọi icon-button có `aria-label` | | grep `aria-label` |
| Không truyền nghĩa chỉ bằng màu (badge có chữ, lỗi có icon + text) | | |
| Tắt motion khi `prefers-reduced-motion` | | DS §7 |
## 6. Lệch HIFI ↔ WF / SRS / DS
*Bắt buộc, kể cả rỗng ("đã đối chiếu n màn hình, không lệch"). Lệch với `SRS` về Tồn tại/Hành vi/Text ⇒ 🔴 chặn G3; lệch với `WF` về bố cục ⇒ Designer quyết; lệch với `DS` ⇒ phải có dòng `DS` §8.*
| # | Màn hình | HIFI có | Tài liệu nói | Loại | Bên thắng | Xử lý | Ai quyết | Trạng thái |
|---|---|---|---|---|---|---|---|---|
**Kết quả `design-check.mjs`:** C-id lệch hai chiều: *(rỗng)* · data-key text lệch SRS §4.2: *(rỗng)* · mã màu thô ngoài token: *(0)* · SCR thiếu: *(0)*.
## 7. Bàn giao cho Dev FE
| | |
|---|---|
| **Lấy từ HIFI** | Bố cục, khoảng cách, token, trạng thái thị giác, thứ tự DOM/focus. Mở `?ids=1&zones=1` để thấy C-id và vùng |
| **Lấy từ SRS** | Hành vi, điều kiện ẩn/khoá, validation, mã lỗi, text (HIFI chỉ *chép* text từ SRS §4.2) |
| **Lấy từ DS** | `tokens.json` → biến CSS/theme của stack đã chọn; lớp `.ds-*` → component tương ứng (DS §3 cột 3) |
| **Không được suy ra từ HIFI** | Dữ liệu mẫu (số liệu, tên) · độ dài/khoảng giá trị (ở SRS §2.3.3) · hành vi khi lỗi (SRS §2.3.5) |
| **Khi Designer sửa trong Figma** | Báo BA ⇒ chạy lại `hifi` + `figma` với `--proto <link>`; `SRS` chỉ đổi nếu lệch Tồn tại/Hành vi được PO chấp nhận |
| **Ảnh tham chiếu** | `FIGMA_<US>_v1.0/png/` — chụp từ HIFI, dùng để đối chiếu pixel khi review giao diện đã build (ba-4) |
## 8. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | 🔴 chặn G3? |
|---|---|---|---|---|---|
---
## Tự chấm
| # | Tiêu chí | ☐/✅ | Ghi chú |
|---|---|---|---|
| 1 | Mọi `SCR` của `SRS` §2.1 có mặt trong HIFI (`#SCR-xx`) và §1 | | design-check H5 |
| 2 | Tập `data-c`/`data-f` trong HIFI khớp **hai chiều** với `SRS` §2.3.1 / §2.3.3 | | design-check H3 |
| 3 | Mọi `data-key` có text **nguyên văn** bằng `SRS` §4.2 (W5) | | design-check H4 |
| 4 | Mọi trạng thái `SRS` §2.3.6 có khối `data-state` tương ứng; hai trạng thái rỗng trông khác nhau | | |
| 5 | HIFI chỉ dùng biến `--ds-*` và lớp `.ds-*`; không mã màu thô trong markup | | design-check H2 |
| 6 | Khối `:root` và khối `ds-components` trong HIFI **trùng nguyên văn** với `ds/tokens.css` + `ds/components.html` | | design-check H1 |
| 7 | Mỗi SCR có ≥ 2 khung nhìn (desktop + mobile) trong HIFI **và** trong `FIGMA/*.svg` | | design-check S1 |
| 8 | Mã màu trong SVG ⊆ tập token | | design-check S1 |
| 9 | §5 tương phản đạt AA cho mọi cặp chữ | | design-check T2 |
| 10 | §6 đã điền; không còn lệch Tồn tại/Hành vi/Text với SRS | | |
| 11 | Có chữ ký Designer (hoặc PO + `DEC-nn`) | | |

View File

@@ -0,0 +1,196 @@
# DS — Design System — <PROJECT>
| | |
|---|---|
| **Version** | 1.0 |
| **Date** | YYYY-MM-DD |
| **Author** | <BA/Design> (skill ba-design, activity `ds`) |
| **Status** | 🟡 Draft |
| **Approved by** | Designer: — · PO: — |
| **Source** | UICONV_<PROJECT> v1.0 · brand guideline: <đường dẫn / "chưa có — OQ-nnn"> · design system tham chiếu: <tên + version / "chưa có"> · SAD §7 (nếu có) |
| **Scope** | Toàn project — mọi `HIFI`, `FIGMA`, `DSPEC` có `PRODUCT = screen` |
| **Profile** | `screen · … · …` |
| **Tệp kèm** | `00-index/ds/tokens.json` (nguồn sự thật) · `ds/tokens.css` (sinh từ json) · `ds/components.html` (component sheet, mở bằng trình duyệt) |
## Change Log
| Version | Date | Người sửa | Thay đổi | CR |
|---|---|---|---|---|
| 1.0 | | | Bản đầu | — |
> **Vai trò của tài liệu này.** `UICONV` nói *hành xử thế nào* (phân trang, vị trí toast, validate lúc nào);
> tài liệu này nói *trông thế nào* (màu, chữ, khoảng cách, bo góc, đổ bóng, trạng thái hover/focus/disabled).
> `HIFI` và `FIGMA` chỉ dùng token và component ở đây; muốn khác phải có dòng ở §8.
>
> **Thứ tự ưu tiên khi lệch:** Brand guideline của khách > Design system tham chiếu (nếu dự án đã chọn) >
> `DS` này > `HIFI` từng US. Thiếu brand ⇒ giá trị ở đây là **đề xuất trung tính** chờ Designer/PO chốt — ghi `OQ`, không tự coi là đã duyệt.
---
## 0. Nguồn và độ tin cậy
| | |
|---|---|
| **Brand guideline** | *(file/URL + version, hoặc "Chưa có" ⇒ mọi giá trị màu/chữ ở §1 là placeholder trung tính, `OQ-nnn`)* |
| **Design system tham chiếu** | *(Material 3 / Ant Design / nội bộ… + version — theo `OQ` ở UICONV §0 hoặc quyết định của Tech Lead `DEC-nn`)* |
| **Prototype đã có** | *(Figma / HTML / SAD §7 — token được trích từ đây nếu có)* |
| **Ai sở hữu** | BA/Design đề xuất · **Designer duyệt** visual · PO duyệt giọng văn/nhận diện · Dev FE xác nhận khả năng hiện thực với stack đã chọn |
| **Độ tin cậy** | 🟢 Trích từ brand/DS thật · 🟡 Suy từ prototype · 🔴 Đề xuất trung tính, chưa có nguồn — *(chọn một, ghi lý do)* |
## 1. Token
*Bảng này tóm tắt; giá trị chuẩn ở `ds/tokens.json`. Sửa ⇒ sửa json, sinh lại css, chạy `design-check.mjs`.*
### 1.1 Màu
| Token | Giá trị | Dùng cho | Nguồn |
|---|---|---|---|
| `color.bg.canvas` | | Nền trang | |
| `color.bg.surface` | | Card, bảng, modal | |
| `color.bg.subtle` | | Header bảng, vùng phụ | |
| `color.border.default` / `.strong` | | Viền chia / viền ô nhập | |
| `color.text.primary` / `.secondary` / `.disabled` / `.inverse` | | Chữ chính / phụ / vô hiệu / trên nền đậm | |
| `color.brand.primary` / `-hover` / `-subtle` | | Nút chính, liên kết, focus / hover / nền chọn | |
| `color.status.danger` / `success` / `warning` (+ `-subtle`) | | Lỗi / thành công / cảnh báo | |
### 1.2 Chữ
| Token | Giá trị | Dùng cho |
|---|---|---|
| `font.family.base` | | Toàn hệ thống — *(font brand hay system stack? nguồn)* |
| `font.size.xs / sm / md / lg / xl / 2xl` | 12 / 14 / 16 / 20 / 24 / 32 px | Hint, nhãn / body / nhãn field lớn, h3 / h2 / h1 / số KPI |
| `font.weight.regular / semibold` | 400 / 600 | |
| `font.line-height.tight / normal` | 1.25 / 1.5 | Tiêu đề / body |
### 1.3 Khoảng cách, bo góc, đổ bóng, kích thước
| Token | Giá trị | Ghi chú |
|---|---|---|
| `space.1 … 12` | 4 · 8 · 12 · 16 · 24 · 32 · 48 px | Thang 4px; không dùng số ngoài thang |
| `radius.sm / md / pill` | 4 / 8 / 999 px | Nút, ô nhập / card, modal / badge |
| `shadow.sm / md` | | Card nổi / modal, toast |
| `size.control.desktop / touch` | 36 / 44 px | Chiều cao nút, ô nhập; **mobile ≥ 44px** (hit target) |
| `size.icon` | 20 px | Icon inline |
| `size.content-max` | 1200 px | Khớp UICONV §1 max-width |
| `breakpoint.tablet / desktop` | 768 / 1024 px | **Phải khớp UICONV §1** |
## 2. Thang chữ áp dụng
| Cấp | Token size / weight / line-height | Dùng ở |
|---|---|---|
| H1 — tiêu đề màn hình | xl / semibold / tight | `C01` kiểu "Tiêu đề cấp 1" |
| H2 — tiêu đề vùng, modal | lg / semibold / tight | |
| H3 — tiêu đề card | md / semibold / tight | |
| Body | sm / regular / normal | Bảng, form, câu dẫn |
| Nhãn field, hint, header bảng | xs / semibold hoặc regular / normal | |
## 3. Từ vựng component — ánh xạ ba chiều
*Cột 1 = UICONV §11 (BA) · cột 2 = lớp CSS trong `ds/components.html` (HIFI dùng) · cột 3 = component của design system tham chiếu (Dev FE dùng). Mọi kiểu trình bày xuất hiện trong `WF` phải có dòng ở đây.*
| Kiểu trình bày (UICONV §11) | Lớp DS | Component DS tham chiếu | Ghi chú |
|---|---|---|---|
| Nút chính | `.ds-btn.ds-btn--primary` | | Một nút chính mỗi màn hình |
| Nút phụ | `.ds-btn.ds-btn--secondary` | | |
| Nút nguy hiểm | `.ds-btn.ds-btn--danger` | | Luôn kèm modal xác nhận |
| Liên kết | `.ds-link` | | |
| Ô nhập / Ô nhập số | `.ds-field` + `.ds-input` | | Nhãn trên field, `*` sau nhãn |
| Chọn 1 (≤7) / (>7) | `.ds-check` radio / `.ds-select` | | |
| Chọn nhiều | `.ds-check` checkbox | | |
| Công tắc | `.ds-switch` | | |
| Bộ đếm số lượng | `.ds-stepper` | | |
| Bảng / Card | `.ds-table-wrap` / `.ds-cards > .ds-card` | | Bảng desktop → card mobile (UICONV §3) |
| Badge trạng thái | `.ds-badge[--info/--success/--warning/--danger]` | | Ánh xạ trạng thái BR §3 → biến thể: *(bảng riêng bên dưới)* |
| Tab | `.ds-tabs > .ds-tab` | | |
| Modal / Drawer / Bottom-sheet | `.ds-modal-backdrop > .ds-modal` | | Nút chính bên phải |
| Toast | `.ds-toast[--error]` | | Vị trí, thời lượng theo UICONV §5 |
| Banner trong vùng | `.ds-banner[--error/--warning]` | | |
| Skeleton | `.ds-skeleton` | | |
| Trạng thái rỗng (2 loại) | `.ds-empty` (có minh hoạ) / `.ds-empty.ds-empty--filter` (không) | | 🔴 Hai loại phải trông khác nhau |
| Trang trạng thái | `.ds-status-page` | | 404 / 403 / lỗi toàn trang |
**Ánh xạ trạng thái nghiệp vụ → badge** *(từ `BR` §3 của từng module; bổ sung dần)*:
| Trạng thái (BR) | Biến thể badge | Nhãn nguyên văn (SRS §4.2) |
|---|---|---|
| | | |
## 4. Trạng thái component
*Mỗi ô ghi thay đổi thị giác (token nào đổi). "—" = không có trạng thái đó. Ô trống = chưa thiết kế.*
| Component | Default | Hover | Focus | Disabled | Loading | Error / Invalid | Selected |
|---|---|---|---|---|---|---|---|
| Nút chính | `brand.primary` | `brand.primary-hover` | ring 2px `focus.ring` | opacity .45 + **tooltip lý do** | spinner + nhãn "Đang …" | — | — |
| Nút phụ | surface + `border.strong` | `bg.subtle` | ring | opacity .45 + tooltip | spinner | — | — |
| Ô nhập | `border.strong` | `text.secondary` | `brand.primary` + ring | `bg.subtle`, chữ disabled | — | `status.danger` + message dưới | — |
| Dòng bảng | surface | `bg.canvas` | ring | — | skeleton | — | `brand.primary-subtle` |
| Tab | `text.secondary` | | ring | | | | `brand.primary` + gạch dưới 2px |
| Card | | | | | | | |
## 5. Icon
| | |
|---|---|
| **Bộ icon** | *(tên bộ + version / "inline SVG tự vẽ theo mẫu components.html")* |
| **Quy cách** | Lưới 24, stroke 1.75, `stroke-linecap: round`, hiển thị 20px (`size.icon`), màu `currentColor` |
| **Cấm** | Emoji, ký hiệu ✓✕▲ dạng chữ thay icon (trừ mũi tên phân trang `‹ ›`), icon tô đặc trộn với icon nét |
| **Danh sách dùng** | search · plus · close · chevron-down · back · check · alert · trash · filter · *(bổ sung theo WF)* |
## 6. Tương phản và khả năng truy cập
*Kết quả chạy `node scripts/design-check.mjs --tokens ds/tokens.json` — chép nguyên văn, không sửa tay.*
| Cặp (fg / bg) | Dùng cho | Tỷ lệ | Ngưỡng | ☐/✅ |
|---|---|---|---|---|
| `text.primary` / `bg.surface` | Chữ thường | | 4.5 | |
| `text.secondary` / `bg.surface` | Hint, chữ phụ | | 4.5 | |
| `text.inverse` / `brand.primary` | Nhãn nút chính | | 4.5 | |
| `text.inverse` / `status.danger` | Nhãn nút nguy hiểm | | 4.5 | |
| `brand.primary` / `bg.surface` | Liên kết | | 4.5 | |
| `status.danger` / `bg.surface` | Message lỗi | | 4.5 | |
| `border.strong` / `bg.surface` | Viền ô nhập (non-text) | | 3.0 | |
| Quy tắc | Giá trị | Nguồn |
|---|---|---|
| Hit target tối thiểu trên mobile | 44 × 44 px | WCAG 2.5.8 / `size.control.touch` |
| Focus luôn thấy được | ring 2px `focus.ring`, offset 2px | WCAG 2.4.7 |
| Không truyền nghĩa chỉ bằng màu | Badge luôn có nhãn chữ; lỗi luôn có icon + text | WCAG 1.4.1 |
| Mọi ô nhập có nhãn | `<label>` bao ngoài hoặc `aria-label` | WCAG 1.3.1 |
| Thứ tự focus | Theo `WF` §3.x.6 của từng US | |
## 7. Motion
| Việc | Thời lượng | Easing | Ghi chú |
|---|---|---|---|
| Hover nút / dòng | 120 ms | ease | |
| Toast vào/ra | 200 ms | ease-out | Thời gian hiển thị theo UICONV §5 |
| Modal mở | 160 ms | ease-out | Không dùng bounce |
| Skeleton shimmer | 1.2 s lặp | linear | Tắt khi `prefers-reduced-motion` |
## 8. Ngoại lệ đã duyệt
*`HIFI`/`FIGMA` lệch DS ⇒ phải có dòng ở đây. Không có dòng ⇒ lệch là lỗi.*
| # | Tài liệu | Token/component lệch | DS nói | Thực tế | Vì sao | Ai duyệt | Ngày |
|---|---|---|---|---|---|---|---|
## 9. Open Questions
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì |
|---|---|---|---|---|
---
## Tự chấm
| # | Tiêu chí | ☐/✅ |
|---|---|---|
| 1 | `tokens.json` parse được; `tokens.css` sinh khớp (design-check T1 = ok) | |
| 2 | Mọi cặp tương phản ở §6 đạt ngưỡng (design-check T2 = ok) | |
| 3 | §3: mọi kiểu trình bày của UICONV §11 có lớp DS **và** có mặt trong `components.html` | |
| 4 | §4: mọi component có đủ default / hover / focus / disabled; component nhập liệu có Error | |
| 5 | `breakpoint.*` khớp UICONV §1; `size.content-max` khớp UICONV §1 | |
| 6 | Hai trạng thái rỗng trong `components.html` trông khác nhau | |
| 7 | §0 ghi rõ độ tin cậy; giá trị placeholder có `OQ` | |
| 8 | Có chữ ký Designer (hoặc PO ký thay + `DEC-nn` "không có Designer") | |

View File

@@ -0,0 +1,137 @@
/* 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 */

View File

@@ -0,0 +1,39 @@
# FIGMA_<US-id>_v1.0 — Gói nhập vào Figma
| | |
|---|---|
| **Sinh bởi** | skill `ba-design`, activity `figma` |
| **Nguồn** | `HIFI_<US-id>_v1.0.html` · `DS_<PROJECT>_v1.0.md` (`00-index/ds/tokens.json`) · `WF_<US-id>_v1.0.md` · `SRS_<US-id>_v1.0.md` |
| **Vì sao không có file `.fig`** | `.fig` là định dạng nhị phân độc quyền; REST API của Figma không cho tạo node thiết kế. Gói này gồm các định dạng Figma **nhập được** và giữ được layer/text/token. |
## Nội dung gói
| File | Định dạng | Nhập vào Figma bằng | Sửa được trong Figma |
|---|---|---|---|
| `SCR-xx.<view>[.<state>].svg` | SVG, một file mỗi màn hình × khung nhìn (× trạng thái khác default) | Kéo thả vào canvas, hoặc **File → Place image / Import** | ✅ Layer vector, text, tên layer = `Zn`/`Cnn` |
| `tokens.dtcg.json` | Design token chuẩn W3C DTCG (bản sao `00-index/ds/tokens.json`) | Plugin **Tokens Studio for Figma** → Import → JSON; hoặc **Variables → Import** nếu tài khoản có API variables | ✅ Thành Figma Variables / Styles |
| `png/SCR-xx.<view>.<state>.png` | Ảnh chụp HIFI bằng Chrome headless (`scripts/render-figma.sh`) | Kéo thả | ❌ Chỉ để tham chiếu và đối chiếu pixel |
| `../HIFI_<US-id>_v1.0.html` | HTML tự chứa | Plugin **html.to.design** (divRIOTS) → *Import from file* hoặc *from URL* khi host tạm bằng `python3 -m http.server` | ✅ Layer đầy đủ theo DOM, auto-layout |
## Thứ tự nhập khuyến nghị
1. **Token trước:** nhập `tokens.dtcg.json` bằng Tokens Studio → *Create Styles / Variables* để có bảng màu, chữ, khoảng cách trùng tên `--ds-*`.
2. **Component sheet:** nhập `../../00-index/ds/components.html` bằng html.to.design vào page *Components*; hoặc dựng component từ SVG và gán style vừa tạo.
3. **Màn hình:** kéo các file SVG vào page *Screens*, mỗi khung nhìn một frame (Desktop 1440×900 · Tablet 834×1112 · Mobile 390×844). Đổi tên frame theo `SCR-xx · <Tên màn hình> · <view>`.
4. **Trạng thái:** đặt các SVG `.<state>.svg` cạnh frame default, hoặc dùng Variants.
5. **Đối chiếu:** mở `png/` cạnh frame để kiểm tra khoảng cách và màu khớp HIFI.
## Lưu ý
- **Font:** SVG ghi font `<tên font base trong tokens.json>`; máy không có font ⇒ Figma thay bằng font gần nhất và báo *Missing fonts*. Cài font brand trước khi nhập.
- **Màu trong SVG là mã nguyên văn** từ `tokens.json` (Figma không đọc biến CSS); thuộc tính `data-token` ghi tên token để tra ngược. Sửa màu ⇒ sửa `tokens.json` rồi chạy lại `figma`, không sửa tay SVG.
- **Hành vi không nằm trong Figma.** Prototype link trong Figma chỉ mô phỏng điều hướng theo SRS §2.2; điều kiện, validation, mã lỗi đọc ở `SRS`. Khi Figma và SRS lệch: SRS thắng về tồn tại/hành vi/text, Figma thắng về bố cục (W11) — ghi lệch vào `WF §5`.
- **Không dùng gói này để thay `HIFI`.** HIFI là nguồn sự thật về visual đã duyệt; Figma là bản làm việc của Designer. Designer sửa trong Figma ⇒ báo BA để chạy lại `hifi` + `figma` với `--proto <link Figma>`.
## Kiểm tra trước khi giao
```bash
node .claude/skills/ba-design/scripts/design-check.mjs --tokens 00-index/ds/tokens.json --svg-dir 03-specification/design/FIGMA_<US-id>_v1.0
```
Phải in `ok` cho: mọi mã màu trong SVG thuộc tập token · mọi `SCR` của SRS §2.1 có ít nhất `desktop` + `mobile` · tập `id="Cnn"` trong SVG ⊆ tập `data-c` của HIFI.

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
FIGMA_<US-id>_v1.0/SCR-xx.desktop.svg — khung màn hình để KÉO THẢ VÀO FIGMA (Figma nhập SVG thành layer vector + text sửa được).
QUY TẮC:
· Một file cho mỗi SCR × khung nhìn: SCR-xx.desktop.svg (1440×900) · SCR-xx.tablet.svg (834×1112) · SCR-xx.mobile.svg (390×844). Trạng thái ≠ default ⇒ thêm hậu tố: SCR-xx.mobile.empty-none.svg.
· Figma KHÔNG hiểu biến CSS ⇒ dùng mã màu NGUYÊN VĂN từ tokens.json và ghi tên token trong data-token để design-check.mjs đối chiếu (mã màu không có trong tokens.json = lỗi).
· Cấu trúc layer = bảng vùng WF §3.x.1: <g id="Zn" data-name="…"> chứa <g id="Cnn"> cho mỗi thành phần. Tên id chính là tên layer trong Figma.
· Font: font-family lấy từ font.family.base; Figma sẽ thay bằng font gần nhất nếu máy không có — ghi rõ ở README-import.md.
· Text NGUYÊN VĂN theo SRS §4.2. Không vẽ hành vi; hành vi ở SRS.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="900" viewBox="0 0 1440 900" font-family="Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif" font-size="14" data-scr="SCR-xx" data-view="desktop" data-state="default" data-source="HIFI_US-xxx_v1.0.html#SCR-xx">
<title>SCR-xx · [Tên màn hình] · desktop · default</title>
<!-- Nền trang -->
<rect id="canvas" width="1440" height="900" fill="#F6F7F9" data-token="color.bg.canvas"/>
<g id="Z0" data-name="Header toàn site">
<rect width="1440" height="56" fill="#FFFFFF" data-token="color.bg.surface"/>
<line x1="0" y1="56.5" x2="1440" y2="56.5" stroke="#D5DAE1" data-token="color.border.default"/>
<text x="24" y="34" font-weight="600" fill="#1B1F24" data-token="color.text.primary">Tên hệ thống</text>
</g>
<g id="Z1" data-name="Header màn hình" transform="translate(120 80)">
<g id="C01"><text x="0" y="28" font-size="24" font-weight="600" fill="#1B1F24" data-token="color.text.primary" data-key="scr01.title">Danh sách cửa hàng</text></g>
<g id="C02" data-name="Nút chính">
<rect x="1096" y="0" width="104" height="36" rx="4" fill="#2456A6" data-token="color.brand.primary"/>
<text x="1148" y="23" text-anchor="middle" font-weight="600" fill="#FFFFFF" data-token="color.text.inverse" data-key="btn.create">Tạo mới</text>
</g>
</g>
<g id="Z2" data-name="Thanh lọc" transform="translate(120 140)">
<g id="C03" data-name="Ô nhập có icon tìm">
<rect width="320" height="36" rx="4" fill="#FFFFFF" stroke="#767F8C" data-token="color.border.strong"/>
<text x="40" y="23" fill="#5B6472" data-token="color.text.secondary" data-key="scr01.search.ph">Nhập mã hoặc tên…</text>
</g>
<g id="C04" data-name="Chọn 1" transform="translate(328 0)">
<rect width="200" height="36" rx="4" fill="#FFFFFF" stroke="#767F8C" data-token="color.border.strong"/>
<text x="12" y="23" fill="#1B1F24" data-token="color.text.primary" data-key="scr01.filter.status">Trạng thái: Tất cả</text>
</g>
</g>
<g id="Z3" data-name="Dữ liệu" transform="translate(120 200)">
<g id="C05" data-name="Bảng">
<rect width="1200" height="180" rx="8" fill="#FFFFFF" stroke="#D5DAE1" data-token="color.border.default"/>
<rect x="1" y="1" width="1198" height="40" rx="8" fill="#EEF1F5" data-token="color.bg.subtle"/>
<text x="16" y="26" font-size="12" font-weight="600" fill="#5B6472" data-token="color.text.secondary">MÃ</text>
<text x="216" y="26" font-size="12" font-weight="600" fill="#5B6472">TÊN</text>
<text x="616" y="26" font-size="12" font-weight="600" fill="#5B6472">TRẠNG THÁI</text>
<text x="816" y="26" font-size="12" font-weight="600" fill="#5B6472">NGÀY TẠO</text>
<line x1="0" y1="88" x2="1200" y2="88" stroke="#D5DAE1"/>
<text x="16" y="70" fill="#1B1F24">STR-001</text><text x="216" y="70" fill="#1B1F24">Cửa hàng A</text>
<rect x="616" y="54" width="52" height="22" rx="11" fill="#EEF1F5" data-token="color.bg.subtle"/><text x="642" y="69" text-anchor="middle" font-size="12" font-weight="600" fill="#1B1F24">Nháp</text>
<text x="816" y="70" fill="#1B1F24">30/08/2026</text>
<text x="1120" y="70" fill="#2456A6" text-decoration="underline" data-token="color.brand.primary" data-key="btn.detail">Chi tiết</text>
</g>
</g>
<g id="Z4" data-name="Phân trang" transform="translate(120 400)">
<g id="C06" data-name="Phân trang">
<text x="1140" y="22" text-anchor="end" font-size="12" fill="#5B6472" data-token="color.text.secondary">1 / 7</text>
</g>
</g>
</svg>

View File

@@ -0,0 +1,358 @@
<!--
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>

View File

@@ -0,0 +1,54 @@
/* tokens.css — SINH TỪ tokens.json, không sửa tay.
Quy tắc tên: --ds-<đường dẫn token nối bằng '-'>. Kiểm khớp: node scripts/design-check.mjs --tokens tokens.json --css tokens.css
Khối :root này được chép nguyên văn vào <style> của mọi HIFI_*.html và components.html (file tự chứa). */
: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;
}

View File

@@ -0,0 +1,105 @@
{
"$schema": "https://tr.designtokens.org/format/",
"$description": "DS_<PROJECT> — design token theo chuẩn W3C DTCG. Nguồn sự thật duy nhất về màu/chữ/khoảng cách; tokens.css và khối :root trong HIFI_*.html phải sinh từ file này (kiểm bằng scripts/design-check.mjs). Giá trị bên dưới là PLACEHOLDER trung tính — thay theo brand guideline; thiếu brand ⇒ giữ và ghi OQ trong DS §0.",
"color": {
"bg": {
"canvas": { "$type": "color", "$value": "#F6F7F9", "$description": "Nền trang" },
"surface": { "$type": "color", "$value": "#FFFFFF", "$description": "Nền card/bảng/modal" },
"subtle": { "$type": "color", "$value": "#EEF1F5", "$description": "Header bảng, vùng phụ" }
},
"border": {
"default": { "$type": "color", "$value": "#D5DAE1" },
"strong": { "$type": "color", "$value": "#767F8C" }
},
"text": {
"primary": { "$type": "color", "$value": "#1B1F24" },
"secondary": { "$type": "color", "$value": "#5B6472" },
"disabled": { "$type": "color", "$value": "#9AA3AF", "$description": "Miễn kiểm tương phản (WCAG 1.4.3 loại trừ thành phần vô hiệu)" },
"inverse": { "$type": "color", "$value": "#FFFFFF" }
},
"brand": {
"primary": { "$type": "color", "$value": "#2456A6", "$description": "Nút chính, liên kết, focus ring" },
"primary-hover": { "$type": "color", "$value": "#1C468A" },
"primary-subtle": { "$type": "color", "$value": "#E7EEF9", "$description": "Nền badge/hàng được chọn" }
},
"status": {
"danger": { "$type": "color", "$value": "#B42318" },
"danger-subtle": { "$type": "color", "$value": "#FDECEA" },
"success": { "$type": "color", "$value": "#1E7B45" },
"success-subtle": { "$type": "color", "$value": "#E6F4EC" },
"warning": { "$type": "color", "$value": "#B7791F" },
"warning-subtle": { "$type": "color", "$value": "#FFF4DE" }
},
"focus": { "ring": { "$type": "color", "$value": "#2456A6" } }
},
"font": {
"family": {
"base": { "$type": "fontFamily", "$value": ["Segoe UI", "Roboto", "Helvetica Neue", "Arial", "sans-serif"], "$description": "PLACEHOLDER — thay bằng font brand; giữ fallback stack" },
"mono": { "$type": "fontFamily", "$value": ["SFMono-Regular", "Consolas", "Menlo", "monospace"] }
},
"size": {
"xs": { "$type": "dimension", "$value": "12px" },
"sm": { "$type": "dimension", "$value": "14px" },
"md": { "$type": "dimension", "$value": "16px" },
"lg": { "$type": "dimension", "$value": "20px" },
"xl": { "$type": "dimension", "$value": "24px" },
"2xl": { "$type": "dimension", "$value": "32px" }
},
"weight": {
"regular": { "$type": "fontWeight", "$value": 400 },
"semibold": { "$type": "fontWeight", "$value": 600 }
},
"line-height": {
"tight": { "$type": "number", "$value": 1.25 },
"normal": { "$type": "number", "$value": 1.5 }
}
},
"space": {
"1": { "$type": "dimension", "$value": "4px" },
"2": { "$type": "dimension", "$value": "8px" },
"3": { "$type": "dimension", "$value": "12px" },
"4": { "$type": "dimension", "$value": "16px" },
"6": { "$type": "dimension", "$value": "24px" },
"8": { "$type": "dimension", "$value": "32px" },
"12": { "$type": "dimension", "$value": "48px" }
},
"radius": {
"sm": { "$type": "dimension", "$value": "4px" },
"md": { "$type": "dimension", "$value": "8px" },
"pill": { "$type": "dimension", "$value": "999px" }
},
"shadow": {
"sm": { "$type": "shadow", "$value": { "color": "#1B1F2414", "offsetX": "0px", "offsetY": "1px", "blur": "2px", "spread": "0px" } },
"md": { "$type": "shadow", "$value": { "color": "#1B1F2426", "offsetX": "0px", "offsetY": "4px", "blur": "12px", "spread": "0px" } }
},
"size": {
"control": {
"desktop": { "$type": "dimension", "$value": "36px", "$description": "Chiều cao nút/ô nhập desktop" },
"touch": { "$type": "dimension", "$value": "44px", "$description": "Chiều cao tối thiểu trên mobile (hit target ≥ 44px)" }
},
"icon": { "$type": "dimension", "$value": "20px" },
"content-max": { "$type": "dimension", "$value": "1200px" }
},
"breakpoint": {
"tablet": { "$type": "dimension", "$value": "768px", "$description": "Khớp UICONV §1" },
"desktop": { "$type": "dimension", "$value": "1024px", "$description": "Khớp UICONV §1" }
},
"$extensions": {
"ba": {
"project": "<PROJECT>",
"version": "1.0",
"source": "UICONV_<PROJECT> v1.0 · brand guideline: <đường dẫn / 'chưa có — OQ-nnn'>",
"contrastPairs": [
{ "fg": "color.text.primary", "bg": "color.bg.surface", "min": 4.5, "use": "Chữ thường trên nền card" },
{ "fg": "color.text.primary", "bg": "color.bg.canvas", "min": 4.5, "use": "Chữ thường trên nền trang" },
{ "fg": "color.text.secondary", "bg": "color.bg.surface", "min": 4.5, "use": "Chữ phụ, hint" },
{ "fg": "color.text.inverse", "bg": "color.brand.primary", "min": 4.5, "use": "Nhãn nút chính" },
{ "fg": "color.text.inverse", "bg": "color.status.danger", "min": 4.5, "use": "Nhãn nút nguy hiểm" },
{ "fg": "color.brand.primary", "bg": "color.bg.surface", "min": 4.5, "use": "Liên kết" },
{ "fg": "color.status.danger", "bg": "color.bg.surface", "min": 4.5, "use": "Message lỗi dưới field" },
{ "fg": "color.text.primary", "bg": "color.brand.primary-subtle", "min": 4.5, "use": "Chữ trên badge/hàng chọn" },
{ "fg": "color.border.strong", "bg": "color.bg.surface", "min": 3.0, "use": "Viền ô nhập (WCAG 1.4.11 non-text)" }
]
}
}
}