@aws/nx-plugin 0.70.0 → 0.71.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.
@@ -0,0 +1,259 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`cognito-auth generator terraform iacProvider > should generate terraform files for cognito auth and snapshot them > terraform-cognito-auth-files 1`] = `
4
+ {
5
+ "add-callback-url.tf": "terraform {
6
+ required_providers {
7
+ aws = {
8
+ source = "hashicorp/aws"
9
+ version = "~> 6.0"
10
+ }
11
+ }
12
+ }
13
+
14
+ # Variables
15
+ variable "callback_url" {
16
+ description = "Callback URL to add (e.g., https://d123456789.cloudfront.net)"
17
+ type = string
18
+ }
19
+
20
+ # Read runtime config to get user pool client details
21
+ module "runtime_config_reader" {
22
+ source = "../../runtime-config/read"
23
+ }
24
+
25
+ # Extract cognito details from runtime config
26
+ locals {
27
+ cognito_props = try(module.runtime_config_reader.config.cognitoProps, null)
28
+
29
+ user_pool_id = local.cognito_props != null ? local.cognito_props.userPoolId : null
30
+ user_pool_client_id = local.cognito_props != null ? local.cognito_props.userPoolWebClientId : null
31
+ }
32
+
33
+ # Validation: Ensure cognito props exist in runtime config
34
+ resource "terraform_data" "validate_cognito_props" {
35
+ lifecycle {
36
+ precondition {
37
+ condition = local.cognito_props != null
38
+ error_message = "ERROR: cognitoProps not found in runtime config. Ensure user-identity module has been deployed first and has added cognitoProps to the runtime configuration."
39
+ }
40
+
41
+ precondition {
42
+ condition = local.user_pool_id != null && local.user_pool_id != ""
43
+ error_message = "ERROR: cognitoProps.userPoolId is missing or empty in runtime config. Check that user-identity module completed successfully."
44
+ }
45
+
46
+ precondition {
47
+ condition = local.user_pool_client_id != null && local.user_pool_client_id != ""
48
+ error_message = "ERROR: cognitoProps.userPoolWebClientId is missing or empty in runtime config. Check that user-identity module completed successfully."
49
+ }
50
+ }
51
+ }
52
+
53
+
54
+ # Update the user pool client with additional callback URL
55
+ resource "null_resource" "add_callback_url" {
56
+ triggers = {
57
+ callback_url = var.callback_url
58
+ user_pool_id = local.user_pool_id
59
+ user_pool_client_id = local.user_pool_client_id
60
+ }
61
+
62
+ provisioner "local-exec" {
63
+ command = <<-EOT
64
+ uv run --with boto3 python -c "
65
+ import boto3
66
+ import sys
67
+
68
+ # Configuration
69
+ user_pool_id = '\${local.user_pool_id}'
70
+ client_id = '\${local.user_pool_client_id}'
71
+ new_callback_url = '\${var.callback_url}'
72
+
73
+ # Initialize Cognito client
74
+ cognito = boto3.client('cognito-idp')
75
+
76
+ try:
77
+ # Get current user pool client configuration
78
+ response = cognito.describe_user_pool_client(
79
+ UserPoolId=user_pool_id,
80
+ ClientId=client_id
81
+ )
82
+
83
+ client_config = response['UserPoolClient']
84
+ current_callback_urls = client_config.get('CallbackURLs', [])
85
+ current_logout_urls = client_config.get('LogoutURLs', [])
86
+
87
+ # Check if URL already exists
88
+ if new_callback_url in current_callback_urls:
89
+ print(f'Callback URL {new_callback_url} already exists')
90
+ else:
91
+ # Add new URL to both callback and logout URLs
92
+ updated_callback_urls = current_callback_urls + [new_callback_url]
93
+ updated_logout_urls = current_logout_urls + [new_callback_url]
94
+
95
+ # Update the user pool client
96
+ # Only include valid update parameters (exclude read-only fields and ones we're setting)
97
+ valid_update_params = [
98
+ 'ClientName', 'RefreshTokenValidity', 'AccessTokenValidity', 'IdTokenValidity',
99
+ 'TokenValidityUnits', 'ReadAttributes', 'WriteAttributes', 'ExplicitAuthFlows',
100
+ 'SupportedIdentityProviders', 'DefaultRedirectURI', 'AllowedOAuthFlows',
101
+ 'AllowedOAuthScopes', 'AllowedOAuthFlowsUserPoolClient', 'AnalyticsConfiguration',
102
+ 'PreventUserExistenceErrors', 'EnableTokenRevocation',
103
+ 'EnablePropagateAdditionalUserContextData', 'AuthSessionValidity', 'RefreshTokenRotation'
104
+ ]
105
+
106
+ update_config = {k: v for k, v in client_config.items() if k in valid_update_params}
107
+
108
+ cognito.update_user_pool_client(
109
+ UserPoolId=user_pool_id,
110
+ ClientId=client_id,
111
+ CallbackURLs=updated_callback_urls,
112
+ LogoutURLs=updated_logout_urls,
113
+ **update_config
114
+ )
115
+
116
+ print(f'Successfully added callback URL: {new_callback_url}')
117
+
118
+ except Exception as e:
119
+ print(f'Error updating callback URLs: {e}')
120
+ sys.exit(1)
121
+ "
122
+ EOT
123
+ }
124
+
125
+ depends_on = [terraform_data.validate_cognito_props]
126
+ }
127
+
128
+ ",
129
+ "user-identity.tf": "module "identity" {
130
+ source = "./identity"
131
+
132
+ user_pool_domain_prefix = "test"
133
+ allow_signup = true
134
+ }
135
+
136
+ # Outputs
137
+ output "region" {
138
+ description = "AWS region"
139
+ value = module.identity.region
140
+ }
141
+
142
+ output "user_pool_id" {
143
+ description = "ID of the Cognito User Pool"
144
+ value = module.identity.user_pool_id
145
+ }
146
+
147
+ output "user_pool_arn" {
148
+ description = "ARN of the Cognito User Pool"
149
+ value = module.identity.user_pool_arn
150
+ }
151
+
152
+ output "user_pool_client_id" {
153
+ description = "ID of the Cognito User Pool Client"
154
+ value = module.identity.user_pool_client_id
155
+ }
156
+
157
+ output "identity_pool_id" {
158
+ description = "ID of the Cognito Identity Pool"
159
+ value = module.identity.identity_pool_id
160
+ }
161
+
162
+ output "authenticated_role_name" {
163
+ description = "Name of the authenticated IAM role"
164
+ value = module.identity.authenticated_role_name
165
+ }
166
+
167
+ output "authenticated_role_arn" {
168
+ description = "ARN of the authenticated IAM role"
169
+ value = module.identity.authenticated_role_arn
170
+ }
171
+
172
+ output "user_pool_domain" {
173
+ description = "Domain of the Cognito User Pool"
174
+ value = module.identity.user_pool_domain
175
+ }
176
+ ",
177
+ }
178
+ `;
179
+
180
+ exports[`cognito-auth generator terraform iacProvider > should place add-callback-url module directly after cloudfront distribution resource > complex-static-website-with-callback-url 1`] = `
181
+ "
182
+ # Some initial resources
183
+ resource "aws_s3_bucket" "website" {
184
+ bucket = "test-bucket"
185
+ }
186
+
187
+ resource "aws_cloudfront_distribution" "website" {
188
+ origin {
189
+ domain_name = aws_s3_bucket.website.bucket_regional_domain_name
190
+ origin_id = "S3-\${aws_s3_bucket.website.id}"
191
+ }
192
+
193
+ enabled = true
194
+ is_ipv6_enabled = true
195
+ default_root_object = "index.html"
196
+
197
+ default_cache_behavior {
198
+ allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
199
+ cached_methods = ["GET", "HEAD"]
200
+ target_origin_id = "S3-\${aws_s3_bucket.website.id}"
201
+ compress = true
202
+ viewer_protocol_policy = "redirect-to-https"
203
+ }
204
+
205
+ restrictions {
206
+ geo_restriction {
207
+ restriction_type = "none"
208
+ }
209
+ }
210
+
211
+ viewer_certificate {
212
+ cloudfront_default_certificate = true
213
+ }
214
+ }
215
+
216
+ # Add CloudFront domain to user pool client callback URLs
217
+ module "add_callback_url" {
218
+ source = "../user-identity/add-callback-url"
219
+
220
+ callback_url = "https://\${aws_cloudfront_distribution.website.domain_name}"
221
+
222
+ depends_on = [aws_cloudfront_distribution.website]
223
+ }
224
+
225
+ # S3 Bucket Policy for CloudFront OAC
226
+ resource "aws_s3_bucket_policy" "website_cloudfront_policy" {
227
+ bucket = aws_s3_bucket.website.id
228
+ policy = jsonencode({
229
+ Version = "2012-10-17"
230
+ Statement = [
231
+ {
232
+ Effect = "Allow"
233
+ Principal = {
234
+ Service = "cloudfront.amazonaws.com"
235
+ }
236
+ Action = "s3:GetObject"
237
+ Resource = "\${aws_s3_bucket.website.arn}/*"
238
+ }
239
+ ]
240
+ })
241
+ }
242
+
243
+ # Some outputs
244
+ output "website_bucket_name" {
245
+ value = aws_s3_bucket.website.bucket
246
+ }
247
+ "
248
+ `;
249
+
250
+ exports[`cognito-auth generator terraform iacProvider > should update static-website.tf with add-callback-url module > static-website-with-callback-url 1`] = `
251
+ "
252
+ module "static_website" {
253
+ source = "../../../core/static-website"
254
+
255
+ website_name = "test-project"
256
+ build_path = "dist/packages/test-project"
257
+ }
258
+ "
259
+ `;
@@ -0,0 +1,335 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`cognito-auth generator uxProvider tests > Cloudscape > should update AppLayout > app-layout-with-auth 1`] = `
4
+ "import { useAuth } from 'react-oidc-context';
5
+ import * as React from 'react';
6
+ import { createContext, useCallback, useEffect, useState } from 'react';
7
+ import { NavItems } from './navitems';
8
+ import Config from '../../config';
9
+
10
+ import {
11
+ BreadcrumbGroup,
12
+ BreadcrumbGroupProps,
13
+ SideNavigation,
14
+ TopNavigation,
15
+ } from '@cloudscape-design/components';
16
+
17
+ import CloudscapeAppLayout, {
18
+ AppLayoutProps,
19
+ } from '@cloudscape-design/components/app-layout';
20
+
21
+ import { matchByPath, useLocation, useNavigate } from '@tanstack/react-router';
22
+ import { Outlet } from '@tanstack/react-router';
23
+
24
+ const getBreadcrumbs = (
25
+ pathName: string,
26
+ search: string,
27
+ defaultBreadcrumb: string,
28
+ availableRoutes?: string[],
29
+ ) => {
30
+ const segments = [
31
+ defaultBreadcrumb,
32
+ ...pathName.split('/').filter((segment) => segment !== ''),
33
+ ];
34
+
35
+ return segments.map((segment, i) => {
36
+ const href =
37
+ i === 0
38
+ ? '/'
39
+ : \`/\${segments
40
+ .slice(1, i + 1)
41
+ .join('/')
42
+ .replace('//', '/')}\`;
43
+
44
+ const matched =
45
+ !availableRoutes || availableRoutes.find((r) => matchByPath(r, href, {}));
46
+
47
+ return {
48
+ href: matched ? \`\${href}\${search}\` : '#',
49
+ text: segment,
50
+ };
51
+ });
52
+ };
53
+
54
+ export interface AppLayoutContext {
55
+ appLayoutProps: AppLayoutProps;
56
+ setAppLayoutProps: (props: AppLayoutProps) => void;
57
+ displayHelpPanel: (helpContent: React.ReactNode) => void;
58
+ }
59
+
60
+ /**
61
+ * Context for updating/retrieving the AppLayout.
62
+ */
63
+ export const AppLayoutContext = createContext({
64
+ appLayoutProps: {},
65
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
66
+ setAppLayoutProps: (_: AppLayoutProps) => {},
67
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
68
+ displayHelpPanel: (_: React.ReactNode) => {},
69
+ });
70
+
71
+ /**
72
+ * Defines the App layout and contains logic for routing.
73
+ */
74
+ const AppLayout: React.FC = () => {
75
+ const { user, removeUser, signoutRedirect, clearStaleState } = useAuth();
76
+ const navigate = useNavigate();
77
+ const appLayout = React.useRef<AppLayoutProps.Ref>(null);
78
+ const [activeBreadcrumbs, setActiveBreadcrumbs] = useState<
79
+ BreadcrumbGroupProps.Item[]
80
+ >([{ text: '/', href: '/' }]);
81
+ const [appLayoutProps, setAppLayoutProps] = useState<AppLayoutProps>({});
82
+ const { pathname, search } = useLocation();
83
+ const setAppLayoutPropsSafe = useCallback(
84
+ (props: AppLayoutProps) => {
85
+ JSON.stringify(appLayoutProps) !== JSON.stringify(props) &&
86
+ setAppLayoutProps(props);
87
+ },
88
+ [appLayoutProps],
89
+ );
90
+ useEffect(() => {
91
+ const breadcrumbs = getBreadcrumbs(
92
+ pathname,
93
+ Object.entries(search).reduce((p, [k, v]) => p + \`\${k}=\${v}\`, ''),
94
+ '/',
95
+ );
96
+ setActiveBreadcrumbs(breadcrumbs);
97
+ }, [pathname, search]);
98
+ const onNavigate = useCallback(
99
+ (
100
+ e: CustomEvent<{
101
+ href: string;
102
+ external?: boolean;
103
+ }>,
104
+ ) => {
105
+ if (!e.detail.external) {
106
+ e.preventDefault();
107
+ setAppLayoutPropsSafe({
108
+ contentType: undefined,
109
+ });
110
+ navigate({ to: e.detail.href });
111
+ }
112
+ },
113
+ [navigate, setAppLayoutPropsSafe],
114
+ );
115
+ return (
116
+ <AppLayoutContext.Provider
117
+ value={{
118
+ appLayoutProps,
119
+ setAppLayoutProps: setAppLayoutPropsSafe,
120
+ displayHelpPanel: (helpContent: React.ReactNode) => {
121
+ setAppLayoutPropsSafe({ tools: helpContent, toolsHide: false });
122
+ appLayout.current?.openTools();
123
+ },
124
+ }}
125
+ >
126
+ <TopNavigation
127
+ identity={{
128
+ href: '/',
129
+ title: Config.applicationName,
130
+ logo: {
131
+ src: Config.logo,
132
+ },
133
+ }}
134
+ utilities={[
135
+ {
136
+ type: 'menu-dropdown',
137
+ text: \`\${user?.profile?.['cognito:username']}\`,
138
+ iconName: 'user-profile-active',
139
+ onItemClick: (e) => {
140
+ if (e.detail.id === 'signout') {
141
+ removeUser();
142
+ signoutRedirect({
143
+ post_logout_redirect_uri: window.location.origin,
144
+ extraQueryParams: {
145
+ redirect_uri: window.location.origin,
146
+ response_type: 'code',
147
+ },
148
+ });
149
+ clearStaleState();
150
+ }
151
+ },
152
+ items: [{ id: 'signout', text: 'Sign out' }],
153
+ },
154
+ ]}
155
+ />
156
+ <CloudscapeAppLayout
157
+ ref={appLayout}
158
+ breadcrumbs={
159
+ <BreadcrumbGroup onFollow={onNavigate} items={activeBreadcrumbs} />
160
+ }
161
+ toolsHide
162
+ navigation={
163
+ <SideNavigation
164
+ header={{ text: Config.applicationName, href: '/' }}
165
+ activeHref={pathname}
166
+ onFollow={onNavigate}
167
+ items={NavItems}
168
+ />
169
+ }
170
+ content={<Outlet />}
171
+ {...appLayoutProps}
172
+ />
173
+ </AppLayoutContext.Provider>
174
+ );
175
+ };
176
+
177
+ export default AppLayout;
178
+ "
179
+ `;
180
+
181
+ exports[`cognito-auth generator uxProvider tests > None > should update AppLayout > app-layout-with-auth 1`] = `
182
+ "import { useAuth } from 'react-oidc-context';
183
+ import * as React from 'react';
184
+
185
+ import { useEffect, useMemo, useState } from 'react';
186
+
187
+ import Config from '../../config';
188
+ import { Link, useLocation, useMatchRoute } from '@tanstack/react-router';
189
+
190
+ const getBreadcrumbs = (
191
+ matchRoute: ReturnType<typeof useMatchRoute>,
192
+ pathName: string,
193
+ search: string,
194
+ defaultBreadcrumb: string,
195
+ availableRoutes?: string[],
196
+ ) => {
197
+ const segments = [
198
+ defaultBreadcrumb,
199
+ ...pathName.split('/').filter((segment) => segment !== ''),
200
+ ];
201
+
202
+ return segments.map((segment, i) => {
203
+ const href =
204
+ i === 0
205
+ ? '/'
206
+ : \`/\${segments
207
+ .slice(1, i + 1)
208
+ .join('/')
209
+ .replace('//', '/')}\`;
210
+
211
+ const matched =
212
+ !availableRoutes || availableRoutes.find((r) => matchRoute({ to: href }));
213
+
214
+ return {
215
+ href: matched ? \`\${href}\${search}\` : '#',
216
+ text: segment,
217
+ };
218
+ });
219
+ };
220
+
221
+ /**
222
+ * Defines the App layout and contains logic for routing.
223
+ */
224
+ const AppLayout: React.FC<React.PropsWithChildren> = ({ children }) => {
225
+ const { user, removeUser, signoutRedirect, clearStaleState } = useAuth();
226
+ const { user, removeUser, signoutRedirect, clearStaleState } = useAuth();
227
+ const [activeBreadcrumbs, setActiveBreadcrumbs] = useState<
228
+ {
229
+ href: string;
230
+ text: string;
231
+ }[]
232
+ >([{ text: '/', href: '/' }]);
233
+ const matchRoute = useMatchRoute();
234
+ const { pathname, search } = useLocation();
235
+ const navItems = useMemo(() => [{ to: '/', label: 'Home' }], []);
236
+ useEffect(() => {
237
+ const breadcrumbs = getBreadcrumbs(
238
+ matchRoute,
239
+ pathname,
240
+ Object.entries(search).reduce((p, [k, v]) => p + \`\${k}=\${v}\`, ''),
241
+ 'Home',
242
+ );
243
+ setActiveBreadcrumbs(breadcrumbs);
244
+ }, [matchRoute, pathname, search]);
245
+ return (
246
+ <div className="app-shell">
247
+ <header className="app-header">
248
+ <div className="app-header-inner">
249
+ <div className="brand">
250
+ <a href="/">
251
+ <img
252
+ src={Config.logo}
253
+ alt={\`\${Config.applicationName} logo\`}
254
+ className="brand-logo"
255
+ />
256
+ <span className="brand-name">{Config.applicationName}</span>
257
+ </a>
258
+ </div>
259
+
260
+ <nav className="app-nav">
261
+ {navItems.map((item) => (
262
+ <Link
263
+ key={item.to}
264
+ to={item.to}
265
+ className={pathname === item.to ? 'active' : undefined}
266
+ >
267
+ {item.label}
268
+ </Link>
269
+ ))}
270
+ </nav>
271
+ <div className="user-greeting">
272
+ <span>Hi, {\`\${user?.profile?.['cognito:username']}\`}</span>
273
+ <button
274
+ type="button"
275
+ className="signout-link"
276
+ onClick={() => {
277
+ removeUser();
278
+ signoutRedirect({
279
+ post_logout_redirect_uri: window.location.origin,
280
+ extraQueryParams: {
281
+ redirect_uri: window.location.origin,
282
+ response_type: 'code',
283
+ },
284
+ });
285
+ clearStaleState();
286
+ }}
287
+ >
288
+ Sign out
289
+ </button>
290
+ </div>
291
+ <div className="user-greeting">
292
+ <span>Hi, {\`\${user?.profile?.['cognito:username']}\`}</span>
293
+ <button
294
+ type="button"
295
+ className="signout-link"
296
+ onClick={() => {
297
+ removeUser();
298
+ signoutRedirect({
299
+ post_logout_redirect_uri: window.location.origin,
300
+ extraQueryParams: {
301
+ redirect_uri: window.location.origin,
302
+ response_type: 'code',
303
+ },
304
+ });
305
+ clearStaleState();
306
+ }}
307
+ >
308
+ Sign out
309
+ </button>
310
+ </div>
311
+ </div>
312
+ </header>
313
+ <main className="app-main">
314
+ <nav className="breadcrumbs" aria-label="Breadcrumb">
315
+ {activeBreadcrumbs.map((crumb, index) => (
316
+ <span className="breadcrumb-segment" key={crumb.href || index}>
317
+ {index > 0 && <span className="breadcrumb-separator">/</span>}
318
+ {index === activeBreadcrumbs.length - 1 ? (
319
+ <span className="breadcrumb-current">{crumb.text}</span>
320
+ ) : (
321
+ <Link to={crumb.href}>{crumb.text}</Link>
322
+ )}
323
+ </span>
324
+ ))}
325
+ </nav>
326
+
327
+ <section className="card">{children}</section>
328
+ </main>
329
+ </div>
330
+ );
331
+ };
332
+
333
+ export default AppLayout;
334
+ "
335
+ `;
@@ -1,6 +1,6 @@
1
1
  import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
