SLOPSHOPPER

stamp-th

ตรายางดิจิทัล: ปั๊มตรา อนุมัติ ด่วน ลับ เสร็จแล้ว หรือข้อความของคุณเอง พร้อมวันที่ เป็นไฟล์ SVG และปั๊มลงแผงแบบสนุกๆ

newpanecommand
★ 3v1.0.0MITupdated 2026-10-24tksoft11/claude-code-mods-th/mods/mod-41-stamp
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · stamp-th
│ ┃ stamp-th ✕ › fix the failing auth test and add an audit log call │ ┃ 📄 กระดาษ (0 ตรา) │ ┃ ปั๊มด้วย /stamp approved ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /stamp │ ⎿ stamp-th: 📮 ตรายางทั้งหมด │ ⎿ stamp-th: approved อนุมัติ │ ⎿ stamp-th: rejected ไม่อนุมัติ │ ⎿ stamp-th: urgent ด่วน │ ⎿ stamp-th: very-urgent ด่วนที่สุด │ ⎿ stamp-th: secret ลับ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · stamp-th
📄 กระดาษ (0 ตรา) ปั๊มด้วย /stamp approved
README

Mod 41: Stamp TH

ตรายางดิจิทัล 10 แบบที่ใช้กันในสำนักงาน (อนุมัติ ไม่อนุมัติ ด่วน ด่วนที่สุด ลับ เสร็จแล้ว ตรวจแล้ว ชำระแล้ว สำเนาถูกต้อง ฉบับร่าง) และตราข้อความของคุณเอง ปั๊มเล่นบน "กระดาษ" ในแผง หรือส่งออกเป็นไฟล์ SVG ที่มีลายหมึกเหมือนปั๊มจริง พร้อมวันที่ไทยและชื่อผู้ปั๊ม เอาไปวางในเอกสารหรือสไลด์ได้

คำสั่ง

