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

Tailwind v4 และ daisyUI — และกับดักชื่อคลาสที่ต่อ string

~22 นาที · ควรผ่าน บทเรียน 127 (asset pipeline) มาก่อน · ใช้ของจาก บท 72 (Tailwind) และ บท 81 (design token) ด้วย

บท 127 ตามดูว่าไฟล์ tailwind.css เดินทางไปถึงเบราว์เซอร์ยังไง · บทนี้ย้อนไปดูว่าไฟล์นั้นถูกสร้างขึ้นมายังไง และเราสั่งอะไรมันได้บ้าง
อ่านจบแล้วคุณจะ: อ่านไฟล์ตั้งค่า Tailwind ของแอปออกทุกบรรทัด · เพิ่มสีของร้านเองได้ · ใช้ปุ่มสำเร็จรูปของ daisyUI และเปลี่ยนธีมทั้งเว็บด้วย attribute ตัวเดียว · เห็นกับตาว่าทำไมชื่อคลาสที่ต่อ string ทำให้สีหาย และพิสูจน์ได้ด้วย grep · และไม่ลืมรัน bin/dev อีกเลย

🔁 ทวนก่อนเริ่ม — คำจากบทก่อน ๆ ที่บทนี้ใช้

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

🗂 โค้ดเริ่มจาก: แอปหลังจบ Lab บท 126 (ตามไม่ทันดูบท 126) · บทนี้ทดลองแล้วทิ้ง — จบบทให้ git restore . + bin/rails assets:clobber + bin/rails tailwindcss:build (บอกอีกทีท้ายภารกิจ)

ผลทุกก้อนรันจริงแล้วก็อปมา · เวลา (ms) · เวลาบนหัวบรรทัดของ bin/dev · เลข PID · คำว่า "1 day ago" ของคุณจะต่างจากของเรา

Tailwind ในแอป Rails อยู่ตรงไหน — เปิดไฟล์ตั้งค่าของจริง

ทวนจากบท 72 ครึ่งย่อหน้า: เราเขียนคลาสอย่าง font-bold ใน view · Tailwind อ่านไฟล์ทุกไฟล์ในโปรเจกต์เป็นตัวหนังสือ แล้วเขียน CSS เฉพาะคลาสที่เจอลงไฟล์เดียว · ไฟล์ CSS จึงเล็ก เพราะไม่มีคลาสที่ไม่มีใครใช้

ใน Next.js เราได้ Tailwind ผ่าน PostCSS และ npm · ใน Rails ใช้ gem tailwindcss-rails ซึ่งพกโปรแกรม Tailwind มาเป็นไฟล์เดียว ไม่ต้องมี Node ไม่ต้องมี npm · ทางเดินของมันมีแค่นี้:

app/views/**/*.erb ─┐ app/helpers/*.rb ─┼─► Tailwind อ่านเป็นตัวหนังสือ ─► app/assets/builds/tailwind.css ─► Propshaft (บท 127) ─► เบราว์เซอร์ ไฟล์อื่นในโปรเจกต์ ─┘ ▲ │ ทำตามใบสั่งใน app/assets/tailwind/application.css

อ่านแผนภาพ: app/views/**/*.erb = "ไฟล์ .erb ทุกไฟล์ ในทุกโฟลเดอร์ย่อยของ app/views" (** = ลงไปกี่ชั้นก็ได้) · ลูกศรขวาคือทางที่ CSS เดินต่อ · ลูกศรขึ้นจากด้านล่างคือไฟล์ตั้งค่าที่บอกว่าให้ Tailwind ทำอะไร

ไฟล์ตั้งค่า (ใบสั่งงาน) ของ Tailwind คือ app/assets/tailwind/application.css · เปิดดูของจริงในแอป (สถานะหลังบท 126):

/* app/assets/tailwind/application.css */
@import "tailwindcss";
@source not "./daisyui{,*}.mjs";
@plugin "./daisyui.mjs";

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

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

แล้วสั่ง build เองหนึ่งรอบ (เปิด terminal ที่โฟลเดอร์โปรเจกต์):

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

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

บรรทัดแรกบอกรุ่นของ Tailwind · บรรทัดดอกไม้บอกว่า daisyUI ถูกเสียบเข้ามาแล้ว · Done in 81ms = เสร็จใน 0.08 วินาที (ของคุณจะต่างนิดหน่อย)
ผลลัพธ์ไปอยู่ที่ app/assets/builds/tailwind.css ซึ่งอยู่ใน .gitignore (ไฟล์รายชื่อของที่ git ไม่ต้องเก็บ) · Tailwind ข้ามไฟล์ในรายชื่อนั้นด้วย จึงไม่อ่านผลงานของตัวเองวนไปมา

🛒 เทียบให้เห็นภาพ — ฝ่ายจัดซื้อที่อ่านเมนูบนกระดาษ

Tailwind คือฝ่ายจัดซื้อของร้าน · ก่อนเปิดร้าน เขาเดินอ่านรายการเมนูทุกแผ่นที่เขียนไว้บนกระดาษ (ไฟล์ view) แล้วซื้อวัตถุดิบเฉพาะที่เห็นชื่อ (เขียน CSS เฉพาะคลาสที่เจอ) · ของที่ไม่มีเมนูไหนเขียนถึง ไม่ซื้อ ตู้เย็นจึงไม่รก
application.css คือใบสั่งงานที่แปะไว้บนโต๊ะฝ่ายจัดซื้อ: "ใช้วัตถุดิบมาตรฐานชุดนี้ (@import) · ห้ามอ่านคู่มือของซัพพลายเออร์ (@source not) · รับชุดจานชามจากเจ้านี้ (@plugin) · สีประจำร้านคือ… (@theme)"

ที่ไม่เหมือนกัน: ฝ่ายจัดซื้อจริงอ่านแล้วเข้าใจความหมาย เห็นคำว่า "ต้มยำ" ก็รู้ว่าต้องซื้อข่าตะไคร้ · Tailwind ไม่เข้าใจอะไรเลย มันแค่หาคำที่หน้าตาตรงกับชื่อคลาสที่มันรู้จัก · และมันอ่านทุกไฟล์ในโปรเจกต์ ไม่ใช่แค่ view — ไฟล์จดโน้ตหรือไฟล์ HTML เก่าที่วางทิ้งไว้ในโปรเจกต์ ถ้ามีชื่อคลาสอยู่ข้างใน มันก็ซื้อของให้ด้วย

🗣 ศัพท์ที่เพิ่งเรียนไป
tailwindcss-rails · tailwindcss:build
gem ที่พาโปรแกรม Tailwind มาใช้ใน Rails โดยไม่ต้องมี Node · คำสั่ง build = อ่านไฟล์แล้วสร้าง app/assets/builds/tailwind.css หนึ่งรอบ · พิมพ์บอก AI ได้ว่า "this app uses tailwindcss-rails (standalone CLI, no Node) — the entry file is app/assets/tailwind/application.css"
source · @source
ไฟล์ที่ Tailwind อ่านเพื่อหาชื่อคลาส · ค่าเริ่มต้นคือทุกไฟล์ในโปรเจกต์ ยกเว้นที่อยู่ใน .gitignore · @source not "…" = สั่งให้ข้ามไฟล์นี้ · ถาม senior ได้ว่า "ไฟล์ CSS ใหญ่ผิดปกติ มีไฟล์ไหนที่ Tailwind ไม่ควรสแกนไหม"

v3 กับ v4 — ไม่มี tailwind.config.js แล้ว และทำไมต้องบอก AI

บท 72 เทียบไว้แล้วว่า Tailwind v3 ตั้งค่าในไฟล์ JavaScript ชื่อ tailwind.config.js · v4 ย้ายทุกอย่างมาอยู่ในไฟล์ CSS · ในแอป Rails ของเราหน้าตาเป็นแบบนี้ (สมมติอยากได้สีประจำร้านชื่อ brand):

v3 — ของเก่าที่อินเทอร์เน็ตเต็มไปหมด

// tailwind.config.js
module.exports = {
  content: ["./app/views/**/*.erb"],
  theme: { extend: { colors: {
    brand: "#ff3d7f"
  } } },
}

v4 — ของจริงในแอปเรา

