ท่ายืดเหยียดแก้ Office Syndrome ที่ทำได้ที่โต๊ะ มีแผงนับเวลาทีละท่า และเตือนให้ลุกขยับทุกชั่วโมง

ท่ายืดเหยียดแก้ Office Syndrome 12 ท่าที่ทำได้บนเก้าอี้ (คอ บ่า ไหล่ หลัง ข้อมือ ตา ขา) แผงพาทำทีละท่าพร้อมนับถอยหลัง แล้วไปท่าถัดไปเอง มีชุดสำเร็จรูป 4 ชุด เตือนให้ลุกขยับ และนับว่าวันนี้ทำไปกี่ท่า
| คำสั่ง | ทำอะไร |
|---|---|
/exercise | ชุดเร็ว 3 นาที |
/exercise neck / typing / back | ชุดคอบ่าไหล่ / ข้อมือ+ตา / หลัง |
/exercise do <ท่า> | ทำท่าเดียว |
/exercise list | ทุกท่า |
/exercise remind on [นาที] / off | เตือนลุกขยับ (ค่าเริ่มต้นทุก 60 นาที) |
/exercise today | วันนี้ทำไปกี่ท่า |
/plugin install dev-exercise-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
เป็นท่ายืดเหยียดทั่วไป ไม่ใช่คำแนะนำทางการแพทย์ ถ้าเจ็บหรือชาให้หยุด แล้วปรึกษาแพทย์หรือนักกายภาพบำบัด
hooks/register.tsx 202 lines1/**
2 * ============================================================================
3 * MOD 33: Dev Exercise TH - ยืดเหยียดแก้ Office Syndrome ที่โต๊ะ (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: ท่ายืดเหยียดสำหรับคนทำงานออฟฟิศ
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ท่ายืดเหยียด 12 ท่า แยกตามส่วน: คอ บ่า ไหล่ หลัง ข้อมือ ตา ขา
9 * 2. แผงพาทำ: บอกวิธีทีละท่า นับถอยหลัง แล้วไปท่าถัดไปเอง
10 * 3. ชุดสำเร็จรูป: เร็ว 3 นาที, คอบ่าไหล่, ข้อมือ+ตา สำหรับคนพิมพ์เยอะ
11 * 4. เตือนให้ลุกขยับทุกชั่วโมง และนับว่าวันนี้ทำไปกี่ท่า
12 *
13 * ⚠️ เป็นท่าทั่วไป ไม่ใช่คำแนะนำทางการแพทย์ ถ้าเจ็บหรือชาให้หยุด และปรึกษาแพทย์/นักกายภาพ
14 */
15
16import type { EngineInterface, Register, Timer } from 'claude-code'
17
18interface Move {
19 id: string
20 name: string
21 area: string
22 seconds: number
23 steps: string
24}
25
26// ==========================================================================
27// บทที่ 1: คลังท่า - ทำได้บนเก้าอี้ ไม่ต้องใช้อุปกรณ์
28// ==========================================================================
29const MOVES: Move[] = [
30 { id: 'neck-tilt', name: 'เอียงคอ', area: 'คอ', seconds: 30, steps: 'นั่งหลังตรง เอียงหูซ้ายไปหาไหล่ซ้ายช้าๆ ค้าง 15 วิ แล้วสลับข้าง ไหล่ไม่ยกตาม' },
31 { id: 'chin-tuck', name: 'เก็บคาง', area: 'คอ', seconds: 30, steps: 'มองตรง ดันคางถอยหลังเหมือนทำคางสองชั้น ค้าง 5 วิ ทำซ้ำ 5 ครั้ง แก้คอยื่น' },
32 { id: 'shoulder-roll', name: 'หมุนไหล่', area: 'บ่า ไหล่', seconds: 30, steps: 'ยกไหล่ขึ้นหาหู หมุนไปด้านหลังเป็นวงกว้างช้าๆ 10 รอบ แล้วหมุนไปด้านหน้า 10 รอบ' },
33 { id: 'shoulder-cross', name: 'ดึงแขนข้ามอก', area: 'บ่า ไหล่', seconds: 30, steps: 'เหยียดแขนขวาข้ามอก ใช้มือซ้ายดึงที่ข้อศอกเข้าหาตัว ค้าง 15 วิ แล้วสลับ' },
34 { id: 'chest-open', name: 'เปิดอก', area: 'หลัง', seconds: 30, steps: 'ประสานมือไว้ด้านหลัง เหยียดแขนลง ดึงสะบักเข้าหากัน ยกอกขึ้น ค้าง 20 วิ' },
35 { id: 'seated-twist', name: 'บิดตัวบนเก้าอี้', area: 'หลัง', seconds: 30, steps: 'นั่งหลังตรง บิดลำตัวไปทางขวา จับพนักเก้าอี้ ค้าง 15 วิ แล้วสลับ หายใจปกติ' },
36 { id: 'cat-cow', name: 'โก่งหลัง-แอ่นหลัง', area: 'หลัง', seconds: 30, steps: 'มือวางบนเข่า หายใจเข้าแอ่นอกมองขึ้น หายใจออกโก่งหลังก้มคาง ทำช้าๆ 8 รอบ' },
37 { id: 'wrist-flex', name: 'ยืดข้อมือ', area: 'ข้อมือ', seconds: 30, steps: 'เหยียดแขนตรง หงายมือ ใช้อีกมือดึงนิ้วลงหาตัว ค้าง 10 วิ แล้วคว่ำมือดึงลง ค้าง 10 วิ ทำสองข้าง' },
38 { id: 'finger-spread', name: 'กาง-กำนิ้ว', area: 'ข้อมือ', seconds: 20, steps: 'กางนิ้วให้สุด 3 วิ แล้วกำมือแน่น 3 วิ ทำซ้ำ 10 ครั้ง' },
39 { id: 'eye-20', name: 'พักตา 20-20-20', area: 'ตา', seconds: 20, steps: 'มองวัตถุที่ไกลประมาณ 6 เมตร (20 ฟุต) นาน 20 วินาที กะพริบตาบ่อยๆ' },
40 { id: 'eye-roll', name: 'กลอกตา', area: 'ตา', seconds: 20, steps: 'หลับตา กลอกตาช้าๆ เป็นวงกลม 5 รอบ แล้วสลับทิศ' },
41 { id: 'calf-raise', name: 'เขย่งปลายเท้า', area: 'ขา', seconds: 30, steps: 'ยืนจับโต๊ะ เขย่งปลายเท้าขึ้น-ลงช้าๆ 15 ครั้ง ช่วยการไหลเวียนเลือดที่ขา' },
42]
43
44const ROUTINES: Record<string, { name: string; moves: string[] }> = {
45 quick: { name: 'เร็ว 3 นาที', moves: ['neck-tilt', 'shoulder-roll', 'chest-open', 'wrist-flex', 'eye-20', 'calf-raise'] },
46 neck: { name: 'คอ บ่า ไหล่', moves: ['chin-tuck', 'neck-tilt', 'shoulder-roll', 'shoulder-cross', 'chest-open'] },
47 typing: { name: 'ข้อมือ + ตา (พิมพ์เยอะ)', moves: ['wrist-flex', 'finger-spread', 'eye-20', 'eye-roll', 'shoulder-roll'] },
48 back: { name: 'หลัง', moves: ['cat-cow', 'seated-twist', 'chest-open', 'calf-raise'] },
49}
50
51const PANE = 'dev-exercise-th'
52let session: { moves: Move[]; index: number; endsAt: number } | undefined
53let ticker: Timer | undefined
54let reminder: Timer | undefined
55let doneToday = { date: '', count: 0 }
56
57const find = (id: string) => MOVES.find(m => m.id === id)
58
59// ==========================================================================
60// บทที่ 2: พาทำทีละท่า - timer ทุก 1 วิ หมดเวลาแล้วไปท่าถัดไป
61// ==========================================================================
62function stopSession(): void {
63 ticker?.cancel()
64 ticker = undefined
65 session = undefined
66}
67
68async function countDone($: EngineInterface): Promise<void> {
69 const today = new Date(await $.clock.now()).toDateString()
70 doneToday = doneToday.date === today ? { date: today, count: doneToday.count + 1 } : { date: today, count: 1 }
71 await $.store.set('done', doneToday)
72}
73
74async function tick($: EngineInterface): Promise<void> {
75 if (!session) return
76 const now = await $.clock.now()
77 if (now >= session.endsAt) {
78 await countDone($)
79 session.index++
80 const next = session.moves[session.index]
81 if (!next) {
82 $.ui.toast(`🎉 ครบ ${session.moves.length} ท่าแล้ว เก่งมาก!`)
83 stopSession()
84 } else {
85 session.endsAt = now + next.seconds * 1000
86 $.ui.toast(`➡️ ท่าถัดไป: ${next.name}`)
87 }
88 }
89 $.ui.invalidate('ui.render')
90}
91
92async function startSession($: EngineInterface, moves: Move[]): Promise<void> {
93 stopSession()
94 session = { moves, index: 0, endsAt: (await $.clock.now()) + (moves[0]?.seconds ?? 30) * 1000 }
95 ticker = $.clock.every(1000, () => void tick($))
96 await $.ui.open({ id: PANE, title: '🧘 ยืดเหยียด', focus: true, closeOnEscape: true })
97 $.ui.invalidate('ui.render')
98}
99
100const HELP = `🧘 Dev Exercise TH (Mod 33)
101========================================
102/exercise ชุดเร็ว 3 นาที (พาทำในแผง)
103/exercise <ชุด> ${Object.entries(ROUTINES).map(([k, r]) => `${k} = ${r.name}`).join(' | ')}
104/exercise do <ท่า> ทำท่าเดียว
105/exercise list ทุกท่า แยกตามส่วน
106/exercise remind on [นาที] | off เตือนลุกขยับ (ค่าเริ่มต้นทุก 60 นาที)
107/exercise today วันนี้ทำไปกี่ท่า
108⚠️ ถ้าเจ็บหรือชาให้หยุด ไม่ใช่คำแนะนำทางการแพทย์`
109
110export const register: Register = on => {
111 on('session.start', async ($, e, next) => {
112 const saved = (await $.store.get('done')) as { date: string; count: number } | undefined
113 doneToday = saved ?? { date: '', count: 0 }
114 const minutes = (await $.store.get('remind')) as number | undefined
115 if (minutes) reminder = $.clock.every(minutes * 60_000, () => $.ui.toast(`🧘 นั่งมา ${minutes} นาทีแล้ว ลุกขยับหน่อย /exercise`))
116 await $.command.register({ name: 'exercise', description: 'ท่ายืดเหยียดแก้ Office Syndrome พาทำในแผง (/exercise help)', argumentHint: '[quick|neck|typing|back]|do|list|remind' })
117 return next(e)
118 })
119
120 on('ui.close', ($, e, next) => {
121 if (e.id === PANE) stopSession()
122 return next(e)
123 })
124
125 // ==========================================================================
126 // บทที่ 3: แผงพาทำ - ชื่อท่า วิธีทำ นับถอยหลัง ปุ่มข้าม
127 // ==========================================================================
128 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
129 const { Box, Text, Button } = $.ui.resolve(e)
130 if (!session) return <Text dimColor>เริ่มด้วย /exercise</Text>
131 const move = session.moves[session.index]
132 if (!move) return <Text>🎉 ครบแล้ว</Text>
133 const left = Math.max(0, Math.ceil((session.endsAt - (await $.clock.now())) / 1000))
134 const bar = '█'.repeat(Math.round((left / move.seconds) * 20)).padEnd(20, '░')
135 return (
136 <Box flexDirection="column" gap={1}>
137 <Text dimColor>
138 ท่าที่ {session.index + 1}/{session.moves.length} · {move.area}
139 </Text>
140 <Text bold>{move.name}</Text>
141 <Text>{move.steps}</Text>
142 <Text color="#26a69a">
143 {bar} {left} วิ
144 </Text>
145 <Button key="skip" hotkey="n" label="ข้ามท่า (n)" onPress={() => {
146 if (session) session.endsAt = 0
147 void tick($)
148 }} />
149 </Box>
150 )
151 })
152
153 on('command.run', { command: 'exercise' }, async ($, e) => {
154 const [sub = 'quick', a1, a2] = e.args.trim().split(/\s+/).filter(Boolean)
155 const routine = ROUTINES[sub]
156 if (routine) {
157 const moves = routine.moves.map(find).filter((m): m is Move => Boolean(m))
158 await startSession($, moves)
159 return { text: `🧘 เริ่มชุด "${routine.name}" ${moves.length} ท่า รวม ${moves.reduce((s, m) => s + m.seconds, 0)} วินาที ดูในแผงได้เลย (กด n เพื่อข้าม, Esc เพื่อหยุด)` }
160 }
161 switch (sub) {
162 case 'do': {
163 const m = find(a1 ?? '')
164 if (!m) return { text: `ไม่มีท่า "${a1 ?? ''}" ดูรายการด้วย /exercise list` }
165 await startSession($, [m])
166 return { text: `🧘 ${m.name} ${m.seconds} วิ\n${m.steps}` }
167 }
168 case 'list': {
169 const areas = [...new Set(MOVES.map(m => m.area))]
170 return { text: areas.map(a => `${a}\n${MOVES.filter(m => m.area === a).map(m => ` ${m.id.padEnd(15)} ${m.name} (${m.seconds} วิ)`).join('\n')}`).join('\n') }
171 }
172 case 'remind': {
173 reminder?.cancel()
174 reminder = undefined
175 if (a1 === 'off') {
176 await $.store.delete('remind')
177 return { text: '🔕 ปิดการเตือนแล้ว' }
178 }
179 const minutes = Math.min(180, Math.max(15, Number(a2) || 60))
180 reminder = $.clock.every(minutes * 60_000, () => $.ui.toast(`🧘 นั่งมา ${minutes} นาทีแล้ว ลุกขยับหน่อย /exercise`))
181 await $.store.set('remind', minutes)
182 return { text: `🔔 จะเตือนให้ลุกขยับทุก ${minutes} นาที` }
183 }
184 case 'today': {
185 const today = new Date(await $.clock.now()).toDateString()
186 return { text: `📊 วันนี้ทำไป ${doneToday.date === today ? doneToday.count : 0} ท่า` }
187 }
188 default:
189 return { text: HELP }
190 }
191 })
192}
193
194/**
195 * ========================================================================
196 * การบ้าน / ต่อยอด
197 * ========================================================================
198 * 1. เตือนอัตโนมัติเมื่อ Claude ทำงานยาว (ระหว่างรอ ลุกยืดเส้นได้พอดี): ดัก turn.start
199 * 2. ใช้ $.audio.speak อ่านขั้นตอนให้ฟัง จะได้ไม่ต้องมองจอ
200 * 3. สรุปรายสัปดาห์ว่ายืดเหยียดไปกี่ท่า
201 */
202