2
  import { ChevronRight, MoreHorizontal } from "lucide-react"
3
+ import { Slot } from "radix-ui"
4
4
 
5
5
  import { cn } from "<%= scopeAlias %>common-shadcn/lib/utils"
6
6
 
@@ -38,7 +38,7 @@ function BreadcrumbLink({
38
38
  }: React.ComponentProps<"a"> & {
39
39
  asChild?: boolean
40
40
  }) {
41
- const Comp = asChild ? Slot : "a"
41
+ const Comp = asChild ? Slot.Root : "a"
42
42
 
43
43
  return (
44
44
  <Comp
@@ -1,6 +1,6 @@
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 "<%= scopeAlias %>common-shadcn/lib/utils"
6
6
 
@@ -22,9 +22,11 @@ const buttonVariants = cva(
22
22
  },
23
23
  size: {
24
24
  default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
25
26
  sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
27
  lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
28
  icon: "size-9",
29
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
28
30
  "icon-sm": "size-8",
29
31
  "icon-lg": "size-10",
30
32
  },
@@ -46,7 +48,7 @@ function Button({
46
48
  VariantProps<typeof buttonVariants> & {
47
49
  asChild?: boolean
48
50
  }) {
49
- const Comp = asChild ? Slot : "button"
51
+ const Comp = asChild ? Slot.Root : "button"
50
52
 
51
53
  return (
52
54
  <Comp
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import * as SeparatorPrimitive from "@radix-ui/react-separator"
4
+ import { Separator as SeparatorPrimitive } from "radix-ui"
5
5
 
6
6
  import { cn } from "<%= scopeAlias %>common-shadcn/lib/utils"
7
7
 
@@ -1,8 +1,8 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from "react"
4
- import * as SheetPrimitive from "@radix-ui/react-dialog"
5
4
  import { XIcon } from "lucide-react"
5
+ import { Dialog as SheetPrimitive } from "radix-ui"
6
6
 
7
7
  import { cn } from "<%= scopeAlias %>common-shadcn/lib/utils"
8
8
 
@@ -48,9 +48,11 @@ function SheetContent({
48
48
  className,
49
49
  children,
50
50
  side = "right",
51
+ showCloseButton = true,
51
52
  ...props
52
53
  }: React.ComponentProps<typeof SheetPrimitive.Content> & {
53
54
  side?: "top" | "right" | "bottom" | "left"
55
+ showCloseButton?: boolean
54
56
  }) {
55
57
  return (
56
58
  <SheetPortal>
@@ -72,10 +74,12 @@ function SheetContent({
72
74
  {...props}
73
75
  >
74
76
  {children}
75
- <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
76
- <XIcon className="size-4" />
77
- <span className="sr-only">Close</span>
78
- </SheetPrimitive.Close>
77
+ {showCloseButton && (
78
+ <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
79
+ <XIcon className="size-4" />
80
+ <span className="sr-only">Close</span>
81
+ </SheetPrimitive.Close>
82
+ )}
79
83
  </SheetPrimitive.Content>
80
84
  </SheetPortal>
81
85
  )