Skip to content

Commit

Permalink
✨ feat: add ActionIconGroup component
Browse files Browse the repository at this point in the history
  • Loading branch information
canisminor1990 committed Jun 12, 2023
1 parent fb15ef5 commit eea5bff
Show file tree
Hide file tree
Showing 18 changed files with 813 additions and 8 deletions.
8 changes: 7 additions & 1 deletion src/ActionIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,13 @@ const ActionIcon = memo<ActionIconProps>(
if (!title) return actionIconBlock;

return (
<Tooltip arrow={arrow} placement={placement} title={title}>
<Tooltip
arrow={arrow}
mouseEnterDelay={1}
overlayStyle={{ pointerEvents: 'none' }}
placement={placement}
title={title}
>
{actionIconBlock}
</Tooltip>
);
Expand Down
45 changes: 45 additions & 0 deletions src/ActionIconGroup/demos/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
ActionIconGroup,
ActionIconGroupProps,
StroyBook,
useControls,
useCreateStore,
} from '@lobehub/ui';
import { Copy, RotateCw } from 'lucide-react';

const items: ActionIconGroupProps['items'] = [
{
icon: Copy,
title: 'Copy',
onClick: () => console.log('click Copy'),
},
{
icon: RotateCw,
title: 'Regenerate',
onClick: () => console.log('click Regenerate'),
},
];

export default () => {
const store = useCreateStore();
const control: ActionIconGroupProps | any = useControls(
{
type: {
value: 'block',
options: ['ghost', 'block', 'pure'],
},
direction: {
value: 'row',
options: ['row', 'column'],
},
spotlight: true,
},
{ store },
);

return (
<StroyBook levaStore={store}>
<ActionIconGroup items={items} {...control} />
</StroyBook>
);
};
14 changes: 14 additions & 0 deletions src/ActionIconGroup/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
nav: Components
group: Common
title: ActionIconGroup
description: ActionIconGroup is a component used to render multi buttons
---

## Default

<code src="./demos/index.tsx" nopadding></code>

## APIs

<API></API>
48 changes: 48 additions & 0 deletions src/ActionIconGroup/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ActionIcon, ActionIconProps, Spotlight } from '@lobehub/ui';
import { memo } from 'react';

import { DivProps } from '@/types';

import { useStyles } from './style';

export interface ActionIconGroupProps extends DivProps {
direction?: 'row' | 'column';
items: ActionIconProps[];
/**
* @description The position of the tooltip relative to the target
* @enum ["top","left","right","bottom","topLeft","topRight","bottomLeft","bottomRight","leftTop","leftBottom","rightTop","rightBottom"]
*/
placement?: ActionIconProps['placement'];
/**
* @description Whether add spotlight background
* @default true
*/
spotlight?: boolean;
/**
* @description The type of the group
* @default 'block'
*/
type?: 'ghost' | 'block' | 'pure';
}

const ActionIconGroup = memo<ActionIconGroupProps>(
({ type = 'block', items, placement, spotlight = true, direction = 'row', ...props }) => {
const { styles } = useStyles({ direction, type });

return (
<div className={styles.container} {...props}>
{spotlight && <Spotlight />}
{items.map((item, index) => (
<ActionIcon
key={index}
placement={placement || (direction === 'column' ? 'right' : 'top')}
size="small"
{...item}
/>
))}
</div>
);
},
);

export default ActionIconGroup;
30 changes: 30 additions & 0 deletions src/ActionIconGroup/style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createStyles } from 'antd-style';

