---
url: https://docs.nemo12.com/design-system/guided-journey/anatomy.md
description: >-
  Khi nào dùng mẫu Guided Journey, giải phẫu từng phần, luật nội dung, tương tác
  và a11y, hiện thực, ví dụ, nên và không nên.
---

# Guided Journey · Khi nào dùng, giải phẫu và luật nội dung

Một phần của [Guided Journey pattern (SRC-1253)](./index.md). Khi nào dùng mẫu Guided Journey, giải phẫu từng phần, luật nội dung, tương tác và a11y, hiện thực, ví dụ, nên và không nên.

## 1. Khi nào dùng

**Dùng khi** learner cần tự trả lời một câu hỏi lớn ("Mình có kịp không?", "Mình nên bắt đầu từ
đâu?") và câu trả lời ghép được từ 3 đến 9 câu nhỏ, mỗi câu một quyết định.

Nhịp của mẫu là **Show, Understand, Explore**: cho thấy câu hỏi, giúp hiểu câu trả lời, rồi mở
đường đi tiếp. Đây là **một công cụ để learner tự khám phá mình, không phải một trang nói về
Nemo**. Chữ trên trang nói về learner; Nemo chỉ xuất hiện như người tính hộ.

**Không dùng khi:**

* nội dung là thông tin tra cứu một chiều (bảng giá, lịch, điều khoản): dùng trang thường;
* chỉ có một câu hỏi: dùng một form hoặc một card;
* các bước không phụ thuộc nhau và learner nên thấy tất cả cùng lúc: dùng lưới card;
* trình tự dài hơn khoảng 9 bước: tách thành nhiều journey (như hub learn chia 6 journey thành 3 tầng);
* là luồng làm bài thi có tính giờ: dùng khung học §6b của DS-001.

## 2. Giải phẫu

Trên learn (mọi trang AI Teen và mọi trang `learn.nemo12.com/ielts`), "site nav" và thanh (b) là
**JourneyShell** của §9 (SRC-1263): NavBar mang các JOURNEY, thanh tiến độ ngay dưới nêu TÊN từng
bước của journey đang đứng (mỗi bước một pill số + tên, SRC-1272), và ở learn `/ielts` thêm Status
Card 3-5 dòng ở góc trên phải vùng nội dung (AI Teen không có, SRC-1272). Sơ đồ dưới là một trang
câu hỏi trên www; thanh tròn-và-mũi-tên dưới đây là bản www.

```text
+---------------------------------------------------------------+
|  site nav  (learn: NavBar = journeys, xem §9)                  |
+---------------------------------------------------------------+
|  (b) STICKY PROGRESS  (1)-->(2)-->(3)-->(4)..>(5)..>(6)        |
|       filled = done, ring = current, grey = later               |
+---------------------------------------------------------------+
|  (a) HERO                                                      |
|      DISCOVER YOUR IELTS PATH            <- label (small caps)  |
|      Mình cần gì để đạt IELTS?           <- h1 = a question     |
|                                                                |
|  (c) NUMBERED QUESTION LIST                                    |
|  (1) +-----------------------------------------------+         |
|   |  | Mình cần IELTS để làm gì?      v   [ Sửa ]    |  view    |
|   |  | Xét tuyển đại học Việt Nam  <- own answer    |          |
|   |  |-----------------------------------------------| divider  |
|   |  |   (Xét tuyển...) (Ứng tuyển du học) ...       |          |
|   |  +-----------------------------------------------+         |
|  (2) +-----------------------------------------------+         |
|   |  | Mình cần band bao nhiêu?                      |  current |
|   |  | Chọn một con số, Nemo dùng nó...  <- hint     |          |
|   |  |-----------------------------------------------|          |
|   |  |   [-]   6.5   [+]          [Xác nhận]         | (d)      |
|   |  |   +---------------- reveal ---------------+   |          |
|   |  |   | computed answer + source link         |   |          |
|   |  +-----------------------------------------------+         |
|  (3) +-----------------------------------------------+         |
|      | Mình đang ở band mấy?   (dim, locked)         |  later   |
|      +-----------------------------------------------+         |
|                                                                |
|  (g) AHA PORTRAIT                                              |
|      +===============================================+         |
|      | Lộ trình IELTS của bạn  . YOUR IELTS PATH     |         |
|      | Mục tiêu  Band 6.5   |  Hiện tại  Band 5.0    |         |
|      | Khoảng cách 1.5 band |  Thời gian ...         |         |
|      | [ ONE primary CTA ]                            |         |
|      +===============================================+         |
+---------------------------------------------------------------+
```

### (a) Hero

Hai dòng, căn giữa: một **nhãn** nhỏ chữ hoa (tên tiếng Anh của journey, ví dụ
`Discover Your IELTS Path`) và một **h1 là một câu hỏi** của learner. Không dòng giới thiệu (chủ dự
án 06.10.2026, SRC-1260: các câu hỏi tự nói learner sẽ làm gì), không ảnh hero.

### (b) Thanh tiến độ dính (sticky progress)

* Các nút tròn đánh số `1..N`, màu `accent` (cam): **đặc** = đã xong (`bg-accent text-white`),
  **viền** = đang làm (`border-2 border-accent text-accent`), **xám** = chưa tới
  (`border border-border text-muted-foreground`).
* Giữa hai nút là icon `ArrowRight` (lucide); mũi tên chuyển sang `text-accent` khi bước **trước nó** đã xong.
* Dính ngay dưới thanh nav của site: `position: sticky`, `top` đo bằng chiều cao của
  `header.n12-nav-deep` khi trang đã chạy, đo lại khi resize; nền `bg-background/95 backdrop-blur`
  và một đường `border-b`.
* Ở 375px chỉ hiện **nhãn của card đang mở** (khi trang truyền `activeId`; không thì của bước hiện
  tại), các bước khác chỉ còn vòng số; chữ tối thiểu 12px.
* **Nhấn card đang mở** (`activeId`, SRC-1266): nút của card đang mở có vòng `ring-2 ring-accent`,
  đậm đủ; mọi nút khác mờ `opacity-50` (nút đã xong vẫn đặc, chỉ mờ đi), `aria-label` của nút đang mở
  thêm ", đang mở". Done/current vẫn do câu trả lời quyết định; cái được nhấn là card đang mở.
* Bấm một nút: card ấy thành card đang mở (nếu bước đã mở được) rồi cuộn tới nó. Nút không nhảy
  anchor gốc của trình duyệt (`preventDefault`, cập nhật `#step-id` bằng `history.replaceState`).
* **Bù thanh dính khi cuộn** (SRC-1266): `scrollToCard(id)` đo lúc chạy chiều cao `header.n12-nav-deep`
  * thanh tiến độ (`[data-journey-bar]`) (`stickyOffset`), rồi cuộn tới `cardScrollTop(top, scrollY,
    offset)` = mép trên card nằm **12px dưới đáy thanh**, ở 1280 lẫn 375. Gọi sau ~60ms để card cũ đã
    gập xong. Mở trang bằng `/ielts#target` hay `/ielts/consider#price` cũng đi đường này (cuộn tức thì).
    `scroll-mt-36` trên card là phương án dự phòng khi chưa có JS. Mỗi nút là một liên kết `#step-id`
    có `aria-current="step"` cho bước hiện tại và `aria-label` đầy đủ ("2. Mục tiêu · TARGET, đã
    xong"), để trạng thái không chỉ nói bằng màu. Cả thanh là `nav aria-label="Tiến độ"`.
* Nguồn: component dùng chung `JourneyProgressBar` (+ `ProgressNodes`) ở
  `apps/web/src/site/JourneyProgressBar.tsx` (SRC-1256), nhận `steps` (id, nhãn ngắn), `markOf`
  (done/current/later) và `onNode`; trang gọi tự định nghĩa done/current. Dùng ở **hai trang**:
  `/ielts` (done = đã trả lời, PR #455, gỡ nhầm ở #456, khôi phục ở #464) và `/ielts/consider`
  (done = learner đã tự mở thẻ ít nhất một lần, lưu trong `sessionStorage`; current = thẻ đầu tiên
  chưa mở; nhãn 1-2 chữ như "Lên điểm", "Học phí"). Đừng bỏ thanh này với lý do "danh sách số đã
  báo vị trí": chủ dự án giữ nó có chủ ý.

### (c) Danh sách câu hỏi đánh số

Component `NumberedQuestion` (`apps/web/src/site/NumberedQuestionList.tsx`):

* vòng số `size-9` bên trái, đường nối dọc mảnh (`w-px bg-border`) xuống mục kế, trừ mục cuối;
* một card bo góc (`rounded-card border border-border bg-card`) có header là **nút disclosure**:
  **tiêu đề** là câu hỏi bằng giọng nội tâm của learner, dưới nó **đúng một dòng tóm tắt**, bên
  phải là chevron xoay 180 độ khi mở;
* dòng tóm tắt là **gợi ý trước khi trả lời** (Nemo sẽ cho bạn biết gì) và là **câu trả lời / kết
  quả của chính learner sau khi trả lời** (`stepSummary`);
* **một divider** (`border-t border-border`, `data-part="card-divider"`) tách hai dòng đầu (tiêu đề +
  tóm tắt) khỏi phần còn lại của card khi card mở (SRC-1260; `/ielts/consider` cũng có);
* **đúng MỘT card đang mở** (SRC-1266): card đang mở (do bấm card, bấm nút trên thanh, hoặc đi tiếp
  sau khi trả lời) mở và đậm đủ; **mọi card khác gập lại và mờ** (`dim`, `opacity-50`). Mở card khác
  thì card ấy thành card đang mở. Bấm vào chính card đang mở thì gập nó, lúc ấy không card nào mờ.
  Card chưa tới bị mờ và khoá (`disabled`). Chế độ sửa chỉ có ở card đang mở: mở card khác là "Huỷ";
* **ô Sửa cố định** (`actions`, `data-part="card-actions"`): góc trên bên phải của header, ngang hàng
  tiêu đề, ngay sau chevron và **nằm ngoài** nút disclosure (không lồng nút trong nút). Ô có bề rộng
  giữ sẵn (`w-[7.75rem]`) đủ cho "Lưu" + "Huỷ", nên đổi "Sửa" thành "Lưu" + "Huỷ" không xê dịch chữ;
* **vòng số theo card đang mở** (`focus`): vòng số của card đang mở sáng lên (`ring-2 ring-accent`),
  các vòng số khác mờ (`opacity-50`). Không truyền `focus` thì vòng số giữ nguyên. Luật này **thay
  luật scroll focus của #474** (SRC-1260): cuộn trang một mình không còn đổi chỗ nhấn; card đang mở
  quyết định cả vòng số bên trái lẫn nút trên thanh.

#### Chế độ của một card trả lời (SRC-1260)

| Chế độ | Khi nào | Input | Nút |
| --- | --- | --- | --- |
| `locked` | bước trước chưa xong | không hiện | không |
| `answer` | card đang mở, chưa trả lời | bật, chọn vào **bản nháp** | "Xác nhận" cuối card (tắt khi chưa đủ) |
| `closed` | không phải card đang mở (mờ) | không hiện | chevron |
| `view` | card đang mở, đã trả lời | hiện giá trị đã chọn nhưng **tắt** (chỉ xem) + Reveal | "Sửa" trong ô cố định |
| `edit` | sau khi bấm "Sửa" | bật, chọn vào bản nháp | "Lưu" + "Huỷ" đúng chỗ "Sửa" |

* **Chọn chưa phải lưu**: mọi input (kể cả chip ở câu 1) chỉ đổi bản nháp; "Xác nhận" / "Lưu" mới ghi
  vào visitor state và tính lại các bước sau. "Huỷ" bỏ bản nháp, giá trị đã lưu hiện lại. Gập card
  đang sửa cũng là "Huỷ". Mỗi lúc chỉ một card ở chế độ `edit`.
* **Bước tính toán không có input** (§f) không có ô Sửa; nó có đúng một nút "Đã hiểu" để gập lại và
  cuộn tới bước kế. Bước có input phụ (ví dụ số giờ mỗi tuần ở `timeline`) theo bảng trên như mọi bước.

### (d) Hé lộ dần: Answer, Reveal, Answer

Mỗi bước: learner chọn, ngay dưới đó hiện một khối **Reveal** (`bg-muted/60`, `rounded-2xl`, motion
`opacity 0 -> 1, y 8 -> 0`) chứa điều Nemo vừa tính ra từ câu trả lời ấy. Sau đó card đóng lại, bước kế
mở ra và được cuộn vào tầm nhìn (`block: "nearest"`, cuộn mượt trừ khi reduced-motion).

### (e) Trí nhớ giữa các bước

* Bước sau **nhắc lại** câu trả lời trước (khoảng cách tính từ band mục tiêu và band hiện tại), **không bao giờ hỏi lại**.
* Câu trả lời lưu trong visitor state (www: sổ `n12.visitor.v2`, entry `ielts`, field `discover`, bản
  bố mẹ field `parentDiscover`, §2h;
  luật ở [visitor-state](../../product/visitor-state/ielts-discover.md)), được kiểm lại mỗi lần đọc. Quay lại trang là
  thấy đúng chỗ mình đã dừng.
* Sửa một câu trước thì mọi bước phía sau **tính lại** từ cùng một hàm thuần; không có số nào lưu cứng.

### (f) Bước tính toán không cần nhập

Một số bước chỉ có Reveal (ví dụ `feasibility`: "Mình có kịp không?"). Bước ấy mở cùng lúc với bước
cung cấp dữ liệu cuối cùng cho nó và được tính là xong khi đủ dữ liệu.

### (g) Kết thúc "Aha"

Khi mọi bước xong, hiện một **card chân dung** (`Card` viền `border-2 border-accent`): tiêu đề "của
bạn", một lưới `dl` gom mọi câu trả lời và kết quả, và **một CTA chính duy nhất**
(`buttonVariants({ variant: "accent" })`). Thêm một dòng nhỏ nói dữ liệu lưu ở đâu và cách sửa.

### (h) Tham số giọng: một journey, nhiều người hỏi (SRC-1273)

Khi cùng một chuỗi câu hỏi có hai người đọc (learner tự hỏi, bố mẹ hỏi về con), **không chép
component**. Component journey nhận một prop `persona` và mọi câu khác nhau theo người đọc nằm trong
**một bảng giọng**; phần còn lại dùng chung tuyệt đối.

| Đổi theo giọng (bảng giọng) | Dùng chung (không bao giờ tách) |
| --- | --- |
| nhãn, h1 (câu hỏi trang của đường dẫn ấy), câu hỏi và gợi ý từng bước | danh sách bước và thứ tự |
| chữ trong Reveal ("bạn" / "con", "Bạn tự chấm" / "Bố mẹ tự đánh giá") | thanh tiến độ, một card đang mở, chế độ xem/sửa, ô Sửa |
| tiêu đề chân dung và CTA chính duy nhất (nhãn + đích) | bù thanh dính khi cuộn, anchor, prerender |
| | model thuần: kiểm câu trả lời, migration, mọi phép tính và nguồn |

* **Giọng mặc định là bản đang chạy**, và bản ấy phải render y hệt từng byte sau khi thêm tham số
  (test so với bản trước, hoặc snapshot).
* **Mỗi giọng lưu riêng.** Câu trả lời lưu theo persona (www: cùng mục visitor state, mỗi persona một
  trường), để hai người dùng chung một máy không ghi đè nhau. Bố mẹ ước của con và con tự chấm là
  hai câu trả lời khác nhau, không phải một.
* **Tự đánh giá ghi đúng người đánh giá**: "bạn tự chấm" cho learner, "Bố mẹ tự đánh giá" cho bố mẹ.
* Dữ liệu từ tài khoản (learn) nói về **tài khoản trên máy này**, không gán cho "con" hay "bạn" khi
  không biết ai đang đăng nhập.
* Hiện thực: `IeltsDiscover` (`persona`, bảng `VOICE`), `PARENT_STEP_COPY` + `stepCopy(persona)` trong
  `ieltsDiscoverModel.ts`; `App.tsx` gắn `key` theo persona để đi qua lại giữa hai trang không mang
  trạng thái giao diện sang. Luật lưu: [visitor-state](../../product/visitor-state/ielts-discover.md).

## 3. Luật nội dung

* **Giọng nội tâm của learner** (học sinh 16-17 tuổi): "Mình cần band bao nhiêu?", không phải "Khám
  phá lộ trình đột phá cùng Nemo". Không ngôn ngữ marketing. Trang cho bố mẹ thì là **giọng bố mẹ hỏi
  về con** ("Con cần band bao nhiêu?"), cùng luật không marketing (§2h).
* **Một quyết định mỗi card.** Hai câu hỏi thì hai card.
* **Tóm tắt một dòng.** Dài hơn một dòng là dấu hiệu card đang chứa hai quyết định.
* **Trạng thái rỗng thật thà.** Thiếu dữ liệu thì nói thiếu và chỉ đường lấy nó ("Chờ bài chẩn
  đoán"); **không bao giờ bịa số**, không đánh dấu "xong" một bước thiếu dữ liệu.
* **Số tính ra phải có nguồn**, kèm liên kết (Cambridge PDF, nhịp học NEMO), và một câu nói rõ đó là
  mức tham chiếu, không phải lời hứa. Tự chấm của learner luôn ghi là "bạn tự chấm", không phải phép đo.
* **Ngôn ngữ theo bề mặt** ([bảng ngôn ngữ trong CLAUDE.md](https://github.com/dac2205/nemo12.com/blob/main/CLAUDE.md)):
  www nói tiếng Việt, tên tiếng Anh chỉ là nhãn đứng sau (SRC-829); learn `/ielts/**` có giao diện
  tiếng Anh, chữ hướng dẫn của Nemo bằng tiếng Việt (SRC-834, SRC-953).
* **Không em dash; ngày luôn DD.MM.YYYY** (hàm `ddmmyyyy` trong `apps/learn/src/ielts/journeys/model.ts`).

## 4. Tương tác và a11y

* Lựa chọn là **radio group** (`RadioChips`: `role="radiogroup"`, `role="radio"`, `aria-checked`,
  roving `tabIndex`); phím mũi tên di chuyển và chọn, Tab rời nhóm. Số band dùng nút `-`/`+` và
  `output aria-live="polite"`.
* Header card là `button` có `aria-expanded` + `aria-controls`, nên Tab/Enter/Space chạy không cần code thêm.
* Thứ tự Tab trong một card: header, ô Sửa ("Sửa", hoặc "Huỷ" rồi "Lưu"), rồi input và nút cuối card.
  Bấm "Sửa" thì focus nhảy vào input đầu tiên đang bật; "Lưu" / "Huỷ" đưa focus về "Sửa". Nút trong
  ô Sửa và nút cuối card cao tối thiểu 44px (`min-h-11`, `min-h-12`).
* Không còn listener cuộn: chỗ nhấn (vòng số, nút trên thanh) đi theo card đang mở, không theo vị
  trí cuộn (SRC-1266 thay luật scroll focus của SRC-1260). Chuyển opacity có `motion-reduce:transition-none`.
* Motion qua preset `m`/`Appear` của `Motion.tsx`; `MotionConfig reducedMotion="user"` tự tắt chuyển
  động, cuộn trang cũng kiểm `prefers-reduced-motion`.
* **Mọi câu hỏi được prerender cho SEO**: bản HTML tĩnh chứa hero và tiêu đề của mọi bước (card đóng
  vẫn mang câu hỏi), bước 1 mở, vì lúc prerender visitor state là `null`.
* **Mỗi bước một anchor** (`li id="target"`, `scroll-mt-24`); `/ielts#target` cuộn tới card ấy sau khi hydrate.
* Vùng chạm tối thiểu 44px (`min-h-11`, `min-h-12`, `size-12`); thiết kế cho **375px trước**, rồi mới
  `sm:` trở lên (DS-001 §4c).

## 5. Hiện thực

**Stack**: `motion` + shadcn/ui + Tailwind v4 (DS-001 §0); không thêm thư viện.

| File | Dùng lại gì |
| --- | --- |
| `apps/web/src/site/NumberedQuestionList.tsx` | `NumberedQuestion`: vòng số, đường nối, card disclosure |
| `apps/web/src/site/IeltsDiscover.tsx` | `RadioChips`, `BandPicker`, `Reveal`, `StepCard`, `Portrait`, luồng `advance`, prop `persona` + bảng `VOICE` (§2h) |
| `apps/web/src/site/ieltsDiscoverModel.ts` | khuôn model thuần: `STEPS`, `STEP_COPY`, `currentStep`, `stepDone`, `stepOpen`, `stepSummary` |
| `apps/web/src/site/IeltsLearnerJourney.tsx` + `ieltsLearnerJourneyData.ts` | biến thể chỉ đọc (`/ielts/consider`): 9 card, chữ ở file data, khối dữ liệu sống từ API |
| `apps/learn/src/ielts/journeys/NumberedStep.tsx` | bản learn của cùng hình (token theme sâu của learn): link, disclosure, hoặc `active` mang CTA |
| `apps/learn/src/ielts/journeys/model.ts` + `JourneyPages.tsx` | hub 6 journey / 3 tầng (`TIERS`, `JOURNEYS`), trạng thái từ dữ liệu API có sẵn |

**Token / class**: `accent` (cam, trạng thái xong và CTA), `border-border`, `bg-card`, `rounded-card`,
`text-ocean-900` (tiêu đề), `text-muted-foreground` (tóm tắt, chưa tới), `bg-muted/60` (Reveal),
`focus-visible:outline-accent`.

**Thêm một journey mới:**

1. File data: danh sách bước + `STEP_COPY` (câu hỏi giọng learner, gợi ý một dòng, nhãn EN).
2. Model thuần (không React, không fetch, không đồng hồ): đọc/kiểm câu trả lời, `currentStep`,
   `stepDone`, `stepSummary`, các phép tính có nguồn ghi ngay trong comment đầu file.
3. Component trang: hero (a) + thanh tiến độ (b) + một `NumberedQuestion` mỗi bước (c) + `Portrait` (g).
4. Lưu câu trả lời vào visitor state (www) hoặc API của learner (learn), có version.
5. Test, chép từ:
   * `apps/web/src/site/ieltsDiscover.test.tsx` (thứ tự bước, nhắc lại câu trả lời, sửa thì tính lại, prerender đủ câu hỏi);
   * `apps/web/src/site/IeltsLearnerJourney.test.tsx` (card disclosure, dữ liệu sống hỏng vẫn còn chữ tĩnh);
   * `apps/learn/src/ielts/journeys/model.test.ts` (thiếu dữ liệu thì không "xong", không bịa số).

## 6. Ví dụ đang chạy

* [nemo12.com/ielts](https://nemo12.com/ielts): Discover Your IELTS Path, 6 bước có nhập + chân dung.
* [nemo12.com/parents/ielts](https://nemo12.com/parents/ielts): cùng màn ấy, giọng bố mẹ (§2h), CTA "Cho con học thử miễn phí".
* [nemo12.com/ielts/consider](https://nemo12.com/ielts/consider): 9 câu hỏi chỉ đọc, "Học ở NEMO thì sao?".
* [learn.nemo12.com/ielts](https://learn.nemo12.com/ielts): hub 6 journey trong 3 tầng (sau đăng nhập).

## 7. Nên và không nên

| Nên | Không nên |
| --- | --- |
| h1 là một câu hỏi của learner | h1 là tên sản phẩm |
| một quyết định, một dòng tóm tắt mỗi card | gộp hai câu hỏi vào một card |
| hiện câu trả lời của learner trên card đã xong | hỏi lại điều đã biết |
| nói thiếu dữ liệu và chỉ cách lấy | điền số mẫu hay số đoán |
| ghi nguồn cho mọi con số tính ra | đưa ước tính như lời hứa |
| một CTA chính ở chân dung | nhiều nút cùng độ nổi |
| thử ở 375px trước | để thanh tiến độ tràn ngang ở điện thoại |
