@stack-spot/portal-components 1.0.0-dev.1775743853760 → 1.0.0-dev.1778512145216

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,165 @@
1
+ import { type AriaAttributes, type CSSProperties, type PropsWithChildren, type SyntheticEvent } from 'react';
2
+ import './style.css';
3
+ /**
4
+ * Available size options for the dialog.
5
+ */
6
+ export type DialogSize = 'small' | 'medium' | 'large' | 'extra-large' | 'fit-content';
7
+ /**
8
+ * Type of dialog display.
9
+ *
10
+ * @remarks
11
+ * - `modal`: Centered dialog with backdrop
12
+ * - `right-panel`: Slide-in panel from the right side
13
+ */
14
+ type DialogType = 'modal' | 'right-panel';
15
+ /**
16
+ * Props for controlled dialog state.
17
+ */
18
+ type ControlledProps = {
19
+ /**
20
+ * Controls whether the dialog is open.
21
+ */
22
+ isOpen: boolean;
23
+ /**
24
+ * Callback to update the dialog's open state.
25
+ */
26
+ setIsOpen: (value: boolean) => void;
27
+ };
28
+ /**
29
+ * Props for uncontrolled dialog state.
30
+ *
31
+ * @remarks
32
+ * Use this when you want the dialog to manage its own state internally.
33
+ */
34
+ type UncontrolledProps = {
35
+ isOpen?: never;
36
+ setIsOpen?: never;
37
+ };
38
+ type AriaProps = Pick<AriaAttributes, 'aria-labelledby' | 'aria-label' | 'aria-describedby' | 'aria-description'>;
39
+ export type DialogProps = PropsWithChildren & (ControlledProps | UncontrolledProps) & AriaProps & {
40
+ /**
41
+ * The display type of the dialog.
42
+ *
43
+ * @defaultValue 'modal'
44
+ */
45
+ type?: DialogType;
46
+ /**
47
+ * Whether clicking outside the dialog should close it.
48
+ *
49
+ * @defaultValue true
50
+ */
51
+ shouldLightDismiss?: boolean;
52
+ /**
53
+ * Initial open state for uncontrolled dialogs.
54
+ *
55
+ * @defaultValue false
56
+ */
57
+ initialOpen?: boolean;
58
+ /**
59
+ * Callback fired when the dialog is closed.
60
+ */
61
+ onClose?: (event?: SyntheticEvent) => void;
62
+ /**
63
+ * The size of the dialog.
64
+ *
65
+ * @defaultValue 'medium'
66
+ */
67
+ size?: DialogSize;
68
+ /**
69
+ * Custom inline styles for the dialog element.
70
+ */
71
+ style?: CSSProperties;
72
+ /**
73
+ * Additional CSS class names.
74
+ */
75
+ className?: string;
76
+ /**
77
+ * Optional identifier for the dialog element.
78
+ */
79
+ id?: string;
80
+ };
81
+ export interface DialogRef extends Pick<HTMLDialogElement, 'addEventListener' | 'removeEventListener'> {
82
+ close: () => void;
83
+ showModal: () => void;
84
+ isOpen: () => boolean;
85
+ }
86
+ /**
87
+ * A flexible dialog component that supports both modal and panel modes.
88
+ *
89
+ * @remarks
90
+ * The Dialog component provides a native HTML dialog element with enhanced functionality:
91
+ * - **Controlled or uncontrolled state**: Manage state externally or let the component handle it
92
+ * - **Light dismiss**: Optional backdrop click to close
93
+ * - **Keyboard support**: ESC key closes the dialog
94
+ * - **Multiple sizes**: Predefined size options
95
+ * - **Accessibility**: Full ARIA support
96
+ * - **Two display modes**: Modal (centered) or right panel (slide-in)
97
+ *
98
+ * **Important:** When using controlled mode, you must provide both `isOpen` and `setIsOpen` props.
99
+ * For uncontrolled mode, use the ref to control the dialog programmatically.
100
+ *
101
+ * @example
102
+ * Uncontrolled dialog with ref:
103
+ * ```tsx
104
+ * const dialogRef = useRef<DialogRef>(null)
105
+ *
106
+ * const handleOpen = () => {
107
+ * dialogRef.current?.showModal()
108
+ * }
109
+ *
110
+ * return (
111
+ * <>
112
+ * <button onClick={handleOpen}>Open Dialog</button>
113
+ * <Dialog
114
+ * ref={dialogRef}
115
+ * size="large"
116
+ * aria-label="Example dialog"
117
+ * >
118
+ * <Text>Dialog content</Text>
119
+ * </Dialog>
120
+ * </>
121
+ * )
122
+ * ```
123
+ *
124
+ * @example
125
+ * Controlled dialog:
126
+ * ```tsx
127
+ * const [isOpen, setIsOpen] = useState(false)
128
+ *
129
+ * return (
130
+ * <>
131
+ * <button onClick={() => setIsOpen(true)}>Open Dialog</button>
132
+ * <Dialog
133
+ * isOpen={isOpen}
134
+ * setIsOpen={setIsOpen}
135
+ * type="right-panel"
136
+ * shouldLightDismiss={false}
137
+ * onClose={() => console.log('Dialog closed')}
138
+ * >
139
+ * <Text>Panel content</Text>
140
+ * </Dialog>
141
+ * </>
142
+ * )
143
+ * ```
144
+ *
145
+ * @example
146
+ * Dialog with custom styling:
147
+ * ```tsx
148
+ * <Dialog
149
+ * size="fit-content"
150
+ * className="custom-dialog"
151
+ * style={{ maxWidth: '600px' }}
152
+ * aria-labelledby="dialog-title"
153
+ * >
154
+ * <h2 id="dialog-title">Custom Dialog</h2>
155
+ * <p>Content here</p>
156
+ * </Dialog>
157
+ * ```
158
+ *
159
+ * @param props - Dialog properties
160
+ * @param ref - Forwarded ref for imperative control
161
+ * @returns A dialog element
162
+ */
163
+ export declare const Dialog: import("react").ForwardRefExoticComponent<DialogProps & import("react").RefAttributes<DialogRef>>;
164
+ export {};
165
+ //# sourceMappingURL=Dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Dialog.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/Dialog.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EAMpB,MAAM,OAAO,CAAA;AACd,OAAO,aAAa,CAAA;AACpB;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,aAAa,GAAG,aAAa,CAAA;AAErF;;;;;;GAMG;AACH,KAAK,UAAU,GAAG,OAAO,GAAG,aAAa,CAAA;AAEzC;;GAEG;AACH,KAAK,eAAe,GAAG;IACrB;;OAEG;IACH,MAAM,EAAE,OAAO,CAAC;IAEhB;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC,CAAA;AAED;;;;;GAKG;AACH,KAAK,iBAAiB,GAAG;IACvB,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB,CAAA;AAED,KAAK,SAAS,GAAG,IAAI,CAAC,cAAc,EAAE,iBAAiB,GAAG,YAAY,GAAG,kBAAkB,GAAG,kBAAkB,CAAC,CAAA;AACjH,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,CAAC,eAAe,GAAG,iBAAiB,CAAC,GAAG,SAAS,GAAG;IAChG;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAElB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,cAAc,KAAK,IAAI,CAAC;IAE3C;;;;OAIG;IACH,IAAI,CAAC,EAAE,UAAU,CAAC;IAElB;;OAEG;IACH,KAAK,CAAC,EAAE,aAAa,CAAC;IAEtB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAA;AAED,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,iBAAiB,EAAE,kBAAkB,GAAG,qBAAqB,CAAC;IACpG,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,OAAO,CAAC;CACvB;AAmFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AACH,eAAO,MAAM,MAAM,mGAgDlB,CAAA"}
@@ -0,0 +1,170 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState, } from 'react';
3
+ import './style.css';
4
+ /**
5
+ * Custom hook for managing dialog state and behavior.
6
+ *
7
+ * @remarks
8
+ * This hook handles:
9
+ * - Opening and closing the dialog
10
+ * - Light dismiss (clicking outside to close)
11
+ * - Escape key handling
12
+ * - Cleanup on unmount
13
+ *
14
+ * @param props - Dialog configuration options
15
+ * @returns Dialog state and control methods
16
+ */
17
+ const useDialog = ({ isOpen: controlledOpen, setIsOpen: setControlledOpen, initialOpen = false, shouldLightDismiss = true, }) => {
18
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen);
19
+ const dialogRef = useRef(null);
20
+ const isOpen = controlledOpen ?? uncontrolledOpen;
21
+ const setIsOpen = setControlledOpen ?? setUncontrolledOpen;
22
+ useEffect(() => {
23
+ const dialog = dialogRef.current;
24
+ if (isOpen && dialog && !dialog.open) {
25
+ dialog.showModal();
26
+ }
27
+ else if (!isOpen) {
28
+ dialog?.close();
29
+ }
30
+ return () => {
31
+ dialog?.close();
32
+ };
33
+ }, [isOpen]);
34
+ useEffect(() => {
35
+ const dialog = dialogRef.current;
36
+ if (!dialog)
37
+ return;
38
+ const handleClose = (event) => {
39
+ event.preventDefault();
40
+ event.stopPropagation();
41
+ setIsOpen(false);
42
+ };
43
+ const lightDismiss = (event) => {
44
+ const { target } = event;
45
+ if (target instanceof Element && target.nodeName === 'DIALOG') {
46
+ handleClose(event);
47
+ }
48
+ };
49
+ const closeOnEscape = (event) => {
50
+ if (event.code === 'Escape') {
51
+ handleClose(event);
52
+ }
53
+ };
54
+ if (shouldLightDismiss) {
55
+ dialog.addEventListener('click', lightDismiss);
56
+ }
57
+ dialog.addEventListener('keydown', closeOnEscape);
58
+ return () => {
59
+ if (shouldLightDismiss) {
60
+ dialog.removeEventListener('click', lightDismiss);
61
+ }
62
+ dialog.removeEventListener('keydown', closeOnEscape);
63
+ };
64
+ }, [shouldLightDismiss, setIsOpen]);
65
+ return {
66
+ dialogRef,
67
+ isOpen,
68
+ close: () => setIsOpen(false),
69
+ showModal: () => setIsOpen(true),
70
+ };
71
+ };
72
+ /**
73
+ * A flexible dialog component that supports both modal and panel modes.
74
+ *
75
+ * @remarks
76
+ * The Dialog component provides a native HTML dialog element with enhanced functionality:
77
+ * - **Controlled or uncontrolled state**: Manage state externally or let the component handle it
78
+ * - **Light dismiss**: Optional backdrop click to close
79
+ * - **Keyboard support**: ESC key closes the dialog
80
+ * - **Multiple sizes**: Predefined size options
81
+ * - **Accessibility**: Full ARIA support
82
+ * - **Two display modes**: Modal (centered) or right panel (slide-in)
83
+ *
84
+ * **Important:** When using controlled mode, you must provide both `isOpen` and `setIsOpen` props.
85
+ * For uncontrolled mode, use the ref to control the dialog programmatically.
86
+ *
87
+ * @example
88
+ * Uncontrolled dialog with ref:
89
+ * ```tsx
90
+ * const dialogRef = useRef<DialogRef>(null)
91
+ *
92
+ * const handleOpen = () => {
93
+ * dialogRef.current?.showModal()
94
+ * }
95
+ *
96
+ * return (
97
+ * <>
98
+ * <button onClick={handleOpen}>Open Dialog</button>
99
+ * <Dialog
100
+ * ref={dialogRef}
101
+ * size="large"
102
+ * aria-label="Example dialog"
103
+ * >
104
+ * <Text>Dialog content</Text>
105
+ * </Dialog>
106
+ * </>
107
+ * )
108
+ * ```
109
+ *
110
+ * @example
111
+ * Controlled dialog:
112
+ * ```tsx
113
+ * const [isOpen, setIsOpen] = useState(false)
114
+ *
115
+ * return (
116
+ * <>
117
+ * <button onClick={() => setIsOpen(true)}>Open Dialog</button>
118
+ * <Dialog
119
+ * isOpen={isOpen}
120
+ * setIsOpen={setIsOpen}
121
+ * type="right-panel"
122
+ * shouldLightDismiss={false}
123
+ * onClose={() => console.log('Dialog closed')}
124
+ * >
125
+ * <Text>Panel content</Text>
126
+ * </Dialog>
127
+ * </>
128
+ * )
129
+ * ```
130
+ *
131
+ * @example
132
+ * Dialog with custom styling:
133
+ * ```tsx
134
+ * <Dialog
135
+ * size="fit-content"
136
+ * className="custom-dialog"
137
+ * style={{ maxWidth: '600px' }}
138
+ * aria-labelledby="dialog-title"
139
+ * >
140
+ * <h2 id="dialog-title">Custom Dialog</h2>
141
+ * <p>Content here</p>
142
+ * </Dialog>
143
+ * ```
144
+ *
145
+ * @param props - Dialog properties
146
+ * @param ref - Forwarded ref for imperative control
147
+ * @returns A dialog element
148
+ */
149
+ export const Dialog = forwardRef(({ type = 'modal', shouldLightDismiss = true, initialOpen = false, isOpen: controlledOpen, setIsOpen: setControlledOpen, onClose, children, size = 'medium', style, className, ...props }, forwardedRef) => {
150
+ const { dialogRef, isOpen, close, showModal } = useDialog({
151
+ isOpen: controlledOpen,
152
+ setIsOpen: setControlledOpen,
153
+ initialOpen,
154
+ shouldLightDismiss,
155
+ });
156
+ useImperativeHandle(forwardedRef, () => ({
157
+ close,
158
+ showModal,
159
+ isOpen: () => isOpen,
160
+ addEventListener(name, callback, options) {
161
+ dialogRef.current?.addEventListener(name, callback, options);
162
+ },
163
+ removeEventListener(name, callback, options) {
164
+ dialogRef.current?.removeEventListener(name, callback, options);
165
+ },
166
+ }), [isOpen, close, showModal]);
167
+ return (_jsx("dialog", { ref: dialogRef, onClose: onClose, ...props, className: `${type} ${size}${className ? ` ${className}` : ''}`, style: style, children: _jsx("div", { className: "dialog-content", children: children }) }));
168
+ });
169
+ Dialog.displayName = 'Dialog';
170
+ //# sourceMappingURL=Dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Dialog.js","sourceRoot":"","sources":["../../../src/components/Modal/Dialog.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import { DialogProps, DialogRef } from './Dialog.js';
2
+ /**
3
+ * Props for the Modal component.
4
+ */
5
+ export type ModalProps = DialogProps & {
6
+ /**
7
+ * The title text displayed in the modal header.
8
+ */
9
+ title?: string;
10
+ /**
11
+ * The subtitle text displayed below the title in the modal header.
12
+ */
13
+ subtitle?: string;
14
+ /**
15
+ * Optional identifier for the modal element.
16
+ */
17
+ id?: string;
18
+ };
19
+ /**
20
+ * A modal dialog component that displays content in an overlay with a header and close button.
21
+ *
22
+ * @example
23
+ * Using ref to control modal:
24
+ * ```tsx
25
+ * const modalRef = useRef<DialogRef>(null)
26
+ *
27
+ * const handleOpenModal = () => {
28
+ * modalRef.current?.showModal()
29
+ * }
30
+ *
31
+ * return (
32
+ * <>
33
+ * <button onClick={handleOpenModal}>Open Modal</button>
34
+ * <Modal
35
+ * ref={modalRef}
36
+ * title="Welcome"
37
+ * subtitle="Please review the information below"
38
+ * >
39
+ * <Text>Modal content goes here</Text>
40
+ * </Modal>
41
+ * </>
42
+ * )
43
+ * ```
44
+ *
45
+ * @example
46
+ * Using controlled state:
47
+ * ```tsx
48
+ * const [isOpen, setIsOpen] = useState(false)
49
+ *
50
+ * return (
51
+ * <>
52
+ * <button onClick={() => setIsOpen(true)}>Open Modal</button>
53
+ * <Modal
54
+ * title="Confirmation"
55
+ * subtitle="Are you sure?"
56
+ * isOpen={isOpen}
57
+ * setIsOpen={setIsOpen}
58
+ * >
59
+ * <Text>Confirm your action</Text>
60
+ * </Modal>
61
+ * </>
62
+ * )
63
+ * ```
64
+ *
65
+ * @param props - The modal properties
66
+ * @param ref - Forwarded ref to access Dialog methods
67
+ * @returns A modal dialog component
68
+ */
69
+ export declare const Modal: import("react").ForwardRefExoticComponent<ModalProps & import("react").RefAttributes<DialogRef>>;
70
+ //# sourceMappingURL=Modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/Modal.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAU,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAEzD;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG;IACrC;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,eAAO,MAAM,KAAK,kGAiCjB,CAAA"}
@@ -0,0 +1,67 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Column, IconBox, Row, Text } from '@stack-spot/citric-react';
3
+ import { forwardRef } from 'react';
4
+ import { Dialog } from './Dialog.js';
5
+ /**
6
+ * A modal dialog component that displays content in an overlay with a header and close button.
7
+ *
8
+ * @example
9
+ * Using ref to control modal:
10
+ * ```tsx
11
+ * const modalRef = useRef<DialogRef>(null)
12
+ *
13
+ * const handleOpenModal = () => {
14
+ * modalRef.current?.showModal()
15
+ * }
16
+ *
17
+ * return (
18
+ * <>
19
+ * <button onClick={handleOpenModal}>Open Modal</button>
20
+ * <Modal
21
+ * ref={modalRef}
22
+ * title="Welcome"
23
+ * subtitle="Please review the information below"
24
+ * >
25
+ * <Text>Modal content goes here</Text>
26
+ * </Modal>
27
+ * </>
28
+ * )
29
+ * ```
30
+ *
31
+ * @example
32
+ * Using controlled state:
33
+ * ```tsx
34
+ * const [isOpen, setIsOpen] = useState(false)
35
+ *
36
+ * return (
37
+ * <>
38
+ * <button onClick={() => setIsOpen(true)}>Open Modal</button>
39
+ * <Modal
40
+ * title="Confirmation"
41
+ * subtitle="Are you sure?"
42
+ * isOpen={isOpen}
43
+ * setIsOpen={setIsOpen}
44
+ * >
45
+ * <Text>Confirm your action</Text>
46
+ * </Modal>
47
+ * </>
48
+ * )
49
+ * ```
50
+ *
51
+ * @param props - The modal properties
52
+ * @param ref - Forwarded ref to access Dialog methods
53
+ * @returns A modal dialog component
54
+ */
55
+ export const Modal = forwardRef(({ title, subtitle, children, ...props }, ref) => {
56
+ const handleClose = () => {
57
+ if (ref && 'current' in ref && ref?.current) {
58
+ ref.current.close();
59
+ }
60
+ else if (props.setIsOpen) {
61
+ props.setIsOpen(false);
62
+ }
63
+ };
64
+ return (_jsx(Dialog, { ref: ref, ...props, children: _jsxs(Column, { bg: "light.400", flex: 1, h: "100%", children: [_jsxs(Row, { justifyContent: "space-between", p: "20px", children: [_jsxs(Column, { children: [_jsx(Text, { appearance: "h4", children: title }), _jsx(Text, { color: "light.700", children: subtitle })] }), _jsx(IconBox, { onClick: handleClose, tag: "button", icon: "TimesMini", appearance: "circle", colorScheme: "light" })] }), _jsx(Column, { flex: 1, className: "content-box", p: "20px", children: children })] }) }));
65
+ });
66
+ Modal.displayName = 'Modal';
67
+ //# sourceMappingURL=Modal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../../src/components/Modal/Modal.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,EAAE,MAAM,EAA0B,MAAM,UAAU,CAAA;AAsBzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,UAAU,CAC7B,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE;IAC/C,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,IAAI,GAAG,IAAI,SAAS,IAAI,GAAG,IAAI,GAAG,EAAE,OAAO,EAAE,CAAC;YAC5C,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;QACrB,CAAC;aAAM,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YAC3B,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACxB,CAAC;IACH,CAAC,CAAA;IAED,OAAO,CACL,KAAC,MAAM,IAAC,GAAG,EAAE,GAAG,KAAM,KAAK,YACzB,MAAC,MAAM,IAAC,EAAE,EAAC,WAAW,EAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAC,MAAM,aACtC,MAAC,GAAG,IAAC,cAAc,EAAC,eAAe,EAAC,CAAC,EAAC,MAAM,aAC1C,MAAC,MAAM,eACL,KAAC,IAAI,IAAC,UAAU,EAAC,IAAI,YAAE,KAAK,GAAQ,EACpC,KAAC,IAAI,IAAC,KAAK,EAAC,WAAW,YAAE,QAAQ,GAAQ,IAClC,EACT,KAAC,OAAO,IACN,OAAO,EAAE,WAAW,EACpB,GAAG,EAAC,QAAQ,EACZ,IAAI,EAAC,WAAW,EAChB,UAAU,EAAC,QAAQ,EACnB,WAAW,EAAC,OAAO,GACnB,IACE,EACN,KAAC,MAAM,IAAC,IAAI,EAAE,CAAC,EAAE,SAAS,EAAC,aAAa,EAAC,CAAC,EAAC,MAAM,YAC9C,QAAQ,GACF,IACF,GACF,CACV,CAAA;AACH,CAAC,CACF,CAAA;AAED,KAAK,CAAC,WAAW,GAAG,OAAO,CAAA"}
@@ -0,0 +1,5 @@
1
+ export { Dialog } from './Dialog.js';
2
+ export type { DialogProps, DialogRef, DialogSize } from './Dialog.js';
3
+ export { Modal } from './Modal.js';
4
+ export type { ModalProps } from './Modal.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAElE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,3 @@
1
+ export { Dialog } from './Dialog.js';
2
+ export { Modal } from './Modal.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Modal/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AAGjC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,103 @@
1
+
2
+ /* Dialog */
3
+ dialog {
4
+ --animation-duration: 0.3s;
5
+ --animation-easing: ease-in-out;
6
+ padding: 0;
7
+ border: 0;
8
+ }
9
+
10
+ dialog.small {
11
+ width: 400px;
12
+ }
13
+
14
+ dialog.medium {
15
+ width: 600px;
16
+ }
17
+
18
+ dialog.large {
19
+ width: 800px;
20
+ }
21
+
22
+ dialog.extra-large {
23
+ width: 70%;
24
+ }
25
+
26
+ dialog>.dialog-content {
27
+ display: flex;
28
+ flex: 1;
29
+ width: 100%;
30
+ height: 100%;
31
+
32
+ .content-box {
33
+ overflow: auto;
34
+ }
35
+ }
36
+
37
+ /* Transition the :backdrop when the dialog modal is promoted to the top layer */
38
+ dialog::backdrop {
39
+ background-color: transparent;
40
+ }
41
+
42
+ dialog:open::backdrop {
43
+ background-color: rgb(0 0 0 / 25%);
44
+ }
45
+
46
+ /* This starting-style rule cannot be nested inside the above selector because the nesting selector cannot represent pseudo-elements. */
47
+ @starting-style {
48
+ dialog:open::backdrop {
49
+ background-color: transparent;
50
+ }
51
+ }
52
+
53
+ /* MODAL */
54
+
55
+ /* Open state of the dialog */
56
+ dialog.modal:open {
57
+ opacity: 1;
58
+ transform: scale(1);
59
+ }
60
+
61
+ /* Closed state of the dialog */
62
+ dialog.modal {
63
+ opacity: 0;
64
+ transform: scale(0.5);
65
+ transition: all var(--animation-duration) allow-discrete;
66
+ }
67
+
68
+ /* Before open state */
69
+ /* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
70
+ @starting-style {
71
+ dialog.modal:open {
72
+ opacity: 0;
73
+ transform: scale(0.5);
74
+ }
75
+ }
76
+
77
+ /* Right Panel */
78
+ /* Closed state of the dialog */
79
+ dialog.right-panel {
80
+ height: 100vh;
81
+ left: auto;
82
+ max-height: 100vh;
83
+ scrollbar-gutter: stable;
84
+ /* Not widely available yet. */
85
+
86
+ opacity: 0;
87
+ translate: 100%;
88
+ transition: all var(--animation-duration) allow-discrete;
89
+ }
90
+
91
+ dialog.right-panel:open {
92
+ opacity: 1;
93
+ translate: 0%;
94
+ }
95
+
96
+ /* Before open state */
97
+ /* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
98
+ @starting-style {
99
+ dialog.right-panel:open {
100
+ opacity: 0;
101
+ translate: 100%;
102
+ }
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stack-spot/portal-components",
3
- "version": "1.0.0-dev.1775743853760",
3
+ "version": "1.0.0-dev.1778512145216",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -28,16 +28,19 @@
28
28
  "./FadingOverflow": "./dist/components/FadingOverflow.js",
29
29
  "./Table": "./dist/components/Table/index.js",
30
30
  "./ContentValidateFilter": "./dist/components/ContentValidateFilter.js",
31
- "./Form": "./dist/components/form/Form/index.js"
31
+ "./Form": "./dist/components/form/Form/index.js",
32
+ "./Dialog": "./dist/components/Modal/Dialog.js",
33
+ "./Modal": "./dist/components/Modal/Modal.js"
32
34
  },
33
35
  "scripts": {
34
- "build": "pnpm package-override --revert=false && rimraf dist && tsc && tsc-esm-fix --target='dist'",
36
+ "build": "pnpm package-override --revert=false && rimraf dist && tsc && tsc-esm-fix --target='dist' && cpy 'src/**/*.css' dist",
35
37
  "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