คำสั่งทำอะไร
/stampดูตราทั้งหมด
/stamp <ตรา>ปั๊มลงกระดาษในแผง เช่น /stamp approved
/stamp custom <ข้อความ> [#สี]ตราข้อความของคุณ
`/stamp export <ตรา\custom ข้อความ>`บันทึกเป็น stamp-*.svg
/stamp owner <ชื่อ>ชื่อผู้ปั๊ม
/stamp clearล้างกระดาษ

ติดตั้ง

/plugin install stamp-th --marketplace tksoft11/claude-code-mods-th

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

โค้ดสอนอะไร

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

  1. ตรายางสำเร็จรูป
  2. วาดตราเป็น SVG พร้อม filter ลายหมึก
  3. แผงกระดาษ
  4. คำสั่ง /stamp และเขียนไฟล์ด้วย $.fs.write

หมายเหตุ

ไฟล์ SVG ใช้ฟอนต์ Sarabun หรือ Noto Sans Thai ถ้าเครื่องไม่มีจะใช้ฟอนต์ไทยที่มีแทน ตรานี้ใช้ตกแต่งเอกสาร ไม่ใช่ลายเซ็นหรือตราที่มีผลทางกฎหมาย

Source 1 files
hooks/register.tsx 178 lines
1/**
2 * ============================================================================
3 * MOD 41: Stamp TH - ตรายางดิจิทัล (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: ตรายาง "อนุมัติ" "ด่วนที่สุด" "ลับ" ในสำนักงาน
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ตรายางสำเร็จรูป 10 แบบ (อนุมัติ ไม่อนุมัติ ด่วน ด่วนที่สุด ลับ เสร็จแล้ว ตรวจแล้ว ชำระแล้ว ...)
9 * 2. ตรายางข้อความของคุณเอง เช่น ชื่อบริษัท ชื่อแบรนด์
10 * 3. ส่งออกเป็นไฟล์ SVG พร้อมวันที่และชื่อผู้ปั๊ม เอาไปวางในเอกสาร/สไลด์ได้
11 * 4. ปั๊มเล่นในแผง: ตราที่ปั๊มจะเรียงซ้อนบน "กระดาษ" แบบสนุกๆ
12 */
13
14import type { EngineInterface, Register } from 'claude-code'
15
16interface StampStyle {
17  text: string
18  color: string
19  shape: 'rect' | 'circle'
20}
21
22// ==========================================================================
23// บทที่ 1: ตรายางสำเร็จรูป (สีและรูปทรงตามที่ใช้กันในสำนักงาน)
24// ==========================================================================
25const STAMPS: Record<string, StampStyle> = {
26  approved: { text: 'อนุมัติ', color: '#1b7f3b', shape: 'rect' },
27  rejected: { text: 'ไม่อนุมัติ', color: '#c62828', shape: 'rect' },
28  urgent: { text: 'ด่วน', color: '#c62828', shape: 'rect' },
29  'very-urgent': { text: 'ด่วนที่สุด', color: '#b71c1c', shape: 'rect' },
30  secret: { text: 'ลับ', color: '#c62828', shape: 'circle' },
31  done: { text: 'เสร็จแล้ว', color: '#1565c0', shape: 'circle' },
32  checked: { text: 'ตรวจแล้ว', color: '#1565c0', shape: 'rect' },
33  paid: { text: 'ชำระแล้ว', color: '#1b7f3b', shape: 'circle' },
34  copy: { text: 'สำเนาถูกต้อง', color: '#37474f', shape: 'rect' },
35  draft: { text: 'ฉบับร่าง', color: '#6d4c41', shape: 'rect' },
36}
37
38let paper: { text: string; color: string; at: number }[] = []
39let owner = ''
40
41function esc(s: string): string {
42  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
43}
44
45function thaiDate(ms: number): string {
46  const d = new Date(ms)
47  const months = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.']
48  return `${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear() + 543}`
49}
50
51// ==========================================================================
52// บทที่ 2: วาดตราเป็น SVG - เอียงนิดๆ ขอบหนา และใส่ texture ให้ดูเหมือนหมึกจริง
53// ==========================================================================
54function stampSvg(s: StampStyle, date: string, by: string): string {
55  const rot = -8
56  const sub = [date, by].filter(Boolean).join(' · ')
57  const ink = `<filter id="ink"><feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="3"/><feColorMatrix values="0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 -2.2 1.6"/><feComposite in="SourceGraphic" operator="in"/></filter>`
58  if (s.shape === 'circle') {
59    return `<svg xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 300 300">${ink}<g filter="url(#ink)" transform="rotate(${rot} 150 150)" fill="none" stroke="${s.color}"><circle cx="150" cy="150" r="130" stroke-width="10"/><circle cx="150" cy="150" r="112" stroke-width="3"/><text x="150" y="${sub ? 158 : 168}" font-size="${s.text.length > 6 ? 40 : 56}" font-weight="800" font-family="Sarabun, 'Noto Sans Thai', sans-serif" text-anchor="middle" fill="${s.color}" stroke="none">${esc(s.text)}</text>${sub ? `<text x="150" y="205" font-size="20" font-family="Sarabun, sans-serif" text-anchor="middle" fill="${s.color}" stroke="none">${esc(sub)}</text>` : ''}</g></svg>`
60  }
61  const width = Math.max(300, [...s.text].length * 52 + 80)
62  return `<svg xmlns="http://www.w3.org/2000/svg" width="${width + 40}" height="200" viewBox="0 0 ${width + 40} 200">${ink}<g filter="url(#ink)" transform="rotate(${rot} ${(width + 40) / 2} 100)" fill="none" stroke="${s.color}"><rect x="20" y="30" width="${width}" height="140" rx="12" stroke-width="10"/><rect x="34" y="44" width="${width - 28}" height="112" rx="6" stroke-width="3"/><text x="${(width + 40) / 2}" y="${sub ? 112 : 122}" font-size="64" font-weight="800" font-family="Sarabun, 'Noto Sans Thai', sans-serif" text-anchor="middle" fill="${s.color}" stroke="none">${esc(s.text)}</text>${sub ? `<text x="${(width + 40) / 2}" y="148" font-size="20" font-family="Sarabun, sans-serif" text-anchor="middle" fill="${s.color}" stroke="none">${esc(sub)}</text>` : ''}</g></svg>`
63}
64
65// ความกว้างที่เห็นบนจอ: สระบน/ล่างและวรรณยุกต์ของไทยไม่กินที่ (ั ิ ี ึ ื ุ ู ฺ ็ ่ ้ ๊ ๋ ์ ...)
66function visibleWidth(text: string): number {
67  return [...text].filter(ch => !/[ัิ-ฺ็-๎]/.test(ch)).length
68}
69
70// ตราแบบข้อความสำหรับแสดงในแชต
71function stampAscii(text: string): string {
72  const w = visibleWidth(text) + 4
73  return [`╔${'═'.repeat(w)}╗`, `║  ${text}  ║`, `╚${'═'.repeat(w)}╝`].join('\n')
74}
75
76const PANE = 'stamp-th'
77
78const HELP = `📮 Stamp TH (Mod 41)
79========================================
80/stamp                    ดูตราทั้งหมด
81/stamp <ชื่อตรา>          ปั๊มลงกระดาษในแผง เช่น /stamp approved
82/stamp custom <ข้อความ> [สี]   ตราข้อความของคุณ เช่น /stamp custom ร้านกาแฟดี #6d4c41
83/stamp export <ชื่อตรา|custom ข้อความ>   บันทึกเป็น stamp-*.svg พร้อมวันที่
84/stamp owner <ชื่อ>       ชื่อผู้ปั๊ม (ใส่ใต้ตราตอน export)
85/stamp clear              ล้างกระดาษ
86ตรา: ${Object.entries(STAMPS).map(([k, s]) => `${k}=${s.text}`).join(' ')}`
87
88export const register: Register = on => {
89  on('session.start', async ($, e, next) => {
90    owner = ((await $.store.get('owner')) as string | undefined) ?? ''
91    await $.command.register({ name: 'stamp', description: 'ตรายางดิจิทัล ปั๊มเล่นในแผงหรือส่งออก SVG (/stamp help)', argumentHint: '<ตรา>|custom|export|owner|clear' })
92    return next(e)
93  })
94
95  // ==========================================================================
96  // บทที่ 3: แผงกระดาษ - ตราที่ปั๊มเรียงต่อกัน
97  // ==========================================================================
98  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
99    const { Box, Text } = $.ui.resolve(e)
100    return (
101      <Box flexDirection="column" backgroundColor="#fdfaf2" paddingX={1}>
102        <Text color="#9e9e9e">📄 กระดาษ ({paper.length} ตรา)</Text>
103        {paper.length === 0 && <Text color="#9e9e9e">ปั๊มด้วย /stamp approved</Text>}
104        {paper.slice(-6).map(p => (
105          <Box flexDirection="column" marginTop={1}>
106            {stampAscii(p.text).split('\n').map(line => (
107              <Text color={p.color} bold>
108                {line}
109              </Text>
110            ))}
111            <Text color="#9e9e9e">{thaiDate(p.at)}</Text>
112          </Box>
113        ))}
114      </Box>
115    )
116  })
117
118  on('command.run', { command: 'stamp' }, async ($, e) => {
119    const [sub = '', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
120    const now = await $.clock.now()
121
122    // custom <ข้อความ...> [สี] - คำสุดท้ายเป็นสีถ้าขึ้นต้นด้วย #
123    const custom = (words: string[]): StampStyle | undefined => {
124      const color = /^#[0-9a-f]{3,6}$/i.test(words.at(-1) ?? '') ? words.pop() : undefined
125      const text = words.join(' ').slice(0, 20)
126      return text ? { text, color: color ?? '#c62828', shape: 'rect' } : undefined
127    }
128
129    const stampIt = async (s: StampStyle) => {
130      paper = [...paper, { text: s.text, color: s.color, at: now }].slice(-50)
131      await $.ui.open({ id: PANE, title: '📮 Stamp' })
132      $.ui.invalidate('ui.render')
133      return { text: `ปั๊ก! 📮\n${stampAscii(s.text)}` }
134    }
135
136    switch (sub) {
137      case '':
138        return { text: ['📮 ตรายางทั้งหมด', ...Object.entries(STAMPS).map(([k, s]) => `  ${k.padEnd(12)} ${s.text}`), '', HELP.split('\n').slice(2).join('\n')].join('\n') }
139      case 'help':
140        return { text: HELP }
141      case 'custom': {
142        const s = custom(rest)
143        return s ? stampIt(s) : { text: 'วิธีใช้: /stamp custom ชื่อแบรนด์ [#สี]' }
144      }
145      case 'export': {
146        const s = rest[0] === 'custom' ? custom(rest.slice(1)) : STAMPS[rest[0] ?? '']
147        if (!s) return { text: 'วิธีใช้: /stamp export approved  หรือ  /stamp export custom ชื่อแบรนด์ #1565c0' }
148        const root = (await $.session.cwd()).replace(/[\\/]$/, '')
149        const name = (rest[0] === 'custom' ? 'custom' : rest[0]) ?? 'stamp'
150        const path = `${root}${root.includes('\\') ? '\\' : '/'}stamp-${name}.svg`
151        await $.fs.write(path, stampSvg(s, thaiDate(now), owner))
152        return { text: `💾 บันทึกตรา "${s.text}" แล้ว: ${path}\nลากไปวางในเอกสาร สไลด์ หรือแปลงเป็น PNG ได้` }
153      }
154      case 'owner':
155        owner = rest.join(' ').slice(0, 30)
156        await $.store.set('owner', owner)
157        return { text: owner ? `✍️ ผู้ปั๊ม: ${owner}` : 'ลบชื่อผู้ปั๊มแล้ว' }
158      case 'clear':
159        paper = []
160        $.ui.invalidate('ui.render')
161        return { text: '🧹 ล้างกระดาษแล้ว' }
162      default: {
163        const s = STAMPS[sub]
164        return s ? stampIt(s) : { text: `ไม่มีตรา "${sub}"\n${HELP}` }
165      }
166    }
167  })
168}
169
170/**
171 * ========================================================================
172 * การบ้าน / ต่อยอด
173 * ========================================================================
174 * 1. ปั๊มตราลงไฟล์ PDF จริงด้วย $.process.run เรียกเครื่องมืออย่าง qpdf หรือ Python
175 * 2. ตราวันที่แบบร้านค้า "รับแล้ว 24 ต.ค. 69" ที่เปลี่ยนวันเอง
176 * 3. ปั๊ม "ตรวจแล้ว" อัตโนมัติเมื่อ Claude รีวิวโค้ดเสร็จ (ดัก turn.complete)
177 */
178