update skill ba
This commit is contained in:
72
.claude/skills/ba-design/GUIDE.md
Normal file
72
.claude/skills/ba-design/GUIDE.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# ba-design — Hướng dẫn sử dụng
|
||||
|
||||
## Dùng khi nào
|
||||
|
||||
Dự án có giao diện (`PRODUCT = screen`), đã có `SRS` + `WF` + `UICONV` của US, và bạn cần một trong:
|
||||
|
||||
- **Design system** cho project: token màu/chữ/khoảng cách, component sheet đủ trạng thái.
|
||||
- **Mockup hi-fi** của US: mọi màn hình × trạng thái × breakpoint, mở bằng trình duyệt, bấm điều hướng thử.
|
||||
- **Gói nhập Figma**: SVG từng màn hình, token, ảnh chụp, hướng dẫn nhập.
|
||||
- **Redline bàn giao Dev FE**: con số cụ thể, ánh xạ component, checklist truy cập.
|
||||
|
||||
Dự án **không có Designer** vẫn dùng được — skill đóng vai Designer đề xuất, PO ký thay kèm `DEC-nn`.
|
||||
|
||||
## Cần chuẩn bị
|
||||
|
||||
| Có | Tốt hơn nếu có |
|
||||
|---|---|
|
||||
| `SRS_<US>`, `WF_<US>` (md + html), `UICONV_<PROJECT>` từ ba-3 | Brand guideline (màu, font, logo) hoặc design system đã chọn |
|
||||
| `node` ≥ 18 để chạy phép kiểm | Chrome/Edge để chụp PNG cho gói Figma |
|
||||
| Biết dự án có Designer hay không | Link Figma/prototype đã có để đối chiếu |
|
||||
|
||||
## Cách gọi
|
||||
|
||||
```
|
||||
/ba-design <PROJECT> ds # sinh design system (một lần)
|
||||
/ba-design US-002 hifi # mockup hi-fi cho US-002
|
||||
/ba-design US-002 proto # bật điều hướng bấm được trên HIFI
|
||||
/ba-design US-002 figma # gói nhập Figma
|
||||
/ba-design US-002 dspec # redline + bàn giao
|
||||
/ba-design US-002 --brand brand/guide.pdf go # chạy liền, không dừng Bước 0
|
||||
```
|
||||
|
||||
Qua pipeline có duyệt: `/ba-pipeline` → stage `design`, activity tương ứng.
|
||||
|
||||
## Đọc kết quả
|
||||
|
||||
- Mở `HIFI_<US>_v1.0.html`, dùng toolbar: chọn màn hình, khung nhìn, trạng thái; tick **C-id/F-id** và
|
||||
**Vùng Zn** để đối chiếu với SRS/WF; chọn **Prototype** để bấm điều hướng.
|
||||
- Chạy phép kiểm:
|
||||
|
||||
```bash
|
||||
node .claude/skills/ba-design/scripts/design-check.mjs \
|
||||
--tokens ba-output/<P>/00-index/ds/tokens.json --css ba-output/<P>/00-index/ds/tokens.css \
|
||||
--components ba-output/<P>/00-index/ds/components.html \
|
||||
--html ba-output/<P>/03-specification/design/HIFI_<US>_v1.0.html \
|
||||
--srs ba-output/<P>/03-specification/SRS_<US>_v1.0.md --wf ba-output/<P>/03-specification/WF_<US>_v1.0.md \
|
||||
--svg-dir ba-output/<P>/03-specification/design/FIGMA_<US>_v1.0
|
||||
```
|
||||
|
||||
Mọi dòng phải `✅`. `🔴 H3` = thành phần lệch SRS; `🔴 H4` = text bị sửa; `🔴 T2` = màu không đủ tương phản.
|
||||
- Chụp ảnh cho gói Figma:
|
||||
|
||||
```bash
|
||||
.claude/skills/ba-design/scripts/render-figma.sh ba-output/<P>/03-specification/design/HIFI_<US>_v1.0.html \
|
||||
ba-output/<P>/03-specification/design/FIGMA_<US>_v1.0/png
|
||||
```
|
||||
|
||||
## Nhập vào Figma
|
||||
|
||||
Đọc `FIGMA_<US>_v1.0/README-import.md`. Tóm tắt: token trước (Tokens Studio), rồi component sheet
|
||||
(html.to.design), rồi kéo SVG từng màn hình vào frame. **Không có file `.fig`** — Figma không cho tạo
|
||||
file bằng API; gói này là các định dạng Figma nhập được và giữ layer.
|
||||
|
||||
## Lỗi thường gặp
|
||||
|
||||
| Triệu chứng | Nguyên nhân | Sửa |
|
||||
|---|---|---|
|
||||
| `🔴 H1 khối :root không trùng` | Sửa tay tokens.css hoặc HIFI thay vì sửa tokens.json | `--emit-css` sinh lại, chép vào HIFI |
|
||||
| `🔴 H3 chỉ có trong HIFI: C07` | HIFI vẽ nút prototype có mà SRS không có | Bọc `hf-proto-only`, ghi WF §5 |
|
||||
| `🔴 H4 text khác SRS` | Design "làm gọn" nhãn | Chép lại nguyên văn; muốn đổi ⇒ OQ cho BA/PO |
|
||||
| HIFI đẹp nhưng Dev hỏi "màu này là gì" | DS §0 ghi độ tin cậy 🔴 mà không ai chốt | Đưa OQ brand lên PO trước G3 |
|
||||
| Figma báo *Missing fonts* | Máy không có font brand | Cài font hoặc đổi `font.family.base` |
|
||||
181
.claude/skills/ba-design/SKILL.md
Normal file
181
.claude/skills/ba-design/SKILL.md
Normal file
@@ -0,0 +1,181 @@
|
||||
---
|
||||
name: ba-design
|
||||
description: Vai trò Design trong quy trình BA — chạy song song GĐ3 khi PRODUCT = screen, sau khi ba-3 đã có SRS + WF + UICONV. Sinh design system cấp project (DS - token chuẩn W3C DTCG, tokens.css, component sheet đủ trạng thái hover/focus/disabled/error), mockup hi-fi tự chứa cho từng US (HIFI - mọi màn hình × trạng thái × breakpoint, có chế độ prototype bấm được), gói nhập Figma (FIGMA - SVG mỗi màn hình, token DTCG, PNG chụp bằng Chrome headless, hướng dẫn nhập; không sinh được .fig) và đặc tả thiết kế/redline bàn giao Dev FE (DSPEC). Mọi thứ truy vết về SRS/WF/UICONV và được kiểm bằng máy - C-id hai chiều, text nguyên văn theo SRS §4.2, chỉ dùng token, tương phản WCAG AA. Kích hoạt khi người dùng nói "thiết kế giao diện", "mockup", "hi-fi", "design system", "token", "chọn màu/font/khoảng cách", "prototype bấm được", "xuất Figma", "file Figma", "redline", "bàn giao dev FE", "component sheet", "kiểm tra tương phản", "dự án không có Designer". Output vào ba-output/<PROJECT>/00-index/ds/ và 03-specification/design/; ký cùng Gate G3 bởi Designer, hoặc PO + DEC khi dự án không có Designer.
|
||||
---
|
||||
|
||||
# ⊕ DESIGN — Vai trò Design trong bộ BA
|
||||
|
||||
Mục tiêu: **biến SRS + WF + UICONV thành thiết kế giao diện hoàn chỉnh mà Dev FE dựng được không phải
|
||||
đoán, Designer sửa được trong Figma, và máy kiểm được là không lệch đặc tả.**
|
||||
|
||||
Skill này **đóng vai Designer khi dự án không có Designer**, và **làm phần chuẩn bị cho Designer**
|
||||
khi có. Nó không thay đổi ranh giới của `ba-3`: `WF` vẫn là low-fi và dừng ở bố cục; phần "Designer
|
||||
còn phải làm" ở `WF` §6 chính là việc của skill này.
|
||||
|
||||
Output — chỉ khi `PRODUCT = screen`; loại khác trả "N/A theo PRODUCT", không ghi file:
|
||||
|
||||
| Mã | Ở đâu | Tần suất |
|
||||
|---|---|---|
|
||||
| `DS` | `00-index/DS_<PROJECT>_v1.0.md` + `00-index/ds/{tokens.json, tokens.css, components.html}` | Một lần / project, bồi đắp dần |
|
||||
| `HIFI` | `03-specification/design/HIFI_<US>_v1.0.html` | Mỗi US, sau `wf` |
|
||||
| `FIGMA` | `03-specification/design/FIGMA_<US>_v1.0/` (`*.svg`, `tokens.dtcg.json`, `png/`, `README-import.md`) | Mỗi US |
|
||||
| `DSPEC` | `03-specification/design/DSPEC_<US>_v1.0.md` | Mỗi US |
|
||||
|
||||
## Bốn nguyên tắc bất di bất dịch
|
||||
|
||||
1. **Không bịa yêu cầu** — ở đây là *không bịa visual*: thiếu brand guideline ⇒ dùng token trung tính
|
||||
của template, ghi độ tin cậy 🔴 ở `DS` §0 và `OQ`, không viết như thể đã có nhận diện.
|
||||
2. **Không quyết định thay PO** — và **không quyết hành vi thay SRS**: HIFI *chép* thành phần, trạng
|
||||
thái, text từ SRS. Muốn thêm nút, đổi nhãn ⇒ ghi lệch ở `DSPEC` §6 / `WF` §5, PO quyết.
|
||||
3. **Mọi phát biểu truy vết được** — mỗi `data-c`/`data-f` về SRS §2.3, mỗi `data-key` về SRS §4.2,
|
||||
mỗi màu về một token, mỗi token về brand/prototype/`OQ`.
|
||||
4. **Không ghi đè tài liệu đã qua gate** — `DS` đã Baselined chỉ sửa qua `CR`; đổi token là đổi mọi HIFI.
|
||||
|
||||
🔴 **Nguyên tắc riêng: skill này không tự duyệt.** Mọi artifact sinh ra là `🟡 Draft` chờ **Designer**
|
||||
ký; dự án không có Designer ⇒ PO ký thay và tham chiếu `DEC-nn` (theo `../ba-lifecycle/references/workflow.md` §3).
|
||||
|
||||
Nạp bắt buộc trước khi làm: `../ba-lifecycle/references/{domain-profiles,workflow,artifact-map,writing-rules}.md`
|
||||
· `templates/` của skill này · `scripts/design-check.mjs` (đọc phần đầu để biết phép kiểm nào sẽ chạy).
|
||||
|
||||
## Bước 0 — Chốt input rồi dừng lại
|
||||
|
||||
**Chưa được ghi file.** Làm bảy việc rồi **dừng chờ trả lời**:
|
||||
|
||||
1. **Profile** — `PRODUCT` phải là `screen`. Không phải ⇒ nói "N/A theo PRODUCT" và dừng.
|
||||
2. **Input từ ba-3 đã có chưa** — bảng `File | Version | Status` cho `UICONV_<PROJECT>`, `SRS_<US>`,
|
||||
`WF_<US>` (md + html). Thiếu `WF` ⇒ HIFI không có nguồn bố cục ⇒ đề nghị chạy `/ba-3-specification`
|
||||
activity `wf` trước; người dùng vẫn muốn ⇒ làm, ghi `OQ` và độ tin cậy 🔴.
|
||||
3. **`DS` đã có chưa** — có ⇒ chỉ đọc và bồi đắp §3 (badge ↔ trạng thái BR), §8 (ngoại lệ); chưa có ⇒
|
||||
lần chạy này phải sinh `DS` trước (activity `ds`).
|
||||
4. **Nguồn visual**, tìm theo thứ tự và trình bảng `Nguồn | Định dạng | Phủ gì | Độ tin cậy`:
|
||||
tham số `--brand` · thư mục `design/`, `brand/`, `prototype/` · link Figma người dùng đưa · `UICONV` §0
|
||||
(design system đã chọn) · `SAD` §7 · `DEC-nn` của Tech Lead về design system. **Không có ⇒ token trung
|
||||
tính của template, độ tin cậy 🔴, nói rõ điều đó.**
|
||||
5. **Dự án có Designer không** — có ⇒ Designer ký `DS`, `HIFI`, `DSPEC`; không ⇒ tìm `DEC-nn` "không có
|
||||
Designer" (ví dụ `DEC-01` của e-commerce); chưa có ⇒ đề xuất ghi, PO ký thay.
|
||||
6. **Phạm vi lần chạy** — đích danh `US` (≤ 3) và activity: `ds` · `hifi` · `proto` · `figma` · `dspec`.
|
||||
7. **Công cụ** — `node` để chạy `scripts/design-check.mjs`; Chrome/Edge để chạy `scripts/render-figma.sh`.
|
||||
Không có Chrome ⇒ `FIGMA/png/` bỏ trống và ghi hướng dẫn chụp tay trong `README-import.md`.
|
||||
|
||||
Rồi **hỏi xác nhận** bảy điểm trên. Bỏ qua khi lệnh có `go`.
|
||||
|
||||
## Thực hiện — 5 activity
|
||||
|
||||
Thứ tự bắt buộc trong một US: `ds` (nếu chưa có) → `hifi` → `proto` (tuỳ chọn) → `figma` → `dspec`.
|
||||
`figma` và `dspec` đọc HIFI, nên HIFI đổi ⇒ chạy lại cả hai.
|
||||
|
||||
### Activity `ds` — Design system cấp project
|
||||
|
||||
Dùng `templates/design-system.md` → `00-index/DS_<PROJECT>_v1.0.md`, và ba tệp kèm trong `00-index/ds/`:
|
||||
|
||||
1. **`tokens.json`** — chép `templates/tokens.json`, điền theo nguồn ở Bước 0. Giữ đúng cấu trúc và tên
|
||||
token (HIFI/CSS phụ thuộc tên). Ba thứ **phải khớp `UICONV`**: `breakpoint.*` với §1, `size.content-max`
|
||||
với §1, `size.control.touch ≥ 44px`. Điền `$extensions.ba.contrastPairs` cho **mọi** cặp chữ/nền dùng
|
||||
trong components.html. Thiếu brand ⇒ giữ giá trị placeholder, ghi `"source": "… — OQ-nnn"`.
|
||||
2. **`tokens.css`** — sinh từ json theo quy tắc `--ds-<đường dẫn nối bằng '-'>`. Có Bash ⇒
|
||||
`node scripts/design-check.mjs --tokens tokens.json --emit-css > tokens.css`; không có ⇒ viết tay
|
||||
đúng quy tắc, phép kiểm T1 sẽ bắt sai lệch.
|
||||
3. **`components.html`** — chép `templates/components.html`, **thay khối `:root`** bằng tokens.css, **giữ
|
||||
nguyên khối `ds-components … BEGIN/END`**. Chỉ sửa CSS component khi có dòng `DS` §8 và sửa đồng thời
|
||||
ở mọi HIFI. Bổ sung component nếu `UICONV` §11 có kiểu trình bày chưa có lớp `.ds-*`.
|
||||
4. **`DS_<PROJECT>.md`** — §3 ánh xạ ba chiều *(UICONV §11 · lớp DS · component design system tham chiếu)*
|
||||
phải phủ **mọi** kiểu trình bày xuất hiện trong các `WF` hiện có; §4 trạng thái đủ default/hover/focus/
|
||||
disabled, component nhập liệu thêm error; §6 chép kết quả T2 nguyên văn.
|
||||
|
||||
🔴 **Không chọn màu brand thay khách.** Placeholder xanh trung tính trong template là để HIFI *chạy được*,
|
||||
không phải đề xuất nhận diện. `DS` §0 phải ghi độ tin cậy và ai sẽ chốt.
|
||||
|
||||
### Activity `hifi` — Mockup hi-fi từng US
|
||||
|
||||
Dùng `templates/hifi.html` → `03-specification/design/HIFI_<US>_v1.0.html`. Làm **sau** `wf` của ba-3.
|
||||
|
||||
1. **`<style>`** = khối `:root` của `ds/tokens.css` + khối `ds-components` của `ds/components.html`
|
||||
**nguyên văn** + phần "Khung xem HIFI" của template giữ nguyên. Phép kiểm H1 so từng ký tự.
|
||||
2. **Mỗi `SCR` của SRS §2.1** một `<section class="hf-frame" id="SCR-xx">` — kể cả modal và trang trạng
|
||||
thái. Bố cục theo bảng vùng `WF` §3.x.1: mỗi vùng `data-zone="Zn" data-name`. Thứ tự DOM = thứ tự focus
|
||||
`WF` §3.x.6.
|
||||
3. **Mỗi thành phần** SRS §2.3.1 → `data-c`; mỗi field §2.3.3 → `data-f`; kiểu trình bày → lớp `.ds-*`
|
||||
theo `DS` §3. Tập ID **khớp hai chiều** với SRS (H3). Thành phần chỉ có ở prototype ⇒
|
||||
`<span class="hf-proto-only" data-ref="WF §5 #n">`, **không** gán `data-c`.
|
||||
4. **Mọi chuỗi hiển thị** → `data-key` + text **nguyên văn** SRS §4.2 (W5, phép kiểm H4). Message lỗi
|
||||
dùng `data-key="E-<DOMAIN>-nnnn"`. Không "làm gọn" câu chữ — text là của BA, không phải của Design.
|
||||
5. **Trạng thái** SRS §2.3.6 / `WF` §3.x.3 → khối `data-state` trong cùng vùng: `default · loading ·
|
||||
empty-none · empty-filter · error · submitting · invalid`. Hai trạng thái rỗng **trông khác nhau**
|
||||
(`.ds-empty` có minh hoạ + nút chính · `.ds-empty--filter` không minh hoạ + nút phụ).
|
||||
6. **Ẩn/khoá/read-only** (W7) → `data-vis`, disabled **bắt buộc `title`** nêu lý do.
|
||||
7. **Responsive** — mọi SCR phải xem được ở `desktop` và `mobile` (bảng → card qua `.ds-table-wrap`/
|
||||
`.ds-cards`; `.ds-hide-m`/`.ds-hide-d`); `tablet` nếu `UICONV` §1 có.
|
||||
8. **Cấm** mã màu/px thô trong markup (trừ độ rộng cột theo SRS §2.3.2), emoji thay icon, font ngoài token.
|
||||
|
||||
Sau khi ghi: chạy `design-check.mjs --tokens … --components … --html HIFI --srs SRS --wf WF`; H1–H5 phải
|
||||
`✅`. Không có Bash ⇒ tự kiểm bằng grep tập `[CF][0-9]{2}` hai phía và báo trong kết quả.
|
||||
|
||||
### Activity `proto` — Prototype bấm được *(tuỳ chọn)*
|
||||
|
||||
Không tạo file mới. Trên HIFI: mỗi cạnh của sơ đồ điều hướng SRS §2.2 → `data-nav="SCR-yy"` trên đúng
|
||||
thành phần gây điều hướng, **kể cả cạnh quay lại** (Huỷ, Đóng, Back). Mở `?mode=proto` để thử. Ghi bảng
|
||||
luồng vào `DSPEC` §4; cạnh có trong HIFI mà SRS không có ⇒ `DSPEC` §6.
|
||||
|
||||
### Activity `figma` — Gói nhập Figma
|
||||
|
||||
Tạo `03-specification/design/FIGMA_<US>_v1.0/`:
|
||||
|
||||
1. **SVG** — từ `templates/figma-package/SCR-xx.desktop.svg`, một file mỗi `SCR × {desktop, mobile}`
|
||||
(+ `tablet` nếu UICONV có; + trạng thái `empty-none`/`error` là hậu tố). Layer `<g id="Zn">` theo WF,
|
||||
`<g id="Cnn">` theo SRS. **Màu là mã nguyên văn từ tokens.json** kèm `data-token` — Figma không đọc
|
||||
biến CSS. Text nguyên văn SRS §4.2.
|
||||
2. **`tokens.dtcg.json`** — bản sao `00-index/ds/tokens.json`.
|
||||
3. **`README-import.md`** — từ template, điền tên US/project, font brand, danh sách file.
|
||||
4. **`png/`** — người điều phối (có Bash) chạy `scripts/render-figma.sh HIFI… FIGMA…/png`; runner không
|
||||
có Bash ⇒ để trống thư mục và ghi việc này vào `humanInputNeeded`.
|
||||
|
||||
🔴 **Nói thẳng với người dùng:** không có file `.fig`; gói này là những định dạng Figma **nhập được** và
|
||||
giữ được layer/token. Designer sửa trong Figma ⇒ link Figma quay lại làm `--proto` cho vòng sau.
|
||||
|
||||
### Activity `dspec` — Đặc tả thiết kế & bàn giao
|
||||
|
||||
Dùng `templates/design-spec.md` → `03-specification/design/DSPEC_<US>_v1.0.md`. Điền §1 bản đồ tệp cho
|
||||
**mọi** SCR, §2 redline (token, kích thước — không viết "xem hình"), §3 ánh xạ trạng thái BR → badge,
|
||||
§5 khả năng truy cập, **§6 lệch** (bắt buộc kể cả rỗng), §7 bàn giao. Chép kết quả `design-check.mjs`
|
||||
vào §6 nguyên văn.
|
||||
|
||||
## Trước khi kết thúc
|
||||
|
||||
In bốn thứ:
|
||||
|
||||
**① Kết quả `design-check.mjs`** nguyên văn (T1, T2, H1–H5, S1). Còn 🔴 ⇒ nói thẳng G3 phần design
|
||||
chưa đạt, liệt kê ID/khoá/mã màu lệch. Không chạy được ⇒ nói rõ phép kiểm nào chưa chạy.
|
||||
|
||||
**② Bảng tự chấm G3 — phần design** (`../ba-lifecycle/references/workflow.md` §2, các dòng *(screen · design)*)
|
||||
dạng ☐/✅.
|
||||
|
||||
**③ Độ tin cậy visual** (🟢/🟡/🔴 theo `DS` §0) và **`OQ` mở** — cái nào chặn G3 đánh 🔴.
|
||||
|
||||
**④ Chữ ký cần** — Designer cho `DS`, `HIFI`, `DSPEC`; không có Designer ⇒ PO + `DEC-nn`. `RIGOR = light`
|
||||
⇒ `HIFI` không bắt buộc (xem workflow.md); `strict` ⇒ thêm tương phản AA cho **mọi** cặp và đủ tablet.
|
||||
|
||||
## Bẫy thường gặp
|
||||
|
||||
**Chép prototype vào HIFI.** Prototype có nút mà SRS không có ⇒ `hf-proto-only`, không `data-c`. Thêm
|
||||
thẳng vào là scope creep bằng hình ảnh (W11).
|
||||
|
||||
**Sửa text cho "đẹp".** "Tạo mới" thành "Tạo" là vi phạm W5 và H4 sẽ bắt. Text là của BA; muốn đổi ⇒ `OQ`.
|
||||
|
||||
**Chọn màu brand rồi coi là chốt.** Placeholder trung tính không phải đề xuất nhận diện; `DS` §0 phải ghi
|
||||
🔴 và ai chốt.
|
||||
|
||||
**Mã màu thô trong markup.** `style="color:#333"` là chỗ HIFI lệch DS im lặng. H2 bắt; sửa bằng biến.
|
||||
|
||||
**Chỉ vẽ desktop.** Bảng → card ở mobile là quyết định thiết kế thật (cột nào ưu tiên — `UICONV` §3), không
|
||||
phải "responsive tự lo".
|
||||
|
||||
**Quên trạng thái rỗng thứ hai, hoặc vẽ giống nhau.** Đây là lỗi kinh điển kế thừa từ ba-3.
|
||||
|
||||
**Disabled không có tooltip.** Nút mờ mà không nói vì sao khiến người dùng tưởng mất quyền (W7).
|
||||
|
||||
**Emoji thay icon.** Không scale, không đổi màu được, mỗi hệ điều hành một hình.
|
||||
|
||||
**Tưởng xuất được `.fig`.** Không có API tạo file Figma; hãy nói rõ ngay từ Bước 0.
|
||||
|
||||
**HIFI lệch WF về vị trí mà không ghi.** Đổi bố cục là quyền của Designer, nhưng phải có dòng ở `DSPEC` §6
|
||||
để BA cập nhật WF — không thì hai tài liệu kể hai câu chuyện.
|
||||
195
.claude/skills/ba-design/scripts/design-check.mjs
Executable file
195
.claude/skills/ba-design/scripts/design-check.mjs
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env node
|
||||
// design-check.mjs — phép kiểm máy cho artifact của skill ba-design (DS / HIFI / FIGMA).
|
||||
// Chỉ dùng Node ≥ 18, không phụ thuộc gói ngoài. Không sửa file. Exit 1 khi có 🔴.
|
||||
//
|
||||
// node design-check.mjs --tokens 00-index/ds/tokens.json [--css 00-index/ds/tokens.css]
|
||||
// [--components 00-index/ds/components.html] [--html 03-specification/design/HIFI_US-002_v1.0.html]
|
||||
// [--srs 03-specification/SRS_US-002_v1.0.md] [--wf 03-specification/WF_US-002_v1.0.md]
|
||||
// [--svg-dir 03-specification/design/FIGMA_US-002_v1.0] [--emit-css] [--json]
|
||||
//
|
||||
// Phép kiểm: T1 tokens.json ↔ tokens.css T2 tương phản WCAG theo $extensions.ba.contrastPairs
|
||||
// H1 :root + khối ds-components trong HTML trùng nguồn H2 mã màu thô trong markup
|
||||
// H3 C-id/F-id SRS ↔ HIFI hai chiều H4 text data-key ↔ SRS §4.2 H5 SCR SRS §2.1 ↔ HIFI
|
||||
// S1 SVG: màu ⊆ token · mỗi SCR có desktop+mobile · id Cnn ⊆ HIFI
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const opt = { html: [] }
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i]
|
||||
if (a === '--html') opt.html.push(args[++i])
|
||||
else if (a.startsWith('--')) { const k = a.slice(2); const nxt = args[i + 1]; if (nxt && !nxt.startsWith('--')) { opt[k] = nxt; i++ } else opt[k] = true }
|
||||
}
|
||||
if (!opt.tokens) { console.error('Thiếu --tokens <tokens.json>'); process.exit(2) }
|
||||
|
||||
const out = []
|
||||
let red = 0
|
||||
const ok = (id, msg, details) => out.push({ id, status: '✅', msg, details })
|
||||
const warn = (id, msg, details) => out.push({ id, status: '🟠', msg, details })
|
||||
const bad = (id, msg, details) => { red++; out.push({ id, status: '🔴', msg, details }) }
|
||||
const read = (p) => fs.readFileSync(p, 'utf8')
|
||||
const norm = (s) => s.replace(/\s+/g, ' ').trim()
|
||||
|
||||
// ---------- Token ----------
|
||||
const tokens = JSON.parse(read(opt.tokens))
|
||||
const flat = {} // 'color.bg.canvas' -> {type, value}
|
||||
;(function walk(node, pth) {
|
||||
for (const [k, v] of Object.entries(node)) {
|
||||
if (k.startsWith('$')) continue
|
||||
if (v && typeof v === 'object' && '$value' in v) flat[[...pth, k].join('.')] = { type: v.$type, value: v.$value }
|
||||
else if (v && typeof v === 'object') walk(v, [...pth, k])
|
||||
}
|
||||
})(tokens, [])
|
||||
const cssName = (p) => '--ds-' + p.replace(/\./g, '-')
|
||||
const cssVal = (t) => {
|
||||
const v = t.value
|
||||
if (Array.isArray(v)) return v.map((f) => (/\s/.test(f) || /[A-Z]/.test(f) ? `"${f}"` : f)).join(',')
|
||||
if (t.type === 'shadow' && v && typeof v === 'object') return `${v.offsetX} ${v.offsetY} ${v.blur} ${v.spread} ${v.color}`
|
||||
return String(v)
|
||||
}
|
||||
const expectedRoot = ':root{\n' + Object.entries(flat).map(([p, t]) => ` ${cssName(p)}:${cssVal(t)};`).join('\n') + '\n}'
|
||||
if (opt['emit-css']) { console.log(expectedRoot); process.exit(0) }
|
||||
const tokenHex = new Set(Object.values(flat).filter((t) => t.type === 'color').map((t) => String(t.value).toUpperCase().slice(0, 7)))
|
||||
ok('T0', `tokens.json: ${Object.keys(flat).length} token, ${tokenHex.size} màu`)
|
||||
|
||||
// T1
|
||||
if (opt.css) {
|
||||
const css = read(opt.css)
|
||||
const m = css.match(/:root\{[\s\S]*?\n\}/)
|
||||
if (!m) bad('T1', 'tokens.css không có khối :root')
|
||||
else {
|
||||
const have = Object.fromEntries([...m[0].matchAll(/(--ds-[\w-]+):([^;]+);/g)].map((x) => [x[1], norm(x[2])]))
|
||||
const want = Object.fromEntries(Object.entries(flat).map(([p, t]) => [cssName(p), norm(cssVal(t))]))
|
||||
const missing = Object.keys(want).filter((k) => !(k in have))
|
||||
const extra = Object.keys(have).filter((k) => !(k in want))
|
||||
const diff = Object.keys(want).filter((k) => k in have && have[k] !== want[k]).map((k) => `${k}: css=${have[k]} json=${want[k]}`)
|
||||
if (missing.length || extra.length || diff.length) bad('T1', 'tokens.css lệch tokens.json', { missing, extra, diff })
|
||||
else ok('T1', 'tokens.css khớp tokens.json')
|
||||
}
|
||||
}
|
||||
|
||||
// T2 contrast
|
||||
function lum(hex) {
|
||||
const h = hex.replace('#', '').slice(0, 6)
|
||||
const c = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255).map((x) => (x <= 0.03928 ? x / 12.92 : ((x + 0.055) / 1.055) ** 2.4))
|
||||
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]
|
||||
}
|
||||
const ratio = (a, b) => { const [l1, l2] = [lum(a), lum(b)].sort((x, y) => y - x); return (l1 + 0.05) / (l2 + 0.05) }
|
||||
const pairs = tokens.$extensions?.ba?.contrastPairs || []
|
||||
const contrast = []
|
||||
for (const p of pairs) {
|
||||
const fg = flat[p.fg]?.value, bg = flat[p.bg]?.value
|
||||
if (!fg || !bg) { bad('T2', `Cặp tương phản tham chiếu token không tồn tại: ${p.fg} / ${p.bg}`); continue }
|
||||
const r = Math.round(ratio(fg, bg) * 100) / 100
|
||||
const pass = r >= (p.min || 4.5)
|
||||
contrast.push({ fg: p.fg, bg: p.bg, ratio: r, min: p.min || 4.5, pass, use: p.use })
|
||||
if (!pass) bad('T2', `Tương phản ${p.fg} / ${p.bg} = ${r} < ${p.min || 4.5} (${p.use || ''})`)
|
||||
}
|
||||
if (pairs.length && contrast.every((c) => c.pass)) ok('T2', `Tương phản: ${contrast.length}/${contrast.length} cặp đạt`, contrast.map((c) => `${c.fg} / ${c.bg} = ${c.ratio} (≥${c.min})`))
|
||||
if (!pairs.length) warn('T2', 'tokens.json không khai $extensions.ba.contrastPairs — không kiểm được tương phản')
|
||||
|
||||
// ---------- HTML ----------
|
||||
const idsOf = (s) => new Set([...s.matchAll(/\b([CF][0-9]{2})\b/g)].map((m) => m[1]))
|
||||
const attrIds = (html) => new Set([...html.matchAll(/data-[cf]="([CF][0-9]{2})"/g)].map((m) => m[1]))
|
||||
const srcComponents = opt.components ? read(opt.components) : null
|
||||
const compBlock = (s) => { const m = s.match(/\/\* ds-components v[\d.]+ — BEGIN[\s\S]*?\/\* ds-components v[\d.]+ — END \*\//); return m ? norm(m[0]) : null }
|
||||
|
||||
let srs = opt.srs ? read(opt.srs) : null
|
||||
let srsIds = srs ? idsOf(srs.replace(/```[\s\S]*?```/g, '')) : null
|
||||
let srsScr = srs ? new Set([...srs.matchAll(/^\|\s*(SCR-\d{2})\s*\|/gm)].map((m) => m[1])) : null
|
||||
let srsText = null
|
||||
if (srs) {
|
||||
const sec = srs.split(/^### 4\.2[^\n]*\n/m)[1]?.split(/^##+ /m)[0] || ''
|
||||
const rows = sec.split('\n').filter((l) => /^\|/.test(l) && !/^\|\s*-+/.test(l))
|
||||
if (rows.length > 1) {
|
||||
const hdr = rows[0].split('|').map((c) => c.trim())
|
||||
const ki = hdr.findIndex((h) => /^Khoá|^Khóa|^Key/i.test(h)), ti = hdr.findIndex((h) => /^(Text \(VI\)|Text VI|Text|VI)$/i.test(h))
|
||||
if (ki >= 0 && ti >= 0) { srsText = {}; for (const r of rows.slice(1)) { const c = r.split('|').map((x) => x.trim()); if (c[ki + 0]) srsText[c[ki].replace(/`/g, '')] = c[ti].replace(/^"|"$/g, '') } }
|
||||
}
|
||||
}
|
||||
let hifiIds = new Set(), hifiScr = new Set()
|
||||
for (const f of opt.html) {
|
||||
const html = read(f), tag = path.basename(f)
|
||||
// H1
|
||||
const m = html.match(/:root\{[\s\S]*?\n\}/)
|
||||
if (!m || norm(m[0]) !== norm(expectedRoot)) bad('H1', `${tag}: khối :root không trùng tokens.json (chạy --emit-css để lấy khối chuẩn)`)
|
||||
else ok('H1', `${tag}: khối :root khớp tokens.json`)
|
||||
if (srcComponents && path.resolve(f) !== path.resolve(opt.components)) {
|
||||
const a = compBlock(html), b = compBlock(srcComponents)
|
||||
if (!a || !b) bad('H1', `${tag}: không tìm thấy khối "ds-components … BEGIN/END" trong HTML hoặc components.html`)
|
||||
else if (a !== b) bad('H1', `${tag}: khối ds-components lệch components.html — chép lại nguyên văn`)
|
||||
else ok('H1', `${tag}: khối ds-components khớp components.html`)
|
||||
}
|
||||
// H2 raw colors in markup (outside <style>)
|
||||
const markup = html.replace(/<style[\s\S]*?<\/style>/g, '').replace(/<!--[\s\S]*?-->/g, '')
|
||||
const raw = [...markup.matchAll(/#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g)].map((x) => x[0].toUpperCase())
|
||||
const nonToken = [...new Set(raw.filter((h) => h.length === 7 && !tokenHex.has(h)))]
|
||||
const inStyleAttr = [...markup.matchAll(/style="[^"]*#[0-9a-fA-F]{3,6}[^"]*"/g)].length
|
||||
if (nonToken.length) bad('H2', `${tag}: ${nonToken.length} mã màu thô ngoài tập token trong markup`, nonToken)
|
||||
else if (inStyleAttr) warn('H2', `${tag}: ${inStyleAttr} style="" có mã màu — nên dùng var(--ds-*)`)
|
||||
else ok('H2', `${tag}: không có mã màu thô trong markup`)
|
||||
// ids / scr
|
||||
const isHifi = /class="hf-frame"/.test(html)
|
||||
if (isHifi) {
|
||||
attrIds(html).forEach((x) => hifiIds.add(x))
|
||||
;[...html.matchAll(/class="hf-frame"[^>]*\bid="(SCR-\d{2})"/g)].forEach((x) => hifiScr.add(x[1]))
|
||||
// H4 text
|
||||
if (srsText) {
|
||||
const mism = [], unknown = []
|
||||
for (const m2 of html.matchAll(/<(\w+)([^>]*?)\sdata-key="([^"]+)"([^>]*)>/g)) {
|
||||
const [, tagName, pre, key, post] = m2
|
||||
const attrs = pre + post
|
||||
let text
|
||||
if (/^(input|select|textarea)$/i.test(tagName)) text = (attrs.match(/placeholder="([^"]*)"/) || attrs.match(/value="([^"]*)"/) || [])[1]
|
||||
else { const rest = html.slice(m2.index + m2[0].length); const close = rest.indexOf(`</${tagName}>`); text = rest.slice(0, close < 0 ? 0 : close).replace(/<[^>]+>/g, '') }
|
||||
text = norm(text || '').replace(/\s*\*$/, '')
|
||||
if (!(key in srsText)) { unknown.push(key); continue }
|
||||
if (norm(srsText[key]) !== text) mism.push(`${key}: HIFI="${text}" SRS="${srsText[key]}"`)
|
||||
}
|
||||
if (mism.length) bad('H4', `${tag}: ${mism.length} data-key có text khác SRS §4.2 (W5)`, mism); else ok('H4', `${tag}: text data-key khớp SRS §4.2`)
|
||||
if (unknown.length) warn('H4', `${tag}: ${unknown.length} data-key không có trong SRS §4.2`, [...new Set(unknown)])
|
||||
} else if (srs) warn('H4', `${tag}: không đọc được bảng SRS §4.2 (cần cột "Khoá" và "Text (VI)") — bỏ phép kiểm H4`)
|
||||
}
|
||||
}
|
||||
if (srsIds && hifiIds.size) {
|
||||
const onlySrs = [...srsIds].filter((x) => !hifiIds.has(x)), onlyHifi = [...hifiIds].filter((x) => !srsIds.has(x))
|
||||
if (onlySrs.length || onlyHifi.length) bad('H3', 'C-id/F-id lệch hai chiều SRS ↔ HIFI', { chiCoTrongSRS: onlySrs, chiCoTrongHIFI: onlyHifi })
|
||||
else ok('H3', `C-id/F-id khớp hai chiều (${hifiIds.size} ID)`)
|
||||
if (opt.wf) { const wfIds = idsOf(read(opt.wf)); const d = [...hifiIds].filter((x) => !wfIds.has(x)); if (d.length) warn('H3', 'ID có trong HIFI nhưng không có trong WF', d) }
|
||||
}
|
||||
if (srsScr && hifiScr.size) {
|
||||
const miss = [...srsScr].filter((s) => !hifiScr.has(s)), extra = [...hifiScr].filter((s) => !srsScr.has(s))
|
||||
if (miss.length) bad('H5', `HIFI thiếu màn hình của SRS §2.1`, miss); else ok('H5', `HIFI phủ ${hifiScr.size}/${srsScr.size} SCR của SRS §2.1`)
|
||||
if (extra.length) warn('H5', 'HIFI có SCR không có trong SRS §2.1', extra)
|
||||
}
|
||||
|
||||
// ---------- SVG ----------
|
||||
if (opt['svg-dir']) {
|
||||
const dir = opt['svg-dir']
|
||||
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.svg')) : []
|
||||
if (!files.length) bad('S1', `${dir}: không có file .svg`)
|
||||
const views = {}, badColor = {}, badIds = {}
|
||||
for (const f of files) {
|
||||
const s = read(path.join(dir, f))
|
||||
const m = f.match(/^(SCR-\d{2})\.(desktop|tablet|mobile)(?:\.([\w-]+))?\.svg$/)
|
||||
if (!m) { warn('S1', `${f}: tên không theo mẫu SCR-xx.<view>[.<state>].svg`); continue }
|
||||
;(views[m[1]] ||= new Set()).add(m[2])
|
||||
const cols = [...new Set([...s.matchAll(/(?:fill|stroke)="(#[0-9a-fA-F]{6})/g)].map((x) => x[1].toUpperCase()))].filter((h) => !tokenHex.has(h))
|
||||
if (cols.length) badColor[f] = cols
|
||||
if (hifiIds.size) { const ids = [...s.matchAll(/\bid="([CF][0-9]{2})"/g)].map((x) => x[1]).filter((x) => !hifiIds.has(x)); if (ids.length) badIds[f] = ids }
|
||||
}
|
||||
if (Object.keys(badColor).length) bad('S1', 'SVG dùng mã màu ngoài tập token', badColor); else if (files.length) ok('S1', `SVG: ${files.length} file, màu ⊆ token`)
|
||||
if (Object.keys(badIds).length) bad('S1', 'SVG có id Cnn/Fnn không có trong HIFI', badIds)
|
||||
const need = srsScr ? [...srsScr] : Object.keys(views)
|
||||
const lack = need.filter((s) => !(views[s]?.has('desktop') && views[s]?.has('mobile')))
|
||||
if (lack.length) bad('S1', 'SCR chưa đủ desktop + mobile trong SVG', lack); else if (need.length) ok('S1', `SVG phủ desktop+mobile cho ${need.length} SCR`)
|
||||
}
|
||||
|
||||
// ---------- Report ----------
|
||||
if (opt.json) console.log(JSON.stringify({ red, results: out, contrast }, null, 2))
|
||||
else {
|
||||
for (const r of out) { console.log(`${r.status} ${r.id} ${r.msg}`); if (r.details) console.log(' ' + (Array.isArray(r.details) ? r.details.join('\n ') : JSON.stringify(r.details))) }
|
||||
console.log(red ? `\n🔴 ${red} lỗi — chưa đạt` : '\nok — không có lỗi 🔴')
|
||||
}
|
||||
process.exit(red ? 1 : 0)
|
||||
27
.claude/skills/ba-design/scripts/render-figma.sh
Executable file
27
.claude/skills/ba-design/scripts/render-figma.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# render-figma.sh — chụp PNG từng màn hình × khung nhìn × trạng thái của HIFI bằng Chrome/Edge headless sẵn có (không cài thêm).
|
||||
# render-figma.sh <HIFI.html> <out-dir> [--scr SCR-01,SCR-03] [--views desktop,tablet,mobile] [--states default,loading,...]
|
||||
# Mặc định: mọi SCR có trong file · desktop+mobile · mọi trạng thái xuất hiện trong file (data-state). Ảnh: <out-dir>/SCR-xx.<view>.<state>.png
|
||||
set -euo pipefail
|
||||
HIFI="${1:?Thiếu đường dẫn HIFI.html}"; OUT="${2:?Thiếu thư mục output}"; shift 2
|
||||
SCRS=""; VIEWS="desktop,mobile"; STATES=""
|
||||
while [ $# -gt 0 ]; do case "$1" in --scr) SCRS="$2"; shift 2;; --views) VIEWS="$2"; shift 2;; --states) STATES="$2"; shift 2;; *) echo "Tham số lạ: $1" >&2; exit 2;; esac; done
|
||||
BROWSER=""
|
||||
for b in google-chrome google-chrome-stable chromium chromium-browser microsoft-edge microsoft-edge-stable msedge; do command -v "$b" >/dev/null 2>&1 && { BROWSER="$b"; break; }; done
|
||||
for p in "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge" "/c/Program Files/Google/Chrome/Application/chrome.exe" "/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"; do [ -z "$BROWSER" ] && [ -x "$p" ] && BROWSER="$p"; done
|
||||
[ -z "$BROWSER" ] && { echo "blocked: không tìm thấy Chrome/Edge — chụp tay: mở HIFI với ?scr=&view=&state= rồi lưu ảnh vào $OUT" >&2; exit 3; }
|
||||
[ -z "$SCRS" ] && SCRS=$(grep -oE 'class="hf-frame"[^>]*id="SCR-[0-9]+"' "$HIFI" | grep -oE 'SCR-[0-9]+' | paste -sd, -)
|
||||
[ -z "$STATES" ] && STATES=$(grep -oE 'data-state="[a-z-]+"' "$HIFI" | grep -oE '"[a-z-]+"' | tr -d '"' | sort -u | paste -sd, -)
|
||||
mkdir -p "$OUT"
|
||||
ABS=$(cd "$(dirname "$HIFI")" && pwd)/$(basename "$HIFI")
|
||||
n=0
|
||||
for scr in ${SCRS//,/ }; do for view in ${VIEWS//,/ }; do
|
||||
case "$view" in desktop) W=1440; H=900;; tablet) W=834; H=1112;; mobile) W=390; H=844;; *) echo "view lạ: $view" >&2; exit 2;; esac
|
||||
for st in ${STATES//,/ }; do
|
||||
f="$OUT/$scr.$view.$st.png"
|
||||
"$BROWSER" --headless=new --disable-gpu --no-first-run --no-default-browser-check --disable-extensions --hide-scrollbars \
|
||||
--allow-file-access-from-files --virtual-time-budget=3000 --window-size="$W,$H" --screenshot="$f" \
|
||||
"file://$ABS?scr=$scr&view=$view&state=$st&mode=mockup" >/dev/null 2>&1 && n=$((n+1)) || echo "lỗi: $scr $view $st" >&2
|
||||
done
|
||||
done; done
|
||||
echo "ok: $n ảnh → $OUT (khung: desktop 1440×900 · tablet 834×1112 · mobile 390×844; nội dung dài hơn khung bị cắt — ghi chú trong DSPEC §1)"
|
||||
303
.claude/skills/ba-design/templates/components.html
Normal file
303
.claude/skills/ba-design/templates/components.html
Normal 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_<PROJECT> · 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>
|
||||
128
.claude/skills/ba-design/templates/design-spec.md
Normal file
128
.claude/skills/ba-design/templates/design-spec.md
Normal 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`) | | |
|
||||
196
.claude/skills/ba-design/templates/design-system.md
Normal file
196
.claude/skills/ba-design/templates/design-system.md
Normal 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") | |
|
||||
137
.claude/skills/ba-design/templates/ds-components.css
Normal file
137
.claude/skills/ba-design/templates/ds-components.css
Normal 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 */
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
358
.claude/skills/ba-design/templates/hifi.html
Normal file
358
.claude/skills/ba-design/templates/hifi.html
Normal 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 — <US-id> · <Tên US></h1>
|
||||
<label>Màn hình <select id="hf-scr"><option value="">Tất cả</option></select></label>
|
||||
<label>Khung nhìn <select id="hf-view"><option value="desktop">Desktop ≥1024</option><option value="tablet">Tablet 768–1023</option><option value="mobile">Mobile <768</option></select></label>
|
||||
<label>Trạng thái <select id="hf-state">
|
||||
<option value="default">Mặc định (có dữ liệu)</option><option value="loading">Đang tải</option>
|
||||
<option value="empty-none">Rỗng — chưa có dữ liệu nào</option><option value="empty-filter">Rỗng — bộ lọc không khớp</option>
|
||||
<option value="error">Lỗi tải dữ liệu</option><option value="submitting">Đang gửi</option><option value="invalid">Validation lỗi</option>
|
||||
</select></label>
|
||||
<label>Chế độ <select id="hf-mode"><option value="mockup">Mockup</option><option value="proto">Prototype (bấm điều hướng)</option></select></label>
|
||||
<label><input type="checkbox" id="hf-ids"> C-id/F-id</label>
|
||||
<label><input type="checkbox" id="hf-zones"> Vùng Zn</label>
|
||||
<span class="hf-legend">Hành vi xem SRS · bố cục xem WF · token xem DS · URL: ?scr=SCR-01&view=mobile&state=empty-none&ids=1</span>
|
||||
</header>
|
||||
|
||||
<main class="hf-stage">
|
||||
|
||||
<!-- ============ SCR-01 (mẫu: danh sách) — mỗi SCR một .hf-frame, id = SCR-id, data-c/data-f/data-key khớp SRS, data-zone khớp WF ============ -->
|
||||
<section class="hf-frame" id="SCR-01" data-scr="SCR-01" data-title="Danh sách cửa hàng">
|
||||
<div class="hf-cap">SCR-01 · Danh sách cửa hàng <small>WF §3.1 · vai trò ROLE-01, ROLE-02 · vào từ Menu</small></div>
|
||||
<header class="ds-header" data-zone="Z0" data-name="Header toàn site">
|
||||
<svg class="ds-icon" viewBox="0 0 24 24" aria-label="menu"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
|
||||
<b>Tên hệ thống</b>
|
||||
<nav class="ds-breadcrumb ds-hide-m"><a data-key="nav.home">Trang chủ</a><span>/</span><span data-key="scr01.title">Danh sách cửa hàng</span></nav>
|
||||
<span style="margin-left:auto" class="ds-row"><svg class="ds-icon" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg></span>
|
||||
</header>
|
||||
<div class="ds-page">
|
||||
<div class="ds-row ds-row--between" data-zone="Z1" data-name="Header màn hình">
|
||||
<h1 data-c="C01" data-key="scr01.title">Danh sách cửa hàng</h1>
|
||||
<button class="ds-btn ds-btn--primary" data-c="C02" data-key="btn.create" data-nav="SCR-03"><svg class="ds-icon" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>Tạo mới</button>
|
||||
</div>
|
||||
<div class="ds-row" data-zone="Z2" data-name="Thanh lọc">
|
||||
<div class="ds-input-wrap" style="width:320px;max-width:100%"><svg class="ds-icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/></svg><input class="ds-input" data-c="C03" data-key="scr01.search.ph" placeholder="Nhập mã hoặc tên…"></div>
|
||||
<select class="ds-select ds-hide-m" style="width:200px" data-c="C04" data-key="scr01.filter.status"><option>Trạng thái: Tất cả</option></select>
|
||||
<button class="ds-btn ds-btn--secondary ds-hide-d" data-key="btn.filter"><svg class="ds-icon" viewBox="0 0 24 24"><path d="M3 5h18l-7 8v6l-4-2v-4z"/></svg>Lọc</button>
|
||||
</div>
|
||||
<div data-zone="Z3" data-name="Dữ liệu">
|
||||
<div data-state="default">
|
||||
<div class="ds-table-wrap"><table class="ds-table" data-c="C05">
|
||||
<thead><tr><th aria-sort="ascending">Mã ▲</th><th>Tên</th><th>Trạng thái</th><th>Ngày tạo</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>STR-001</td><td>Cửa hàng A</td><td><span class="ds-badge">Nháp</span></td><td>30/08/2026</td><td><a class="ds-link" data-nav="SCR-02" data-key="btn.detail">Chi tiết</a></td></tr>
|
||||
<tr><td>STR-002</td><td>Cửa hàng B</td><td><span class="ds-badge ds-badge--success">Đã duyệt</span></td><td>29/08/2026</td><td><a class="ds-link" data-nav="SCR-02" data-key="btn.detail">Chi tiết</a></td></tr>
|
||||
</tbody></table></div>
|
||||
<div class="ds-cards" data-c="C05">
|
||||
<div class="ds-card" data-nav="SCR-02"><div class="ds-row ds-row--between"><b>STR-001</b><span class="ds-badge">Nháp</span></div><div>Cửa hàng A</div><div class="ds-small ds-muted">30/08/2026</div></div>
|
||||
<div class="ds-card" data-nav="SCR-02"><div class="ds-row ds-row--between"><b>STR-002</b><span class="ds-badge ds-badge--success">Đã duyệt</span></div><div>Cửa hàng B</div><div class="ds-small ds-muted">29/08/2026</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-state="loading"><div class="ds-card"><div class="ds-skeleton"></div><div class="ds-skeleton" style="width:85%"></div><div class="ds-skeleton" style="width:92%"></div><div class="ds-skeleton" style="width:70%"></div></div></div>
|
||||
<div data-state="empty-none"><div class="ds-card"><div class="ds-empty"><svg class="ds-empty__illus" viewBox="0 0 120 88" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="16" y="20" width="88" height="56" rx="6"/><path d="M16 36h88M32 52h28M32 62h44"/></svg><span data-key="empty.none">Chưa có cửa hàng nào. Tạo cửa hàng đầu tiên?</span><button class="ds-btn ds-btn--primary" data-key="btn.create" data-nav="SCR-03">Tạo mới</button></div></div></div>
|
||||
<div data-state="empty-filter"><div class="ds-card"><div class="ds-empty ds-empty--filter"><span data-key="empty.filter">Không tìm thấy kết quả phù hợp với bộ lọc.</span><button class="ds-btn ds-btn--secondary" data-key="btn.clearFilter">Xoá lọc</button></div></div></div>
|
||||
<div data-state="error"><div class="ds-banner ds-banner--error"><svg class="ds-icon" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 8v5m0 3h.01"/></svg><span data-key="error.load">Không tải được dữ liệu.</span><button class="ds-btn ds-btn--secondary" style="margin-left:auto;height:32px" data-key="btn.retry">Thử lại</button></div></div>
|
||||
</div>
|
||||
<div class="ds-row ds-row--between" data-zone="Z4" data-name="Phân trang">
|
||||
<span class="hf-proto-only" data-ref="WF §5 #1"><button class="ds-btn ds-btn--secondary">Xuất Excel</button></span>
|
||||
<div class="ds-pagination" data-c="C06"><button class="ds-btn ds-btn--secondary ds-btn--icon" aria-label="Trang trước">‹</button><span>1 / 7</span><button class="ds-btn ds-btn--secondary ds-btn--icon" aria-label="Trang sau">›</button><select class="ds-select ds-hide-m" style="width:110px;height:32px"><option>20 dòng</option></select></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ============ SCR-03 (mẫu: form trong modal) ============ -->
|
||||
<section class="hf-frame" id="SCR-03" data-scr="SCR-03" data-title="Tạo cửa hàng">
|
||||
<div class="hf-cap">SCR-03 · Tạo cửa hàng <small>Modal · mở từ C02 · WF §3.3</small></div>
|
||||
<div style="min-height:560px;position:relative">
|
||||
<div class="ds-modal-backdrop" style="position:absolute">
|
||||
<div class="ds-modal" role="dialog" aria-labelledby="scr03-title">
|
||||
<div class="ds-modal__head" data-zone="Z1" data-name="Tiêu đề modal"><h3 id="scr03-title" data-c="C01" data-key="scr03.title">Tạo cửa hàng</h3><button class="ds-btn ds-btn--ghost ds-btn--icon" data-c="C08" data-key="btn.close" data-nav="SCR-01" aria-label="Đóng"><svg class="ds-icon" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
|
||||
<div class="ds-modal__body" data-zone="Z2" data-name="Form">
|
||||
<label class="ds-field"><span class="ds-field__label" data-key="f01.label">Mã <b class="ds-req">*</b></span>
|
||||
<span data-state="default"><input class="ds-input" data-f="F01" data-key="f01.ph" placeholder="3–20 ký tự, A–Z 0–9 -"><span class="ds-field__hint" data-key="f01.hint">Không trùng với mã đã có</span></span>
|
||||
<span data-state="invalid"><input class="ds-input is-invalid" data-f="F01" value="ab"><span class="ds-field__error" data-key="E-XXX-0001">Mã phải có từ 3 đến 20 ký tự.</span></span>
|
||||
</label>
|
||||
<label class="ds-field"><span class="ds-field__label" data-key="f02.label">Loại <b class="ds-req">*</b></span><select class="ds-select" data-f="F02"><option>Thường</option></select></label>
|
||||
<label class="ds-field"><span class="ds-field__label" data-key="f03.label">Tên</span><input class="ds-input" data-f="F03" data-key="f03.ph" placeholder="Tên hiển thị"></label>
|
||||
</div>
|
||||
<div class="ds-modal__foot" data-zone="Z3" data-name="Hành động">
|
||||
<button class="ds-btn ds-btn--secondary" data-c="C07" data-key="btn.cancel" data-nav="SCR-01">Huỷ</button>
|
||||
<span data-state="default" style="display:inline"><button class="ds-btn ds-btn--primary" data-c="C09" data-key="btn.save">Lưu</button></span>
|
||||
<span data-state="submitting"><button class="ds-btn ds-btn--primary is-loading" data-c="C09" aria-disabled="true"><span class="ds-spinner"></span>Đang lưu…</button></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
<p class="hf-note">Mockup hi-fi · token và component theo DS_<PROJECT> · hành vi theo SRS_<US-id> · bố cục theo WF_<US-id> · khung đứt đỏ = chỉ có ở prototype, chưa có trong SRS (WF §5)</p>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
var q=new URLSearchParams(location.search), b=document.body;
|
||||
var scr=document.getElementById('hf-scr'),view=document.getElementById('hf-view'),state=document.getElementById('hf-state'),mode=document.getElementById('hf-mode'),ids=document.getElementById('hf-ids'),zones=document.getElementById('hf-zones');
|
||||
document.querySelectorAll('.hf-frame').forEach(function(f){var o=document.createElement('option');o.value=f.id;o.textContent=f.id+' · '+(f.dataset.title||'');scr.appendChild(o);});
|
||||
if(q.get('scr'))scr.value=q.get('scr');if(q.get('view'))view.value=q.get('view');if(q.get('state'))state.value=q.get('state');if(q.get('mode'))mode.value=q.get('mode');ids.checked=q.get('ids')==='1';zones.checked=q.get('zones')==='1';
|
||||
function apply(){
|
||||
b.setAttribute('data-view',view.value);b.setAttribute('data-mode',mode.value);
|
||||
b.classList.toggle('hf-ids',ids.checked);b.classList.toggle('hf-zones',zones.checked);
|
||||
if(scr.value){b.setAttribute('data-only','1');}else{b.removeAttribute('data-only');}
|
||||
document.querySelectorAll('.hf-frame').forEach(function(f){f.setAttribute('data-active',String(!scr.value||f.id===scr.value));});
|
||||
var s=state.value;
|
||||
document.querySelectorAll('[data-state]').forEach(function(el){
|
||||
var st=el.getAttribute('data-state'),on;
|
||||
if(s==='submitting')on=st==='submitting'||(st==='default'&&!el.closest('.ds-modal__foot'));
|
||||
else if(s==='invalid')on=st==='invalid'||(st==='default'&&!el.closest('.ds-field'));
|
||||
else on=st===s||(st==='default'&&s!=='default'&&!el.parentElement.querySelector('[data-state="'+s+'"]'));
|
||||
el.style.display=on?(el.tagName==='SPAN'?'inline':'block'):'none';
|
||||
});
|
||||
}
|
||||
[scr,view,state,mode,ids,zones].forEach(function(el){el.addEventListener('change',apply);});
|
||||
document.addEventListener('click',function(e){
|
||||
if(b.getAttribute('data-mode')!=='proto')return;
|
||||
var t=e.target.closest('[data-nav]');if(!t)return;e.preventDefault();
|
||||
var to=document.getElementById(t.getAttribute('data-nav'));if(!to)return;
|
||||
scr.value=to.id;state.value='default';apply();to.scrollIntoView();
|
||||
});
|
||||
apply();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
54
.claude/skills/ba-design/templates/tokens.css
Normal file
54
.claude/skills/ba-design/templates/tokens.css
Normal 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;
|
||||
}
|
||||
105
.claude/skills/ba-design/templates/tokens.json
Normal file
105
.claude/skills/ba-design/templates/tokens.json
Normal 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)" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user