@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
174 lines • 7.87 kB
JavaScript
import { Shade, createComponent } from '@furystack/shades';
import { buildTransition, cssVariableTheme } from '../services/css-variable-theme.js';
import { promisifyAnimation } from '../utils/promisify-animation.js';
import { Icon } from './icons/icon.js';
import { close } from './icons/icon-definitions.js';
import { Modal } from './modal.js';
import { Typography } from './typography.js';
const showAnimation = async (el) => {
const panel = el?.querySelector?.('.dialog-panel');
if (panel) {
await promisifyAnimation(panel, [
{ opacity: 0, transform: 'scale(0.9)' },
{ opacity: 1, transform: 'scale(1)' },
], {
duration: 200,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards',
});
}
};
const hideAnimation = async (el) => {
const panel = el?.querySelector?.('.dialog-panel');
if (panel) {
await promisifyAnimation(panel, [
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.9)' },
], {
duration: 150,
easing: 'cubic-bezier(0.4, 0, 0.2, 1)',
fill: 'forwards',
});
}
};
export const Dialog = Shade({
customElementName: 'shade-dialog',
css: {
fontFamily: cssVariableTheme.typography.fontFamily,
'& .dialog-panel': {
position: 'relative',
display: 'flex',
flexDirection: 'column',
background: cssVariableTheme.background.paper,
backgroundImage: cssVariableTheme.background.paperImage,
color: cssVariableTheme.text.primary,
borderRadius: cssVariableTheme.shape.borderRadius.md,
boxShadow: cssVariableTheme.shadows.xl,
margin: cssVariableTheme.spacing.lg,
maxHeight: 'calc(100vh - 64px)',
overflow: 'hidden',
fontFamily: cssVariableTheme.typography.fontFamily,
},
'& .dialog-header': {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: `${cssVariableTheme.spacing.md} ${cssVariableTheme.spacing.lg}`,
borderBottom: `1px solid ${cssVariableTheme.divider}`,
flexShrink: '0',
},
'& .dialog-title': {
flex: '1',
minWidth: '0',
},
'& .dialog-close': {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
border: 'none',
background: 'transparent',
color: cssVariableTheme.text.secondary,
borderRadius: cssVariableTheme.shape.borderRadius.sm,
cursor: 'pointer',
fontSize: '18px',
lineHeight: '1',
padding: '0',
flexShrink: '0',
transition: buildTransition(['background', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default], ['color', cssVariableTheme.transitions.duration.fast, cssVariableTheme.transitions.easing.default]),
},
'& .dialog-close:hover': {
background: cssVariableTheme.action.hoverBackground,
color: cssVariableTheme.text.primary,
},
'& .dialog-body': {
padding: cssVariableTheme.spacing.lg,
overflowY: 'auto',
flex: '1',
},
'& .dialog-actions': {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.lg}`,
borderTop: `1px solid ${cssVariableTheme.divider}`,
gap: cssVariableTheme.spacing.sm,
flexShrink: '0',
},
},
render: ({ props, children }) => {
const { isVisible, title, onClose, actions, maxWidth = '560px', fullWidth, trapFocus = true, navSection } = props;
const handleClose = () => {
onClose?.();
};
return (createComponent(Modal, { isVisible: isVisible, onClose: handleClose, showAnimation: showAnimation, hideAnimation: hideAnimation, trapFocus: trapFocus, navSection: navSection, backdropStyle: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: cssVariableTheme.action.backdrop,
zIndex: '1300',
} },
createComponent("div", { className: "dialog-panel", style: {
width: fullWidth ? '100%' : undefined,
maxWidth,
}, onclick: (ev) => ev.stopPropagation() },
title || onClose ? (createComponent("div", { className: "dialog-header" },
title ? (createComponent(Typography, { variant: "h5", className: "dialog-title", style: { margin: '0' } }, title)) : (createComponent("span", null)),
onClose ? (createComponent("button", { className: "dialog-close", onclick: handleClose, "aria-label": "Close dialog" },
createComponent(Icon, { icon: close, size: "small" }))) : null)) : null,
createComponent("div", { className: "dialog-body" }, children),
actions ? createComponent("div", { className: "dialog-actions" }, actions) : null)));
},
});
/**
* Renders a pre-built confirm dialog with confirm/cancel buttons.
* Returns a JSX element that should be placed in the component tree.
*
* @param isVisible - Boolean controlling the dialog visibility
* @param options - Configuration for the confirm dialog
* @returns A Dialog JSX element
*
* @example
* ```tsx
* const [isConfirmOpen, setConfirmOpen] = useState('confirmOpen', false)
* // ...
* {ConfirmDialog(isConfirmOpen, {
* title: 'Delete Item',
* message: 'Are you sure you want to delete this item?',
* onConfirm: () => { deleteItem(); setConfirmOpen(false) },
* onCancel: () => setConfirmOpen(false),
* })}
* ```
*/
export const ConfirmDialog = (isVisible, options) => {
const { title, message, confirmText = 'Confirm', cancelText = 'Cancel', onConfirm, onCancel } = options;
const handleCancel = () => {
onCancel?.();
};
const handleConfirm = () => {
onConfirm();
};
return (createComponent(Dialog, { isVisible: isVisible, title: title, onClose: handleCancel, maxWidth: "440px", actions: createComponent(createComponent, null,
createComponent("button", { className: "dialog-cancel-btn", onclick: handleCancel, style: {
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: 'transparent',
color: cssVariableTheme.text.secondary,
cursor: 'pointer',
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
} }, cancelText),
createComponent("button", { className: "dialog-confirm-btn", onclick: handleConfirm, style: {
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: cssVariableTheme.palette.primary.main,
color: cssVariableTheme.palette.primary.mainContrast,
cursor: 'pointer',
fontSize: cssVariableTheme.typography.fontSize.md,
fontWeight: cssVariableTheme.typography.fontWeight.medium,
} }, confirmText)) }, typeof message === 'string' ? (createComponent(Typography, { variant: "body1", color: "textSecondary", style: { margin: '0' } }, message)) : (message)));
};
//# sourceMappingURL=dialog.js.map