export const useStyles = createStyles(
(
{ css, token, stylish, cx },
{ direction, type }: { direction: 'row' | 'column'; type: 'ghost' | 'block' | 'pure' },
) => {
const typeStylish = css`
background-color: ${type === 'block' ? token.colorFillTertiary : 'transparent'};
border: 1px solid ${type === 'block' ? 'transparent' : token.colorBorder};
`;

return {
container: cx(
type !== 'pure' && typeStylish,
stylish.blur,
css`
position: relative;
display: flex;
flex-direction: ${direction};
padding: 2px;
border-radius: ${token.borderRadius}px;
`,
),
};
},
);
9 changes: 6 additions & 3 deletions src/Avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ export interface AvatarProps extends AntAvatarProps {

const Avatar = memo<AvatarProps>(
({ className, avatar, title, size = 40, shape = 'circle', background, ...props }) => {
const { styles, cx } = useStyles({ background, size });

const isImage = avatar && ['/', 'http', 'data:'].some((i) => avatar.startsWith(i));
const isEmoji =
avatar && !isImage && /\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g.test(avatar);

const { styles, cx } = useStyles({ background, size, isEmoji });

const text = isImage ? title : avatar;

return !isImage ? (
<AntAvatar className={cx(styles.avatar, className)} shape={shape} size={size} {...props}>
{text?.toUpperCase().slice(0, 2)}
{isEmoji ? text : text?.toUpperCase().slice(0, 2)}
</AntAvatar>
) : (
<AntAvatar
Expand Down
7 changes: 5 additions & 2 deletions src/Avatar/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { createStyles } from 'antd-style';
import { readableColor } from 'polished';

export const useStyles = createStyles(
({ css, token }, { background, size }: { background?: string; size: number }) => {
(
{ css, token },
{ background, size, isEmoji }: { background?: string; isEmoji: boolean; size: number },
) => {
const backgroundColor = background ?? token.colorBgContainer;
const color = readableColor(backgroundColor);

Expand All @@ -18,7 +21,7 @@ export const useStyles = createStyles(
border: 1px solid ${background ? 'transparent' : token.colorSplit};
> .ant-avatar-string {
font-size: ${size * 0.5}px;
font-size: ${size * (isEmoji ? 0.7 : 0.5)}px;
font-weight: 700;
line-height: 1 !important;
color: ${color};
Expand Down
4 changes: 2 additions & 2 deletions src/ContextMenu/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BoxSelectIcon, CopyIcon } from 'lucide-react';

export default () => {
return (
<>
<div>
<div>RightClick</div>
<ContextMenu
items={[
Expand Down Expand Up @@ -32,6 +32,6 @@ export default () => {
},
]}
/>
</>
</div>
);
};
68 changes: 68 additions & 0 deletions src/Menu/MenuItem/icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const strokeWidth = 2.2;
const iconSize = '13px';

export const CommandIcon = () => (
<svg
fill="none"
height={iconSize}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
viewBox="0 0 24 24"
width={iconSize}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"></path>
</svg>
);
export const ControlIcon = () => (
<svg
className="lucide lucide-chevron-up"
fill="none"
height={iconSize}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
viewBox="0 0 24 24"
width={iconSize}
xmlns="http://www.w3.org/2000/svg"
>
<polyline points="18 15 12 9 6 15"></polyline>
</svg>
);
export const ShiftIcon = () => (
<svg
className="lucide lucide-chevron-up"
fill="none"
height={iconSize}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
viewBox="0 0 24 24"
width={iconSize}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M9 18v-6H5l7-7 7 7h-4v6H9z"></path>
</svg>
);

export const AltIcon = () => (
<svg
className="lucide lucide-chevron-up"
fill="none"
height={iconSize}
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
viewBox="0 0 24 24"
width={iconSize}
xmlns="http://www.w3.org/2000/svg"
>
<path d="M3 3h6l6 18h6"></path>
<path d="M14 3h7"></path>
</svg>
);
79 changes: 79 additions & 0 deletions src/Menu/MenuItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ChevronRightIcon } from 'lucide-react';
import { ReactNode, forwardRef } from 'react';
import { Flexbox } from 'react-layout-kit';

import { AltIcon, CommandIcon, ControlIcon, ShiftIcon } from './icons';
import { useStyles } from './style';

const KEYBOARD_ICON_MAP: Record<string, any> = {
meta: <CommandIcon />,
control: <ControlIcon />,

shift: <ShiftIcon />,
alt: <AltIcon />,
enter: '↵',
};

const CODE_MAP: Record<string, 'meta' | 'control' | 'shift' | 'alt'> = {
meta: 'meta',
command: 'meta',
cmd: 'meta',
ctl: 'control',
control: 'control',
shift: 'shift',
alt: 'alt',
};

interface MenuItemProps {
active?: boolean;
disabled?: boolean;
icon?: ReactNode;
label: ReactNode;
nested?: boolean;
selected?: boolean;
shortcut?: string[];
}

const MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(
({ label, icon, disabled, nested, shortcut, active, selected, ...props }, ref) => {
const { styles, cx } = useStyles();

return (
<button
type={'button'}
{...props}
className={cx(styles.item, {
[styles.selected]: selected,
[styles.active]: active,
})}
disabled={disabled}
ref={ref}
role="menuitem"
>
<Flexbox gap={8} horizontal>
{icon && <span>{icon}</span>}
{label}
</Flexbox>
{nested ? (
<span aria-hidden>
<ChevronRightIcon className={styles.arrow} />
</span>
) : shortcut ? (
<Flexbox align={'center'} horizontal>
{shortcut.map((c) => {
const code = CODE_MAP[c.toLowerCase()];

return (
<kbd className={styles.kbd} key={c}>
{code ? KEYBOARD_ICON_MAP[code] : c.toUpperCase()}
</kbd>
);
})}
</Flexbox>
) : null}
</button>
);
},
);

export default MenuItem;
Loading

1 comment on commit eea5bff

@vercel
Copy link

@vercel vercel bot commented on eea5bff Jun 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

lobe-ui – ./

lobe-ui-lobehub.vercel.app
lobe-ui.vercel.app
ui.lobehub.com
lobe-ui-git-master-lobehub.vercel.app

Please sign in to comment.