gov-layout 1.2.24 → 1.2.26

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
@@ -241,11 +241,21 @@ import { UserHeader } from 'gov-layout';
241
241
  notifications={[
242
242
  {
243
243
  id: 1,
244
- title: 'คำร้องได้รับการอนุมัติ',
245
- description: 'คำร้องหมายเลข #1234',
244
+ title: 'คำร้องใหม่รอตรวจสอบ',
245
+ description: 'มีคำร้องใหม่เข้ามา กรุณาตรวจสอบ',
246
246
  date: '2 ชม. ที่แล้ว',
247
- type: 'success',
247
+ type: 'warning',
248
248
  isRead: false,
249
+ category: 'action', // ← แสดงในแท็บ "ต้องดำเนินการ"
250
+ },
251
+ {
252
+ id: 2,
253
+ title: 'คำร้องได้รับการอนุมัติ',
254
+ description: 'คำร้องหมายเลข #1234 อนุมัติสำเร็จ',
255
+ date: '5 ชม. ที่แล้ว',
256
+ type: 'success',
257
+ isRead: true,
258
+ category: 'general', // ← แสดงในแท็บ "แจ้งเตือนทั่วไป"
249
259
  },
250
260
  ]}
251
261
  onToggleSidebar={() => setOpen(true)}
@@ -273,15 +283,32 @@ import { UserHeader } from 'gov-layout';
273
283
  | `notificationBell` | `ReactNode?` | - | custom bell icon |
274
284
  | `className` | `string?` | - | className เพิ่มเติม |
275
285
 
286
+ ### 🔔 Notification Filter Tabs (v1.2.25+)
287
+
288
+ Dropdown แจ้งเตือนมีแท็บกรอง 3 หมวด พร้อม badge ตัวเลข:
289
+
290
+ | แท็บ | กรองจาก `category` | ตัวอย่างใช้งาน |
291
+ |------|-------------------|----------------|
292
+ | **ทั้งหมด** | แสดงทุกรายการ | ดูภาพรวม |
293
+ | **ต้องดำเนินการ** | `'action'` | คำร้องใหม่รอตรวจสอบ, ต้องอัปโหลดเอกสาร, มีคิวใหม่ |
294
+ | **แจ้งเตือนทั่วไป** | `'general'` หรือไม่ระบุ | อนุมัติสำเร็จ, จองสำเร็จ, ยกเลิกแล้ว |
295
+
296
+ > 💡 ถ้าไม่ส่ง `category` จะถูกจัดเป็น **แจ้งเตือนทั่วไป** อัตโนมัติ (backward compatible)
297
+
276
298
  ### Features
277
299
 
278
300
  - ✅ Notification bell พร้อม badge (99+ เมื่อเกิน)
279
301
  - ✅ ไม่มีแจ้งเตือน → ไม่แสดง badge
280
302
  - ✅ Notification dropdown แบบ scroll
303
+ - ✅ **Filter tabs**: ทั้งหมด / ต้องดำเนินการ / แจ้งเตือนทั่วไป 🆕
304
+ - ✅ **Badge ตัวเลข** แต่ละแท็บ 🆕
305
+ - ✅ **Category badge** "ต้องดำเนินการ" ติดแต่ละรายการ 🆕
306
+ - ✅ **Empty state แยกตาม filter** 🆕
281
307
  - ✅ กดแจ้งเตือนแต่ละรายการ → `onNotificationClick`
282
308
  - ✅ Subtitle ใต้ข้อความทักทาย (เช่น "ผู้สูงอายุ")
283
309
  - ✅ ปุ่มเปิด sidebar (☰)
284
310
  - ✅ กดโปรไฟล์ผู้ใช้ (avatar + ชื่อ) → `onProfile`
311
+ - ✅ รองรับ Dark Mode
285
312
 
286
313
  ---
287
314
 
@@ -433,6 +460,8 @@ interface User {
433
460
  }
434
461
 
435
462
  // การแจ้งเตือน
