ui(components): New Dialog and Modal components for ui-next (#4772)

Co-authored-by: sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Dan Rukasandsedghi authored and GitHub committed 2025-02-28 15:17:55 -05:00
1 parent 62b4fa3e89
commit a3b51aa8db
90 files changed
+3459 -1357

No files matched your search

@@ -190,7 +190,8 @@ const DataRow: React.FC<DataRowProps> = ({
return (
<div
ref={rowRef}
className={`flex flex-col ${isVisible ? '' : 'opacity-60'}`}>
className={`flex flex-col ${isVisible ? '' : 'opacity-60'}`}
>
<div
className={`flex items-center ${
isSelected ? 'bg-popover' : 'bg-muted'
@@ -291,7 +292,11 @@ const DataRow: React.FC<DataRowProps> = ({
<Icons.More className="h-6 w-6" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuContent
align="end"
// this was causing issue for auto focus on input dialog
onCloseAutoFocus={e => e.preventDefault()}
>
<>
<DropdownMenuItem onClick={e => handleAction('Rename', e)}>
<Icons.Rename className="text-foreground" />
+116 -30
View File
@@ -3,8 +3,41 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Cross2Icon } from '@radix-ui/react-icons';
import { cn } from '../../lib/utils';
import { useDraggable } from './useDraggable';
const Dialog = DialogPrimitive.Root;
interface DialogContextValue {
isDraggable?: boolean;
shouldCloseOnEsc?: boolean;
shouldCloseOnOverlayClick?: boolean;
showOverlay?: boolean;
}
const DialogContext = React.createContext<DialogContextValue>({
isDraggable: false,
shouldCloseOnEsc: true,
shouldCloseOnOverlayClick: true,
});
interface DialogRootProps extends DialogPrimitive.DialogProps {
isDraggable?: boolean;
shouldCloseOnEsc?: boolean;
shouldCloseOnOverlayClick?: boolean;
showOverlay?: boolean;
}
const Dialog = ({
isDraggable,
shouldCloseOnEsc = true,
shouldCloseOnOverlayClick = true,
showOverlay = true,
...props
}: DialogRootProps) => (
<DialogContext.Provider
value={{ isDraggable, shouldCloseOnEsc, shouldCloseOnOverlayClick, showOverlay }}
>
<DialogPrimitive.Root {...props} />
</DialogContext.Provider>
);
const DialogTrigger = DialogPrimitive.Trigger;
@@ -14,12 +47,14 @@ const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
className?: string;
}
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
<div
ref={ref}
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-40 bg-black/60',
className
)}
{...props}
@@ -27,36 +62,83 @@ const DialogOverlay = React.forwardRef<
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {}
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
DialogContentProps & {
children?: React.ReactNode;
}
>(({ className, children, ...props }, ref) => {
const { isDraggable, shouldCloseOnEsc, shouldCloseOnOverlayClick, showOverlay } =
React.useContext(DialogContext);
const { handlePointerDown, setRefs, initialTransform } = useDraggable(
{
enabled: isDraggable,
},
ref
);
// When not isDraggable, Tailwind centers the dialog.
// When isDraggable, we remove the built‑in centering so our inline transform takes over.
const contentClassName = cn(
'max-w-md w-full bg-muted data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid gap-4 p-4 shadow-lg duration-200 sm:rounded-lg',
!isDraggable ? 'translate-x-[-50%] translate-y-[-50%]' : '',
className
);
const style = isDraggable ? { ...props.style, transform: initialTransform } : props.style;
const content = (
<DialogPrimitive.Content
ref={ref}
ref={setRefs}
className={contentClassName}
{...props}
style={style}
onPointerDown={isDraggable ? handlePointerDown : props.onPointerDown}
onEscapeKeyDown={event => {
if (!shouldCloseOnEsc) {
event.preventDefault();
}
}}
onInteractOutside={event => {
if (!shouldCloseOnOverlayClick) {
event.preventDefault();
}
}}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<Cross2Icon className="text-primary h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
);
return (
<DialogPortal>
{showOverlay && !isDraggable && <DialogOverlay />}
{content}
</DialogPortal>
);
});
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
return (
<div
className={cn(
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
'drag-handle relative flex select-none flex-col space-y-1.5 text-center sm:text-left',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)}
{...props}
/>
);
{props.children}
</div>
);
};
DialogHeader.displayName = 'DialogHeader';
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
@@ -69,11 +151,13 @@ DialogFooter.displayName = 'DialogFooter';
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> & {
className?: string;
}
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-xl font-semibold leading-none tracking-tight', className)}
className={cn('text-primary-light text-xl font-normal leading-none tracking-tight', className)}
{...props}
/>
));
@@ -81,11 +165,13 @@ DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> & {
className?: string;
}
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-muted-foreground text-base', className)}
className={cn('text-base', className)}
{...props}
/>
));
@@ -0,0 +1,108 @@
import * as React from 'react';
interface Offset {
x: number;
y: number;
}
interface DragState {
startX: number;
startY: number;
initialOffset: Offset;
}
interface UseDraggableProps {
enabled?: boolean;
}
interface UseDraggableReturn {
offset: React.RefObject<Offset>;
internalRef: React.RefObject<HTMLDivElement>;
handlePointerDown: (e: React.PointerEvent<HTMLDivElement>) => void;
setRefs: (node: HTMLDivElement) => void;
initialTransform: string | undefined;
}
export function useDraggable(
props: UseDraggableProps,
ref?: React.ForwardedRef<HTMLDivElement>
): UseDraggableReturn {
const { enabled = false } = props;
const offsetRef = React.useRef<Offset>({ x: 0, y: 0 });
const internalRef = React.useRef<HTMLDivElement>(null);
const dragState = React.useRef<DragState | null>(null);
const handlePointerMove = React.useCallback((e: PointerEvent) => {
if (!dragState.current) {
return;
}
const deltaX = e.clientX - dragState.current.startX;
const deltaY = e.clientY - dragState.current.startY;
const newOffset = {
x: dragState.current.initialOffset.x + deltaX,
y: dragState.current.initialOffset.y + deltaY,
};
offsetRef.current = newOffset;
if (internalRef.current) {
internalRef.current.style.transform = `translate(-50%, -50%) translate(${newOffset.x}px, ${newOffset.y}px)`;
}
}, []);
const handlePointerUp = React.useCallback(() => {
if (internalRef.current) {
internalRef.current.style.transition = '';
}
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
dragState.current = null;
}, [handlePointerMove]);
const handlePointerDown = React.useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
const target = e.target as HTMLElement;
if (!target.closest('.drag-handle')) {
return;
}
if (internalRef.current) {
internalRef.current.style.transition = 'none';
}
dragState.current = {
startX: e.clientX,
startY: e.clientY,
initialOffset: { ...offsetRef.current },
};
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
},
[handlePointerMove, handlePointerUp]
);
const setRefs = React.useCallback(
(node: HTMLDivElement) => {
internalRef.current = node;
if (typeof ref === 'function') {
ref(node);
} else if (ref) {
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
}
},
[ref]
);
const initialTransform = enabled
? `translate(-50%, -50%) translate(${offsetRef.current.x}px, ${offsetRef.current.y}px)`
: undefined;
return {
offset: offsetRef,
internalRef,
handlePointerDown,
setRefs,
initialTransform,
};
}
@@ -62,6 +62,11 @@ const DropdownMenuContent = React.forwardRef<
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
onKeyDown={e => {
// Todo: or maybe we just want to prevent arrows?
e.stopPropagation();
props.onKeyDown?.(e);
}}
className={cn(
'bg-popover text-popover-foreground border-input z-50 min-w-[8rem] overflow-hidden rounded border p-1 shadow-md',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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',
@@ -0,0 +1,102 @@
import React from 'react';
import { Button } from '../Button/Button';
import { cn } from '../../lib/utils';
interface FooterActionProps {
children: React.ReactNode;
className?: string;
}
interface ActionProps extends FooterActionProps {
onClick: () => void;
className?: string;
}
type FooterActionComponent = React.FC<FooterActionProps> & {
Left: React.FC<FooterActionProps>;
Right: React.FC<FooterActionProps>;
Primary: React.FC<ActionProps>;
Secondary: React.FC<ActionProps>;
Auxiliary: React.FC<ActionProps>;
};
export const FooterAction: FooterActionComponent = ({ children, className }: FooterActionProps) => {
// Convert children to array for easier inspection
const arrayChildren = React.Children.toArray(children);
// Check if we have a <FooterAction.Left> or <FooterAction.Right> among children
const hasLeft = arrayChildren.some(
(child: any) => child.type?.displayName === 'FooterAction.Left'
);
const hasRight = arrayChildren.some(
(child: any) => child.type?.displayName === 'FooterAction.Right'
);
// Decide on the justification class based on presence of Left/Right
let justifyClass = 'justify-between'; // default
if (hasLeft && !hasRight) {
justifyClass = 'justify-start';
} else if (!hasLeft && hasRight) {
justifyClass = 'justify-end';
}
// If both or neither are present, keep justify-between (or adjust if you like)
return (
<div className={cn('flex w-full flex-shrink-0 items-center', justifyClass, className)}>
{children}
</div>
);
};
FooterAction.displayName = 'FooterAction';
FooterAction.Left = ({ children }: FooterActionProps) => {
return <div className="flex items-center">{children}</div>;
};
FooterAction.Left.displayName = 'FooterAction.Left';
FooterAction.Right = ({ children }: FooterActionProps) => {
return <div className="flex items-center space-x-2">{children}</div>;
};
FooterAction.Right.displayName = 'FooterAction.Right';
// Primary action: Solid button (default)
FooterAction.Primary = ({ children, onClick, className = 'min-w-[80px]' }: ActionProps) => {
return (
<Button
variant="default"
onClick={onClick}
className={className}
>
{children}
</Button>
);
};
FooterAction.Primary.displayName = 'FooterAction.Primary';
// Secondary action: Ghost button
FooterAction.Secondary = ({ children, onClick, className = 'min-w-[80px]' }: ActionProps) => {
return (
<Button
variant="secondary"
onClick={onClick}
className={className}
>
{children}
</Button>
);
};
FooterAction.Secondary.displayName = 'FooterAction.Secondary';
// Tertiary action: Ghost button with different styling
FooterAction.Auxiliary = ({ children, onClick, className }: ActionProps) => {
return (
<Button
variant="ghost"
onClick={onClick}
className={className}
>
{children}
</Button>
);
};
FooterAction.Auxiliary.displayName = 'FooterAction.Auxiliary';
@@ -0,0 +1,3 @@
import { FooterAction } from './FooterAction';
export { FooterAction };
@@ -46,6 +46,7 @@ import Show from './Sources/Show';
import SidePanelCloseLeft from './Sources/SidePanelCloseLeft';
import SidePanelCloseRight from './Sources/SidePanelCloseRight';
import SortingAscending from './Sources/SortingAscending';
import SocialGithub from './Sources/SocialGithub';
import SortingDescending from './Sources/SortingDescending';
import StatusError from './Sources/StatusError';
import StatusSuccess from './Sources/StatusSuccess';
@@ -471,6 +472,7 @@ export const Icons = {
Show,
SidePanelCloseLeft,
SidePanelCloseRight,
SocialGithub,
SortingAscending,
SortingDescending,
Sorting,
@@ -0,0 +1,39 @@
import React from 'react';
import type { IconProps } from '../types';
export const SocialGithub = (props: IconProps) => (
<svg
width="24px"
height="24px"
viewBox="0 0 24 24"
version="1.1"
>
<g
id="SocialGithub"
stroke="none"
strokeWidth="1"
fill="none"
fillRule="evenodd"
>
<rect
id="Rectangle"
x="0"
y="0"
width="24"
height="24"
></rect>
<g
id="github-mark"
transform="translate(1, 1)"
fill="#FFFFFF"
>
<path
d="M11.009102,0 C4.92135296,0 0,5.04164559 0,11.2788487 C0,16.2645778 3.15328046,20.4848935 7.52771627,21.9785956 C8.07463342,22.0908868 8.2749669,21.7359091 8.2749669,21.4373062 C8.2749669,21.1758281 8.25693914,20.279561 8.25693914,19.3457108 C5.19447352,20.018083 4.55674153,18.0011956 4.55674153,18.0011956 C4.0645837,16.6940344 3.33536083,16.3580774 3.33536083,16.3580774 C2.33301741,15.6671428 3.40837326,15.6671428 3.40837326,15.6671428 C4.52023532,15.7418508 5.10365868,16.824888 5.10365868,16.824888 C6.08774899,18.5427141 7.67351578,18.0573412 8.31147311,17.7585091 C8.4025133,17.0302205 8.69433765,16.5260559 9.00418977,16.2460154 C6.56165372,15.9845373 3.99179662,15.0135622 3.99179662,10.6811845 C3.99179662,9.44873132 4.42896978,8.44040221 5.12168644,7.65619715 C5.01239315,7.37615666 4.62952861,6.21818233 5.23120508,4.66833465 C5.23120508,4.66833465 6.16076142,4.36950256 8.25671379,5.8260798 C9.15406554,5.57919102 10.0794899,5.45359714 11.009102,5.4525397 C11.9386583,5.4525397 12.8862424,5.58339332 13.7612648,5.8260798 C15.8574425,4.36950256 16.7869988,4.66833465 16.7869988,4.66833465 C17.3886753,6.21818233 17.0055854,7.37615666 16.8962921,7.65619715 C17.6072619,8.44040221 18.0264073,9.44873132 18.0264073,10.6811845 C18.0264073,15.0135622 15.4565502,15.9657457 12.995761,16.2460154 C13.3968787,16.6007639 13.7430117,17.2729069 13.7430117,18.3373817 C13.7430117,19.8498753 13.7249839,21.0637661 13.7249839,21.437077 C13.7249839,21.7359091 13.9255427,22.0908868 14.4722345,21.9788248 C18.8466703,20.4846643 22,16.2645778 22,11.2788487 C22.0179786,5.04164559 17.0785978,0 11.009102,0 Z"
id="Path"
></path>
</g>
</g>
</svg>
);
export default SocialGithub;
@@ -0,0 +1,44 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../Dialog';
import { cn } from '../../lib/utils';
export interface ModalProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
contentClassName?: string;
shouldCloseOnEsc?: boolean;
shouldCloseOnOverlayClick?: boolean;
containerClassName?: string;
}
const Modal: React.FC<ModalProps> = ({
isOpen,
onClose,
title,
children,
shouldCloseOnEsc = true,
shouldCloseOnOverlayClick = true,
containerClassName,
}) => {
return (
<Dialog
open={isOpen}
onOpenChange={open => !open && onClose()}
shouldCloseOnEsc={shouldCloseOnEsc}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
>
<DialogContent className={containerClassName}>
{title && (
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>
)}
<div className={cn('mt-2')}>{children}</div>
</DialogContent>
</Dialog>
);
};
export default Modal;
@@ -0,0 +1,258 @@
import React, { createContext, useContext, useRef, useEffect } from 'react';
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { Label, Input as InputComponent, FooterAction } from '../../components';
import { cn } from '../../lib/utils';
interface InputDialogContextValue {
value: string;
setValue: (value: string) => void;
submitOnEnter?: boolean;
}
const InputDialogContext = createContext<InputDialogContextValue | null>(null);
export type InputDialogRootProps = {
/** The controlled value of the input */
value?: string;
/** The default value for uncontrolled usage */
defaultValue?: string;
/** Callback when input value changes */
onChange?: (value: string) => void;
/** Optional className for the root container */
className?: string;
/** Enable save on Enter key press */
submitOnEnter?: boolean;
children: React.ReactNode;
};
const InputDialogRoot = React.forwardRef<HTMLDivElement, InputDialogRootProps>(
({ value, defaultValue = '', onChange, className, submitOnEnter, children }, ref) => {
const [internalValue, setInternalValue] = useControllableState({
prop: value,
defaultProp: defaultValue,
onChange,
});
return (
<InputDialogContext.Provider
value={{
value: internalValue,
setValue: setInternalValue,
submitOnEnter,
}}
>
<div
ref={ref}
className={cn('flex flex-col', className)}
>
{children}
</div>
</InputDialogContext.Provider>
);
}
);
InputDialogRoot.displayName = 'InputDialog';
export interface InputDialogFieldProps extends React.HTMLAttributes<HTMLDivElement> {
/** Optional className for the field container */
className?: string;
children: React.ReactNode;
}
const Field = React.forwardRef<HTMLDivElement, InputDialogFieldProps>(
({ className, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn('mb-4 flex flex-col space-y-2', className)}
{...props}
>
{children}
</div>
);
}
);
Field.displayName = 'InputDialog.Field';
export interface InputDialogInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
/** ID for the input field */
id?: string;
/** Optional className for the input container */
className?: string;
/** Save handler */
onSave?: (value: string) => void;
/** Placeholder text for the input field */
placeholder?: string;
}
const InputDialogInput = React.forwardRef<HTMLInputElement, InputDialogInputProps>(
({ id = 'dialog-input', className, onSave, ...props }, ref) => {
const context = useContext(InputDialogContext);
if (!context) {
throw new Error('InputDialog.Input must be used within an InputDialog');
}
const { value, setValue } = context;
const inputRef = useRef<HTMLInputElement>(null);
// Combine the forwarded ref with our local ref
React.useImperativeHandle(ref, () => inputRef.current);
// Focus the input when it mounts
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (context.submitOnEnter && e.key === 'Enter') {
e.preventDefault();
const saveButton = document.querySelector(
'[data-cy="input-dialog-save-button"]'
) as HTMLButtonElement;
if (saveButton) {
saveButton.click();
}
}
};
return (
<div className={cn('w-full', className)}>
<InputComponent
ref={inputRef}
id={id}
value={value}
onChange={e => setValue(e.target.value)}
onKeyDown={handleKeyDown}
{...props}
/>
</div>
);
}
);
InputDialogInput.displayName = 'InputDialog.Input';
export interface InputDialogLabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
/** Optional className for the label */
className?: string;
/** For attribute to match input ID */
htmlFor?: string;
children: React.ReactNode;
}
const InputDialogLabel = React.forwardRef<HTMLLabelElement, InputDialogLabelProps>(
({ className, htmlFor = 'dialog-input', children, ...props }, ref) => {
return (
<Label
ref={ref}
className={cn(className)}
htmlFor={htmlFor}
{...props}
>
{children}
</Label>
);
}
);
InputDialogLabel.displayName = 'InputDialog.Label';
export interface InputDialogActionsProps extends React.HTMLAttributes<HTMLDivElement> {
/** Optional className for the actions container */
className?: string;
children: React.ReactNode;
}
const Actions = React.forwardRef<HTMLDivElement, InputDialogActionsProps>(
({ className, children, ...props }, ref) => {
return (
<div
ref={ref}
{...props}
>
<FooterAction className={cn(className)}>
<FooterAction.Right>{children}</FooterAction.Right>
</FooterAction>
</div>
);
}
);
Actions.displayName = 'InputDialog.Actions';
export interface InputDialogActionButtonProps {
/** Optional className for the button */
className?: string;
/** Click handler that receives the current input value */
onClick: (value: string) => void;
children: React.ReactNode;
}
const ActionsSecondary = React.forwardRef<HTMLDivElement, InputDialogActionButtonProps>(
({ className, onClick, children, ...props }, ref) => {
const context = useContext(InputDialogContext);
if (!context) {
throw new Error('InputDialog.ActionsSecondary must be used within an InputDialog');
}
const { value } = context;
return (
<div
ref={ref}
{...props}
>
<FooterAction.Secondary
onClick={() => onClick(value)}
className={cn(className)}
>
{children}
</FooterAction.Secondary>
</div>
);
}
);
ActionsSecondary.displayName = 'InputDialog.ActionsSecondary';
const ActionsPrimary = React.forwardRef<HTMLDivElement, InputDialogActionButtonProps>(
({ className, onClick, children, ...props }, ref) => {
const context = useContext(InputDialogContext);
if (!context) {
throw new Error('InputDialog.ActionsPrimary must be used within an InputDialog');
}
const { value } = context;
return (
<div
ref={ref}
{...props}
data-cy="input-dialog-save-button"
onClick={() => onClick(value)}
>
<FooterAction.Primary
onClick={() => onClick(value)}
className={cn(className)}
>
{children}
</FooterAction.Primary>
</div>
);
}
);
ActionsPrimary.displayName = 'InputDialog.ActionsPrimary';
export const InputDialog = Object.assign(InputDialogRoot, {
Label: InputDialogLabel,
Input: InputDialogInput,
Field,
Actions,
ActionsSecondary,
ActionsPrimary,
});
@@ -0,0 +1,106 @@
import * as React from 'react';
import { Input } from '../Input';
import { ScrollArea } from '../ScrollArea';
import { cn } from '../../lib/utils';
interface PresetDialogProps {
children: React.ReactNode;
className?: string;
}
export function PresetDialog({ children, className }: PresetDialogProps) {
return <div className={cn('flex h-[500px] max-w-lg flex-col', className)}>{children}</div>;
}
/**
* Subcomponent: PresetBody
* A dark "box" container that wraps the filter and grid area.
* Adjust bg color, padding, border, etc. to match your design.
*/
interface PresetBodyProps {
children: React.ReactNode;
className?: string;
}
function PresetBody({ children, className }: PresetBodyProps) {
return (
<div
className={cn(
// Adjust these classes for your desired look
'flex min-h-0 flex-1 flex-col rounded-md border border-white/10 bg-black p-2',
className
)}
>
{children}
</div>
);
}
/**
* Subcomponent: PresetFilter
*/
interface PresetFilterProps {
children: React.ReactNode;
className?: string;
}
function PresetFilter({ children, className }: PresetFilterProps) {
return (
<div className={cn('mb-2 flex w-full flex-shrink-0 items-center space-x-2', className)}>
{children}
</div>
);
}
/**
* Subcomponent: PresetSearch
*/
interface PresetSearchProps extends React.InputHTMLAttributes<HTMLInputElement> {
className?: string;
}
function PresetSearch({ className, ...props }: PresetSearchProps) {
return (
<Input
className={cn('w-full', className)}
{...props}
/>
);
}
/**
* Subcomponent: PresetGrid
*/
interface PresetGridProps {
children: React.ReactNode;
maxHeight?: string;
className?: string;
}
function PresetGrid({ children, maxHeight = 'flex-1', className }: PresetGridProps) {
return (
<ScrollArea className={cn('min-h-0 flex-1', className)}>
<div className="grid grid-cols-4 gap-2 pr-2">{children}</div>
</ScrollArea>
);
}
/**
* Subcomponent: PresetOption
*/
interface PresetOptionProps {
label?: string;
className?: string;
}
function PresetOption({ label = 'Label', className }: PresetOptionProps) {
return (
<div className={cn('flex flex-col items-start space-y-1', className)}>
{/* Default dark placeholder box (swap in an <img> if you like) */}
<div className="bg-popover h-16 w-24 rounded" />
<div className="text-muted-foreground text-left text-base">{label}</div>
</div>
);
}
/* Attach subcomponents to PresetDialog as static properties */
PresetDialog.PresetBody = PresetBody;
PresetDialog.PresetFilter = PresetFilter;
PresetDialog.PresetSearch = PresetSearch;
PresetDialog.PresetGrid = PresetGrid;
PresetDialog.PresetOption = PresetOption;
@@ -0,0 +1,2 @@
export { InputDialog } from './InputDialog';
export { PresetDialog } from './PresetDialog';
@@ -0,0 +1,114 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
import { Button } from '../Button';
import { Icons } from '../Icons';
interface AboutModalProps {
children: React.ReactNode;
className?: string;
}
export function AboutModal({ children, className }: AboutModalProps) {
return <div className={cn('space-y-1 text-center', className)}>{children}</div>;
}
/** Sub component: Product Name */
interface ProductNameProps {
children: React.ReactNode;
className?: string;
}
function ProductName({ children, className }: ProductNameProps) {
return (
<div className={cn('text-foreground pt-3 text-2xl font-medium leading-none', className)}>
{children}
</div>
);
}
/** Sub-component: Product Version */
interface ProductVersionProps {
children: React.ReactNode;
className?: string;
}
function ProductVersion({ children, className }: ProductVersionProps) {
return (
<div className={cn('text-muted-foreground text-2xl font-light leading-none', className)}>
{children}
</div>
);
}
/** Sub-component: Product Beta */
interface ProductBetaProps {
children: React.ReactNode;
className?: string;
}
function ProductBeta({ children, className }: ProductBetaProps) {
return (
<div className={cn('text-muted-foreground text-xl font-light', className)}>{children}</div>
);
}
/** Sub-component: Body (wraps all detail items) */
interface BodyProps {
children: React.ReactNode;
className?: string;
}
function Body({ children, className }: BodyProps) {
return (
<div className={cn('my-3 flex flex-col items-center space-y-0', className)}>{children}</div>
);
}
/** Sub-component: Detail Item */
interface DetailItemProps {
label: string;
value: string;
className?: string;
}
function DetailItem({ label, value, className }: DetailItemProps) {
return (
<div className={cn('flex flex-col items-center', className)}>
<div className="text-muted-foreground pt-2 text-sm font-semibold tracking-wide">{label}</div>
<div className="text-muted-foreground text-sm">{value}</div>
</div>
);
}
/** Sub-component: Social Item */
interface SocialItemProps {
icon: string;
url: string;
text: string;
className?: string;
}
function SocialItem({ icon, url, text, className }: SocialItemProps) {
return (
<div className={cn('text-foreground flex items-center', className)}>
<div className="inline-block">
<Icons.ByName name={icon} />
</div>
<Button
asChild
variant="link"
className="py-6 text-lg"
>
<a
href={`https://github.com/${url}`}
target="_blank"
rel="noopener noreferrer"
>
{text}
</a>
</Button>
</div>
);
}
/** Attach sub-components to AboutModal as static properties */
AboutModal.ProductName = ProductName;
AboutModal.ProductVersion = ProductVersion;
AboutModal.ProductBeta = ProductBeta;
AboutModal.Body = Body;
AboutModal.DetailItem = DetailItem;
AboutModal.SocialItem = SocialItem;
@@ -0,0 +1,231 @@
import * as React from 'react';
import { Input } from '../Input/Input';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../Select/Select';
import { Switch } from '../Switch/Switch';
import { cn } from '../../lib/utils';
interface ImageModalProps {
children: React.ReactNode;
className?: string;
}
/**
* Main ImageModal container. By default, we do not force any
* layout here. We'll use a "Body" subcomponent for the main area
* that sets up a flex row with a 70/30 split.
*/
export function ImageModal({ children, className }: ImageModalProps) {
return <div className={cn(className)}>{children}</div>;
}
/* -------------------------------------------------------------------------- */
/* Body subcomponent */
interface ImageBodyProps {
children: React.ReactNode;
className?: string;
}
function ImageBody({ children, className }: ImageBodyProps) {
return <div className={cn('flex flex-col sm:flex-row', className)}>{children}</div>;
}
/* -------------------------------------------------------------------------- */
/* ImageVisual subcomponent */
interface ImageVisualProps {
className?: string;
children: React.ReactNode;
}
function ImageVisual({ children, className }: ImageVisualProps) {
return (
<div
className={cn(
'flex-1 items-center justify-center rounded-2xl bg-black/80 p-4 sm:flex-[7]',
'flex', // ensure the container is a flex box
className
)}
>
<div className="h-[512px] w-[512px] overflow-auto">{children}</div>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* ImageOptions subcomponent */
interface ImageOptionsProps {
children: React.ReactNode;
className?: string;
}
function ImageOptions({ children, className }: ImageOptionsProps) {
return <div className={cn('flex-1 space-y-5 p-4 sm:flex-[3]', className)}>{children}</div>;
}
/* -------------------------------------------------------------------------- */
/* Filename subcomponent */
interface FilenameProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'children'> {
children: React.ReactNode;
className?: string;
/** Handler is optional. If not provided, default to a no‐op. */
onChange?: React.ChangeEventHandler<HTMLInputElement>;
}
function Filename({ children, className, value, onChange, ...props }: FilenameProps) {
return (
<div className={cn('text-foreground space-y-1', className)}>
<label className="block text-base">{children}</label>
<Input
{...props}
className={cn('w-full', className)}
value={value}
onChange={onChange ?? (() => {})}
/>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* Filetype subcomponent */
interface FiletypeProps {
selected: string;
/** Handler is optional. If not provided, we do nothing. */
onSelect?: (val: string) => void;
className?: string;
/** Array of file type options */
options?: Array<{ value: string; label: string }>;
}
function Filetype({ selected, onSelect, className, options = [] }: FiletypeProps) {
const defaultOptions = [
{ value: 'jpg', label: 'JPG' },
{ value: 'png', label: 'PNG' },
];
const fileTypeOptions = options.length ? options : defaultOptions;
return (
<Select
value={selected}
onValueChange={val => onSelect?.(val)}
>
<SelectTrigger
aria-label="File type"
className={cn('w-[5.5rem] sm:w-24', className)}
>
<SelectValue />
</SelectTrigger>
<SelectContent>
{fileTypeOptions.map(option => (
<SelectItem
key={option.value}
value={option.value}
>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
/* -------------------------------------------------------------------------- */
/* ImageSize subcomponent */
interface ImageSizeProps {
children: React.ReactNode;
width: string;
height: string;
/** Handlers optional. If not provided, default no‐op. */
onWidthChange?: React.ChangeEventHandler<HTMLInputElement>;
onHeightChange?: React.ChangeEventHandler<HTMLInputElement>;
className?: string;
maxWidth?: string;
maxHeight?: string;
}
function ImageSize({
children,
width,
height,
onWidthChange,
onHeightChange,
className,
maxWidth,
maxHeight,
}: ImageSizeProps) {
return (
<div className={cn('text-foreground space-y-1', className)}>
<label className="block text-base">{children}</label>
{/* Flex container for width/height inputs */}
<div className="flex items-center space-x-4">
{/* Width group */}
<div className="flex items-center space-x-2">
<span className="text-foreground text-base">W</span>
<Input
value={width}
onChange={onWidthChange ?? (() => {})}
placeholder="Width"
className="w-20"
max={maxWidth}
/>
</div>
{/* Height group */}
<div className="text-foreground flex items-center space-x-2 text-base">
<span className="text-foreground text-base">H</span>
<Input
value={height}
onChange={onHeightChange ?? (() => {})}
placeholder="Height"
className="w-20"
max={maxHeight}
/>
</div>
</div>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* SwitchOption subcomponent */
interface SwitchOptionProps {
children: React.ReactNode;
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
className?: string;
}
function SwitchOption({
children,
checked,
defaultChecked,
onCheckedChange,
className,
}: SwitchOptionProps) {
return (
<div className={cn('text-foreground flex items-center space-x-2', className)}>
<Switch
checked={checked}
defaultChecked={defaultChecked}
onCheckedChange={val => onCheckedChange?.(val)}
/>
<span className="text-base">{children}</span>
</div>
);
}
/* -------------------------------------------------------------------------- */
/* Attach subcomponents onto the main ImageModal function. */
ImageModal.Body = ImageBody;
ImageModal.ImageVisual = ImageVisual;
ImageModal.ImageOptions = ImageOptions;
ImageModal.Filename = Filename;
ImageModal.Filetype = Filetype;
ImageModal.ImageSize = ImageSize;
ImageModal.SwitchOption = SwitchOption;
@@ -0,0 +1,120 @@
import * as React from 'react';
import { Label } from '../Label';
import { Input } from '../Input';
import { cn } from '../../lib/utils';
interface UserPreferencesModalProps {
children: React.ReactNode;
className?: string;
}
export function UserPreferencesModal({ children, className }: UserPreferencesModalProps) {
return (
<div className={cn('flex max-h-[80vh] w-full max-w-4xl flex-col overflow-hidden', className)}>
{children}
</div>
);
}
/** Body
* Automatically wraps content in a scrollable area.
*/
interface BodyProps {
children: React.ReactNode;
className?: string;
}
function Body({ children, className }: BodyProps) {
return (
<div className={cn('flex-1 overflow-y-auto', className)}>
<div className={cn('mt-1 mb-4 flex flex-col space-y-4', className)}>{children}</div>
</div>
);
}
/** Subheading
* Section labels
*/
interface SubHeadingProps {
children: React.ReactNode;
className?: string;
}
function SubHeading({ children, className }: SubHeadingProps) {
return <span className={cn('text-muted-foreground text-lg', className)}>{children}</span>;
}
/** Responsive 3-column grid for hotkeys, etc. */
interface HotkeysGridProps {
children: React.ReactNode;
className?: string;
}
function HotkeysGrid({ children, className }: HotkeysGridProps) {
return (
<div className={cn('grid grid-cols-1 gap-3 gap-x-16 md:grid-cols-2 lg:grid-cols-3', className)}>
{children}
</div>
);
}
/** A single hotkey row: label + input */
interface HotkeyProps {
label: string;
placeholder?: string;
className?: string;
value?: string;
onChange?: (value: string) => void;
hotkeys?: {
record: (callback: (sequence: string[]) => void) => void;
pause: () => void;
unpause: () => void;
startRecording: () => void;
};
}
function Hotkey({ label, placeholder, className, value, onChange, hotkeys }: HotkeyProps) {
const [isRecording, setIsRecording] = React.useState(false);
const onInputKeyDown = (event: React.KeyboardEvent) => {
event.preventDefault();
hotkeys?.record((sequence: string[]) => {
const keys = sequence.join('+');
hotkeys?.unpause();
setIsRecording(false);
onChange?.(keys);
});
};
const onFocus = () => {
setIsRecording(true);
hotkeys?.pause();
hotkeys?.startRecording();
};
const onBlur = () => {
setIsRecording(false);
hotkeys?.unpause();
};
return (
<div className={cn('flex items-center justify-between space-x-2', className)}>
<Label className="whitespace-nowrap">{label}</Label>
<Input
className={cn(
'w-16 text-center transition-colors',
isRecording && 'bg-accent text-accent-foreground caret-accent-foreground'
)}
placeholder={isRecording ? 'Press keys...' : placeholder}
value={value}
onKeyDown={onInputKeyDown}
onFocus={onFocus}
onBlur={onBlur}
readOnly={!isRecording}
/>
</div>
);
}
/** Attach subcomponents as static properties for a nicer API */
UserPreferencesModal.Body = Body;
UserPreferencesModal.HotkeysGrid = HotkeysGrid;
UserPreferencesModal.Hotkey = Hotkey;
UserPreferencesModal.SubHeading = SubHeading;
@@ -0,0 +1,3 @@
export { UserPreferencesModal } from './UserPreferencesModal';
export { ImageModal } from './ImageModal';
export { AboutModal } from './AboutModal';
+11
View File
@@ -54,6 +54,10 @@ import { DisplaySetMessageListTooltip } from './DisplaySetMessageListTooltip';
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './Tooltip';
import { ToolboxUI, Toolbox } from './OHIFToolbox';
import Numeric from './Numeric';
import { InputDialog, PresetDialog } from './OHIFDialogs';
import { AboutModal, ImageModal, UserPreferencesModal } from './OHIFModals';
import Modal from './Modal/Modal';
import { FooterAction } from './FooterAction';
import {
DropdownMenu,
@@ -225,4 +229,11 @@ export {
ToolButtonListDropDown,
ToolButtonListItem,
ToolButtonListDivider,
InputDialog,
PresetDialog,
Modal,
AboutModal,
ImageModal,
UserPreferencesModal,
FooterAction,
};
@@ -0,0 +1,89 @@
import React, { useState, createContext, useContext, useCallback, useEffect, useMemo } from 'react';
import ManagedDialog, { ManagedDialogProps } from './ManagedDialog';
interface DialogContextType {
show: (options: ManagedDialogProps) => string;
hide: (id: string) => void;
hideAll: () => void;
isEmpty: () => boolean;
}
interface DialogService {
setServiceImplementation: (implementation: DialogContextType) => void;
getCustomComponent?: () => React.ComponentType<ManagedDialogProps> | null;
}
interface DialogProviderProps {
children: React.ReactNode;
dialog?: React.ComponentType<ManagedDialogProps>;
service?: DialogService | null;
}
const DialogContext = createContext<DialogContextType | null>(null);
export const useDialog = () => {
const context = useContext(DialogContext);
if (!context) {
throw new Error('useDialog must be used within a DialogProvider');
}
return context;
};
const DialogProvider: React.FC<DialogProviderProps> = ({
children,
dialog: DialogComponent = ManagedDialog,
service = null,
}) => {
const [dialogs, setDialogs] = useState<(ManagedDialogProps & { id: string })[]>([]);
const show = useCallback((options: ManagedDialogProps) => {
const id = options.id;
setDialogs(prev => [...prev, { ...options, id }]);
return id;
}, []);
const hide = useCallback((id: string) => {
setDialogs(prev => prev.filter(dialog => dialog.id !== id));
}, []);
const hideAll = useCallback(() => {
setDialogs([]);
}, []);
const isEmpty = useCallback(() => dialogs.length === 0, [dialogs]);
const contextValue = useMemo(
() => ({
show,
hide,
hideAll,
isEmpty,
}),
[show, hide, hideAll, isEmpty]
);
useEffect(() => {
if (service) {
service.setServiceImplementation(contextValue);
}
}, [service, contextValue]);
const CustomDialog = service?.getCustomComponent?.();
const RenderedDialog = CustomDialog || DialogComponent;
return (
<DialogContext.Provider value={contextValue}>
{dialogs.map(dialog => (
<RenderedDialog
key={dialog.id}
onClose={hide}
isOpen={true}
{...dialog}
/>
))}
{children}
</DialogContext.Provider>
);
};
export { DialogProvider };
@@ -0,0 +1,76 @@
import React from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../components/Dialog/Dialog';
import { cn } from '../lib/utils';
export interface ManagedDialogProps {
id: string;
isOpen?: boolean;
title?: string;
description?: string;
content: React.ComponentType<{ hide?: () => void }>;
contentProps?: Record<string, unknown>;
isDraggable?: boolean;
shouldCloseOnEsc?: boolean;
shouldCloseOnOverlayClick?: boolean;
defaultPosition?: { x: number; y: number };
onClose?: (id: string) => void;
unstyled?: boolean;
showOverlay?: boolean;
containerClassName?: string;
}
const ManagedDialog: React.FC<ManagedDialogProps> = ({
id,
isOpen,
title,
content: DialogContentComponent,
contentProps,
isDraggable,
shouldCloseOnEsc = false,
shouldCloseOnOverlayClick = false,
showOverlay = true,
defaultPosition,
onClose,
unstyled,
containerClassName,
}) => {
return (
<Dialog
open={isOpen}
modal={false} // keep modal behavior off for independent windows
onOpenChange={open => {
if (!open) {
onClose(id);
}
}}
isDraggable={isDraggable}
shouldCloseOnEsc={shouldCloseOnEsc}
shouldCloseOnOverlayClick={shouldCloseOnOverlayClick}
showOverlay={showOverlay}
>
<DialogContent
className={cn(unstyled ? 'p-0' : '', containerClassName)}
style={{
...(defaultPosition
? {
position: 'fixed',
left: `${defaultPosition.x}px`,
top: `${defaultPosition.y}px`,
transform: 'translate(0, 0)',
margin: 0,
animation: 'none',
}
: {}),
}}
>
{!unstyled && <DialogHeader>{title && <DialogTitle>{title}</DialogTitle>}</DialogHeader>}
<DialogContentComponent
{...contentProps}
hide={() => onClose(id)}
/>
</DialogContent>
</Dialog>
);
};
export default ManagedDialog;
@@ -0,0 +1,102 @@
import React, { useState, createContext, useContext, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
interface ModalOptions {
title?: string;
shouldCloseOnEsc?: boolean;
content?: React.ComponentType;
contentProps?: Record<string, unknown>;
containerClassName?: string;
}
interface ModalContextType {
show: (options: Partial<ModalOptions>) => void;
hide: () => void;
containerClassName?: string;
}
interface ModalComponentProps {
isOpen: boolean;
onClose: () => void;
title?: string;
children: React.ReactNode;
}
const ModalContext = createContext<ModalContextType | null>(null);
export const useModal = () => {
const ctx = useContext(ModalContext);
if (!ctx) {
throw new Error('useModal must be used within a ModalProvider');
}
return ctx;
};
const DEFAULT_OPTIONS: ModalOptions = {
title: '',
shouldCloseOnEsc: true,
};
interface ModalService {
setServiceImplementation: (implementation: ModalContextType) => void;
getCustomComponent: () => React.ComponentType<ModalComponentProps> | null;
}
interface ModalProviderProps {
children: React.ReactNode;
modal: React.ComponentType<ModalComponentProps>;
service?: ModalService | null;
}
const ModalProvider: React.FC<ModalProviderProps> = ({
children,
modal: ModalComponent,
service = null,
}) => {
const { t } = useTranslation('Modals');
const [options, setOptions] = useState<ModalOptions>(DEFAULT_OPTIONS);
const ModalContent = options.content;
const show = useCallback((props: Partial<ModalOptions>) => {
setOptions(prev => ({ ...prev, ...props }));
}, []);
const hide = useCallback(() => {
setOptions(DEFAULT_OPTIONS);
}, []);
useEffect(() => {
if (service) {
service.setServiceImplementation({ show, hide });
}
}, [hide, service, show]);
const { title } = options;
const CustomModal = service?.getCustomComponent();
const RenderedModal = CustomModal || ModalComponent;
return (
<ModalContext.Provider value={{ show, hide }}>
{ModalContent && (
<RenderedModal
isOpen={true}
onClose={hide}
title={t(title)}
{...options}
>
<ModalContent
{...options.contentProps}
show={show}
hide={hide}
/>
</RenderedModal>
)}
{children}
</ModalContext.Provider>
);
};
export { ModalProvider };
export const ModalConsumer = ModalContext.Consumer;
@@ -1,7 +1,13 @@
import NotificationProvider, { useNotification } from './NotificationProvider';
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './ViewportGridProvider';
import { ToolboxProvider, useToolbox } from './ToolboxContext';
import { ModalProvider, useModal } from './ModalProvider';
import { DialogProvider, useDialog } from './DialogProvider';
import ManagedDialog from './ManagedDialog';
export { useNotification, NotificationProvider };
export { ViewportGridContext, ViewportGridProvider, useViewportGrid };
export { ToolboxProvider, useToolbox };
export { ModalProvider, useModal };
export { DialogProvider, useDialog };
export { ManagedDialog };
+24
View File
@@ -100,6 +100,13 @@ import {
ToolButtonListDivider,
Toolbox,
Numeric,
InputDialog,
PresetDialog,
Modal,
AboutModal,
ImageModal,
UserPreferencesModal,
FooterAction,
} from './components';
import { DataRow } from './components/DataRow';
@@ -108,6 +115,11 @@ import {
NotificationProvider,
useToolbox,
ToolboxProvider,
useModal,
ModalProvider,
DialogProvider,
useDialog,
ManagedDialog,
} from './contextProviders';
import { ViewportGridContext, ViewportGridProvider, useViewportGrid } from './contextProviders';
import * as utils from './utils';
@@ -225,4 +237,16 @@ export {
useToolbox,
utils,
Numeric,
AboutModal,
ImageModal,
UserPreferencesModal,
InputDialog,
PresetDialog,
Modal,
useModal,
ModalProvider,
FooterAction,
DialogProvider,
useDialog,
ManagedDialog,
};