@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.
Files changed (41) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/webapplications/feature-react-authentication/package-lock.json +1173 -401
  3. package/dist/force-app/main/default/webapplications/feature-react-authentication/package.json +1 -3
  4. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/app.tsx +2 -5
  5. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/authHelpers.ts +73 -0
  6. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/{utils → components/auth}/authenticationConfig.ts +9 -0
  7. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/{authentication-route.tsx → authenticationRouteLayout.tsx} +1 -1
  8. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/{private-route.tsx → privateRouteLayout.tsx} +1 -1
  9. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/sessionTimeout/SessionTimeoutValidator.tsx +616 -0
  10. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/sessionTimeout/sessionTimeService.ts +161 -0
  11. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/auth/sessionTimeout/sessionTimeoutConfig.ts +77 -0
  12. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/alert.tsx +17 -13
  13. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/button.tsx +35 -22
  14. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/card.tsx +27 -12
  15. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/dialog.tsx +143 -0
  16. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/field.tsx +157 -46
  17. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/index.ts +1 -0
  18. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/input.tsx +3 -3
  19. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/label.tsx +2 -2
  20. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/pagination.tsx +87 -74
  21. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/select.tsx +156 -124
  22. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/separator.tsx +26 -0
  23. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/skeleton.tsx +1 -0
  24. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/spinner.tsx +5 -16
  25. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/table.tsx +68 -95
  26. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/components/ui/tabs.tsx +47 -84
  27. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/context/AuthContext.tsx +12 -0
  28. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/form.tsx +1 -1
  29. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/useCountdownTimer.ts +266 -0
  30. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/hooks/useRetryWithBackoff.ts +109 -0
  31. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/layouts/AuthAppLayout.tsx +12 -0
  32. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ChangePassword.tsx +3 -2
  33. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ForgotPassword.tsx +1 -1
  34. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Login.tsx +3 -3
  35. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Profile.tsx +3 -2
  36. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/Register.tsx +4 -5
  37. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/pages/ResetPassword.tsx +3 -2
  38. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/routes.tsx +5 -5
  39. package/dist/force-app/main/default/webapplications/feature-react-authentication/src/utils/helpers.ts +0 -74
  40. package/dist/package.json +1 -1
  41. 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
- "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
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 [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
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={role ?? (variant === "destructive" ? "alert" : "status")}
30
+ role="alert"
31
31
  className={cn(alertVariants({ variant }), className)}
32
32
  {...props}
33
33
  />
34
34
  );
35
35
  }
36
36
 
37
- // Maintenance/A11y: Added 'asChild' pattern or a simple 'as' prop
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("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
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
- "col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
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
- export { Alert, AlertTitle, AlertDescription };
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
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer",
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 shadow-xs hover:bg-primary/90",
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 shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
- secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
18
- ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
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: "h-9 px-4 py-2 has-[>svg]:px-3",
23
- sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
24
- lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
25
- icon: "size-9",
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
- const Button = React.forwardRef<
36
- HTMLButtonElement,
37
- React.ComponentProps<"button"> &
38
- VariantProps<typeof buttonVariants> & {
39
- asChild?: boolean;
40
- }
41
- >(function Button({ className, variant, size, asChild = false, ...props }, ref) {
42
- const Comp = asChild ? Slot : "button";
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({ className, ...props }: React.ComponentProps<"div">) {
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 flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
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 grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
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
- <Component
38
+ <div
36
39
  data-slot="card-title"
37
- className={cn("leading-none font-semibold tracking-tight", className)}
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 <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
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("flex items-center px-6 [.border-t]:pt-6", className)}
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
+ };