SLOPSHOPPER

dev-exercise-th

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

newpanecommandtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · dev-exercise-th
│ ┃ 🧘 ยืดเหยียด ✕ › fix the failing auth test and add an audit log call │ ┃ ท่าที่ 1/6 · คอ │ ┃ ⏺ Read(src/auth.ts) │ ┃ เอียงคอ ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ นั่งหลังตรง เอียงหูซ้ายไปหาไหล่ซ้ายช้าๆ ค้าง ⎿ Added 2 lines, removed 1 line │ ┃ 15 วิ แล้วสลับข้าง ไหล่ไม่ยกตาม ⏺ Bash(bun test) │ ┃ ⎿ 3 pass, 1 fail │ ┃ ████████████████████ 30 วิ │ ┃ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ [ ข้ามท่า (n) ] │ ✻ Worked for 42s · done 4:20 PM │ │ › /exercise │ ⎿ dev-exercise-th: 🧘 เริ่มชุด "เร็ว 3 นาที" 6 ท่า รวม 170 วินาที ด │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 🧘 ยืดเหยียด
ท่าที่ 1/6 · คอ เอียงคอ นั่งหลังตรง เอียงหูซ้ายไปหาไหล่ซ้ายช้าๆ ค้าง 15 วิ แล้วสลับข้าง ไหล่ไม่ยกตาม ████████████████████ 30 วิ [ ข้ามท่า (n) ]
README

Mod 33: Dev Exercise TH

ท่ายืดเหยียดแก้ 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. คลังท่าและชุดท่า
  2. พาทำทีละท่าด้วย timer แล้วไปท่าถัดไปเอง
  3. แผงพาทำ แถบนับถอยหลัง และปุ่มข้าม (n)
  4. คำสั่ง /exercise

หมายเหตุ

เป็นท่ายืดเหยียดทั่วไป ไม่ใช่คำแนะนำทางการแพทย์ ถ้าเจ็บหรือชาให้หยุด แล้วปรึกษาแพทย์หรือนักกายภาพบำบัด

Source 1 files
hooks/register.tsx 202 lines
1/**
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