+ {SECURITY_CONFIG[category].subCategories.map((subCategory: ScanSubCategories) => {
+ // Explicit handling if subcategory is null
+ if (!scanResult[category][subCategory]) {
+ return null
+ }
+
+ const scanFailed: boolean =
+ category === CATEGORIES.IMAGE_SCAN &&
+ getStatusForScanList(scanResult[category][subCategory].list ?? []) === 'Failed'
+
+ const severities =
+ subCategory === SUB_CATEGORIES.MISCONFIGURATIONS
+ ? scanResult[category][subCategory]?.misConfSummary?.status
+ : scanResult[category][subCategory]?.summary?.severities
+
+ return (
+
+ )
+ })}
+
+ )
+
return (
<>
-
+
{Object.keys(SECURITY_CONFIG).map((category: ScanCategories) => {
const categoryFailed: boolean =
category !== CATEGORIES.IMAGE_SCAN &&
(scanResult.codeScan?.status === 'Failed' || scanResult.kubernetesManifest?.status === 'Failed')
- const { scanToolName, scanToolUrl } = getScanToolInfo(category)
-
return (
-
- {SECURITY_CONFIG[category].label}
-
-
- {categoryFailed ? (
-
-
-
- ) : (
-
- {SECURITY_CONFIG[category].subCategories.map((subCategory: ScanSubCategories) => {
- // Explicit handling if subcategory is null
- if (!scanResult[category][subCategory]) {
- return null
- }
-
- const scanFailed: boolean =
- category === CATEGORIES.IMAGE_SCAN &&
- getStatusForScanList(scanResult[category][subCategory].list ?? []) ===
- 'Failed'
-
- const severities =
- subCategory === SUB_CATEGORIES.MISCONFIGURATIONS
- ? scanResult[category][subCategory]?.misConfSummary?.status
- : scanResult[category][subCategory]?.summary?.severities
-
- return (
-
- )
- })}
-
- )}
+ {renderHeader(category)}
+ {renderSecurityCards({ category, categoryFailed })}
)
})}
diff --git a/src/Shared/Components/Security/SecurityDetailsCards/index.tsx b/src/Shared/Components/Security/SecurityDetailsCards/index.tsx
index d7e742d01..beaa34035 100644
--- a/src/Shared/Components/Security/SecurityDetailsCards/index.tsx
+++ b/src/Shared/Components/Security/SecurityDetailsCards/index.tsx
@@ -14,5 +14,6 @@
* limitations under the License.
*/
+export { ReportTabEmptyState } from './ReportTabEmptyState'
export { default as SecurityCard } from './SecurityCard'
export { default as SecurityDetailsCards } from './SecurityDetailsCards'
diff --git a/src/Shared/Components/Security/SecurityDetailsCards/types.tsx b/src/Shared/Components/Security/SecurityDetailsCards/types.tsx
index 31f5b376f..987fd6618 100644
--- a/src/Shared/Components/Security/SecurityDetailsCards/types.tsx
+++ b/src/Shared/Components/Security/SecurityDetailsCards/types.tsx
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import { IconName } from '@Shared/Components/Icon'
+
import { ScanResultDTO, SecurityModalPropsType, SeveritiesDTO } from '../SecurityModal/types'
import { ScanCategories, ScanSubCategories } from '../types'
@@ -28,3 +30,9 @@ export interface SecurityCardProps {
export interface SecurityDetailsCardsProps extends Pick
{
scanResult: ScanResultDTO
}
+
+export type ReportTabEmptyStateProps = {
+ title: string
+ subtitle: string
+ icon?: IconName
+}
diff --git a/src/Shared/Components/Security/SecurityModal/SecurityModal.tsx b/src/Shared/Components/Security/SecurityModal/SecurityModal.tsx
index 4e0c55de6..54b2cc870 100644
--- a/src/Shared/Components/Security/SecurityModal/SecurityModal.tsx
+++ b/src/Shared/Components/Security/SecurityModal/SecurityModal.tsx
@@ -16,8 +16,8 @@
import React, { useState } from 'react'
-import { ReactComponent as ICBack } from '@Icons/ic-caret-left-small.svg'
-import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
+import ICBack from '@Icons/ic-caret-left-small.svg?react'
+import ICClose from '@Icons/ic-close.svg?react'
import {
ClipboardButton,
ErrorScreenManager,
diff --git a/src/Shared/Components/Security/SecurityModal/components/IndexedTextDisplay.tsx b/src/Shared/Components/Security/SecurityModal/components/IndexedTextDisplay.tsx
index b72479055..9115955a2 100644
--- a/src/Shared/Components/Security/SecurityModal/components/IndexedTextDisplay.tsx
+++ b/src/Shared/Components/Security/SecurityModal/components/IndexedTextDisplay.tsx
@@ -16,7 +16,7 @@
import React from 'react'
-import { ReactComponent as ICInfoOutline } from '@Icons/ic-info-outlined.svg'
+import ICInfoOutline from '@Icons/ic-info-outlined.svg?react'
import { ClipboardButton } from '@Common/index'
import { IndexedTextDisplayPropsType } from '../types'
diff --git a/src/Shared/Components/Security/SecurityModal/components/InfoCard.tsx b/src/Shared/Components/Security/SecurityModal/components/InfoCard.tsx
index 086f5268b..c533b28ed 100644
--- a/src/Shared/Components/Security/SecurityModal/components/InfoCard.tsx
+++ b/src/Shared/Components/Security/SecurityModal/components/InfoCard.tsx
@@ -17,7 +17,7 @@
import React from 'react'
import dayjs from 'dayjs'
-import { ReactComponent as ICClock } from '@Icons/ic-clock.svg'
+import ICClock from '@Icons/ic-clock.svg?react'
import { SegmentedBarChart } from '@Common/SegmentedBarChart'
import { ScannedByToolModal } from '@Shared/Components/ScannedByToolModal'
diff --git a/src/Shared/Components/Security/SecurityModal/components/Table.tsx b/src/Shared/Components/Security/SecurityModal/components/Table.tsx
index c857e11b4..d2ff53870 100644
--- a/src/Shared/Components/Security/SecurityModal/components/Table.tsx
+++ b/src/Shared/Components/Security/SecurityModal/components/Table.tsx
@@ -16,7 +16,7 @@
import React, { useMemo, useState } from 'react'
-import { ReactComponent as ICExpand } from '@Icons/ic-expand.svg'
+import ICExpand from '@Icons/ic-expand.svg?react'
import { SortingOrder } from '@Common/Constants'
import { SortableTableHeaderCell } from '@Common/SortableTableHeaderCell'
@@ -120,15 +120,21 @@ const Table: React.FC = ({ headers, rows, defaultSortIndex, hasE
)}
- {row.cells.map((cell, index) => (
-
- {cell.component || cell.cellContent}
-
- ))}
+ {row.cells.map((cell, index) => {
+ const CellComponent =
+ cell.component ?? (typeof cell.cellContent === 'string' ? cell.cellContent : null)
+ return CellComponent ? (
+
+ {CellComponent}
+
+ ) : null
+ })}
{hasExpandableRows && rowExpandedStateArray[rowIndex] && row.expandableComponent}
diff --git a/src/Shared/Components/Security/SecurityModal/config/CodeScan.tsx b/src/Shared/Components/Security/SecurityModal/config/CodeScan.tsx
index fc911e5f0..dbb74f172 100644
--- a/src/Shared/Components/Security/SecurityModal/config/CodeScan.tsx
+++ b/src/Shared/Components/Security/SecurityModal/config/CodeScan.tsx
@@ -43,7 +43,7 @@ import {
stringifySeverities,
} from '../utils'
-export const getCodeScanVulnerabilities = (data: CodeScan['vulnerability'], hidePolicy: boolean) => ({
+export const getCodeScanVulnerabilities = (data: CodeScan['vulnerability'], hidePolicy: boolean): TablePropsType => ({
headers: [
{ headerText: 'cve id', isSortable: false, width: 150 },
{
@@ -107,7 +107,7 @@ export const getCodeScanVulnerabilities = (data: CodeScan['vulnerability'], hide
defaultSortIndex: 1,
})
-export const getCodeScanLicense = (data: CodeScan['license']) => ({
+export const getCodeScanLicense = (data: CodeScan['license']): TablePropsType => ({
headers: [
{ headerText: 'classification', isSortable: false, width: 150 },
{
@@ -242,7 +242,7 @@ export const getCodeScanMisconfigurations = (
status: StatusType['status'],
scanToolName: StatusType['scanToolName'],
scanToolUrl: StatusType['scanToolUrl'],
-) => ({
+): TablePropsType => ({
headers: [
{ headerText: 'file path (relative)', isSortable: true, width: 289 },
{ headerText: 'scan summary', isSortable: true, width: 289, compareFunc: compareSeverities },
@@ -347,7 +347,7 @@ export const getCodeScanExposedSecrets = (
status: StatusType['status'],
scanToolName: StatusType['scanToolName'],
scanToolUrl: StatusType['scanToolUrl'],
-) => ({
+): TablePropsType => ({
headers: [
{ headerText: 'file path (relative)', isSortable: true, width: 372 },
{ headerText: 'scan summary', isSortable: true, width: 372, compareFunc: compareSeverities },
diff --git a/src/Shared/Components/Security/SecurityModal/config/ImageScan.tsx b/src/Shared/Components/Security/SecurityModal/config/ImageScan.tsx
index 1220a2c32..d0652a0b4 100644
--- a/src/Shared/Components/Security/SecurityModal/config/ImageScan.tsx
+++ b/src/Shared/Components/Security/SecurityModal/config/ImageScan.tsx
@@ -16,8 +16,8 @@
import dayjs from 'dayjs'
-import { ReactComponent as ICError } from '@Icons/ic-error-cross.svg'
-import { ReactComponent as ICSuccess } from '@Icons/ic-success.svg'
+import ICError from '@Icons/ic-error-cross.svg?react'
+import ICSuccess from '@Icons/ic-success.svg?react'
import { Progressing } from '@Common/Progressing'
import { DATE_TIME_FORMATS, ZERO_TIME_STRING } from '../../../../../Common/Constants'
@@ -162,7 +162,7 @@ const getVulnerabilitiesData = (
data: ImageScan['vulnerability'],
setDetailViewData: OpenDetailViewButtonProps['setDetailViewData'],
hidePolicy: boolean,
-) => ({
+): TablePropsType => ({
headers: [
{ headerText: 'image', isSortable: false, width: 256 },
{ headerText: 'vulnerability', isSortable: false, width: 256 },
@@ -258,7 +258,7 @@ const getLicenseDetailData = (element: ImageScanLicenseListType) => ({
const getLicenseData = (
data: ImageScan['license'],
setDetailViewData: OpenDetailViewButtonProps['setDetailViewData'],
-) => ({
+): TablePropsType => ({
headers: [
{ headerText: 'image', isSortable: false, width: 256 },
{ headerText: 'risks detected', isSortable: false, width: 256 },
diff --git a/src/Shared/Components/Security/SecurityModal/constants.tsx b/src/Shared/Components/Security/SecurityModal/constants.tsx
index b75cdeb10..7182e686a 100644
--- a/src/Shared/Components/Security/SecurityModal/constants.tsx
+++ b/src/Shared/Components/Security/SecurityModal/constants.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ReactComponent as MechanicalOperation } from '@Icons/ic-mechanical-operation.svg'
+import MechanicalOperation from '@Icons/ic-mechanical-operation.svg?react'
import PageNotFound from '@Images/ic-page-not-found.svg'
import { ScanCategoriesWithLicense } from '../types'
diff --git a/src/Shared/Components/Security/SecurityModal/index.ts b/src/Shared/Components/Security/SecurityModal/index.ts
index 9ddf29487..1ebbe01d9 100644
--- a/src/Shared/Components/Security/SecurityModal/index.ts
+++ b/src/Shared/Components/Security/SecurityModal/index.ts
@@ -22,10 +22,12 @@ export {
} from './config'
export { CATEGORY_LABELS, SEVERITIES_LABEL_COLOR_MAP } from './constants'
export { default as SecurityModal } from './SecurityModal'
-export { getSecurityScan } from './service'
+export { getSecurityScan, getSecurityScanRecommendations } from './service'
export type {
+ DockerScanStatusTypes,
GetResourceScanDetailsPayloadType,
GetResourceScanDetailsResponseType,
+ ScanRecommendationsDTO,
ScanResultDTO,
SidebarDataChildType,
SidebarPropsType,
diff --git a/src/Shared/Components/Security/SecurityModal/service.ts b/src/Shared/Components/Security/SecurityModal/service.ts
index 6ae40c5c0..f4bda1fec 100644
--- a/src/Shared/Components/Security/SecurityModal/service.ts
+++ b/src/Shared/Components/Security/SecurityModal/service.ts
@@ -19,7 +19,7 @@ import { ROUTES } from '@Common/Constants'
import { getUrlWithSearchParams } from '@Common/Helper'
import { ResponseType } from '@Common/Types'
-import { ScanResultDTO, ScanResultParamsType } from './types'
+import { ScanRecommendationsDTO, ScanResultDTO, ScanResultParamsType } from './types'
export const getSecurityScan = async ({
appId,
@@ -39,3 +39,16 @@ export const getSecurityScan = async ({
const response = await get
(url)
return response
}
+
+export const getSecurityScanRecommendations = async ({
+ appId,
+ buildId,
+}): Promise> => {
+ const params = {
+ appId,
+ buildId,
+ }
+ const url = getUrlWithSearchParams(ROUTES.SCAN_RESULT_RECOMMENDATIONS, params)
+ const response = await get(url)
+ return response
+}
diff --git a/src/Shared/Components/Security/SecurityModal/types.ts b/src/Shared/Components/Security/SecurityModal/types.ts
index 38ec60fc8..88bead3bf 100644
--- a/src/Shared/Components/Security/SecurityModal/types.ts
+++ b/src/Shared/Components/Security/SecurityModal/types.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import React from 'react'
+import React, { type JSX } from 'react'
import { Entity } from '@Common/SegmentedBarChart/types'
import { ServerErrors } from '@Common/ServerError'
@@ -252,6 +252,55 @@ export type ScanResultDTO = {
[CATEGORIES.KUBERNETES_MANIFEST]: KubernetesManifest
}
+export type CodeSnippetLine = {
+ line: number
+ content: string
+ isIssue: boolean
+}
+
+export type CodeSnippet = {
+ before: CodeSnippetLine[]
+ current: CodeSnippetLine
+ after: CodeSnippetLine[]
+}
+
+export enum DockerScanStatusTypes {
+ PENDING = 0,
+ RUNNING = 1,
+ COMPLETED = 2,
+ FAILED = 3,
+ SKIPPED = 4,
+}
+
+export interface ScanRecommendationsDTO {
+ severity_summary: {
+ error: number
+ info: number
+ style: number
+ warning: number
+ }
+ results: {
+ code: string
+ file: string
+ line: number
+ level: string
+ title: string
+ message: string
+ severity: string
+ codeSnippet: CodeSnippet
+ documentationUrl: string
+ }[]
+ appId: number
+ buildId: number
+ createdOn: number
+ dockerfileScanEnabled: boolean
+ dockerfileHash: string
+ id: number
+ pipelineId: number
+ status: DockerScanStatusTypes
+ scanEnabled: boolean
+}
+
export interface SidebarPropsType {
modalState: SecurityModalStateType
setModalState: React.Dispatch>
diff --git a/src/Shared/Components/Security/SecurityModal/utils.tsx b/src/Shared/Components/Security/SecurityModal/utils.tsx
index c0a646607..4c5440ac7 100644
--- a/src/Shared/Components/Security/SecurityModal/utils.tsx
+++ b/src/Shared/Components/Security/SecurityModal/utils.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { ReactComponent as NoVulnerability } from '@Icons/ic-vulnerability-not-found.svg'
+import NoVulnerability from '@Icons/ic-vulnerability-not-found.svg?react'
import { SegmentedBarChartProps } from '@Common/SegmentedBarChart'
import { VulnerabilityType } from '@Common/Types'
import { ScannedByToolModal } from '@Shared/Components/ScannedByToolModal'
diff --git a/src/Shared/Components/Security/Vulnerabilities/types.ts b/src/Shared/Components/Security/Vulnerabilities/types.ts
index 12828c331..60908771e 100644
--- a/src/Shared/Components/Security/Vulnerabilities/types.ts
+++ b/src/Shared/Components/Security/Vulnerabilities/types.ts
@@ -19,8 +19,7 @@ import { ImageCardAccordionProps } from '@Shared/Components/ImageCardAccordion/t
import { MaterialSecurityInfoType } from '../../../types'
export interface VulnerabilitiesProps
- extends MaterialSecurityInfoType,
- Pick {
+ extends MaterialSecurityInfoType, Pick {
artifactId: number
applicationId: number
environmentId: number
diff --git a/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx b/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx
index 1971725fd..7581f3df9 100644
--- a/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx
+++ b/src/Shared/Components/SelectPicker/FilterSelectPicker.tsx
@@ -16,8 +16,8 @@
import { useEffect, useMemo, useRef, useState } from 'react'
-import { ReactComponent as ICFilter } from '@Icons/ic-filter.svg'
-import { ReactComponent as ICFilterApplied } from '@Icons/ic-filter-applied.svg'
+import ICFilter from '@Icons/ic-filter.svg?react'
+import ICFilterApplied from '@Icons/ic-filter-applied.svg?react'
import { IS_PLATFORM_MAC_OS } from '@Common/Constants'
import { useRegisterShortcut, UseRegisterShortcutProvider } from '@Common/Hooks'
import { SupportedKeyboardKeysType } from '@Common/Hooks/UseRegisterShortcut/types'
@@ -37,7 +37,7 @@ const FilterSelectPicker = ({
isUserIdentifier,
...props
}: FilterSelectPickerProps) => {
- const selectRef = useRef['selectRef']['current']>()
+ const selectRef = useRef['selectRef']['current']>(null)
const [isMenuOpen, setIsMenuOpen] = useState(menuIsOpen)
const { triggerAutoClickTimestamp, setTriggerAutoClickTimestampToNow, resetTriggerAutoClickTimestamp } =
diff --git a/src/Shared/Components/SelectPicker/GroupedFilterSelectPicker.tsx b/src/Shared/Components/SelectPicker/GroupedFilterSelectPicker.tsx
index aad328598..715763923 100644
--- a/src/Shared/Components/SelectPicker/GroupedFilterSelectPicker.tsx
+++ b/src/Shared/Components/SelectPicker/GroupedFilterSelectPicker.tsx
@@ -14,14 +14,15 @@
* limitations under the License.
*/
-import { KeyboardEvent, useEffect, useRef, useState } from 'react'
+import { KeyboardEvent, useEffect, useMemo, useRef, useState } from 'react'
import { useRegisterShortcut } from '@Common/Hooks'
import { ActionMenu, ActionMenuItemType, ActionMenuProps } from '../ActionMenu'
import { Icon } from '../Icon'
+import { Popover, usePopover } from '../Popover'
import FilterSelectPicker from './FilterSelectPicker'
-import { GroupedFilterSelectPickerProps } from './type'
+import { FilterSelectPickerMapSelectPickerVariant, GroupedFilterSelectPickerProps } from './type'
import './selectPicker.scss'
@@ -41,6 +42,29 @@ export const GroupedFilterSelectPicker = ({
// HOOKS
const { registerShortcut, unregisterShortcut } = useRegisterShortcut()
+ const selectedItemConfig = selectedActionMenuItem ? filterSelectPickerPropsMap[selectedActionMenuItem] : null
+ const isPopOverVariant = selectedItemConfig?.variant === 'popover'
+
+ const {
+ open: isFilterPopoverOpen,
+ triggerProps: filterPopoverTriggerProps,
+ overlayProps: filterPopoverOverlayProps,
+ popoverProps: filterPopoverContentProps,
+ openPopover: openFilterPopover,
+ closePopover: closeFilterPopover,
+ scrollableRef: filterPopoverScrollableRef,
+ } = usePopover({
+ id: `${id}-grouped-filter-popover`,
+ position: isPopOverVariant ? selectedItemConfig.popoverConfig?.position : undefined,
+ alignment: isPopOverVariant ? selectedItemConfig.popoverConfig?.alignment : undefined,
+ width: isPopOverVariant ? selectedItemConfig.popoverConfig?.width : undefined,
+ onOpen: (open) => {
+ if (!open) {
+ setSelectedActionMenuItem(null)
+ }
+ },
+ })
+
useEffect(() => {
const shortcutCallback = () => {
triggerButtonRef.current?.click()
@@ -60,6 +84,12 @@ export const GroupedFilterSelectPicker = ({
}
}, [selectedActionMenuItem])
+ useEffect(() => {
+ if (selectedActionMenuItem && isPopOverVariant) {
+ openFilterPopover()
+ }
+ }, [selectedActionMenuItem])
+
// HANDLERS
const handleMenuItemClick: ActionMenuProps['onClick'] = (item) => {
setSelectedActionMenuItem(item.id)
@@ -76,18 +106,8 @@ export const GroupedFilterSelectPicker = ({
}
}
- return selectedActionMenuItem ? (
-
-
-
- ) : (
-
+ const filterTriggerButton = useMemo(
+ () => (
-
+ ),
+ [isFilterApplied],
)
+
+ const renderContent = () => {
+ if (selectedActionMenuItem && isPopOverVariant) {
+ return (
+
+ {selectedItemConfig.component(closeFilterPopover, filterPopoverScrollableRef)}
+
+ )
+ }
+
+ if (selectedActionMenuItem) {
+ const config = filterSelectPickerPropsMap[selectedActionMenuItem]
+ if (config.variant !== 'popover') {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { variant: _variant, ...filterProps } = config as FilterSelectPickerMapSelectPickerVariant
+ return (
+
+
+
+ )
+ }
+ }
+
+ return (
+
+ {filterTriggerButton}
+
+ )
+ }
+
+ return {renderContent()}
}
diff --git a/src/Shared/Components/SelectPicker/common.tsx b/src/Shared/Components/SelectPicker/common.tsx
index a10bb5335..ed404b87d 100644
--- a/src/Shared/Components/SelectPicker/common.tsx
+++ b/src/Shared/Components/SelectPicker/common.tsx
@@ -28,8 +28,8 @@ import {
ValueContainerProps,
} from 'react-select'
-import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg'
-import { ReactComponent as ICClose } from '@Icons/ic-close.svg'
+import ICCaretDown from '@Icons/ic-caret-down.svg?react'
+import ICClose from '@Icons/ic-close.svg?react'
import { Checkbox } from '@Common/Checkbox'
import { ReactSelectInputAction } from '@Common/Constants'
import { getAlphabetIcon, noop } from '@Common/Helper'
diff --git a/src/Shared/Components/SelectPicker/type.ts b/src/Shared/Components/SelectPicker/type.ts
index e09ce9ee4..705fc2ed0 100644
--- a/src/Shared/Components/SelectPicker/type.ts
+++ b/src/Shared/Components/SelectPicker/type.ts
@@ -29,9 +29,12 @@ import { ComponentSizeType } from '@Shared/constants'
import { ActionMenuProps } from '../ActionMenu'
import { ButtonComponentType, ButtonProps, ButtonVariantType } from '../Button'
import { FormFieldWrapperProps } from '../FormFieldWrapper/types'
+import { UsePopoverProps, UsePopoverReturnType } from '../Popover'
-export interface SelectPickerOptionType
- extends OptionType {
+export interface SelectPickerOptionType extends OptionType<
+ OptionValue,
+ OptionLabel
+> {
/**
* Description to be displayed for the option
*/
@@ -338,7 +341,8 @@ export type SelectPickerGroupHeadingProps = GroupHeadingProps, 'options' | 'isDisabled' | 'placeholder' | 'isLoading'>
>,
Pick<
@@ -373,14 +377,31 @@ export type SelectPickerTextAreaProps = Omit<
> &
Pick
-export interface GroupedFilterSelectPickerProps
- extends Omit<
- ActionMenuProps,
- 'onClick' | 'disableDescriptionEllipsis' | 'children' | 'buttonProps' | 'isSearchable'
- > {
+export type FilterSelectPickerMapSelectPickerVariant = {
+ variant?: 'selectPicker'
+} & Omit
+
+type FilterSelectPickerMapPopOverVariant = {
+ variant: 'popover'
+ /**
+ * Component rendered inside the Popover.
+ * Receives `closePopover` as a prop to allow programmatic closing.
+ */
+ component: (closePopover: () => void, scrollableRef: UsePopoverReturnType['scrollableRef']) => ReactElement
+ /**
+ * Optional positioning config forwarded to usePopover
+ */
+ popoverConfig?: Pick