บทเรียน 129 · Rails · Day 3

Lab Vibe 2 — navbar, card และปุ่มสลับธีมสว่าง/มืด

~30 นาทีอ่าน + ลงมือทำตามอีก ~40 นาที · ควรผ่าน บท 126 (Lab Vibe 1) · บท 127 (asset · precompile) · บท 128 (Tailwind v4 · daisyUI) มาก่อน

Lab ท้ายบล็อก B (CSS) · ตอนนี้หน้าเว็บของเราอ่านรู้เรื่องแล้ว แต่ยังหน้าตาเหมือน scaffold ดิบ ๆ: ปุ่มน้ำเงินตายตัว ไม่มีแถบเมนู · บทนี้จะแต่งให้ดูเป็นเว็บจริงด้วยชิ้นสำเร็จรูปของ daisyUI · ใส่แถบเมนูด้านบน · และทำปุ่มสลับธีมสว่าง/มืดที่จำค่าไว้ได้
อ่านจบแล้วคุณจะ: เปลี่ยนสีทั้งเว็บได้ด้วยป้ายเดียวบนแท็ก <html> · เขียน JavaScript ตัวแรกแบบ Rails (Stimulus) ได้ · ตรวจงานตาม checklist ของสไลด์หน้า 34 ได้ทีละข้อด้วยวิธีที่วัดได้จริง · และรู้ว่า AI มักพลาดตรงไหนในงานแต่งหน้าเว็บ

🔁 ทวนก่อนเริ่ม — Lab นี้ใช้คำพวกนี้ตลอด ถ้าข้อไหนไม่คุ้นให้กลับไปบทนั้นก่อน

🧰 ตรวจด้วย Ruby 4.0.9 · Rails 8.1.4 · tailwindcss-rails 4.7.0 (Tailwind 4.3.3) · daisyUI 5.7.50 · stimulus-rails 1.3.4 (สไลด์และ repo ของอาจารย์ใช้ Ruby 4.0.5–4.0.6 · Rails 8.1.3 — รุ่นแก้บั๊กในสายเดียวกัน เรื่องในบทนี้ไม่ต่างกัน)

🗂 โค้ดเริ่มจาก: แอปหลังจบ Lab บท 126 ที่ commit ไว้ใน branch my-day3 (ตามไม่ทันดูบท 126) · บทนี้เก็บจริง — จบแล้ว commit · บท 130 เริ่มจากตรงนี้ · branch day3-b-done ของอาจารย์ทำเรื่องเดียวกัน แต่หน้าตาไฟล์ต่างจากบทนี้ (ใช้ระบบแปลภาษา t(".title") และไอคอนอีกแบบ) อย่าก็อปปนกัน

ผลทุกก้อนรันจริงแล้วก็อปมา · ตัดบรรทัดที่ไม่เกี่ยวเป็น … · ค่าที่เปลี่ยนทุกครั้ง เช่น เวลา ms · เลข commit · คำว่า "1 day ago" (นับจากวันที่คุณรัน seeds) ของคุณจะต่าง · สีที่วัดได้เขียนเป็น oklch(…) — คือวิธีเขียนสีแบบหนึ่งของ CSS ไม่ต้องอ่านตัวเลขเป็น แค่ดูว่า "เปลี่ยน" หรือ "ไม่เปลี่ยน"

แผนที่ของ Lab: ทำเองก่อน แล้วค่อยเขียนใบสั่งให้ AI

สไลด์หน้า 34 ให้ "ใบสั่งงาน AI" มาหนึ่งใบ (เป้าหมาย · ข้อกำหนด · ห้าม · เกณฑ์ตรวจรับ — สี่ส่วนเดียวกับบท 121) · บทนี้จะทำทุกข้อในใบนั้นด้วยมือก่อน เพื่อให้รู้ว่างานที่ถูกหน้าตาเป็นยังไง · พอถึงตอนท้าย คุณจะอ่านงานของ AI ออกว่าขาดอะไร

ขั้น 0 เตรียมร้าน bin/dev · git status สะอาด │ ขั้น 1 เปิดธีมใน CSS บอก daisyUI ว่าร้านเรามีธีม light กับ dark (3 บรรทัด) │ ขั้น 2 navbar + layout แถบเมนูบนสุดทุกหน้า (ไฟล์ใหม่ 1 + แก้ 3 บรรทัด) │ ขั้น 3 ปุ่มสลับธีม JavaScript ตัวแรก (Stimulus) · จำค่าไว้ในเบราว์เซอร์ (ไฟล์ใหม่ 1) │ └─ ลองกด → เห็นกับตาว่าทำไมต้อง semantic color ขั้น 4 หน้า posts เปลี่ยนสีตายตัวของ scaffold เป็นชิ้นของ daisyUI (6 ไฟล์ + pagination) │ ขั้น 5 หน้า categories แบบเดียวกัน + แปลไทย (6 ไฟล์ + controller 3 บรรทัด) │ ตรวจรับ checklist สไลด์หน้า 34 ทีละข้อ → prompts/02-styling.md → git commit

ไฟล์ที่แตะทั้งหมด 19 ไฟล์ ฟังดูเยอะ แต่เกือบทั้งหมดคือเปลี่ยนชื่อคลาส — logic ของ Ruby แทบไม่ขยับเลย · ของใหม่จริง ๆ มีแค่ขั้น 1–3 · ขั้น 4–5 คือฝึกมือ

ขั้น 0 — เตรียมร้านก่อนเริ่ม

เปิด terminal สองหน้าต่าง (หรือสองแท็บ) ที่โฟลเดอร์ของแอป · หน้าต่างแรกปล่อยร้านเปิดค้างไว้ หน้าต่างที่สองไว้พิมพ์คำสั่งอื่น:

# หน้าต่างที่ 1 — เปิดร้านพร้อมคนคอยสร้าง CSS (ต้องเป็น bin/dev ไม่ใช่ bin/rails server)
$ bin/dev

# หน้าต่างที่ 2 — ต้องไม่ขึ้นอะไรเลย = ไม่มีของทดลองค้างจากบท 127–128
$ git status --short

⚠️ ต้องเป็น bin/dev · ถ้าเปิดแค่ bin/rails server คลาสใหม่ที่คุณพิมพ์ (เช่น card) จะไม่มีในไฟล์ CSS เพราะไม่มีใครสร้างให้ใหม่ → หน้าเว็บไม่เปลี่ยน แล้วจะนึกว่าตัวเองพิมพ์ผิด (บท 128) · ถ้า git status --short ขึ้นชื่อไฟล์ ให้ git restore . ก่อน (= ทิ้งทุกการแก้ที่ยังไม่ได้ commit กลับไปเป็นแบบ commit ล่าสุด · บรรทัดที่ขึ้นต้นด้วย ?? คือไฟล์ใหม่ที่ git ไม่รู้จัก คำสั่งนี้ไม่ลบให้ ต้องลบไฟล์นั้นเอง)

ขั้น 1 — เปิดธีมใน CSS: บอก daisyUI ว่าร้านเรามีธีมอะไรบ้าง

ธีม (theme) คือชุดสีทั้งชุดของเว็บ: สีพื้น สีตัวอักษร สีปุ่มหลัก สีเตือน · daisyUI มีธีมสำเร็จรูปให้หลายสิบชุด แต่จะใส่ลงไฟล์ CSS เฉพาะธีมที่เรา "เปิด" · เปิดแล้วจะเลือกใช้ธีมไหน ก็แค่ติดป้าย data-theme="ชื่อธีม" ที่แท็ก <html>

เปิดไฟล์ app/assets/tailwind/application.css · ตอนนี้บรรทัดที่ 3 เป็นแบบนี้:

@plugin "./daisyui.mjs";

เปลี่ยนบรรทัดนั้นเป็นบล็อก 3 บรรทัด · ไฟล์ application.css ทั้งไฟล์หลังแก้:

@import "tailwindcss";
@source not "./daisyui{,*}.mjs";
@plugin "./daisyui.mjs" {
  themes: light --default, dark --prefersdark;
}

@theme {
  --font-sans: "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif;
}

⚠️ สไลด์หน้า 30 เขียน @plugin "daisyui" (ไม่มี ./ และ .mjs) · อย่าก็อปแบบนั้น — repo นี้ไม่มี Node ไม่มี package ชื่อ daisyui ให้หา มีแต่ไฟล์ daisyui.mjs วางไว้ในโฟลเดอร์ · ลองแล้วได้ (ผลจริง ย่อ path):

Error:
┌
│ Error: Can't resolve 'daisyui' in '…/app/assets/tailwind'
└
bin/rails aborted!
…

ธีมทำงานยังไง? daisyUI ไม่ได้เขียนสีลงในคลาสตรง ๆ แต่เขียนเป็นตัวแปรของ CSS (ชื่อขึ้นต้นด้วย --) แล้วให้แต่ละธีมตั้งค่าตัวแปรชุดเดียวกันต่างกัน · ตัวอย่างจากไฟล์ CSS ที่ build ได้จริง (ตัดมาบางส่วน):

