Repository navigation
UI style
Trang này ghi lại style của Zhesen: màu, chữ, khoảng cách, animation và các luật owner đã chốt. Mọi trang mới hoặc trang được làm lại theo style này. Người sửa UI bằng Claude Code đọc trang này trước. Việc đưa các trang còn lại sang style này nằm ở issue #87.
Các trang đã dùng style này:
| Trang | Issue |
|---|---|
Header, footer, trang / (landing page và homepage khi đã đăng nhập) |
#86 |
Trang Dịch /dictionary và trang chi tiết từ /dictionary/<lang>/<id>
|
#87 |
Sổ tay /wordlist
|
#87 |
Khu admin /admin và /rescue
|
#87 |
Lý thuyết /theory và mọi trang con |
#87 |
Luyện tập /practice và 6 chế độ luyện |
#87 |
/account, /login, /register, trang 404 và trang lỗi |
#87 |
Mọi trang của Zhesen đã dùng style này. Tính năng mới cũng dùng style này, không cần owner duyệt lại giao diện.
| Luật | Nguồn |
|---|---|
| Toàn bộ Zhesen dùng trắng, đen và một dải xanh 8 bậc | Owner, 2026-09-29 |
Không dùng #03045a: "hơi đậm, đừng dùng nữa nhé, dùng các màu sáng sáng, nhẹ nhẹ pastel một tí" |
Owner, 2026-09-29 |
| Nền các section sáng, pastel. Ngoại lệ duy nhất là 3 cột ngôn ngữ khi quả cầu mở phẳng, màu đậm của chúng đã được owner duyệt cùng quả cầu | Owner, 2026-09-29 |
| Trang cho người học không ghi số entry, licence hay cách đăng nhập | Owner |
| Không có khối độn: dải kêu gọi cuối trang, câu trấn an, câu liệt kê tính năng, tiêu đề lặp lại nội dung | Owner |
| Không viết cứng chi tiết dễ đổi khi Zhesen phát triển, như tên kỳ thi hay số lượng mục | Owner, 2026-09-29 |
| Mọi tính năng mới theo style này: "sau này có dev feature/function mới thì cũng cứ áp dụng theo design style có sẵn là được" | Owner, 2026-09-30 |
| Dùng đa dạng dải xanh, không chỉ 2 màu: "palette có khá nhiều màu xanh mà, sử dụng đa dạng lên". Hai đoạn chữ khác vai trò đặt cạnh nhau, như câu ví dụ và câu dịch, phải phân biệt được ngay | Owner, 2026-09-30 |
Copy tiếng Việt theo .claude/rules/copy.md: không "bạn", không "Hãy", dùng từ trong glossary |
Owner, issue #68 |
Không placeholder bịa như ban@vidu.com, không câu dịch word-for-word từ tiếng Anh như "phiên ôn", "lượt giữ chuỗi" |
Owner, 2026-10-13 |
| Logo là chữ Z gấp từ dải băng, xem mục Logo | Owner, 2026-10-17 |
| Logo đứng thay chữ Z trong tên, chữ "hesen" chuyển màu qua 3 màu logo, header chỉ hiện logo, icon tab không nền | Owner, 2026-10-18 |
Token nằm trong app/globals.css. Component dùng token, không viết mã màu trực tiếp.
| Token | Sáng | Dùng cho |
|---|---|---|
--sea-50 |
#c3e7ef |
Nền chip, nền nhạt |
--sea-100 |
#a7dfea |
Viền, nền thẻ |
--sea-200 |
#8bd6e8 |
Nền thẻ, cột biểu đồ |
--sea-300 |
#45c2db |
Màu tiếng Tây Ban Nha |
--sea-400 |
#0cafd2 |
Điểm nhấn |
--sea-500 |
#0090bf |
Chữ lớn, icon |
--sea-600 |
#0172b6 |
Nút chính, màu tiếng Trung |
--sea-700 |
#023c85 |
Chữ tiêu đề, màu tiếng Anh |
Dải --sea-* giữ nguyên ở theme tối. Thứ gì phải đổi theo theme thì dùng token ngữ nghĩa, mỗi token có giá trị tối riêng trong khối :root[data-theme="dark"]:
| Nhóm | Token | Vai trò |
|---|---|---|
| Khung trang |
--zs-bg, --zs-ink, --zs-soft, --zs-line, --zs-field, --zs-pen, --zs-chip
|
Nền, chữ, chữ phụ, đường kẻ, ô nhập, link, chip |
| Nút chính |
--zs-btn, --zs-btn-ink, --zs-btn-hover
|
Sáng: chữ trắng trên #0172b6, hover #023c85. Tối: nút trắng chữ xanh |
| Dấu tô |
--zs-mark, --zs-mark-ink
|
Nền và chữ của phần được tô, như dạng từ bất quy tắc trên trang chi tiết từ |
| Nền section |
--tint-1, --tint-2, --tint-3, --edge, --lift
|
3 mức nền pastel, viền thẻ, bóng đổ |
| Ngôn ngữ |
--l-en, --l-zh, --l-es và bản nhạt --l-*-t
|
Mỗi thứ tiếng một màu, dùng chung cho quả cầu và homepage |
| Quả cầu | --g-* |
Đại dương, đất liền, viền nước, Việt Nam, bóng đêm |
Tailwind có sẵn class bg-sea-50 tới bg-sea-700. Lưu ý là class bg-white và text-white của repo này đổi thành màu tối ở theme tối, nên chỗ cần trắng thật thì dùng token --zs-*.
Bảng màu không có đỏ, nên trạng thái lỗi và thao tác xoá được đánh dấu bằng hình dạng:
| Trạng thái | Cách vẽ | Mẫu |
|---|---|---|
| Lỗi | Chữ màu ink kèm icon cảnh báo aria-hidden, giữ nguyên role="alert" nếu có |
components/search/ErrorLine.tsx, components/lookup/FeedbackButton.tsx
|
| Nút xoá | Viền 1.5 px màu ink, nền trong suốt, tô đặc khi hover. Nút chính vẫn tô đặc màu xanh |
components/wordlist/Wordlist.module.css, class .danger
|
| Link xoá trong dòng | Chữ màu ink có gạch chân, khác hẳn các link bên cạnh ngay cả khi chưa hover |
components/wordlist/Wordlist.module.css, .act[data-danger]
|
Hai màu dùng để phân biệt hai thứ, như chấm chú thích hay trạng thái bật tắt, phải khác nhau ở cả hai theme. Ở theme tối --zs-pen là --sea-300, nên một cặp --zs-pen và sea-300 khác nhau ở theme sáng sẽ trùng nhau ở theme tối.
Không làm mờ chữ bằng hậu tố độ mờ trên token, như text-(--zs-pen)/60. Test test/text-contrast.test.ts chỉ bắt text-black/<n>, nên chữ mờ kiểu này lọt qua test. Dùng một token đặc đạt 4.5:1 ở cả hai theme.
Trong một nhóm lựa chọn như LayoutPicker, bộ chọn chiều tra hay nút bật tắt ngôn ngữ, mục đang chọn tô đặc bằng --zs-btn với chữ --zs-btn-ink. Nền trắng trên nền pastel không đủ 3:1 để phân biệt mục đang chọn.
Rule :focus-visible chỉ thêm outline, không đổi border-radius, để nút pill giữ hình khi được focus bằng bàn phím.
Chữ nhỏ trên nền #0cafd2 hoặc #0090bf phải màu đen. Chữ #023c85 trên hai nền này dưới mức tương phản 4.5:1. Biến --ink của repo vẫn là đen, vì chữ độ mờ 55% bằng #023c85 chỉ còn khoảng 3.2:1 và test tương phản test/text-contrast.test.ts sẽ fail.
Logo là chữ Z gấp từ một dải băng, nhạt ở dải trên và đậm dần xuống dải dưới. Owner chọn ngày 2026-10-17 từ các phác thảo tạo bằng AI, sau đó phác thảo được vẽ lại thành vector.
| Phần | Theme sáng | Theme tối |
|---|---|---|
| Dải trên |
--sea-300 #45c2db
|
--sea-200 #8bd6e8
|
| Dải chéo |
--sea-500 #0090bf
|
--sea-400 #0cafd2
|
| Dải dưới |
--sea-700 #023c85
|
--sea-600 #0172b6
|
| Chữ "hesen" | Chuyển màu từ trái sang phải qua 3 màu trên | Như theme sáng, dùng 3 màu theme tối |
Hai tam giác bóng màu đen, độ mờ 16% và 26%, cho thấy dải chéo luồn dưới dải trên rồi đè lên dải dưới. Trên web, màu ba dải là token --zs-logo-1 tới --zs-logo-3, component vẽ logo là ZMark (components/layout/ZMark.tsx), và component vẽ cả tên là BrandMark (components/layout/SiteHeader.tsx).
Luật dùng:
- Logo đứng thay chữ Z trong tên. Phần còn lại "hesen" dùng Be Vietnam Pro 800, letter-spacing -0.02em. Logo cao bằng chiều cao chữ hoa và đứng trên baseline.
- Header chỉ hiện logo. Khi rê chuột hoặc focus bằng bàn phím vào logo, chữ "hesen" hiện ra bên phải. Footer và ảnh chia sẻ luôn hiện đủ tên.
- Icon trên tab trình duyệt là logo không nền, đổi sang 3 màu theme tối khi trình duyệt dùng theme tối. Icon app, icon PWA và avatar giữ ô nền trắng, vì launcher tô góc trong suốt thành màu đen.
- Khoảng trống quanh logo ít nhất bằng bề dày một dải, tức 30% chiều cao logo.
- Cỡ nhỏ nhất là 16 px. Ở cỡ đó favicon bỏ hai tam giác bóng.
- Không đổi màu ra ngoài dải xanh, không xoay, không kéo méo, không thêm viền hay bóng đổ.
- Trên nền tối hoặc nền ảnh thì dùng bản tối. In một màu thì dùng bản mono.
Bộ file nằm ở public/brand/, tải tại https://zhesen.net/brand/<tên file>:
| File | Dùng cho |
|---|---|
zhesen-logo.svg, zhesen-logo.png
|
Cả tên, logo thay chữ Z, nền sáng |
zhesen-logo-dark.svg, zhesen-logo-dark.png
|
Cả tên, logo thay chữ Z, nền tối |
zhesen-mark.svg, zhesen-mark.png
|
Chỉ hình chữ Z, nền sáng |
zhesen-mark-dark.svg |
Chỉ hình chữ Z, nền tối |
zhesen-mark-mono.svg |
Một màu --sea-700, cho bản in một màu |
zhesen-icon.png |
Ô vuông nền trắng 1024 px, cho avatar mạng xã hội |
Trên site, logo nằm ở app/icon.svg, app/favicon.ico (16, 32 và 48 px), app/apple-icon.png (180 px) và 3 icon PWA trong public/. Icon maskable giữ logo trong vòng tròn an toàn 80% của Android. Mọi trang có ảnh chia sẻ chung app/opengraph-image.tsx, riêng trang chi tiết từ có ảnh chia sẻ của từng từ.
Script scripts/brand-build.js tạo lại mọi file trên từ một bộ toạ độ. Chạy ở gốc repo: npm i --no-save opentype.js@1.3.4, rồi node <wiki>/scripts/brand-build.js.
| Font | Dùng cho | Khai báo |
|---|---|---|
| Be Vietnam Pro 400, 600, 700, 800 | Chữ giao diện của mọi trang đã làm lại |
app/layout.tsx, class font-ui
|
| Newsreader | Từ tiếng Anh và tiếng Tây Ban Nha. Tiếng Tây Ban Nha in nghiêng |
components/home/fonts.ts cho / và /wordlist; components/lookup/fonts.ts cho mọi trang dưới /dictionary, gắn ở app/dictionary/layout.tsx
|
| Serif có sẵn của máy | Chữ Hán | Không tải font, vì Noto Serif SC không có subset tiếng Trung để preload |
| Geist | Phần thân các trang chưa làm lại | app/layout.tsx |
Cỡ chữ theo tỉ lệ 1.333: 12, 16, 21, 28, 37.5, 50, 66.6 px. Tiêu đề đậm 800, letter-spacing âm từ -0.02em tới -0.04em. Câu dài không được xuống dòng giữa cụm như "tiếng Anh," hay "tiếng Tây Ban Nha": bọc cụm đó trong span nowrap.
Mọi trang và header dùng chung một độ rộng --container-page và class mx-auto max-w-page px-6, để mép trái phải thẳng hàng. Ở 390 px lề hai bên là 16 tới 24 px và không có thanh cuộn ngang. Chiều cao header nằm ở biến --header-h: 64 px trên màn rộng, 96 px dưới 640 px. Anchor #id phải hiện dưới header.
Header gồm logo, 4 link Dịch, Lý thuyết, Luyện tập, Sổ tay và góc tài khoản. Góc tài khoản chỉ hiện chữ cái đầu trong vòng tròn, email nằm ở tooltip và trang /account. Footer gồm logo, nhóm Học, nhóm Tài khoản và dòng ©. Cả hai là components/layout/SiteHeader.tsx và components/layout/SiteFooter.tsx.
Trang có nhiều bố cục thì dùng chung component LayoutPicker (components/ui/LayoutPicker.tsx), như trang chi tiết từ và homepage. Lựa chọn nhớ theo trình duyệt. Script chạy trước khi vẽ trang đặt lựa chọn lên thẻ <html>, nên trang không nháy sang bố cục khác lúc tải.
Easing nằm trong app/globals.css, dùng được như class Tailwind:
| Token | Đường cong | Dùng cho |
|---|---|---|
--ease-enter |
cubic-bezier(0.05, 0.7, 0.1, 1) |
Phần tử hiện vào |
--ease-std |
cubic-bezier(0.2, 0, 0, 1) |
Đổi trạng thái, hover |
--ease-spring |
linear(...) hơi nảy |
Nút, thẻ, mũi tên |
Luật chuyển động:
- Mỗi animation có bản thay thế khi người dùng bật giảm chuyển động (
prefers-reduced-motion). Khi đó trang hiện trạng thái cuối, không bỏ nội dung. Hook đọc thiết lập này làuseReducedMotion(lib/hooks/useReducedMotion.ts). - Thứ gì chuyển động quá 5 giây, như quả cầu quay, phải có nút dừng (WCAG 2.2.2).
- Khối nội dung hiện dần một lần khi cuộn tới, lệch nhau theo thứ tự, bằng component
Reveal(components/home/Reveal.tsx) và attributedata-reveal. - Số đếm lên, cột biểu đồ mọc lên và thẻ lật chỉ chạy một lần và dưới 5 giây.
- Canvas dừng vẽ khi ra khỏi màn hình hoặc khi tab bị ẩn.
- Hover đổi màu trong 150 ms với
--ease-std. Dịch chuyển và phóng to dùng--ease-spring, từ 160 tới 520 ms tùy độ lớn của phần tử.
- CSS riêng của một khu vực nằm trong CSS Module cạnh component, như
components/home/Landing.module.cssvàHome.module.css. - Biến CSS riêng cho từng phần tử, như thứ tự hiện
--i, đặt qua rule[data-*]trong CSS Module. TypeCSSPropertiescủa React không nhận--xnếu không ép kiểu. - Kiểm tra trên bản đã build ở 1440x900, 1280x720 và 390x844, theme sáng và tối, có và không có giảm chuyển động. Skill
/verify-uichứa các bước này.
Owner duyệt các bản demo HTML ngày 2026-09-29. Ảnh dưới chụp bản build đọc database production.
Landing page cho người chưa đăng nhập. Quả cầu quay, tô màu các nước nói từng thứ tiếng, và từ vừa tra bay từ Hà Nội tới từng nơi.

Cuộn xuống thì quả cầu mở phẳng thành bản đồ, rồi tách thành 3 cột, mỗi cột một thứ tiếng và một con số về thứ tiếng đó.


Theme tối dùng cùng bố cục, nền đen và dải xanh giữ nguyên.

Homepage khi đã đăng nhập có 3 bố cục. Bàn học là mặc định.



Ở 390 px mọi thứ xếp một cột.

Trang Dịch. Mỗi chiều tra có một ô riêng, kết quả của từng thứ tiếng nằm trên nền pastel riêng.

Trang chi tiết từ ở bố cục Tổng quan. Headword tiếng Anh dùng Newsreader, dạng bất quy tắc được tô bằng --zs-mark.

Sổ tay ở chế độ lưới. Mỗi card có dải màu ngôn ngữ ở mép trái, nút xoá là link có gạch chân.

Ngoài wiki
