Skip to content

Commit 3054b79

Browse files
committed
feat: add component hover-card
1 parent 6178edb commit 3054b79

File tree

13 files changed

+259
-1
lines changed

13 files changed

+259
-1
lines changed

packages/ui/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
"main": "./dist/index.js",
1212
"module": "./dist/index.js",
1313
"types": "./dist/index.d.ts",
14-
"files": ["dist"],
14+
"files": [
15+
"dist"
16+
],
1517
"scripts": {
1618
"build": "pnpm run pgk:prod && pnpm run build:components && pnpm run registry",
1719
"build:components": "tsdown",
@@ -32,6 +34,7 @@
3234
"@radix-ui/react-context-menu": "^2.2.15",
3335
"@radix-ui/react-dialog": "^1.1.14",
3436
"@radix-ui/react-dropdown-menu": "^2.1.15",
37+
"@radix-ui/react-hover-card": "^1.1.14",
3538
"@radix-ui/react-label": "2.1.7",
3639
"@radix-ui/react-menu": "^2.1.15",
3740
"@radix-ui/react-popover": "1.1.14",
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Root, Trigger } from '@radix-ui/react-hover-card';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import HoverCardArrow from './HoverCardArrow';
6+
import HoverCardContent from './HoverCardContent';
7+
import type { HoverCardProps } from './types';
8+
9+
const HoverCard = forwardRef<ComponentRef<typeof HoverCardContent>, HoverCardProps>((props, ref) => {
10+
const { arrowProps, children, className, classNames, showArrow, trigger, ...rest } = props;
11+
12+
return (
13+
<Root
14+
data-slot="hover-card-root"
15+
{...rest}
16+
>
17+
<Trigger
18+
asChild
19+
data-slot="hover-card-trigger"
20+
>
21+
{trigger}
22+
</Trigger>
23+
24+
<HoverCardContent
25+
className={className || classNames?.content}
26+
ref={ref}
27+
>
28+
{children}
29+
30+
{showArrow && (
31+
<HoverCardArrow
32+
{...arrowProps}
33+
className={classNames?.arrow}
34+
/>
35+
)}
36+
</HoverCardContent>
37+
</Root>
38+
);
39+
});
40+
41+
HoverCard.displayName = 'HoverCard';
42+
43+
export default HoverCard;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Arrow } from '@radix-ui/react-hover-card';
2+
3+
import { cn } from '@/lib/utils';
4+
5+
import { hoverCardVariants } from './hover-card-variants';
6+
import type { HoverCardArrowProps } from './types';
7+
8+
const HoverCardArrow = (props: HoverCardArrowProps) => {
9+
const { className, size, ...rest } = props;
10+
11+
const { arrow } = hoverCardVariants({ size });
12+
13+
const mergedClass = cn(arrow(), className);
14+
15+
return (
16+
<Arrow
17+
{...rest}
18+
className={mergedClass}
19+
data-slot="hover-card-arrow"
20+
/>
21+
);
22+
};
23+
24+
export default HoverCardArrow;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Content, Portal } from '@radix-ui/react-hover-card';
2+
import type { ComponentRef } from 'react';
3+
import { forwardRef } from 'react';
4+
5+
import { cn } from '@/lib/utils';
6+
7+
import { hoverCardVariants } from './hover-card-variants';
8+
import type { HoverCardContentProps } from './types';
9+
10+
const HoverCardContent = forwardRef<ComponentRef<typeof Content>, HoverCardContentProps>((props, ref) => {
11+
const { align = 'center', className, sideOffset = 8, ...rest } = props;
12+
13+
const { content } = hoverCardVariants();
14+
15+
const mergedCls = cn(content(), className);
16+
17+
return (
18+
<Portal data-slot="hover-card-portal">
19+
<Content
20+
align={align}
21+
className={mergedCls}
22+
data-slot="hover-card-content"
23+
ref={ref}
24+
sideOffset={sideOffset}
25+
{...rest}
26+
/>
27+
</Portal>
28+
);
29+
});
30+
31+
HoverCardContent.displayName = 'HoverCardContent';
32+
33+
export default HoverCardContent;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { tv } from 'tailwind-variants';
2+
3+
export const hoverCardVariants = tv({
4+
slots: {
5+
arrow: 'fill-popover stroke-border',
6+
content: [
7+
`w-auto p-4 rounded-md border bg-popover text-popover-foreground shadow-md outline-none z-50 will-change-transform`,
8+
`data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 data-[state=open]:fade-in-0 data-[state=open]:animate-in`,
9+
`data-[state=closed]:zoom-out-95 data-[state=closed]:fade-out-0 data-[state=closed]:animate-out`,
10+
`data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2`
11+
]
12+
}
13+
});
14+
15+
export type HoverCardSlots = keyof typeof hoverCardVariants.slots;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { Root as HoverCardRoot, Trigger as HoverCardTrigger } from '@radix-ui/react-hover-card';
2+
3+
export { default as HoverCard } from './HoverCard';
4+
export { default as HoverCardArrow } from './HoverCardArrow';
5+
export { default as HoverCardContent } from './HoverCardContent';
6+
7+
export * from './types';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type {
2+
HoverCardArrowProps as _HoverCardArrowProps,
3+
HoverCardContentProps as _HoverCardContentProps,
4+
HoverCardProps as _HoverCardProps
5+
} from '@radix-ui/react-hover-card';
6+
7+
import type { BaseNodeProps, ClassValue } from '@/types/other';
8+
9+
import type { HoverCardSlots } from './hover-card-variants';
10+
11+
export type HoverCardClassNames = Partial<Record<HoverCardSlots, ClassValue>>;
12+
13+
export type HoverCardProps = BaseNodeProps<_HoverCardProps> & {
14+
arrowProps?: HoverCardArrowProps;
15+
classNames?: HoverCardClassNames;
16+
contentProps?: Omit<HoverCardContentProps, 'children' | 'className'>;
17+
showArrow?: boolean;
18+
trigger?: React.ReactNode;
19+
};
20+
21+
export type HoverCardArrowProps = BaseNodeProps<_HoverCardArrowProps>;
22+
23+
export type HoverCardContentProps = BaseNodeProps<_HoverCardContentProps>;

packages/ui/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ export * from './components/chip';
2424

2525
export * from './components/collapsible';
2626

27+
export * from './components/combobox';
28+
2729
export * from './components/command';
2830

2931
export * from './components/config-provider';
@@ -38,6 +40,8 @@ export * from './components/divider';
3840

3941
export * from './components/dropdown-menu';
4042

43+
export * from './components/hover-card';
44+
4145
export * from './components/icon';
4246

4347
export * from './components/keyboard-key';

playground/public/icon.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Avatar, Button, Card, HoverCard } from 'soybean-react-ui';
2+
3+
const HoverCardDefaultDemo = () => {
4+
return (
5+
<Card
6+
split
7+
title="Default"
8+
>
9+
<HoverCard
10+
classNames={{ content: 'w-64' }}
11+
trigger={<Button variant="link">@soybeanjs</Button>}
12+
>
13+
<div className="flex justify-between space-x-4">
14+
<Avatar
15+
fallback="VC"
16+
src="/icon.svg"
17+
/>
18+
19+
<div className="space-y-1">
20+
<h4 className="text-sm font-semibold">@soybeanjs</h4>
21+
<p className="text-sm">SoybeanJS is a front-end technology team, built by Soybean.</p>
22+
</div>
23+
</div>
24+
</HoverCard>
25+
</Card>
26+
);
27+
};
28+
29+
export default HoverCardDefaultDemo;

0 commit comments

Comments
 (0)