gov-layout 1.3.4 → 1.3.6

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 CHANGED
@@ -1,585 +1,588 @@
1
- # gov-layout
2
-
3
- Government Layout Components สำหรับเว็บแอปพลิเคชันภาครัฐ
4
-
5
- > ใช้คู่กับ `gov-token-css` เพื่อให้สีตรงตาม Design System
6
-
7
- ---
8
-
9
- ## 📥 Installation
10
-
11
- ```bash
12
- npm install gov-layout gov-token-css
13
- ```
14
-
15
- ```css
16
- /* ใน globals.css */
17
- @import "gov-token-css";
18
- ```
19
-
20
- ---
21
-
22
- ## 📦 Components ทั้งหมด
23
-
24
- | Component | ใช้สำหรับ | import |
25
- |-----------|----------|--------|
26
- | `StaffSidebar` | Sidebar เจ้าหน้าที่ | `import { StaffSidebar } from 'gov-layout'` |
27
- | `UserHeader` | Header ผู้ใช้ทั่วไป | `import { UserHeader } from 'gov-layout'` |
28
- | `UserSidebar` | Sidebar ผู้ใช้ (slide-in) | `import { UserSidebar } from 'gov-layout'` |
29
- | `SettingsPanel` | หน้าตั้งค่า (font + theme) | `import { SettingsPanel } from 'gov-layout'` |
30
- | `SettingsProvider` | Context wrapper | `import { SettingsProvider } from 'gov-layout'` |
31
- | `useSettings` | Hook อ่าน/เปลี่ยนค่า | `import { useSettings } from 'gov-layout'` |
32
- | `Icons` | ไอคอน built-in 33 ตัว | `import { Icons } from 'gov-layout'` |
33
-
34
- ---
35
-
36
- ## 🎯 Icons (ไอคอน built-in)
37
-
38
- import ครั้งเดียว ใช้ได้ทุกไอคอน — ไม่ต้องสร้าง SVG เอง
39
-
40
- ### วิธีใช้งาน
41
-
42
- ```tsx
43
- import { Icons } from 'gov-layout';
44
-
45
- // ใช้ตรงๆ
46
- <Icons.Folder />
47
- <Icons.User />
48
- <Icons.Gear />
49
-
50
- // ปรับขนาด (default = 20)
51
- <Icons.Home size={24} />
52
-
53
- // ใส่ใน MenuItem
54
- const menuItems: MenuItem[] = [
55
- { id: 'services', title: 'งานบริการ', icon: <Icons.Folder />, path: '/services' },
56
- { id: 'users', title: 'จัดการผู้ใช้', icon: <Icons.User />, path: '/users' },
57
- { id: 'reports', title: 'รายงาน', icon: <Icons.BarChart />, path: '/reports' },
58
- ];
59
- ```
60
-
61
- ### ไอคอนทั้งหมด
62
-
63
- | ชื่อ | Component | ใช้สำหรับ |
64
- |------|-----------|----------|
65
- | 🏠 | `Icons.Home` | หน้าแรก / Dashboard |
66
- | 🔍 | `Icons.Search` | ค้นหา |
67
- | 🔔 | `Icons.Bell` | แจ้งเตือน |
68
- | 📁 | `Icons.Folder` | งานบริการ / หมวดหมู่ |
69
- | 📋 | `Icons.Clipboard` | แบบฟอร์ม / คำร้อง |
70
- | 📄 | `Icons.FileText` | รายงาน / เอกสาร |
71
- | 📅 | `Icons.Calendar` | ตารางงาน / นัดหมาย |
72
- | 👤 | `Icons.User` | ข้อมูลผู้ใช้ |
73
- | 👥 | `Icons.Users` | จัดการสมาชิก |
74
- | ⚙️ | `Icons.Gear` | ตั้งค่าระบบ |
75
- | 🔧 | `Icons.Wrench` | ซ่อมบำรุง |
76
- | 🛡️ | `Icons.Shield` | สิทธิ์การใช้งาน |
77
- | | `Icons.HelpCircle` | ช่วยเหลือ |
78
- | 📊 | `Icons.BarChart` | สถิติ / รายงาน |
79
- | 🕐 | `Icons.History` | ประวัติการใช้งาน |
80
- | 💾 | `Icons.Database` | สำรองข้อมูล |
81
- | 🏢 | `Icons.Building` | หน่วยงาน / องค์กร |
82
- | 📍 | `Icons.MapPin` | สถานที่ |
83
- | 📞 | `Icons.Phone` | ติดต่อ |
84
- | ✉️ | `Icons.Mail` | อีเมล / ข้อความ |
85
- | | `Icons.CheckCircle` | สำเร็จ / อนุมัติ |
86
- | ⚠️ | `Icons.AlertTriangle` | คำเตือน |
87
- | | `Icons.XCircle` | ปฏิเสธ / ข้อผิดพลาด |
88
- | | `Icons.PlusCircle` | เพิ่มรายการ |
89
- | 🚪 | `Icons.LogOut` | ออกจากระบบ |
90
- | ⬇️ | `Icons.Download` | ดาวน์โหลด |
91
- | ⬆️ | `Icons.Upload` | อัปโหลด |
92
- | 🖨️ | `Icons.Printer` | พิมพ์เอกสาร |
93
- | | `Icons.Star` | รายการโปรด |
94
- | ❤️ | `Icons.Heart` | ถูกใจ |
95
- | 👁️ | `Icons.Eye` | ดูรายละเอียด |
96
- | ✏️ | `Icons.Edit` | แก้ไข |
97
- | 🗑️ | `Icons.Trash` | ลบ |
98
-
99
- ### IconProps
100
-
101
- | Prop | Type | Default | คำอธิบาย |
102
- |------|------|---------|----------|
103
- | `size` | `number?` | `20` | ขนาด (width & height) |
104
- | `className` | `string?` | - | CSS class |
105
- | `style` | `CSSProperties?` | - | inline style |
106
-
107
- > 💡 ถ้าต้องการ import ทีละตัวก็ได้: `import { FolderIcon, UserIcon } from 'gov-layout'`
108
-
109
- ## 1. StaffSidebar (เจ้าหน้าที่)
110
-
111
- Sidebar ฝั่งซ้ายแบบ fixed — รองรับพับ/กาง (collapsible)
112
-
113
- ### ตัวอย่างใช้งาน
114
-
115
- ```tsx
116
- import { StaffSidebar } from 'gov-layout';
117
- import type { MenuItem } from 'gov-layout';
118
-
119
- // ไม่ต้องระบุ icon sidebar จับคู่ไอคอนให้อัตโนมัติจาก id
120
- const menuItems: MenuItem[] = [
121
- {
122
- id: 'services', // 📁 FolderIcon
123
- title: 'งานบริการ',
124
- children: [
125
- { id: 'water', title: 'ประปา', path: '/services/water' },
126
- { id: 'tax', title: 'ภาษี', path: '/services/tax' },
127
- ],
128
- dividerAfter: true,
129
- },
130
- { id: 'users', title: 'จัดการผู้ใช้', path: '/users' }, // → 👥 UsersIcon
131
- { id: 'reports', title: 'รายงาน', path: '/reports' }, // → 📊 BarChartIcon
132
- { id: 'roles', title: 'สิทธิ์การใช้งาน', path: '/roles' }, // → 🛡️ ShieldIcon
133
- { id: 'logs', title: 'ประวัติ', path: '/logs' }, // → 🕐 HistoryIcon
134
- { id: 'backup', title: 'สำรองข้อมูล', path: '/backup' }, // → 💾 DatabaseIcon
135
- ];
136
-
137
- <StaffSidebar
138
- orgLogo="/logo.png"
139
- orgName="เทศบาลตำบลหลักเมือง"
140
- orgSubtitle="จังหวัดราชบุรี"
141
- menuItems={menuItems}
142
- user={{ firstName: 'สมชาย', lastName: 'ใจดี' }}
143
- roleLabel="เจ้าหน้าที่"
144
- currentPath="/services/water"
145
- onNavigate={(path) => router.push(path)}
146
- onLogout={() => signOut()}
147
- onProfile={() => router.push('/profile')}
148
- collapsible
149
- />
150
- ```
151
-
152
- > **💡 Auto-Icon:** ไม่ต้อง import ไอคอน — sidebar จับคู่จาก `id` อัตโนมัติ
153
- > ถ้าอยากกำหนดเอง ก็ส่ง `icon` prop ได้ตามปกติ: `icon: <Icons.Folder />`
154
-
155
- > **💡 ข้อมูลองค์กร (`orgLogo`, `orgName`, `orgSubtitle`) มาจากไหนก็ได้:**
156
- > - ดึงจาก **ตัวกลาง SSO** หลังล็อกอิน (แนะนำ) เช่น `useSSOAuth().organization`
157
- > - ดึงจาก **API** เช่น `fetchOrgInfo()`
158
- > - **Fix ค่า** ตรงๆ ก็ได้ ถ้าใช้ระบบเดียว
159
- >
160
- > Library ไม่ผูกกับแหล่งข้อมูลใดๆ แค่รับ props แล้วแสดงผล
161
-
162
- ### Default Bottom Menu
163
-
164
- ไม่ต้องตั้งค่าเอง — มี **ตั้งค่าระบบ** + **ช่วยเหลือ** อยู่ด้านล่างอัตโนมัติ
165
-
166
- ```
167
- งานบริการ
168
- ประปา / ภาษี / ทะเบียน
169
- ──────────────
170
- จัดการผู้ใช้
171
- รายงาน
172
- ตั้งค่าระบบ ← default (ไม่ต้องส่ง)
173
- ช่วยเหลือ ← default (ไม่ต้องส่ง)
174
- ↕ spacer
175
- โปรไฟล์ + ออกจากระบบ
176
- ```
177
-
178
- ถ้าอยากเปลี่ยน bottom menu เอง:
179
-
180
- ```tsx
181
- <StaffSidebar
182
- bottomMenuItems={[
183
- { id: 'settings', title: 'ตั้งค่า', icon: <Icons.Gear />, path: '/settings' },
184
- ]}
185
- ...
186
- />
187
- ```
188
-
189
- ถ้าไม่อยากมี bottom menu:
190
-
191
- ```tsx
192
- <StaffSidebar bottomMenuItems={[]} ... />
193
- ```
194
-
195
- ### Props ทั้งหมด
196
-
197
- | Prop | Type | Default | คำอธิบาย |
198
- |------|------|---------|----------|
199
- | `orgLogo` | `string?` | - | URL รูปตราองค์กร |
200
- | `orgName` | `string` | **required** | ชื่อองค์กร |
201
- | `orgSubtitle` | `string?` | - | ชื่อรอง เช่น จังหวัด |
202
- | `menuItems` | `MenuItem[]` | **required** | เมนูหลัก |
203
- | `bottomMenuItems` | `MenuItem[]?` | ตั้งค่าระบบ + ช่วยเหลือ | เมนูด้านล่าง |
204
- | `user` | `User \| null` | **required** | ข้อมูลผู้ใช้ |
205
- | `roleLabel` | `string` | **required** | ป้ายตำแหน่ง เช่น "เจ้าหน้าที่" |
206
- | `onNavigate` | `(path) => void` | **required** | callback เมื่อคลิกเมนู |
207
- | `onLogout` | `() => void` | **required** | callback ออกจากระบบ |
208
- | `onProfile` | `() => void?` | - | callback เมื่อกดโปรไฟล์ผู้ใช้ |
209
- | `currentPath` | `string?` | - | path ปัจจุบัน (highlight active) |
210
- | `width` | `string?` | `'280px'` | ความกว้าง sidebar |
211
- | `collapsible` | `boolean?` | `false` | เปิดโหมดพับ/กาง |
212
- | `isOpen` | `boolean?` | - | controlled open/close |
213
- | `onToggle` | `() => void?` | - | callback เมื่อกดพับ/กาง |
214
-
215
- ### Features
216
-
217
- - ✅ Dropdown submenu (พับ/กางอัตโนมัติ)
218
- - ✅ Auto-expand เมื่อ child active
219
- - ✅ Active item highlight
220
- - ✅ Collapsible พับเป็น icon-only 64px, กางเป็น 280px
221
- - ✅ Tooltip เมื่อพับ
222
- - ✅ Default ตั้งค่าระบบ + ช่วยเหลือ (override ได้)
223
- - ✅ โปรไฟล์ + ออกจากระบบ ล่างสุดเสมอ
224
- - ✅ `dividerAfter` เส้นคั่นระหว่างกลุ่ม
225
- - ✅ ใช้ Standard Avatar Placeholder กรณีที่ไม่มีรูปโปรไฟล์หรือโหลดรูปไม่สำเร็จ (v1.3.2+) 🆕
226
-
227
- ---
228
-
229
- ## 2. UserHeader (ผู้ใช้ทั่วไป)
230
-
231
- Header ด้านบนพร้อม notification bell
232
-
233
- ```tsx
234
- import { UserHeader } from 'gov-layout';
235
-
236
- <UserHeader
237
- user={{
238
- firstName: 'ชนธัญ',
239
- pictureUrl: '/avatar.jpg',
240
- subtitle: 'ผู้สูงอายุ', // แสดงใต้ข้อความทักทาย (optional)
241
- }}
242
- notifications={[
243
- {
244
- id: 1,
245
- title: 'คำร้องใหม่รอตรวจสอบ',
246
- description: 'มีคำร้องใหม่เข้ามา กรุณาตรวจสอบ',
247
- date: '2 ชม. ที่แล้ว',
248
- type: 'warning',
249
- isRead: false,
250
- category: 'action', // ← แสดงในแท็บ "ต้องดำเนินการ"
251
- },
252
- {
253
- id: 2,
254
- title: 'คำร้องได้รับการอนุมัติ',
255
- description: 'คำร้องหมายเลข #1234 อนุมัติสำเร็จ',
256
- date: '5 ชม. ที่แล้ว',
257
- type: 'success',
258
- isRead: true,
259
- category: 'general', // ← แสดงในแท็บ "แจ้งเตือนทั่วไป"
260
- },
261
- ]}
262
- onToggleSidebar={() => setOpen(true)}
263
- onMarkAllRead={() => markAllRead()}
264
- onViewAll={() => router.push('/notifications')}
265
- onNotificationClick={(notif) => router.push(`/notifications/${notif.id}`)}
266
- onProfile={() => router.push('/profile')}
267
- />
268
- ```
269
-
270
- ### Props
271
-
272
- | Prop | Type | Default | คำอธิบาย |
273
- |------|------|---------|----------|
274
- | `user.firstName` | `string?` | - | ชื่อ |
275
- | `user.lastName` | `string?` | - | นามสกุล |
276
- | `user.pictureUrl` | `string?` | - | URL รูปโปรไฟล์ |
277
- | `user.subtitle` | `string?` | - | ข้อความใต้ชื่อ เช่น "ผู้สูงอายุ" |
278
- | `notifications` | `NotificationItem[]?` | `[]` | รายการแจ้งเตือน |
279
- | `onToggleSidebar` | `() => void?` | - | callback เปิด sidebar |
280
- | `onMarkAllRead` | `() => void?` | - | callback อ่านทั้งหมดแล้ว |
281
- | `onViewAll` | `() => void?` | - | callback ดูทั้งหมด |
282
- | `onNotificationClick` | `(notification) => void?` | - | callback เมื่อกดแจ้งเตือนแต่ละรายการ |
283
- | `onProfile` | `() => void?` | - | callback เมื่อกดโปรไฟล์ |
284
- | `notificationBell` | `ReactNode?` | - | custom bell icon |
285
- | `className` | `string?` | - | className เพิ่มเติม |
286
-
287
- ### 🔔 Notification Filter Tabs (v1.2.25+)
288
-
289
- Dropdown แจ้งเตือนมีแท็บกรอง 3 หมวด พร้อม badge ตัวเลข:
290
-
291
- | แท็บ | กรองจาก `category` | ตัวอย่างใช้งาน |
292
- |------|-------------------|----------------|
293
- | **ทั้งหมด** | แสดงทุกรายการ | ดูภาพรวม |
294
- | **ต้องดำเนินการ** | `'action'` | คำร้องใหม่รอตรวจสอบ, ต้องอัปโหลดเอกสาร, มีคิวใหม่ |
295
- | **แจ้งเตือนทั่วไป** | `'general'` หรือไม่ระบุ | อนุมัติสำเร็จ, จองสำเร็จ, ยกเลิกแล้ว |
296
-
297
- > 💡 ถ้าไม่ส่ง `category` จะถูกจัดเป็น **แจ้งเตือนทั่วไป** อัตโนมัติ (backward compatible)
298
-
299
- ### Features
300
-
301
- - ✅ Notification bell พร้อม badge (99+ เมื่อเกิน)
302
- - ✅ ไม่มีแจ้งเตือน → ไม่แสดง badge
303
- - ✅ Notification dropdown แบบ scroll
304
- - ✅ **Filter tabs**: ทั้งหมด / ต้องดำเนินการ / แจ้งเตือนทั่วไป 🆕
305
- - ✅ **Badge ตัวเลข** แต่ละแท็บ 🆕
306
- - ✅ **Category badge** "ต้องดำเนินการ" ติดแต่ละรายการ 🆕
307
- - ✅ **Empty state แยกตาม filter** 🆕
308
- - ✅ กดแจ้งเตือนแต่ละรายการ `onNotificationClick`
309
- - ✅ Subtitle ใต้ข้อความทักทาย (เช่น "ผู้สูงอายุ")
310
- - ✅ ปุ่มเปิด sidebar (☰)
311
- - ✅ กดโปรไฟล์ผู้ใช้ (avatar + ชื่อ) → `onProfile`
312
- - ✅ รองรับ Dark Mode
313
-
314
- ---
315
-
316
- ## 3. UserSidebar (ผู้ใช้ทั่วไป)
317
-
318
- Sidebar ฝั่งขวาแบบ slide-in + overlay
319
-
320
- ```tsx
321
- import { UserSidebar } from 'gov-layout';
322
-
323
- <UserSidebar
324
- isOpen={isSidebarOpen}
325
- onClose={() => setIsSidebarOpen(false)}
326
- user={{ firstName: 'สมหญิง', lastName: 'ใจดี', pictureUrl: '/avatar.jpg' }}
327
- roleLabel="ผู้สูงอายุ"
328
- menuItems={[
329
- { id: 'profile', title: 'ข้อมูลส่วนตัว', path: '/profile' },
330
- { id: 'services', title: 'บริการหลัก', path: '/services' },
331
- { id: 'settings', title: 'ตั้งค่าระบบ', path: '/settings' },
332
- ]}
333
- onNavigate={(path) => router.push(path)}
334
- onLogout={() => signOut()}
335
- onProfile={() => router.push('/profile')}
336
- />
337
- ```
338
-
339
- > **หมายเหตุ:** `roleLabel` แต่ละระบบส่งค่าเองได้ เช่น "ผู้สูงอายุ", "ผู้ใช้ปกติ", "อาสาสมัคร"
340
-
341
- ---
342
-
343
- ## 4. SettingsPanel (ตั้งค่าระบบ) 🆕
344
-
345
- ปรับขนาดตัวอักษร (5 ระดับ) + โหมดสว่าง/มืด — ค่าจำใน localStorage
346
-
347
- ### ขั้นตอนที่ 1: ครอบ SettingsProvider
348
-
349
- ```tsx
350
- // app/providers.tsx ต้องเป็น 'use client'
351
- 'use client';
352
- import { SettingsProvider } from 'gov-layout';
353
-
354
- export default function Providers({ children }: { children: React.ReactNode }) {
355
- return <SettingsProvider>{children}</SettingsProvider>;
356
- }
357
- ```
358
-
359
- ```tsx
360
- // app/layout.tsx
361
- import Providers from './providers';
362
-
363
- export default function RootLayout({ children }) {
364
- return (
365
- <html lang="th">
366
- <body>
367
- <Providers>{children}</Providers>
368
- </body>
369
- </html>
370
- );
371
- }
372
- ```
373
-
374
- ### ขั้นตอนที่ 2: วาง SettingsPanel
375
-
376
- ```tsx
377
- import { SettingsPanel } from 'gov-layout';
378
-
379
- // ผู้ใช้ทั่วไป → ทั้ง font + theme
380
- <SettingsPanel />
381
-
382
- // เจ้าหน้าที่แค่ปรับขนาดฟอนต์
383
- <SettingsPanel showTheme={false} />
384
- ```
385
-
386
- | Prop | Type | Default | คำอธิบาย |
387
- |------|------|---------|----------|
388
- | `showTheme` | `boolean?` | `true` | แสดงตัวเลือกโหมดสว่าง/มืด |
389
- | `className` | `string?` | - | className เพิ่มเติม |
390
-
391
- ### ขั้นตอนที่ 3: ใช้ useSettings() hook (ถ้าต้องการ)
392
-
393
- ```tsx
394
- import { useSettings } from 'gov-layout';
395
-
396
- function MyComponent() {
397
- const { theme, toggleTheme, fontSize, setFontSize, fontSizeOption } = useSettings();
398
-
399
- return (
400
- <div>
401
- <p>ธีม: {theme}</p>
402
- <p>ฟอนต์: {fontSize} (×{fontSizeOption.scale})</p>
403
- <button onClick={toggleTheme}>สลับธีม</button>
404
- <button onClick={() => setFontSize('large')}>ฟอนต์ใหญ่</button>
405
- </div>
406
- );
407
- }
408
- ```
409
-
410
- ### ขนาดตัวอักษร (5 ระดับ)
411
-
412
- | ค่า | Label | Scale | ผลลัพธ์ |
413
- |-----|-------|-------|---------|
414
- | `xsmall` | เล็กมาก | ×0.8 | ย่อทุกอย่าง 80% |
415
- | `small` | เล็ก | ×0.9 | ย่อเล็กน้อย |
416
- | `medium` | กลาง | ×1.0 | ค่าเริ่มต้น |
417
- | `large` | ใหญ่ | ×1.2 | ขยาย 120% |
418
- | `xlarge` | ใหญ่มาก | ×1.4 | ขยาย 140% |
419
-
420
- ### หลักการทำงาน
421
-
422
- - **Theme** — เพิ่ม/ลบ class `dark` บน `<html>` ➜ ใช้ CSS `html.dark` จัดสี
423
- - **Font size** — ปรับ `body.style.zoom` + CSS variables ตาม scale
424
- - **Persistence** — เก็บใน localStorage (`app-theme`, `app-font-size`)
425
-
426
- ### Dark mode CSS ที่ต้องเพิ่ม
427
-
428
- ```css
429
- /* globals.css เพิ่มสำหรับ dark mode */
430
- html.dark body { background-color: #0f172a; color: #f1f5f9; }
431
- html.dark aside { background-color: #1e293b !important; }
432
- html.dark header { background-color: #1e293b !important; }
433
- html.dark h1,
434
- html.dark h2,
435
- html.dark h3 { color: #f1f5f9 !important; }
436
- html.dark p { color: #94a3b8 !important; }
437
- ```
438
-
439
- ---
440
-
441
- ## 📐 Types
442
-
443
- ```ts
444
- // เมนู
445
- interface MenuItem {
446
- id: string;
447
- title: string;
448
- path?: string; // path สำหรับ navigate
449
- icon?: React.ReactNode; // icon component
450
- children?: MenuItem[]; // submenu → แสดงเป็น dropdown
451
- dividerAfter?: boolean; // เส้นคั่นด้านล่าง
452
- }
453
-
454
- // ผู้ใช้
455
- interface User {
456
- id?: string | number;
457
- firstName?: string;
458
- lastName?: string;
459
- pictureUrl?: string;
460
- role?: string;
461
- }
462
-
463
- // การแจ้งเตือน
464
- type NotificationCategory = 'action' | 'general';
465
-
466
- interface NotificationItem {
467
- id: string | number;
468
- title: string;
469
- description: string;
470
- date: string;
471
- type: 'info' | 'success' | 'warning' | 'error' | 'reminder';
472
- isRead: boolean;
473
- category?: NotificationCategory; // 🆕 หมวดหมู่: 'action' | 'general'
474
- }
475
-
476
- // ตั้งค่า
477
- type Theme = 'light' | 'dark';
478
- type FontSizeKey = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
479
- ```
480
-
481
- ---
482
-
483
- ## 📁 Layout Examples
484
-
485
- ### Staff Layout (เจ้าหน้าที่)
486
-
487
- ```tsx
488
- 'use client';
489
- import { StaffSidebar, SettingsPanel } from 'gov-layout';
490
-
491
- export default function AdminLayout({ children }) {
492
- const [currentPath, setCurrentPath] = useState('/');
493
-
494
- return (
495
- <div style={{ display: 'flex' }}>
496
- <StaffSidebar
497
- orgLogo="/logo.png"
498
- orgName="เทศบาลตำบลหลักเมือง"
499
- orgSubtitle="จังหวัดราชบุรี"
500
- menuItems={menuItems}
501
- user={user}
502
- roleLabel="เจ้าหน้าที่"
503
- currentPath={currentPath}
504
- onNavigate={(path) => setCurrentPath(path)}
505
- onLogout={() => signOut()}
506
- onProfile={() => setCurrentPath('/profile')}
507
- collapsible
508
- />
509
- <main style={{ marginLeft: 280, flex: 1, padding: 32 }}>
510
- {currentPath === '/settings' ? (
511
- <SettingsPanel showTheme={false} />
512
- ) : (
513
- children
514
- )}
515
- </main>
516
- </div>
517
- );
518
- }
519
- ```
520
-
521
- ### User Layout (ผู้ใช้ทั่วไป)
522
-
523
- ```tsx
524
- 'use client';
525
- import { UserHeader, UserSidebar, SettingsPanel } from 'gov-layout';
526
-
527
- export default function UserLayout({ children }) {
528
- const [open, setOpen] = useState(false);
529
- const [currentPath, setCurrentPath] = useState('/');
530
-
531
- return (
532
- <>
533
- <UserHeader
534
- user={{ ...user, subtitle: 'ผู้สูงอายุ' }}
535
- notifications={notifications}
536
- onToggleSidebar={() => setOpen(true)}
537
- onNotificationClick={(notif) => setCurrentPath(`/notifications/${notif.id}`)}
538
- onProfile={() => setCurrentPath('/profile')}
539
- />
540
- <UserSidebar
541
- isOpen={open}
542
- onClose={() => setOpen(false)}
543
- user={user}
544
- roleLabel="ผู้ใช้ทั่วไป"
545
- menuItems={menuItems}
546
- onNavigate={(path) => setCurrentPath(path)}
547
- onLogout={() => signOut()}
548
- onProfile={() => setCurrentPath('/profile')}
549
- />
550
- <main style={{ padding: 32 }}>
551
- {currentPath === '/settings' ? (
552
- <SettingsPanel showTheme={true} />
553
- ) : (
554
- children
555
- )}
556
- </main>
557
- </>
558
- );
559
- }
560
- ```
561
-
562
- ---
563
-
564
- ## 🔧 Sub-Components
565
-
566
- ใช้แยกกันได้ถ้าต้องการ customize เฉพาะส่วน
567
-
568
- ```tsx
569
- import {
570
- SidebarHeader, // logo + ชื่อองค์กร
571
- SidebarMenu, // เมนู dropdown
572
- SidebarUserProfile, // avatar + logout
573
- ThemeSettings, // UI เลือก theme อย่างเดียว
574
- FontSizeSettings, // UI เลือก font size อย่างเดียว
575
- } from 'gov-layout';
576
- ```
577
-
578
- ---
579
-
580
- ## 🎨 Styling
581
-
582
- - Components ใช้ **inline styles** + CSS variables จาก `gov-token-css`
583
- - ถ้าไม่ได้ import `gov-token-css` จะใช้สี **fallback** อัตโนมัติ
584
- - Dark mode ต้องเพิ่ม CSS เอง (ดู section Dark mode CSS ด้านบน)
585
- - Font size ใช้ `body.style.zoom` scale ทุกอย่างรวมถึง inline `px`
1
+ # gov-layout
2
+
3
+ Government Layout Components สำหรับเว็บแอปพลิเคชันภาครัฐ
4
+
5
+ > ใช้คู่กับ `gov-token-css` เพื่อให้สีตรงตาม Design System
6
+
7
+ ---
8
+
9
+ ## 📥 Installation
10
+
11
+ ```bash
12
+ npm install gov-layout gov-token-css
13
+ ```
14
+
15
+ ```css
16
+ /* ใน globals.css */
17
+ @import "gov-token-css";
18
+ @import "gov-layout/styles.css";
19
+ ```
20
+
21
+ > ใช้ `gov-layout/styles.css` เพื่อให้ class สีเช่น `text-text-primary` ทำงาน แม้ไม่ได้ import component จาก `gov-layout`
22
+
23
+ ---
24
+
25
+ ## 📦 Components ทั้งหมด
26
+
27
+ | Component | ใช้สำหรับ | import |
28
+ |-----------|----------|--------|
29
+ | `StaffSidebar` | Sidebar เจ้าหน้าที่ | `import { StaffSidebar } from 'gov-layout'` |
30
+ | `UserHeader` | Header ผู้ใช้ทั่วไป | `import { UserHeader } from 'gov-layout'` |
31
+ | `UserSidebar` | Sidebar ผู้ใช้ (slide-in) | `import { UserSidebar } from 'gov-layout'` |
32
+ | `SettingsPanel` | หน้าตั้งค่า (font + theme) | `import { SettingsPanel } from 'gov-layout'` |
33
+ | `SettingsProvider` | Context wrapper | `import { SettingsProvider } from 'gov-layout'` |
34
+ | `useSettings` | Hook อ่าน/เปลี่ยนค่า | `import { useSettings } from 'gov-layout'` |
35
+ | `Icons` | ไอคอน built-in 33 ตัว | `import { Icons } from 'gov-layout'` |
36
+
37
+ ---
38
+
39
+ ## 🎯 Icons (ไอคอน built-in)
40
+
41
+ import ครั้งเดียว ใช้ได้ทุกไอคอน — ไม่ต้องสร้าง SVG เอง
42
+
43
+ ### วิธีใช้งาน
44
+
45
+ ```tsx
46
+ import { Icons } from 'gov-layout';
47
+
48
+ // ใช้ตรงๆ
49
+ <Icons.Folder />
50
+ <Icons.User />
51
+ <Icons.Gear />
52
+
53
+ // ปรับขนาด (default = 20)
54
+ <Icons.Home size={24} />
55
+
56
+ // ใส่ใน MenuItem
57
+ const menuItems: MenuItem[] = [
58
+ { id: 'services', title: 'งานบริการ', icon: <Icons.Folder />, path: '/services' },
59
+ { id: 'users', title: 'จัดการผู้ใช้', icon: <Icons.User />, path: '/users' },
60
+ { id: 'reports', title: 'รายงาน', icon: <Icons.BarChart />, path: '/reports' },
61
+ ];
62
+ ```
63
+
64
+ ### ไอคอนทั้งหมด
65
+
66
+ | ชื่อ | Component | ใช้สำหรับ |
67
+ |------|-----------|----------|
68
+ | 🏠 | `Icons.Home` | หน้าแรก / Dashboard |
69
+ | 🔍 | `Icons.Search` | ค้นหา |
70
+ | 🔔 | `Icons.Bell` | แจ้งเตือน |
71
+ | 📁 | `Icons.Folder` | งานบริการ / หมวดหมู่ |
72
+ | 📋 | `Icons.Clipboard` | แบบฟอร์ม / คำร้อง |
73
+ | 📄 | `Icons.FileText` | รายงาน / เอกสาร |
74
+ | 📅 | `Icons.Calendar` | ตารางงาน / นัดหมาย |
75
+ | 👤 | `Icons.User` | ข้อมูลผู้ใช้ |
76
+ | 👥 | `Icons.Users` | จัดการสมาชิก |
77
+ | ⚙️ | `Icons.Gear` | ตั้งค่าระบบ |
78
+ | 🔧 | `Icons.Wrench` | ซ่อมบำรุง |
79
+ | 🛡️ | `Icons.Shield` | สิทธิ์การใช้งาน |
80
+ | | `Icons.HelpCircle` | ช่วยเหลือ |
81
+ | 📊 | `Icons.BarChart` | สถิติ / รายงาน |
82
+ | 🕐 | `Icons.History` | ประวัติการใช้งาน |
83
+ | 💾 | `Icons.Database` | สำรองข้อมูล |
84
+ | | `Icons.Building` | หน่วยงาน / องค์กร |
85
+ | 📍 | `Icons.MapPin` | สถานที่ |
86
+ | 📞 | `Icons.Phone` | ติดต่อ |
87
+ | ✉️ | `Icons.Mail` | อีเมล / ข้อความ |
88
+ | | `Icons.CheckCircle` | สำเร็จ / อนุมัติ |
89
+ | ⚠️ | `Icons.AlertTriangle` | คำเตือน |
90
+ | | `Icons.XCircle` | ปฏิเสธ / ข้อผิดพลาด |
91
+ | | `Icons.PlusCircle` | เพิ่มรายการ |
92
+ | 🚪 | `Icons.LogOut` | ออกจากระบบ |
93
+ | ⬇️ | `Icons.Download` | ดาวน์โหลด |
94
+ | ⬆️ | `Icons.Upload` | อัปโหลด |
95
+ | 🖨️ | `Icons.Printer` | พิมพ์เอกสาร |
96
+ | | `Icons.Star` | รายการโปรด |
97
+ | ❤️ | `Icons.Heart` | ถูกใจ |
98
+ | 👁️ | `Icons.Eye` | ดูรายละเอียด |
99
+ | ✏️ | `Icons.Edit` | แก้ไข |
100
+ | 🗑️ | `Icons.Trash` | ลบ |
101
+
102
+ ### IconProps
103
+
104
+ | Prop | Type | Default | คำอธิบาย |
105
+ |------|------|---------|----------|
106
+ | `size` | `number?` | `20` | ขนาด (width & height) |
107
+ | `className` | `string?` | - | CSS class |
108
+ | `style` | `CSSProperties?` | - | inline style |
109
+
110
+ > 💡 ถ้าต้องการ import ทีละตัวก็ได้: `import { FolderIcon, UserIcon } from 'gov-layout'`
111
+
112
+ ## 1. StaffSidebar (เจ้าหน้าที่)
113
+
114
+ Sidebar ฝั่งซ้ายแบบ fixed — รองรับพับ/กาง (collapsible)
115
+
116
+ ### ตัวอย่างใช้งาน
117
+
118
+ ```tsx
119
+ import { StaffSidebar } from 'gov-layout';
120
+ import type { MenuItem } from 'gov-layout';
121
+
122
+ // ไม่ต้องระบุ icon — sidebar จับคู่ไอคอนให้อัตโนมัติจาก id
123
+ const menuItems: MenuItem[] = [
124
+ {
125
+ id: 'services', // 📁 FolderIcon
126
+ title: 'งานบริการ',
127
+ children: [
128
+ { id: 'water', title: 'ประปา', path: '/services/water' },
129
+ { id: 'tax', title: 'ภาษี', path: '/services/tax' },
130
+ ],
131
+ dividerAfter: true,
132
+ },
133
+ { id: 'users', title: 'จัดการผู้ใช้', path: '/users' }, // → 👥 UsersIcon
134
+ { id: 'reports', title: 'รายงาน', path: '/reports' }, // → 📊 BarChartIcon
135
+ { id: 'roles', title: 'สิทธิ์การใช้งาน', path: '/roles' }, // → 🛡️ ShieldIcon
136
+ { id: 'logs', title: 'ประวัติ', path: '/logs' }, // → 🕐 HistoryIcon
137
+ { id: 'backup', title: 'สำรองข้อมูล', path: '/backup' }, // → 💾 DatabaseIcon
138
+ ];
139
+
140
+ <StaffSidebar
141
+ orgLogo="/logo.png"
142
+ orgName="เทศบาลตำบล Biza"
143
+ orgSubtitle="จังหวัดราชบุรี"
144
+ menuItems={menuItems}
145
+ user={{ firstName: 'สมชาย', lastName: 'ใจดี' }}
146
+ roleLabel="เจ้าหน้าที่"
147
+ currentPath="/services/water"
148
+ onNavigate={(path) => router.push(path)}
149
+ onLogout={() => signOut()}
150
+ onProfile={() => router.push('/profile')}
151
+ collapsible
152
+ />
153
+ ```
154
+
155
+ > **💡 Auto-Icon:** ไม่ต้อง import ไอคอน — sidebar จับคู่จาก `id` อัตโนมัติ
156
+ > ถ้าอยากกำหนดเอง ก็ส่ง `icon` prop ได้ตามปกติ: `icon: <Icons.Folder />`
157
+
158
+ > **💡 ข้อมูลองค์กร (`orgLogo`, `orgName`, `orgSubtitle`) มาจากไหนก็ได้:**
159
+ > - ดึงจาก **ตัวกลาง SSO** หลังล็อกอิน (แนะนำ) เช่น `useSSOAuth().organization`
160
+ > - ดึงจาก **API** เช่น `fetchOrgInfo()`
161
+ > - **Fix ค่า** ตรงๆ ก็ได้ ถ้าใช้ระบบเดียว
162
+ >
163
+ > Library ไม่ผูกกับแหล่งข้อมูลใดๆ — แค่รับ props แล้วแสดงผล
164
+
165
+ ### Default Bottom Menu
166
+
167
+ ไม่ต้องตั้งค่าเอง — มี **ตั้งค่าระบบ** + **ช่วยเหลือ** อยู่ด้านล่างอัตโนมัติ
168
+
169
+ ```
170
+ งานบริการ ▽
171
+ ประปา / ภาษี / ทะเบียน
172
+ ──────────────
173
+ จัดการผู้ใช้
174
+ รายงาน
175
+ ตั้งค่าระบบ ← default (ไม่ต้องส่ง)
176
+ ช่วยเหลือ ← default (ไม่ต้องส่ง)
177
+ ↕ spacer
178
+ โปรไฟล์ + ออกจากระบบ
179
+ ```
180
+
181
+ ถ้าอยากเปลี่ยน bottom menu เอง:
182
+
183
+ ```tsx
184
+ <StaffSidebar
185
+ bottomMenuItems={[
186
+ { id: 'settings', title: 'ตั้งค่า', icon: <Icons.Gear />, path: '/settings' },
187
+ ]}
188
+ ...
189
+ />
190
+ ```
191
+
192
+ ถ้าไม่อยากมี bottom menu:
193
+
194
+ ```tsx
195
+ <StaffSidebar bottomMenuItems={[]} ... />
196
+ ```
197
+
198
+ ### Props ทั้งหมด
199
+
200
+ | Prop | Type | Default | คำอธิบาย |
201
+ |------|------|---------|----------|
202
+ | `orgLogo` | `string?` | - | URL รูปตราองค์กร |
203
+ | `orgName` | `string` | **required** | ชื่อองค์กร |
204
+ | `orgSubtitle` | `string?` | - | ชื่อรอง เช่น จังหวัด |
205
+ | `menuItems` | `MenuItem[]` | **required** | เมนูหลัก |
206
+ | `bottomMenuItems` | `MenuItem[]?` | ตั้งค่าระบบ + ช่วยเหลือ | เมนูด้านล่าง |
207
+ | `user` | `User \| null` | **required** | ข้อมูลผู้ใช้ |
208
+ | `roleLabel` | `string` | **required** | ป้ายตำแหน่ง เช่น "เจ้าหน้าที่" |
209
+ | `onNavigate` | `(path) => void` | **required** | callback เมื่อคลิกเมนู |
210
+ | `onLogout` | `() => void` | **required** | callback ออกจากระบบ |
211
+ | `onProfile` | `() => void?` | - | callback เมื่อกดโปรไฟล์ผู้ใช้ |
212
+ | `currentPath` | `string?` | - | path ปัจจุบัน (highlight active) |
213
+ | `width` | `string?` | `'280px'` | ความกว้าง sidebar |
214
+ | `collapsible` | `boolean?` | `false` | เปิดโหมดพับ/กาง |
215
+ | `isOpen` | `boolean?` | - | controlled open/close |
216
+ | `onToggle` | `() => void?` | - | callback เมื่อกดพับ/กาง |
217
+
218
+ ### Features
219
+
220
+ - ✅ Dropdown submenu (พับ/กางอัตโนมัติ)
221
+ - ✅ Auto-expand เมื่อ child active
222
+ - ✅ Active item highlight
223
+ - ✅ Collapsible พับเป็น icon-only 64px, กางเป็น 280px
224
+ - ✅ Tooltip เมื่อพับ
225
+ - ✅ Default ตั้งค่าระบบ + ช่วยเหลือ (override ได้)
226
+ - ✅ โปรไฟล์ + ออกจากระบบ ล่างสุดเสมอ
227
+ - ✅ `dividerAfter` เส้นคั่นระหว่างกลุ่ม
228
+ - ✅ ใช้ Standard Avatar Placeholder กรณีที่ไม่มีรูปโปรไฟล์หรือโหลดรูปไม่สำเร็จ (v1.3.2+) 🆕
229
+
230
+ ---
231
+
232
+ ## 2. UserHeader (ผู้ใช้ทั่วไป)
233
+
234
+ Header ด้านบนพร้อม notification bell
235
+
236
+ ```tsx
237
+ import { UserHeader } from 'gov-layout';
238
+
239
+ <UserHeader
240
+ user={{
241
+ firstName: 'ชนธัญ',
242
+ pictureUrl: '/avatar.jpg',
243
+ subtitle: 'ผู้สูงอายุ', // แสดงใต้ข้อความทักทาย (optional)
244
+ }}
245
+ notifications={[
246
+ {
247
+ id: 1,
248
+ title: 'คำร้องใหม่รอตรวจสอบ',
249
+ description: 'มีคำร้องใหม่เข้ามา กรุณาตรวจสอบ',
250
+ date: '2 ชม. ที่แล้ว',
251
+ type: 'warning',
252
+ isRead: false,
253
+ category: 'action', // ← แสดงในแท็บ "ต้องดำเนินการ"
254
+ },
255
+ {
256
+ id: 2,
257
+ title: 'คำร้องได้รับการอนุมัติ',
258
+ description: 'คำร้องหมายเลข #1234 อนุมัติสำเร็จ',
259
+ date: '5 ชม. ที่แล้ว',
260
+ type: 'success',
261
+ isRead: true,
262
+ category: 'general', // ← แสดงในแท็บ "แจ้งเตือนทั่วไป"
263
+ },
264
+ ]}
265
+ onToggleSidebar={() => setOpen(true)}
266
+ onMarkAllRead={() => markAllRead()}
267
+ onViewAll={() => router.push('/notifications')}
268
+ onNotificationClick={(notif) => router.push(`/notifications/${notif.id}`)}
269
+ onProfile={() => router.push('/profile')}
270
+ />
271
+ ```
272
+
273
+ ### Props
274
+
275
+ | Prop | Type | Default | คำอธิบาย |
276
+ |------|------|---------|----------|
277
+ | `user.firstName` | `string?` | - | ชื่อ |
278
+ | `user.lastName` | `string?` | - | นามสกุล |
279
+ | `user.pictureUrl` | `string?` | - | URL รูปโปรไฟล์ |
280
+ | `user.subtitle` | `string?` | - | ข้อความใต้ชื่อ เช่น "ผู้สูงอายุ" |
281
+ | `notifications` | `NotificationItem[]?` | `[]` | รายการแจ้งเตือน |
282
+ | `onToggleSidebar` | `() => void?` | - | callback เปิด sidebar |
283
+ | `onMarkAllRead` | `() => void?` | - | callback อ่านทั้งหมดแล้ว |
284
+ | `onViewAll` | `() => void?` | - | callback ดูทั้งหมด |
285
+ | `onNotificationClick` | `(notification) => void?` | - | callback เมื่อกดแจ้งเตือนแต่ละรายการ |
286
+ | `onProfile` | `() => void?` | - | callback เมื่อกดโปรไฟล์ |
287
+ | `notificationBell` | `ReactNode?` | - | custom bell icon |
288
+ | `className` | `string?` | - | className เพิ่มเติม |
289
+
290
+ ### 🔔 Notification Filter Tabs (v1.2.25+)
291
+
292
+ Dropdown แจ้งเตือนมีแท็บกรอง 3 หมวด พร้อม badge ตัวเลข:
293
+
294
+ | แท็บ | กรองจาก `category` | ตัวอย่างใช้งาน |
295
+ |------|-------------------|----------------|
296
+ | **ทั้งหมด** | แสดงทุกรายการ | ดูภาพรวม |
297
+ | **ต้องดำเนินการ** | `'action'` | คำร้องใหม่รอตรวจสอบ, ต้องอัปโหลดเอกสาร, มีคิวใหม่ |
298
+ | **แจ้งเตือนทั่วไป** | `'general'` หรือไม่ระบุ | อนุมัติสำเร็จ, จองสำเร็จ, ยกเลิกแล้ว |
299
+
300
+ > 💡 ถ้าไม่ส่ง `category` จะถูกจัดเป็น **แจ้งเตือนทั่วไป** อัตโนมัติ (backward compatible)
301
+
302
+ ### Features
303
+
304
+ - ✅ Notification bell พร้อม badge (99+ เมื่อเกิน)
305
+ - ✅ ไม่มีแจ้งเตือน ไม่แสดง badge
306
+ - ✅ Notification dropdown แบบ scroll
307
+ - ✅ **Filter tabs**: ทั้งหมด / ต้องดำเนินการ / แจ้งเตือนทั่วไป 🆕
308
+ - ✅ **Badge ตัวเลข** แต่ละแท็บ 🆕
309
+ - ✅ **Category badge** "ต้องดำเนินการ" ติดแต่ละรายการ 🆕
310
+ - ✅ **Empty state แยกตาม filter** 🆕
311
+ - ✅ กดแจ้งเตือนแต่ละรายการ → `onNotificationClick`
312
+ - ✅ Subtitle ใต้ข้อความทักทาย (เช่น "ผู้สูงอายุ")
313
+ - ✅ ปุ่มเปิด sidebar (☰)
314
+ - ✅ กดโปรไฟล์ผู้ใช้ (avatar + ชื่อ) → `onProfile`
315
+ - ✅ รองรับ Dark Mode
316
+
317
+ ---
318
+
319
+ ## 3. UserSidebar (ผู้ใช้ทั่วไป)
320
+
321
+ Sidebar ฝั่งขวาแบบ slide-in + overlay
322
+
323
+ ```tsx
324
+ import { UserSidebar } from 'gov-layout';
325
+
326
+ <UserSidebar
327
+ isOpen={isSidebarOpen}
328
+ onClose={() => setIsSidebarOpen(false)}
329
+ user={{ firstName: 'สมหญิง', lastName: 'ใจดี', pictureUrl: '/avatar.jpg' }}
330
+ roleLabel="ผู้สูงอายุ"
331
+ menuItems={[
332
+ { id: 'profile', title: 'ข้อมูลส่วนตัว', path: '/profile' },
333
+ { id: 'services', title: 'บริการหลัก', path: '/services' },
334
+ { id: 'settings', title: 'ตั้งค่าระบบ', path: '/settings' },
335
+ ]}
336
+ onNavigate={(path) => router.push(path)}
337
+ onLogout={() => signOut()}
338
+ onProfile={() => router.push('/profile')}
339
+ />
340
+ ```
341
+
342
+ > **หมายเหตุ:** `roleLabel` แต่ละระบบส่งค่าเองได้ เช่น "ผู้สูงอายุ", "ผู้ใช้ปกติ", "อาสาสมัคร"
343
+
344
+ ---
345
+
346
+ ## 4. SettingsPanel (ตั้งค่าระบบ) 🆕
347
+
348
+ ปรับขนาดตัวอักษร (5 ระดับ) + โหมดสว่าง/มืด — ค่าจำใน localStorage
349
+
350
+ ### ขั้นตอนที่ 1: ครอบ SettingsProvider
351
+
352
+ ```tsx
353
+ // app/providers.tsx ← ต้องเป็น 'use client'
354
+ 'use client';
355
+ import { SettingsProvider } from 'gov-layout';
356
+
357
+ export default function Providers({ children }: { children: React.ReactNode }) {
358
+ return <SettingsProvider>{children}</SettingsProvider>;
359
+ }
360
+ ```
361
+
362
+ ```tsx
363
+ // app/layout.tsx
364
+ import Providers from './providers';
365
+
366
+ export default function RootLayout({ children }) {
367
+ return (
368
+ <html lang="th">
369
+ <body>
370
+ <Providers>{children}</Providers>
371
+ </body>
372
+ </html>
373
+ );
374
+ }
375
+ ```
376
+
377
+ ### ขั้นตอนที่ 2: วาง SettingsPanel
378
+
379
+ ```tsx
380
+ import { SettingsPanel } from 'gov-layout';
381
+
382
+ // ผู้ใช้ทั่วไปทั้ง font + theme
383
+ <SettingsPanel />
384
+
385
+ // เจ้าหน้าที่ → แค่ปรับขนาดฟอนต์
386
+ <SettingsPanel showTheme={false} />
387
+ ```
388
+
389
+ | Prop | Type | Default | คำอธิบาย |
390
+ |------|------|---------|----------|
391
+ | `showTheme` | `boolean?` | `true` | แสดงตัวเลือกโหมดสว่าง/มืด |
392
+ | `className` | `string?` | - | className เพิ่มเติม |
393
+
394
+ ### ขั้นตอนที่ 3: ใช้ useSettings() hook (ถ้าต้องการ)
395
+
396
+ ```tsx
397
+ import { useSettings } from 'gov-layout';
398
+
399
+ function MyComponent() {
400
+ const { theme, toggleTheme, fontSize, setFontSize, fontSizeOption } = useSettings();
401
+
402
+ return (
403
+ <div>
404
+ <p>ธีม: {theme}</p>
405
+ <p>ฟอนต์: {fontSize} (×{fontSizeOption.scale})</p>
406
+ <button onClick={toggleTheme}>สลับธีม</button>
407
+ <button onClick={() => setFontSize('large')}>ฟอนต์ใหญ่</button>
408
+ </div>
409
+ );
410
+ }
411
+ ```
412
+
413
+ ### ขนาดตัวอักษร (5 ระดับ)
414
+
415
+ | ค่า | Label | Scale | ผลลัพธ์ |
416
+ |-----|-------|-------|---------|
417
+ | `xsmall` | เล็กมาก | ×0.8 | ย่อทุกอย่าง 80% |
418
+ | `small` | เล็ก | ×0.9 | ย่อเล็กน้อย |
419
+ | `medium` | กลาง | ×1.0 | ค่าเริ่มต้น |
420
+ | `large` | ใหญ่ | ×1.2 | ขยาย 120% |
421
+ | `xlarge` | ใหญ่มาก | ×1.4 | ขยาย 140% |
422
+
423
+ ### หลักการทำงาน
424
+
425
+ - **Theme** — เพิ่ม/ลบ class `dark` บน `<html>` ➜ ใช้ CSS `html.dark` จัดสี
426
+ - **Font size** — ปรับ `body.style.zoom` + CSS variables ตาม scale
427
+ - **Persistence** — เก็บใน localStorage (`app-theme`, `app-font-size`)
428
+
429
+ ### Dark mode CSS ที่ต้องเพิ่ม
430
+
431
+ ```css
432
+ /* globals.css เพิ่มสำหรับ dark mode */
433
+ html.dark body { background-color: #0f172a; color: #f1f5f9; }
434
+ html.dark aside { background-color: #1e293b !important; }
435
+ html.dark header { background-color: #1e293b !important; }
436
+ html.dark h1,
437
+ html.dark h2,
438
+ html.dark h3 { color: #f1f5f9 !important; }
439
+ html.dark p { color: #94a3b8 !important; }
440
+ ```
441
+
442
+ ---
443
+
444
+ ## 📐 Types
445
+
446
+ ```ts
447
+ // เมนู
448
+ interface MenuItem {
449
+ id: string;
450
+ title: string;
451
+ path?: string; // path สำหรับ navigate
452
+ icon?: React.ReactNode; // icon component
453
+ children?: MenuItem[]; // submenu → แสดงเป็น dropdown
454
+ dividerAfter?: boolean; // เส้นคั่นด้านล่าง
455
+ }
456
+
457
+ // ผู้ใช้
458
+ interface User {
459
+ id?: string | number;
460
+ firstName?: string;
461
+ lastName?: string;
462
+ pictureUrl?: string;
463
+ role?: string;
464
+ }
465
+
466
+ // การแจ้งเตือน
467
+ type NotificationCategory = 'action' | 'general';
468
+
469
+ interface NotificationItem {
470
+ id: string | number;
471
+ title: string;
472
+ description: string;
473
+ date: string;
474
+ type: 'info' | 'success' | 'warning' | 'error' | 'reminder';
475
+ isRead: boolean;
476
+ category?: NotificationCategory; // 🆕 หมวดหมู่: 'action' | 'general'
477
+ }
478
+
479
+ // ตั้งค่า
480
+ type Theme = 'light' | 'dark';
481
+ type FontSizeKey = 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge';
482
+ ```
483
+
484
+ ---
485
+
486
+ ## 📁 Layout Examples
487
+
488
+ ### Staff Layout (เจ้าหน้าที่)
489
+
490
+ ```tsx
491
+ 'use client';
492
+ import { StaffSidebar, SettingsPanel } from 'gov-layout';
493
+
494
+ export default function AdminLayout({ children }) {
495
+ const [currentPath, setCurrentPath] = useState('/');
496
+
497
+ return (
498
+ <div style={{ display: 'flex' }}>
499
+ <StaffSidebar
500
+ orgLogo="/logo.png"
501
+ orgName="เทศบาลตำบล Biza"
502
+ orgSubtitle="จังหวัดราชบุรี"
503
+ menuItems={menuItems}
504
+ user={user}
505
+ roleLabel="เจ้าหน้าที่"
506
+ currentPath={currentPath}
507
+ onNavigate={(path) => setCurrentPath(path)}
508
+ onLogout={() => signOut()}
509
+ onProfile={() => setCurrentPath('/profile')}
510
+ collapsible
511
+ />
512
+ <main style={{ marginLeft: 280, flex: 1, padding: 32 }}>
513
+ {currentPath === '/settings' ? (
514
+ <SettingsPanel showTheme={false} />
515
+ ) : (
516
+ children
517
+ )}
518
+ </main>
519
+ </div>
520
+ );
521
+ }
522
+ ```
523
+
524
+ ### User Layout (ผู้ใช้ทั่วไป)
525
+
526
+ ```tsx
527
+ 'use client';
528
+ import { UserHeader, UserSidebar, SettingsPanel } from 'gov-layout';
529
+
530
+ export default function UserLayout({ children }) {
531
+ const [open, setOpen] = useState(false);
532
+ const [currentPath, setCurrentPath] = useState('/');
533
+
534
+ return (
535
+ <>
536
+ <UserHeader
537
+ user={{ ...user, subtitle: 'ผู้สูงอายุ' }}
538
+ notifications={notifications}
539
+ onToggleSidebar={() => setOpen(true)}
540
+ onNotificationClick={(notif) => setCurrentPath(`/notifications/${notif.id}`)}
541
+ onProfile={() => setCurrentPath('/profile')}
542
+ />
543
+ <UserSidebar
544
+ isOpen={open}
545
+ onClose={() => setOpen(false)}
546
+ user={user}
547
+ roleLabel="ผู้ใช้ทั่วไป"
548
+ menuItems={menuItems}
549
+ onNavigate={(path) => setCurrentPath(path)}
550
+ onLogout={() => signOut()}
551
+ onProfile={() => setCurrentPath('/profile')}
552
+ />
553
+ <main style={{ padding: 32 }}>
554
+ {currentPath === '/settings' ? (
555
+ <SettingsPanel showTheme={true} />
556
+ ) : (
557
+ children
558
+ )}
559
+ </main>
560
+ </>
561
+ );
562
+ }
563
+ ```
564
+
565
+ ---
566
+
567
+ ## 🔧 Sub-Components
568
+
569
+ ใช้แยกกันได้ถ้าต้องการ customize เฉพาะส่วน
570
+
571
+ ```tsx
572
+ import {
573
+ SidebarHeader, // logo + ชื่อองค์กร
574
+ SidebarMenu, // เมนู dropdown
575
+ SidebarUserProfile, // avatar + logout
576
+ ThemeSettings, // UI เลือก theme อย่างเดียว
577
+ FontSizeSettings, // UI เลือก font size อย่างเดียว
578
+ } from 'gov-layout';
579
+ ```
580
+
581
+ ---
582
+
583
+ ## 🎨 Styling
584
+
585
+ - Components ใช้ **inline styles** + CSS variables จาก `gov-token-css`
586
+ - ถ้าไม่ได้ import `gov-token-css` จะใช้สี **fallback** อัตโนมัติ
587
+ - Dark mode ต้องเพิ่ม CSS เอง (ดู section Dark mode CSS ด้านบน)
588
+ - Font size ใช้ `body.style.zoom` → scale ทุกอย่างรวมถึง inline `px`