/* app/assets/builds/tailwind.css — ไฟล์ที่ Tailwind สร้าง (บรรทัดเดียวยาว ๆ ตัดมาให้ดู) */
[data-theme=dark]{color-scheme:dark;--color-base-100:oklch(25.33% .016 252.42);--color-base-200:oklch(23.26% .014 253.1);…
.bg-base-100{background-color:var(--color-base-100)}

ถ้าเคยทำ บท 81 (design token ของ shadcn) นี่คือกลไกเดียวกันเป๊ะ — ที่นั่นใช้คลาส .dark ประกาศ --primary ซ้ำ · ที่นี่ใช้ป้าย data-theme แทน

เทียบให้เห็นภาพ

ธีมคือการเปลี่ยนธีมทั้งร้านด้วยป้ายเดียวที่หน้าประตู · ร้านเตรียมชุดของไว้แล้วสองชุด: "ชุดกลางวัน" (ผ้าปูขาว ไฟสว่าง) กับ "ชุดกลางคืน" (ผ้าปูเข้ม ไฟสลัว) · ป้ายหน้าประตู (<html data-theme>) เขียนว่า "กลางคืน" เมื่อไหร่ พนักงานทุกคนหยิบของชุดกลางคืนพร้อมกัน · คนจัดจานไม่ต้องรู้ว่าชุดไหนสีอะไร แค่หยิบ "ผ้าปูโต๊ะประจำร้าน" (bg-base-100) ตามป้าย · ส่วน themes: ใน CSS คือรายการชุดที่ร้านซื้อมาเก็บไว้ — ชุดที่ไม่ได้ซื้อ ต่อให้แขวนป้ายชื่อมันก็ไม่มีของให้หยิบ

ที่ไม่เหมือนกัน: ร้านจริงถ้าแขวนป้ายชื่อชุดที่ไม่มี พนักงานจะงงแล้วมาถาม · CSS ไม่ถามอะไรเลย มันใช้ชุดตั้งต้น (light) เงียบ ๆ ไม่มี error ที่ไหน (ภารกิจท้ายบทจะได้เห็นกับตา) · และป้ายนี้อยู่ในเบราว์เซอร์ของลูกค้าแต่ละคน ไม่ใช่ป้ายเดียวของทั้งร้าน — คนหนึ่งเลือกมืด อีกคนยังเห็นสว่างได้

ศัพท์ที่ใช้คุยต่อได้
theme · data-theme
ชุดสีทั้งชุดของเว็บ · data-theme คือ attribute บนแท็ก (ส่วนใหญ่ <html>) ที่บอก daisyUI ว่าใช้ชุดไหน · บอก AI ได้ว่า "switch daisyUI themes by setting data-theme on the html element, not by adding a dark class"
CSS variable (custom property)
ตัวแปรของ CSS ชื่อขึ้นต้นด้วย -- อ่านค่าด้วย var(--ชื่อ) · ธีมทุกแบบคือ "ตั้งตัวแปรชุดเดียวกันด้วยค่าต่างกัน" · ถาม senior ได้ว่า "สีนี้มาจาก CSS variable ของธีม หรือเขียนค่าตายตัวไว้"
--default · --prefersdark
ป้ายกำกับในรายการ themes: ของ daisyUI 5 · --default = ธีมเมื่อไม่มีป้าย · --prefersdark = ธีมเมื่อเครื่องตั้งโหมดมืด · บอก AI ได้ว่า "we use daisyUI 5 with Tailwind v4: configure themes inside @plugin in CSS, there is no tailwind.config.js"

บันทึกไฟล์ · bin/dev จะสร้าง CSS ใหม่ให้เอง (ดูในหน้าต่างที่ 1) · ถ้าอยากสั่งเองก็ได้:

$ bin/rails tailwindcss:build
≈ tailwindcss v4.3.3

/*! 🌼 daisyUI 5.7.50 */
Done in 92ms

ไฟล์ app/assets/builds/tailwind.css ที่ได้ใหม่ ต่างจากก่อนแก้มากแค่ไหน? (มีธีมใหม่เพิ่มเข้ามากี่ชุด?)

ไม่ต่างเลยสักไบต์ · ผมก็อปไฟล์ CSS ตอนก่อนแก้เก็บไว้ชื่อ before.css แล้วเทียบด้วย cmp (คำสั่งเทียบไฟล์สองไฟล์ · ไม่พิมพ์อะไรเลย = เหมือนกันทุกไบต์):

$ cmp app/assets/builds/tailwind.css before.css
$ grep -o '\[data-theme=[a-z]*\]' app/assets/builds/tailwind.css | sort | uniq -c
   1 [data-theme=dark]
   2 [data-theme=light]

เพราะค่าตั้งต้นของ daisyUI 5 ก็คือ light --default, dark --prefersdark อยู่แล้ว · บรรทัดที่เราเขียนจึงไม่ได้เปลี่ยนผล แต่เปลี่ยนความชัด: คนอ่านไฟล์ (และ AI) รู้ทันทีว่าร้านนี้เปิดธีมอะไร และมีที่ให้เติมธีมที่สาม (ภารกิจท้ายบท) · คำสั่ง grep … | sort | uniq -c = หาข้อความ [data-theme=…] ทุกจุดแล้วนับว่าแต่ละชื่อเจอกี่ครั้ง — ใช้ตรวจว่าธีมไหนอยู่ในไฟล์บ้าง

ขั้น 2 — navbar: แถบเมนูบนสุดที่อยู่ทุกหน้า

แถบเมนูต้องโผล่ทุกหน้า → ที่ของมันคือ layout (จานรองที่ห่อทุกหน้า) · แต่เราจะไม่เขียนลงใน layout ตรง ๆ ให้แยกเป็น partial ชื่อ shared/_navbar.html.erb ตามข้อกำหนดของสไลด์ — layout จะได้สั้นอ่านง่าย และหาเจอว่าเมนูอยู่ไฟล์ไหน

สร้างไฟล์ใหม่ app/views/shared/_navbar.html.erb (โฟลเดอร์ shared/ มีแล้วจากบท 126):

<%# locals: () -%>
<header class="navbar bg-base-100 shadow-sm">
  <div class="flex-1">
    <%= link_to "Wongnok Blog", posts_path, class: "btn btn-ghost text-lg sm:text-xl" %>
  </div>

  <nav class="flex-none flex items-center gap-1">
    <%= link_to "บทความ", posts_path, class: "btn btn-ghost btn-sm" %>
    <%= link_to "หมวด", categories_path, class: "btn btn-ghost btn-sm" %>
    <button type="button" class="btn btn-ghost btn-sm" aria-label="สลับธีมสว่าง/มืด"
            data-controller="theme" data-action="theme#toggle">🌓</button>
  </nav>
</header>

แล้วเปิด layout app/views/layouts/application.html.erb เลื่อนลงล่างสุด แก้ 3 จุด · ก่อนแก้:

  <body>
    <main class="container mx-auto mt-28 px-5 flex">
      <%= yield %>
    </main>
  </body>

หลังแก้:

  <body class="min-h-screen bg-base-200">
    <%= render "shared/navbar" %>

    <main class="container mx-auto px-4 py-8 flex">
      <%= yield %>
    </main>
  </body>

เปิด http://localhost:3000/posts จะเห็นแถบเมนูบนสุด · ดู HTML ที่เบราว์เซอร์ได้รับจริง (ผลจริง ตัดมาเฉพาะช่วงต้น <body>):

$ curl -s localhost:3000/posts
<!-- BEGIN app/views/layouts/application.html.erb
--><!DOCTYPE html>
<html>
…
  <body class="min-h-screen bg-base-200">
    <!-- BEGIN app/views/shared/_navbar.html.erb
--><header class="navbar bg-base-100 shadow-sm">
  <div class="flex-1">
    <a class="btn btn-ghost text-lg sm:text-xl" href="/posts">Wongnok Blog</a>
  </div>

  <nav class="flex-none flex items-center gap-1">
    <a class="btn btn-ghost btn-sm" href="/posts">บทความ</a>
    <a class="btn btn-ghost btn-sm" href="/categories">หมวด</a>
    <button type="button" class="btn btn-ghost btn-sm" aria-label="สลับธีมสว่าง/มืด"
            data-controller="theme" data-action="theme#toggle">🌓</button>
  </nav>
</header>
<!-- END app/views/shared/_navbar.html.erb -->

    <main class="container mx-auto px-4 py-8 flex">

ขั้น 3 — ปุ่มสลับธีม: JavaScript ตัวแรกของเราแบบ Rails (Stimulus)

ปุ่มต้องทำสองอย่าง: ① กดแล้วเปลี่ยนป้าย data-theme บน <html> ② จำไว้ว่าเลือกอะไร เปิดหน้าใหม่แล้วติดป้ายเดิมให้ · ทั้งสองอย่างเกิดในเบราว์เซอร์ ไม่เกี่ยวกับ server → ต้องใช้ JavaScript

ถ้าเป็น React (บท 73) เราจะเขียน onClick ในตัว component · ฝั่ง Rails ห้ามเขียน <script> ปนใน view (กฎในสไลด์ และ prompts/00-context.md) · Rails 8 ใช้ Stimulus แทน: เขียน JavaScript แยกไว้ในไฟล์ของมัน แล้วให้ HTML ติดป้ายบอกว่าจุดไหนต้องการ JavaScript ตัวไหน · ป้ายนั้นคือ data-controller="theme" ที่เราเขียนไว้ใน navbar แล้ว

เทียบให้เห็นภาพ

Stimulus controller คืออุปกรณ์บนโต๊ะที่ทำงานเองโดยไม่ต้องเรียกครัว — เหมือนสวิตช์หรี่ไฟข้างโต๊ะ ลูกค้ากดเองได้เลย ครัวไม่ต้องรู้ · data-controller="theme" คือป้ายเล็ก ๆ ที่ติดบนโต๊ะว่า "ตรงนี้มีอุปกรณ์ชื่อ theme" · ทุกครั้งที่พนักงานจัดโต๊ะ (หน้าเว็บโหลด) พนักงานเห็นป้ายแล้วติดตั้งอุปกรณ์ให้ (Stimulus เรียก connect()) · data-action="theme#toggle" คือป้ายที่ปุ่มบอกว่า "กดฉันแล้วให้อุปกรณ์ theme ทำงาน toggle"

ที่ไม่เหมือนกัน: สวิตช์ในร้านจริงหรี่ไฟได้แค่โต๊ะตัวเอง · ปุ่มของเราเปลี่ยนป้ายที่ <html> ซึ่งครอบทั้งหน้า — กดจุดเดียวสีเปลี่ยนทั้งเว็บ · และอุปกรณ์นี้ไม่ได้ถูกติดตั้งครั้งเดียวแล้วอยู่ตลอด: navbar อยู่ใน <body> ซึ่งถูกวางใหม่ทุกครั้งที่เปลี่ยนหน้า (กลไกละเอียดในบท 130) → connect() จึงรันทุกหน้า (ผมวัดแล้ว และนี่คือสิ่งที่เราต้องการพอดี)

สร้างไฟล์ใหม่ app/javascript/controllers/theme_controller.js · ชื่อไฟล์สำคัญ: theme_controller.js ↔ data-controller="theme" ต้องตรงกัน (เหมือนชื่อไฟล์ partial กับ render):

import { Controller } from "@hotwired/stimulus"

// ปุ่มสลับธีมสว่าง/มืด (อยู่ใน shared/_navbar.html.erb)
export default class extends Controller {
  // โหลดหน้า: ถ้าเคยเลือกธีมไว้ ใส่กลับให้
  connect() {
    const saved = localStorage.getItem("theme")
    if (saved) document.documentElement.dataset.theme = saved
  }

  // กดปุ่ม: สลับ light ↔ dark แล้วจำไว้
  toggle() {
    const html = document.documentElement
    const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches
    const current = html.dataset.theme || (systemDark ? "dark" : "light")
    const next = current === "dark" ? "light" : "dark"

    html.dataset.theme = next
    localStorage.setItem("theme", next)
  }
}

อ่านทีละบรรทัด:

ทำไมต้องถามเครื่องก่อน? ตอนยังไม่เคยกด <html> ไม่มีป้าย · ถ้าเครื่องตั้งโหมดมืด daisyUI จะโชว์มืด (เพราะ --prefersdark) · ถ้าโค้ดคิดเอาเองว่า "ไม่มีป้าย = สว่าง" กดครั้งแรกจะได้ "มืด" ซ้ำ = กดแล้วไม่เปลี่ยน · ผมทดสอบใน Chrome ที่ตั้งเครื่องเป็นโหมดมืด: ก่อนกดไม่มีป้ายแต่พื้นมืด → กดครั้งแรกได้ data-theme="light" ถูกต้อง

ไม่ต้องลงทะเบียนอะไรที่ไหน · config/importmap.rb มีบรรทัด pin_all_from "app/javascript/controllers", under: "controllers" อยู่แล้ว = "ไฟล์ทุกไฟล์ในโฟลเดอร์ controllers ใส่ในสมุดรายชื่อให้หมด" · ตรวจได้ (ผลจริง ตัดมาบรรทัดที่เกี่ยว):

$ bin/importmap json
…
    "controllers/hello_controller": "/assets/controllers/hello_controller-708796bd.js",
…
    "controllers/theme_controller": "/assets/controllers/theme_controller-5a29af2b.js"
…

-5a29af2b คือเลขรุ่นท้ายชื่อไฟล์จากบท 127 (ถ้าไฟล์ของคุณตรงกับข้างบนทุกตัวอักษร เลขจะตรงกัน) · hello_controller คือตัวอย่างที่ Rails ให้มาตั้งแต่สร้างแอป

ทำไมไม่มี disconnect() (method เก็บกวาดตอนอุปกรณ์ถูกถอด — คล้าย cleanup ของ useEffect)? เพราะเราไม่ได้ตั้งนาฬิกาหรือดักฟังอะไรเอง · การกดปุ่มผูกผ่าน data-action ซึ่ง Stimulus ถอดให้เอง · connect · disconnect · ป้ายแบบอื่น ๆ ของ Stimulus ละเอียดในบท 134 — วันนี้รู้แค่นี้พอใช้

เทียบให้เห็นภาพ

localStorage คือสมุดโน้ตเล่มเล็กที่ลูกค้าพกติดกระเป๋ามาเอง เขียนไว้ว่า "ร้านนี้ ชอบนั่งโซนไฟสลัว" · ทุกครั้งที่มาร้านนี้ พนักงานเปิดสมุดของลูกค้าดูแล้วจัดโต๊ะให้ตามนั้น (connect() อ่าน getItem) · ร้านไม่ต้องจดอะไรเลย

แล้วมันขัดกับเรื่อง "สมุดสต็อกเล่มที่สองบนโต๊ะลูกค้า" (บั๊ก sync — บท 122) ไหม? ไม่ขัด เพราะธีมเป็นความชอบของลูกค้าคนนั้น ไม่ใช่ข้อมูลของร้าน · ไม่มีใครอื่นต้องเห็นตรงกัน · บทความ หมวด คอมเมนต์ ยังต้องอยู่ที่ server ชุดเดียวเหมือนเดิม

ที่ไม่เหมือนกัน: สมุดนี้ผูกกับเบราว์เซอร์ + เว็บนี้ ไม่ได้ผูกกับตัวคน — เปิดจากมือถือ เปลี่ยนเบราว์เซอร์ หรือโหมดไม่ระบุตัวตน = สมุดเล่มใหม่ว่าง ๆ · และร้าน (server) มองไม่เห็นสมุดนี้เลย นั่นคือเหตุผลที่ curl ได้ <html> ไม่มีป้าย

ศัพท์ที่ใช้คุยต่อได้
Stimulus controller
ไฟล์ JavaScript ใน app/javascript/controllers/ ชื่อ xxx_controller.js ที่ผูกกับ HTML ผ่านป้าย · คนละอย่างกับ controller ของ Rails (ฝั่ง server) แม้ชื่อเหมือน · บอก AI ได้ว่า "write a Stimulus controller instead of an inline script tag; no npm, we use importmap"
data-controller · data-action
data-controller="theme" = จุดนี้ใช้ theme_controller.js · data-action="theme#toggle" = เมื่อเกิดเหตุการณ์ ให้เรียก method toggle · บนปุ่มไม่บอกเหตุการณ์ = คลิก (เขียนเต็มคือ click->theme#toggle) · ถาม senior ได้ว่า "ปุ่มนี้ผูก action กับ controller ตัวไหน"
connect()
lifecycle callback (method ที่ระบบเรียกเองตามจังหวะชีวิตของ element) ของ Stimulus · รันเมื่อ element ที่มีป้ายโผล่บนหน้า · ถาม AI ได้ว่า "is it safe to read localStorage in connect(), or will it run more than once?" (คำตอบ: รันทุกครั้งที่ element ใหม่โผล่ ต้องเขียนให้รันซ้ำได้ — ของเราตั้งค่าเดิมซ้ำ ไม่เป็นไร)
localStorage · prefers-color-scheme
localStorage = ที่เก็บข้อความเล็ก ๆ ในเบราว์เซอร์ แยกตามเว็บ อยู่ข้ามการ refresh และปิดเปิดเบราว์เซอร์ · prefers-color-scheme = ค่าโหมดสว่าง/มืดที่ผู้ใช้ตั้งในเครื่อง · บอก AI ได้ว่า "persist the chosen theme in localStorage and fall back to prefers-color-scheme"

บันทึก แล้วกลับไปหน้า /posts กด refresh หนึ่งครั้ง (ให้ไฟล์ JS ใหม่โหลด) แล้วกด 🌓

ตอนนี้เราแก้แค่ navbar กับ layout · หน้า /posts ยังเป็นคลาสสีตายตัวของ scaffold (bg-blue-600 · text-gray-700 · bg-gray-100) อยู่เต็มไปหมด

เครื่องตั้งโหมดสว่าง · กด 🌓 หนึ่งครั้ง · อะไรเปลี่ยนเป็นมืด อะไรค้างสีเดิม?

ผมเปิด Chrome จริง วัดสีของทุก element ที่มองเห็น ก่อนและหลังกด แล้วนับตัวที่สีไม่เปลี่ยน · (getComputedStyle = คำสั่ง JavaScript ที่ถามเบราว์เซอร์ว่า "แท็กนี้ตอนนี้ใช้สีอะไรจริง ๆ" · element = แท็กหนึ่งตัวบนหน้า)

before theme: (none)
after theme: dark ls: dark
checked 78 unchanged 35
 - bg a.shrink-0.rounded-md.px-3.5.py-2.5.bg-blue-600…  [เขียนบทความใหม่] oklch(0.546 0.245 262.881)
 - color span.font-medium.text-blue-700 [เครื่องดื่ม] oklch(0.488 0.243 264.376)
 - color p.mt-2.text-gray-700 [นมสีชมพูจากน้ำแดง หว] oklch(0.373 0.034 259.733)
 - bg a.rounded-md.px-3.py-2.bg-gray-100…  [ถัดไป →] oklch(0.967 0.003 264.542)
 …
body oklch(0.98 0 0) -> oklch(0.2326 0.014 253.1)

อ่านผลยังไง: บรรทัดที่ขึ้นต้นด้วย - คือตัวที่ค้าง · bg = สีพื้นค้าง / color = สีตัวอักษรค้าง · ต่อด้วย ชื่อแท็ก.คลาส.คลาส (เช่น a.shrink-0… = แท็ก <a> ที่มีคลาส shrink-0 ฯลฯ) · […] = ข้อความในแท็กนั้น · ท้ายสุด = สีที่ค้างอยู่ · บรรทัดสุดท้าย = สีพื้นของ <body> ก่อน -> หลัง

เปลี่ยน: พื้นทั้งหน้า (bg-base-200) · navbar (bg-base-100) · หัวข้อ "บทความทั้งหมด" และชื่อบทความ — เพราะไม่ได้ระบุสีเลย จึงใช้สีตัวอักษรของธีมตาม <body> · localStorage จดว่า dark แล้ว (ls: dark)

ค้าง 35 จุดจาก 78: ปุ่ม "เขียนบทความใหม่" ยังน้ำเงินตัวขาว · ชื่อหมวดยังน้ำเงิน · คำโปรยบทความ text-gray-700 เป็นสีเทาเข้มบนพื้นมืด — แทบอ่านไม่ออก · ปุ่ม "ถัดไป" เป็นกล่องเทาอ่อนสว่างโร่กลางพื้นมืด · (นอกจาก 35 จุดนี้ ซึ่งนับแค่สีพื้นกับสีตัวอักษร) เส้นคั่น border-gray-200 ก็ยังเป็นสีเทาอ่อนเกือบขาวเหมือนเดิม จึงเห็นเป็นเส้นขาวชัดบนพื้นมืด

นี่คือข้อ "AI พลาดประจำ" ข้อ 3 ของสไลด์หน้า 34 ที่เห็นกับตา: สีที่เขียนชื่อสีตรง ๆ ไม่รู้จักธีม — ขั้น 4–5 คือไล่เปลี่ยนให้หมด

ขั้น 4 — หน้า posts: เปลี่ยนสีตายตัวเป็นชิ้นของ daisyUI

หลักเดียวของขั้นนี้: ทุกที่ที่เขียนชื่อสีตรง ๆ (blue · gray · red · green) ให้เปลี่ยนเป็นชิ้นของ daisyUI หรือสีตามหน้าที่ · ตอนนี้ใน app/views มีบรรทัดแบบนั้นอยู่ 43 บรรทัด (นับจริงด้วย grep ที่จะใช้ตอนตรวจรับ) · จบขั้น 5 ต้องเหลือ 0

เทียบให้เห็นภาพ

daisyUI คือชุดจานชามสำเร็จรูปที่เข้าชุดกัน: จานการ์ด (card) · ป้ายหมวด (badge) · ปุ่ม (btn) — หยิบมาใช้ได้เลยไม่ต้องปั้นเอง · และทุกชิ้นสั่งสีด้วย"สีประจำร้าน": "สีหลักของร้าน" (primary) · "สีผ้าปูโต๊ะ" (base-100) · "สีตัวหนังสือบนผ้าปู" (base-content) · เปลี่ยนธีมร้านเมื่อไหร่ "สีหลักของร้าน" ก็เปลี่ยนตาม · ส่วน bg-blue-600 คือการสั่ง"สีน้ำเงินเบอร์ 600" ตรง ๆ — ร้านเปลี่ยนธีมกี่รอบ มันก็น้ำเงินเหมือนเดิม

ที่ไม่เหมือนกัน: ชุดจานจริงต้องเลือกระหว่างชุดสำเร็จกับปั้นเอง · daisyUI ผสมได้ — ข้างในมันก็คือคลาส Tailwind ชุดเดิม เขียน btn btn-primary w-full sm:w-auto ได้สบาย (ชิ้นสำเร็จ + ปรับขนาดเอง) · ที่ห้ามคือผสมสีตายตัว ไม่ใช่ห้ามผสมคลาส Tailwind

ศัพท์ที่ใช้คุยต่อได้
component class · modifier
component class = คลาสชิ้นสำเร็จของ daisyUI (btn · card · badge · alert · join) · modifier = คลาสที่ต่อท้ายเพื่อปรับชิ้นนั้น (btn-primary · btn-sm · btn-ghost) ต้องใช้คู่กับตัวหลักเสมอ · บอก AI ได้ว่า "use daisyUI component classes (card, badge, btn) with modifiers; keep Tailwind utilities only for layout and spacing"
semantic color
สีที่ตั้งชื่อตามหน้าที่ (primary · error · base-100 · base-content) แทนชื่อสี · เปลี่ยนตามธีมอัตโนมัติ · ใส่ใน prompt ได้ว่า "use daisyUI semantic colors only (primary, base-100, base-content); never hard-coded palette colors like bg-blue-600"

ไฟล์ app/views/posts/_post.html.erb — แทนทั้งไฟล์

<%# locals: (post:, full: false) -%>
<article id="<%= dom_id post %>" class="card bg-base-100 shadow-sm">
  <div class="card-body">
    <p class="flex flex-wrap items-center gap-2 text-sm text-base-content/70">
      <span class="badge badge-primary"><%= post.category.name %></span>
      <%= time_ago_in_words(post.created_at) %> ago
    </p>

    <% if full %>
      <h1 class="card-title text-3xl"><%= post.title %></h1>
      <p class="leading-relaxed whitespace-pre-line"><%= post.description %></p>
    <% else %>
      <h2 class="card-title">
        <%= link_to post.title, post, class: "link link-hover" %>
      </h2>
      <p class="text-base-content/80"><%= truncate(post.description, length: 80, separator: " ") %></p>
    <% end %>
  </div>
</article>

Ruby/ERB ทุกบรรทัดเหมือนบท 126 (ป้าย locals · dom_id · if full) · เปลี่ยนแค่โครงกับคลาส: ห่อเนื้อหาด้วย <div class="card-body"> เพิ่มหนึ่งชั้น · ตัด · หน้าเวลาออก (badge แยกตัวเองชัดแล้ว) · ตัด mt-… ออก (card-body เว้นระยะให้เอง)

คลาสทำอะไรแทนของเดิม
card · card-bodyจานการ์ด: มุมโค้ง · card-body = ไส้การ์ดที่มีระยะขอบด้านในและเว้นระยะระหว่างบรรทัดให้py-5 border-b border-gray-200
card-titleหัวข้อในการ์ด ตัวหนา ขนาดพอดี · หน้า show ต่อ text-3xl ให้ใหญ่ขึ้นtext-xl font-semibold · text-3xl font-bold
leading-relaxed whitespace-pre-line(ของเดิมบท 126) ระยะห่างบรรทัดหลวมขึ้น · ขึ้นบรรทัดใหม่ตามที่ผู้เขียนกด Enter ในเนื้อหา(ไม่เปลี่ยน)
badge · badge-primaryป้ายเล็กมุมมน · พื้นสีหลักของธีม ตัวอักษรสีคู่กันfont-medium text-blue-700
link · link-hoverลิงก์ของ daisyUI · link-hover = ขีดเส้นใต้เฉพาะตอนเอาเมาส์ชี้hover:underline
text-base-content/70 · /80สีตัวหนังสือของธีม จางลงเหลือ 70% / 80% (/70 = ความทึบ) · ใช้กับข้อความรอง (บรรทัดเวลา · คำโปรย)text-gray-500 · text-gray-700
flex flex-wrap items-center gap-2เรียงป้ายกับเวลาในแถว จอแคบให้ขึ้นบรรทัดใหม่ได้ เว้นช่องไฟ 2(ใหม่)

ไฟล์ app/views/posts/index.html.erb — แทนทั้งไฟล์

<% content_for :title, "บทความทั้งหมด" %>

<div class="w-full max-w-3xl mx-auto">
  <% if notice.present? %>
    <p class="alert alert-success mb-5" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center gap-4 mb-6">
    <h1 class="font-bold text-3xl sm:text-4xl">บทความทั้งหมด</h1>
    <%= link_to "เขียนบทความใหม่", new_post_path, class: "btn btn-primary shrink-0" %>
  </div>

  <div id="posts" class="space-y-4">
    <%= render(@posts) || tag.p("ไม่พบบทความ", class: "text-center text-base-content/60 my-10") %>
  </div>

  <%= render "shared/pagination", pagy: @pagy %>
</div>

เปลี่ยน 4 จุดจากบท 126:

ไฟล์ app/views/posts/show.html.erb — แทนทั้งไฟล์

<% content_for :title, @post.title %>

<div class="w-full max-w-3xl mx-auto">
  <% if notice.present? %>
    <p class="alert alert-success mb-5" id="notice"><%= notice %></p>
  <% end %>

  <nav class="breadcrumbs text-sm mb-2" aria-label="breadcrumb">
    <ul>
      <li><%= link_to "บทความทั้งหมด", posts_path %></li>
      <li><%= link_to @post.category.name, @post.category %></li>
      <li><%= @post.title %></li>
    </ul>
  </nav>

  <%= render @post, full: true %>

  <div class="mt-6 flex flex-col sm:flex-row gap-2">
    <%= link_to "แก้ไข", edit_post_path(@post), class: "btn" %>
    <%= link_to "กลับไปหน้ารวม", posts_path, class: "btn btn-ghost" %>
    <%= button_to "ลบบทความ", @post, method: :delete, class: "btn btn-error w-full", data: { turbo_confirm: "ลบบทความนี้จริงไหม?" } %>
  </div>
</div>

HTML ที่ได้จริงของ breadcrumb ในหน้า /posts/1 (ผลจาก curl -s localhost:3000/posts/1 ตัดมาช่วงนี้):

  <nav class="breadcrumbs text-sm mb-2" aria-label="breadcrumb">
    <ul>
      <li><a href="/posts">บทความทั้งหมด</a></li>
      <li><a href="/categories/1">อาหารไทย</a></li>
      <li>ต้มยำกุ้งน้ำข้น</li>
    </ul>
  </nav>

ไม่มี › ใน HTML เลย — ลูกศรที่เห็นบนจอมาจาก CSS ของ daisyUI ล้วน ๆ

ไฟล์ app/views/shared/_pagination.html.erb — แทนทั้งไฟล์

<%# locals: (pagy:) -%>
<nav class="mt-8 flex justify-center" aria-label="เปลี่ยนหน้า">
  <div class="join">
    <% if pagy.previous %>
      <%= link_to "← ก่อนหน้า", pagy.page_url(:previous), class: "join-item btn btn-sm" %>
    <% else %>
      <span class="join-item btn btn-sm btn-disabled">← ก่อนหน้า</span>
    <% end %>

    <span class="join-item btn btn-sm pointer-events-none">หน้า <%= pagy.page %> / <%= pagy.last %></span>

    <% if pagy.next %>
      <%= link_to "ถัดไป →", pagy.page_url(:next), class: "join-item btn btn-sm" %>
    <% else %>
      <span class="join-item btn btn-sm btn-disabled">ถัดไป →</span>
    <% end %>
  </div>
</nav>

ไฟล์ฟอร์ม app/views/posts/_form.html.erb — แทนทั้งไฟล์

<%= form_with(model: post, class: "contents") do |form| %>
  <% if post.errors.any? %>
    <div id="error_explanation" role="alert" class="alert alert-error mt-3">
      <div>
        <h2 class="font-bold">บันทึกไม่ได้ มี <%= post.errors.count %> จุดที่ต้องแก้:</h2>

        <ul class="list-disc ml-6">
          <% post.errors.each do |error| %>
            <li><%= error.full_message %></li>
          <% end %>
        </ul>
      </div>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :title, "หัวข้อ", class: "block font-medium" %>
    <%= form.text_field :title, class: ["input w-full mt-2", {"input-error": post.errors[:title].any?}] %>
  </div>

  <div class="my-5">
    <%= form.label :description, "เนื้อหา", class: "block font-medium" %>
    <%= form.textarea :description, rows: 4, class: ["textarea w-full mt-2", {"textarea-error": post.errors[:description].any?}] %>
  </div>

  <div class="my-5">
    <%= form.label :category_id, "หมวด", class: "block font-medium" %>
    <%= form.collection_select :category_id, Category.all, :id, :name, { prompt: "เลือกหมวด" }, class: ["select w-full mt-2", {"select-error": post.errors[:category].any?}] %>
  </div>

  <div class="inline">
    <%= form.submit "บันทึก", class: "btn btn-primary w-full sm:w-auto" %>
  </div>
<% end %>

ไฟล์ new.html.erb กับ edit.html.erb ของ posts — แทนทั้งไฟล์

⚠️ ตั้งแต่ตรงนี้ไป โค้ดหลายก้อนมีบรรทัดแรกเป็น <%# app/views/… %> — ผมเขียนบอกชื่อไฟล์ให้อ่านง่าย ไม่ต้องพิมพ์ลงไฟล์ · เริ่มพิมพ์จากบรรทัดที่สอง (ถ้าเผลอพิมพ์ไปก็ไม่พัง เพราะ <%# %> คือคอมเมนต์ แต่ไฟล์จะไม่ตรงกับบท 130)

<%# app/views/posts/new.html.erb %>
<% content_for :title, "เขียนบทความใหม่" %>

<div class="md:w-2/3 w-full mx-auto">
  <h1 class="font-bold text-4xl">เขียนบทความใหม่</h1>

  <%= render "form", post: @post %>

  <%= link_to "กลับไปหน้ารวม", posts_path, class: "btn btn-ghost w-full sm:w-auto mt-2 sm:mt-0 sm:ml-2" %>
</div>
<%# app/views/posts/edit.html.erb %>
<% content_for :title, "แก้ไขบทความ" %>

<div class="md:w-2/3 w-full mx-auto">
  <h1 class="font-bold text-4xl">แก้ไขบทความ</h1>

  <%= render "form", post: @post %>

  <%= link_to "ดูบทความนี้", @post, class: "btn btn-ghost w-full sm:w-auto mt-2 sm:mt-0 sm:ml-2" %>
  <%= link_to "กลับไปหน้ารวม", posts_path, class: "btn btn-ghost w-full sm:w-auto mt-2 sm:mt-0 sm:ml-2" %>
</div>

เปลี่ยนจากบท 126 แค่สองอย่าง: เพิ่ม mx-auto (จัดกล่องไว้กลางจอเหมือน index/show) · ลิงก์เป็น btn btn-ghost (แทนกล่องเทา bg-gray-100) · ส่วน md:w-2/3 w-full เป็นของเดิม = จอเล็กกว้างเต็ม · จอตั้งแต่ขนาด md ขึ้นไปกว้างสองในสามของจอ · mt-2 sm:mt-0 sm:ml-2 = จอแคบเว้นด้านบน (ปุ่มเรียงลง) · จอกว้างเว้นด้านซ้ายแทน (ปุ่มเรียงข้างกัน)

ขั้น 5 — หน้า categories: ทำแบบเดียวกัน + แปลไทย

หน้า /categories ยังเป็น scaffold ภาษาอังกฤษล้วน · ถ้าไม่แก้ เกณฑ์ "สลับธีมแล้วเปลี่ยนทั้งเว็บ" จะไม่ผ่าน (กดเมนู "หมวด" แล้วเจอปุ่มน้ำเงินตายตัว) · คลาสเกือบทั้งหมดเจอในขั้น 4 แล้ว (ตัวใหม่มีแค่ divide-y อธิบายใต้โค้ดแรก) · พิมพ์ตามได้เลย

app/views/categories/index.html.erb — แทนทั้งไฟล์

<% content_for :title, "หมวดทั้งหมด" %>

<div class="w-full max-w-3xl mx-auto">
  <% if notice.present? %>
    <p class="alert alert-success mb-5" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center gap-4">
    <h1 class="font-bold text-3xl sm:text-4xl">หมวดทั้งหมด</h1>
    <%= link_to "เพิ่มหมวด", new_category_path, class: "btn btn-primary shrink-0" %>
  </div>

  <div id="categories" class="min-w-full divide-y divide-base-300">
    <% if @categories.any? %>
      <% @categories.each do |category| %>
        <div class="flex flex-col sm:flex-row justify-between items-center pb-5 sm:pb-0">
          <%= render category %>
          <div class="w-full sm:w-auto flex flex-col sm:flex-row gap-2">
            <%= link_to "ดู", category, class: "btn btn-sm" %>
            <%= link_to "แก้ไข", edit_category_path(category), class: "btn btn-sm" %>
            <%= button_to "ลบ", category, method: :delete, class: "btn btn-sm btn-error w-full", data: { turbo_confirm: "ลบหมวดนี้จริงไหม?" } %>
          </div>
        </div>
      <% end %>
    <% else %>
      <p class="text-center text-base-content/60 my-10">ยังไม่มีหมวด</p>
    <% end %>
  </div>
</div>

divide-y divide-base-300 = เส้นคั่นระหว่างแถว ใช้สีเส้นระดับ 300 ของธีม (แทน divide-gray-200) · gap-2 แทน space-x-2 space-y-2 ของ scaffold ที่ทำให้ปุ่มเหลื่อมกันบนมือถือ

app/views/categories/show.html.erb — แทนทั้งไฟล์

<% content_for :title, @category.name %>

<div class="md:w-2/3 w-full mx-auto">
  <% if notice.present? %>
    <p class="alert alert-success mb-5" id="notice"><%= notice %></p>
  <% end %>

  <h1 class="font-bold text-4xl">หมวด: <%= @category.name %></h1>

  <%= render @category %>

  <div class="flex flex-col sm:flex-row gap-2">
    <%= link_to "แก้ไขหมวดนี้", edit_category_path(@category), class: "btn" %>
    <%= link_to "กลับไปหน้าหมวด", categories_path, class: "btn btn-ghost" %>
    <%= button_to "ลบหมวดนี้", @category, method: :delete, class: "btn btn-error w-full", data: { turbo_confirm: "ลบหมวดนี้จริงไหม?" } %>
  </div>
</div>

new.html.erb · edit.html.erb · _form.html.erb · _category.html.erb ของ categories

<%# app/views/categories/new.html.erb %>
<% content_for :title, "เพิ่มหมวด" %>

<div class="md:w-2/3 w-full mx-auto">
  <h1 class="font-bold text-4xl">เพิ่มหมวด</h1>

  <%= render "form", category: @category %>

  <%= link_to "กลับไปหน้าหมวด", categories_path, class: "btn btn-ghost w-full sm:w-auto mt-2 sm:mt-0 sm:ml-2" %>
</div>
<%# app/views/categories/edit.html.erb %>
<% content_for :title, "แก้ไขหมวด" %>

<div class="md:w-2/3 w-full mx-auto">
  <h1 class="font-bold text-4xl">แก้ไขหมวด</h1>

  <%= render "form", category: @category %>

  <%= link_to "ดูหมวดนี้", @category, class: "btn btn-ghost w-full sm:w-auto mt-2 sm:mt-0 sm:ml-2" %>
  <%= link_to "กลับไปหน้าหมวด", categories_path, class: "btn btn-ghost w-full sm:w-auto mt-2 sm:mt-0 sm:ml-2" %>
</div>
<%# app/views/categories/_form.html.erb %>
<%= form_with(model: category, class: "contents") do |form| %>
  <% if category.errors.any? %>
    <div id="error_explanation" role="alert" class="alert alert-error mt-3">
      <div>
        <h2 class="font-bold">บันทึกไม่ได้ มี <%= category.errors.count %> จุดที่ต้องแก้:</h2>

        <ul class="list-disc ml-6">
          <% category.errors.each do |error| %>
            <li><%= error.full_message %></li>
          <% end %>
        </ul>
      </div>
    </div>
  <% end %>

  <div class="my-5">
    <%= form.label :name, "ชื่อหมวด", class: "block font-medium" %>
    <%= form.text_field :name, class: ["input w-full mt-2", {"input-error": category.errors[:name].any?}] %>
  </div>

  <div class="my-5">
    <%= form.label :slug, "slug (ภาษาอังกฤษตัวเล็ก a-z 0-9 และขีด)", class: "block font-medium" %>
    <%= form.text_field :slug, class: ["input w-full mt-2", {"input-error": category.errors[:slug].any?}] %>
  </div>

  <div class="inline">
    <%= form.submit "บันทึก", class: "btn btn-primary w-full sm:w-auto" %>
  </div>
<% end %>

บรรทัดแรกที่บอกชื่อไฟล์ ไม่ต้องพิมพ์ เหมือนเดิม · _category.html.erb แก้แค่ป้ายสองบรรทัด (Name: → ชื่อหมวด: · Slug: → slug:) ส่วนอื่นไม่แตะ:

    <strong class="block font-medium mb-1">ชื่อหมวด:</strong>
…
    <strong class="block font-medium mb-1">slug:</strong>

สุดท้าย ข้อความแจ้งหลังบันทึกใน app/controllers/categories_controller.rb แปลเป็นไทยให้ตรงกับ posts (แก้แค่ข้อความในเครื่องหมายคำพูด 3 จุด):

        format.html { redirect_to @category, notice: "สร้างหมวดแล้ว" }
…
        format.html { redirect_to @category, notice: "บันทึกการแก้ไขแล้ว", status: :see_other }
…
      format.html { redirect_to categories_path, notice: "ลบหมวดแล้ว", status: :see_other }

(เดิมคือ "Category was successfully created." · "…updated." · "…destroyed." · ข้อนี้ไม่แก้ก็ไม่พังอะไร แต่แก้เพื่อให้โค้ดตรงกับบทถัดไป)

ตรวจรับตาม checklist ของสไลด์หน้า 34 — ทีละข้อ

อย่าตรวจด้วย "ดูแล้วน่าจะได้" · ทุกข้อมีวิธีที่วัดได้ · ผมตรวจทุกข้อด้วย Chrome จริงผ่านสคริปต์ (วัดสีทุก element แทนตา) · คุณตรวจเองได้ด้วย DevTools ของ Chrome (กด F12 หรือ Cmd+Option+I บน Mac · แท็บ Console = ช่องพิมพ์ JavaScript สั่งหน้านั้นได้ตรง ๆ)

ข้อ 1 · สลับธีมแล้วเปลี่ยนทั้งเว็บ ไม่ใช่แค่บางส่วน

ตรวจด้วยตาทีละหน้า (posts · show · new · categories) แล้วยืนยันด้วยคำสั่งค้นหาสีตายตัวที่เหลือ:

$ grep -rnE "(bg|text|border|divide)-(gray|blue|red|green)-" app/views
$

ไม่มีผลลัพธ์ = ไม่เหลือสีตายตัวสักบรรทัด (ก่อนเริ่ม Lab คำสั่งเดียวกันได้ 43 บรรทัด) · -E = ใช้รูปแบบค้นหาแบบมี ( | ) ได้ · -rn = ค้นทุกไฟล์ในโฟลเดอร์ บอกเลขบรรทัด

ผลวัดจริงใน Chrome (เครื่องตั้งโหมดสว่าง · กด 🌓 ครั้งเดียว · นับ element ที่สีพื้นหรือสีตัวอักษรไม่เปลี่ยน):

หน้าelement ที่ตรวจไม่เปลี่ยนตามธีม
/posts890
/posts/1282 — ปุ่ม "ลบบทความ" (พื้น + ตัวอักษร)
/posts/new230
/categories6910 — ปุ่ม "ลบ" 5 ปุ่ม × (พื้น + ตัวอักษร)

⚠️ ปุ่มลบสีแดงไม่เปลี่ยน — ไม่ใช่บั๊กของคุณ · ผมวัดสี btn-error ได้ oklch(0.71 0.194 13.428) ทั้งธีม light และ dark · แปลว่าธีมสองชุดนี้ตั้ง "สีอันตราย" เป็นค่าเดียวกัน (กล่อง alert-success สีเขียวก็เหมือนกัน) · มันยังเป็น semantic color อยู่ — ถ้าเปลี่ยนเป็นธีมอื่น (เช่น cupcake ในภารกิจ) สีจะเปลี่ยนตาม · ตัวที่ต้องสงสัยคือคลาสที่มีชื่อสีอยู่ในตัว ไม่ใช่ตัวที่สีบังเอิญเท่าเดิม

ตรวจเองใน Console: พิมพ์ getComputedStyle(document.body).backgroundColor (= "พื้นของ <body> ตอนนี้สีอะไรจริง ๆ") แล้วกด Enter · ก่อนกด 🌓 ได้ oklch(0.98 0 0) · หลังกดได้ oklch(0.2326 0.014 253.1) (ค่าของผม — ถ้าไฟล์ตรงกันของคุณก็ควรได้เท่านี้)

ข้อ 2 · refresh แล้วธีมที่เลือกยังอยู่

กดให้เป็นมืด แล้วกด refresh · ผลวัดจริงใน Chrome (reload แบบไม่ใช้ของในแคช):

before: dark dark
after reload: dark oklch(0.2326 0.014 253.1) <html data-theme="dark">…

📝 ข้อจำกัดที่ควรรู้: ตอนกด refresh เบราว์เซอร์ได้ HTML ที่ยังไม่มีป้ายก่อน แล้ว JavaScript ค่อยติดป้ายตามมา · บนเครื่องเร็วเสร็จก่อนวาดจอ แต่เครื่องช้าอาจเห็นสีสว่างแวบหนึ่งก่อนเป็นมืด · วิธีแก้จริงคือให้ server ติดป้ายมาตั้งแต่ HTML แรก (เก็บธีมใน cookie — ของที่ server อ่านได้) ซึ่งเกินขอบ Lab นี้

ข้อ 3 · รัน assets:precompile แล้วสียังอยู่ครบ

สไลด์บอกว่า "ข้อนี้คือข้อที่คนลืมตรวจ" · assets:precompile คือการซ้อมเตรียมไฟล์แบบขึ้นเครื่องจริงบนเครื่องเรา (บท 127) · ถ้าคลาสไหนหลุดจาก CSS ตอนซ้อม ก็จะหลุดบนเครื่องจริงด้วย · ผลจริง:

$ bin/rails assets:precompile
≈ tailwindcss v4.3.3

/*! 🌼 daisyUI 5.7.50 */
Done in 92ms
Warning: You are precompiling assets in development. Rails will not serve any changed assets until you delete public/assets/.manifest.json

บรรทัด Warning: แปลว่า "คุณกำลัง precompile บนเครื่อง development · จากนี้ Rails จะไม่เสิร์ฟไฟล์ที่คุณแก้ใหม่ จนกว่าจะลบ public/assets/.manifest.json" — จำไว้ เดี๋ยวต้องเก็บกวาด ·
ได้ไฟล์ CSS ติดเลขรุ่นใน public/assets/ · ตรวจว่าคลาสหลักที่เราใช้อยู่ในนั้นครบ (ผลจริง):

$ ls public/assets/ | grep tailwind
tailwind-1c1af83f.css
$ grep -o 'navbar\|card-body\|badge-primary\|btn-primary\|breadcrumbs\|join-item' public/assets/tailwind-*.css | sort | uniq -c
   1 badge-primary
  28 breadcrumbs
   1 btn-primary
   8 card-body
   4 join-item
   2 navbar
$ grep -o '\[data-theme=[a-z]*\]' public/assets/tailwind-*.css | sort | uniq -c
   1 [data-theme=dark]
   2 [data-theme=light]

เสร็จแล้วต้องลบของที่ซ้อมทิ้งทันที:

$ bin/rails assets:clobber
$

⚠️ สองกับดักของคู่นี้ ① ลืม clobber → Rails เสิร์ฟ CSS ตัวที่ซ้อมไว้ใน public/assets ไปเรื่อย ๆ แก้คลาสแล้วไม่เห็นผล (ข้อความ Warning: ข้างบนเตือนตรง ๆ) · ② clobber ลบ app/assets/builds/tailwind.css ไปด้วย · ถ้าตอนนั้นเปิดแค่ bin/rails server หน้าเว็บจะไม่มี style เลย — ผมดู HTML หลัง clobber แล้ว บรรทัด <link rel="stylesheet" href="/assets/tailwind-1c1af83f.css" …> หายไปทั้งบรรทัด เหลือแค่ application-8b441ae0.css · แก้: ปล่อยให้ bin/dev สร้างใหม่ (แก้ไฟล์ view อะไรก็ได้หนึ่งครั้ง) หรือสั่ง bin/rails tailwindcss:build

🔎 ถ้าคุณลอง grep bg-blue-600 ในไฟล์ CSS แล้วเจอ อย่าตกใจ · มันมาจากไฟล์ prompts/02-styling.md ที่เราจะสร้างข้างล่าง (มีประโยค "ไม่ใช่ bg-blue-600") · ผมพิสูจน์แล้ว: ย้ายไฟล์ .md ออกชั่วคราว build ใหม่ → นับได้ 0 · ใส่คืน → นับได้ 1 · นี่คือหลักเดียวกับกับดักต่อ string ในบท 128: Tailwind อ่านข้อความในไฟล์ของโปรเจกต์ ไม่ได้รันโค้ด — เห็นชื่อคลาสที่ไหนก็สร้างให้ ไม่สนว่าเป็นไฟล์อะไร

ศัพท์ที่ใช้คุยต่อได้
assets:precompile · assets:clobber
precompile = เตรียม CSS/JS แบบ production ไว้ใน public/assets · clobber = ลบของที่ precompile ไว้ (และ CSS ที่ build ไว้) ทิ้ง · บอก AI ได้ว่า "after changing views, verify with bin/rails assets:precompile, grep the class in public/assets, then run assets:clobber"
production build
ไฟล์ชุดที่จะขึ้นเครื่องจริง (ไม่มีคนคอย watch · ติดเลขรุ่นแล้ว) · การซ้อม precompile บนเครื่องเราคือการดูของชุดนี้ก่อนลูกค้าเห็น · ถาม senior ได้ว่า "CI (ระบบตรวจอัตโนมัติ) ของเรารัน assets:precompile ก่อน deploy ไหม ถ้าคลาสหายจะรู้ตอนไหน"

ข้อ 4 · ไม่มีไฟล์ใหม่ในโปรเจกต์นอกจาก .erb

ดูไฟล์ใหม่ทั้งหมดก่อน commit (ผลจริง · ?? = ไฟล์ใหม่ที่ git ยังไม่รู้จัก · M = ไฟล์เดิมที่ถูกแก้):

$ git status --short
 M app/assets/tailwind/application.css
 M app/controllers/categories_controller.rb
 M app/views/categories/_category.html.erb
 M app/views/categories/_form.html.erb
 M app/views/categories/edit.html.erb
 M app/views/categories/index.html.erb
 M app/views/categories/new.html.erb
 M app/views/categories/show.html.erb
 M app/views/layouts/application.html.erb
 M app/views/posts/_form.html.erb
 M app/views/posts/_post.html.erb
 M app/views/posts/edit.html.erb
 M app/views/posts/index.html.erb
 M app/views/posts/new.html.erb
 M app/views/posts/show.html.erb
 M app/views/shared/_pagination.html.erb
?? app/javascript/controllers/theme_controller.js
?? app/views/shared/_navbar.html.erb
?? prompts/02-styling.md

ไฟล์ใหม่ 3 ไฟล์ · มี .js กับ .md ด้วย → ตามตัวอักษร ข้อนี้ไม่ผ่าน · แต่ลองอ่านเจตนา: ข้อนี้อยู่คู่กับข้อ "ห้าม" ที่บอกว่าห้ามสร้าง tailwind.config.js · ห้าม npm install / package.json · ห้ามไฟล์ .css ใหม่ — มันตั้งใจจับ AI ที่ไปสร้างไฟล์ตั้งค่าแบบ Tailwind v3 หรือ Node ตรวจตรงนั้นได้ (ผลจริง):

$ ls tailwind.config.js package.json node_modules
ls: node_modules: No such file or directory
ls: package.json: No such file or directory
ls: tailwind.config.js: No such file or directory

ไม่มีสักไฟล์ = ผ่านตามเจตนา · ส่วน theme_controller.js จำเป็น เพราะข้อกำหนดของใบสั่งเดียวกันให้ "เปลี่ยน data-theme + จำใน localStorage" ซึ่งต้องใช้ JavaScript และห้ามเขียน <script> ใน view → ที่เดียวที่ถูกคือ Stimulus controller (branch ของอาจารย์ก็มีไฟล์นี้) · .md คือไฟล์ prompt ที่สไลด์ให้เก็บเอง · บทเรียนจากข้อนี้: checklist เขียนโดยคน ผิดได้ — ถ้าข้อหนึ่งขัดกับอีกข้อ ให้ถามว่า "ข้อนี้ตั้งใจกันอะไร" แล้วตรวจตรงนั้น

ข้อ 5 · ย่อหน้าต่างเหลือความกว้างมือถือแล้วยังอ่านได้

DevTools → กดไอคอนรูปมือถือ (Cmd+Shift+M) → ตั้งความกว้าง 375 · แล้วพิมพ์ใน Console document.documentElement.scrollWidth = ความกว้างจริงของเนื้อหาทั้งหน้า · ถ้าได้มากกว่า 375 แปลว่ามีของล้นจอ ต้องเลื่อนซ้ายขวา

ผลวัดจริง 8 หน้า × 2 ธีม (ตัดมาบางหน้า · ทุกหน้าได้เท่ากัน):

dark /posts 375/375
dark /posts?page=3 375/375
dark /posts/1 375/375
dark /categories 375/375
light /posts/new 375/375
light /categories/new 375/375

ตัวเลขซ้าย = scrollWidth · ขวา = ความกว้างจอ · เท่ากันทุกหน้า = ไม่มีอะไรล้น · ที่ 375px แถบ navbar ยังอยู่บรรทัดเดียว (เพราะใช้ btn-sm และชื่อเว็บ text-lg) · ปุ่มในหน้า show เรียงลงเต็มกว้าง (flex-col · ขึ้น sm:flex-row เมื่อจอกว้าง)

ใบสั่งงาน AI ของ Lab นี้: prompts/02-styling.md

ตอนนี้คุณทำเองครบแล้ว · เก็บใบสั่งของสไลด์หน้า 34 ไว้ในโฟลเดอร์ prompts/ (ข้าง ๆ 01-views.md ของบท 126) · ครั้งหน้าที่ต้องทำงานแบบนี้กับโปรเจกต์อื่น ใช้ไฟล์นี้สั่ง AI แล้วตรวจด้วยวิธีเดียวกับหัวข้อข้างบน · สร้างไฟล์ใหม่ prompts/02-styling.md:

## เป้าหมาย

ทำให้หน้า index กับ show ดูเป็นเว็บจริง
มี navbar และสลับธีมสว่าง/มืดได้

## ข้อกำหนด

- ใช้ component ของ daisyUI (card, navbar, badge)
  ไม่เขียน CSS เอง ไม่สร้างไฟล์ .css ใหม่
- ใช้ semantic color (primary, base-100)
  ไม่ใช่ bg-blue-600
- navbar เป็น partial shared/_navbar.html.erb
- ปุ่มสลับธีม เปลี่ยน data-theme บน `<html>`
- จำธีมที่เลือกไว้ใน localStorage
- responsive ใช้ได้บนมือถือ

## ห้าม

- สร้าง tailwind.config.js (เราใช้ v4)
- ต่อ string เป็นชื่อคลาส Tailwind
- แนะนำให้ npm install หรือเพิ่ม package.json
- เขียน `<style>` ใน view

## เกณฑ์ตรวจรับ

- [ ] สลับธีมแล้วเปลี่ยนทั้งเว็บ ไม่ใช่แค่บางส่วน
- [ ] refresh แล้วธีมที่เลือกยังอยู่
- [ ] รัน assets:precompile แล้วสียังอยู่ครบ
- [ ] ไม่มีไฟล์ใหม่ในโปรเจคนอกจาก .erb
- [ ] ย่อหน้าต่างเหลือความกว้างมือถือแล้วยังอ่านได้

เนื้อหาตรงกับสไลด์ทุกบรรทัด และเป็นไฟล์เดียวกับใน branch day3-b-done ของอาจารย์ทุกตัวอักษร · ต่างจากภาพสไลด์แค่ backtick ที่ครอบ `<html>` กับ `<style>` (บนสไลด์ไม่เห็น แต่ในไฟล์ของอาจารย์มี) = วิธีเขียนโค้ดในไฟล์ Markdown

วิธีใช้ครั้งหน้า (แบบเดียวกับบท 121): ใน Claude Code พิมพ์ @prompts/02-styling.md ทำตามไฟล์นี้ · พอ AI บอกว่าเสร็จ อย่าเชื่อทันที — ไล่ตรวจ 5 ข้อด้วยวิธีในหัวข้อตรวจรับข้างบน แล้วเทียบกับตาราง "AI พลาดประจำ" ข้างล่าง

ที่ AI พลาดประจำ (สไลด์หน้า 34) — อาการ และตรวจยังไง

AI ทำทำไมผิดตรวจเจอด้วย
สั่งให้สร้าง tailwind.config.jsตอบแบบ Tailwind v3 · v4 ตั้งค่าใน CSS (@plugin · @theme) — ไฟล์นั้นจะถูกเมินเฉย ๆ หรือทำให้คนตามงานงงls tailwind.config.js ต้องไม่มี
บอกให้ npm install daisyui · เขียน @plugin "daisyui"repo นี้ไม่มี Node · daisyUI เป็นไฟล์ daisyui.mjs ในโฟลเดอร์ · build จะพังด้วย Can't resolve 'daisyui'ls package.json ต้องไม่มี · bin/rails tailwindcss:build ต้องผ่าน
ใช้ bg-blue-600 · text-gray-700สีตายตัว ไม่รู้จักธีม → สลับแล้วค้าง (เห็นใน predict ขั้น 3: ค้าง 35 จุด)grep หาสีตายตัว ข้อ 1 ต้องว่าง
(เพิ่มเอง) เขียน <script> ใน layout เพื่อสลับธีมขัดข้อห้าม · โค้ดกระจายไม่มีที่อยู่ · ทางที่ถูกคือ Stimulus controllergrep -rn "<script" app/views ต้องไม่เจอใน view ของเรา

เก็บเข้าโปรเจกต์

$ git add .
$ git commit -m "styling: daisyUI + navbar + ปุ่มสลับธีม"
[my-day3 7ba4b9f] styling: daisyUI + navbar + ปุ่มสลับธีม
 19 files changed, 186 insertions(+), 108 deletions(-)
 create mode 100644 app/javascript/controllers/theme_controller.js
 create mode 100644 app/views/shared/_navbar.html.erb
 create mode 100644 prompts/02-styling.md

เลข commit (7ba4b9f) ของคุณจะต่าง · 19 files changed, 186 insertions(+), 108 deletions(-) ควรตรงกัน ถ้าพิมพ์ครบทุกบรรทัด · ผมเทียบไฟล์ทั้งโปรเจกต์หลัง commit นี้กับโค้ดที่บท 130 ใช้แล้ว ตรงกันทุกไฟล์ · create mode 100644 = ไฟล์ใหม่ 3 ไฟล์ที่ตรงกับข้อ 4 ข้างบน

🎯 ภารกิจ: เปิดธีมที่สาม cupcake (~10 นาที · ทำหลัง commit แล้วเท่านั้น)

  1. ใน app/assets/tailwind/application.css เติม , cupcake ต่อท้ายรายการ themes: (ก่อน ;) · บันทึก
  2. รอ bin/dev build ใหม่ (หรือสั่ง bin/rails tailwindcss:build) แล้วนับธีมด้วย grep -o '\[data-theme=[a-z]*\]' app/assets/builds/tailwind.css | sort | uniq -c
  3. เปิด /posts · DevTools Console พิมพ์ localStorage.setItem("theme", "cupcake") แล้วกด refresh
  4. กด 🌓 สองครั้ง · สังเกตว่าแต่ละครั้งได้ธีมอะไร · คิดว่าทำไม cupcake หายไปจากการกดปุ่ม (ดูบรรทัด const next = …)
  5. ลองพิมพ์ localStorage.setItem("theme", "synthwave") (ธีมที่ไม่ได้เปิด) แล้ว refresh · เห็นอะไร?
  6. คืนของ: git restore app/assets/tailwind/application.css · Console localStorage.removeItem("theme") · git status --short ต้องว่าง

ควรเห็น: ข้อ 2 มีบรรทัด 1 [data-theme=cupcake] เพิ่มมา · ข้อ 3 พื้นเป็นสีครีม ปุ่มหลักกับป้ายหมวดเป็นสีเขียวมิ้นต์ ปุ่มมนกลม · ข้อ 4 ได้ dark แล้ว light

ถ้าเจออาการนี้ แปลว่า…

ข้อ 1–2 — ผลจริง:

$ git diff
…
 @plugin "./daisyui.mjs" {
-  themes: light --default, dark --prefersdark;
+  themes: light --default, dark --prefersdark, cupcake;
 }
…
$ bin/rails tailwindcss:build
≈ tailwindcss v4.3.3

/*! 🌼 daisyUI 5.7.50 */
Done in 93ms
$ grep -o '\[data-theme=[a-z]*\]' app/assets/builds/tailwind.css | sort | uniq -c
   1 [data-theme=cupcake]
   1 [data-theme=dark]
   2 [data-theme=light]

ข้อ 3–5 — วัดใน Chrome จริง (ป้าย · ค่าในสมุด · สีพื้น · สีปุ่มหลัก):

reload   {"theme":"cupcake","saved":"cupcake","body":"oklch(0.93982 0.007 61.449)","primary":"oklch(0.85 0.138 181.071)"}
click1   {"theme":"dark","saved":"dark","body":"oklch(0.2326 0.014 253.1)","primary":"oklch(0.58 0.233 277.117)"}
click2   {"theme":"light","saved":"light","body":"oklch(0.98 0 0)","primary":"oklch(0.45 0.24 277.023)"}
synthwave(not enabled) {"theme":"synthwave","saved":"synthwave","body":"oklch(0.98 0 0)","primary":"oklch(0.45 0.24 277.023)"}
  • reload: connect() อ่านสมุดได้ cupcake → ติดป้ายให้ → สีเปลี่ยนทั้งหน้า · ไม่ต้องแก้ JavaScript สักบรรทัด เพราะ controller แค่ "ก็อปค่าจากสมุดไปติดป้าย"
  • click1: current = "cupcake" ไม่ใช่ "dark" → next = "dark" · click2: มืด → สว่าง · cupcake หลุดวงจร เพราะ toggle() รู้จักแค่สองค่า — ถ้าอยากวนสามธีม ต้องแก้ toggle() (ลองหลังเรียนบท 134)
  • synthwave: ป้ายติดแล้ว สมุดจดแล้ว แต่สีพื้นกับปุ่มเท่ากับ light ทุกค่า — ชุดที่ร้านไม่ได้ซื้อ แขวนป้ายไปก็ไม่มีของ (analogy ขั้น 1) · ไม่มี error ใน Console

ข้อ 6 — git restore คืนไฟล์เป็นแบบใน commit ล่าสุด (ไม่มี cupcake) · bin/dev build ใหม่ให้เอง · สมุดใน localStorage ต้องลบเองเพราะ git ไม่รู้จักของในเบราว์เซอร์

Quiz

ข้อ 1 · เพื่อนเพิ่มปุ่ม "แชร์" ใน navbar เขียน class="btn bg-blue-600 text-white" · กดสลับเป็นธีมมืด — ปุ่มแชร์เป็นยังไง?

ตัวเลือกที่ 2 ถูก · predict ในขั้น 3 วัดจริงแล้ว: ปุ่ม bg-blue-600 ได้ oklch(0.546 0.245 262.881) ทั้งก่อนและหลังกด

ตัวเลือกที่ 1 ผิด — daisyUI เปลี่ยนเฉพาะสีที่อ่านจากตัวแปรของธีม · bg-blue-600 เป็นสีของ Tailwind เอง ไม่เกี่ยวกับธีม · ตัวเลือกที่ 3 ผิด — btn ให้รูปทรงกับสีตั้งต้น แต่ bg-blue-600 สั่งสีพื้นทับไปแล้ว · ตัวเลือกที่ 4 ผิด — ผสมได้ตามสบาย (btn btn-primary w-full) ที่มีปัญหาคือสีตายตัว ไม่ใช่การผสม

จุดที่ต้องจำ: อยากให้สีตามธีม ต้องเรียกสีตามหน้าที่ (btn-primary · bg-base-100) ไม่ใช่ตามชื่อสี · ตรวจด้วย grep ชื่อสีใน app/views

ข้อ 2 · เลือกธีมมืดแล้วกด refresh ธีมยังมืดอยู่ — อะไรทำให้มันจำได้?

ตัวเลือกที่ 4 ถูก · toggle() จดด้วย localStorage.setItem · หน้าใหม่โหลด → Stimulus เจอปุ่ม → connect() อ่าน getItem → ติดป้าย

ตัวเลือกที่ 1 ผิด — curl ได้ <html> ที่ไม่มี data-theme · server ไม่รู้เลยว่าเราเลือกอะไร · ตัวเลือกที่ 2 ผิด — ไฟล์ CSS มีทุกธีมที่เปิด (light กับ dark) เหมือนกันสำหรับทุกคน ไม่ได้จำอะไรของใคร · ตัวเลือกที่ 3 ผิด — ไม่มีโค้ดไหนเขียนไฟล์ CSS · และถ้าเปลี่ยน --default จะมืดทุกคน ไม่ใช่เฉพาะคนที่เลือก

จุดที่ต้องจำ: ธีมเก็บในเบราว์เซอร์ของแต่ละคน — เปลี่ยนเครื่องหรือเบราว์เซอร์ = เริ่มใหม่ · ถ้าอยากให้ server รู้ ต้องใช้ cookie (เกินขอบ Lab นี้)

ข้อ 3 · รัน bin/rails assets:precompile ตรวจข้อ 3 ผ่าน แล้วรัน bin/rails assets:clobber · จากนั้นปิด bin/dev เปิดใหม่ด้วย bin/rails server อย่างเดียว — หน้าเว็บเป็นยังไง?

ตัวเลือกที่ 1 ถูก · หลัง clobber ผมดู HTML จริง: บรรทัด <link … href="/assets/tailwind-1c1af83f.css"> หายไปทั้งบรรทัด เพราะ app/assets/builds/tailwind.css ถูกลบ

ตัวเลือกที่ 2 คือความเข้าใจที่คนส่วนใหญ่มี — แต่ของจริง clobber ลบไฟล์ใน builds ด้วย · ตัวเลือกที่ 3 คืออาการตอนลืม clobber (ไม่ใช่หลัง clobber) · ตัวเลือกที่ 4 ผิด — ผมได้ 200 หน้าเปิดได้ แค่ไม่มี style เพราะ Rails แค่ไม่ใส่ลิงก์ไฟล์ที่ไม่มีอยู่

จุดที่ต้องจำ: ซ้อม precompile แล้ว clobber ทุกครั้ง · หลัง clobber ให้ bin/dev (หรือ tailwindcss:build) สร้าง CSS ใหม่ก่อนทำงานต่อ

ข้อ 4 · ตรวจข้อ 1 แล้วพบว่าปุ่ม "ลบบทความ" (btn btn-error) สีแดงเท่าเดิมทั้งธีมสว่างและมืด — ควรทำยังไง?

ตัวเลือกที่ 3 ถูก · ผมวัด btn-error ได้ oklch(0.71 0.194 13.428) ทั้งสองธีม = ธีม light/dark ของ daisyUI ตั้งสีนี้เหมือนกัน · ในธีม cupcake มันจะเปลี่ยนตาม

ตัวเลือกที่ 1 คือการถอยกลับไปใช้สีตายตัว — ตอนนี้ดูเหมือนเดิม แต่วันที่เปลี่ยนธีมอื่นมันจะค้าง · ตัวเลือกที่ 2 ผิดข้อห้ามของใบสั่งงานตรง ๆ · ตัวเลือกที่ 4 อ่านเกณฑ์ตามตัวอักษรจนทำลายฟีเจอร์ — เจตนาของเกณฑ์คือ "ไม่มีส่วนไหนล็อกสีไว้" ไม่ใช่ "ทุกพิกเซลต้องเปลี่ยน"

จุดที่ต้องจำ: สีที่บังเอิญเท่าเดิมกับสีที่ล็อกไว้ ดูด้วยตาแยกไม่ออก · ให้ดูที่คลาส — มีชื่อสี (red · blue) = ล็อก · มีชื่อหน้าที่ (error · primary) = ตามธีม

ถามกลับได้เลย

ถ้าสลับธีมแล้วมีบางส่วนค้าง หรือ AI แต่งหน้าให้แล้วคุณไม่แน่ใจว่าผ่านเกณฑ์ไหม — เอาไปถาม senior หรือ AI ได้เลย · ส่งไปสามอย่าง: ไฟล์ view ที่สงสัย · ผลของ grep -rnE "(bg|text|border|divide)-(gray|blue|red|green)-" app/views · และ git status --short · ถ้าปุ่ม 🌓 ไม่ทำงาน แนบข้อความสีแดงใน DevTools Console มาด้วย

ถ้าเปิดสไลด์ดูประกอบ

หน้าสไลด์เขียนของจริงต้องสนใจไหม
30@plugin "daisyui" { … }repo นี้ไม่มี Node · ต้องเป็น @plugin "./daisyui.mjs" ในไฟล์ app/assets/tailwind/application.css · ก็อปแบบสไลด์แล้ว build พัง Can't resolve 'daisyui'สนใจ — ขั้น 1
30themes: light --default, dark --prefersdark, synthwave;ไวยากรณ์ถูก · Lab เปิดแค่ light + dark ตามโจทย์ "สว่าง/มืด" · ซึ่งเป็นค่าตั้งต้นของ daisyUI 5 อยู่แล้ว (CSS เหมือนเดิมทุกไบต์)แค่รู้ไว้ — เติมธีมได้แบบภารกิจ
34เกณฑ์ "ไม่มีไฟล์ใหม่นอกจาก .erb"ต้องมี theme_controller.js (ปุ่มสลับธีมต้องใช้ JS และห้าม <script> ใน view) และ prompts/02-styling.mdสนใจ — ตรวจตามเจตนา (ข้อ 4)
34assets:precompile "ใช้เวลา 20 วินาที"เครื่องผมราว 1 วินาทีสำหรับแอปนี้ · ทิ้งท้ายด้วย Warning ให้ลบไฟล์รายชื่อ .manifest.json — คือเหตุผลที่ต้อง assets:clobberแค่รู้ไว้
34ใช้ bg-blue-600 → สลับธีมแล้วไม่เปลี่ยนจริง · วัดได้ 35 จุดที่ค้างในหน้า /posts ก่อนแก้ขั้น 4ตรงกับของจริง

อ้างอิง