SLOPSHOPPER

sweet-alert-th

Alert สวยๆ แบบ SweetAlert ใน Claude Code: dialog กลางจอมีปุ่มกด, ถามยืนยัน, ช่องกรอกข้อมูล, toast และคิว alert

newpaneguardcommandtoasttimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · sweet-alert-th
│ ┃ 🎨 Sweet Alert ✕ › fix the failing auth test and╭───────────────────────────────────╮ │ ┃ ╭──────────────────────────────────────────╮ │ sweet-alert-th │ │ ┃ │ │ ⏺ Read(src/auth.ts) │ ❌ Bash ล้มเหลว: src/auth.test.ts: │ │ ┃ │ ❓ │ ⎿ Read 6 lines ╰───────────────────────────────────╯ │ ┃ │ ยืนยัน? │ ⏺ Update(src/auth.ts) │ ┃ │ คุณแน่ใจไหม? │ ⎿ Added 2 lines, removed 1 line │ ┃ │ [ ยกเลิก ] [ ใช่, ทำเลย ] │ ⏺ Bash(bun test) │ ┃ │ รออยู่ในคิวอีก 1 อัน │ ⎿ 3 pass, 1 fail │ ┃ │ │ │ ┃ ╰──────────────────────────────────────────╯ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /alert │ ⎿ sweet-alert-th: ❌ ไม่รู้จัก "" ใช้ได้: success, error, warning, │ ⎿ sweet-alert-th: /alert help เพื่อดูวิธีใช้ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 🎨 Sweet Alert
╭──────────────────────────────────────────────────────────╮ │ │ │ ❓ │ │ ยืนยัน? │ │ คุณแน่ใจไหม? │ │ [ ยกเลิก ] [ ใช่, ทำเลย ] │ │ รออยู่ในคิวอีก 1 อัน │ │ │ ╰──────────────────────────────────────────────────────────╯
README

Mod 11: Sweet Alert TH

Alert สวยๆ แบบ SweetAlert ใน Claude Code: dialog กลางจอ มีขอบสีตามประเภท (✅ success, ❌ error, ⚠️ warning, ℹ️ info, ❓ question, ⏳ loading) มีปุ่มให้กดจริง ถามยืนยันได้ ให้กรอกข้อมูลได้ และถ้าสั่งหลาย alert ติดกันจะแสดงทีละอันตามคิว นอกจากนี้ ถ้า Bash, Edit หรือ Write ล้มเหลว จะเด้ง toast เตือนให้เอง

คำสั่ง