/* app/assets/tailwind/application.css */
@theme {
  --font-sans: "Noto Sans Thai", …;
  --color-brand: #ff3d7f;
}
/* แล้วใช้ class="text-brand" ได้เลย */

ฝั่งซ้ายเป็น JavaScript: module.exports = { … } = "ส่งก้อนตั้งค่านี้ออกไปให้โปรแกรมอื่นใช้" · content = รายชื่อไฟล์ที่ให้อ่าน · extend.colors = "เพิ่มสีนี้ต่อจากสีมาตรฐาน" · ไม่ต้องจำ แค่จำหน้าตาไว้ — เห็นแบบนี้ในคำตอบของ AI หรือในเว็บ = ของ v3

ฝั่ง v4 ไม่ต้องบอก content (ไฟล์ไหนให้อ่าน) เพราะมันอ่านทุกไฟล์เอง · ชื่อตัวแปรมีแบบแผน: --color- นำหน้า = สี · ได้คลาส text-brand bg-brand border-brand มาทันที
ลองจริง: เพิ่มบรรทัด --color-brand: #ff3d7f; ใน @theme ตามฝั่งขวา แล้ว build

$ bin/rails tailwindcss:build
$ grep -c -- '--color-brand' app/assets/builds/tailwind.css

ยังไม่มี view ไหนใช้คลาส text-brand เลย · grep -c นับว่ามีกี่บรรทัดที่เจอคำนี้ — ไฟล์ CSS ที่ได้ถูกบีบเหลือบรรทัดเดียว ผลจึงมีแค่ 1 = เจอ · 0 = ไม่เจอ (ใช้แบบนี้ทั้งบท) · -- ก่อนคำค้น = บอก grep ว่า "คำที่ตามมาไม่ใช่ตัวเลือกของ grep" เพราะมันขึ้นต้นด้วยขีดสองตัว

ตัวแปร --color-brand จะอยู่ในไฟล์ CSS ที่ได้ไหม?

≈ tailwindcss v4.3.3

/*! 🌼 daisyUI 5.7.50 */
Done in 78ms
0

ไม่อยู่ · Tailwind v4 ใส่ตัวแปรของ @theme ลงไฟล์เฉพาะตัวที่มีคนใช้ — กฎเดียวกับคลาส · ฝ่ายจัดซื้อไม่ซื้อสีที่ไม่มีเมนูไหนสั่ง

ทีนี้ใส่ text-brand ให้หัวข้อหน้า app/views/posts/index.html.erb (ทวนบท 125–126: บรรทัดขึ้นต้น - = ของเดิมที่ลบ · + = ของใหม่ที่ใส่แทน · ตอนพิมพ์จริงไม่ต้องพิมพ์ - +):

-    <h1 class="font-bold text-3xl sm:text-4xl">บทความทั้งหมด</h1>
+    <h1 class="font-bold text-3xl sm:text-4xl text-brand">บทความทั้งหมด</h1>
$ bin/rails tailwindcss:build
$ grep -o -- '--color-brand:[^;}]*' app/assets/builds/tailwind.css
--color-brand:#ff3d7f
$ grep -o '\.text-brand{[^}]*}' app/assets/builds/tailwind.css
.text-brand{color:var(--color-brand)}

เพราะไฟล์ถูกบีบเป็นบรรทัดยาวบรรทัดเดียว (minify) จึงใช้ grep -o = "พิมพ์เฉพาะส่วนที่ตรง" · [^;}]* = "ตัวอะไรก็ได้ที่ไม่ใช่ ; หรือ }" (อ่านไปจนจบค่า) · ได้ทั้งตัวแปร และคลาส .text-brand ที่ชี้ไปหาตัวแปรนั้น · เปิดหน้า /posts หัวข้อเป็นสีชมพู
(ตั้งแต่ตรงนี้ ผมตัดข้อความ 3 บรรทัดของ build — รุ่น Tailwind · ดอกไม้ daisyUI · Done in … — ออก เหลือแค่ผลของ grep)

แล้วถ้าทำตามเว็บเก่า ๆ สร้าง tailwind.config.js ล่ะ? ผมสร้างไฟล์นี้ที่โฟลเดอร์บนสุดของโปรเจกต์ ตั้งสีชื่อ mint แล้วใส่ text-mint ใน <h1> เดิม:

// tailwind.config.js — ไฟล์ทดลอง ลบทิ้งหลังลอง
module.exports = {
  content: ["./app/views/**/*.html.erb"],
  theme: { extend: { colors: { mint: "#3eb489" } } },
}
$ bin/rails tailwindcss:build
≈ tailwindcss v4.3.3

/*! 🌼 daisyUI 5.7.50 */
Done in 88ms
$ grep -c 'text-mint' app/assets/builds/tailwind.css
0

ไม่มี error สักบรรทัด · build ผ่าน · แต่สีไม่มา · v4 ไม่เปิดไฟล์นี้เลย มันวางอยู่เฉย ๆ เหมือนกระดาษที่ไม่มีใครอ่าน
นี่คือเหตุผลที่ prompts/00-context.md ของแอปนี้มีบรรทัดนี้อยู่แล้ว (เปิดดูได้ — บท 121 พาอ่านไปแล้ว):

- tailwindcss-rails (Tailwind v4) + daisyUI 5 ผ่าน @plugin ในไฟล์ CSS
  ห้ามสร้าง tailwind.config.js — Tailwind v4 ไม่อ่านไฟล์นั้น

AI ฝึกจากเนื้อหาในอินเทอร์เน็ต · v3 อยู่มาหลายปี ตัวอย่างเยอะกว่า v4 มาก · ถ้าไม่บอก มันมีโอกาสสูงที่จะตอบแบบ v3 ("สร้าง tailwind.config.js แล้วใส่ content") · เราทำตาม → ไม่มี error → นั่งงงว่าทำไมสีไม่มา · บรรทัดเดียวใน 00-context.md ตัดปัญหานี้ทิ้งทั้งก้อน

ชื่อคลาสบางตัวก็ขยับหนึ่งขั้นใน v4 เช่น เงาบาง ๆ ที่ v3 เรียก shadow-sm ตอนนี้ชื่อ shadow-xs · โค้ดจาก AI ที่ใช้ชื่อแบบ v3 จึงไม่พัง แต่หน้าตาต่างจากที่ตั้งใจนิดหน่อย (เงาหนากว่าหนึ่งขั้น) · ตารางชื่อที่เปลี่ยนอยู่ใน upgrade guide ท้ายบท

📒 เทียบให้เห็นภาพ — สมุดสั่งงานเล่มเก่าที่ไม่มีใครเปิด

ร้านเราเคยมีสมุดสั่งงานฝ่ายจัดซื้อแยกเป็นเล่ม (v3 · tailwind.config.js) · พอเปลี่ยนระบบ ร้านย้ายคำสั่งทั้งหมดไปเขียนในใบสั่งงานที่แปะบนโต๊ะ (v4 · @theme ใน CSS) แล้วเลิกเปิดสมุดเล่มเก่า · พนักงานใหม่ (AI) ที่เคยทำร้านอื่นมา จะหยิบสมุดเล่มเก่ามาเขียนคำสั่งเพิ่ม — เขียนสวยแค่ไหนก็ไม่มีใครอ่าน

ที่ไม่เหมือนกัน: ร้านจริงอาจมีคนเห็นสมุดเล่มเก่าแล้วเตือน · Tailwind v4 เงียบสนิท ไม่เตือนว่ามีไฟล์ที่มันไม่อ่าน · เราจึงต้องเป็นคนเตือนพนักงานใหม่เองตั้งแต่วันแรก (บรรทัดใน 00-context.md ข้างบน)

🗣 ศัพท์ที่เพิ่งเรียนไป
theme variable (--color-* · --font-*)
ตัวแปร CSS ใน @theme ที่ชื่อขึ้นต้นตามแบบแผน แล้ว Tailwind สร้างคลาสให้ (--color-brand → text-brand · bg-brand) · ใส่ลงไฟล์เฉพาะตัวที่ถูกใช้ · พิมพ์บอก AI ได้ว่า "add a brand color as a Tailwind v4 theme variable in @theme, not in tailwind.config.js"
Tailwind v3 vs v4
v3 = ตั้งค่าใน tailwind.config.js + ต้องบอก content · v4 = ตั้งค่าใน CSS + หาไฟล์เอง · ไวยากรณ์ v3 ในโปรเจกต์ v4 มักไม่ error แค่ไม่มีผล · ถาม senior ได้ว่า "ตัวอย่างที่ผมเจอเป็น v3 ใช่ไหม ต้องแปลงอะไรบ้างก่อนใช้กับ v4"

