[Glitch] Callout component
Port c1414f1161b5fc1b8b6755d22b76415c82b64d7c to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
46c83fc91d
commit
aa71913356
@ -0,0 +1,93 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import { Callout } from '.';
|
||||
|
||||
const meta = {
|
||||
title: 'Components/Callout',
|
||||
args: {
|
||||
children: 'Contents here',
|
||||
title: 'Title',
|
||||
onPrimary: action('Primary action clicked'),
|
||||
primaryLabel: 'Primary',
|
||||
onSecondary: action('Secondary action clicked'),
|
||||
secondaryLabel: 'Secondary',
|
||||
onClose: action('Close clicked'),
|
||||
},
|
||||
component: Callout,
|
||||
render(args) {
|
||||
return (
|
||||
<div style={{ minWidth: 'min(400px, calc(100vw - 2rem))' }}>
|
||||
<Callout {...args} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
} satisfies Meta<typeof Callout>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
variant: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
export const NoIcon: Story = {
|
||||
args: {
|
||||
icon: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoActions: Story = {
|
||||
args: {
|
||||
onPrimary: undefined,
|
||||
onSecondary: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const OnlyText: Story = {
|
||||
args: {
|
||||
onClose: undefined,
|
||||
onPrimary: undefined,
|
||||
onSecondary: undefined,
|
||||
icon: false,
|
||||
},
|
||||
};
|
||||
|
||||
// export const Subtle: Story = {
|
||||
// args: {
|
||||
// variant: 'subtle',
|
||||
// },
|
||||
// };
|
||||
|
||||
export const Feature: Story = {
|
||||
args: {
|
||||
variant: 'feature',
|
||||
},
|
||||
};
|
||||
|
||||
export const Inverted: Story = {
|
||||
args: {
|
||||
variant: 'inverted',
|
||||
},
|
||||
};
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
variant: 'success',
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
variant: 'warning',
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
variant: 'error',
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,27 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { useDismissible } from '@/flavours/glitch/hooks/useDismissible';
|
||||
|
||||
import { Callout } from '.';
|
||||
import type { CalloutProps } from '.';
|
||||
|
||||
type DismissibleCalloutProps = CalloutProps & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const DismissibleCallout: FC<DismissibleCalloutProps> = (props) => {
|
||||
const { dismiss, wasDismissed } = useDismissible(props.id);
|
||||
|
||||
const { onClose } = props;
|
||||
const handleClose = useCallback(() => {
|
||||
dismiss();
|
||||
onClose?.();
|
||||
}, [dismiss, onClose]);
|
||||
|
||||
if (wasDismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Callout {...props} onClose={handleClose} />;
|
||||
};
|
||||
150
app/javascript/flavours/glitch/components/callout/index.tsx
Normal file
150
app/javascript/flavours/glitch/components/callout/index.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import ErrorIcon from '@/material-icons/400-24px/error.svg?react';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
|
||||
import type { IconProp } from '../icon';
|
||||
import { Icon } from '../icon';
|
||||
import { IconButton } from '../icon_button';
|
||||
|
||||
import classes from './styles.module.css';
|
||||
|
||||
export interface CalloutProps {
|
||||
variant?:
|
||||
| 'default'
|
||||
// | 'subtle'
|
||||
| 'feature'
|
||||
| 'inverted'
|
||||
| 'success'
|
||||
| 'warning'
|
||||
| 'error';
|
||||
title?: ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
/** Set to false to hide the icon. */
|
||||
icon?: IconProp | boolean;
|
||||
onPrimary?: () => void;
|
||||
primaryLabel?: string;
|
||||
onSecondary?: () => void;
|
||||
secondaryLabel?: string;
|
||||
onClose?: () => void;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const variantClasses = {
|
||||
default: classes.variantDefault as string,
|
||||
// subtle: classes.variantSubtle as string,
|
||||
feature: classes.variantFeature as string,
|
||||
inverted: classes.variantInverted as string,
|
||||
success: classes.variantSuccess as string,
|
||||
warning: classes.variantWarning as string,
|
||||
error: classes.variantError as string,
|
||||
} as const;
|
||||
|
||||
export const Callout: FC<CalloutProps> = ({
|
||||
className,
|
||||
variant = 'default',
|
||||
title,
|
||||
children,
|
||||
icon,
|
||||
onPrimary: primaryAction,
|
||||
primaryLabel,
|
||||
onSecondary: secondaryAction,
|
||||
secondaryLabel,
|
||||
onClose,
|
||||
id,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={classNames(
|
||||
className,
|
||||
classes.wrapper,
|
||||
variantClasses[variant],
|
||||
)}
|
||||
data-variant={variant}
|
||||
id={id}
|
||||
>
|
||||
<CalloutIcon variant={variant} icon={icon} />
|
||||
<div className={classes.content}>
|
||||
<div className={classes.body}>
|
||||
{title && <h3>{title}</h3>}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{(primaryAction ?? secondaryAction) && (
|
||||
<div className={classes.actionWrapper}>
|
||||
{secondaryAction && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={secondaryAction}
|
||||
className={classes.action}
|
||||
>
|
||||
{secondaryLabel ?? 'Click'}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{primaryAction && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={primaryAction}
|
||||
className={classes.action}
|
||||
>
|
||||
{primaryLabel ?? 'Click'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{onClose && (
|
||||
<IconButton
|
||||
icon='close'
|
||||
title={intl.formatMessage({
|
||||
id: 'callout.dismiss',
|
||||
defaultMessage: 'Dismiss',
|
||||
})}
|
||||
iconComponent={CloseIcon}
|
||||
className={classes.close}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
const CalloutIcon: FC<Pick<CalloutProps, 'variant' | 'icon'>> = ({
|
||||
variant = 'default',
|
||||
icon,
|
||||
}) => {
|
||||
if (icon === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!icon || icon === true) {
|
||||
switch (variant) {
|
||||
case 'inverted':
|
||||
case 'success':
|
||||
icon = CheckIcon;
|
||||
break;
|
||||
case 'warning':
|
||||
icon = WarningIcon;
|
||||
break;
|
||||
case 'error':
|
||||
icon = ErrorIcon;
|
||||
break;
|
||||
default:
|
||||
icon = InfoIcon;
|
||||
}
|
||||
}
|
||||
|
||||
return <Icon id={variant} icon={icon} className={classes.icon} />;
|
||||
};
|
||||
@ -0,0 +1,125 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
padding: 12px;
|
||||
gap: 8px;
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
color: var(--color-text-primary);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 4px;
|
||||
border-radius: 9999px;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media screen and (width >= 630px) {
|
||||
.content {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.icon + .content,
|
||||
.wrapper:has(.close) .content {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.actionWrapper {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.action {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
text-decoration: underline;
|
||||
transition: color 0.1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-brand-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
color: inherit;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.variantDefault {
|
||||
.icon {
|
||||
background-color: var(--color-bg-brand-soft);
|
||||
}
|
||||
}
|
||||
|
||||
/* .variantSubtle {
|
||||
border: 1px solid var(--color-bg-brand-softer);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
}
|
||||
} */
|
||||
|
||||
.variantFeature {
|
||||
background-color: var(--color-bg-brand-base);
|
||||
color: var(--color-text-on-brand-base);
|
||||
|
||||
button:hover {
|
||||
color: color-mix(var(--color-text-on-brand-base), transparent 20%);
|
||||
}
|
||||
}
|
||||
|
||||
.variantInverted {
|
||||
background-color: var(--color-bg-inverted);
|
||||
color: var(--color-text-on-inverted);
|
||||
}
|
||||
|
||||
.variantSuccess {
|
||||
background-color: var(--color-bg-success-softer);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-success-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.variantWarning {
|
||||
background-color: var(--color-bg-warning-softer);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-warning-soft);
|
||||
}
|
||||
}
|
||||
|
||||
.variantError {
|
||||
background-color: var(--color-bg-error-softer);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-error-soft);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user