From b4b3c10ca50f37f474dcf3b1e1439dc51d21719e Mon Sep 17 00:00:00 2001 From: Pavel Bublikov Date: Thu, 15 May 2025 14:55:07 +0400 Subject: [PATCH 1/2] fix: migration component Alert --- .../src/content/docs/components/alert.mdx | 164 ++++++++++++++++-- .../ui/src/components/alert/Alert.test.tsx | 12 +- .../src/components/alert/AlertContainer.tsx | 64 ------- .../src/components/alert/AlertDescription.tsx | 2 +- .../ui/src/components/alert/AlertLink.tsx | 41 +++++ .../ui/src/components/alert/AlertRoot.tsx | 140 +++++++++++++++ .../ui/src/components/alert/AlertTitle.tsx | 2 +- packages/ui/src/components/alert/index.ts | 10 +- .../src/views/auth/forgot-password-page.tsx | 4 +- .../ui/src/views/auth/new-password-page.tsx | 4 +- packages/ui/src/views/auth/otp-page.tsx | 4 +- packages/ui/src/views/auth/signup-page.tsx | 4 +- .../connectors/connector-entity-form.tsx | 4 +- .../connector-reference.tsx | 4 +- .../delegate-selector-form.tsx | 4 +- .../ui/src/views/labels/label-form-page.tsx | 4 +- .../create-pipeline-dialog.tsx | 4 +- .../profile-settings-keys-create-dialog.tsx | 4 +- .../profile-settings-token-create-dialog.tsx | 4 +- .../profile-settings-general-page.tsx | 4 +- .../profile-settings-keys-page.tsx | 4 +- .../project-general/project-general-page.tsx | 4 +- .../components/invite-member-dialog.tsx | 4 +- .../conversation/pull-request-panel.tsx | 4 +- .../pull-request-conversation-page.tsx | 4 +- .../components/create-branch-dialog.tsx | 4 +- .../ui/src/views/repo/repo-create/index.tsx | 4 +- .../components/clone-repo-dialog.tsx | 4 +- .../create-tag/create-tag-dialog.tsx | 4 +- .../gcp-regions-multiselect-view.tsx | 4 +- .../secrets/create-secret/create-secret.tsx | 4 +- .../src/views/secrets/secret-entity-form.tsx | 4 +- packages/ui/tailwind-design-system.ts | 2 + .../tailwind-utils-config/components/alert.ts | 143 +++++++++++++++ .../tailwind-utils-config/components/index.ts | 1 + 35 files changed, 540 insertions(+), 137 deletions(-) delete mode 100644 packages/ui/src/components/alert/AlertContainer.tsx create mode 100644 packages/ui/src/components/alert/AlertLink.tsx create mode 100644 packages/ui/src/components/alert/AlertRoot.tsx create mode 100644 packages/ui/tailwind-utils-config/components/alert.ts diff --git a/apps/portal/src/content/docs/components/alert.mdx b/apps/portal/src/content/docs/components/alert.mdx index 665ae2ec97..4adaffe899 100644 --- a/apps/portal/src/content/docs/components/alert.mdx +++ b/apps/portal/src/content/docs/components/alert.mdx @@ -4,21 +4,56 @@ description: Alert component beta: true --- -The `Alert` component provides a flexible UI element for displaying alert messages. It is composed of several subcomponents such as `AlertContainer`, `AlertTitle`, and `AlertDescription` to offer a structured and customizable interface. +The `Alert` component provides a flexible UI element for displaying alert messages. It is composed of several subcomponents such as `AlertRoot`, `AlertTitle`, `AlertDescription`, and `AlertLink` to offer a structured and customizable interface. import { DocsPage } from "@/components/docs-page"; - - Error Alert - This is an error alert description. - - - Warning Alert - This is a warning alert description. - + + Alert Title + This is an alert description. + + + We couldn’t complete your request because it violates an Open Policy Agent (OPA) rule set by your organization. Please review the applicable policies or contact your administrator for clarification, then try creating the feature flag again. + + + Warning Alert + This is a warning alert description. + + + Dismissible Alert + + + Link Alert + Learn more + + + Link Alert as child + + + + Crowded Alert not expandable + LoremLorem ipsum dolor sit amet consectetur adipisicing elit. Unde molestiae hic, iste nostrum, quia eaque amet autem optio rem earum nihil rerum illo! Possimus + + + Crowded Alert + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. + Learn more + + + Crowded Alert + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. + + + Crowded Alert + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. + + + Expandable but not crowded + LoremLorem ipsum dolor sit amet consectetur adipisicing elit. Unde molestiae hic + `} /> @@ -28,10 +63,24 @@ import { DocsPage } from "@/components/docs-page"; import { Alert } from '@harnessio/ui/components' return ( - - Alert Title - This is the alert description. - + + My Alert + + This is the description of the alert + + + Learn more + + ) ``` @@ -40,17 +89,17 @@ return ( All parts of the `Alert` component can be imported and composed as required. ```typescript jsx - + - + ``` ## API Reference -### Container +### Root -The `Container` component serves as the main container for all alert elements. +The `Root` component serves as the main container for all alert elements. void", + defaultValue: "", }, ]} /> @@ -119,3 +202,50 @@ The `Description` component displays the description of the alert. }, ]} /> + +### Link + +The `Link` component is used to create a link within the alert. + +```typescript jsx +Learn more +``` + + diff --git a/packages/ui/src/components/alert/Alert.test.tsx b/packages/ui/src/components/alert/Alert.test.tsx index bf9d92e730..dd1167e168 100644 --- a/packages/ui/src/components/alert/Alert.test.tsx +++ b/packages/ui/src/components/alert/Alert.test.tsx @@ -2,18 +2,26 @@ import { render, screen } from '@testing-library/react' import { Alert } from './' +beforeAll(() => { + global.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + } +}) + describe('Alert', () => { test('it should display the alert title and description', async () => { const title = 'TEST TITLE' const description = 'TEST DESCRIPTION' render( - + {title}

{description}

-
+ ) expect(await screen.findByRole('heading', { name: title })).toBeInTheDocument() diff --git a/packages/ui/src/components/alert/AlertContainer.tsx b/packages/ui/src/components/alert/AlertContainer.tsx deleted file mode 100644 index 2f02dfa3ac..0000000000 --- a/packages/ui/src/components/alert/AlertContainer.tsx +++ /dev/null @@ -1,64 +0,0 @@ -// export interface AlertContainerProps extends PropsWithChildren> { -// className?: string -// } - -// export const AlertContainer = forwardRef( -// ({ className, variant, children }, ref) => ( -//
-// {children} -//
-// ) -// ) - -import { forwardRef, PropsWithChildren, useState } from 'react' - -import { cn } from '@utils/cn' -import { cva, type VariantProps } from 'class-variance-authority' - -import { Icon } from '../icon' - -const alertVariants = cva( - 'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-cn-foreground [&>svg~*]:pl-7', - { - variants: { - variant: { - default: 'bg-cn-background text-cn-foreground', - destructive: - 'border-cn-borders-danger/50 text-cn-foreground-danger dark:border-cn-borders-danger [&>svg]:text-cn-foreground-danger' - } - }, - defaultVariants: { - variant: 'default' - } - } -) - -export interface AlertContainerProps extends PropsWithChildren> { - className?: string - closable?: boolean -} - -export const AlertContainer = forwardRef( - ({ className, variant, children, closable }, ref) => { - const [isVisible, setIsVisible] = useState(true) - - if (!isVisible) return null - - return ( -
- {children} - {closable && ( - - )} -
- ) - } -) - -AlertContainer.displayName = 'AlertContainer' diff --git a/packages/ui/src/components/alert/AlertDescription.tsx b/packages/ui/src/components/alert/AlertDescription.tsx index e0d0124d93..7ac719d8aa 100644 --- a/packages/ui/src/components/alert/AlertDescription.tsx +++ b/packages/ui/src/components/alert/AlertDescription.tsx @@ -7,7 +7,7 @@ export interface AlertDescriptionProps extends PropsWithChildren(({ className, children }, ref) => ( -
+
{children}
)) diff --git a/packages/ui/src/components/alert/AlertLink.tsx b/packages/ui/src/components/alert/AlertLink.tsx new file mode 100644 index 0000000000..41f3490831 --- /dev/null +++ b/packages/ui/src/components/alert/AlertLink.tsx @@ -0,0 +1,41 @@ +import { forwardRef } from 'react' + +import { Link, LinkProps } from '@components/link' +import { Slot } from '@radix-ui/react-slot' +import { cn } from '@utils/cn' + +export interface AlertLinkProps extends LinkProps { + external?: boolean + asChild?: boolean +} + +export const AlertLink = forwardRef( + ({ external = false, asChild = false, children, className, ...linkProps }, ref) => { + if (asChild) { + return ( +
+ {children} +
+ ) + } + + const externalProps = external ? { target: '_blank', rel: 'noopener noreferrer' } : {} + + return ( +
+ + {children} + +
+ ) + } +) + +AlertLink.displayName = 'AlertLink' diff --git a/packages/ui/src/components/alert/AlertRoot.tsx b/packages/ui/src/components/alert/AlertRoot.tsx new file mode 100644 index 0000000000..25cb61b47a --- /dev/null +++ b/packages/ui/src/components/alert/AlertRoot.tsx @@ -0,0 +1,140 @@ +import { forwardRef, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react' + +import { Button } from '@components/button' +import { Icon, IconNameMap } from '@components/icon' +import { useResizeObserver } from '@hooks/use-resize-observer' +import { cn } from '@utils/cn' +import { cva, type VariantProps } from 'class-variance-authority' + +const alertVariants = cva('cn-alert', { + variants: { + theme: { + info: 'cn-alert-info', + danger: 'cn-alert-danger', + warning: 'cn-alert-warning' + } + }, + defaultVariants: { + theme: 'info' + } +}) + +const iconMap: Record['theme']>, keyof typeof IconNameMap> = { + info: 'info-circle', + danger: 'cross-circle', + warning: 'warning-triangle-outline' +} + +const MAX_HEIGHT = 60 + +export interface AlertRootProps extends PropsWithChildren> { + className?: string + dismissible?: boolean + onDismiss?: () => void + expandable?: boolean +} + +export const AlertRoot = forwardRef( + ({ className, theme, children, dismissible, onDismiss, expandable }, ref) => { + const [isVisible, setIsVisible] = useState(true) + const [isExpanded, setIsExpanded] = useState(false) + const [isOverflowing, setIsOverflowing] = useState(false) + const contentRef = useRef(null) + + const handleDismiss = () => { + setIsVisible(false) + onDismiss?.() + } + + const iconName: keyof typeof IconNameMap = iconMap[theme ?? 'info'] + + const toggleExpand = () => setIsExpanded(prev => !prev) + + const shouldShowButton = expandable && isOverflowing + + useResizeObserver( + contentRef, + el => { + if (!el) return + const { scrollHeight } = el + setIsOverflowing(scrollHeight > MAX_HEIGHT) + }, + 100 + ) + + useLayoutEffect(() => { + if (!expandable || !contentRef.current) return + + const el = contentRef.current + setIsOverflowing(el.scrollHeight > MAX_HEIGHT) + }, [children, expandable]) + + if (!isVisible) return null + + return ( +
+ {dismissible && ( + + )} + + + +
+
+
+ {children} + + {shouldShowButton && ( +
+ )} +
+
+ + {shouldShowButton && ( + + )} +
+
+ ) + } +) + +AlertRoot.displayName = 'AlertRoot' diff --git a/packages/ui/src/components/alert/AlertTitle.tsx b/packages/ui/src/components/alert/AlertTitle.tsx index e66048a61b..ea19eef3bb 100644 --- a/packages/ui/src/components/alert/AlertTitle.tsx +++ b/packages/ui/src/components/alert/AlertTitle.tsx @@ -7,7 +7,7 @@ export interface AlertTitleProps extends PropsWithChildren(({ className, children }, ref) => ( -
+
{children}
)) diff --git a/packages/ui/src/components/alert/index.ts b/packages/ui/src/components/alert/index.ts index e2696974bd..461b874cb2 100644 --- a/packages/ui/src/components/alert/index.ts +++ b/packages/ui/src/components/alert/index.ts @@ -1,11 +1,13 @@ -import { AlertContainer, type AlertContainerProps } from './AlertContainer' import { AlertDescription, type AlertDescriptionProps } from './AlertDescription' +import { AlertLink, type AlertLinkProps } from './AlertLink' +import { AlertRoot, type AlertRootProps } from './AlertRoot' import { AlertTitle, type AlertTitleProps } from './AlertTitle' -export { AlertContainerProps, AlertDescriptionProps, AlertTitleProps } +export { AlertRootProps, AlertDescriptionProps, AlertTitleProps, AlertLinkProps } export const Alert = { - Container: AlertContainer, + Root: AlertRoot, Title: AlertTitle, - Description: AlertDescription + Description: AlertDescription, + Link: AlertLink } diff --git a/packages/ui/src/views/auth/forgot-password-page.tsx b/packages/ui/src/views/auth/forgot-password-page.tsx index b3a6b31305..6d2c5673b6 100644 --- a/packages/ui/src/views/auth/forgot-password-page.tsx +++ b/packages/ui/src/views/auth/forgot-password-page.tsx @@ -85,9 +85,9 @@ export function ForgotPasswordPage({ isLoading, onSubmit, error }: ForgotPasswor
{serverError && ( - + {serverError} - + )}
diff --git a/packages/ui/src/views/auth/new-password-page.tsx b/packages/ui/src/views/auth/new-password-page.tsx index 0000500765..dbd5632feb 100644 --- a/packages/ui/src/views/auth/new-password-page.tsx +++ b/packages/ui/src/views/auth/new-password-page.tsx @@ -93,9 +93,9 @@ export function NewPasswordPage({ isLoading, handleFormSubmit, error }: NewPassw
{serverError && ( - + {serverError} - + )}
diff --git a/packages/ui/src/views/auth/otp-page.tsx b/packages/ui/src/views/auth/otp-page.tsx index 579024437c..f60e64bd17 100644 --- a/packages/ui/src/views/auth/otp-page.tsx +++ b/packages/ui/src/views/auth/otp-page.tsx @@ -99,9 +99,9 @@ export function OTPPage({
{serverError && ( - + {serverError} - + )}
{/* TODO: Design system: Replace with FormWrapper once OTP component is ready*/} diff --git a/packages/ui/src/views/auth/signup-page.tsx b/packages/ui/src/views/auth/signup-page.tsx index 481edc0022..98d41f3ac5 100644 --- a/packages/ui/src/views/auth/signup-page.tsx +++ b/packages/ui/src/views/auth/signup-page.tsx @@ -114,9 +114,9 @@ export function SignUpPage({ isLoading, handleSignUp, error }: SignUpPageProps) {...register('confirmPassword', { onChange: handleInputChange })} /> {serverError && ( - + {serverError} - + )}
{tokenGenerationError && ( - + {tokenGenerationError} - + )} ) : ( diff --git a/packages/ui/src/views/repo/repo-tags/components/create-tag/create-tag-dialog.tsx b/packages/ui/src/views/repo/repo-tags/components/create-tag/create-tag-dialog.tsx index eb94334df8..806e5b162e 100644 --- a/packages/ui/src/views/repo/repo-tags/components/create-tag/create-tag-dialog.tsx +++ b/packages/ui/src/views/repo/repo-tags/components/create-tag/create-tag-dialog.tsx @@ -110,11 +110,11 @@ export const CreateTagDialog: FC = ({ {error && ( - + {t('views:repos.error', 'Error:')} {error} - + )} diff --git a/packages/ui/src/views/secrets/components/gcp-regions-multiselect-view.tsx b/packages/ui/src/views/secrets/components/gcp-regions-multiselect-view.tsx index 29a5d46b35..f7a99a1f02 100644 --- a/packages/ui/src/views/secrets/components/gcp-regions-multiselect-view.tsx +++ b/packages/ui/src/views/secrets/components/gcp-regions-multiselect-view.tsx @@ -59,9 +59,9 @@ export function GcpRegionsMultiSelect(props: GcpRegionsMultiSelectProps): React. {isLoading ? ( ) : error ? ( - + {error?.toString() || 'Failed to fetch regions'} - + ) : ( {apiError && ( - + {apiError?.toString()} - + )}
diff --git a/packages/ui/src/views/secrets/secret-entity-form.tsx b/packages/ui/src/views/secrets/secret-entity-form.tsx index e6a727148c..7388ca7a57 100644 --- a/packages/ui/src/views/secrets/secret-entity-form.tsx +++ b/packages/ui/src/views/secrets/secret-entity-form.tsx @@ -82,9 +82,9 @@ export const SecretEntityForm = forwardRef {apiError && ( - + {apiError.toString()} - + )} {!isDrawer && ( diff --git a/packages/ui/tailwind-design-system.ts b/packages/ui/tailwind-design-system.ts index 646a34f8ee..e5c979c82d 100644 --- a/packages/ui/tailwind-design-system.ts +++ b/packages/ui/tailwind-design-system.ts @@ -5,6 +5,7 @@ import type { PluginAPI, Config as TailwindConfig } from 'tailwindcss/types/conf import { accordionStyles, + alertStyles, avatarStyles, badgeStyles, buttonStyles, @@ -433,6 +434,7 @@ export default { plugin(({ addComponents }) => { addComponents([ accordionStyles, + alertStyles, badgeStyles, buttonStyles, dialogStyles, diff --git a/packages/ui/tailwind-utils-config/components/alert.ts b/packages/ui/tailwind-utils-config/components/alert.ts new file mode 100644 index 0000000000..4d81bd08da --- /dev/null +++ b/packages/ui/tailwind-utils-config/components/alert.ts @@ -0,0 +1,143 @@ +import { CSSRuleObject } from 'tailwindcss/types/config' + +const themes = ['info', 'danger', 'warning'] as const + +const themeStyleMapper: Record<(typeof themes)[number], { backgroundColor: string; color: string }> = { + info: { + backgroundColor: 'gray-soft', + color: 'text-2' + }, + danger: { + backgroundColor: 'red-soft', + color: 'text-danger' + }, + warning: { + backgroundColor: 'yellow-soft', + color: 'text-warning' + } +} + +function createAlertVariantStyles() { + const combinationStyles: CSSRuleObject = {} + + themes.forEach(theme => { + const style: CSSRuleObject = {} + const { backgroundColor, color } = themeStyleMapper[theme as keyof typeof themeStyleMapper] + + style[`backgroundColor`] = `var(--cn-set-${backgroundColor}-bg)` + style[`> .cn-alert-icon`] = { + color: `var(--cn-${color})` + } + + combinationStyles[`&:where(.cn-alert-${theme})`] = style + }) + + return combinationStyles +} + +export default { + '.cn-alert': { + gap: 'var(--cn-alert-gap)', + paddingTop: 'var(--cn-alert-py)', + paddingBottom: 'var(--cn-alert-py)', + paddingLeft: 'var(--cn-alert-pl)', + paddingRight: 'var(--cn-alert-pr)', + borderRadius: 'var(--cn-alert-radius)', + minWidth: 'var(--cn-alert-min-width)', + backgroundColor: 'var(--cn-set-gray-soft-bg)', + color: 'var(--cn-text-1)', + '@apply w-full flex relative': '', + + '&-content-box': { + background: 'inherit', + transition: 'grid-template-rows 0.2s ease-out', + '@apply grid grid-rows-[0fr]': '' + }, + + '&-content': { + background: 'inherit', + gap: 'var(--cn-spacing-1)', + '@apply grid relative': '' + }, + + '&-content-expanded': { + '@apply grid-rows-[1fr]': '' + }, + + '&-min-h-content': { + '@apply min-h-[60px]': '' + }, + + '&-text-wrap': { + background: 'inherit', + gap: 'var(--cn-spacing-3)', + '@apply grid justify-items-start': '' + }, + + '&-fade-overlay': { + background: 'inherit', + maskImage: 'linear-gradient(to top, black, transparent)', + visibility: 'visible', + opacity: '1', + transition: 'opacity 0.2s linear', + '@apply absolute inset-0 pointer-events-none': '', + + '&-not-visible': { + visibility: 'hidden', + opacity: '0', + transition: 'visibility 0s 2s, opacity 0.2s linear' + } + }, + + '&-rotate-180': { + transform: 'rotate(180deg)', + transition: 'transform 0.2s ease-out' + }, + + '&-overflow': { + overflow: 'hidden' + }, + + '&-title': { + '@apply font-body-strong': '' + }, + + '&-description': { + '@apply font-body-normal break-words': '' + }, + + '&-close-button': { + '&-icon': { + flexShrink: '0', + width: 'var(--cn-icon-size-sm)', + height: 'var(--cn-icon-size-sm)' + }, + + '&:where(:focus-visible)': { + // TODO: remove !important after fixing the cn-button:focus-visible + position: 'absolute !important' + }, + '@apply absolute right-2 top-2': '' + }, + + '&-expand-button': { + '&-icon': { + width: 'var(--cn-icon-size-default)', + height: 'var(--cn-icon-size-default)' + } + }, + + '&-icon': { + flexShrink: '0', + width: 'var(--cn-icon-size-md)', + height: 'var(--cn-icon-size-md)', + marginTop: 'var(--cn-spacing-px)' + }, + + '&-link-wrapper': { + paddingTop: 'var(--cn-spacing-1)' + }, + + ...createAlertVariantStyles() + } +} diff --git a/packages/ui/tailwind-utils-config/components/index.ts b/packages/ui/tailwind-utils-config/components/index.ts index 1153d64263..f5bb36bd96 100644 --- a/packages/ui/tailwind-utils-config/components/index.ts +++ b/packages/ui/tailwind-utils-config/components/index.ts @@ -1,4 +1,5 @@ export { default as accordionStyles } from './accordion' +export { default as alertStyles } from './alert' export { default as badgeStyles } from './badge' export { default as buttonStyles } from './button' export { default as dialogStyles } from './dialog' From b0b743cb0a9026f2ed36f8ac48c24cac57df137f Mon Sep 17 00:00:00 2001 From: Pavel Bublikov Date: Fri, 16 May 2025 19:30:55 +0400 Subject: [PATCH 2/2] fix: styled AlertRoot, fix alert.mdx --- .../src/content/docs/components/alert.mdx | 49 +++++++++---------- packages/ui/config/vitest-setup.ts | 8 +++ .../ui/src/components/alert/Alert.test.tsx | 8 --- .../ui/src/components/alert/AlertRoot.tsx | 37 ++++++++++++-- .../tailwind-utils-config/components/alert.ts | 26 +++++----- 5 files changed, 79 insertions(+), 49 deletions(-) diff --git a/apps/portal/src/content/docs/components/alert.mdx b/apps/portal/src/content/docs/components/alert.mdx index 4adaffe899..f9b8f7149d 100644 --- a/apps/portal/src/content/docs/components/alert.mdx +++ b/apps/portal/src/content/docs/components/alert.mdx @@ -4,7 +4,7 @@ description: Alert component beta: true --- -The `Alert` component provides a flexible UI element for displaying alert messages. It is composed of several subcomponents such as `AlertRoot`, `AlertTitle`, `AlertDescription`, and `AlertLink` to offer a structured and customizable interface. +The `Alert` component provides a flexible UI element for displaying alert messages. It is composed of several subcomponents such as `Alert.Root`, `Alert.Title`, `Alert.Description`, and `Alert.Link` to offer a structured and customizable interface. import { DocsPage } from "@/components/docs-page"; @@ -16,7 +16,7 @@ import { DocsPage } from "@/components/docs-page"; This is an alert description. - We couldn’t complete your request because it violates an Open Policy Agent (OPA) rule set by your organization. Please review the applicable policies or contact your administrator for clarification, then try creating the feature flag again. + We couldn’t complete your request because it violates an Open Policy Agent (OPA) rule set by your organization. Please review the applicable policies or contact your administrator for clarification, then try creating the feature flag again. Warning Alert @@ -25,26 +25,26 @@ import { DocsPage } from "@/components/docs-page"; Dismissible Alert - + Link Alert Learn more - + Link Alert as child Crowded Alert not expandable - LoremLorem ipsum dolor sit amet consectetur adipisicing elit. Unde molestiae hic, iste nostrum, quia eaque amet autem optio rem earum nihil rerum illo! Possimus + LoremLorem ipsum dolor sit amet consectetur adipisicing elit. Unde molestiae hic, iste nostrum, quia eaque amet autem optio rem earum nihil rerum illo! Possimus Crowded Alert - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. Learn more Crowded Alert - Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. Crowded Alert @@ -52,7 +52,14 @@ import { DocsPage } from "@/components/docs-page"; Expandable but not crowded - LoremLorem ipsum dolor sit amet consectetur adipisicing elit. Unde molestiae hic + Lorem, ipsum dolor sit amet consectetur adipisicing elit. + + + Crowded title Crowded title Crowded title Crowded title Crowded title Crowded title Crowded title Crowded title Crowded title Crowded title Crowded title + Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque. + + + Without Title Lorem, ipsum dolor sit amet consectetur adipisicing elit. Placeat magnam suscipit fugit officiis dignissimos iusto similique deleniti. Quod odio amet quisquam, blanditiis necessitatibus aliquam cumque beatae debitis sunt nemo perspiciatis a saepe enim voluptas? Incidunt vel officiis quam, corrupti ea aliquid nobis! Soluta accusantium ex, alias quod velit explicabo modi exercitationem eaque incidunt fuga, nihil fugiat voluptatum doloribus repudiandae atque.
`} /> @@ -62,22 +69,19 @@ import { DocsPage } from "@/components/docs-page"; ```typescript jsx import { Alert } from '@harnessio/ui/components' +//... + return ( My Alert This is the description of the alert - + Learn more @@ -92,12 +96,13 @@ All parts of the `Alert` component can be imported and composed as required. + ``` ## API Reference -### Root +### `Root` The `Root` component serves as the main container for all alert elements. @@ -106,17 +111,15 @@ The `Root` component serves as the main container for all alert elements. { name: "children", description: - "You can pass in your `Alert.Title` and `Alert.Description` as children", + "You can pass in your `Alert.Title`, `Alert.Description` and `Alert.Link` as children", required: true, value: "ReactNode", - defaultValue: "", }, { name: "className", description: "Additional class names to apply to the alert container", required: false, value: "string", - defaultValue: "", }, { name: "theme", @@ -124,7 +127,6 @@ The `Root` component serves as the main container for all alert elements. "Theme of the alert. Can be one of 'info', 'warning', or 'danger'.", required: false, value: "'info' | 'warning' | 'danger'", - defaultValue: "'info'", }, { name: "dismissible", @@ -132,7 +134,6 @@ The `Root` component serves as the main container for all alert elements. "If true, the alert can be dismissed by the user. Default is false.", required: false, value: "boolean", - defaultValue: "false", }, { name: "expandable", @@ -140,7 +141,6 @@ The `Root` component serves as the main container for all alert elements. "If true, the alert can be expanded to show more content. Default is false.", required: false, value: "boolean", - defaultValue: "false", }, { name: "onDismiss", @@ -148,12 +148,11 @@ The `Root` component serves as the main container for all alert elements. "Callback function that is called when the alert is dismissed.", required: false, value: "() => void", - defaultValue: "", }, ]} /> -### Title +### `Title` The `Title` component displays the title of the alert. @@ -178,7 +177,7 @@ The `Title` component displays the title of the alert. ]} /> -### Description +### `Description` The `Description` component displays the description of the alert. @@ -203,7 +202,7 @@ The `Description` component displays the description of the alert. ]} /> -### Link +### `Link` The `Link` component is used to create a link within the alert. diff --git a/packages/ui/config/vitest-setup.ts b/packages/ui/config/vitest-setup.ts index 7cd6d9af2b..be6f482d5e 100644 --- a/packages/ui/config/vitest-setup.ts +++ b/packages/ui/config/vitest-setup.ts @@ -2,6 +2,14 @@ import { cleanup } from '@testing-library/react' import '@testing-library/jest-dom' +beforeAll(() => { + global.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + } +}) + afterEach(() => { cleanup() }) diff --git a/packages/ui/src/components/alert/Alert.test.tsx b/packages/ui/src/components/alert/Alert.test.tsx index dd1167e168..05358dffe1 100644 --- a/packages/ui/src/components/alert/Alert.test.tsx +++ b/packages/ui/src/components/alert/Alert.test.tsx @@ -2,14 +2,6 @@ import { render, screen } from '@testing-library/react' import { Alert } from './' -beforeAll(() => { - global.ResizeObserver = class { - observe() {} - unobserve() {} - disconnect() {} - } -}) - describe('Alert', () => { test('it should display the alert title and description', async () => { const title = 'TEST TITLE' diff --git a/packages/ui/src/components/alert/AlertRoot.tsx b/packages/ui/src/components/alert/AlertRoot.tsx index 25cb61b47a..40c509988c 100644 --- a/packages/ui/src/components/alert/AlertRoot.tsx +++ b/packages/ui/src/components/alert/AlertRoot.tsx @@ -1,4 +1,14 @@ -import { forwardRef, PropsWithChildren, useLayoutEffect, useRef, useState } from 'react' +import { + Children, + forwardRef, + isValidElement, + PropsWithChildren, + ReactNode, + useLayoutEffect, + useMemo, + useRef, + useState +} from 'react' import { Button } from '@components/button' import { Icon, IconNameMap } from '@components/icon' @@ -6,6 +16,8 @@ import { useResizeObserver } from '@hooks/use-resize-observer' import { cn } from '@utils/cn' import { cva, type VariantProps } from 'class-variance-authority' +import { AlertTitle } from './AlertTitle' + const alertVariants = cva('cn-alert', { variants: { theme: { @@ -25,7 +37,8 @@ const iconMap: Record['theme']>, warning: 'warning-triangle-outline' } -const MAX_HEIGHT = 60 +const MAX_HEIGHT_WITH_TITLE = 70 +const MAX_HEIGHT_WITHOUT_TITLE = 60 export interface AlertRootProps extends PropsWithChildren> { className?: string @@ -52,6 +65,17 @@ export const AlertRoot = forwardRef( const shouldShowButton = expandable && isOverflowing + const hasTitle = Children.toArray(children).some( + (child: ReactNode) => isValidElement(child) && child?.type === AlertTitle + ) + + const minHContentClassName = useMemo( + () => (hasTitle ? 'cn-alert-min-h-content' : 'cn-alert-min-h-content-no-title'), + [hasTitle] + ) + + const MAX_HEIGHT = useMemo(() => (hasTitle ? MAX_HEIGHT_WITH_TITLE : MAX_HEIGHT_WITHOUT_TITLE), [hasTitle]) + useResizeObserver( contentRef, el => { @@ -81,6 +105,7 @@ export const AlertRoot = forwardRef( variant="transparent" size="sm" iconOnly + aria-label="Close alert" > @@ -92,13 +117,13 @@ export const AlertRoot = forwardRef(
( > {isExpanded ? 'Show less' : 'Show more'} diff --git a/packages/ui/tailwind-utils-config/components/alert.ts b/packages/ui/tailwind-utils-config/components/alert.ts index 4d81bd08da..51a2e7fb52 100644 --- a/packages/ui/tailwind-utils-config/components/alert.ts +++ b/packages/ui/tailwind-utils-config/components/alert.ts @@ -57,7 +57,11 @@ export default { '&-content': { background: 'inherit', gap: 'var(--cn-spacing-1)', - '@apply grid relative': '' + '@apply grid relative': '', + + '&-overflow': { + overflow: 'hidden' + } }, '&-content-expanded': { @@ -65,6 +69,10 @@ export default { }, '&-min-h-content': { + '@apply min-h-[70px]': '' + }, + + '&-min-h-content-no-title': { '@apply min-h-[60px]': '' }, @@ -89,15 +97,6 @@ export default { } }, - '&-rotate-180': { - transform: 'rotate(180deg)', - transition: 'transform 0.2s ease-out' - }, - - '&-overflow': { - overflow: 'hidden' - }, - '&-title': { '@apply font-body-strong': '' }, @@ -123,7 +122,12 @@ export default { '&-expand-button': { '&-icon': { width: 'var(--cn-icon-size-default)', - height: 'var(--cn-icon-size-default)' + height: 'var(--cn-icon-size-default)', + + '&-rotate-180': { + transform: 'rotate(180deg)', + transition: 'transform 0.2s ease-out' + } } },