daisyUI 5 — ชุดจานชามสำเร็จรูปที่วางบน Tailwind

Tailwind ให้อิฐทีละก้อน · ปุ่มหนึ่งปุ่มต้องเรียงคลาสยาว ๆ เอง · daisyUI คือส่วนเสริม (plugin) ที่ตั้งชื่อให้ชุดคลาสที่ใช้บ่อย เช่น btn = หน้าตาปุ่มทั้งชุด · btn-primary = ปุ่มสีหลักของร้าน · มีทั้ง card · navbar · badge · alert ฯลฯ (สไลด์อ้างว่าราว 60 ตัว) และใช้ CSS ล้วน ไม่มี JavaScript

Tailwind เปล่า ๆ

<button class="px-4 py-2 rounded-lg bg-blue-600
  text-white font-medium hover:bg-blue-700
  focus:ring-2 focus:ring-blue-400
  disabled:opacity-50 transition">
  บันทึก
</button>

daisyUI

<button class="btn btn-primary">บันทึก</button>

<%# ผสมกับคลาส Tailwind ได้ตามสบาย %>
<button class="btn btn-primary w-full mt-4">
  บันทึก
</button>

ฝั่งซ้าย: px-4 py-2 = ระยะในซ้ายขวา/บนล่าง · rounded-lg = มุมมน · hover: focus: disabled: = ใช้คลาสนี้เฉพาะตอนเมาส์ชี้/ตอนถูกเลือก/ตอนกดไม่ได้ (บท 72) · ring-2 = เส้นขอบเรืองรอบปุ่ม · opacity-50 = จางลงครึ่งหนึ่ง · transition = เปลี่ยนสีแบบนุ่ม ๆ ไม่กระตุก
ฝั่งขวาได้ของพวกนี้ครบในสองคำ · w-full mt-4 ต่อท้าย = คลาส Tailwind ธรรมดา ใช้ปนกันได้ · <%# %> = คอมเมนต์ของ ERB (บท 122)

daisyUI ในแอปนี้มาเป็นไฟล์ ไม่ได้ลงด้วย npm · ls app/assets/tailwind แล้วเจอ:

$ ls app/assets/tailwind
application.css
daisyui-theme.mjs
daisyui.mjs
$ 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

daisyui.mjs คือตัว daisyUI ทั้งก้อนที่ก็อปมาวางในโปรเจกต์ (.mjs = ไฟล์ JavaScript ชนิดหนึ่ง · Tailwind เรียกใช้ตอน build ไม่ได้ส่งไปเบราว์เซอร์) · daisyui-theme.mjs เป็นตัวช่วยสร้างธีมของเราเอง ซึ่งแอปนี้ยังไม่ได้เรียกใช้ · การก็อป library มาเก็บในโปรเจกต์แบบนี้เรียกว่า vendor · ไม่มี package.json ไม่มี node_modules ตรงกับที่บท 127 เล่าว่าแอป Rails 8 ไม่ต้องมี Node

สไลด์หน้า 29–30 เขียน @plugin "daisyui"; (แบบโปรเจกต์ที่ลงด้วย npm) · พิมพ์ตามในแอปนี้แล้ว build พังทันที:

≈ tailwindcss v4.3.3

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

แอปเราต้องเขียน @plugin "./daisyui.mjs"; (มี ./ และ .mjs) · ส่วน … คือ path โฟลเดอร์ของคุณ

