@salesforce/webapp-template-feature-react-authentication-experimental 1.44.0 → 1.45.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/dist/CHANGELOG.md +8 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/package-lock.json +1173 -401
- package/dist/force-app/main/default/webapplications/feature-react-authentication/package.json +1 -3
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/app.tsx +2 -5
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/authHelpers.ts +73 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/{utils → components/auth}/authenticationConfig.ts +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/{authentication-route.tsx → authenticationRouteLayout.tsx} +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/{private-route.tsx → privateRouteLayout.tsx} +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/sessionTimeout/SessionTimeoutValidator.tsx +616 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/sessionTimeout/sessionTimeService.ts +161 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/sessionTimeout/sessionTimeoutConfig.ts +77 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/alert.tsx +17 -13
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/button.tsx +35 -22
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/card.tsx +27 -12
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/dialog.tsx +143 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/field.tsx +157 -46
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/index.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/input.tsx +3 -3
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/label.tsx +2 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/pagination.tsx +87 -74
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/select.tsx +156 -124
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/skeleton.tsx +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/spinner.tsx +5 -16
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/table.tsx +68 -95
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/tabs.tsx +47 -84
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/context/AuthContext.tsx +12 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/form.tsx +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/useCountdownTimer.ts +266 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/useRetryWithBackoff.ts +109 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/layouts/AuthAppLayout.tsx +12 -0
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ChangePassword.tsx +3 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ForgotPassword.tsx +1 -1
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Login.tsx +3 -3
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Profile.tsx +3 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Register.tsx +4 -5
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ResetPassword.tsx +3 -2
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/routes.tsx +5 -5
- package/dist/force-app/main/default/webapplications/feature-react-authentication/src/utils/helpers.ts +0 -74
- package/dist/package.json +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionTimeServlet API service
|
|
3
|
+
* Handles communication with the session validation endpoint
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SESSION_CONFIG } from "./sessionTimeoutConfig";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Response from SessionTimeServlet API
|
|
10
|
+
*/
|
|
11
|
+
export interface SessionResponse {
|
|
12
|
+
/** Session phase */
|
|
13
|
+
sp: number;
|
|
14
|
+
/** Seconds remaining in session */
|
|
15
|
+
sr: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse the servlet response text into SessionResponse object
|
|
20
|
+
* Handles CSRF protection prefix
|
|
21
|
+
*
|
|
22
|
+
* @param text - Raw response text from servlet
|
|
23
|
+
* @returns Parsed session response
|
|
24
|
+
* @throws Error if response cannot be parsed
|
|
25
|
+
*/
|
|
26
|
+
function parseResponseResult(text: string): SessionResponse {
|
|
27
|
+
let cleanedText = text;
|
|
28
|
+
|
|
29
|
+
// Strip CSRF protection prefix if present
|
|
30
|
+
if (cleanedText.startsWith(SESSION_CONFIG.CSRF_TOKEN)) {
|
|
31
|
+
cleanedText = cleanedText.substring(SESSION_CONFIG.CSRF_TOKEN.length);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Trim whitespace
|
|
35
|
+
cleanedText = cleanedText.trim();
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(cleanedText) as SessionResponse;
|
|
39
|
+
|
|
40
|
+
// Validate response structure
|
|
41
|
+
if (typeof parsed.sp !== "number" || typeof parsed.sr !== "number") {
|
|
42
|
+
throw new Error("Invalid response structure: missing sp or sr properties");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return parsed;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("[sessionTimeService] Failed to parse response:", error, "Text:", cleanedText);
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Failed to parse session response: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Call SessionTimeServlet API
|
|
56
|
+
* Internal function used by both poll and extend functions
|
|
57
|
+
*
|
|
58
|
+
* @param basePath - Community base path (e.g., "/sfsites/c/")
|
|
59
|
+
* @param extend - Whether to extend the session (updateTimedOutSession param)
|
|
60
|
+
* @returns Session response with remaining time
|
|
61
|
+
* @throws Error if API call fails or security checks fail
|
|
62
|
+
*/
|
|
63
|
+
async function callSessionTimeServlet(
|
|
64
|
+
basePath: string,
|
|
65
|
+
extend: boolean = false,
|
|
66
|
+
): Promise<SessionResponse> {
|
|
67
|
+
// Build URL with cache-busting timestamp
|
|
68
|
+
const timestamp = Date.now();
|
|
69
|
+
let url = `${basePath}${SESSION_CONFIG.SERVLET_URL}?buster=${timestamp}`;
|
|
70
|
+
|
|
71
|
+
if (extend) {
|
|
72
|
+
url += "&updateTimedOutSession=true";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(url, {
|
|
77
|
+
method: "GET",
|
|
78
|
+
credentials: "same-origin", // Include cookies for session
|
|
79
|
+
cache: "no-cache",
|
|
80
|
+
// Security headers
|
|
81
|
+
headers: {
|
|
82
|
+
"X-Requested-With": "XMLHttpRequest", // Helps identify XHR requests
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
// Provide more context for common error codes
|
|
88
|
+
if (response.status === 401) {
|
|
89
|
+
throw new Error("Session expired or unauthorized");
|
|
90
|
+
} else if (response.status === 403) {
|
|
91
|
+
throw new Error("Access forbidden");
|
|
92
|
+
} else if (response.status === 404) {
|
|
93
|
+
throw new Error("Session endpoint not found");
|
|
94
|
+
} else {
|
|
95
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Security: Validate content type (should be text or JSON)
|
|
100
|
+
const contentType = response.headers.get("content-type");
|
|
101
|
+
if (contentType && !contentType.includes("text") && !contentType.includes("json")) {
|
|
102
|
+
throw new Error(`Unexpected content type: ${contentType}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const text = await response.text();
|
|
106
|
+
const parsed = parseResponseResult(text);
|
|
107
|
+
|
|
108
|
+
// Apply latency buffer to account for network delay
|
|
109
|
+
const adjustedSecondsRemaining = Math.max(0, parsed.sr - SESSION_CONFIG.LATENCY_BUFFER_SECONDS);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
sp: parsed.sp,
|
|
113
|
+
sr: adjustedSecondsRemaining,
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// Don't log the full URL in production to avoid leaking sensitive info
|
|
117
|
+
console.error("[sessionTimeService] API call failed:", error);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Poll SessionTimeServlet to check remaining session time
|
|
124
|
+
* Called periodically to monitor session status
|
|
125
|
+
*
|
|
126
|
+
* @param basePath - Community base path (e.g., "/sfsites/c/")
|
|
127
|
+
* @returns Session response with remaining time (after latency buffer adjustment)
|
|
128
|
+
* @throws Error if API call fails
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* const { sr, sp } = await pollSessionTimeServlet('/sfsites/c/');
|
|
132
|
+
* if (sr <= 300) {
|
|
133
|
+
* // Less than 5 minutes remaining
|
|
134
|
+
* showWarning();
|
|
135
|
+
* }
|
|
136
|
+
*/
|
|
137
|
+
export async function pollSessionTimeServlet(basePath: string): Promise<SessionResponse> {
|
|
138
|
+
return callSessionTimeServlet(basePath, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extend the current session time
|
|
143
|
+
* Called when user clicks "Continue Working" in warning modal
|
|
144
|
+
*
|
|
145
|
+
* @param basePath - Community base path (e.g., "/sfsites/c/")
|
|
146
|
+
* @returns Session response with new remaining time
|
|
147
|
+
* @throws Error if API call fails
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const { sr } = await extendSessionTime('/sfsites/c/');
|
|
151
|
+
* console.log(`Session extended. ${sr} seconds remaining.`);
|
|
152
|
+
*/
|
|
153
|
+
export async function extendSessionTime(basePath: string): Promise<SessionResponse> {
|
|
154
|
+
return callSessionTimeServlet(basePath, true);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Export parseResponseResult for testing purposes
|
|
159
|
+
* @internal
|
|
160
|
+
*/
|
|
161
|
+
export { parseResponseResult };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants for session timeout monitoring
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Retry Configuration
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/** Initial delay for first retry attempt (2 seconds) */
|
|
10
|
+
export const INITIAL_RETRY_DELAY = 2000;
|
|
11
|
+
|
|
12
|
+
/** Maximum number of retry attempts before giving up */
|
|
13
|
+
export const MAX_RETRY_ATTEMPTS = 10;
|
|
14
|
+
|
|
15
|
+
/** Maximum retry delay (30 minutes) */
|
|
16
|
+
export const MAX_RETRY_DELAY = 30 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Session Storage Keys
|
|
20
|
+
// ============================================================================
|
|
21
|
+
|
|
22
|
+
/** sessionStorage keys used by session validator */
|
|
23
|
+
export const STORAGE_KEYS = {
|
|
24
|
+
/** Flag to show session expired message on login page */
|
|
25
|
+
SHOW_SESSION_MESSAGE: "lwrSessionValidator.showSessionMessage",
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Servlet Configuration
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/** SessionTimeServlet configuration */
|
|
33
|
+
export const SESSION_CONFIG = {
|
|
34
|
+
/** Relative URL to SessionTimeServlet */
|
|
35
|
+
SERVLET_URL: "/sfsites/c/_nc_external/system/security/session/SessionTimeServlet",
|
|
36
|
+
|
|
37
|
+
/** Latency buffer to subtract from server response (seconds) */
|
|
38
|
+
LATENCY_BUFFER_SECONDS: 3,
|
|
39
|
+
|
|
40
|
+
/** CSRF protection prefix in servlet responses */
|
|
41
|
+
CSRF_TOKEN: "while(1);\n",
|
|
42
|
+
} as const;
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// UI Labels
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* UI labels for session timeout components
|
|
50
|
+
*/
|
|
51
|
+
export const LABELS = {
|
|
52
|
+
/** Title for session warning modal */
|
|
53
|
+
sessionWarningTitle: "Session Timeout Warning",
|
|
54
|
+
|
|
55
|
+
/** Message text in session warning modal */
|
|
56
|
+
sessionWarningMessage:
|
|
57
|
+
"For security, we log you out if you’re inactive for too long. To continue working, click Continue before the time expires.",
|
|
58
|
+
|
|
59
|
+
/** Text for "Continue" button */
|
|
60
|
+
continueButton: "Continue",
|
|
61
|
+
|
|
62
|
+
/** Text for "Log Out" button */
|
|
63
|
+
logoutButton: "Log Out",
|
|
64
|
+
|
|
65
|
+
/** Message shown on login page after session expires */
|
|
66
|
+
invalidSessionMessage: "Your session has expired. Please log in again.",
|
|
67
|
+
|
|
68
|
+
/** Accessibility label for close button */
|
|
69
|
+
closeLabel: "Close",
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Session Timeout Configuration
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
/** Session warning time in seconds (30 seconds) */
|
|
77
|
+
export const SESSION_WARNING_TIME = 30;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
1
2
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
3
|
|
|
3
4
|
import { cn } from "../../lib/utils";
|
|
4
5
|
|
|
5
6
|
const alertVariants = cva(
|
|
6
|
-
"
|
|
7
|
+
"grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4 w-full relative group/alert",
|
|
7
8
|
{
|
|
8
9
|
variants: {
|
|
9
10
|
variant: {
|
|
10
11
|
default: "bg-card text-card-foreground",
|
|
11
12
|
destructive:
|
|
12
|
-
"text-destructive bg-card
|
|
13
|
+
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
|
|
13
14
|
},
|
|
14
15
|
},
|
|
15
16
|
defaultVariants: {
|
|
@@ -21,29 +22,26 @@ const alertVariants = cva(
|
|
|
21
22
|
function Alert({
|
|
22
23
|
className,
|
|
23
24
|
variant,
|
|
24
|
-
role,
|
|
25
25
|
...props
|
|
26
26
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
|
27
27
|
return (
|
|
28
28
|
<div
|
|
29
29
|
data-slot="alert"
|
|
30
|
-
role=
|
|
30
|
+
role="alert"
|
|
31
31
|
className={cn(alertVariants({ variant }), className)}
|
|
32
32
|
{...props}
|
|
33
33
|
/>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
// to allow rendering semantic headings (h1-h6).
|
|
39
|
-
interface AlertTitleProps extends React.ComponentProps<"div"> {
|
|
40
|
-
as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "div" | "p";
|
|
41
|
-
}
|
|
42
|
-
function AlertTitle({ className, as: Component = "div", ...props }: AlertTitleProps) {
|
|
37
|
+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
43
38
|
return (
|
|
44
39
|
<div
|
|
45
40
|
data-slot="alert-title"
|
|
46
|
-
className={cn(
|
|
41
|
+
className={cn(
|
|
42
|
+
"font-medium group-has-[>svg]/alert:col-start-2 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
47
45
|
{...props}
|
|
48
46
|
/>
|
|
49
47
|
);
|
|
@@ -54,7 +52,7 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
|
|
|
54
52
|
<div
|
|
55
53
|
data-slot="alert-description"
|
|
56
54
|
className={cn(
|
|
57
|
-
"
|
|
55
|
+
"text-muted-foreground text-sm text-balance md:text-pretty [&_p:not(:last-child)]:mb-4 [&_a]:hover:text-foreground [&_a]:underline [&_a]:underline-offset-3",
|
|
58
56
|
className,
|
|
59
57
|
)}
|
|
60
58
|
{...props}
|
|
@@ -62,4 +60,10 @@ function AlertDescription({ className, ...props }: React.ComponentProps<"div">)
|
|
|
62
60
|
);
|
|
63
61
|
}
|
|
64
62
|
|
|
65
|
-
|
|
63
|
+
function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
64
|
+
return (
|
|
65
|
+
<div data-slot="alert-action" className={cn("absolute top-2 right-2", className)} {...props} />
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { Alert, AlertTitle, AlertDescription, AlertAction };
|
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { Slot } from "@radix-ui/react-slot";
|
|
3
2
|
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import { Slot } from "radix-ui";
|
|
4
4
|
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
"
|
|
8
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
|
-
default: "bg-primary text-primary-foreground
|
|
13
|
-
destructive:
|
|
14
|
-
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
|
12
|
+
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
15
13
|
outline:
|
|
16
|
-
"border bg-background
|
|
17
|
-
secondary:
|
|
18
|
-
|
|
14
|
+
"border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
15
|
+
secondary:
|
|
16
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
17
|
+
ghost:
|
|
18
|
+
"hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground",
|
|
19
|
+
destructive:
|
|
20
|
+
"bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30",
|
|
19
21
|
link: "text-primary underline-offset-4 hover:underline",
|
|
20
22
|
},
|
|
21
23
|
size: {
|
|
22
|
-
default:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
icon:
|
|
24
|
+
default:
|
|
25
|
+
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
26
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
|
+
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
29
|
+
icon: "size-8",
|
|
30
|
+
"icon-xs":
|
|
31
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
+
"icon-sm":
|
|
33
|
+
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
34
|
+
"icon-lg": "size-9",
|
|
26
35
|
},
|
|
27
36
|
},
|
|
28
37
|
defaultVariants: {
|
|
@@ -32,23 +41,27 @@ const buttonVariants = cva(
|
|
|
32
41
|
},
|
|
33
42
|
);
|
|
34
43
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
function Button({
|
|
45
|
+
className,
|
|
46
|
+
variant = "default",
|
|
47
|
+
size = "default",
|
|
48
|
+
asChild = false,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<"button"> &
|
|
51
|
+
VariantProps<typeof buttonVariants> & {
|
|
52
|
+
asChild?: boolean;
|
|
53
|
+
}) {
|
|
54
|
+
const Comp = asChild ? Slot.Root : "button";
|
|
43
55
|
|
|
44
56
|
return (
|
|
45
57
|
<Comp
|
|
46
|
-
ref={ref}
|
|
47
58
|
data-slot="button"
|
|
59
|
+
data-variant={variant}
|
|
60
|
+
data-size={size}
|
|
48
61
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
49
62
|
{...props}
|
|
50
63
|
/>
|
|
51
64
|
);
|
|
52
|
-
}
|
|
65
|
+
}
|
|
53
66
|
|
|
54
67
|
export { Button, buttonVariants };
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
1
3
|
import { cn } from "../../lib/utils";
|
|
2
4
|
|
|
3
|
-
function Card({
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
size = "default",
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
|
4
10
|
return (
|
|
5
11
|
<div
|
|
6
12
|
data-slot="card"
|
|
13
|
+
data-size={size}
|
|
7
14
|
className={cn(
|
|
8
|
-
"bg-card text-card-foreground
|
|
15
|
+
"ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-xl py-4 text-sm ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col",
|
|
9
16
|
className,
|
|
10
17
|
)}
|
|
11
18
|
{...props}
|
|
@@ -18,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
18
25
|
<div
|
|
19
26
|
data-slot="card-header"
|
|
20
27
|
className={cn(
|
|
21
|
-
"@container/card-header grid auto-rows-min
|
|
28
|
+
"gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]",
|
|
22
29
|
className,
|
|
23
30
|
)}
|
|
24
31
|
{...props}
|
|
@@ -26,15 +33,14 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
26
33
|
);
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
function CardTitle({
|
|
30
|
-
className,
|
|
31
|
-
as: Component = "h3",
|
|
32
|
-
...props
|
|
33
|
-
}: React.ComponentProps<"div"> & { as?: any }) {
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
34
37
|
return (
|
|
35
|
-
<
|
|
38
|
+
<div
|
|
36
39
|
data-slot="card-title"
|
|
37
|
-
className={cn(
|
|
40
|
+
className={cn(
|
|
41
|
+
"text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
38
44
|
{...props}
|
|
39
45
|
/>
|
|
40
46
|
);
|
|
@@ -61,14 +67,23 @@ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
64
|
-
return
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-slot="card-content"
|
|
73
|
+
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
68
80
|
return (
|
|
69
81
|
<div
|
|
70
82
|
data-slot="card-footer"
|
|
71
|
-
className={cn(
|
|
83
|
+
className={cn(
|
|
84
|
+
"bg-muted/50 rounded-b-xl border-t p-4 group-data-[size=sm]/card:p-3 flex items-center",
|
|
85
|
+
className,
|
|
86
|
+
)}
|
|
72
87
|
{...props}
|
|
73
88
|
/>
|
|
74
89
|
);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Dialog as DialogPrimitive } from "radix-ui";
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Button } from "./button";
|
|
6
|
+
import { XIcon } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
9
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
13
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
17
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
21
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function DialogOverlay({
|
|
25
|
+
className,
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
28
|
+
return (
|
|
29
|
+
<DialogPrimitive.Overlay
|
|
30
|
+
data-slot="dialog-overlay"
|
|
31
|
+
className={cn(
|
|
32
|
+
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50",
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function DialogContent({
|
|
41
|
+
className,
|
|
42
|
+
children,
|
|
43
|
+
showCloseButton = true,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
46
|
+
showCloseButton?: boolean;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<DialogPortal>
|
|
50
|
+
<DialogOverlay />
|
|
51
|
+
<DialogPrimitive.Content
|
|
52
|
+
data-slot="dialog-content"
|
|
53
|
+
className={cn(
|
|
54
|
+
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
{showCloseButton && (
|
|
61
|
+
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
|
62
|
+
<Button variant="ghost" className="absolute top-2 right-2" size="icon-sm">
|
|
63
|
+
<XIcon />
|
|
64
|
+
<span className="sr-only">Close</span>
|
|
65
|
+
</Button>
|
|
66
|
+
</DialogPrimitive.Close>
|
|
67
|
+
)}
|
|
68
|
+
</DialogPrimitive.Content>
|
|
69
|
+
</DialogPortal>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
74
|
+
return (
|
|
75
|
+
<div data-slot="dialog-header" className={cn("gap-2 flex flex-col", className)} {...props} />
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function DialogFooter({
|
|
80
|
+
className,
|
|
81
|
+
showCloseButton = false,
|
|
82
|
+
children,
|
|
83
|
+
...props
|
|
84
|
+
}: React.ComponentProps<"div"> & {
|
|
85
|
+
showCloseButton?: boolean;
|
|
86
|
+
}) {
|
|
87
|
+
return (
|
|
88
|
+
<div
|
|
89
|
+
data-slot="dialog-footer"
|
|
90
|
+
className={cn(
|
|
91
|
+
"bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
92
|
+
className,
|
|
93
|
+
)}
|
|
94
|
+
{...props}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
{showCloseButton && (
|
|
98
|
+
<DialogPrimitive.Close asChild>
|
|
99
|
+
<Button variant="outline">Close</Button>
|
|
100
|
+
</DialogPrimitive.Close>
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
107
|
+
return (
|
|
108
|
+
<DialogPrimitive.Title
|
|
109
|
+
data-slot="dialog-title"
|
|
110
|
+
className={cn("text-base leading-none font-medium", className)}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function DialogDescription({
|
|
117
|
+
className,
|
|
118
|
+
...props
|
|
119
|
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
120
|
+
return (
|
|
121
|
+
<DialogPrimitive.Description
|
|
122
|
+
data-slot="dialog-description"
|
|
123
|
+
className={cn(
|
|
124
|
+
"text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
Dialog,
|
|
134
|
+
DialogClose,
|
|
135
|
+
DialogContent,
|
|
136
|
+
DialogDescription,
|
|
137
|
+
DialogFooter,
|
|
138
|
+
DialogHeader,
|
|
139
|
+
DialogOverlay,
|
|
140
|
+
DialogPortal,
|
|
141
|
+
DialogTitle,
|
|
142
|
+
DialogTrigger,
|
|
143
|
+
};
|