- name
- app-shell
- description
- ข้อกำหนดว่าทุกแอปของนายช่างหมูต้องมีเปลือกเหมือนกัน — แถบนำทางที่กลับหน้าหลักได้ ชื่อแอปกับคำบรรยาย ทางไปแอปทั้งหมด และท้ายเว็บ ใช้ skill นี้ทุกครั้งที่สร้างแอปใหม่ เพิ่มหน้าใหม่ให้แอปเดิม หรือทำต้นแบบที่ผู้ใช้จะได้เห็น
Trigger เมื่อ: "สร้างแอปใหม่", "เพิ่มแอป", "ทำต้นแบบ", "ไม่มี header", "แถบนำทาง", "กลับหน้าหลัก", "แอปทั้งหมด", "ทำให้เหมือนกันทุกแอป", "app shell", "app header", "prototype page"
ห้ามเขียนแถบนำทางขึ้นมาเองในไฟล์ของแอป ให้ใช้ของกลางเสมอ
# เปลือกของแอป — ทุกแอปต้องเหมือนกัน
## กฎเดียวที่ต้องจำ
**ผู้ใช้ที่เปิดแอปไหนของนายช่างหมูก็ตาม ต้องเจอทางกลับบ้านที่เดิม หน้าตาเดิม และคำเดิม**
คนที่หลงอยู่ในแอปแล้วหาทางออกไม่เจอ จะปิดแท็บทิ้ง ไม่ใช่ไปหาปุ่มที่อื่น และแอปที่หน้าตา
คนละอย่างกันอ่านเหมือนคนละเว็บ ไม่ใช่เหมือนชุดเครื่องมือของเจ้าเดียวกัน
## ของกลางที่ต้องใช้
`src/components/platform/app-shell.tsx` คือเปลือกมาตรฐาน ประกอบด้วยสี่ชั้นตามลำดับ
| ชั้น | สิ่งที่ให้ | มาจาก |
|---|---|---|
| แถบนำทาง | ตรานายช่างหมู · ชื่อพื้นที่ทำงาน · เมนูบัญชี | `SiteHeader` |
| แถบบริบท | ปุ่มบ้าน "หน้าแรก" · "แอปทั้งหมด" · ชื่อแอป · ป้ายสิทธิ์จากทะเบียน | `AppShell` |
| ชื่อแอป | ชื่อโปรแกรมเต็ม และประโยคบอกว่าใช้ทำอะไร | `AppShell` (โหมด app เท่านั้น) |
| ท้ายเว็บ | ข้อมูลผู้ให้บริการและเครื่องหมายรับรอง | `PlatformFooter` |
**ต้นแบบใช้เปลือกเดียวกันผ่านโหมด prototype** — `<AppShell app={app} mode="prototype">`:
ไม่อ่านทะเบียน ไม่มีป้ายสิทธิ์ มีป้าย "ต้นแบบ" แทน และข้ามบล็อกชื่อแอปเพราะ workspace
ของต้นแบบมีหัวเรื่องใหญ่ของตัวเองแล้ว (v0.79.0 ปิด IP-155/156/157 ด้วยโหมดนี้)
```tsx
// src/app/apps/<slug>/layout.tsx
import { AppShell } from "@/components/platform/app-shell";
import { platformApps } from "@/lib/platform";
export default function Layout({ children }: { children: React.ReactNode }) {
const app = platformApps.find((a) => a.slug === "<slug>")!;
return <AppShell app={app}>{children}</AppShell>;
}
```
**ห้ามเขียนแถบนำทางขึ้นมาเองในไฟล์ของแอป** ถ้า `AppShell` ยังให้สิ่งที่ต้องการไม่ได้
ให้แก้ `AppShell` ไม่ใช่เขียนอันใหม่ข้าง ๆ เพราะอันที่สองจะไม่มีวันตามอันแรกทัน
## กับดักที่เคยเกิดจริง
**หนึ่ง — ต้นแบบอยู่นอก `/apps` โดยตั้งใจ และนั่นทำให้มันไม่ได้เปลือก**
ADR 0014 กำหนดว่าแอปเป็น "แอปที่ประกาศแล้ว" เพราะผู้ดูแลกดประกาศ ไม่ใช่เพราะมีคนเขียน
หน้าเว็บหน้าหนึ่ง ต้นแบบจึงอยู่ที่ `/prototype/<ชื่อ>` ไม่ใช่ `/apps/<slug>`
**การย้ายต้นแบบเข้า `/apps` เพื่อให้ได้เปลือก เป็นวิธีแก้ที่ผิด** เพราะมันจะกลายเป็นการอ้างว่า
ประกาศแล้วทั้งที่ยังไม่ได้ประกาศ วิธีที่ถูกคือแยกเปลือกออกมาให้ต้นแบบใส่ได้ **โดยที่แถบบริบท
ไม่แสดงป้ายสิทธิ์และไม่อ้างอะไรเกี่ยวกับความพร้อม** ตามที่ ADR 0015 กำหนด
**สอง — แอปที่เขียนแถบเอง จะเพี้ยนจากของกลางทันทีที่ของกลางเปลี่ยน**
PRICEMETR เคยเขียนแถบของตัวเองไว้ใน `price-workspace.tsx` ชื่อคลาส `gl-platbar`
ผลคือคำว่า "Home Center" เขียนคนละแบบกับ "แอปทั้งหมด" ของ `landingActionContract`
และเมื่อ `AppShell` เพิ่มเมนูบัญชี แถบนั้นก็ไม่ได้ตามมาด้วย — แถบนั้นถูกรื้อแล้วใน v0.79.0
(IP-157) และคำวินิจฉัยปุ่มบ้านของมันย้ายไปเป็นมาตรฐานของ `AppShell` ให้ทุกแอปแทน
**สี่ — ฉากหลังของ workspace ที่เป็นผืน `position: fixed` ทึบ จะทาสีทับแถบบริบทของเปลือก**
ผืนฉากหลังแบบ `.gl-ambient` อยู่ใน stacking context ที่มาทีหลังใน DOM จึงวาดทับ
ของที่อยู่ก่อนหน้า `.app-shell__context` ต้องมี `position: relative; z-index: 1` กันไว้
(เจอจริงตอนย้าย PRICEMETR: แถบอยู่ครบใน DOM, hit-test เจอ แต่ตามองไม่เห็น)
**สาม — ต้นแบบที่ไม่มีเปลือกเลย จะไม่มีใครสังเกตจนกว่าเจ้าของงานจะเห็น**
แอปผู้ช่วยสร้างแผนงานเดินมาถึง v0.59.0 โดยไม่มีทางกลับหน้าหลักเลยสักปุ่ม เพราะทุกคน
ที่ทำงานกับมันเปิดจาก URL ตรง ๆ ไม่มีใครเข้ามาแบบผู้ใช้จริง
## เช็กลิสต์ก่อนบอกว่าแอปหรือหน้าใหม่เสร็จ
- [ ] ใช้ `AppShell` ไม่ได้เขียนแถบเอง
- [ ] มีทางกลับ "แอปทั้งหมด" ที่ใช้คำจาก `landingActionContract` ไม่ใช่คำที่พิมพ์เอง
- [ ] ชื่อแอปและคำบรรยายมาจาก `platformApps` ไม่ใช่ข้อความฝังในหน้า
- [ ] ป้ายสิทธิ์มาจากทะเบียนเท่านั้น แอปที่ยังไม่ประกาศต้องไม่มีป้าย (ADR 0014 · 0015)
- [ ] มี `PlatformFooter`
- [ ] ทุกปุ่มมีปฏิกิริยาตอนชี้และตอนกด ภายใต้ `prefers-reduced-motion`
- [ ] ตรวจทุกความกว้างจริงในเบราว์เซอร์ Edge ไม่ใช่แค่จอเดสก์ท็อป
- [ ] ห้าม emoji ทุกที่
## ฟีเจอร์ที่ทุกแอปต้องมีเท่ากัน
เจ้าของงานกำหนดเมื่อ 2026-08-26 ว่าแอปใหม่ทุกตัวต้องได้ของชุดนี้ตั้งแต่วันแรก
ไม่ใช่ค่อยเติมทีหลังทีละแอป
| ฟีเจอร์ | ที่มาของมาตรฐาน |
|---|---|
| เปลือกและแถบนำทาง | `AppShell` |
| เอกสารพิมพ์ A4 | [docs/PRINT_DESIGN_SPEC.md](../../../docs/PRINT_DESIGN_SPEC.md) |
| การแบ่งหน้าของตารางยาว | [docs/skilldoc-nguad.md](../../../docs/skilldoc-nguad.md) |
| เงินเป็น BigInt satang | ห้ามให้แบบจำลองคืนค่าเป็นบาท |
| สีและตัวอักษร | [DESIGN.md](../../../DESIGN.md) |
| ผู้ช่วย AI ที่ร่างให้แล้วผู้ใช้แก้ | รูปแบบเดียวกันทุกแอป |
| ทางกลับหน้าหลักของแพลตฟอร์ม | ต้องเห็นได้ตลอดเวลา ไม่ใช่โผล่เฉพาะตอนเลื่อน |
| ทางกลับหน้าหลักของแอปเอง | แอปที่มีมากกว่าหนึ่งชั้นต้องมีทางกลับชั้นบนสุดของตัวเอง |
| ป้ายบอกว่าตอนนี้อยู่ที่ไหน | ชื่อชั้นปัจจุบันอยู่คู่กับปุ่มกลับเสมอ |
**ทางกลับต้องมีสามชั้น ไม่ใช่ชั้นเดียว** บทเรียนจาก PRICEMETR ที่เจ้าของงานต้องทักถึงสองรอบ
| ชั้น | พาไปไหน | ต้องเห็นเมื่อไหร่ |
|---|---|---|
| ออกจากแอป | หน้ารวมของแพลตฟอร์ม | ตลอดเวลา ล็อกติดขอบบน |
| หน้าหลักของแอป | จุดเริ่มต้นของแอปนั้น | ทุกที่ที่ไม่ใช่หน้าหลัก |
| ถอยหนึ่งชั้น | ชั้นก่อนหน้า | เมื่ออยู่ลึกกว่าหนึ่งชั้น |
**และห้ามใช้รูปแบบ "ลูกศรซ้าย + ชื่อแบรนด์"** เพราะเป็นลายเซ็นของคู่แข่ง เจ้าของงานสั่งเปลี่ยน
เมื่อ 2026-08-26 ให้ใช้ป้ายที่บอกหน้าที่กับไอคอนบ้าน แทนชื่อแพลตฟอร์ม — ตั้งแต่ v0.79.0
กติกานี้เป็นของ `AppShell` แล้ว: ปุ่มบ้าน "หน้าแรก" (คำจาก `landingActionContract.homeLabel`)
อยู่หัวแถวของแถบบริบททุกแอปโดยอัตโนมัติ ไม่ต้องเขียนเอง
## ก่อนเริ่มงานทุกครั้ง เรียก `/grill-with-docs`
**นี่คือข้อกำหนดของเจ้าของงานเมื่อ 2026-08-26 ไม่ใช่คำแนะนำ** ทุกเซสชันที่รับงานต่อจาก
เอกสารส่งต่อ ต้องเรียก `/grill-with-docs` เป็นสิ่งแรกก่อนแตะโค้ด
สกิลนั้นเรียก `grilling` กับ `domain-modeling` มาทำงานคู่กัน คือซักจนกว่าจะเข้าใจตรงกัน
แล้วเขียนสิ่งที่ตกผลึกลง `CONTEXT.md` และ ADR ทันทีที่ตกผลึก
เหตุผลที่กลายเป็นกฎ เซสชันที่ผ่านมามีทั้งกรณีที่เริ่มด้วยแผนผิดจนเกือบทำโรดแมปหายแปดรายการ
และกรณีที่รื้อหน้าจอสามรอบเพราะไม่ได้ถามก่อนว่า "เหมือนคู่แข่ง" หมายถึงตรงไหน
การซักก่อนลงมือจับเรื่องพวกนี้ได้ตั้งแต่ก่อนเสียเวลา
**เอกสารส่งต่อทุกฉบับต้องมีบรรทัดนี้อยู่ในคำสั่งเปิดเซสชัน** ถ้าฉบับไหนไม่มี ให้เติมก่อนส่งต่อ
## เมื่อพบแอปที่ไม่ตรงมาตรฐาน
อย่ารีบแก้ให้ตรงทันที ให้ถามก่อนว่ามาตรฐานถูกหรือแอปนั้นถูก บางครั้งแอปที่แตกแถวคือแอปที่
เจอความจริงที่มาตรฐานยังไม่รู้ ถ้ามาตรฐานผิด ให้แก้มาตรฐานแล้วแอปที่เหลือตามมา
ไม่ใช่บังคับแอปที่ถูกให้ผิดตาม
## แผงผู้ช่วยกลาง — Assistant Dock (IP-185, 2026-08-28)
ผู้ช่วย AI ของทุกแอปอยู่ในแผงเดียวของเปลือกกลาง **ห้ามเขียนแผงผู้ช่วยกลางเองในไฟล์ของแอป**
(บทเรียน gl-platbar ฉบับผู้ช่วย — มีด่านตรวจ `src/lib/assistant-dock.contract.test.ts` เฝ้าอยู่จริง)
วิธีใช้: ใน workspace ของแอป render `<AppAssistant title="..." status={{label,tone}} busy={...}>เนื้อหา</AppAssistant>`
จาก `@/components/platform/assistant-dock` — เนื้อหาเป็นของแอป 100% ถูก portal เข้าแผงกลาง
closure/state/handler ของ workspace ใช้ได้ปกติ · หน้าที่ไม่ render AppAssistant จะไม่มีแผงแม้แต่ปุ่ม
พฤติกรรมที่แผงกลางจัดการให้แล้ว (อย่าทำซ้ำในแอป): เดสก์ท็อปแผงตรึงขวาเต็มความสูง
ดันเนื้อหาหลบไม่บัง (Right Docked Assistant Panel) · มือถือ ≤640px เป็นแผ่นเลื่อนจากล่าง ·
เริ่มแบบกาง (Default State: Expanded) แล้วจำที่ผู้ใช้เลือก คีย์เดียวทั้งแพลตฟอร์ม
(Global preference — `naichangmoo.assistant-dock.v1`) · นิ่งใต้ prefers-reduced-motion
### กับดักที่เจอแล้วตอนสร้าง
1. **จุดปลาย portal (`.assistant-dock__body`) ต้องอยู่ใน DOM เสมอแม้ตอนย่อ** — ซ่อนด้วย
`hidden`/CSS เท่านั้น ถอด element ออกเมื่อไหร่ state ของเนื้อหาผู้ช่วย (เช่นคำสั่งที่พิมพ์ค้าง)
หายเกลี้ยง มีด่านตรวจกันไว้แล้ว
2. **แผงตรึงเต็มความสูงต้องวัดความสูงแถบนำทางจริง** (`--dock-top`) — เมนูจอแคบห่อหลายแถวได้
ค่าคงที่เดาผิดเสมอ (กับดักเดียวกับ gl-floatbar)
3. วันที่ PRICEMETR ได้ผู้ช่วย (IP-188): เม็ดยามุมขวาล่างจะทับ `gl-basket` — ต้องเผื่อ offset ตอนนั้น
Voir sur GitHub