daisyUI ทำตามกฎฝ่ายจัดซื้อเหมือนกัน · ก่อนใส่ปุ่มในหน้าไหนเลย grep -c '\.btn-primary{' ในไฟล์ CSS ได้ 0 · พอเพิ่ม <button class="btn btn-primary"> ลงใน view แล้ว build ได้ 1 · ใช้ชิ้นไหน ได้ CSS ชิ้นนั้น ไม่ได้ยัด 60 ชิ้นมาทั้งหมด
(คำค้น \.btn-primary{ = "จุด ตามด้วยชื่อคลาส ตามด้วย {" ซึ่งคือหน้าตาของกฎ CSS ในไฟล์ · \. แปลว่าจุดจริง ๆ เพราะจุดเปล่า ๆ ใน grep แปลว่า "ตัวอะไรก็ได้")

🍽 เทียบให้เห็นภาพ — ชุดจานชามสำเร็จรูปที่เข้าชุดกัน

Tailwind เปล่า ๆ เหมือนร้านที่ซื้อดิน สี เตาเผา มาปั้นจานเองทุกใบ — ได้ตามใจทุกอย่าง แต่จานแต่ละใบอาจไม่เข้ากัน · daisyUI คือชุดจานชามสำเร็จรูปจากโรงงาน สั่งว่า "จานใหญ่ 1 · ถ้วยน้ำจิ้ม 2" (btn · card · badge) ได้ของที่ลวดลายเข้าชุดกันทันที · อยากเติมอะไรก็เอาสีมาแต้มเพิ่มบนจานสำเร็จได้ (w-full mt-4)

ที่ไม่เหมือนกัน: จานโรงงานจริงเป็นของคนละชนิดกับจานปั้นเอง · daisyUI ปั้นด้วยดินชุดเดียวกับ Tailwind — ข้างใน btn ก็คือค่า CSS ชุดเดียวกับที่เราเขียนเองได้ แค่มีชื่อเรียก · และโรงงานนี้ไม่ส่งของล่วงหน้า ใบไหนไม่มีใครสั่งในกระดาษเมนู ก็ไม่ผลิต

🗣 ศัพท์ที่เพิ่งเรียนไป
component class (daisyUI)
ชื่อคลาสหนึ่งคำที่แทนชุดคลาส Tailwind ทั้งชุด (btn · card · badge · navbar) · ผสมกับ utility class ได้ · พิมพ์บอก AI ได้ว่า "use daisyUI 5 component classes (btn, card, badge) instead of long Tailwind class lists"
vendor · vendored library · @plugin
vendor = ก็อปไฟล์ของ library มาเก็บในโปรเจกต์เอง แทนการลงผ่าน npm · @plugin "./daisyui.mjs" = เสียบไฟล์นั้นเข้า Tailwind · ถาม senior ได้ว่า "daisyUI ของเรา vendored ไว้ ถ้าจะอัปเกรดต้องเปลี่ยนไฟล์ยังไง"

สีตามหน้าที่ และ data-theme — เปลี่ยนธีมทั้งร้านด้วยป้ายเดียว

ทวนบท 81: bg-blue-600 = "น้ำเงินเบอร์ 600" ตายตัว · btn-primary = "สีหลักของร้าน" ซึ่งจะเป็นสีอะไรก็ขึ้นกับธีม · daisyUI ตั้งชื่อสีตามหน้าที่ไว้ชุดหนึ่ง เช่น primary (สีหลัก) · secondary · accent · base-100 (สีพื้นหลังหน้า) · base-content (สีตัวหนังสือบนพื้น)

daisyUI มีธีมสำเร็จ 35 ชุด (นับจากไฟล์ daisyui.mjs รุ่น 5.7.50 จริง) · สลับธีมด้วยการใส่ attribute data-theme ที่แท็ก <html> บนสุดของหน้า:

<html data-theme="dark">
<html data-theme="cupcake">

ผมวางปุ่มสองปุ่มไว้ใน app/views/posts/index.html.erb — ปุ่ม daisyUI กับปุ่ม Tailwind เปล่า — แล้วเปิดด้วย Chrome จริง · เปลี่ยน data-theme ทีละค่าแล้ววัดสีพื้นที่เบราว์เซอร์ใช้จริง (oklch(…) คือวิธีเขียนสีอีกแบบ เหมือน #ff3d7f · ไม่ต้องอ่านออก ดูแค่ว่าเปลี่ยนหรือไม่เปลี่ยน · ตัวเลขแรกคือความสว่าง 0–1):

<div class="my-4 flex gap-2">
  <button class="btn btn-primary">บันทึก</button>
  <button class="px-4 py-2 rounded-lg bg-blue-600 text-white">บันทึก</button>
</div>
data-theme บน <html>พื้นปุ่ม btn-primaryพื้นปุ่ม bg-blue-600พื้นหลังหน้า
(ไม่ใส่ · เครื่องตั้งโหมดสว่าง)oklch(0.45 0.24 277.023)oklch(0.546 0.245 262.881)oklch(1 0 0) ขาว
"light"oklch(0.45 0.24 277.023)oklch(0.546 0.245 262.881)oklch(1 0 0) ขาว
"dark"oklch(0.58 0.233 277.117)oklch(0.546 0.245 262.881)oklch(0.2533 0.016 252.42) เกือบดำ

ปุ่ม btn-primary เปลี่ยนสีตามธีม (สว่างขึ้นให้เด่นบนพื้นมืด) · ปุ่ม bg-blue-600 สีเดิมทุกแถว เพราะเราสั่งเลขสีตายตัวไว้ · ตอนเป็น dark หน้าจอจริงเห็นอีกอย่าง: คำอธิบายบทความที่เป็น text-gray-700 (เทาเข้มตายตัว) แทบจมหายไปในพื้นมืด — นี่คือเหตุผลที่ Lab บท 129 ย้ายหน้าไปใช้สีตามหน้าที่

daisyUI มี 35 ธีม แต่ไม่ได้เปิดใช้ทุกธีม · ไฟล์ application.css ของเราเขียนแค่ @plugin "./daisyui.mjs"; ไม่ได้บอกว่าเอาธีมไหน

ถ้าใส่ <html data-theme="synthwave"> (ธีมโทนม่วงชมพูนีออน) ปุ่ม btn-primary และพื้นหลังหน้าจะเป็นสีอะไร?

วัดใน Chrome ได้ปุ่ม oklch(0.45 0.24 277.023) · พื้นหลัง oklch(1 0 0) = หน้าตาเหมือนธีม light ทุกอย่าง · ไม่มี error · ไม่มีอะไรเตือน

เพราะค่าเริ่มต้นของ daisyUI 5 เปิดแค่ light กับ dark · ชื่อธีมที่ไม่ได้เปิดไม่มี CSS ให้ใช้ · หน้าเลยตกไปใช้ธีมหลัก (light) · ต้องเปิดเองในบล็อก @plugin:

@plugin "./daisyui.mjs" {
  themes: light --default, dark --prefersdark, synthwave;
}
$ bin/rails tailwindcss:build
$ grep -o '\[data-theme=[a-z]*\]' app/assets/builds/tailwind.css | sort | uniq -c
   1 [data-theme=dark]
   2 [data-theme=light]
   1 [data-theme=synthwave]

คำสั่งยาวแปลว่า "ดึงทุกคำที่หน้าตาเป็น [data-theme=ชื่อ] · เรียง · นับว่าแต่ละชื่อมีกี่ครั้ง" · ก่อนเปิด มีแค่ dark กับ light · หลังเปิด synthwave โผล่มา · วัดใน Chrome ใหม่: ปุ่ม oklch(0.71 0.202 349.761) (ชมพู) · พื้นหลัง oklch(0.15 0.09 281.288) (ม่วงเข้ม)

อ่านบล็อก themes: ทีละส่วน (ไวยากรณ์นี้ตรวจกับ daisyUI 5.7.50 ที่ติดตั้งจริงแล้ว ใช้ได้ตามสไลด์หน้า 30):

อยากลองเปลี่ยนธีมเองโดยยังไม่แก้ไฟล์: เปิดหน้า /posts ใน Chrome → กด F12 (หรือ คลิกขวา → Inspect) → แท็บ Console → พิมพ์:

document.documentElement.setAttribute("data-theme", "dark")

document.documentElement = แท็ก <html> ของหน้านี้ · setAttribute("ชื่อ", "ค่า") = ใส่ attribute ให้แท็กนั้น · กด Enter แล้วหน้าเปลี่ยนเป็นโหมดมืดทันที · refresh แล้วกลับเป็นเหมือนเดิม เพราะเราแก้แค่ในเบราว์เซอร์ (ปุ่มสลับธีมที่จำค่าได้ทำใน Lab บท 129)

🪧 เทียบให้เห็นภาพ — ป้ายธีมร้านกับ "สีประจำร้าน"

สั่งพนักงานทาสีว่า "ทาน้ำเงินเบอร์ 600" (bg-blue-600) → ไม่ว่าร้านจะจัดธีมอะไร ผนังนั้นก็น้ำเงินเบอร์ 600 · สั่งว่า "ทาสีประจำร้าน" (btn-primary) → พนักงานไปดูป้ายธีมหน้าร้าน (data-theme บน <html>) ว่าวันนี้ธีมอะไร แล้วใช้สีของธีมนั้น · เปลี่ยนป้ายเดียว ทั้งร้านเปลี่ยนตาม

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

🗣 ศัพท์ที่เพิ่งเรียนไป
semantic color (primary · base-100 · base-content)
สีที่ตั้งชื่อตามหน้าที่ ค่าจริงเปลี่ยนตามธีม · ตรงข้ามกับสีตายตัวอย่าง blue-600 gray-700 · พิมพ์บอก AI ได้ว่า "use daisyUI semantic colors (bg-base-100, text-base-content, btn-primary) instead of fixed colors like gray-700, so dark mode works"
data-theme · --default · --prefersdark
attribute บน <html> ที่บอก daisyUI ว่าใช้ธีมไหน · --default = ธีมเมื่อไม่ได้ระบุ · --prefersdark = ธีมเมื่อเครื่องผู้ใช้ตั้งโหมดมืด · ถาม senior ได้ว่า "เราจะให้ธีมตามเครื่องผู้ใช้ หรือให้ผู้ใช้กดเลือกแล้วจำไว้"

★ กับดักชื่อคลาสที่ต่อ string — ฉบับ ERB

บท 72 หัวข้อ 6 เจอกับดักนี้ใน React แล้ว: className={`text-${color}-600`} สีหาย · ใน ERB หน้าตาต่างกันนิดเดียว แต่เป็นเรื่องเดียวกันทุกอย่าง · และเป็นโค้ดที่ AI เขียนให้บ่อยมาก เพราะมันดูฉลาดและสั้น

ลองใน app/views/posts/_post.html.erb (แม่พิมพ์บทความหนึ่งชิ้น จากบท 126) · อยากให้ชื่อหมวด "ของหวาน" เป็นสีชมพู หมวดอื่นน้ำเงิน:

-    <span class="font-medium text-blue-700"><%= post.category.name %></span>
+    <% color = post.category.slug == "desserts" ? "pink" : "blue" %>
+    <span class="font-medium text-<%= color %>-700"><%= post.category.name %></span>

บรรทัด <% %> คำนวณในครัวเงียบ ๆ (บท 122): ถ้า slug ของหมวดเป็น "desserts" ได้คำว่า "pink" ไม่งั้น "blue" (เงื่อนไข ? ค่าถ้าจริง : ค่าถ้าเท็จ = if-else แบบบรรทัดเดียว · หน้าตาเดียวกับใน TypeScript) · บรรทัดถัดมาเอาคำนั้นไปเสียบกลางชื่อคลาส · รัน bin/dev แล้วเปิด /posts (bin/dev = เปิด server พร้อมตัวที่สร้าง CSS ใหม่ให้ทุกครั้งที่แก้ไฟล์ · บท 120 เคยใช้ · ละเอียดในหัวข้อถัดไป)

ก่อนแก้ ชื่อหมวดทุกหมวดเป็นสีน้ำเงิน · หลังแก้ "ของหวาน" จะชมพูไหม? แล้วหมวดอื่นยังน้ำเงินไหม?

ไม่มีหมวดไหนมีสีเลย — แม้แต่น้ำเงินที่เมื่อกี้ยังขึ้นอยู่ · HTML ที่ได้ (curl) มีคลาสครบถูกต้อง:

    <span class="font-medium text-blue-700">เครื่องดื่ม</span>
    <span class="font-medium text-blue-700">อาหารริมทาง</span>
    <span class="font-medium text-pink-700">ของหวาน</span>

แต่ในไฟล์ CSS ไม่มีกฎของทั้งคู่:

$ grep -c 'text-pink-700' app/assets/builds/tailwind.css
0
$ grep -c 'text-blue-700' app/assets/builds/tailwind.css
0

Chrome วัดสีชื่อหมวดได้ oklch(0.551 0.027 264.364) ทุกหมวด = สีเดียวกับ <p class="text-sm text-gray-500"> ที่ครอบ <span> นี้อยู่ (span ไม่มีกฎสีของตัวเอง เลยใช้สีของแท็กที่ครอบมันไว้)

น้ำเงินหายเพราะคำว่า text-blue-700 เคยอยู่ในไฟล์นี้ที่เดียวในทั้งโปรเจกต์ · พอเราลบมันไปแทนด้วยการต่อ string ฝ่ายจัดซื้อก็ไม่เห็นคำนี้อีก เลยไม่ซื้อ

เหตุผลเดียวกับเรื่องฝ่ายจัดซื้อตั้งแต่ต้นบท: Tailwind อ่านไฟล์เป็นตัวหนังสือ ไม่ได้รันโค้ดของเรา · สิ่งที่มันเห็นในไฟล์คือ text-<%= color %>-700 ซึ่งไม่ใช่ชื่อคลาสอะไรเลย · คำว่า text-pink-700 เพิ่งเกิดตอน Rails เติมแม่แบบ ซึ่งตอนนั้นไฟล์ CSS ถูกสร้างเสร็จไปนานแล้ว

🗣 เทียบให้เห็นภาพ — เมนูที่สั่งด้วยปากตอนเสิร์ฟ

ฝ่ายจัดซื้อซื้อของก่อนร้านเปิด จากเมนูที่เขียนบนกระดาษ · บนกระดาษเขียนว่า "แกงสี ___ (แล้วแต่ลูกค้า)" — ไม่มีชื่อแกงไหนเต็ม ๆ ฝ่ายจัดซื้อเลยไม่ซื้ออะไรเลย · ตอนเสิร์ฟ คนจัดจานเติมช่องว่างว่า "แกงเขียวหวาน" — แต่ในตู้ไม่มีพริกแกงเขียว จานออกไปเปล่า ๆ
ท่าที่ถูก = เขียนบนกระดาษให้ครบว่า "แกงเขียวหวาน หรือ แกงเผ็ด" แล้วตอนเสิร์ฟค่อยเลือกหนึ่งอย่าง · ฝ่ายจัดซื้อเห็นทั้งสองชื่อ ซื้อมาครบ

ที่ไม่เหมือนกัน: ร้านจริงขาดของ พนักงานยังวิ่งไปซื้อเพิ่มกลางกะได้ · Tailwind ไม่ได้อยู่ในร้านตอนเสิร์ฟ มันไม่มีทางรู้ว่าตอนเติมแม่แบบเกิดชื่ออะไรขึ้น · และร้านจริงจานเปล่าลูกค้าบ่นทันที · ของเราไม่มี error สักบรรทัด แค่สีหายเงียบ ๆ

ท่าที่ถูก — เขียนชื่อคลาสเต็มทุกตัว แล้วค่อยเลือก:

<% color_class = post.category.slug == "desserts" ? "text-pink-700" : "text-blue-700" %>
<span class="font-medium <%= color_class %>"><%= post.category.name %></span>
$ grep -c 'text-pink-700' app/assets/builds/tailwind.css
1
$ grep -c 'text-blue-700' app/assets/builds/tailwind.css
1

ตอนนี้คำ "text-pink-700" กับ "text-blue-700" อยู่ในไฟล์เป็นตัวหนังสือครบทั้งคำ ฝ่ายจัดซื้อเห็นทั้งคู่ · Chrome วัดได้ชมพู oklch(0.525 0.223 3.958) · น้ำเงิน oklch(0.488 0.243 264.376)

ทำไมบางทีมัน "ดูเหมือนใช้ได้" · ตัวอย่างในสไลด์หน้า 31 ใช้ text-<%= color %>-500 กับ "red"/"green" · ในแอปนี้สีขึ้นจริง — เพราะไฟล์ที่ scaffold สร้างมามีคำนี้เต็ม ๆ อยู่แล้ว:

$ git grep -n -e "text-red-500" -e "text-green-500" app/views
app/views/categories/_form.html.erb:3:    <div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-md mt-3">
app/views/categories/index.html.erb:5:    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-md inline-block" id="notice"><%= notice %></p>
…

(git grep = ค้นคำในไฟล์ที่ git เก็บอยู่ · -n = บอกเลขบรรทัด · -e หน้าแต่ละคำ = ค้นหลายคำพร้อมกัน · … = ตัดบรรทัดที่เหลือออก)

โค้ดที่ผิดจึงรอดเพราะโชค · วันหนึ่งมีคนเปลี่ยนเป็น "orange" หรือแก้หน้าตากล่องแจ้งเตือนของ scaffold จนคำนั้นหายไป สีก็หายโดยไม่มีใครแตะบรรทัดนั้นเลย

หลักการที่กว้างกว่า Tailwind: เครื่องมือที่ "อ่านโค้ดโดยไม่รันมัน" (static analysis) มองไม่เห็นของที่โปรแกรมประกอบขึ้นตอนรันเสมอ · ต่อไปเจอเครื่องมือไหนที่ "อ่านไฟล์แล้วเลือกเก็บของ" ให้ถามตัวเองว่ามันเห็นชื่อเต็มไหม

หาจุดต่อ string ในโปรเจกต์ทีเดียวทั้งหมดได้ด้วยคำสั่งนี้ (ลองกับโค้ดที่ผิดข้างบน):

$ git grep -n -E '[a-z]-(<%=|#\{)' app/views
app/views/posts/_post.html.erb:5:    <span class="font-medium text-<%= color %>-700"><%= post.category.name %></span>

แปลว่า "หาบรรทัดใน app/views ที่มีตัวอักษร ตามด้วย - แล้วตามด้วย <%= หรือ #{ ทันที" — คือรูปแบบ text-<%= … %> และ "bg-#{…}" (การเสียบค่าใน string ของ Ruby · บท 91) · -E = ให้ | ในวงเล็บแปลว่า "หรือ" · \{ = ปีกกาจริง ๆ · โค้ดที่สะอาดจะไม่พิมพ์อะไรออกมาเลย

🗣 ศัพท์ที่เพิ่งเรียนไป
dynamic class name (ชื่อคลาสที่ต่อ string)
ชื่อคลาสที่ประกอบจากตัวแปรตอนรัน (text-<%= color %>-700 · "bg-#{status}-100") · Tailwind มองไม่เห็น · แก้ด้วยการเขียนชื่อเต็มทุกตัวแล้วเลือก · พิมพ์บอก AI ได้ว่า "never build Tailwind class names with interpolation — write every full class name literally and pick one"
static analysis
การวิเคราะห์โค้ดด้วยการอ่านไฟล์ ไม่ได้รัน · เร็วและปลอดภัย แต่เห็นแค่ตัวหนังสือที่เขียนไว้ · Tailwind และเครื่องมือตรวจโค้ดอีกหลายตัวทำงานแบบนี้ · ถาม senior ได้ว่า "เครื่องมือตัวนี้ทำงานแบบ static ใช่ไหม ต้องระวังของที่สร้างตอนรันตรงไหนบ้าง"

bin/dev — เปิดร้านพร้อมฝ่ายจัดซื้อประจำกะ

ทุกครั้งที่เราเพิ่มคลาสใหม่ ไฟล์ CSS ต้องถูกสร้างใหม่ · ถ้าต้องสั่ง tailwindcss:build เองทุกครั้งคงเหนื่อย · bin/rails tailwindcss:watch คือตัวที่เฝ้าไฟล์ แก้เมื่อไหร่ build ให้เองทันที · และ bin/dev คือคำสั่งที่เปิด server กับตัวเฝ้าไฟล์พร้อมกันในคำสั่งเดียว ตามรายการในไฟล์ Procfile.dev:

# Procfile.dev
web: bin/rails server
css: bin/rails tailwindcss:watch

หนึ่งบรรทัด = หนึ่งโปรแกรม · ชื่อหน้า : (web · css) เป็นป้ายที่จะโผล่หน้าทุกบรรทัดของ output · รันแล้วได้ (ตัดท้ายมา):

$ bin/dev
10:57:32 web.1  | started with pid 25776
10:57:32 css.1  | started with pid 25777
10:57:32 web.1  | => Booting Puma
…
10:57:33 web.1  | * Listening on http://127.0.0.1:3000
10:57:33 web.1  | * Listening on http://[::1]:3000
10:57:33 web.1  | Use Ctrl-C to stop
10:57:33 css.1  | ≈ tailwindcss v4.3.3
10:57:33 css.1  |
10:57:33 css.1  | /*! 🌼 daisyUI 5.7.50 */
10:57:33 css.1  | Done in 193ms

web.1 = server ของ Rails (ข้อความชุดเดียวกับ bin/rails server) · css.1 = ตัวเฝ้าไฟล์ของ Tailwind ที่ build รอบแรกให้ทันที · แก้ view ทีหลังจะเห็นบรรทัด css.1 | Done in … โผล่เพิ่ม · กด Ctrl-C ทีเดียวปิดทั้งคู่ · pid = เลขประจำโปรแกรมที่ระบบแจกให้ · เวลาและเลข pid ของคุณจะต่าง
(bin/dev ใช้โปรแกรมชื่อ foreman จัดการหลายโปรแกรมพร้อมกัน · ครั้งแรกถ้าเครื่องไม่มี มันจะขึ้น Installing foreman... แล้วลงให้เอง)

ทีนี้ลองแบบลืม · ปิด bin/dev แล้วเปิดแค่ bin/rails server · เพิ่มคลาส text-orange-600 ให้หัวข้อหน้า /posts:

$ curl -s localhost:3000/posts | grep -o '<h1[^>]*>'
<h1 class="font-bold text-3xl sm:text-4xl text-orange-600">
$ grep -c text-orange-600 app/assets/builds/tailwind.css
0

curl -s localhost:3000/posts = ขอหน้า /posts แบบที่เบราว์เซอร์ได้รับ · | = ส่งผลต่อให้คำสั่งถัดไป · grep -o '<h1[^>]*>' = ดึงเฉพาะแท็กเปิด <h1 …> · HTML มีคลาสใหม่แล้ว (Rails อ่าน view ใหม่ทุกครั้งตอน development) · แต่ไฟล์ CSS ยังเป็นฉบับเก่า ไม่มีใครสร้างให้ · หน้าจอจึงไม่ส้ม · ปิด server แล้วเปิด bin/dev แทน:

$ grep -c text-orange-600 app/assets/builds/tailwind.css
1

สไลด์หน้า 32 อ้างว่า "เพิ่มคลาสแล้วไม่เห็นผล 90% คือลืมรัน bin/dev" (ตัวเลขนี้ไม่ได้วัด) · แต่อาการตรงกับที่เราเห็นทุกอย่าง · เช็คข้อนี้เป็นอย่างแรกทุกครั้ง

กับดักซ้อน — ตัวเฝ้าไฟล์จำคลาสเก่า: บนเครื่องเห็นสี แต่เครื่องจริงไม่เห็น

production = เครื่อง server จริงที่ผู้ใช้เข้า · ตอนเอาแอปขึ้นไป (deploy) ไฟล์ CSS จะถูกสร้างใหม่ตั้งแต่ศูนย์ด้วย assets:precompile (บท 127) · กับดักต่อ string ข้างบนสีหายตั้งแต่บนเครื่องเรา (ทดลองผ่าน bin/dev) — แต่มีอยู่ทางหนึ่งที่บนเครื่องยังเห็นสี · ผมลองลำดับนี้โดยเปิด bin/dev ค้างไว้ตลอด:

ขั้นโค้ดใน _post.html.erbgrep -c pink / blueสีบนจอ
1ต่อ string (เปิด bin/dev ตอนนี้)0 / 0ไม่มีสี
2เปลี่ยนเป็นชื่อเต็ม1 / 1มีสี
3เปลี่ยนกลับเป็นต่อ string (เช่น AI "จัดโค้ดให้สั้นลง")1 / 1ยังมีสี
4เหมือนขั้น 3 · สั่ง bin/rails assets:precompile (แบบขึ้นเครื่องจริง · บท 127)0 / 0 ในไฟล์ public/assets/tailwind-…cssบน production ไม่มีสี

ขั้น 3 คือตัวการ: ตัว watch ที่เปิดค้างอยู่เพิ่มคลาสใหม่ที่เพิ่งเห็น แต่ไม่ลบคลาสเก่าที่เคยเห็น จนกว่าจะปิดแล้วเปิดใหม่ · บนเครื่องเราจึงยังเห็นสีจากของที่ "จำไว้" · พอสร้างใหม่ตั้งแต่ศูนย์ (ปิดเปิด bin/dev · tailwindcss:build · precompile ตอน deploy) ของที่จำไว้หายหมด
บทเรียน: อย่าเชื่อสายตาบนเครื่องตัวเอง · ใช้ grep หลัง build ใหม่ทุกครั้งเป็นตัวตัดสิน

🧑‍🍳 เทียบให้เห็นภาพ — เปิดร้านพร้อมฝ่ายจัดซื้อประจำกะ

bin/rails server อย่างเดียว = เปิดครัวรับออร์เดอร์ แต่ฝ่ายจัดซื้อไม่ได้เข้ากะ · เชฟเพิ่มเมนูใหม่ในกระดาษ ไม่มีใครไปซื้อวัตถุดิบ จานออกมาขาดของ · bin/dev = เปิดครัวพร้อมฝ่ายจัดซื้อประจำกะที่ยืนเฝ้ากระดาษเมนู เห็นเมนูใหม่ปุ๊บวิ่งไปซื้อปั๊บ

ที่ไม่เหมือนกัน: ฝ่ายจัดซื้อประจำกะคนนี้ไม่เคยทิ้งของจนกว่าจะหมดกะ — เมนูที่ขีดฆ่าไปแล้ว วัตถุดิบยังอยู่ในตู้ (ขั้น 3 ในตารางข้างบน) · ร้านจริงถ้าเก็บของค้างไว้ก็แค่เปลืองที่ · แต่ของเราทำให้บนเครื่องเราหน้าตาดีกว่าของจริง ซึ่งอันตรายกว่า

สรุปคำสั่งที่ใช้บ่อยของเรื่อง CSS (รายการเต็มของ gem ดูได้ด้วย bin/rails -T tailwindcss):

คำสั่งใช้ตอนไหน
bin/devทำงานปกติทุกวัน — server + Tailwind watch
bin/rails tailwindcss:buildสร้าง CSS ใหม่หนึ่งรอบ แล้วจบ · ใช้ตอนพิสูจน์ด้วย grep หรือหลัง clobber
bin/rails assets:precompileลองเตรียมไฟล์แบบขึ้นเครื่องจริง (บท 127) · ลองแล้วต้อง clobber ทุกครั้ง
bin/rails assets:clobberล้างของที่ precompile ไว้ — ลบ app/assets/builds/tailwind.css ไปด้วย ต้อง build ใหม่ (บท 127)
bin/importmap jsonดูว่า JavaScript ชื่ออะไรชี้ไปไฟล์ไหน (บท 127) · ส่วน bin/importmap pin <ชื่อ> ไปโหลด library จากเน็ต ใช้เมื่อตั้งใจเพิ่มจริงเท่านั้น

และใครทำอะไรในบล็อก B (สไลด์หน้า 33 ข้อ 2 · จับคู่ให้แล้ว):

ชื่อหน้าที่ในร้านอาหาร
Propshaftติดเลขรุ่น (fingerprint) แล้วเสิร์ฟไฟล์พนักงานติดเลขรุ่นบนปกเมนูแล้ววางบนชั้น
importmapแมปชื่อ library ไปหาไฟล์สมุดรายชื่อ "ชื่อเล่น → อยู่ชั้นไหน"
tailwindcss-railsสร้าง CSS จากคลาสที่สแกนเจอฝ่ายจัดซื้อที่อ่านเมนูบนกระดาษ
daisyUIชั้น component + ธีม บน Tailwindชุดจานชามสำเร็จรูป + ป้ายธีมร้าน
🗣 ศัพท์ที่เพิ่งเรียนไป
bin/dev · Procfile.dev
คำสั่งเปิดหลายโปรแกรมพร้อมกันตามรายการใน Procfile.dev (ตอนนี้มี web กับ css) · พิมพ์บอก AI ได้ว่า "I'm running bin/dev (web + tailwindcss:watch), but my new class still has no style" — มันจะได้ไม่ตอบว่า "ลืมรัน watch หรือเปล่า"
watch mode
โหมดที่โปรแกรมเฝ้าไฟล์แล้วทำงานใหม่ทุกครั้งที่ไฟล์เปลี่ยน · ของ Tailwind เพิ่มคลาสใหม่ได้ แต่ไม่ลบคลาสเก่าจนกว่าจะเริ่มใหม่ · ถาม senior ได้ว่า "ก่อน deploy เราตรวจ CSS จาก build ที่สร้างใหม่หมดหรือยัง"

เอาไปใช้กับ AI — ประโยคใน prompt และ checklist ตรวจงาน

ตอนนี้คุณทำเองเป็นแล้ว · เวลาสั่ง AI ทำงานหน้าตา (Lab บท 129 จะทำจริง) ให้แนบ @prompts/00-context.md เสมอ (มีบรรทัด "ห้ามสร้าง tailwind.config.js" อยู่แล้ว) · และใส่ข้อห้ามนี้ในส่วน "ห้าม" ของ prompt (โครง 4 ส่วนจากบท 89):

ห้าม:
- สร้าง tailwind.config.js หรือ package.json / npm install
- เปลี่ยน @plugin "./daisyui.mjs" เป็น @plugin "daisyui"
- ต่อชื่อคลาสจากตัวแปร (เช่น text-<%= color %>-500 หรือ "bg-#{x}-100")
  ให้เขียนชื่อคลาสเต็มทุกตัว แล้วเลือกด้วยเงื่อนไข

AI ส่งงานมาแล้ว ตรวจรับด้วย 4 ข้อนี้ (ทุกข้อเป็นคำสั่งที่คุณรันเป็นแล้วในบทนี้):

  1. ls tailwind.config.js package.json → ต้องขึ้น No such file or directory ทั้งคู่
  2. git grep -n -E '[a-z]-(<%=|#\{)' app/views → ต้องไม่มีผลลัพธ์
  3. bin/rails tailwindcss:build แล้ว grep -c คลาสใหม่ที่ AI ใส่มาสัก 2–3 ตัว → ต้องได้ 1 ขึ้นไป
  4. เปลี่ยน data-theme เป็น dark ใน Console → ตัวหนังสือยังอ่านออกทุกจุด (ถ้าจม = AI ใช้สีตายตัวแทนสีตามหน้าที่)

🎯 ภารกิจ (~10 นาที) — หากับดัก แก้ แล้วพิสูจน์ด้วย grep

สมมติ AI เขียนป้าย "ใหม่ / เก่า" ให้บทความ (ใหม่ = เขียนภายใน 7 วัน) · ก็อปไปวางใน app/views/posts/_post.html.erb ใต้บรรทัด </p> ตัวแรก (บรรทัดที่ปิดชื่อหมวดกับเวลา):

<% tone = post.created_at > 7.days.ago ? "amber" : "slate" %>
<span class="<%= "mt-2 inline-block rounded px-2 text-xs bg-#{tone}-100" %>">
  <%= post.created_at > 7.days.ago ? "ใหม่" : "เก่า" %>
</span>

7.days.ago = "เวลาเมื่อ 7 วันก่อน" (ของแถมจาก Rails) · post.created_at > 7.days.ago = "เขียนหลังจาก 7 วันก่อน" = ใหม่ · #{tone} = เสียบค่าตัวแปรกลาง string ของ Ruby
บรรทัด 2 มีเครื่องหมายคำพูดซ้อนกัน (class="<%= "…" %>") ไม่พัง เพราะ Rails ทำส่วน <%= %> เสร็จก่อนส่ง · เบราว์เซอร์ได้แค่ class="mt-2 … bg-amber-100"

  1. สั่ง bin/rails tailwindcss:build แล้วนับ: grep -c 'bg-amber-100' app/assets/builds/tailwind.css และ grep -c 'bg-slate-100' app/assets/builds/tailwind.css
  2. สั่ง git grep -n -E '[a-z]-(<%=|#\{)' app/views · ควรเจอบรรทัดที่เพิ่งวาง
  3. แก้ให้ถูก (ใบ้: ตั้งตัวแปร tone_class ที่เก็บชื่อคลาสเต็ม แล้วย้ายคลาสที่ตายตัวออกมานอก <%= %>) · build ใหม่ แล้วนับด้วย grep -c อีกรอบ
  4. เปิด bin/dev แล้วดู /posts

ควรเห็น: ข้อ 1 ได้ 0 ทั้งคู่ · ข้อ 2 เจอ 1 บรรทัด · ข้อ 3 ได้ 1 ทั้งคู่ และคำสั่ง git grep ไม่พิมพ์อะไร · ข้อ 4 ป้าย "ใหม่" พื้นเหลืองอ่อน "เก่า" พื้นเทาอ่อน (หน้าแรกของเรามี "ใหม่" 6 ชิ้น "เก่า" 4 ชิ้น · ของคุณขึ้นกับวันที่รัน seeds)

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

เห็นอะไรแปลว่า
ข้อ 1 ได้ 1 ทั้งที่ยังไม่แก้ มีไฟล์อื่นในโปรเจกต์เขียนคำนั้นไว้เต็ม ๆ (เช่นไฟล์จดโน้ตที่ไม่อยู่ใน .gitignore) · หาด้วย git grep --untracked -l 'bg-amber-100' (--untracked = ค้นไฟล์ที่ git ยังไม่ได้เก็บด้วย · -l = บอกแค่ชื่อไฟล์ · ผมลองวางไฟล์ notes.txt ที่มีคำนี้ไว้ ได้ build 1 และคำสั่งนี้ชี้ไปที่ notes.txt) · หรือคุณเปิด bin/dev ค้างไว้ตอนลองแบบชื่อเต็ม (watch จำ) → ปิดแล้ว build ใหม่
grep -c ได้ 1 แล้ว แต่หน้าเว็บยังไม่มีสี มี public/assets ค้างจากการลอง precompile (บท 127) → bin/rails assets:clobber → bin/rails tailwindcss:build → เปิด server ใหม่
ป้ายมีสี แต่ไม่มีมุมมน ไม่มีระยะ คุณย้าย rounded px-2 text-xs ไปต่อ string อีกที่ (เช่น "rounded-#{size}") · รันคำสั่ง git grep ของข้อ 2 อีกรอบ
$ bin/rails tailwindcss:build
$ grep -c 'bg-amber-100' app/assets/builds/tailwind.css
0
$ grep -c 'bg-slate-100' app/assets/builds/tailwind.css
0

ข้อสังเกต: rounded px-2 text-xs ที่อยู่ใน string เดียวกันได้กฎครบ (ผมเช็คแล้ว ก่อนวางโค้ดนี้ทั้งสามตัวไม่มีในไฟล์ CSS) — ฝ่ายจัดซื้ออ่านทุกคำที่หน้าตาเป็นชื่อคลาส แม้อยู่ใน string ของ Ruby · พังเฉพาะคำที่ถูกต่อ

โค้ดที่แก้แล้ว:

<% tone_class = post.created_at > 7.days.ago ? "bg-amber-100" : "bg-slate-100" %>
<span class="mt-2 inline-block rounded px-2 text-xs <%= tone_class %>">
  <%= post.created_at > 7.days.ago ? "ใหม่" : "เก่า" %>
</span>
$ bin/rails tailwindcss:build
$ grep -c 'bg-amber-100' app/assets/builds/tailwind.css
1
$ grep -c 'bg-slate-100' app/assets/builds/tailwind.css
1

Chrome วัดพื้นป้าย "ใหม่" ได้ oklch(0.962 0.059 95.617) (เหลืองอ่อน) · "เก่า" ได้ oklch(0.968 0.007 247.896) (เทาอมฟ้าอ่อน)

ก่อนไปบท 129 ล้างของทดลองทั้งบท:

git restore .
rm -f tailwind.config.js
bin/rails assets:clobber
bin/rails tailwindcss:build
git status

git status ต้องขึ้น nothing to commit, working tree clean · (rm -f ลบไฟล์ทดลองที่ git ยังไม่รู้จัก ถ้าคุณสร้างตามหัวข้อ v3/v4 · ไม่มีก็ไม่เป็นไร)

1. (จากสไลด์หน้า 33 ข้อ 1) ในแอปนี้เขียน <% level = post.created_at > 7.days.ago ? "red" : "gray" %> แล้ว <span class="badge bg-<%= level %>-100"> · ผลคือป้ายบทความเก่าพื้นเทาขึ้นปกติ แต่ป้ายบทความใหม่ไม่มีพื้นแดง · เพราะอะไร?

ตัวเลือกที่ 3 ถูก · ผมลอง build แล้ว grep -c '\.bg-gray-100{' ได้ 1 (มาจากปุ่ม "Show" "Edit" ในหน้าหมวดที่ scaffold สร้างไว้ และปุ่ม "แก้ไข" "กลับไปหน้ารวม" ในหน้าบทความ) · \.bg-red-100{ ได้ 0 · สีเทาจึงรอดเพราะโชค สีแดงไม่รอด · แก้ด้วยการเขียน "bg-red-100" "bg-gray-100" เต็มทั้งคำแล้วเลือก

ตัวเลือกที่ 1 ผิด — daisyUI ไม่มีกฎห้ามแบบนั้น คลาสแดงไม่มาเพราะไม่มีในไฟล์ CSS · ตัวเลือกที่ 2 ผิด — HTML ที่ได้มี bg-red-100 จริงสำหรับบทความใหม่ Ruby คำนวณถูก · ปัญหาอยู่ที่ฝั่ง CSS · ตัวเลือกที่ 4 ผิด — สีอย่าง red-100 เป็นของ Tailwind ไม่ได้มากับธีมของ daisyUI

จุดที่ต้องจำ: สีจากการต่อ string บางตัว "ดูเหมือนใช้ได้" เพราะบังเอิญมีชื่อเต็มในไฟล์อื่น · ตัดสินด้วย grep หลัง build ไม่ใช่ด้วยสายตา

2. (จากสไลด์หน้า 33 ข้อ 3) ขอให้ AI เพิ่มสีประจำร้าน มันตอบว่า "สร้างไฟล์ tailwind.config.js ใส่ content กับ plugins: [require("daisyui")]" · ควรทำยังไง?

ตัวเลือกที่ 2 ถูก · ผมสร้าง tailwind.config.js ที่ตั้งสี mint แล้วใช้ text-mint · build ผ่านแต่ grep -c text-mint ได้ 0 · ส่วน --color-brand ใน @theme ได้ .text-brand จริง

ตัวเลือกที่ 1 ผิด — v4 ไม่อ่านไฟล์นี้อัตโนมัติเลย ไม่ใช่อ่านก่อนหลัง · ตัวเลือกที่ 3 ผิด — ถ้าลบ @plugin daisyUI จะหายทั้งชุด เพราะไฟล์ JS นั้นไม่ถูกอ่าน · และแอปเราไม่มี package daisyui ให้ require · ตัวเลือกที่ 4 ผิด — ไม่มี error เลย นี่แหละที่ทำให้กับดักนี้น่ากลัว

จุดที่ต้องจำ: Tailwind v4 ตั้งค่าใน CSS · ไวยากรณ์ v3 มักไม่ error แค่ไม่มีผล · เขียนห้ามไว้ใน 00-context.md ตั้งแต่แรก

3. หน้าหนึ่งมีปุ่ม class="btn btn-primary" กับปุ่ม class="bg-blue-600 text-white" · เปลี่ยน <html> เป็น data-theme="dark" · ปุ่มไหนเปลี่ยนสี?

ตัวเลือกที่ 1 ถูก · วัดใน Chrome จริง: btn-primary เปลี่ยนจาก oklch(0.45 0.24 277.023) เป็น oklch(0.58 0.233 277.117) · bg-blue-600 เป็น oklch(0.546 0.245 262.881) ทั้งสองธีม

ตัวเลือกที่ 2 ผิด — กลับกัน สีที่ล็อกคือเลขสีตายตัวของ Tailwind · ตัวเลือกที่ 3 ผิด — data-theme เปลี่ยนเฉพาะสีที่อ้างชื่อตามหน้าที่ (primary · base-100 …) · สีตายตัวไม่สนธีม · ตัวเลือกที่ 4 ผิด — ทุกธีมที่เปิดไว้อยู่ในไฟล์ CSS แล้ว เบราว์เซอร์สลับได้ทันทีไม่ต้อง build

จุดที่ต้องจำ: อยากให้ธีมมืดใช้ได้ทั้งหน้า ต้องใช้สีตามหน้าที่ · สีตายตัวคือจุดที่ "จม" เวลาสลับธีม

4. เพื่อนเปิด bin/rails server ค้างไว้ เพิ่ม text-orange-600 ให้หัวข้อ · curl เห็นคลาสใน HTML แล้ว แต่หน้าจอไม่ส้ม · สาเหตุที่น่าจะเป็นที่สุดคืออะไร?

ตัวเลือกที่ 4 ถูก · ผมลองแล้ว: HTML มี text-orange-600 แต่ grep -c ในไฟล์ CSS ได้ 0 · พอเปิด bin/dev ได้ 1

ตัวเลือกที่ 1 ผิด — curl เห็นคลาสใหม่แล้ว แปลว่า HTML ใหม่มาแล้ว ปัญหาอยู่ที่ไฟล์ CSS · ตัวเลือกที่ 2 ผิด — สีมาตรฐานอย่าง orange-600 มีให้อยู่แล้ว @theme ใช้เพิ่มสีของเราเอง · ตัวเลือกที่ 3 ผิด — ตอน development Rails อ่าน view ใหม่ทุก request อยู่แล้ว ไม่ต้องรีสตาร์ต (HTML ที่มีคลาสใหม่คือหลักฐาน)

จุดที่ต้องจำ: HTML มีคลาส แต่ไม่มีผล → ถามก่อนเลยว่า "เปิด bin/dev อยู่ไหม" แล้วพิสูจน์ด้วย grep -c

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

หน้าสไลด์เขียนของจริง (tailwindcss-rails 4.7 · daisyUI 5.7.50)ต้องสนใจไหม
29ไฟล์ app/assets/stylesheets/application.tailwind.css แอปนี้ใช้ app/assets/tailwind/application.css (ที่อยู่ของ tailwindcss-rails รุ่นใหม่) ใช่ — แก้ไฟล์ที่ถูก
29–30@plugin "daisyui"; daisyUI ถูก vendor เป็นไฟล์ · ต้องเขียน @plugin "./daisyui.mjs"; · พิมพ์ตามสไลด์ได้ Can't resolve 'daisyui' ใช่ — หัวข้อ daisyUI
30themes: light --default, dark --prefersdark, synthwave; ใช้ได้จริง · light + dark เป็นค่าเริ่มต้นอยู่แล้ว (ไม่เขียนก็ได้ CSS เหมือนกันทุกไบต์) รู้ไว้
31"บนเครื่องเห็นสี เพราะ dev สร้างทุกคลาส" bin/dev สแกนแบบเดียวกับ build จริง สีหายตั้งแต่บนเครื่อง · ที่ "เห็นบนเครื่อง" มาจาก watch จำคลาสเก่า หรือชื่อเต็มบังเอิญมีในไฟล์อื่น รู้ไว้ — หัวข้อกับดัก
31text-<%= color %>-500 กับ red/green ในแอปนี้สีขึ้น เพราะ scaffold มี text-red-500 text-green-500 เต็ม ๆ อยู่แล้ว · ลองกับสีอื่นจึงเห็นบั๊ก รู้ไว้
32"90% คือลืมรัน bin/dev" ตัวเลขไม่ได้วัด · อาการตรงกับที่เราลอง ไม่ต้อง

อ้างอิง