Design System & UI
Mọi website thuộc conan.school và mọi sub-domain dùng chung một bộ ba. Đây là luật, không phải gợi ý, mỗi site tự chọn thư viện riêng là cách chắc chắn nhất để nền tảng trông như năm sản phẩm của năm công ty.
Bộ ba
| Lớp | Bắt buộc | Cấm |
|---|---|---|
| Style engine | Tailwind CSS v4 | CSS-in-JS, styled-components, SASS cho việc Tailwind làm được |
| Component nền | shadcn/ui | MUI, Ant Design, Chakra, Mantine, Bootstrap |
| Chuyển động | Framer Motion (gói npm motion) | GSAP, react-spring, keyframes CSS tay |
Áp cho: www, tokyo (com.conan.school/), admin và cả cụm legacy/admin-*, mam, mentors, video, play, docs.
Luôn dùng bản mới nhất
Thêm site mới hay chạm vào package.json của site cũ thì cài bản mới nhất:
npm i motion tailwindcss @tailwindcss/vite class-variance-authority tailwind-merge clsx lucide-reactGhim một bản cũ chỉ khi có lý do thật, và lý do phải được ghi ngay cạnh dòng đó.
Framer Motion phát hành dưới tên motion
Từ v12, Framer Motion đổi tên gói. Import đúng:
import { motion, AnimatePresence } from "motion/react"Site nào còn framer-motion (hiện là mam.conan.school) thì chuyển sang motion khi có dịp chạm vào.
shadcn/ui
npx shadcn@latest add button dialog tabsComponent sinh ra nằm trong src/components/ui/ và là mã của mình, được sửa, nhưng sửa để hợp token dùng chung, không hardcode màu.
Token vẫn là một nguồn duy nhất
shadcn và Motion không thay packages/design-system/theme.css. Site chỉ import:
@import "tailwindcss";
@import "../../packages/design-system/theme.css";
@import "../../packages/design-system/base.css"; /* tuỳ chọn */Không site nào khai @theme riêng, không site nào override token. Đổi --color-primary một dòng là cả nền tảng đổi theo, đó là lý do hệ này tồn tại.
Xem thêm: Quy tắc triển khai, skill .claude/skills/design-system/.
Hiệu ứng theo cuộn (kiểu trang Apple)
Hai bộ, cùng một họ API:
| Site | File |
|---|---|
| www.conan.school | app/components/motion/scroll.tsx |
| content.conan.school | src/motion/scroll.tsx |
Các khối của bộ www: Reveal (trồi lên và rõ dần một lần), Stagger (lưới hiện so le), HeroFade (khối đầu trang lùi lên và mờ khi cuộn qua), Parallax, ZoomIn, IrisReveal (ảnh mở từ một vòng tròn), Tilt (thẻ dựng đứng lên), WordReveal (câu sáng dần từng từ), DrawLine (vạch cam vẽ dần), ScrollProgress (thanh tiến độ, gắn một lần ở root.tsx). Trang chủ và 20 trang bên trong dùng bộ này từ 07.10.2026, kèm khoảng cách các phần giãn ra khoảng 1,5 lần.
Ba luật:
- Mọi giá trị gắn với cuộn đi qua
useRange.MotionConfig reducedMotion="user"(ởroot.tsx) chỉ tắt animation, không tắt style gắn vào scroll;useRangetrả trạng thái cuối khi người dùng bật giảm chuyển động. - Chữ đang đọc không được chạy. Hiệu ứng dành cho khung, ảnh, thẻ, tiêu đề. Thân bài viết, mô tả khoá, nội dung chương sách đứng yên.
overflow-x: cliptrênmain, khônghidden. Thẻ bay vào từ cạnh nằm ngoài khung lúc đầu và làm cả trang cuộn ngang;hiddensửa được chuyện đó nhưng biếnmainthành khung cuộn, và mọi khốistickybên trong thôi ghim.
Bẫy khi kiểm thử: trong pane browser đang ẩn (hay một tab nền), trình duyệt chỉ chạy khoảng 2 khung hình mỗi giây, nên các khối Reveal đứng ở trạng thái mờ ban đầu dù IntersectionObserver đã báo phần tử vào khung. Trông như lỗi, nhưng không phải: mở pane ra hoặc kiểm document.visibilityState trước khi kết luận.
Bẫy build: vite build không kiểm type, nên một route dùng HeroFade mà quên import vẫn build xanh rồi sập lúc chạy (đã suýt xảy ra ở news.$slug.tsx). Chạy npx react-router typegen && npx tsc --noEmit sau khi gắn hiệu ứng.
Luật màu: chỉ dùng token
Mọi màu đến từ packages/design-system/theme.css. Không hex tự chế, không bảng màu Tailwind.
| Tự chế | Token đúng |
|---|---|
#111 | foreground |
#d94f2b, #d84b2a | primary |
#faf5f2, #fffaf7, #fff7ed | accent |
emerald-600/700/800 | emerald |
emerald-50/100 | emerald/10 · emerald/15 |
amber-* | amber · amber/10 |
red-600/700/800 | destructive |
text-white trên nền token | text-background |
primary dành cho HÀNH ĐỘNG, không dành cho trang trí
primary là màu bão hoà nhất trong hệ. Nó là màu của nút chính (bg-primary, biến default của Button), của vòng focus, của trạng thái đang chọn. Sức mạnh của nó đến từ việc nó hiếm: trên một màn hình mà chỉ một hai thứ mang màu này, mắt đi thẳng tới đó.
Tô primary lên mảng trang trí lớn là tiêu hết sức mạnh đó. Nút CTA lập tức chìm vào nền, vì nó cùng màu với đồ trang trí quanh nó.
Đã xảy ra thật, 07.09.2026. Giao diện email dùng primary cho thanh tiêu đề, viền khung, viền ô mã số và vạch chú thích. Kết quả: nút "Vào học ngay" không nổi hơn bất cứ thứ gì trong thư. Lỗi không nằm ở nút, nút vẫn đúng token; lỗi nằm ở chỗ màu dành cho hành động bị tiêu vào trang trí.
Trang trí lấy màu ở đâu
| Cần gì | Dùng token |
|---|---|
| Mảng nhạt, nền khối, vùng nhấn nhẹ | accent + accent-foreground |
| Mảng đậm: thanh tiêu đề, chân trang, dải phân cách | surface-inverse + surface-inverse-foreground |
| Nền NGOÀI khung nội dung (khung thư, trang đọc) | canvas |
| Mảng nền rất nhạt bên trong khung trắng | surface-subtle |
| Nhãn nhạt khi KHÔNG dùng được độ mờ (HTML thư) | emerald-soft, amber-soft |
| Đường kẻ, viền | border |
| Nút chính, focus, trạng thái đang chọn | primary, và chỉ những chỗ này |
surface-inverse (#111111) cùng giá trị với foreground, cố ý: nó không phải màu mới, chỉ là tên ngữ nghĩa cho "dùng làm NỀN tối", để không ai phải viết bg-foreground (đọc như lỗi) hay tự bịa một mã hex tối.
Bốn token thêm 16.09.2026 (canvas, surface-subtle, emerald-soft, amber-soft) sinh ra từ việc dựng lại giao diện email. Hai token *-soft tồn tại vì HTML thư không dùng được độ mờ: web viết bg-emerald/10, còn email client bỏ qua màu có kênh alpha ở nhiều chỗ, nên bản nhạt phải là một mã ĐẶC có tên. Web dùng chúng cũng được, chúng là token ngữ nghĩa, không phải màu riêng của email.
Cách tự kiểm
Nhìn một màn hình và đếm số mảng mang màu primary. Nhiều hơn hai, hoặc có một mảng primary lớn hơn cái nút, là đã sai. Với email, api.conan.school/src/shared/email/templates.ts giữ bất biến chặt hơn: primary chỉ xuất hiện bên trong thẻ <a> của nút.
Độ đậm nhạt dùng độ mờ (/10, /30), không dùng bậc số của Tailwind.
Vì sao nghiêm
Đã vi phạm thật ngày 31.08.2026: 114 chỗ trong 27 file của trang học dùng màu tự chế. Hệ quả là đổi --color-primary không lan tới trang học, mười sáu chỗ vẫn giữ #d84b2a. Một hệ token mà không ai dùng thì chỉ là file trang trí.
Nguy hiểm nhất là bảng màu Tailwind: emerald-700 trông hợp lệ hơn một mã hex bịa ra, nhưng hệ thiết kế đã có --color-emerald cho đúng mục đích ngữ nghĩa ấy.
grep -rn "emerald-[0-9]\|amber-[0-9]\|red-[0-9]\|#[0-9a-fA-F]\{3,6\}" <file>