@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/components.json +21 -0
- package/package.json +108 -0
- package/scripts/audit-base-ui-props.mjs +179 -0
- package/scripts/audit-tailwind-typography.mjs +100 -0
- package/src/components/design-system/design-system-showcase.tsx +138 -0
- package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
- package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
- package/src/components/design-system/sections/showcase-forms.tsx +373 -0
- package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
- package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
- package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
- package/src/components/design-system/sections/showcase-overview.tsx +368 -0
- package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
- package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
- package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
- package/src/components/shared/button/add-new-button.tsx +19 -0
- package/src/components/shared/button/base/action-button.tsx +134 -0
- package/src/components/shared/button/base/link-button.tsx +130 -0
- package/src/components/shared/button/cancel-button.tsx +27 -0
- package/src/components/shared/button/confirm-button.tsx +26 -0
- package/src/components/shared/button/delete-button.tsx +36 -0
- package/src/components/shared/button/floating-save-button.tsx +89 -0
- package/src/components/shared/button/index.ts +13 -0
- package/src/components/shared/button/print-button.tsx +67 -0
- package/src/components/shared/button/refresh-button.tsx +63 -0
- package/src/components/shared/button/save-button.tsx +29 -0
- package/src/components/shared/button/submit-button.tsx +58 -0
- package/src/components/shared/client-redirect.tsx +11 -0
- package/src/components/shared/combobox.tsx +116 -0
- package/src/components/shared/data-list.tsx +65 -0
- package/src/components/shared/date/date-range-picker.tsx +106 -0
- package/src/components/shared/description-list.tsx +27 -0
- package/src/components/shared/empty-row.tsx +13 -0
- package/src/components/shared/empty-state.tsx +31 -0
- package/src/components/shared/env-snippet.tsx +62 -0
- package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
- package/src/components/shared/form/form-field-boolean.tsx +93 -0
- package/src/components/shared/form/form-field-color-select.tsx +86 -0
- package/src/components/shared/form/form-field-combobox.tsx +167 -0
- package/src/components/shared/form/form-field-date.tsx +55 -0
- package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
- package/src/components/shared/form/form-field-input.tsx +372 -0
- package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
- package/src/components/shared/form/form-field-radio-group.tsx +56 -0
- package/src/components/shared/form/form-field-select.tsx +312 -0
- package/src/components/shared/form/simple-select.tsx +152 -0
- package/src/components/shared/form-dialog.tsx +109 -0
- package/src/components/shared/form-message.tsx +30 -0
- package/src/components/shared/initials.tsx +33 -0
- package/src/components/shared/layout/error-state.tsx +160 -0
- package/src/components/shared/layout/page-shell.tsx +57 -0
- package/src/components/shared/link/transition-link.tsx +72 -0
- package/src/components/shared/multi-select-combobox.tsx +392 -0
- package/src/components/shared/name-id-fields.tsx +133 -0
- package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
- package/src/components/shared/notice.tsx +35 -0
- package/src/components/shared/number/formatted-number.tsx +133 -0
- package/src/components/shared/page-header.tsx +25 -0
- package/src/components/shared/page-skeleton.tsx +18 -0
- package/src/components/shared/row-actions.tsx +93 -0
- package/src/components/shared/row-menu.tsx +55 -0
- package/src/components/shared/secret-reveal.tsx +53 -0
- package/src/components/shared/section-card.tsx +44 -0
- package/src/components/shared/stat.tsx +26 -0
- package/src/components/shared/status-badge.tsx +47 -0
- package/src/components/shared/text/primary-secondary-text.tsx +186 -0
- package/src/components/shared/theme/index.ts +4 -0
- package/src/components/shared/theme/theme-menu-items.tsx +43 -0
- package/src/components/shared/theme/theme-provider.tsx +39 -0
- package/src/components/shared/theme/theme-switch.tsx +36 -0
- package/src/components/shared/theme/theme-toggle.tsx +76 -0
- package/src/components/shared/time/client-date-formatter.tsx +188 -0
- package/src/components/shared/ui-text.tsx +47 -0
- package/src/components/ui/__tests__/combobox.test.tsx +40 -0
- package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
- package/src/components/ui/__tests__/slider.test.tsx +18 -0
- package/src/components/ui/__tests__/tabs.test.tsx +33 -0
- package/src/components/ui/accordion.tsx +69 -0
- package/src/components/ui/alert-dialog.tsx +161 -0
- package/src/components/ui/alert.tsx +68 -0
- package/src/components/ui/aspect-ratio.tsx +22 -0
- package/src/components/ui/attachment.tsx +194 -0
- package/src/components/ui/avatar.tsx +92 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/breadcrumb.tsx +102 -0
- package/src/components/ui/button-group.tsx +77 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/calendar.tsx +184 -0
- package/src/components/ui/card.tsx +87 -0
- package/src/components/ui/carousel.tsx +230 -0
- package/src/components/ui/chart.tsx +338 -0
- package/src/components/ui/checkbox.tsx +27 -0
- package/src/components/ui/collapsible.tsx +17 -0
- package/src/components/ui/combobox.tsx +272 -0
- package/src/components/ui/command.tsx +180 -0
- package/src/components/ui/context-menu.tsx +244 -0
- package/src/components/ui/date-picker.tsx +308 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +208 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +93 -0
- package/src/components/ui/field.tsx +223 -0
- package/src/components/ui/form.tsx +161 -0
- package/src/components/ui/hover-card.tsx +46 -0
- package/src/components/ui/input-group.tsx +145 -0
- package/src/components/ui/input-otp.tsx +85 -0
- package/src/components/ui/input.tsx +19 -0
- package/src/components/ui/item.tsx +187 -0
- package/src/components/ui/kbd.tsx +26 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/menubar.tsx +266 -0
- package/src/components/ui/navigation-menu.tsx +160 -0
- package/src/components/ui/pagination.tsx +117 -0
- package/src/components/ui/popover.tsx +76 -0
- package/src/components/ui/progress.tsx +66 -0
- package/src/components/ui/radio-group.tsx +38 -0
- package/src/components/ui/relative-date.tsx +40 -0
- package/src/components/ui/resizable.tsx +41 -0
- package/src/components/ui/scroll-area.tsx +67 -0
- package/src/components/ui/searchable-select.tsx +124 -0
- package/src/components/ui/select.tsx +189 -0
- package/src/components/ui/separator.tsx +21 -0
- package/src/components/ui/sheet.tsx +124 -0
- package/src/components/ui/sidebar.tsx +691 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +55 -0
- package/src/components/ui/sonner.tsx +45 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +32 -0
- package/src/components/ui/table.tsx +95 -0
- package/src/components/ui/tabs.tsx +79 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/toggle-group.tsx +86 -0
- package/src/components/ui/toggle.tsx +44 -0
- package/src/components/ui/tooltip.tsx +54 -0
- package/src/hooks/use-copy-to-clipboard.ts +27 -0
- package/src/hooks/use-mobile.ts +21 -0
- package/src/index.ts +156 -0
- package/src/lib/formatters.ts +44 -0
- package/src/lib/permission.ts +75 -0
- package/src/lib/status-colors.ts +52 -0
- package/src/lib/utils.ts +6 -0
- package/src/next.ts +15 -0
- 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
|
+
}
|