@suphark/ui 0.1.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.
Files changed (147) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/components.json +21 -0
  5. package/package.json +108 -0
  6. package/scripts/audit-base-ui-props.mjs +179 -0
  7. package/scripts/audit-tailwind-typography.mjs +100 -0
  8. package/src/components/design-system/design-system-showcase.tsx +138 -0
  9. package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
  10. package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
  11. package/src/components/design-system/sections/showcase-forms.tsx +373 -0
  12. package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
  13. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
  14. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
  15. package/src/components/design-system/sections/showcase-overview.tsx +368 -0
  16. package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
  17. package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
  18. package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
  19. package/src/components/shared/button/add-new-button.tsx +19 -0
  20. package/src/components/shared/button/base/action-button.tsx +134 -0
  21. package/src/components/shared/button/base/link-button.tsx +130 -0
  22. package/src/components/shared/button/cancel-button.tsx +27 -0
  23. package/src/components/shared/button/confirm-button.tsx +26 -0
  24. package/src/components/shared/button/delete-button.tsx +36 -0
  25. package/src/components/shared/button/floating-save-button.tsx +89 -0
  26. package/src/components/shared/button/index.ts +13 -0
  27. package/src/components/shared/button/print-button.tsx +67 -0
  28. package/src/components/shared/button/refresh-button.tsx +63 -0
  29. package/src/components/shared/button/save-button.tsx +29 -0
  30. package/src/components/shared/button/submit-button.tsx +58 -0
  31. package/src/components/shared/client-redirect.tsx +11 -0
  32. package/src/components/shared/combobox.tsx +116 -0
  33. package/src/components/shared/data-list.tsx +65 -0
  34. package/src/components/shared/date/date-range-picker.tsx +106 -0
  35. package/src/components/shared/description-list.tsx +27 -0
  36. package/src/components/shared/empty-row.tsx +13 -0
  37. package/src/components/shared/empty-state.tsx +31 -0
  38. package/src/components/shared/env-snippet.tsx +62 -0
  39. package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
  40. package/src/components/shared/form/form-field-boolean.tsx +93 -0
  41. package/src/components/shared/form/form-field-color-select.tsx +86 -0
  42. package/src/components/shared/form/form-field-combobox.tsx +167 -0
  43. package/src/components/shared/form/form-field-date.tsx +55 -0
  44. package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
  45. package/src/components/shared/form/form-field-input.tsx +372 -0
  46. package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
  47. package/src/components/shared/form/form-field-radio-group.tsx +56 -0
  48. package/src/components/shared/form/form-field-select.tsx +312 -0
  49. package/src/components/shared/form/simple-select.tsx +152 -0
  50. package/src/components/shared/form-dialog.tsx +109 -0
  51. package/src/components/shared/form-message.tsx +30 -0
  52. package/src/components/shared/initials.tsx +33 -0
  53. package/src/components/shared/layout/error-state.tsx +160 -0
  54. package/src/components/shared/layout/page-shell.tsx +57 -0
  55. package/src/components/shared/link/transition-link.tsx +72 -0
  56. package/src/components/shared/multi-select-combobox.tsx +392 -0
  57. package/src/components/shared/name-id-fields.tsx +133 -0
  58. package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
  59. package/src/components/shared/notice.tsx +35 -0
  60. package/src/components/shared/number/formatted-number.tsx +133 -0
  61. package/src/components/shared/page-header.tsx +25 -0
  62. package/src/components/shared/page-skeleton.tsx +18 -0
  63. package/src/components/shared/row-actions.tsx +93 -0
  64. package/src/components/shared/row-menu.tsx +55 -0
  65. package/src/components/shared/secret-reveal.tsx +53 -0
  66. package/src/components/shared/section-card.tsx +44 -0
  67. package/src/components/shared/stat.tsx +26 -0
  68. package/src/components/shared/status-badge.tsx +47 -0
  69. package/src/components/shared/text/primary-secondary-text.tsx +186 -0
  70. package/src/components/shared/theme/index.ts +4 -0
  71. package/src/components/shared/theme/theme-menu-items.tsx +43 -0
  72. package/src/components/shared/theme/theme-provider.tsx +39 -0
  73. package/src/components/shared/theme/theme-switch.tsx +36 -0
  74. package/src/components/shared/theme/theme-toggle.tsx +76 -0
  75. package/src/components/shared/time/client-date-formatter.tsx +188 -0
  76. package/src/components/shared/ui-text.tsx +47 -0
  77. package/src/components/ui/__tests__/combobox.test.tsx +40 -0
  78. package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
  79. package/src/components/ui/__tests__/slider.test.tsx +18 -0
  80. package/src/components/ui/__tests__/tabs.test.tsx +33 -0
  81. package/src/components/ui/accordion.tsx +69 -0
  82. package/src/components/ui/alert-dialog.tsx +161 -0
  83. package/src/components/ui/alert.tsx +68 -0
  84. package/src/components/ui/aspect-ratio.tsx +22 -0
  85. package/src/components/ui/attachment.tsx +194 -0
  86. package/src/components/ui/avatar.tsx +92 -0
  87. package/src/components/ui/badge.tsx +48 -0
  88. package/src/components/ui/breadcrumb.tsx +102 -0
  89. package/src/components/ui/button-group.tsx +77 -0
  90. package/src/components/ui/button.tsx +57 -0
  91. package/src/components/ui/calendar.tsx +184 -0
  92. package/src/components/ui/card.tsx +87 -0
  93. package/src/components/ui/carousel.tsx +230 -0
  94. package/src/components/ui/chart.tsx +338 -0
  95. package/src/components/ui/checkbox.tsx +27 -0
  96. package/src/components/ui/collapsible.tsx +17 -0
  97. package/src/components/ui/combobox.tsx +272 -0
  98. package/src/components/ui/command.tsx +180 -0
  99. package/src/components/ui/context-menu.tsx +244 -0
  100. package/src/components/ui/date-picker.tsx +308 -0
  101. package/src/components/ui/dialog.tsx +137 -0
  102. package/src/components/ui/drawer.tsx +208 -0
  103. package/src/components/ui/dropdown-menu.tsx +257 -0
  104. package/src/components/ui/empty.tsx +93 -0
  105. package/src/components/ui/field.tsx +223 -0
  106. package/src/components/ui/form.tsx +161 -0
  107. package/src/components/ui/hover-card.tsx +46 -0
  108. package/src/components/ui/input-group.tsx +145 -0
  109. package/src/components/ui/input-otp.tsx +85 -0
  110. package/src/components/ui/input.tsx +19 -0
  111. package/src/components/ui/item.tsx +187 -0
  112. package/src/components/ui/kbd.tsx +26 -0
  113. package/src/components/ui/label.tsx +19 -0
  114. package/src/components/ui/menubar.tsx +266 -0
  115. package/src/components/ui/navigation-menu.tsx +160 -0
  116. package/src/components/ui/pagination.tsx +117 -0
  117. package/src/components/ui/popover.tsx +76 -0
  118. package/src/components/ui/progress.tsx +66 -0
  119. package/src/components/ui/radio-group.tsx +38 -0
  120. package/src/components/ui/relative-date.tsx +40 -0
  121. package/src/components/ui/resizable.tsx +41 -0
  122. package/src/components/ui/scroll-area.tsx +67 -0
  123. package/src/components/ui/searchable-select.tsx +124 -0
  124. package/src/components/ui/select.tsx +189 -0
  125. package/src/components/ui/separator.tsx +21 -0
  126. package/src/components/ui/sheet.tsx +124 -0
  127. package/src/components/ui/sidebar.tsx +691 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +55 -0
  130. package/src/components/ui/sonner.tsx +45 -0
  131. package/src/components/ui/spinner.tsx +16 -0
  132. package/src/components/ui/switch.tsx +32 -0
  133. package/src/components/ui/table.tsx +95 -0
  134. package/src/components/ui/tabs.tsx +79 -0
  135. package/src/components/ui/textarea.tsx +17 -0
  136. package/src/components/ui/toggle-group.tsx +86 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +54 -0
  139. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  140. package/src/hooks/use-mobile.ts +21 -0
  141. package/src/index.ts +156 -0
  142. package/src/lib/formatters.ts +44 -0
  143. package/src/lib/permission.ts +75 -0
  144. package/src/lib/status-colors.ts +52 -0
  145. package/src/lib/utils.ts +6 -0
  146. package/src/next.ts +15 -0
  147. package/src/styles.css +214 -0
