ebm-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/bin/cli.js +150 -0
- package/package.json +22 -0
- package/skills/ebm-auth/REFERENCE.md +299 -0
- package/skills/ebm-auth/SKILL.md +38 -0
- package/skills/ebm-form/REFERENCE.md +365 -0
- package/skills/ebm-form/SKILL.md +45 -0
- package/skills/ebm-init/REFERENCE.md +264 -0
- package/skills/ebm-init/SKILL.md +36 -0
- package/skills/ebm-table/REFERENCE.md +337 -0
- package/skills/ebm-table/SKILL.md +37 -0
- package/skills/ebm-thai/REFERENCE.md +127 -0
- package/skills/ebm-thai/SKILL.md +29 -0
- package/skills/ebm-upload/REFERENCE.md +521 -0
- package/skills/ebm-upload/SKILL.md +33 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ebm-init
|
|
3
|
+
description: Initialize a new Next.js EBM project. Runs create-next-app then configures Tailwind v4, color mode, UI library (shadcn/ui, Ant Design, MUI, or none), and database (Prisma or Drizzle with PostgreSQL/MySQL/SQLite). Saves choices to ebm.config.json for other ebm-* skills to read. Use when user invokes /ebm-init, asks to start a new Next.js project, or scaffold a new dashboard.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /ebm-init
|
|
7
|
+
|
|
8
|
+
### Step 1 — Ask (in order, one at a time)
|
|
9
|
+
```
|
|
10
|
+
1. Project type: landing | backoffice | both
|
|
11
|
+
2. Package manager: npm | yarn | pnpm
|
|
12
|
+
3. Tailwind CSS? yes → dark/light/system + primary color (#hex or preset)
|
|
13
|
+
no → skip
|
|
14
|
+
4. UI library? shadcn/ui | Ant Design | MUI | none
|
|
15
|
+
5. Database? yes → ORM: Prisma | Drizzle
|
|
16
|
+
provider: PostgreSQL | MySQL | SQLite
|
|
17
|
+
no → skip
|
|
18
|
+
6. Add auth now? yes → run /ebm-auth after init
|
|
19
|
+
no → skip
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Step 2 — Run CLI
|
|
23
|
+
```bash
|
|
24
|
+
npx create-next-app@latest . --typescript --app --src-dir --no-tailwind --no-eslint
|
|
25
|
+
```
|
|
26
|
+
Then scaffold additional files per answers. See [REFERENCE.md](REFERENCE.md).
|
|
27
|
+
|
|
28
|
+
### Step 3 — Save config
|
|
29
|
+
Write answers to `ebm.config.json` in project root.
|
|
30
|
+
|
|
31
|
+
### Step 4 — Summary + next steps
|
|
32
|
+
|
|
33
|
+
## Shared rules
|
|
34
|
+
- Tailwind v4: `@import "tailwindcss"` + `@tailwindcss/postcss`, no config file
|
|
35
|
+
- Path alias: `@/*` → `./src/*` always
|
|
36
|
+
- Thai UI text: use formal Thai — see `/ebm-thai` glossary
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
# ebm-table Reference
|
|
2
|
+
|
|
3
|
+
## Input required from user
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
1. Feature name — e.g. "users", "products" (used for file/route naming)
|
|
7
|
+
2. Prisma model — e.g. "User", "Product" (used in Prisma queries)
|
|
8
|
+
3. Columns — e.g. "name:string, email:string, role:enum, createdAt:date"
|
|
9
|
+
4. Optional features:
|
|
10
|
+
- Column filters? (yes/no)
|
|
11
|
+
- Row selection + bulk actions? (yes/no)
|
|
12
|
+
- Export CSV/Excel? (yes/no)
|
|
13
|
+
- Expandable rows? (yes/no)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Config detection
|
|
17
|
+
Read `ebm.config.json` for `uiLib` and `projectType`.
|
|
18
|
+
- `projectType: backoffice` → route: `src/app/(dashboard)/[feature]/page.tsx`
|
|
19
|
+
- `projectType: landing` → route: `src/app/[feature]/page.tsx`
|
|
20
|
+
- `projectType: both` → ask which section
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Files to generate
|
|
25
|
+
|
|
26
|
+
| File | Purpose |
|
|
27
|
+
|------|---------|
|
|
28
|
+
| `src/app/(dashboard)/[feature]/page.tsx` | Page — reads URL params, passes to table |
|
|
29
|
+
| `src/components/tables/[Feature]Table.tsx` | Table component with columns, actions |
|
|
30
|
+
| `src/app/api/[feature]/route.ts` | GET endpoint with pagination/search/sort |
|
|
31
|
+
| `src/lib/schemas/[feature].schema.ts` | Zod schema for query params validation |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## URL query params pattern
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
GET /api/[feature]?page=1&pageSize=10&search=&sortBy=createdAt&sortOrder=desc
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Response shape:
|
|
42
|
+
```typescript
|
|
43
|
+
{
|
|
44
|
+
data: T[]
|
|
45
|
+
total: number
|
|
46
|
+
page: number
|
|
47
|
+
pageSize: number
|
|
48
|
+
totalPages: number
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Template: `src/lib/schemas/[feature].schema.ts`
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { z } from 'zod'
|
|
58
|
+
|
|
59
|
+
export const [feature]QuerySchema = z.object({
|
|
60
|
+
page: z.coerce.number().min(1).default(1),
|
|
61
|
+
pageSize: z.coerce.number().min(1).max(100).default(10),
|
|
62
|
+
search: z.string().optional().default(''),
|
|
63
|
+
sortBy: z.string().optional().default('createdAt'),
|
|
64
|
+
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
export type [Feature]Query = z.infer<typeof [feature]QuerySchema>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Template: `src/app/api/[feature]/route.ts`
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { NextRequest, NextResponse } from 'next/server'
|
|
76
|
+
import { getServerSession } from 'next-auth'
|
|
77
|
+
import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options'
|
|
78
|
+
import { prisma } from '@/lib/prisma'
|
|
79
|
+
import { [feature]QuerySchema } from '@/lib/schemas/[feature].schema'
|
|
80
|
+
|
|
81
|
+
export async function GET(req: NextRequest) {
|
|
82
|
+
const session = await getServerSession(authOptions)
|
|
83
|
+
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
84
|
+
|
|
85
|
+
const params = Object.fromEntries(req.nextUrl.searchParams)
|
|
86
|
+
const query = [feature]QuerySchema.parse(params)
|
|
87
|
+
|
|
88
|
+
const where = query.search
|
|
89
|
+
? {
|
|
90
|
+
OR: [
|
|
91
|
+
// Add searchable string fields here e.g.:
|
|
92
|
+
// { name: { contains: query.search, mode: 'insensitive' } },
|
|
93
|
+
// { email: { contains: query.search, mode: 'insensitive' } },
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
: {}
|
|
97
|
+
|
|
98
|
+
const [data, total] = await Promise.all([
|
|
99
|
+
prisma.[model].findMany({
|
|
100
|
+
where,
|
|
101
|
+
skip: (query.page - 1) * query.pageSize,
|
|
102
|
+
take: query.pageSize,
|
|
103
|
+
orderBy: { [query.sortBy]: query.sortOrder },
|
|
104
|
+
}),
|
|
105
|
+
prisma.[model].count({ where }),
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
return NextResponse.json({
|
|
109
|
+
data,
|
|
110
|
+
total,
|
|
111
|
+
page: query.page,
|
|
112
|
+
pageSize: query.pageSize,
|
|
113
|
+
totalPages: Math.ceil(total / query.pageSize),
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Template: `src/app/(dashboard)/[feature]/page.tsx`
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Suspense } from 'react'
|
|
124
|
+
import { [Feature]Table } from '@/components/tables/[Feature]Table'
|
|
125
|
+
|
|
126
|
+
export default function [Feature]Page() {
|
|
127
|
+
return (
|
|
128
|
+
<div>
|
|
129
|
+
<div className="flex items-center justify-between mb-6">
|
|
130
|
+
<h1 className="text-2xl font-bold text-white">[Feature Label]</h1>
|
|
131
|
+
<a
|
|
132
|
+
href="/dashboard/[feature]/new"
|
|
133
|
+
className="px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors"
|
|
134
|
+
>
|
|
135
|
+
+ Add [Item]
|
|
136
|
+
</a>
|
|
137
|
+
</div>
|
|
138
|
+
<Suspense fallback={<div className="text-slate-400">Loading...</div>}>
|
|
139
|
+
<[Feature]Table />
|
|
140
|
+
</Suspense>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Template: `src/components/tables/[Feature]Table.tsx` (shadcn/ui)
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
'use client'
|
|
152
|
+
|
|
153
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
154
|
+
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
|
|
155
|
+
|
|
156
|
+
interface [Feature]Row {
|
|
157
|
+
id: number | string
|
|
158
|
+
// Add columns from user input
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface TableResponse {
|
|
162
|
+
data: [Feature]Row[]
|
|
163
|
+
total: number
|
|
164
|
+
page: number
|
|
165
|
+
pageSize: number
|
|
166
|
+
totalPages: number
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function [Feature]Table() {
|
|
170
|
+
const router = useRouter()
|
|
171
|
+
const pathname = usePathname()
|
|
172
|
+
const searchParams = useSearchParams()
|
|
173
|
+
|
|
174
|
+
const page = Number(searchParams.get('page') ?? 1)
|
|
175
|
+
const pageSize = Number(searchParams.get('pageSize') ?? 10)
|
|
176
|
+
const search = searchParams.get('search') ?? ''
|
|
177
|
+
const sortBy = searchParams.get('sortBy') ?? 'createdAt'
|
|
178
|
+
const sortOrder = (searchParams.get('sortOrder') ?? 'desc') as 'asc' | 'desc'
|
|
179
|
+
|
|
180
|
+
const [data, setData] = useState<TableResponse | null>(null)
|
|
181
|
+
const [loading, setLoading] = useState(true)
|
|
182
|
+
|
|
183
|
+
const setParam = useCallback((key: string, value: string) => {
|
|
184
|
+
const params = new URLSearchParams(searchParams.toString())
|
|
185
|
+
params.set(key, value)
|
|
186
|
+
if (key !== 'page') params.set('page', '1')
|
|
187
|
+
router.push(`${pathname}?${params.toString()}`)
|
|
188
|
+
}, [router, pathname, searchParams])
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
setLoading(true)
|
|
192
|
+
const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize), search, sortBy, sortOrder })
|
|
193
|
+
fetch(`/api/[feature]?${params}`)
|
|
194
|
+
.then((r) => r.json())
|
|
195
|
+
.then((d) => { setData(d); setLoading(false) })
|
|
196
|
+
}, [page, pageSize, search, sortBy, sortOrder])
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="bg-slate-800 border border-slate-700 rounded-xl overflow-hidden">
|
|
200
|
+
{/* Search bar */}
|
|
201
|
+
<div className="p-4 border-b border-slate-700">
|
|
202
|
+
<input
|
|
203
|
+
type="text"
|
|
204
|
+
placeholder="Search..."
|
|
205
|
+
defaultValue={search}
|
|
206
|
+
onChange={(e) => setParam('search', e.target.value)}
|
|
207
|
+
className="w-full max-w-sm bg-slate-900 border border-slate-600 rounded-lg px-3 py-2 text-white placeholder-slate-500 text-sm focus:outline-none focus:border-blue-500"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
{/* Table */}
|
|
212
|
+
<div className="overflow-x-auto">
|
|
213
|
+
<table className="w-full text-sm">
|
|
214
|
+
<thead>
|
|
215
|
+
<tr className="border-b border-slate-700 text-slate-400">
|
|
216
|
+
{/* Generate th per column — add onClick for sort */}
|
|
217
|
+
<th className="text-left px-4 py-3 font-medium cursor-pointer hover:text-white"
|
|
218
|
+
onClick={() => { setParam('sortBy', 'name'); setParam('sortOrder', sortOrder === 'asc' ? 'desc' : 'asc') }}>
|
|
219
|
+
Name {sortBy === 'name' && (sortOrder === 'asc' ? '↑' : '↓')}
|
|
220
|
+
</th>
|
|
221
|
+
<th className="text-left px-4 py-3 font-medium">Email</th>
|
|
222
|
+
<th className="text-left px-4 py-3 font-medium">Actions</th>
|
|
223
|
+
</tr>
|
|
224
|
+
</thead>
|
|
225
|
+
<tbody>
|
|
226
|
+
{loading ? (
|
|
227
|
+
<tr><td colSpan={99} className="px-4 py-8 text-center text-slate-500">Loading...</td></tr>
|
|
228
|
+
) : data?.data.length === 0 ? (
|
|
229
|
+
<tr><td colSpan={99} className="px-4 py-8 text-center text-slate-500">No data found</td></tr>
|
|
230
|
+
) : (
|
|
231
|
+
data?.data.map((row) => (
|
|
232
|
+
<tr key={row.id} className="border-b border-slate-700/50 hover:bg-slate-700/30 transition-colors">
|
|
233
|
+
{/* Generate td per column */}
|
|
234
|
+
<td className="px-4 py-3 text-white">{(row as any).name}</td>
|
|
235
|
+
<td className="px-4 py-3 text-slate-300">{(row as any).email}</td>
|
|
236
|
+
<td className="px-4 py-3">
|
|
237
|
+
<div className="flex items-center gap-2">
|
|
238
|
+
<a href={`/dashboard/[feature]/${row.id}`}
|
|
239
|
+
className="text-xs text-blue-400 hover:text-blue-300 transition-colors">View</a>
|
|
240
|
+
<a href={`/dashboard/[feature]/${row.id}/edit`}
|
|
241
|
+
className="text-xs text-slate-400 hover:text-white transition-colors">Edit</a>
|
|
242
|
+
<button onClick={() => {/* delete handler */}}
|
|
243
|
+
className="text-xs text-red-400 hover:text-red-300 transition-colors">Delete</button>
|
|
244
|
+
</div>
|
|
245
|
+
</td>
|
|
246
|
+
</tr>
|
|
247
|
+
))
|
|
248
|
+
)}
|
|
249
|
+
</tbody>
|
|
250
|
+
</table>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Pagination */}
|
|
254
|
+
{data && data.totalPages > 1 && (
|
|
255
|
+
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-700">
|
|
256
|
+
<span className="text-sm text-slate-400">
|
|
257
|
+
{((page - 1) * pageSize) + 1}–{Math.min(page * pageSize, data.total)} of {data.total}
|
|
258
|
+
</span>
|
|
259
|
+
<div className="flex gap-2">
|
|
260
|
+
<button
|
|
261
|
+
disabled={page <= 1}
|
|
262
|
+
onClick={() => setParam('page', String(page - 1))}
|
|
263
|
+
className="px-3 py-1 text-sm rounded bg-slate-700 text-slate-300 hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
264
|
+
>← Prev</button>
|
|
265
|
+
<button
|
|
266
|
+
disabled={page >= data.totalPages}
|
|
267
|
+
onClick={() => setParam('page', String(page + 1))}
|
|
268
|
+
className="px-3 py-1 text-sm rounded bg-slate-700 text-slate-300 hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
269
|
+
>Next →</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## UI Library variants
|
|
281
|
+
|
|
282
|
+
### Ant Design
|
|
283
|
+
Replace table body with `<Table>` component:
|
|
284
|
+
```typescript
|
|
285
|
+
import { Table, Input } from 'antd'
|
|
286
|
+
// columns array with { title, dataIndex, sorter, render }
|
|
287
|
+
// Use Table's pagination prop: { current: page, pageSize, total, onChange }
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### MUI
|
|
291
|
+
Replace with `<DataGrid>`:
|
|
292
|
+
```typescript
|
|
293
|
+
import { DataGrid } from '@mui/x-data-grid'
|
|
294
|
+
// columns array with { field, headerName, sortable, renderCell }
|
|
295
|
+
// paginationModel={{ page: page-1, pageSize }}, onPaginationModelChange
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Optional: Column filters
|
|
301
|
+
Add filter dropdowns above table:
|
|
302
|
+
```typescript
|
|
303
|
+
const status = searchParams.get('status') ?? ''
|
|
304
|
+
// <select onChange={(e) => setParam('status', e.target.value)}>
|
|
305
|
+
// Add to API where clause: ...(status && { status })
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Optional: Row selection + bulk actions
|
|
309
|
+
```typescript
|
|
310
|
+
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
311
|
+
// Checkbox column + bulk action bar shown when selected.size > 0
|
|
312
|
+
// Bulk actions: delete selected, export selected
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Optional: Export CSV
|
|
316
|
+
```typescript
|
|
317
|
+
// Add button → GET /api/[feature]?export=csv (no pagination, all rows)
|
|
318
|
+
// API: if (export === 'csv') return CSV response with Content-Disposition header
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Post-generation summary
|
|
324
|
+
```
|
|
325
|
+
✓ Generated 4 files for [feature] table
|
|
326
|
+
|
|
327
|
+
Files:
|
|
328
|
+
src/app/(dashboard)/[feature]/page.tsx
|
|
329
|
+
src/components/tables/[Feature]Table.tsx
|
|
330
|
+
src/app/api/[feature]/route.ts
|
|
331
|
+
src/lib/schemas/[feature].schema.ts
|
|
332
|
+
|
|
333
|
+
TODO in generated files:
|
|
334
|
+
1. Add OR search fields in route.ts (marked with comment)
|
|
335
|
+
2. Add column td cells in [Feature]Table.tsx
|
|
336
|
+
3. Implement delete handler
|
|
337
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ebm-table
|
|
3
|
+
description: Generate a server-side paginated data table with search, sort, and URL-based state. Creates a table component, API route with GET + pagination, and Zod schema. Reads UI library and project type from ebm.config.json. Use when user invokes /ebm-table or asks to build a data table, list page, or admin table.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /ebm-table
|
|
7
|
+
|
|
8
|
+
### Step 1 — Ask (in order)
|
|
9
|
+
```
|
|
10
|
+
1. Feature name — e.g. "users"
|
|
11
|
+
2. Prisma model — e.g. "User"
|
|
12
|
+
3. Columns — e.g. "name:string, email:string, role:enum, createdAt:date"
|
|
13
|
+
4. Optional: column filters? bulk actions? export CSV? expandable rows?
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Step 2 — Read config
|
|
17
|
+
```
|
|
18
|
+
ebm.config.json → uiLib, projectType
|
|
19
|
+
backoffice → src/app/(dashboard)/[feature]/page.tsx
|
|
20
|
+
landing → src/app/[feature]/page.tsx
|
|
21
|
+
both → ask which section
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Step 3 — Generate 4 files
|
|
25
|
+
```
|
|
26
|
+
src/app/(dashboard)/[feature]/page.tsx
|
|
27
|
+
src/components/tables/[Feature]Table.tsx
|
|
28
|
+
src/app/api/[feature]/route.ts
|
|
29
|
+
src/lib/schemas/[feature].schema.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
See [REFERENCE.md](REFERENCE.md) for templates and UI lib variants.
|
|
33
|
+
|
|
34
|
+
## Shared rules
|
|
35
|
+
- URL query params for state: `?page=1&pageSize=10&search=&sortBy=&sortOrder=`
|
|
36
|
+
- Path alias: `@/*` → `./src/*` always
|
|
37
|
+
- Thai UI text: use formal Thai — see `/ebm-thai` glossary
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# ebm-thai Reference
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Enforce formal Thai language in all UI labels, buttons, messages, and placeholders.
|
|
5
|
+
Apply this glossary whenever generating Thai text in any ebm-skills command.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Glossary — Actions (ปุ่ม / เมนู)
|
|
10
|
+
|
|
11
|
+
| ❌ ไม่ทางการ | ✅ ทางการ |
|
|
12
|
+
|-------------|----------|
|
|
13
|
+
| ลบ | ลบข้อมูล |
|
|
14
|
+
| แก้ | แก้ไข |
|
|
15
|
+
| เพิ่ม | เพิ่มข้อมูล |
|
|
16
|
+
| ดู | ดูรายละเอียด |
|
|
17
|
+
| หา / ค้น | ค้นหา |
|
|
18
|
+
| กด | คลิก |
|
|
19
|
+
| ใส่ | กรอก |
|
|
20
|
+
| ส่ง | ส่งข้อมูล |
|
|
21
|
+
| บันทึก | บันทึกข้อมูล |
|
|
22
|
+
| ออก | ออกจากระบบ |
|
|
23
|
+
| เข้า | เข้าสู่ระบบ |
|
|
24
|
+
| กลับ | ย้อนกลับ |
|
|
25
|
+
|
|
26
|
+
## Glossary — Status / Toggle
|
|
27
|
+
|
|
28
|
+
| ❌ ไม่ทางการ | ✅ ทางการ |
|
|
29
|
+
|-------------|----------|
|
|
30
|
+
| เปิด (toggle) | เปิดใช้งาน |
|
|
31
|
+
| ปิด (toggle) | ปิดการใช้งาน |
|
|
32
|
+
| ใช้งาน / ไม่ใช้ | ใช้งาน / ไม่ใช้งาน |
|
|
33
|
+
| โอเค / ok | ตกลง |
|
|
34
|
+
|
|
35
|
+
## Glossary — Form / Placeholder
|
|
36
|
+
|
|
37
|
+
| ❌ ไม่ทางการ | ✅ ทางการ |
|
|
38
|
+
|-------------|----------|
|
|
39
|
+
| ใส่ชื่อ... | กรอกชื่อ |
|
|
40
|
+
| ใส่อีเมล... | กรอกอีเมล |
|
|
41
|
+
| เลือก... | โปรดเลือก |
|
|
42
|
+
|
|
43
|
+
## Glossary — Messages / Feedback
|
|
44
|
+
|
|
45
|
+
| ❌ ไม่ทางการ | ✅ ทางการ |
|
|
46
|
+
|-------------|----------|
|
|
47
|
+
| ลบแล้ว | ลบข้อมูลเรียบร้อยแล้ว |
|
|
48
|
+
| บันทึกแล้ว | บันทึกข้อมูลเรียบร้อยแล้ว |
|
|
49
|
+
| ผิดพลาด | เกิดข้อผิดพลาด |
|
|
50
|
+
| โหลด... | กำลังโหลดข้อมูล |
|
|
51
|
+
| คุณ (ในข้อความ) | ผู้ใช้งาน |
|
|
52
|
+
|
|
53
|
+
## Glossary — Ownership pattern
|
|
54
|
+
|
|
55
|
+
| ❌ ไม่ทางการ | ✅ ทางการ |
|
|
56
|
+
|-------------|----------|
|
|
57
|
+
| โปรไฟล์ของฉัน | ข้อมูลส่วนตัว |
|
|
58
|
+
| รายการของฉัน | รายการ |
|
|
59
|
+
| งานของฉัน | งานที่รับผิดชอบ |
|
|
60
|
+
| การแจ้งเตือนของฉัน | การแจ้งเตือน |
|
|
61
|
+
|
|
62
|
+
**Rule:** ตัด "ของฉัน" / "ของคุณ" ออกทั้งหมด ใช้ชื่อ section แทน
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## `/ebm-thai` — Scan & Fix mode
|
|
67
|
+
|
|
68
|
+
### Step 1 — Scan codebase
|
|
69
|
+
Search for Thai text in `.tsx`, `.ts`, `.jsx`, `.js` files:
|
|
70
|
+
- String literals containing Thai characters
|
|
71
|
+
- JSX text nodes
|
|
72
|
+
- Placeholder / label / title / button text
|
|
73
|
+
|
|
74
|
+
### Step 2 — Identify violations
|
|
75
|
+
Match against glossary. Flag:
|
|
76
|
+
- Exact matches (ลบ, แก้, ใส่ ฯลฯ)
|
|
77
|
+
- Pattern "X ของฉัน" / "X ของคุณ"
|
|
78
|
+
- Casual pronouns (คุณ, ฉัน in UI-facing text)
|
|
79
|
+
|
|
80
|
+
### Step 3 — Present findings
|
|
81
|
+
Show a table:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
File | Line | Found | Suggestion
|
|
85
|
+
----------------------------------------|------|--------------|------------------
|
|
86
|
+
src/components/tables/UserTable.tsx | 42 | "ลบ" | "ลบข้อมูล"
|
|
87
|
+
src/components/forms/ProfileForm.tsx | 18 | "โปรไฟล์ของฉัน" | "ข้อมูลส่วนตัว"
|
|
88
|
+
src/app/(dashboard)/dashboard/page.tsx | 67 | "บันทึกแล้ว" | "บันทึกข้อมูลเรียบร้อยแล้ว"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Step 4 — Ask before fixing
|
|
92
|
+
```
|
|
93
|
+
พบ [N] คำที่ควรแก้ไข
|
|
94
|
+
แก้ไขทั้งหมดเลย? (yes/no/เลือกทีละไฟล์)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Step 5 — Apply fixes
|
|
98
|
+
Replace strings in-place. Preserve surrounding JSX, string interpolation, and formatting.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Embed rules (used by all ebm-skills commands)
|
|
103
|
+
|
|
104
|
+
When generating ANY Thai-language UI text, apply these rules automatically:
|
|
105
|
+
|
|
106
|
+
1. **Button labels** — ใช้คำกริยาทางการ: "บันทึกข้อมูล" ไม่ใช่ "บันทึก" เมื่อเป็น action หลัก
|
|
107
|
+
2. **Placeholder text** — ขึ้นต้นด้วย "กรอก" หรือ "โปรดเลือก" เสมอ
|
|
108
|
+
3. **Success messages** — ลงท้ายด้วย "เรียบร้อยแล้ว"
|
|
109
|
+
4. **Error messages** — ขึ้นต้นด้วย "เกิดข้อผิดพลาด"
|
|
110
|
+
5. **Section titles** — ห้ามมีคำว่า "ของฉัน" / "ของคุณ" ใช้ชื่อ section ตรงๆ
|
|
111
|
+
6. **Toggle labels** — ใช้ "เปิดใช้งาน" / "ปิดการใช้งาน" ไม่ใช่ "เปิด" / "ปิด"
|
|
112
|
+
7. **Confirmation dialogs** — ใช้ "ยืนยัน" + ชื่อ action เช่น "ยืนยันการลบข้อมูล"
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Post-scan summary template
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
✓ สแกน [N] ไฟล์ พบ [N] คำที่ควรแก้ไข
|
|
120
|
+
|
|
121
|
+
แก้ไขแล้ว:
|
|
122
|
+
src/components/... (3 คำ)
|
|
123
|
+
src/app/... (1 คำ)
|
|
124
|
+
|
|
125
|
+
ไม่พบปัญหา:
|
|
126
|
+
src/lib/...
|
|
127
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ebm-thai
|
|
3
|
+
description: Scan the codebase for informal Thai UI text (buttons, labels, placeholders, messages) and replace with formal Thai alternatives using a built-in glossary. Also enforces formal Thai automatically when generating any UI text. Use when user invokes /ebm-thai, asks to fix Thai language formality, or after generating Thai UI text that was rejected for being too informal.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /ebm-thai
|
|
7
|
+
|
|
8
|
+
### Step 1 — Scan codebase
|
|
9
|
+
Search `.tsx`, `.ts`, `.jsx`, `.js` files for Thai text in:
|
|
10
|
+
- JSX text nodes, string literals, placeholder/label/title/button props
|
|
11
|
+
|
|
12
|
+
### Step 2 — Match against glossary
|
|
13
|
+
Flag violations from [REFERENCE.md](REFERENCE.md):
|
|
14
|
+
- Exact matches (ลบ, แก้, ใส่ ฯลฯ)
|
|
15
|
+
- Pattern "X ของฉัน" / "X ของคุณ" → ตัดออก ใช้ชื่อ section แทน
|
|
16
|
+
- Casual pronouns (คุณ, ฉัน) ในข้อความ UI
|
|
17
|
+
|
|
18
|
+
### Step 3 — Present findings table
|
|
19
|
+
Show file + line + found + suggestion. Then ask:
|
|
20
|
+
```
|
|
21
|
+
พบ [N] คำที่ควรแก้ไข — แก้ไขทั้งหมดเลย? (yes / no / เลือกทีละไฟล์)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Step 4 — Apply fixes in-place
|
|
25
|
+
Preserve surrounding JSX, interpolation, and formatting.
|
|
26
|
+
|
|
27
|
+
See [REFERENCE.md](REFERENCE.md) for full glossary.
|
|
28
|
+
|
|
29
|
+
> **Embed rule:** เมื่อ generate Thai UI text ในทุก command ให้ใช้ glossary นี้อัตโนมัติ
|