NVX UI — UI/UX Framework
On this page
🇹🇭 ภาษาไทยอยู่ด้านล่าง · Thai version below — ไปที่ภาษาไทย
NVX UI is the design system behind NVX Stack Builder: design tokens (CSS variables → Tailwind theme), a small component library in src/components/ui, and a living style guide at /design that renders every token and component variant in Thai and English, in both themes.
v0.5 Apple-inspired developer theme. The look references developer.apple.com and is black-first: pure-black canvas, grey surfaces separated by tone and hairline borders, big bold tightly tracked sans headlines, generous spacing, 12–22px radii, pill buttons, a restrained blue accent (filled primary CTA plus link text), and real tool logos as app-icon tiles. It's the default for everyone, whatever the OS colour preference. A light variant (Apple's #f5f5f7 canvas) stays available from the theme toggle. The terminal flavour is now limited to code windows: $ prompts, macOS traffic lights, a blinking cursor in the hero.
src/app/tokens.css ← token values (.dark = default black theme, :root = light variant)
src/app/globals.css ← maps tokens into Tailwind (@theme inline), motion utilities, keyframes
src/lib/design-tokens.ts ← token manifest (names, descriptions) used by /design and tests
src/components/ui/ ← components (import from "@/components/ui")
src/components/design-system.tsx + src/app/design/page.tsx ← the /design style guide
tests/ui-components.test.tsx, tests/tokens.test.ts ← component + token/contrast tests
1. Design tokens#
All values live in src/app/tokens.css as --nvx-* variables. .dark overrides only what changes; everything else cascades from :root. globals.css maps them into the Tailwind theme so you write semantic utilities, never raw palette colours:
| Group | Tokens (--nvx-…) |
Tailwind utilities |
|---|---|---|
| Surfaces | bg (#000), surface, surface-2, surface-3, raised (selected segment/tab), border, border-strong |
bg-bg, bg-surface, bg-surface-2, border-border, border-border-strong |
| Text | fg, muted, subtle |
text-fg, text-muted, text-subtle |
| Brand | primary #0071e3, primary-hover, primary-fg, primary-soft, primary-soft-fg, link (#2997ff dark / #0066cc light), accent, ring, brand-pink #fb0fab + brand-glow (mascot only, decorative) |
bg-primary text-primary-fg for filled CTAs, text-link for blue text (never text-primary: #0071e3 is not AA on black), ring-ring |
| Status | success/warning/danger/info + -soft + -fg (danger-hover) |
bg-success-soft text-success-fg, bg-danger … |
| Code | code-bg, code-fg, code-prompt (terminal green), code-muted, code-border, chrome-close/min/zoom (traffic lights) — always dark |
bg-code-bg text-code-fg text-code-prompt |
| Decoration | glow-1, glow-2, tile-top, tile-bottom, tile-edge, tile-sheen |
.hero-glow (soft blue/purple radial glows), .tool-tile (app-icon tile), .nav-glass (translucent header) |
| Overlay | overlay |
bg-overlay |
| Radius | xs 4px · sm 6px · md 10px · lg 12px · xl 18px · 2xl 22px · full | rounded-md (inputs, badges), rounded-xl (cards, code windows), rounded-full (buttons, chips, switches) |
| Spacing | 4px base unit (--spacing: 0.25rem) |
p-1 = 4px, gap-4 = 16px — prefer 2/3/4/6/8 |
| Shadow | dark: surfaces separate by tone + hairline, shadow-md soft drop on hover, shadow-lg dialogs/toasts; light: soft Apple-style shadows |
shadow-sm … |
| Motion | duration-fast 120ms, duration-base 200ms, duration-slow 320ms, ease-standard, ease-emphasized |
motion-fast, motion-base, animate-fade-in, animate-pop-in, animate-slide-in, animate-sheet-in |
| Focus | — | focus-ring utility (2px ring outline on :focus-visible) |
Typography. font-sans = Inter (SF-like) → IBM Plex Sans Thai → system UI, for headlines, body, nav and buttons. font-mono = IBM Plex Mono → Plex Sans Thai, only for code/pre/kbd/samp, commands and small tags (badge sm). Headlines are bold with tight tracking (tracking-tighter −0.03em); Thai headings automatically get letter-spacing: 0 and line-height 1.3. Scale: text-display 64px (hero, 44px on mobile), text-4xl/sm:text-5xl (page h1), text-3xl/sm:text-[40px] (section h2), text-xl (stage titles), text-base/17px (body), text-sm (UI), text-xs (captions/badges). Thai needs line-height ≥ 1.5 for body — don't use leading-none/leading-tight on Thai body text. Links use the link utility (text-link, underline on hover).
Theme. The black theme is the .dark class on <html>. It's rendered by the server and is the default. The inline pre-paint script only removes it when localStorage["nvx-theme"] === "light" (the toggle), and the OS prefers-color-scheme is deliberately ignored. Because components only use semantic tokens, nothing in a component should ever need a dark: variant — if you reach for one, add/adjust a token instead.
Reduced motion. Under prefers-reduced-motion: reduce all durations become 0.01ms and animations are disabled. The hero cursor stays visible but stops blinking.
Icons. No emoji anywhere in the UI.
- Tool logos (Node.js, npm, pnpm, Yarn, Python, PyPI, uv, Next.js, React, Vite, Express, FastAPI, Streamlit, TypeScript, Tailwind, Docker, GitHub Actions, Vitest, ESLint, pytest, Ruff, …) come from simple-icons (CC0 data).
scripts/gen-brand-icons.mjscopies only the icons we use intosrc/components/brand/brand-icons.data.ts(runnode scripts/gen-brand-icons.mjsafter adding an id) so the bundle stays small. Logos are trademarks of their owners, used only to identify the tools (seeNOTICE). OpenAI isn't in simple-icons, so AI features use a genericaitile (lucide Sparkles). src/components/brand/tool-icon.tsx:<ToolTile id size="sm|md|lg" />is an app-icon tile (28/40/56px, radius 8/11/15px) with the logo in its official colour. Use it on template cards, template detail, builder pick-lists and step cards.<ToolIcon id colored? />is an inline 1em mark,currentColorby default (tags, badges, segmented options).- Helpers:
templateTool(t),tagTool(tag),commandTool(commands)(first known CLI → logo, elseterminal),addonTool(id, runtimes).
- UI icons are lucide-react line icons (
h-4 w-4,aria-hidden; icon-only buttons keep anaria-label).
Code windows (the remaining terminal touch). CodeBlock and the hero card use code-bg, macOS traffic lights (chrome-* tokens), a $ prompt in code-prompt green, and term-cursor (blinking block cursor, aria-hidden, static under reduced motion). The v0.4 term-prompt/term-section/term-dos/term-bracket/scanline classes were removed.
2. Components#
Import everything from the barrel: import { Button, Card, useToast } from "@/components/ui". Interactive pieces are built on Radix UI primitives (headless, accessible); styling is Tailwind + tokens. card, badge, empty-state, kbd, skeleton, button-classes are server-safe; the rest are client components.
| Component | File | Variants / props | Notes |
|---|---|---|---|
Button |
button.tsx |
variant: primary · secondary · outline · soft · ghost · danger; size: sm · md · lg · icon; loading; asChild |
Defaults to type="button". loading ⇒ disabled + aria-busy + spinner. Icon-only buttons need aria-label. buttonClasses() (server-safe) styles a <Link>. |
Input, Textarea, Select |
field.tsx |
size sm·md·lg, invalid |
Native controls (best mobile + a11y support). |
Field, Label |
field.tsx |
label, hint, error, render-prop child |
Wires id, aria-describedby, aria-invalid; error has role="alert". |
Checkbox, Switch |
checkbox.tsx |
label, description, checked, onCheckedChange, disabled |
Radix. Always labelled. |
Segmented |
segmented.tsx |
legend, options, value, onChange, size, hideLegend |
Native radio group in a <fieldset> — arrow keys work. |
Card + CardHeader/Title/Description/Content/Footer |
card.tsx |
interactive, as (div/article/section/aside/li); CardTitle as h2/h3/h4 |
Pick the heading level that fits the page outline. |
Badge |
badge.tsx |
neutral · primary · success · warning · danger · info · outline; size sm·md |
Text carries the meaning, not colour. |
Tabs |
tabs.tsx |
Radix Tabs (TabsList needs aria-label) |
←/→ roving focus. |
Dialog, DialogTrigger, DialogContent, DialogClose |
dialog.tsx |
title (required), description, side center·right, hideTitle, closeLabel |
Focus trap, Esc, scroll lock, focus return. side="right" = sheet (mobile nav). |
ToastProvider, useToast |
toast.tsx |
toast({ title, description?, variant: default·success·error·info, duration? }) |
Radix Toast; polite (errors assertive); swipe/✕ to dismiss; pauses on hover/focus. useToastOptional for library code. |
Tooltip, TooltipProvider |
tooltip.tsx |
content, side |
Opens on hover and focus. Supplements, never replaces, an accessible name. |
CodeBlock |
code-block.tsx |
code, title, prompt, copyLabel, copiedLabel, toastTitle, maxHeight |
Copy button with live label; $ prompt glyphs aren't copied; toast when inside provider. App wrapper with i18n: src/components/copy-button.tsx. |
Stepper |
stepper.tsx |
items, current, onStepChange?, label |
<nav> + <ol>; current = aria-current="step"; completed show ✓ + sr-only “(completed)”. Compact on mobile (only the current label visible; others stay screen-reader text). |
EmptyState |
empty-state.tsx |
icon, title, description, action, tone neutral·danger, headingAs |
Danger tone = role="alert" for recoverable errors. |
Skeleton, Spinner |
skeleton.tsx |
className; Spinner label |
Wrap skeletons in role="status" with an sr-only label. |
Kbd, SearchIcon, cn, copyText |
kbd.tsx, icons.tsx, cn.ts, copy.ts |
Helpers. |
Brand mascot (src/components/brand/mascot.tsx, <Mascot size interactive? label? />): the pink blob logo as a living character. It is an eyeless body image (public/brand/mascot-body-*) with two glossy capsule eyes drawn in SVG on the same 1024 grid. CSS animations (.nvx-mascot* in globals.css): idle float + breathing, a single and a double blink per cycle, and a squish on hover/click (interactive). With interactive, the eyes follow a mouse pointer ((hover: hover) and (pointer: fine) only, throttled with requestAnimationFrame). The mascot is fully static under prefers-reduced-motion and decorative (aria-hidden) unless you pass label. The header uses 28px (float + blink); the home hero uses 112px (84px on mobile) in the empty corner of the code window. The pink is never used for text, buttons, links or focus: the interactive system stays blue. Assets, favicon, app icons and the OG card come from scripts/gen-brand-assets.py.
App-level composites built from these: Header (desktop nav, mobile sheet, theme/lang), CommandPalette (cmdk inside Dialog, Ctrl/⌘ + K), SortableSteps (dnd-kit), StepCard, TemplateCard, AgentPanel, Builder (4-stage wizard).
3. Adding a component#
- Check first — can an existing component + a variant do it? Prefer adding a variant over a new component.
- Create
src/components/ui/<name>.tsx. Add"use client"only if it uses state, effects, context or event handlers. Use a Radix primitive for anything with complex keyboard/focus behaviour (menus, popovers, comboboxes…). - Style with tokens only (
bg-surface,text-muted,border-border,focus-ring,motion-fast…). No hex values, noslate-*/indigo-*, nodark:. Need a new colour role? Add--nvx-<role>to both:rootand.darkintokens.css, map it in@theme inline, list it insrc/lib/design-tokens.ts. - API conventions —
variant+sizeprops with string unions and sensible defaults; acceptclassName(merged last viacn); forward refs for form controls; spread remaining native props; user-visible strings (labels like “Close”) come in as props so callers can translate them. - Export it from
src/components/ui/index.ts. - Document it on
/design: add an entry toSECTIONSand a<Section>insrc/components/design-system.tsxshowing every variant/size/state, with bilingual title + description and a usage snippet. Add a row to the table above (EN + TH). - Test it in
tests/ui-components.test.tsx(// @vitest-environment jsdom, Testing Library): role/name, keyboard behaviour, ARIA states. Runnpm run lint && npm run typecheck && npm test && npm run build.
4. Accessibility rules#
- Semantics first. Use real
<button>,<a href>,<label>,<fieldset>/<legend>, headings in order (oneh1per page). Lists of things are<ul>/<ol>. - Every control has an accessible name — a visible label (
Field,Checkbox label) oraria-labelfor icon-only buttons. Tooltips don't count. - Keyboard: everything reachable with Tab, operable with Enter/Space, Esc closes overlays; never remove focus outlines — use
focus-ring. Dialogs trap and return focus (Radix). Step reordering works without a mouse (drag handle → Space → ↑/↓ → Space, or the ↑/↓ buttons). - Contrast: text ≥ 4.5:1, large text/focus ring ≥ 3:1, in both themes.
tests/tokens.test.tsenforces this for the token pairs we use. - Don't rely on colour alone — badges and states always include text or an icon + sr-only text.
- Announce changes: toasts and
aria-liveregions for copy/download/plan results; errors userole="alert"; loading regions setaria-busyand includerole="status"text. - Motion: use motion tokens; everything respects
prefers-reduced-motion. - Language:
<html lang>follows the TH/EN toggle; every UI string exists in both languages (src/lib/i18n.ts). - Touch targets: ≥ 32px (
size="sm"), 40px default; mobile nav items are 48px tall.
ภาษาไทย#
NVX UI คือระบบดีไซน์ของ NVX Stack Builder ประกอบด้วย design token (ตัวแปร CSS → ธีม Tailwind), ไลบรารีคอมโพเนนต์ ขนาดเล็กใน src/components/ui และ คู่มือสไตล์แบบมีชีวิต ที่ /design ซึ่งแสดงโทเคนและคอมโพเนนต์ทุกแบบ ทั้งภาษาไทย/อังกฤษ และโหมดสว่าง/มืด
1. Design token#
ค่าทั้งหมดอยู่ใน src/app/tokens.css เป็นตัวแปร --nvx-* โดย .dark เขียนทับเฉพาะค่าที่ต่างกัน ส่วน globals.css แมปเข้าธีม Tailwind เพื่อให้เขียน utility เชิงความหมาย แทนสีดิบ
- พื้นผิว/ข้อความ:
bg-bg,bg-surface,bg-surface-2/3,border-border(-strong),text-fg,text-muted,text-subtle - แบรนด์:
bg-primary text-primary-fg(ปุ่มหลักสีฟ้า#0071e3),text-linkสำหรับข้อความ/ลิงก์สีฟ้า (#2997ffธีมดำ /#0066ccธีมสว่าง — ห้ามใช้text-primaryกับข้อความ เพราะไม่ผ่าน AA บนพื้นดำ),bg-primary-soft text-primary-soft-fg,accent,ring;--nvx-brand-pink/--nvx-brand-glowใช้กับมาสคอตเท่านั้น - สถานะ:
success/warning/danger/infoพร้อม-soft(พื้น) และ-fg(ข้อความบนพื้น) - ธีม v0.5 (อ้างอิง developer.apple.com): ค่าเริ่มต้นคือธีมดำสำหรับทุกคน (ไม่สนใจค่าสว่าง/มืดของระบบ) พื้นดำสนิท พื้นผิวสีเทาแยกด้วยน้ำหนักสีและเส้นขอบบาง หัวข้อตัวใหญ่หนา เว้นระยะกว้าง มุมโค้ง 12–22px ปุ่มทรงแคปซูล สีฟ้าใช้อย่างจำกัด และโลโก้เครื่องมือจริงเป็นไทล์แบบไอคอนแอป ธีมสว่าง (พื้น
#f5f5f7) ยังเลือกได้จากปุ่มสลับ - โค้ด:
bg-code-bg text-code-fg text-code-prompt(มืดเสมอ มีไฟสามสีแบบ macOS และ prompt$สีเขียว) - มุมโค้ง: xs 4px · sm 6px · md 10px (ช่องกรอก ป้าย) · lg 12px · xl 18px (การ์ด หน้าต่างโค้ด) · 2xl 22px ·
rounded-full(ปุ่ม ชิป สวิตช์) - ระยะห่าง: หน่วยฐาน 4px (
p-1= 4px) ใช้ 2/3/4/6/8 เป็นหลัก - เงา: ธีมดำแยกพื้นผิวด้วยน้ำหนักสีและเส้นขอบ ·
shadow-mdเงานุ่มเมื่อชี้ ·shadow-lgหน้าต่าง/toast · ธีมสว่างใช้เงานุ่มแบบ Apple - การเคลื่อนไหว: 120 / 200 / 320ms,
ease-standard,ease-emphasizedและ utilitymotion-fast,motion-base,animate-* - ตัวอักษร: Inter (แนวเดียวกับ SF) สำหรับหัวข้อ เนื้อหา เมนู และปุ่ม, IBM Plex Sans Thai สำหรับอักษรไทย (หัวข้อภาษาไทยใช้ระยะตัวอักษร 0 และบรรทัด 1.3 อัตโนมัติ), IBM Plex Mono ใช้เฉพาะโค้ด คำสั่ง และป้ายเล็ก สเกล:
text-display64px (มือถือ 44px),text-4xl/sm:text-5xl(h1),text-3xl/sm:text-[40px](h2),text-xl,text-base,text-sm,text-xs— เนื้อหาภาษาไทยต้องใช้ระยะบรรทัด ≥ 1.5 - ธีม: คลาส
.darkบน<html>ถูกเรนเดอร์จากเซิร์ฟเวอร์เป็นค่าเริ่มต้น และจะถูกถอดออกเฉพาะเมื่อผู้ใช้เลือกธีมสว่าง (localStorage["nvx-theme"] = "light") คอมโพเนนต์ไม่ควรต้องใช้dark:เลย — ถ้าจำเป็น ให้เพิ่ม/ปรับโทเคนแทน - ลดการเคลื่อนไหว: เมื่อผู้ใช้ตั้ง
prefers-reduced-motionระยะเวลาทั้งหมดเป็น 0.01ms และเคอร์เซอร์จะหยุดกะพริบ - ไอคอน: ไม่ใช้อีโมจิ โลโก้เครื่องมือ (Node.js, npm, Python, Next.js, React, Vite, FastAPI, Docker ฯลฯ) มาจาก simple-icons (ข้อมูล CC0) ผ่าน
scripts/gen-brand-icons.mjs→src/components/brand/brand-icons.data.tsใช้<ToolTile>(ไทล์ไอคอนแอป สีทางการ) และ<ToolIcon>(โลโก้ในบรรทัด) จากsrc/components/brand/tool-icon.tsxโลโก้เป็นเครื่องหมายการค้าของเจ้าของ ใช้เพื่อระบุเครื่องมือเท่านั้น (ดูNOTICE) OpenAI ไม่มีใน simple-icons จึงใช้ไทล์aiทั่วไปแทน ไอคอน UI อื่นใช้ lucide-react - รายละเอียดเทอร์มินัลที่เหลือ: เฉพาะหน้าต่างโค้ด — prompt
$, ไฟสามสี และterm-cursor(เคอร์เซอร์กะพริบ หยุดนิ่งเมื่อลดการเคลื่อนไหว) คลาสterm-prompt/term-section/term-dos/term-bracketและ scanline ถูกนำออกแล้ว
2. คอมโพเนนต์#
นำเข้าจาก @/components/ui ส่วนที่โต้ตอบได้สร้างบน Radix UI (headless และเข้าถึงได้)
- Button — 6 แบบ (primary, secondary, outline, soft, ghost, danger) × 4 ขนาด (sm, md, lg, icon),
loading(ปิดใช้งาน +aria-busy),asChildสำหรับ Link;buttonClasses()ใช้ได้ใน Server Component - Input / Textarea / Select / Field / Label — คอนโทรลเนทีฟ;
Fieldผูก id, คำใบ้, ข้อผิดพลาด (aria-describedby,aria-invalid,role="alert") - Checkbox / Switch (Radix) และ Segmented (กลุ่มเรดิโอเนทีฟ ใช้ลูกศรได้)
- Card (+ Header/Title/Description/Content/Footer,
interactive,as) และ Badge 7 แบบ 2 ขนาด - Tabs (Radix, ←/→), Dialog (กักโฟกัส, Esc, คืนโฟกัส;
side="right"= แผ่นข้างสำหรับเมนูมือถือ) - Toast (
useToast(); default/success/error/info) และ Tooltip (เปิดทั้งตอนชี้และโฟกัส) - CodeBlock — ปุ่มคัดลอกพร้อมป้ายที่เปลี่ยนสถานะ ไม่คัดลอกเครื่องหมาย
$และแสดง toast - Stepper —
aria-current="step", ขั้นที่เสร็จแสดง ✓, คลิกได้, แบบกะทัดรัดบนมือถือ - EmptyState — อธิบายสถานะว่าง/ข้อผิดพลาดพร้อมปุ่มถัดไป (
tone="danger"=role="alert") - Skeleton / Spinner, Kbd, SearchIcon
มาสคอตแบรนด์ (src/components/brand/mascot.tsx): โลโก้ก้อนสีชมพูที่ขยับได้ ประกอบด้วยภาพตัวที่ลบตาออก และตาแคปซูลมันวาวสองข้างวาดด้วย SVG ทำให้กะพริบตาและกลอกตาได้ แอนิเมชันเป็น CSS ล้วน: ลอยและหายใจ กะพริบตาแบบครั้งเดียวและสองครั้งต่อรอบ และเด้งยุบเมื่อชี้หรือคลิก (interactive) ตามองตามเมาส์เฉพาะอุปกรณ์ที่มี pointer ละเอียด หยุดนิ่งทั้งหมดเมื่อผู้ใช้ตั้ง prefers-reduced-motion ใน header ใช้ขนาด 28px (ลอยและกะพริบเท่านั้น) ส่วน hero หน้าแรกใช้ 112px (มือถือ 84px) วางในมุมว่างของหน้าต่างโค้ด สี brand-pink #fb0fab ใช้ตกแต่งเท่านั้น ห้ามใช้กับข้อความ ปุ่ม หรือลิงก์
คอมโพเนนต์ระดับแอป: Header (เมนูเดสก์ท็อป/มือถือ), CommandPalette (Ctrl/⌘ + K), SortableSteps (ลากวางด้วย dnd-kit + คีย์บอร์ด), StepCard, TemplateCard, AgentPanel, Builder (วิซาร์ด 4 ขั้น)
3. วิธีเพิ่มคอมโพเนนต์ใหม่#
- ตรวจก่อนว่าใช้คอมโพเนนต์เดิม + เพิ่ม variant ได้หรือไม่
- สร้าง
src/components/ui/<name>.tsxใส่"use client"เฉพาะเมื่อมี state/effect/event; ใช้ Radix สำหรับพฤติกรรมคีย์บอร์ด/โฟกัสที่ซับซ้อน - ใช้ โทเคนเท่านั้น ห้ามใช้ hex,
slate-*,indigo-*หรือdark:— ถ้าต้องการสีใหม่ ให้เพิ่ม--nvx-<role>ทั้งใน:rootและ.dark, แมปใน@theme inlineและเพิ่มในsrc/lib/design-tokens.ts - API:
variant+sizeเป็น string union พร้อมค่าเริ่มต้น, รับclassName(รวมด้วยcn), forward ref สำหรับฟอร์ม, ข้อความที่ผู้ใช้เห็นรับเป็น prop เพื่อแปลภาษาได้ - export จาก
src/components/ui/index.ts - เพิ่มตัวอย่างทุกแบบในหน้า
/design(src/components/design-system.tsx) พร้อมชื่อ/คำอธิบายสองภาษาและโค้ดตัวอย่าง และอัปเดตตารางในเอกสารนี้ - เขียนเทสต์ใน
tests/ui-components.test.tsx(role/ชื่อ, คีย์บอร์ด, สถานะ ARIA) แล้วรันnpm run lint && npm run typecheck && npm test && npm run build
4. กฎการเข้าถึง (Accessibility)#
- ใช้ HTML ที่ถูกความหมาย:
<button>,<a href>,<label>,<fieldset>/<legend>, หัวข้อเรียงลำดับ (หนึ่งh1ต่อหน้า) - ทุกคอนโทรลต้องมีชื่อที่เข้าถึงได้ — ป้ายที่มองเห็น หรือ
aria-labelสำหรับปุ่มไอคอน (Tooltip ไม่นับ) - ใช้งานด้วยคีย์บอร์ดได้ทั้งหมด: Tab, Enter/Space, Esc ปิดหน้าต่าง; ห้ามลบเส้นโฟกัส (ใช้
focus-ring); จัดลำดับขั้นตอนได้โดยไม่ใช้เมาส์ (ที่จับ → Space → ↑/↓ → Space หรือปุ่ม ↑/↓) - คอนทราสต์: ข้อความ ≥ 4.5:1, ข้อความใหญ่/วงโฟกัส ≥ 3:1 ทั้งสองธีม (ตรวจอัตโนมัติใน
tests/tokens.test.ts) - ไม่สื่อความหมายด้วยสีอย่างเดียว
- ประกาศการเปลี่ยนแปลง: toast และ
aria-liveสำหรับคัดลอก/ดาวน์โหลด/ผลลัพธ์ AI, ข้อผิดพลาดใช้role="alert", ส่วนที่กำลังโหลดใช้aria-busy+role="status" - เคารพ
prefers-reduced-motion,<html lang>ตามภาษาที่เลือก และทุกข้อความมีทั้งไทยและอังกฤษ - พื้นที่แตะ ≥ 32px (ขนาด sm), ค่าเริ่มต้น 40px, เมนูมือถือสูง 48px
Source: docs/UI-FRAMEWORK.md · /docs/ui-framework.md