@@ -0,0 +1,19 @@
1
+ import { Plus } from "lucide-react";
2
+ import { type ComponentProps, forwardRef } from "react";
3
+ import { ActionButton } from "./base/action-button";
4
+
5
+ interface AddNewButtonProps extends ComponentProps<typeof ActionButton> {
6
+ label?: string;
7
+ responsive?: boolean;
8
+ }
9
+
10
+ export const AddNewButton = forwardRef<HTMLButtonElement, AddNewButtonProps>(
11
+ ({ label = "Add New", responsive = false, children, ...props }, ref) => {
12
+ return (
13
+ <ActionButton ref={ref} icon={<Plus />} responsive={responsive} {...props}>
14
+ {children || label}
15
+ </ActionButton>
16
+ );
17
+ },
18
+ );
19
+ AddNewButton.displayName = "AddNewButton";
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/ui/button";
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipProvider,
8
+ TooltipTrigger,
9
+ } from "@suphark/ui/components/ui/tooltip";
10
+ import {
11
+ type CheckPermissionOptions,
12
+ type CheckPermissionRequirement,
13
+ usePermission,
14
+ } from "@suphark/ui/lib/permission";
15
+ import { cn } from "@suphark/ui/lib/utils";
16
+ import { Loader2 } from "lucide-react";
17
+ import * as React from "react";
18
+
19
+ // ดึง Type Props มาจาก Component โดยตรง
20
+ type ButtonProps = React.ComponentProps<typeof Button>;
21
+
22
+ type TooltipContentProps = React.ComponentProps<typeof TooltipContent>;
23
+
24
+ /**
25
+ * Extends standard ButtonProps and adds tooltip and external loading state.
26
+ */
27
+ export interface ActionButtonProps extends ButtonProps, CheckPermissionOptions {
28
+ /**
29
+ * Optional: Permission requirement to render this button (e.g. "po-tracking:create")
30
+ */
31
+ permission?: CheckPermissionRequirement;
32
+ /**
33
+ * Optional text or element to display in a tooltip on hover.
34
+ */
35
+ tooltip?: React.ReactNode;
36
+ /**
37
+ * Specifies the preferred side of the trigger to render against.
38
+ */
39
+ tooltipSide?: TooltipContentProps["side"]; // "bottom" | "top" | "right" | "left"
40
+ /**
41
+ * An optional icon element (e.g., <CircleDollarSign />) to display.
42
+ * It will be replaced by a spinner when isLoading is true.
43
+ */
44
+ icon?: React.ReactElement<{ className?: string }>;
45
+ /**
46
+ * A boolean prop passed from the parent to control the loading state.
47
+ */
48
+ isLoading?: boolean;
49
+ /**
50
+ * If true, the label will be hidden on mobile screens (max-sm) and the tooltip
51
+ * will automatically show the label if not explicitly provided.
52
+ */
53
+ responsive?: boolean;
54
+ }
55
+
56
+ /**
57
+ * A component that combines Button, Tooltip, and an external loading state
58
+ * for in-page actions (e.g., opening dialogs, submitting forms).
59
+ */
60
+ export function ActionButton({
61
+ permission,
62
+ organizationId,
63
+ orgTag,
64
+ tooltip,
65
+ tooltipSide,
66
+ icon,
67
+ isLoading,
68
+ responsive,
69
+ children,
70
+ className,
71
+ disabled,
72
+ onClick,
73
+ variant,
74
+ ...props
75
+ }: ActionButtonProps) {
76
+ // Call usePermission unconditionally to comply with Rules of Hooks
77
+ const hasAccess = usePermission(permission || "public", { organizationId, orgTag });
78
+
79
+ if (permission && !hasAccess) {
80
+ return null;
81
+ }
82
+ // 1. Determine which icon to display
83
+ const displayIcon = isLoading ? (
84
+ <Loader2
85
+ className={cn("h-4 w-4 animate-spin", children && (responsive ? "sm:mr-2" : "mr-2"))}
86
+ />
87
+ ) : icon ? (
88
+ // Clone the provided icon to add standard styling
89
+ React.cloneElement(icon, {
90
+ className: cn("h-4 w-4", children && (responsive ? "sm:mr-2" : "mr-2"), icon.props.className),
91
+ })
92
+ ) : null;
93
+
94
+ // 2. Wrap children if responsive
95
+ const content = responsive ? (
96
+ <span className="hidden sm:inline-block">{children}</span>
97
+ ) : (
98
+ children
99
+ );
100
+
101
+ // 3. Auto-tooltip for responsive mode
102
+ const finalTooltip = tooltip || (responsive ? children : null);
103
+
104
+ // 4. Create the core button content
105
+ const buttonContent = (
106
+ <Button
107
+ className={cn(responsive && "max-sm:px-2", className)}
108
+ disabled={isLoading || disabled} // Disable if loading or explicitly disabled
109
+ onClick={onClick}
110
+ variant={variant}
111
+ {...props}
112
+ >
113
+ {displayIcon}
114
+ {content}
115
+ </Button>
116
+ );
117
+
118
+ // 5. Wrap with Tooltip if provided or auto-generated
119
+ if (finalTooltip) {
120
+ return (
121
+ <TooltipProvider>
122
+ <Tooltip>
123
+ <TooltipTrigger render={buttonContent as React.ReactElement} />
124
+ <TooltipContent side={tooltipSide}>
125
+ {typeof finalTooltip === "string" ? <p>{finalTooltip}</p> : finalTooltip}
126
+ </TooltipContent>
127
+ </Tooltip>
128
+ </TooltipProvider>
129
+ );
130
+ }
131
+
132
+ // 6. Return plain button if no tooltip
133
+ return buttonContent;
134
+ }
@@ -0,0 +1,130 @@
1
+ "use client";
2
+
3
+ // [FIX 1.1] นำเข้าเฉพาะ Button
4
+ import { Button } from "@suphark/ui/components/ui/button";
5
+ import {
6
+ Tooltip,
7
+ TooltipContent,
8
+ TooltipProvider,
9
+ TooltipTrigger,
10
+ } from "@suphark/ui/components/ui/tooltip";
11
+ import {
12
+ type CheckPermissionOptions,
13
+ type CheckPermissionRequirement,
14
+ usePermission,
15
+ } from "@suphark/ui/lib/permission";
16
+ import { cn } from "@suphark/ui/lib/utils";
17
+ import { Loader2 } from "lucide-react";
18
+ import { useRouter } from "next/navigation";
19
+ import * as React from "react";
20
+ import { useTransition } from "react";
21
+
22
+ // [FIX 1.2] ดึง Type Props มาจาก Component โดยตรง
23
+ type ButtonProps = React.ComponentProps<typeof Button>;
24
+
25
+ /**
26
+ * Extends standard ButtonProps and adds navigation and tooltip features.
27
+ */
28
+ export interface LinkButtonProps extends ButtonProps, CheckPermissionOptions {
29
+ /**
30
+ * Optional: Permission requirement to render this button (e.g. "po-tracking:read")
31
+ */
32
+ permission?: CheckPermissionRequirement;
33
+ /**
34
+ * The path to navigate to.
35
+ */
36
+ href: string;
37
+ /**
38
+ * Optional text or element to display in a tooltip on hover.
39
+ */
40
+ tooltip?: React.ReactNode;
41
+ /**
42
+ * An optional icon element (e.g., <BarChart3 />) to display.
43
+ * It will be replaced by a spinner during transition.
44
+ */
45
+ // [FIX 2] ระบุ Type ของ icon ให้ชัดเจนขึ้น
46
+ icon?: React.ReactElement<{ className?: string }>;
47
+ }
48
+
49
+ /**
50
+ * A component that combines Button, Tooltip, and client-side navigation
51
+ * with a loading transition (spinner).
52
+ */
53
+ export function LinkButton({
54
+ permission,
55
+ organizationId,
56
+ orgTag,
57
+ href,
58
+ tooltip,
59
+ icon,
60
+ children,
61
+ className,
62
+ disabled,
63
+ onClick,
64
+ ...props
65
+ }: LinkButtonProps) {
66
+ // Call usePermission unconditionally to comply with Rules of Hooks
67
+ const hasAccess = usePermission(permission || "public", { organizationId, orgTag });
68
+ const router = useRouter();
69
+ const [isPending, startTransition] = useTransition();
70
+
71
+ if (permission && !hasAccess) {
72
+ return null;
73
+ }
74
+
75
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
76
+ // Allow any custom onClick logic passed by the parent
77
+ if (onClick) {
78
+ (onClick as any)(e);
79
+ }
80
+
81
+ // Prevent default button behavior
82
+ e.preventDefault();
83
+
84
+ // Start navigation transition
85
+ startTransition(() => {
86
+ router.push(href);
87
+ });
88
+ };
89
+
90
+ // 1. Determine which icon to display
91
+ const displayIcon = isPending ? (
92
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
93
+ ) : icon ? (
94
+ // Clone the provided icon to add standard styling
95
+ // (ตอนนี้ TypeScript จะเข้าใจ icon.props.className แล้ว)
96
+ React.cloneElement(icon, {
97
+ className: cn("mr-2 h-4 w-4", icon.props.className),
98
+ })
99
+ ) : null;
100
+
101
+ // 2. Create the core button content
102
+ const buttonContent = (
103
+ <Button
104
+ className={className}
105
+ disabled={isPending || disabled} // Disable if pending or explicitly disabled
106
+ onClick={handleClick}
107
+ {...props}
108
+ >
109
+ {displayIcon}
110
+ {children}
111
+ </Button>
112
+ );
113
+
114
+ // 3. Wrap with Tooltip if provided
115
+ if (tooltip) {
116
+ return (
117
+ <TooltipProvider>
118
+ <Tooltip>
119
+ <TooltipTrigger render={buttonContent as React.ReactElement} />
120
+ <TooltipContent>
121
+ {typeof tooltip === "string" ? <p>{tooltip}</p> : tooltip}
122
+ </TooltipContent>
123
+ </Tooltip>
124
+ </TooltipProvider>
125
+ );
126
+ }
127
+
128
+ // 4. Return plain button if no tooltip
129
+ return buttonContent;
130
+ }
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButton,
5
+ type ActionButtonProps,
6
+ } from "@suphark/ui/components/shared/button/base/action-button";
7
+ import { X } from "lucide-react";
8
+ import type * as React from "react";
9
+
10
+ interface CancelButtonProps extends ActionButtonProps {
11
+ label?: React.ReactNode;
12
+ }
13
+
14
+ export function CancelButton({
15
+ children,
16
+ icon,
17
+ label = "Cancel",
18
+ type = "button",
19
+ variant = "outline",
20
+ ...props
21
+ }: CancelButtonProps) {
22
+ return (
23
+ <ActionButton type={type} variant={variant} icon={icon ?? <X />} {...props}>
24
+ {children ?? label}
25
+ </ActionButton>
26
+ );
27
+ }
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButton,
5
+ type ActionButtonProps,
6
+ } from "@suphark/ui/components/shared/button/base/action-button";
7
+ import { Check } from "lucide-react";
8
+ import type * as React from "react";
9
+
10
+ interface ConfirmButtonProps extends ActionButtonProps {
11
+ label?: React.ReactNode;
12
+ }
13
+
14
+ export function ConfirmButton({
15
+ children,
16
+ icon,
17
+ label = "Confirm",
18
+ type = "button",
19
+ ...props
20
+ }: ConfirmButtonProps) {
21
+ return (
22
+ <ActionButton type={type} icon={icon ?? <Check />} {...props}>
23
+ {children ?? label}
24
+ </ActionButton>
25
+ );
26
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButton,
5
+ type ActionButtonProps,
6
+ } from "@suphark/ui/components/shared/button/base/action-button";
7
+ import { Trash2 } from "lucide-react";
8
+ import type * as React from "react";
9
+
10
+ interface DeleteButtonProps extends ActionButtonProps {
11
+ label?: React.ReactNode;
12
+ loadingLabel?: React.ReactNode;
13
+ }
14
+
15
+ export function DeleteButton({
16
+ children,
17
+ icon,
18
+ isLoading,
19
+ label = "Delete",
20
+ loadingLabel = "Deleting...",
21
+ type = "button",
22
+ variant = "destructive",
23
+ ...props
24
+ }: DeleteButtonProps) {
25
+ return (
26
+ <ActionButton
27
+ type={type}
28
+ variant={variant}
29
+ icon={icon ?? <Trash2 />}
30
+ isLoading={isLoading}
31
+ {...props}
32
+ >
33
+ {children ?? (isLoading ? loadingLabel : label)}
34
+ </ActionButton>
35
+ );
36
+ }
@@ -0,0 +1,89 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/ui/button";
4
+ import { cn } from "@suphark/ui/lib/utils";
5
+ import { Check, Loader2, Save, X } from "lucide-react";
6
+ import { useEffect, useRef, useState } from "react";
7
+
8
+ interface FloatingSaveButtonProps {
9
+ isPending: boolean;
10
+ hasChanges: boolean;
11
+ onSave: () => void;
12
+ onCancel?: () => void;
13
+ saveText?: string;
14
+ successText?: string;
15
+ pendingText?: string;
16
+ cancelText?: string;
17
+ className?: string;
18
+ buttonClassName?: string;
19
+ }
20
+
21
+ export function FloatingSaveButton({
22
+ isPending,
23
+ hasChanges,
24
+ onSave,
25
+ onCancel,
26
+ saveText = "Save",
27
+ successText = "Saved",
28
+ pendingText = "Saving...",
29
+ cancelText = "Cancel",
30
+ className,
31
+ buttonClassName,
32
+ }: FloatingSaveButtonProps) {
33
+ const [showSuccess, setShowSuccess] = useState(false);
34
+ const wasPending = useRef(false);
35
+
36
+ useEffect(() => {
37
+ if (wasPending.current && !isPending && !hasChanges) {
38
+ setShowSuccess(true);
39
+ const timer = setTimeout(() => setShowSuccess(false), 2000);
40
+ return () => clearTimeout(timer);
41
+ }
42
+ wasPending.current = isPending;
43
+ }, [isPending, hasChanges]);
44
+
45
+ if (!hasChanges && !showSuccess) return null;
46
+
47
+ return (
48
+ <div
49
+ className={cn(
50
+ "fade-in slide-in-from-bottom-4 fixed right-8 bottom-8 z-50 flex animate-in items-center gap-3 duration-300",
51
+ className,
52
+ )}
53
+ >
54
+ {onCancel && hasChanges && !isPending && !showSuccess && (
55
+ <Button
56
+ variant="secondary"
57
+ size="lg"
58
+ onClick={onCancel}
59
+ className="h-14 rounded-full border-2 border-border bg-background/80 px-6 shadow-xl backdrop-blur-md transition-all hover:bg-muted"
60
+ >
61
+ <X className="mr-2 h-4 w-4" />
62
+ {cancelText}
63
+ </Button>
64
+ )}
65
+
66
+ <Button
67
+ size="lg"
68
+ className={cn(
69
+ "h-14 gap-3 rounded-full border-4 border-background px-8 text-base shadow-2xl transition-all duration-300",
70
+ showSuccess
71
+ ? "scale-105 bg-emerald-500 text-white shadow-[0_0_20px_rgba(16,185,129,0.4)] hover:bg-emerald-500"
72
+ : "bg-primary text-primary-foreground hover:bg-primary/90",
73
+ buttonClassName,
74
+ )}
75
+ disabled={isPending || showSuccess}
76
+ onClick={onSave}
77
+ >
78
+ {isPending ? (
79
+ <Loader2 className="h-5 w-5 animate-spin" />
80
+ ) : showSuccess ? (
81
+ <Check className="zoom-in h-5 w-5 animate-in duration-300" />
82
+ ) : (
83
+ <Save className="h-5 w-5" />
84
+ )}
85
+ <span>{isPending ? pendingText : showSuccess ? successText : saveText}</span>
86
+ </Button>
87
+ </div>
88
+ );
89
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * ปุ่มมาตรฐานสำหรับ action ทั่วไป — ใช้ก่อนเขียน <Button> ดิบเสมอ
3
+ * ปุ่มที่ต้องใช้ next/navigation (LinkButton, RefreshButton) อยู่ที่ `@suphark/ui/next`
4
+ */
5
+ export { AddNewButton } from "./add-new-button";
6
+ export { ActionButton, type ActionButtonProps } from "./base/action-button";
7
+ export { CancelButton } from "./cancel-button";
8
+ export { ConfirmButton } from "./confirm-button";
9
+ export { DeleteButton } from "./delete-button";
10
+ export { FloatingSaveButton } from "./floating-save-button";
11
+ export { PrintButton } from "./print-button";
12
+ export { SaveButton } from "./save-button";
13
+ export { SubmitButton } from "./submit-button";
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButton,
5
+ type ActionButtonProps,
6
+ } from "@suphark/ui/components/shared/button/base/action-button";
7
+ import { cn } from "@suphark/ui/lib/utils";
8
+ import { Printer } from "lucide-react";
9
+
10
+ // PrintButtonProps extends ActionButtonProps
11
+ type PrintButtonProps = ActionButtonProps & {
12
+ href?: string;
13
+ target?: string;
14
+ };
15
+
16
+ /**
17
+ * A shared client component that provides a button to trigger the browser's print functionality.
18
+ * It is built on `ActionButton` and automatically hidden when the page is being printed.
19
+ *
20
+ * @param {React.ReactNode} [children] - (Optional) The text to display. Defaults to "Print".
21
+ * @param {React.ReactElement} [icon] - (Optional) Overrides the default <Printer /> icon.
22
+ * @param {string} [tooltip] - (Optional) Tooltip text to display on hover.
23
+ * @param {function} [onClick] - (Optional) A custom onClick handler. If not provided, it defaults to `window.print()`.
24
+ * @param {string} [variant] - (Optional) Button variant. Defaults to "outline".
25
+ * @param {string} [className] - (Optional) Additional CSS classes.
26
+ */
27
+ export function PrintButton({
28
+ children,
29
+ icon,
30
+ onClick,
31
+ className,
32
+ variant,
33
+ href,
34
+ target,
35
+ ...props
36
+ }: PrintButtonProps) {
37
+ // 1. G G (Default values)
38
+ const buttonText = children ?? "Print";
39
+ const buttonIcon = icon ?? <Printer />;
40
+ const buttonVariant = variant ?? "outline";
41
+
42
+ // 2. Default onClick logic
43
+ // If href is provided, we don't want to trigger window.print() by default
44
+ const handleClick = onClick ?? (href ? undefined : () => window.print());
45
+
46
+ const content = (
47
+ <ActionButton
48
+ variant={buttonVariant}
49
+ icon={buttonIcon}
50
+ onClick={handleClick}
51
+ className={cn("print:hidden", className)}
52
+ {...props}
53
+ >
54
+ {buttonText}
55
+ </ActionButton>
56
+ );
57
+
58
+ if (href) {
59
+ return (
60
+ <a href={href} target={target} rel={target === "_blank" ? "noopener noreferrer" : undefined}>
61
+ {content}
62
+ </a>
63
+ );
64
+ }
65
+
66
+ return content;
67
+ }
@@ -0,0 +1,63 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButton,
5
+ type ActionButtonProps,
6
+ } from "@suphark/ui/components/shared/button/base/action-button";
7
+ import { cn } from "@suphark/ui/lib/utils";
8
+ import { RefreshCcw } from "lucide-react";
9
+ import { useRouter } from "next/navigation";
10
+ import * as React from "react";
11
+
12
+ type RefreshButtonProps = ActionButtonProps;
13
+
14
+ /**
15
+ * A shared client component that provides a button to trigger a router refresh.
16
+ * It is built on `ActionButton`.
17
+ *
18
+ * @param {React.ReactNode} [children] - (Optional) The text to display. Defaults to "Refresh".
19
+ * @param {React.ReactElement} [icon] - (Optional) Overrides the default <RefreshCcw /> icon.
20
+ * @param {function} [onClick] - (Optional) A custom onClick handler. If not provided, it defaults to `router.refresh()`.
21
+ * @param {string} [variant] - (Optional) Button variant. Defaults to "outline".
22
+ * @param {string} [className] - (Optional) Additional CSS classes.
23
+ */
24
+ export function RefreshButton({
25
+ children,
26
+ icon,
27
+ onClick,
28
+ className,
29
+ variant,
30
+ ...props
31
+ }: RefreshButtonProps) {
32
+ const router = useRouter();
33
+
34
+ // 1. Default values
35
+ const buttonText = children ?? "Refresh";
36
+ const buttonIcon = icon ?? <RefreshCcw />;
37
+ const buttonVariant = variant ?? "outline";
38
+
39
+ // 2. Default onClick to router.refresh()
40
+ // Track loading state for router refresh
41
+ const [isPending, startTransition] = React.useTransition();
42
+
43
+ const handleClick =
44
+ onClick ??
45
+ (() => {
46
+ startTransition(() => {
47
+ router.refresh();
48
+ });
49
+ });
50
+
51
+ return (
52
+ <ActionButton
53
+ variant={buttonVariant}
54
+ icon={buttonIcon}
55
+ onClick={handleClick}
56
+ className={cn(className)}
57
+ isLoading={props.isLoading || isPending}
58
+ {...props}
59
+ >
60
+ {buttonText}
61
+ </ActionButton>
62
+ );
63
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ import {
4
+ ActionButton,
5
+ type ActionButtonProps,
6
+ } from "@suphark/ui/components/shared/button/base/action-button";
7
+ import { Save } from "lucide-react";
8
+ import type * as React from "react";
9
+
10
+ interface SaveButtonProps extends ActionButtonProps {
11
+ label?: React.ReactNode;
12
+ loadingLabel?: React.ReactNode;
13
+ }
14
+
15
+ export function SaveButton({
16
+ children,
17
+ icon,
18
+ isLoading,
19
+ label = "Save",
20
+ loadingLabel = "Saving...",
21
+ type = "submit",
22
+ ...props
23
+ }: SaveButtonProps) {
24
+ return (
25
+ <ActionButton type={type} icon={icon ?? <Save />} isLoading={isLoading} {...props}>
26
+ {children ?? (isLoading ? loadingLabel : label)}
27
+ </ActionButton>
28
+ );
29
+ }