คำสั่งทำอะไร
/alert success บันทึกแล้วแสดง alert (ปิดเองใน 5 วินาที)
`/alert error หัวข้อ \ข้อความ`แยกหัวข้อกับข้อความด้วย `\`
/alert demoดูตัวอย่างทุกแบบ ต่อกันตามคิว
/alert historyประวัติและผลการตอบของแต่ละ alert
`/alert answer <yes\no\ข้อความ>`ตอบคำถามที่ค้างอยู่ด้วยคำสั่ง (ใช้ตอนเปิด dialog ไม่ได้)
/alert clearล้างทั้งหมด
/confirm <คำถาม>ถามยืนยัน มีปุ่ม "ใช่" กับ "ยกเลิก"
/prompt <คำถาม>ให้กรอกข้อมูลแล้วกด Enter
/toast <type> <ข้อความ>แจ้งเตือนเล็กๆ มุมจอ

ใน dialog: กด Tab เพื่อเลื่อนระหว่างปุ่ม, Enter เพื่อกด, y / n เป็นปุ่มลัด และ Esc เพื่อปิด (นับเป็นยกเลิก)

ติดตั้ง

/plugin install sweet-alert-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. กำหนดหน้าตาแต่ละประเภท (ไอคอน, สี, หัวข้อ) ไว้ใน object เดียว
  2. ระบบคิว: แสดงทีละอัน แล้วหยิบอันถัดไปเมื่ออันเดิมถูกตอบ
  3. เปิดแผงแบบ dialog ด้วย $.ui.open({ focus, closeOnEscape, holdToasts }) และปิดอัตโนมัติด้วย $.clock.after
  4. จัดการ event ของผู้ใช้: onPress ของ Button, onSubmit ของ Input และดัก ui.close ตอนผู้ใช้กด Esc
  5. วาด dialog ด้วย Box ที่มีขอบสี (borderStyle, borderColor)
  6. เช็คก่อนใช้ element ที่บางหน้าจอไม่มี (Input ไม่มีบน mobile)

หมายเหตุ

Mod วาด HTML/CSS ไม่ได้ จึงไม่มี animation แบบ SweetAlert บนเว็บ สิ่งที่ได้คือ dialog ที่ใช้ element ของ Claude Code เอง ซึ่งใช้ได้ทั้งใน terminal, แอป Desktop, VS Code และ mobile

Source 1 files
hooks/register.tsx 327 lines
1/**
2 * ============================================================================
3 * MOD 11: Sweet Alert TH - Alert สวยๆ แบบ SweetAlert (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: SweetAlert2 (https://sweetalert2.github.io) ทำใหม่ให้ใช้ใน Claude Code
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /alert แสดง dialog กลางจอ ขอบสีตามประเภท: success, error, warning, info, question, loading
9 * 2. /confirm ถามยืนยัน มีปุ่ม "ใช่" กับ "ยกเลิก" ให้กดจริง
10 * 3. /prompt ให้กรอกข้อมูลในช่อง Input แล้วกด Enter
11 * 4. /toast แจ้งเตือนเล็กๆ มุมจอ
12 * 5. มีคิว: สั่งหลาย alert ติดกัน จะแสดงทีละอันตามลำดับ
13 * 6. ปิดอัตโนมัติได้ + เก็บประวัติว่าแต่ละ alert ถูกตอบว่าอะไร
14 * 7. tool ที่สำคัญล้มเหลว (Bash/Edit/Write) จะเด้ง toast เตือนเอง
15 *
16 * แนวคิดสำคัญ:
17 * - Mod วาด HTML/CSS ไม่ได้ แต่วาด element ของ Claude Code ได้: Box (มีขอบสี), Text, Button, Input
18 * - เปิดแผงแบบ dialog ด้วย $.ui.open({ focus, closeOnEscape, holdToasts })
19 * - ปุ่มมี onPress, ช่องกรอกมี onSubmit -> นี่คือการ "จัดการ event" เมื่อผู้ใช้กด
20 */
21
22import type { EngineInterface, Register, Timer } from 'claude-code'
23
24type AlertType = 'success' | 'error' | 'warning' | 'info' | 'question' | 'loading'
25type AlertKind = 'alert' | 'confirm' | 'prompt'
26type AlertResult = 'confirm' | 'cancel' | 'timeout'
27
28interface SweetAlert {
29  id: number
30  kind: AlertKind
31  type: AlertType
32  title: string
33  message: string
34  time: number
35  autoCloseMs?: number // ปิดอัตโนมัติหลังกี่ ms (ไม่ใส่ = รอให้กด)
36  confirmText: string
37  cancelText?: string // มีค่า = แสดงปุ่มยกเลิก
38  result?: AlertResult
39  value?: string // ค่าที่กรอก (เฉพาะ prompt)
40}
41
42type AlertOptions = Partial<Pick<SweetAlert, 'autoCloseMs' | 'confirmText' | 'cancelText'>>
43
44const PANE = 'sweet-alert-th'
45
46// ==========================================================================
47// บทที่ 1: หน้าตาของแต่ละประเภท (ไอคอน + สี + หัวข้อเริ่มต้น)
48// ==========================================================================
49const STYLE: Record<AlertType, { icon: string; color: string; title: string }> = {
50  success: { icon: '✅', color: '#2ee6a6', title: 'สำเร็จ!' },
51  error: { icon: '❌', color: '#ff7b72', title: 'เกิดข้อผิดพลาด!' },
52  warning: { icon: '⚠️', color: '#ffca28', title: 'คำเตือน!' },
53  info: { icon: 'ℹ️', color: '#4fc3f7', title: 'ข้อมูล' },
54  question: { icon: '❓', color: '#a371f7', title: 'คำถาม' },
55  loading: { icon: '⏳', color: '#8b949e', title: 'กำลังโหลด...' },
56}
57const TYPES = Object.keys(STYLE) as AlertType[]
58const TOAST_TYPES: AlertType[] = ['success', 'error', 'warning', 'info']
59
60// ==========================================================================
61// บทที่ 2: state - alert ที่แสดงอยู่, คิวที่รอ, และประวัติ
62// ==========================================================================
63let current: SweetAlert | undefined
64let queue: SweetAlert[] = []
65let history: SweetAlert[] = []
66let nextId = 0
67let autoCloseTimer: Timer | undefined
68let isPanePlaced = true // false = เปิดแผงไม่ได้ (เช่นรันแบบ claude -p) -> ตอบด้วย /alert answer แทน
69
70async function createAlert($: EngineInterface, kind: AlertKind, type: AlertType, title: string, message: string, options: AlertOptions = {}): Promise<SweetAlert> {
71  const alert: SweetAlert = {
72    id: nextId++,
73    kind,
74    type,
75    title,
76    message,
77    time: await $.clock.now(),
78    confirmText: options.confirmText ?? 'ตกลง',
79    cancelText: options.cancelText,
80    autoCloseMs: options.autoCloseMs,
81  }
82  history = [...history, alert].slice(-50) // เก็บแค่ 50 อันหลังสุด
83  queue.push(alert)
84  if (!current) await showNext($)
85  return alert
86}
87
88// ==========================================================================
89// บทที่ 3: ระบบคิว - หยิบ alert ถัดไปมาแสดง
90// ==========================================================================
91async function showNext($: EngineInterface): Promise<void> {
92  autoCloseTimer?.cancel()
93  autoCloseTimer = undefined
94  current = queue.shift()
95
96  if (!current) {
97    await $.ui.close({ id: PANE })
98    return
99  }
100
101  // dialog: รับคีย์บอร์ด, Esc ปิดได้, toast รอจนกว่า dialog จะปิด
102  const opened = await $.ui.open({ id: PANE, title: '🎨 Sweet Alert', focus: true, closeOnEscape: true, holdToasts: true })
103  isPanePlaced = opened.isPlaced
104  $.ui.invalidate('ui.render')
105
106  const shown = current
107  if (shown.autoCloseMs) {
108    // timer ทำงานแบบ sync จึงห่อ async ด้วย void
109    autoCloseTimer = $.clock.after(shown.autoCloseMs, () => void resolveAlert($, shown, 'timeout'))
110  }
111}
112
113// ==========================================================================
114// บทที่ 4: จัดการ event เมื่อผู้ใช้ตอบ (กดปุ่ม, กรอกข้อมูล, Esc, หมดเวลา)
115// ==========================================================================
116async function resolveAlert($: EngineInterface, alert: SweetAlert, result: AlertResult, value?: string): Promise<void> {
117  if (alert !== current || alert.result) return // ตอบไปแล้ว หรือไม่ใช่อันที่แสดงอยู่
118  alert.result = result
119  alert.value = value
120
121  // ผลลัพธ์ของ confirm / prompt -> แจ้งผลด้วย toast หรือ alert ต่อ
122  if (alert.kind === 'confirm') {
123    $.ui.toast(result === 'confirm' ? `✅ ยืนยันแล้ว: ${alert.message}` : `❌ ยกเลิก: ${alert.message}`)
124  }
125  if (alert.kind === 'prompt' && result === 'confirm') {
126    queue.unshift({ ...alert, id: nextId++, kind: 'alert', type: 'success', title: 'ได้รับข้อมูลแล้ว!', message: `ค่าที่กรอก: ${value || '(ว่าง)'}`, result: undefined, value: undefined, cancelText: undefined, confirmText: 'ตกลง', autoCloseMs: 3000 })
127  }
128
129  await showNext($)
130}
131
132function describe(a: SweetAlert): string {
133  const status = a.result === 'confirm' ? (a.kind === 'prompt' ? `✅ ได้ค่า "${a.value ?? ''}"` : '✅ ยืนยัน') : a.result === 'cancel' ? '❌ ยกเลิก' : a.result === 'timeout' ? '⌛ ปิดเอง' : '⏳ รอ'
134  return `${STYLE[a.type].icon} [${a.id}] ${a.title} - ${a.message.slice(0, 50)} ${status}`
135}
136
137// แยก "หัวข้อ | ข้อความ" (ถ้าไม่มี | ใช้หัวข้อเริ่มต้นของประเภทนั้น)
138function splitTitle(type: AlertType, text: string): [string, string] {
139  if (!text.includes('|')) return [STYLE[type].title, text || STYLE[type].title]
140  const [title = '', ...rest] = text.split('|')
141  return [title.trim(), rest.join('|').trim()]
142}
143
144const HELP = `🎨 Sweet Alert TH (Mod 11)
145========================================
146/alert <type> <ข้อความ>
147/alert <type> <หัวข้อ> | <ข้อความ>
148   type: success, error, warning, info, question, loading
149/alert demo       ดูตัวอย่างทุกแบบ (แสดงต่อกันตามคิว)
150/alert history    ประวัติและผลการตอบ
151/alert answer <yes|no|ข้อความ>  ตอบ alert ที่ค้างอยู่ด้วยคำสั่ง
152/alert clear      ล้างทั้งหมด
153
154/confirm <คำถาม>         ถามยืนยัน ใช่/ยกเลิก
155/prompt <คำถาม>          ให้กรอกข้อมูล
156/toast <type> <ข้อความ>  แจ้งเตือนเล็กๆ มุมจอ
157
158ใน dialog: Tab เลื่อนปุ่ม, Enter กด, Esc ปิด (= ยกเลิก)`
159
160function pendingNote(): string {
161  return isPanePlaced ? '' : '\n(เปิด dialog ไม่ได้ในหน้าจอนี้: ตอบด้วย /alert answer yes|no|ข้อความ)'
162}
163
164export const register: Register = on => {
165  on('session.start', async ($, e, next) => {
166    current = undefined
167    queue = []
168    history = []
169    await $.command.register({ name: 'alert', description: 'แสดง Sweet Alert (/alert help)', argumentHint: '<type> [หัวข้อ |] ข้อความ' })
170    await $.command.register({ name: 'confirm', description: 'ถามยืนยัน ใช่/ยกเลิก ใน dialog', argumentHint: '<คำถาม>' })
171    await $.command.register({ name: 'prompt', description: 'ให้กรอกข้อมูลใน dialog', argumentHint: '<คำถาม>' })
172    await $.command.register({ name: 'toast', description: 'แจ้งเตือนเล็กๆ มุมจอ', argumentHint: '<type> <ข้อความ>' })
173    return next(e)
174  })
175
176  // tool สำคัญล้มเหลว -> toast เตือน (ไม่สแปมกับ tool อ่านไฟล์)
177  on('tool.call', async ($, e, next) => {
178    const ran = await next(e)
179    if (ran.isError && (e.tool === 'Bash' || e.tool === 'Edit' || e.tool === 'Write')) {
180      $.ui.toast(`❌ ${e.tool} ล้มเหลว: ${(ran.text ?? '').split('\n')[0]?.slice(0, 80) ?? ''}`)
181    }
182    return ran
183  })
184
185  // ผู้ใช้กด Esc หรือปุ่มปิดของแผง = ยกเลิก alert ที่แสดงอยู่ และทิ้งคิวที่เหลือ
186  on('ui.close', async ($, e, next) => {
187    if (e.id === PANE && e.origin.kind === 'person' && current) {
188      const closing = current
189      queue = []
190      closing.result = 'cancel'
191      current = undefined
192      autoCloseTimer?.cancel()
193      if (closing.kind === 'confirm') $.ui.toast(`❌ ยกเลิก: ${closing.message}`)
194    }
195    return next(e)
196  })
197
198  // ==========================================================================
199  // บทที่ 5: วาด dialog - Box ขอบสี + ไอคอน + หัวข้อ + ข้อความ + ปุ่ม/ช่องกรอก
200  // ==========================================================================
201  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
202    const elements = $.ui.resolve(e)
203    const { Box, Text, Button } = elements
204    const alert = current
205    if (!alert) return <Text dimColor>ไม่มี alert</Text>
206
207    const style = STYLE[alert.type]
208    // mobile ไม่มี Input -> เช็คก่อนใช้
209    const Input = 'Input' in elements ? elements.Input : undefined
210    const isPrompt = alert.kind === 'prompt'
211
212    return (
213      <Box flexDirection="column" alignItems="center" borderStyle="round" borderColor={style.color} paddingX={2} paddingY={1}>
214        <Text>{style.icon}</Text>
215        <Text bold color={style.color}>
216          {alert.title}
217        </Text>
218        <Text>{alert.message}</Text>
219        {isPrompt && Input && (
220          <Input key="value" placeholder="พิมพ์แล้วกด Enter" autoFocus onSubmit={value => void resolveAlert($, alert, 'confirm', value)} />
221        )}
222        {isPrompt && !Input && <Text dimColor>หน้าจอนี้กรอกข้อมูลไม่ได้ ใช้ /alert answer ข้อความ</Text>}
223        <Box gap={2}>
224          {alert.cancelText && <Button key="cancel" label={alert.cancelText} hotkey="n" onPress={() => void resolveAlert($, alert, 'cancel')} />}
225          {!isPrompt && <Button key="confirm" label={alert.confirmText} hotkey="y" variant="primary" autoFocus onPress={() => void resolveAlert($, alert, 'confirm')} />}
226        </Box>
227        {alert.autoCloseMs !== undefined && <Text dimColor>ปิดอัตโนมัติใน {alert.autoCloseMs / 1000} วินาที</Text>}
228        {queue.length > 0 && <Text dimColor>รออยู่ในคิวอีก {queue.length} อัน</Text>}
229      </Box>
230    )
231  })
232
233  // ==========================================================================
234  // บทที่ 6: คำสั่ง /alert
235  // ==========================================================================
236  on('command.run', { command: 'alert' }, async ($, e) => {
237    const args = e.args.trim()
238    const [sub = 'help', ...restWords] = args.split(/\s+/)
239    const rest = restWords.join(' ')
240
241    if (sub === 'help') return { text: HELP }
242
243    if (sub === 'history') {
244      return { text: history.length ? [`🎨 ประวัติ Sweet Alert (${history.length})`, ...[...history].reverse().slice(0, 15).map(describe)].join('\n') : 'ยังไม่มี alert ลอง /alert demo' }
245    }
246
247    if (sub === 'clear') {
248      queue = []
249      history = []
250      if (current) {
251        current = undefined
252        await showNext($) // คิวว่าง -> ปิดแผง
253      }
254      return { text: '🗑️ ล้าง alert ทั้งหมดแล้ว' }
255    }
256
257    if (sub === 'answer') {
258      // alert ธรรมดาที่ขวางหน้าคิวอยู่ ให้กดตกลงไปก่อน จะได้ตอบคำถามที่รออยู่จริงๆ
259      while (current?.kind === 'alert') await resolveAlert($, current, 'confirm')
260      if (!current) return { text: 'ไม่มีคำถามที่รอคำตอบ' }
261      const alert = current
262      if (alert.kind === 'prompt') await resolveAlert($, alert, rest ? 'confirm' : 'cancel', rest)
263      else await resolveAlert($, alert, /^(no|n|ไม่)$/i.test(rest) ? 'cancel' : 'confirm')
264      return { text: describe(alert) }
265    }
266
267    if (sub === 'demo') {
268      await createAlert($, 'alert', 'success', 'สำเร็จ!', 'ไฟล์ถูกบันทึกเรียบร้อยแล้ว', { autoCloseMs: 2500 })
269      await createAlert($, 'alert', 'error', 'เกิดข้อผิดพลาด!', 'ไม่สามารถเชื่อมต่อ database ได้', { autoCloseMs: 2500 })
270      await createAlert($, 'alert', 'warning', 'คำเตือน!', 'ไฟล์นี้มีขนาดใหญ่กว่า 10MB', { autoCloseMs: 2500 })
271      await createAlert($, 'alert', 'info', 'ข้อมูล', 'มี 5 ไฟล์ที่ถูกแก้ไขใน session นี้', { autoCloseMs: 2500 })
272      await createAlert($, 'confirm', 'question', 'ยืนยันไหม?', 'คุณชอบ Mod นี้ไหม?', { confirmText: 'ชอบ!', cancelText: 'เฉยๆ' })
273      return { text: `🎨 เริ่ม demo 5 แบบ (แสดงต่อกันตามคิว)${pendingNote()}` }
274    }
275
276    if (TYPES.includes(sub as AlertType)) {
277      const type = sub as AlertType
278      const [title, message] = splitTitle(type, rest)
279      const kind: AlertKind = type === 'question' ? 'confirm' : 'alert'
280      await createAlert($, kind, type, title, message, {
281        autoCloseMs: type === 'loading' || type === 'question' ? undefined : 5000,
282        cancelText: type === 'question' ? 'ยกเลิก' : undefined,
283      })
284      return { text: `${STYLE[type].icon} ${title}: ${message}${pendingNote()}` }
285    }
286
287    return { text: `❌ ไม่รู้จัก "${sub}" ใช้ได้: ${TYPES.join(', ')}\n/alert help เพื่อดูวิธีใช้` }
288  })
289
290  on('command.run', { command: 'confirm' }, async ($, e) => {
291    const message = e.args.trim() || 'คุณแน่ใจไหม?'
292    await createAlert($, 'confirm', 'question', 'ยืนยัน?', message, { confirmText: 'ใช่, ทำเลย', cancelText: 'ยกเลิก' })
293    return { text: `❓ ${message}${pendingNote()}` }
294  })
295
296  on('command.run', { command: 'prompt' }, async ($, e) => {
297    const message = e.args.trim() || 'กรุณากรอกข้อมูล:'
298    await createAlert($, 'prompt', 'question', 'กรอกข้อมูล', message, { confirmText: 'ส่ง', cancelText: 'ยกเลิก' })
299    return { text: `📝 ${message}${pendingNote()}` }
300  })
301
302  // ==========================================================================
303  // บทที่ 7: /toast - ใช้ $.ui.toast ของ Claude Code ตรงๆ
304  // ==========================================================================
305  on('command.run', { command: 'toast' }, async ($, e) => {
306    const [first = '', ...restWords] = e.args.trim().split(/\s+/)
307    const hasType = TOAST_TYPES.includes(first as AlertType)
308    const type: AlertType = hasType ? (first as AlertType) : 'info'
309    const message = hasType ? restWords.join(' ') : e.args.trim()
310    if (!message) return { text: 'วิธีใช้: /toast success บันทึกแล้ว! (type: success, error, warning, info)' }
311
312    $.ui.toast(`${STYLE[type].icon} ${message}`, { timeoutMs: 3000 })
313    return { text: `🍞 ${STYLE[type].icon} ${message}` }
314  })
315}
316
317/**
318 * ========================================================================
319 * การบ้าน / ต่อยอด
320 * ========================================================================
321 * 1. ทำ Progress Alert: เก็บเปอร์เซ็นต์ใน alert แล้วอัปเดตด้วย $.ui.invalidate ทุกครั้งที่เปลี่ยน
322 * 2. เพิ่มเสียง: $.audio.play({ asset }) ด้วยไฟล์เสียงในโฟลเดอร์ของ Mod
323 * 3. ทำเป็นด่านยืนยันก่อนทำอะไรอันตราย: ดัก tool.call ของ Bash ที่มี "git push"
324 *    แล้วใช้ $.ui.ask("จะ push จริงไหม?", ["ใช่", "ไม่"]) ก่อนเรียก next(e)
325 * 4. ส่ง alert สำคัญเป็น notification ของระบบด้วย $.ui.notify(...)
326 */
327