463
+ type NotificationCategory = 'action' | 'general';
464
+
436
465
  interface NotificationItem {
437
466
  id: string | number;
438
467
  title: string;
@@ -440,6 +469,7 @@ interface NotificationItem {
440
469
  date: string;
441
470
  type: 'info' | 'success' | 'warning' | 'error' | 'reminder';
442
471
  isRead: boolean;
472
+ category?: NotificationCategory; // 🆕 หมวดหมู่: 'action' | 'general'
443
473
  }
444
474
 
445
475
  // ตั้งค่า
package/dist/index.d.mts CHANGED
@@ -82,6 +82,7 @@ interface SidebarUserProfileProps {
82
82
  }
83
83
  declare function SidebarUserProfile({ user, roleLabel, onLogout, collapsed, onProfile, }: SidebarUserProfileProps): react_jsx_runtime.JSX.Element | null;
84
84
 
85
+ type NotificationCategory = 'action' | 'general';
85
86
  interface NotificationItem {
86
87
  id: string | number;
87
88
  title: string;
@@ -89,6 +90,8 @@ interface NotificationItem {
89
90
  date: string;
90
91
  type: 'info' | 'success' | 'warning' | 'error' | 'reminder';
91
92
  isRead: boolean;
93
+ /** หมวดหมู่การแจ้งเตือน: 'action' = ต้องดำเนินการ, 'general' = แจ้งเตือนทั่วไป */
94
+ category?: NotificationCategory;
92
95
  }
93
96
  interface UserHeaderProps {
94
97
  user?: {
@@ -262,4 +265,4 @@ declare const Icons: {
262
265
  readonly Trash: typeof TrashIcon;
263
266
  };
264
267
 
265
- export { AlertTriangleIcon, BarChartIcon, BellIcon, BottomBar, type BottomBarProps, BuildingIcon, CalendarIcon, CheckCircleIcon, ClipboardIcon, DatabaseIcon, DownloadIcon, EditIcon, EyeIcon, FONT_SIZE_OPTIONS, FileTextIcon, FolderIcon, type FontSizeKey, type FontSizeOption, FontSizeSettings, GearIcon, HeartIcon, HelpCircleIcon, HistoryIcon, HomeIcon, type IconProps, Icons, LogOutIcon, MailIcon, MapPinIcon, type MenuItem, type NotificationItem, PhoneIcon, PlusCircleIcon, PrinterIcon, SearchIcon, SettingsPanel, SettingsProvider, ShieldIcon, SidebarHeader, SidebarMenu, SidebarUserProfile, StaffSidebar, type StaffSidebarProps, StarIcon, type Theme, ThemeSettings, TrashIcon, UploadIcon, type User, UserHeader, UserIcon, UserSidebar, UsersIcon, WrenchIcon, XCircleIcon, useDarkMode, useSettings };
268
+ export { AlertTriangleIcon, BarChartIcon, BellIcon, BottomBar, type BottomBarProps, BuildingIcon, CalendarIcon, CheckCircleIcon, ClipboardIcon, DatabaseIcon, DownloadIcon, EditIcon, EyeIcon, FONT_SIZE_OPTIONS, FileTextIcon, FolderIcon, type FontSizeKey, type FontSizeOption, FontSizeSettings, GearIcon, HeartIcon, HelpCircleIcon, HistoryIcon, HomeIcon, type IconProps, Icons, LogOutIcon, MailIcon, MapPinIcon, type MenuItem, type NotificationCategory, type NotificationItem, PhoneIcon, PlusCircleIcon, PrinterIcon, SearchIcon, SettingsPanel, SettingsProvider, ShieldIcon, SidebarHeader, SidebarMenu, SidebarUserProfile, StaffSidebar, type StaffSidebarProps, StarIcon, type Theme, ThemeSettings, TrashIcon, UploadIcon, type User, UserHeader, UserIcon, UserSidebar, UsersIcon, WrenchIcon, XCircleIcon, useDarkMode, useSettings };
package/dist/index.d.ts CHANGED
@@ -82,6 +82,7 @@ interface SidebarUserProfileProps {
82
82
  }
83
83
  declare function SidebarUserProfile({ user, roleLabel, onLogout, collapsed, onProfile, }: SidebarUserProfileProps): react_jsx_runtime.JSX.Element | null;
84
84
 
85
+ type NotificationCategory = 'action' | 'general';
85
86
  interface NotificationItem {
86
87
  id: string | number;
87
88
  title: string;
@@ -89,6 +90,8 @@ interface NotificationItem {
89
90
  date: string;
90
91
  type: 'info' | 'success' | 'warning' | 'error' | 'reminder';
91
92
  isRead: boolean;
93
+ /** หมวดหมู่การแจ้งเตือน: 'action' = ต้องดำเนินการ, 'general' = แจ้งเตือนทั่วไป */
94
+ category?: NotificationCategory;
92
95
  }
93
96
  interface UserHeaderProps {
94
97
  user?: {
@@ -262,4 +265,4 @@ declare const Icons: {
262
265
  readonly Trash: typeof TrashIcon;
263
266
  };
264
267
 
265
- export { AlertTriangleIcon, BarChartIcon, BellIcon, BottomBar, type BottomBarProps, BuildingIcon, CalendarIcon, CheckCircleIcon, ClipboardIcon, DatabaseIcon, DownloadIcon, EditIcon, EyeIcon, FONT_SIZE_OPTIONS, FileTextIcon, FolderIcon, type FontSizeKey, type FontSizeOption, FontSizeSettings, GearIcon, HeartIcon, HelpCircleIcon, HistoryIcon, HomeIcon, type IconProps, Icons, LogOutIcon, MailIcon, MapPinIcon, type MenuItem, type NotificationItem, PhoneIcon, PlusCircleIcon, PrinterIcon, SearchIcon, SettingsPanel, SettingsProvider, ShieldIcon, SidebarHeader, SidebarMenu, SidebarUserProfile, StaffSidebar, type StaffSidebarProps, StarIcon, type Theme, ThemeSettings, TrashIcon, UploadIcon, type User, UserHeader, UserIcon, UserSidebar, UsersIcon, WrenchIcon, XCircleIcon, useDarkMode, useSettings };
268
+ export { AlertTriangleIcon, BarChartIcon, BellIcon, BottomBar, type BottomBarProps, BuildingIcon, CalendarIcon, CheckCircleIcon, ClipboardIcon, DatabaseIcon, DownloadIcon, EditIcon, EyeIcon, FONT_SIZE_OPTIONS, FileTextIcon, FolderIcon, type FontSizeKey, type FontSizeOption, FontSizeSettings, GearIcon, HeartIcon, HelpCircleIcon, HistoryIcon, HomeIcon, type IconProps, Icons, LogOutIcon, MailIcon, MapPinIcon, type MenuItem, type NotificationCategory, type NotificationItem, PhoneIcon, PlusCircleIcon, PrinterIcon, SearchIcon, SettingsPanel, SettingsProvider, ShieldIcon, SidebarHeader, SidebarMenu, SidebarUserProfile, StaffSidebar, type StaffSidebarProps, StarIcon, type Theme, ThemeSettings, TrashIcon, UploadIcon, type User, UserHeader, UserIcon, UserSidebar, UsersIcon, WrenchIcon, XCircleIcon, useDarkMode, useSettings };
package/dist/index.js CHANGED
@@ -1130,6 +1130,7 @@ function UserHeader({
1130
1130
  const displayName = `${user?.firstName || ""} ${user?.lastName || ""}`.trim() || "\u0E1C\u0E39\u0E49\u0E43\u0E0A\u0E49";
1131
1131
  const firstChar = user?.firstName?.charAt(0) || "\u0E1C";
1132
1132
  const [isNotifOpen, setIsNotifOpen] = react.useState(false);
1133
+ const [activeFilter, setActiveFilter] = react.useState("all");
1133
1134
  const notifRef = react.useRef(null);
1134
1135
  const isDark = useDarkMode();
1135
1136
  react.useEffect(() => {
@@ -1142,6 +1143,13 @@ function UserHeader({
1142
1143
  return () => document.removeEventListener("mousedown", handleClickOutside);
1143
1144
  }, []);
1144
1145
  const unreadCount = notifications.filter((n) => !n.isRead).length;
1146
+ const actionCount = notifications.filter((n) => n.category === "action").length;
1147
+ const generalCount = notifications.filter((n) => !n.category || n.category === "general").length;
1148
+ const filteredNotifications = notifications.filter((n) => {
1149
+ if (activeFilter === "all") return true;
1150
+ if (activeFilter === "action") return n.category === "action";
1151
+ return !n.category || n.category === "general";
1152
+ });
1145
1153
  return /* @__PURE__ */ jsxRuntime.jsx(
1146
1154
  "header",
1147
1155
  {
@@ -1340,34 +1348,95 @@ function UserHeader({
1340
1348
  "div",
1341
1349
  {
1342
1350
  style: {
1343
- padding: "16px",
1344
- borderBottom: `1px solid ${isDark ? "#374151" : "#f3f4f6"}`,
1345
- display: "flex",
1346
- alignItems: "center",
1347
- justifyContent: "space-between",
1351
+ padding: "16px 16px 0",
1348
1352
  background: isDark ? "#1f2937" : "#fafafa"
1349
1353
  },
1350
1354
  children: [
1351
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, fontSize: "14px", color: isDark ? "#ffffff" : "#111" }, children: "\u0E01\u0E32\u0E23\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19" }),
1352
- onMarkAllRead && /* @__PURE__ */ jsxRuntime.jsx(
1353
- "button",
1355
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: "12px" }, children: [
1356
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { fontWeight: 600, fontSize: "14px", color: isDark ? "#ffffff" : "#111" }, children: "\u0E01\u0E32\u0E23\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19" }),
1357
+ onMarkAllRead && /* @__PURE__ */ jsxRuntime.jsx(
1358
+ "button",
1359
+ {
1360
+ onClick: onMarkAllRead,
1361
+ style: {
1362
+ fontSize: "12px",
1363
+ color: "var(--color-alias-color-brand-primary, #1e7d55)",
1364
+ background: "none",
1365
+ border: "none",
1366
+ cursor: "pointer",
1367
+ fontWeight: 500
1368
+ },
1369
+ children: "\u0E2D\u0E48\u0E32\u0E19\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14"
1370
+ }
1371
+ )
1372
+ ] }),
1373
+ /* @__PURE__ */ jsxRuntime.jsx(
1374
+ "div",
1354
1375
  {
1355
- onClick: onMarkAllRead,
1356
1376
  style: {
1357
- fontSize: "12px",
1358
- color: "var(--color-alias-color-brand-primary, #1e7d55)",
1359
- background: "none",
1360
- border: "none",
1361
- cursor: "pointer",
1362
- fontWeight: 500
1377
+ display: "flex",
1378
+ gap: "6px",
1379
+ paddingBottom: "12px",
1380
+ borderBottom: `1px solid ${isDark ? "#374151" : "#e5e7eb"}`
1363
1381
  },
1364
- children: "\u0E2D\u0E48\u0E32\u0E19\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14"
1382
+ children: [
1383
+ { key: "all", label: "\u0E17\u0E31\u0E49\u0E07\u0E2B\u0E21\u0E14", count: notifications.length },
1384
+ { key: "action", label: "\u0E15\u0E49\u0E2D\u0E07\u0E14\u0E33\u0E40\u0E19\u0E34\u0E19\u0E01\u0E32\u0E23", count: actionCount },
1385
+ { key: "general", label: "\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19\u0E17\u0E31\u0E48\u0E27\u0E44\u0E1B", count: generalCount }
1386
+ ].map((tab) => {
1387
+ const isActive = activeFilter === tab.key;
1388
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1389
+ "button",
1390
+ {
1391
+ onClick: () => setActiveFilter(tab.key),
1392
+ style: {
1393
+ display: "inline-flex",
1394
+ alignItems: "center",
1395
+ gap: "5px",
1396
+ padding: "5px 10px",
1397
+ fontSize: "12px",
1398
+ fontWeight: isActive ? 600 : 400,
1399
+ color: isActive ? isDark ? "#ffffff" : "#1e7d55" : isDark ? "#9ca3af" : "#6b7280",
1400
+ background: isActive ? isDark ? "rgba(30,125,85,0.2)" : "rgba(30,125,85,0.08)" : "transparent",
1401
+ border: `1px solid ${isActive ? isDark ? "rgba(30,125,85,0.4)" : "rgba(30,125,85,0.25)" : isDark ? "#4b5563" : "#e5e7eb"}`,
1402
+ borderRadius: "16px",
1403
+ cursor: "pointer",
1404
+ transition: "all 0.15s ease",
1405
+ whiteSpace: "nowrap"
1406
+ },
1407
+ children: [
1408
+ tab.label,
1409
+ /* @__PURE__ */ jsxRuntime.jsx(
1410
+ "span",
1411
+ {
1412
+ style: {
1413
+ minWidth: "18px",
1414
+ height: "18px",
1415
+ display: "inline-flex",
1416
+ alignItems: "center",
1417
+ justifyContent: "center",
1418
+ borderRadius: "9px",
1419
+ fontSize: "10px",
1420
+ fontWeight: 700,
1421
+ lineHeight: 1,
1422
+ padding: "0 4px",
1423
+ background: isActive ? isDark ? "#1e7d55" : "#1e7d55" : isDark ? "#4b5563" : "#e5e7eb",
1424
+ color: isActive ? "#ffffff" : isDark ? "#d1d5db" : "#6b7280"
1425
+ },
1426
+ children: tab.count
1427
+ }
1428
+ )
1429
+ ]
1430
+ },
1431
+ tab.key
1432
+ );
1433
+ })
1365
1434
  }
1366
1435
  )
1367
1436
  ]
1368
1437
  }
1369
1438
  ),
1370
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxHeight: "60vh", overflowY: "auto" }, children: notifications.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1439
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { maxHeight: "60vh", overflowY: "auto" }, children: filteredNotifications.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(
1371
1440
  "div",
1372
1441
  {
1373
1442
  style: {
@@ -1376,21 +1445,22 @@ function UserHeader({
1376
1445
  color: "#9ca3af",
1377
1446
  fontSize: "14px"
1378
1447
  },
1379
- children: "\u{1F514} \u0E44\u0E21\u0E48\u0E21\u0E35\u0E01\u0E32\u0E23\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19\u0E43\u0E2B\u0E21\u0E48"
1448
+ children: activeFilter === "all" ? "\u{1F514} \u0E44\u0E21\u0E48\u0E21\u0E35\u0E01\u0E32\u0E23\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19\u0E43\u0E2B\u0E21\u0E48" : activeFilter === "action" ? "\u2705 \u0E44\u0E21\u0E48\u0E21\u0E35\u0E23\u0E32\u0E22\u0E01\u0E32\u0E23\u0E17\u0E35\u0E48\u0E15\u0E49\u0E2D\u0E07\u0E14\u0E33\u0E40\u0E19\u0E34\u0E19\u0E01\u0E32\u0E23" : "\u{1F4CB} \u0E44\u0E21\u0E48\u0E21\u0E35\u0E01\u0E32\u0E23\u0E41\u0E08\u0E49\u0E07\u0E40\u0E15\u0E37\u0E2D\u0E19\u0E17\u0E31\u0E48\u0E27\u0E44\u0E1B"
1380
1449
  }
1381
- ) : notifications.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
1450
+ ) : filteredNotifications.map((item) => /* @__PURE__ */ jsxRuntime.jsxs(
1382
1451
  "div",
1383
1452
  {
1384
1453
  onClick: () => onNotificationClick?.(item),
1385
1454
  style: {
1386
1455
  padding: "12px 16px",
1387
- borderBottom: "1px solid #f9fafb",
1456
+ borderBottom: `1px solid ${isDark ? "#2d3748" : "#f3f4f6"}`,
1388
1457
  cursor: "pointer",
1389
1458
  background: !item.isRead ? isDark ? "rgba(59,130,246,0.15)" : "rgba(219,234,254,0.3)" : "transparent",
1390
1459
  position: "relative",
1391
1460
  display: "flex",
1392
1461
  gap: "12px",
1393
- alignItems: "flex-start"
1462
+ alignItems: "flex-start",
1463
+ transition: "background-color 0.15s ease"
1394
1464
  },
1395
1465
  onMouseEnter: (e) => {
1396
1466
  e.currentTarget.style.backgroundColor = isDark ? "#1f2937" : "#f9fafb";
@@ -1432,6 +1502,22 @@ function UserHeader({
1432
1502
  }
1433
1503
  ),
1434
1504
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
1505
+ item.category === "action" && /* @__PURE__ */ jsxRuntime.jsx(
1506
+ "span",
1507
+ {
1508
+ style: {
1509
+ display: "inline-block",
1510
+ fontSize: "10px",
1511
+ fontWeight: 600,
1512
+ color: "#b45309",
1513
+ background: "#fef3c7",
1514
+ padding: "1px 6px",
1515
+ borderRadius: "4px",
1516
+ marginBottom: "4px"
1517
+ },
1518
+ children: "\u0E15\u0E49\u0E2D\u0E07\u0E14\u0E33\u0E40\u0E19\u0E34\u0E19\u0E01\u0E32\u0E23"
1519
+ }
1520
+ ),
1435
1521
  /* @__PURE__ */ jsxRuntime.jsx(
1436
1522
  "p",
1437
1523
  {