36
38
  "check-tree-shaking": "agadoo"
37
39
  },
38
40
  "peerDependencies": {
39
41
  "@citric/core": "^6.5.1",
40
42
  "@citric/icons": "^5.10.0",
43
+ "@stack-spot/citric-react": "^0.41.2",
41
44
  "@citric/ui": "^6.10.1",
42
45
  "@stack-spot/portal-theme": "^1.1.1",
43
46
  "@stack-spot/portal-translate": "^2.1.0",
@@ -51,16 +54,17 @@
51
54
  "@types/react": "^18.2.37",
52
55
  "@types/react-dom": "^18.2.15",
53
56
  "@types/react-syntax-highlighter": "^15.5.13",
54
- "@typescript-eslint/eslint-plugin": "^6.10.0",
55
- "@typescript-eslint/parser": "^6.10.0",
57
+ "@typescript-eslint/eslint-plugin": "^8.25.0",
58
+ "@typescript-eslint/parser": "^8.25.0",
56
59
  "agadoo": "^3.0.0",
57
- "eslint": "^8.53.0",
60
+ "cpy-cli": "^5.0.0",
61
+ "eslint": "^9.26.0",
58
62
  "eslint-plugin-filenames": "^1.3.2",
59
63
  "eslint-plugin-import": "^2.29.0",
60
- "eslint-plugin-lodash": "^7.4.0",
64
+ "eslint-plugin-lodash": "^8.0.0",
61
65
  "eslint-plugin-promise": "^6.1.1",
62
66
  "eslint-plugin-react": "^7.33.2",
63
- "eslint-plugin-react-hooks": "^4.6.0",
67
+ "eslint-plugin-react-hooks": "^5.1.0",
64
68
  "eslint-plugin-react-refresh": "^0.4.4",
65
69
  "react": "18.2.0",
66
70
  "react-dom": "18.2.0",
@@ -0,0 +1,318 @@
1
+ import {
2
+ type AriaAttributes,
3
+ type CSSProperties,
4
+ type PropsWithChildren,
5
+ type SyntheticEvent,
6
+ forwardRef,
7
+ useEffect,
8
+ useImperativeHandle,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import './style.css'
13
+ /**
14
+ * Available size options for the dialog.
15
+ */
16
+ export type DialogSize = 'small' | 'medium' | 'large' | 'extra-large' | 'fit-content'
17
+
18
+ /**
19
+ * Type of dialog display.
20
+ *
21
+ * @remarks
22
+ * - `modal`: Centered dialog with backdrop
23
+ * - `right-panel`: Slide-in panel from the right side
24
+ */
25
+ type DialogType = 'modal' | 'right-panel'
26
+
27
+ /**
28
+ * Props for controlled dialog state.
29
+ */
30
+ type ControlledProps = {
31
+ /**
32
+ * Controls whether the dialog is open.
33
+ */
34
+ isOpen: boolean,
35
+
36
+ /**
37
+ * Callback to update the dialog's open state.
38
+ */
39
+ setIsOpen: (value: boolean) => void,
40
+ }
41
+
42
+ /**
43
+ * Props for uncontrolled dialog state.
44
+ *
45
+ * @remarks
46
+ * Use this when you want the dialog to manage its own state internally.
47
+ */
48
+ type UncontrolledProps = {
49
+ isOpen?: never,
50
+ setIsOpen?: never,
51
+ }
52
+
53
+ type AriaProps = Pick<AriaAttributes, 'aria-labelledby' | 'aria-label' | 'aria-describedby' | 'aria-description'>
54
+ export type DialogProps = PropsWithChildren & (ControlledProps | UncontrolledProps) & AriaProps & {
55
+ /**
56
+ * The display type of the dialog.
57
+ *
58
+ * @defaultValue 'modal'
59
+ */
60
+ type?: DialogType,
61
+
62
+ /**
63
+ * Whether clicking outside the dialog should close it.
64
+ *
65
+ * @defaultValue true
66
+ */
67
+ shouldLightDismiss?: boolean,
68
+
69
+ /**
70
+ * Initial open state for uncontrolled dialogs.
71
+ *
72
+ * @defaultValue false
73
+ */
74
+ initialOpen?: boolean,
75
+
76
+ /**
77
+ * Callback fired when the dialog is closed.
78
+ */
79
+ onClose?: (event?: SyntheticEvent) => void,
80
+
81
+ /**
82
+ * The size of the dialog.
83
+ *
84
+ * @defaultValue 'medium'
85
+ */
86
+ size?: DialogSize,
87
+
88
+ /**
89
+ * Custom inline styles for the dialog element.
90
+ */
91
+ style?: CSSProperties,
92
+
93
+ /**
94
+ * Additional CSS class names.
95
+ */
96
+ className?: string,
97
+
98
+ /**
99
+ * Optional identifier for the dialog element.
100
+ */
101
+ id?: string,
102
+ }
103
+
104
+ export interface DialogRef extends Pick<HTMLDialogElement, 'addEventListener' | 'removeEventListener'> {
105
+ close: () => void,
106
+ showModal: () => void,
107
+ isOpen: () => boolean,
108
+ }
109
+
110
+ /**
111
+ * Custom hook for managing dialog state and behavior.
112
+ *
113
+ * @remarks
114
+ * This hook handles:
115
+ * - Opening and closing the dialog
116
+ * - Light dismiss (clicking outside to close)
117
+ * - Escape key handling
118
+ * - Cleanup on unmount
119
+ *
120
+ * @param props - Dialog configuration options
121
+ * @returns Dialog state and control methods
122
+ */
123
+ const useDialog = ({
124
+ isOpen: controlledOpen,
125
+ setIsOpen: setControlledOpen,
126
+ initialOpen = false,
127
+ shouldLightDismiss = true,
128
+ }: Pick<DialogProps, 'isOpen' | 'setIsOpen' | 'initialOpen' | 'shouldLightDismiss'>) => {
129
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(initialOpen)
130
+ const dialogRef = useRef<HTMLDialogElement>(null)
131
+
132
+ const isOpen = controlledOpen ?? uncontrolledOpen
133
+ const setIsOpen = setControlledOpen ?? setUncontrolledOpen
134
+
135
+ useEffect(() => {
136
+ const dialog = dialogRef.current
137
+ if (isOpen && dialog && !dialog.open) {
138
+ dialog.showModal()
139
+ } else if (!isOpen) {
140
+ dialog?.close()
141
+ }
142
+ return () => {
143
+ dialog?.close()
144
+ }
145
+ }, [isOpen])
146
+
147
+ useEffect(() => {
148
+ const dialog = dialogRef.current
149
+ if (!dialog) return
150
+
151
+ const handleClose = (event: Event | KeyboardEvent) => {
152
+ event.preventDefault()
153
+ event.stopPropagation()
154
+ setIsOpen(false)
155
+ }
156
+
157
+ const lightDismiss = (event: Event) => {
158
+ const { target } = event
159
+ if (target instanceof Element && target.nodeName === 'DIALOG') {
160
+ handleClose(event)
161
+ }
162
+ }
163
+
164
+ const closeOnEscape = (event: KeyboardEvent) => {
165
+ if (event.code === 'Escape') {
166
+ handleClose(event)
167
+ }
168
+ }
169
+
170
+ if (shouldLightDismiss) {
171
+ dialog.addEventListener('click', lightDismiss)
172
+ }
173
+ dialog.addEventListener('keydown', closeOnEscape)
174
+
175
+ return () => {
176
+ if (shouldLightDismiss) {
177
+ dialog.removeEventListener('click', lightDismiss)
178
+ }
179
+ dialog.removeEventListener('keydown', closeOnEscape)
180
+ }
181
+ }, [shouldLightDismiss, setIsOpen])
182
+
183
+ return {
184
+ dialogRef,
185
+ isOpen,
186
+ close: () => setIsOpen(false),
187
+ showModal: () => setIsOpen(true),
188
+ }
189
+ }
190
+
191
+ /**
192
+ * A flexible dialog component that supports both modal and panel modes.
193
+ *
194
+ * @remarks
195
+ * The Dialog component provides a native HTML dialog element with enhanced functionality:
196
+ * - **Controlled or uncontrolled state**: Manage state externally or let the component handle it
197
+ * - **Light dismiss**: Optional backdrop click to close
198
+ * - **Keyboard support**: ESC key closes the dialog
199
+ * - **Multiple sizes**: Predefined size options
200
+ * - **Accessibility**: Full ARIA support
201
+ * - **Two display modes**: Modal (centered) or right panel (slide-in)
202
+ *
203
+ * **Important:** When using controlled mode, you must provide both `isOpen` and `setIsOpen` props.
204
+ * For uncontrolled mode, use the ref to control the dialog programmatically.
205
+ *
206
+ * @example
207
+ * Uncontrolled dialog with ref:
208
+ * ```tsx
209
+ * const dialogRef = useRef<DialogRef>(null)
210
+ *
211
+ * const handleOpen = () => {
212
+ * dialogRef.current?.showModal()
213
+ * }
214
+ *
215
+ * return (
216
+ * <>
217
+ * <button onClick={handleOpen}>Open Dialog</button>
218
+ * <Dialog
219
+ * ref={dialogRef}
220
+ * size="large"
221
+ * aria-label="Example dialog"
222
+ * >
223
+ * <Text>Dialog content</Text>
224
+ * </Dialog>
225
+ * </>
226
+ * )
227
+ * ```
228
+ *
229
+ * @example
230
+ * Controlled dialog:
231
+ * ```tsx
232
+ * const [isOpen, setIsOpen] = useState(false)
233
+ *
234
+ * return (
235
+ * <>
236
+ * <button onClick={() => setIsOpen(true)}>Open Dialog</button>
237
+ * <Dialog
238
+ * isOpen={isOpen}
239
+ * setIsOpen={setIsOpen}
240
+ * type="right-panel"
241
+ * shouldLightDismiss={false}
242
+ * onClose={() => console.log('Dialog closed')}
243
+ * >
244
+ * <Text>Panel content</Text>
245
+ * </Dialog>
246
+ * </>
247
+ * )
248
+ * ```
249
+ *
250
+ * @example
251
+ * Dialog with custom styling:
252
+ * ```tsx
253
+ * <Dialog
254
+ * size="fit-content"
255
+ * className="custom-dialog"
256
+ * style={{ maxWidth: '600px' }}
257
+ * aria-labelledby="dialog-title"
258
+ * >
259
+ * <h2 id="dialog-title">Custom Dialog</h2>
260
+ * <p>Content here</p>
261
+ * </Dialog>
262
+ * ```
263
+ *
264
+ * @param props - Dialog properties
265
+ * @param ref - Forwarded ref for imperative control
266
+ * @returns A dialog element
267
+ */
268
+ export const Dialog = forwardRef<DialogRef, DialogProps>(
269
+ ({
270
+ type = 'modal',
271
+ shouldLightDismiss = true,
272
+ initialOpen = false,
273
+ isOpen: controlledOpen,
274
+ setIsOpen: setControlledOpen,
275
+ onClose,
276
+ children,
277
+ size = 'medium',
278
+ style,
279
+ className,
280
+ ...props
281
+ }, forwardedRef,
282
+ ) => {
283
+ const { dialogRef, isOpen, close, showModal } = useDialog({
284
+ isOpen: controlledOpen,
285
+ setIsOpen: setControlledOpen,
286
+ initialOpen,
287
+ shouldLightDismiss,
288
+ })
289
+
290
+ useImperativeHandle(forwardedRef, () => ({
291
+ close,
292
+ showModal,
293
+ isOpen: () => isOpen,
294
+ addEventListener(name: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) {
295
+ dialogRef.current?.addEventListener(name, callback, options)
296
+ },
297
+ removeEventListener(name: string, callback: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) {
298
+ dialogRef.current?.removeEventListener(name, callback, options)
299
+ },
300
+ }), [isOpen, close, showModal])
301
+
302
+ return (
303
+ <dialog
304
+ ref={dialogRef}
305
+ onClose={onClose}
306
+ {...props}
307
+ className={`${type} ${size}${className ? ` ${className}` : ''}`}
308
+ style={style}
309
+ >
310
+ <div className="dialog-content">
311
+ {children}
312
+ </div>
313
+ </dialog>
314
+ )
315
+ },
316
+ )
317
+
318
+ Dialog.displayName = 'Dialog'
@@ -0,0 +1,110 @@
1
+ import { Column, IconBox, Row, Text } from '@stack-spot/citric-react'
2
+ import { forwardRef } from 'react'
3
+ import { Dialog, DialogProps, DialogRef } from './Dialog'
4
+
5
+ /**
6
+ * Props for the Modal component.
7
+ */
8
+ export type ModalProps = DialogProps & {
9
+ /**
10
+ * The title text displayed in the modal header.
11
+ */
12
+ title?: string,
13
+
14
+ /**
15
+ * The subtitle text displayed below the title in the modal header.
16
+ */
17
+ subtitle?: string,
18
+
19
+ /**
20
+ * Optional identifier for the modal element.
21
+ */
22
+ id?: string,
23
+ }
24
+
25
+ /**
26
+ * A modal dialog component that displays content in an overlay with a header and close button.
27
+ *
28
+ * @example
29
+ * Using ref to control modal:
30
+ * ```tsx
31
+ * const modalRef = useRef<DialogRef>(null)
32
+ *
33
+ * const handleOpenModal = () => {
34
+ * modalRef.current?.showModal()
35
+ * }
36
+ *
37
+ * return (
38
+ * <>
39
+ * <button onClick={handleOpenModal}>Open Modal</button>
40
+ * <Modal
41
+ * ref={modalRef}
42
+ * title="Welcome"
43
+ * subtitle="Please review the information below"
44
+ * >
45
+ * <Text>Modal content goes here</Text>
46
+ * </Modal>
47
+ * </>
48
+ * )
49
+ * ```
50
+ *
51
+ * @example
52
+ * Using controlled state:
53
+ * ```tsx
54
+ * const [isOpen, setIsOpen] = useState(false)
55
+ *
56
+ * return (
57
+ * <>
58
+ * <button onClick={() => setIsOpen(true)}>Open Modal</button>
59
+ * <Modal
60
+ * title="Confirmation"
61
+ * subtitle="Are you sure?"
62
+ * isOpen={isOpen}
63
+ * setIsOpen={setIsOpen}
64
+ * >
65
+ * <Text>Confirm your action</Text>
66
+ * </Modal>
67
+ * </>
68
+ * )
69
+ * ```
70
+ *
71
+ * @param props - The modal properties
72
+ * @param ref - Forwarded ref to access Dialog methods
73
+ * @returns A modal dialog component
74
+ */
75
+ export const Modal = forwardRef<DialogRef, ModalProps>(
76
+ ({ title, subtitle, children, ...props }, ref) => {
77
+ const handleClose = () => {
78
+ if (ref && 'current' in ref && ref?.current) {
79
+ ref.current.close()
80
+ } else if (props.setIsOpen) {
81
+ props.setIsOpen(false)
82
+ }
83
+ }
84
+
85
+ return (
86
+ <Dialog ref={ref} {...props}>
87
+ <Column bg="light.400" flex={1} h="100%">
88
+ <Row justifyContent="space-between" p="20px">
89
+ <Column>
90
+ <Text appearance="h4">{title}</Text>
91
+ <Text color="light.700">{subtitle}</Text>
92
+ </Column>
93
+ <IconBox
94
+ onClick={handleClose}
95
+ tag="button"
96
+ icon="TimesMini"
97
+ appearance="circle"
98
+ colorScheme="light"
99
+ />
100
+ </Row>
101
+ <Column flex={1} className="content-box" p="20px">
102
+ {children}
103
+ </Column>
104
+ </Column>
105
+ </Dialog>
106
+ )
107
+ },
108
+ )
109
+
110
+ Modal.displayName = 'Modal'
@@ -0,0 +1,6 @@
1
+ export { Dialog } from './Dialog'
2
+ export type { DialogProps, DialogRef, DialogSize } from './Dialog'
3
+
4
+ export { Modal } from './Modal'
5
+ export type { ModalProps } from './Modal'
6
+
@@ -0,0 +1,103 @@
1
+
2
+ /* Dialog */
3
+ dialog {
4
+ --animation-duration: 0.3s;
5
+ --animation-easing: ease-in-out;
6
+ padding: 0;
7
+ border: 0;
8
+ }
9
+
10
+ dialog.small {
11
+ width: 400px;
12
+ }
13
+
14
+ dialog.medium {
15
+ width: 600px;
16
+ }
17
+
18
+ dialog.large {
19
+ width: 800px;
20
+ }
21
+
22
+ dialog.extra-large {
23
+ width: 70%;
24
+ }
25
+
26
+ dialog>.dialog-content {
27
+ display: flex;
28
+ flex: 1;
29
+ width: 100%;
30
+ height: 100%;
31
+
32
+ .content-box {
33
+ overflow: auto;
34
+ }
35
+ }
36
+
37
+ /* Transition the :backdrop when the dialog modal is promoted to the top layer */
38
+ dialog::backdrop {
39
+ background-color: transparent;
40
+ }
41
+
42
+ dialog:open::backdrop {
43
+ background-color: rgb(0 0 0 / 25%);
44
+ }
45
+
46
+ /* This starting-style rule cannot be nested inside the above selector because the nesting selector cannot represent pseudo-elements. */
47
+ @starting-style {
48
+ dialog:open::backdrop {
49
+ background-color: transparent;
50
+ }
51
+ }
52
+
53
+ /* MODAL */
54
+
55
+ /* Open state of the dialog */
56
+ dialog.modal:open {
57
+ opacity: 1;
58
+ transform: scale(1);
59
+ }
60
+
61
+ /* Closed state of the dialog */
62
+ dialog.modal {
63
+ opacity: 0;
64
+ transform: scale(0.5);
65
+ transition: all var(--animation-duration) allow-discrete;
66
+ }
67
+
68
+ /* Before open state */
69
+ /* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
70
+ @starting-style {
71
+ dialog.modal:open {
72
+ opacity: 0;
73
+ transform: scale(0.5);
74
+ }
75
+ }
76
+
77
+ /* Right Panel */
78
+ /* Closed state of the dialog */
79
+ dialog.right-panel {
80
+ height: 100vh;
81
+ left: auto;
82
+ max-height: 100vh;
83
+ scrollbar-gutter: stable;
84
+ /* Not widely available yet. */
85
+
86
+ opacity: 0;
87
+ translate: 100%;
88
+ transition: all var(--animation-duration) allow-discrete;
89
+ }
90
+
91
+ dialog.right-panel:open {
92
+ opacity: 1;
93
+ translate: 0%;
94
+ }
95
+
96
+ /* Before open state */
97
+ /* Needs to be after the previous dialog:open rule to take effect, as the specificity is the same */
98
+ @starting-style {
99
+ dialog.right-panel:open {
100
+ opacity: 0;
101
+ translate: 100%;
102
+ }
103
+ }