TNRR Design Principles
เอกสารอธิบาย ทำไม ของทุกการตัดสินใจออกแบบใน TNRR AI Portal. ใช้คู่กับ /design-system (visual catalog) — หน้านี้เล่าหลักการ, หน้านั้นโชว์ของจริง. ทุกหัวข้ออ้างอิงไฟล์ + บรรทัดใน repo — ถ้าโค้ดเปลี่ยนต้องอัปเดตที่นี่ด้วย.
3 เสาหลัก: Trust · Discovery · AI Transparency
เลือกแนวทางตาม context ของผู้ใช้ — นักวิจัย/นักศึกษา/ผู้กำหนดนโยบายไทย ที่มาหา 'ข้อมูลงานวิจัยที่เชื่อถือได้' ไม่ใช่ consumer app
ข้อมูลงานวิจัยของประเทศไม่เหมาะกับ playful design. การเลือก Ocean Blue เป็นสีหลัก + typography แบบสะอาด + spacing ที่ไม่อัด = ผู้อ่านรู้สึกว่ากำลังอยู่ในแหล่งข้อมูลทางการ
app/globals.css :10-21components/layout/navbar.tsxผู้ใช้ไม่ได้รู้จัก dataset มาก่อน. Badge สีสัน semantic (topic/career/type/keyword) + แผนที่ + clustering + knowledge graph เป็นเครื่องมือให้ค่อยๆ narrow จากความคลุมเครือไปหางานที่ต้องการ
components/ui/badge.tsx :24-34app/clustering, /map, /analyticsAI ในงานวิจัยต้องไม่ขายความแน่นอนเกินจริง. Streaming cursor บอกว่ากำลังตอบสด; Research Meter 3-สี แยก หลักฐาน 'สนับสนุน/กล่าวถึง/ขัดแย้ง' ให้ผู้ใช้ตัดสินใจเองแทนที่จะเชื่อ AI ทั้งก้อน
components/ai/research-meter.tsxapp/globals.css :303-312 (cursor)searchResearch() มี 3-tier fallback ไม่คืน empty เพื่อให้ demo ไม่หน้าขาว.ระบบสี — 4 palette แยกบทบาท + 3 semantic สำหรับ Research
แต่ละ palette มี 'หน้าที่' ไม่ทับกัน. ไม่ใช้สีเดียวทั้งไซต์เพราะจะแยกไม่ออกว่าอะไรคืองานของแพลตฟอร์ม vs AI vs action
ใช้กับ: brand logo, navbar, primary button, link สำคัญ, chart series ที่ 1. สีน้ำเงินลึกสื่อถึงทางการและหน่วยงานรัฐ (วช.)
globals.css :10-21ใช้กับ: glass-ai panel, streaming cursor, focus ring, Badge variant='ai', AI button gradient. Teal-500 เป็น --ring เพื่อแยก focus ring ออกจากสีหลัก — ให้ motion ของ AI มี identity ชัด
globals.css :23-33, 178, 221components/ai/ai-panel.tsxใช้กับ: Badge variant='career', chart-3, highlight เชิงคน/มนุษยศาสตร์. ตัดกับ ocean blue โดยไม่ทำให้รู้สึกเตือนภัย (ถ้าใช้แดงแทนจะดุเกิน)
globals.css :36-42ใช้กับ: Research Meter 'กล่าวถึง', warning state, chart-4. เลือก amber แทนเหลืองจัดเพราะอ่านง่ายกว่าในทั้ง light และ dark
globals.css :45-48ข้อสำคัญ: สีแดงใน Research Meter ไม่ได้ หมายความว่า AI ผิด — มันแสดงว่ามีหลักฐานขัดแย้งในคลัง. นี่คือ transparency ที่ตั้งใจให้เห็น ไม่ใช่บั๊ก. ดู components/ai/research-meter.tsx:26-35
ทุกหน้าเขียน class จาก semantic (bg-background, text-foreground, ring-ring) — ไม่เรียก primary-700 ตรง ยกเว้นเมื่อต้องการสีที่ ไม่ flip ใน dark mode (เช่น footer ที่ตั้งใจให้พื้นหลังเข้มเสมอ). ดู mapping ที่ app/globals.css:158-227
variant="primary" (ocean), ปุ่มที่เกี่ยวกับ AI ใช้ variant="ai" (teal gradient). การสลับจะทำให้ผู้ใช้สับสนว่า action ไหนเป็นของแพลตฟอร์ม action ไหนเป็นของ AIThai-first pairing + สเกล 5 ระดับ
กลุ่มผู้อ่านหลักเป็นคนไทย — ต้องอ่านภาษาไทยสบายก่อน ภาษาอังกฤษเป็นส่วนประกอบ
Plex Thai มี proportional weight + ligature ที่อ่านยาวๆ ได้สบาย, รองรับวรรณยุกต์ครบ, และคู่กับ Inter ได้ดีเพราะ x-height ใกล้กัน. Fallback เป็น Noto Sans Thai สำหรับเครื่องที่โหลด Plex ไม่ทัน
globals.css :80-83, 247app/layout.tsx (font loading)5 ระดับพอสำหรับ long-form documentation + card list. Display สำหรับ hero หรือ page title เท่านั้น, heading สำหรับ section, body 15px เป็น default (ไม่ใช้ 16px เพราะทำให้หน้า data-heavy ดูแน่น), caption 12px สำหรับ metadata, overline 11px สำหรับ kicker
globals.css :316-326CSS negative tracking ที่ทำให้ตัวอักษร Latin ดูแน่นขึ้น จะทำให้สระ/วรรณยุกต์ไทยซ้อนกันอ่านไม่ออก. ระบบตั้ง -0.01em ถึง -0.02em เฉพาะ h1/h2 แต่ Plex Thai ออกแบบมารองรับระดับนี้ — ถ้าจะต่ำกว่านี้ต้องเช็คภาษาไทยก่อน
globals.css :248-2511440px container + 4px rhythm + lg-only gutter shift
Layout เดียวใช้ซ้ำทั้งไซต์ — ลด cognitive load ของผู้ใช้และของทีม
1440 เป็นความกว้างของ laptop แถวหน้า (MacBook 14/16, งานวิจัย desktop ส่วนใหญ่) และไม่ยืดบน 4K. เกิน 1440 paragraph จะกว้างเกิน readability (>90ch). ทุก page, navbar, footer ใช้ค่านี้เท่ากัน
components/layout/navbar.tsx :91components/layout/footer.tsx :716px ที่ mobile + tablet พอให้นิ้วโป้งจับขอบได้, 24px ที่ lg (≥1024) ให้หายใจ. ไม่ใช้ md breakpoint เพราะ tablet แคบยังอยากได้พื้นที่คอนเทนต์มากกว่ารอบ
CLAUDE.md Responsive sectionTailwind default ก็ 4px อยู่แล้ว — ไม่เพิ่ม custom step. ภายใน card ใช้ gap-2/3, ระหว่าง section ใช้ space-y-14 (56px)
<div className="mx-auto max-w-[1440px] px-4 lg:px-6 py-8 lg:py-12">
{/* content */}
</div>shadcn-on-Base-UI + CVA variants + data-slot targeting
ไม่ได้เลือก shadcn เพราะ trend — เลือกเพราะรวม 3 อย่างที่ต้องการ: a11y จาก Base UI, variant system จาก CVA, และ flexibility ที่เจ้าของ codebase เป็น source
Base UI ให้ keyboard nav, ARIA role, focus trap ฟรี. เราไม่ต้อง implement เอง ไม่ต้องเพิ่ม react-aria. Button/Input/Menu ทั้งหมด wrap Base UI เสมอ
components/ui/button.tsx :1components/ui/input.tsx :2shadcn default มีแค่ default / secondary / outline / ghost / link / destructive ซึ่งยังขาด 'AI' และ 'primary navy' ที่เป็น identity ของไซต์. Custom ผ่าน CVA เพิ่มใน components/ui/button.tsx โดยไม่ fork Base UI
components/ui/button.tsx :22-48CardHeader / CardContent / CardFooter แต่ละอันมี data-slot ของตัวเอง — CSS เขียน selector แบบ semantic ได้ (เช่น group-has-[data-slot='card-action'] เพื่อรู้ว่ามี action button อยู่). Container query (@container/card-header) ให้ header responsive โดยไม่ต้อง media query
components/ui/card.tsx :14-174 pattern ที่แยก TNRR ออกจาก dashboard ทั่วไป
ทุก pattern นี้ตั้งใจสื่อ 'AI ทำงานแบบไหน' ให้ผู้ใช้ไม่ต้องเดา — เพราะ AI ในงานวิจัยต้อง justifiable
ข้อความ AI ที่ยังไม่จบจะมี teal cursor กะพริบต่อท้าย (class .streaming-cursor). แยกให้ผู้ใช้รู้ว่านี่คือข้อความสด ถ้าปิดตอนนี้ข้อมูลไม่หาย — ต่างจาก spinner ที่บอกแค่ว่า 'รอ'
<div className={cn(!done && "streaming-cursor")}>{text}</div>app/globals.css :303-312components/search/ai-summary-strip.tsx :60Bar แบ่ง success / amber / error = สนับสนุน / กล่าวถึง / ขัดแย้ง. แต่ละส่วนมี transition-delay คนละ 150ms — ลำดับ cascade 0/150/300ms ทำให้ผู้ใช้อ่านค่าแต่ละด้านได้ทัน ไม่ใช่เห็นพร้อมกันแล้วข้ามไป. Detail สำคัญ: สีแดง = หลักฐานสวนทาง ไม่ใช่บั๊ก
<div className="h-full bg-success-500 transition-[width] duration-700 ease-out"
style={{ width: `${s}%`, transitionDelay: "0ms" }} />
<div className="h-full bg-amber-400" style={{ transitionDelay: "150ms" }} />
<div className="h-full bg-error-600" style={{ transitionDelay: "300ms" }} />components/ai/research-meter.tsx :24-36Class .glass-ai ให้ card พื้นหลัง teal-tinted + backdrop-blur + teal-200 border. ทุก AI content (summary strip, chatbot, DocChat) wrap ด้วย AiPanel — ผู้ใช้เห็นปุ๊บรู้ว่านี่คือสิ่งที่ AI สร้าง ไม่ใช่ metadata ของเอกสารต้นฉบับ
.glass-ai {
background: var(--card);
border: 1px solid var(--color-teal-200);
backdrop-filter: blur(12px);
}
.glass-ai::before { background: var(--gradient-ai); }app/globals.css :276-290components/ai/ai-panel.tsxlib/mock-ai.ts มี detectTopic (8 keyword pattern), detectIntent (7 chat intent), และ computeMeter ที่ deterministic ตาม paperId+columnType. lib/sse-server.ts wrap streaming ให้ส่งทีละคำพร้อม jitter เล็กน้อย — ได้ feeling LLM ไม่ต้องพึ่ง API จริง. ทุก route ใน app/api/ ต้อง runtime='edge' เพราะ Netlify regular function cap 10s จะตัด AI flow ของเรา (8-12s)
export const runtime = "edge";
export async function POST() {
return sseStream(async (send) => {
await streamTokens(send, answer); // word-by-word with jitter
send("done", {});
});
}lib/mock-ai.tslib/sse-server.tslib/mock-docchat.tslib/mock-columns.tslib/mock-ai.ts / lib/mock-docchat.ts. การเพิ่ม if-else ใน handler ทำให้ตรวจสอบ deterministic ยาก และ diverge จาก visual catalog ของ /workspace ที่ pre-seed ผ่าน mock-columnsDesktop-first + additive — ไม่ใช่ mobile-first
กลุ่มผู้ใช้หลัก (นักวิจัย) ใช้ laptop/desktop. Mobile support เพิ่มเข้ามาโดยไม่ลดอะไรของ desktop
Navbar links เต็ม ≥1024px. เมื่อ <lg แสดงปุ่ม hamburger เปิด Sheet drawer (280-320px) — ให้ผู้ใช้ยังเข้าได้ทุกหน้าโดยไม่ hide feature
components/layout/navbar.tsx :95-142บนมือถือ sidebar hide หมด — action หลัก (DocChat, New Workspace) ย้ายเป็น FAB ตำแหน่งคงที่: lg:hidden fixed bottom-5 right-5 z-30. ซึ่งเป็นมือถืออยู่แล้วที่เอื้อมถึง
components/research/docchat-fab.tsx :16Sidebar ปกติ hide <md หรือ <lg แล้วเปิดผ่าน Sheet. หลัก: เปิดเป็น overlay เสมอ ไม่ดัน content. ผู้ใช้มือถือเลื่อนหาผลการค้นหาก่อน — filter เป็นขั้นถัดไป
Vocabulary แบบเรียบ — motion เป็น garnish ไม่ใช่ payload
การเคลื่อนไหวไม่ควรเป็นสาเหตุที่ทำให้ feature ทำงานได้. ทุกอย่างต้อง degrade อย่างสง่างามเมื่อ prefers-reduced-motion
translateY(-0.5) + shadow-cardhLift เบาๆ ชวนคลิกโดยไม่ bounce (ไม่ใช้ scale). ให้ feel premium เหมาะกับ tone ทางการ
components/research/research-card.tsx :47150ms delay stepตามองทีละส่วน — ไม่ปล่อยเผยพร้อมกันแล้วข้ามไป. 150ms เป็น perceptual sweet spot
components/ai/research-meter.tsx :24-361.8s ease-in-out infiniteshimmer สื่อ 'กำลังโหลด' โดยไม่ต้อง spinner กลาง. เร็วพอให้ไม่รำคาญ ช้าพอให้ไม่ hyper
app/globals.css :292-301600ms cubic-bezier(0.16, 1, 0.3, 1)Curve exponential out — เริ่มไว จบนุ่ม. 600ms พอให้รู้สึกว่ามี transition ไม่หั่นกระด้าง
app/globals.css :113-116, 314Media query ใน globals.css ตัด animation-duration และ transition-duration เหลือ 0.01ms เมื่อ OS ตั้งค่า reduce motion — feature ต้องยังทำงานเต็ม ไม่หาย. อย่าเขียน animation ที่ 'ต้องเห็นเพื่อเข้าใจ'
app/globals.css :259-264Focus · ARIA ภาษาไทย · keyboard ฟรีจาก Base UI
เข้าถึงได้ไม่ใช่ตัวเลือก — เป็น baseline. ทุก interactive ต้องใช้คีย์บอร์ดได้และต้องมี visible focus
Global rule *:focus-visible ใช้ --ring = teal-500 (light) / teal-400 (dark) — ทุกปุ่ม ทุก input ได้ focus indicator เหมือนกัน. ไม่ override โดยไม่จำเป็น
app/globals.css :252-256Screen reader ของผู้ใช้ไทยต้องอ่านออกเป็นภาษาไทย. ตัวอย่าง: aria-label='เปิดเมนู', 'ย่อ', 'ขยาย', 'สร้างใหม่'. ห้าม aria-label ว่างหรือใช้ภาษาอังกฤษถ้า UI เป็นไทย
components/layout/navbar.tsx :97components/search/ai-summary-strip.tsx :49-52Input / Textarea มี aria-invalid:border-destructive aria-invalid:ring-destructive/20 — แค่ set aria-invalid={true} สถานะ error visible อัตโนมัติ ไม่ต้องเขียน class เพิ่ม
components/ui/input.tsx :11-12Menu, Select, Tabs, Dialog ของ @base-ui/react รองรับ arrow keys, escape, focus trap อยู่แล้ว. ถ้าเราเขียน custom component ต้องทดสอบ tab / shift-tab / esc ให้ครบก่อน ship
next-themes class-based + token 2 ชั้น
Light เป็น default (ตาม use case ทางการ), dark เป็น opt-in. ทั้งสองต้องทดสอบทุกหน้า
ชั้นแรก (primary-50..950, teal-50..900, ink-0..950) คงที่ทุก theme. ชั้นสอง (background, foreground, muted, ring, chart-1..5) swap ตาม :root vs .dark. เวลาเขียน component ใช้ semantic เสมอ — ยกเว้นเมื่อต้องการสีเฉพาะ theme ตั้งใจ (เช่น footer dark navy ทั้ง light/dark)
app/globals.css :158-227ใน light mode teal-50 (อ่อนสุด) เป็นพื้น AI, ใน dark ใช้ teal-800 (เข้ม) + foreground teal-200. Swap นี้คงความ 'เป็นพื้นที่ของ AI' แต่ไม่ทำให้พื้นเจิดจ้าเกินในธีมมืด
app/globals.css :215-216useTheme().resolvedTheme แล้วส่งสี explicit เป็น prop. Default Tooltip ยังเป็นสีขาวใน dark mode — ต้อง override contentStyle. ดู pattern ที่ TrendChart และ AuthorNetworkคนเลือกข้อความเพื่อ copy — ให้สีเดียวกันในทั้ง 2 theme เพื่อ muscle memory
app/globals.css :257-258Content & Voice — Thai-first, action-oriented, AI-as-assistant
เนื้อหาและทุกคำพูดในระบบต้องสื่อสารภายใต้ tone เดียวกัน — ทางการพอให้น่าเชื่อถือ, ชัดพอให้ทำงานต่อได้
ผู้ใช้เป็นนักวิจัย/นักศึกษาบัณฑิต/ข้าราชการ. ใช้ 'ค้นหางานวิจัย' ไม่ใช่ 'หาอะไรดี'; 'เพิ่มใน Workspace' ไม่ใช่ 'ใส่ไว้เดี๋ยว'. แต่ไม่ต้อง stiff — หลีกเลี่ยงคำโบราณ เช่น 'โปรดกระทำ'
'สร้างรายงาน', 'เพิ่มใน Workspace', 'ค้นหา', 'ดาวน์โหลด PDF'. ไม่ใช้ 'ไปกันเลย', 'เริ่มต้น' เพราะผู้ใช้ต้องรู้ว่า <em>อะไร</em> จะเกิดขึ้น
searchResearch() มี 3-tier fallback ตั้งใจคืนผลเสมอ — โดยดีไซน์เพื่อหลีกเลี่ยง empty state ใน demo. ถ้าหลีกเลี่ยงไม่ได้จริงๆ ให้เขียน 'ลองคำค้น...' + แนะนำ 2-3 ตัวอย่าง ไม่ใช่ '—' หรือ 'No results'
lib/fixtures/research.ts searchResearchแทนที่ 'Error: request failed' ให้เขียน 'ขออภัย ดึงข้อมูลไม่ได้ กรุณาลองใหม่'. แทนที่ 'Loading' ให้ใช้ 'กำลังสรุป...', 'กำลังค้นหา...'. Tone นี้ map กับ streaming cursor — ผู้ใช้รู้สึกว่า 'คน' กำลังช่วยอยู่
components/search/ai-summary-strip.tsxWorkspace, Knowledge Graph, Clustering, Dashboard, SSE. การแปลฝืนเช่น 'กราฟความรู้' ทำให้ค้นยาก และศัพท์เหล่านี้เป็นภาษากลางในงานวิจัย. แต่สำหรับคำที่มีคำไทยดี (Search → 'ค้นหา', Report → 'รายงาน') ให้ใช้ไทย
UI ทั่วไปใช้ พ.ศ. (เช่น footer '© พ.ศ. 2569'). แต่ metadata งานวิจัยหลายชิ้นเป็น ค.ศ. (ตาม DOI / ORCID). กติกา: สำหรับ UI chrome ใช้ พ.ศ., สำหรับข้อมูลจากคลังวิจัยคงตามต้นฉบับ
components/layout/footer.tsx :56เอกสารนี้ sync ด้วยมือกับโค้ดจริง — ถ้าแก้ app/globals.css, components/ui/*, lib/mock-ai.ts ควรเช็คว่า section ที่เกี่ยวข้องในหน้านี้ยังตรง. สำหรับ token values หรือ component playground ให้ดูที่ /design-system.