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

ตรายางดิจิทัล 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ไฟล์ SVG ใช้ฟอนต์ Sarabun หรือ Noto Sans Thai ถ้าเครื่องไม่มีจะใช้ฟอนต์ไทยที่มีแทน ตรานี้ใช้ตกแต่งเอกสาร ไม่ใช่ลายเซ็นหรือตราที่มีผลทางกฎหมาย
hooks/register.tsx 178 lines1/**
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
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