@abstraks-dev/ui-library 2.2.1 → 2.4.0

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,211 @@
1
+ "use strict";
2
+
3
+ var _react = _interopRequireDefault(require("react"));
4
+ var _react2 = require("@testing-library/react");
5
+ var _Prompt = require("../components/Prompt");
6
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
8
+ describe('Prompt Component', () => {
9
+ const defaultProps = {
10
+ message: 'Are you sure you want to proceed?',
11
+ onConfirm: jest.fn(),
12
+ onCancel: jest.fn(),
13
+ open: true
14
+ };
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ });
18
+ describe('Rendering', () => {
19
+ test('renders prompt when open is true', () => {
20
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
21
+ expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
22
+ expect(_react2.screen.getByText('Are you sure you want to proceed?')).toBeInTheDocument();
23
+ });
24
+ test('does not render when open is false', () => {
25
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
26
+ open: false
27
+ })));
28
+ expect(_react2.screen.queryByRole('dialog')).not.toBeInTheDocument();
29
+ });
30
+ test('renders with default confirm button text', () => {
31
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
32
+ expect(_react2.screen.getByText('Yes')).toBeInTheDocument();
33
+ });
34
+ test('renders with custom confirm button text', () => {
35
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
36
+ confirmText: "Delete"
37
+ })));
38
+ expect(_react2.screen.getByText('Delete')).toBeInTheDocument();
39
+ expect(_react2.screen.queryByText('Yes')).not.toBeInTheDocument();
40
+ });
41
+ test('renders with default cancel button text', () => {
42
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
43
+ expect(_react2.screen.getByText('Cancel')).toBeInTheDocument();
44
+ });
45
+ test('renders with custom cancel button text', () => {
46
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
47
+ cancelText: "No"
48
+ })));
49
+ expect(_react2.screen.getByText('No')).toBeInTheDocument();
50
+ expect(_react2.screen.queryByText('Cancel')).not.toBeInTheDocument();
51
+ });
52
+ test('renders both confirm and cancel buttons by default', () => {
53
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
54
+ expect(_react2.screen.getByText('Yes')).toBeInTheDocument();
55
+ expect(_react2.screen.getByText('Cancel')).toBeInTheDocument();
56
+ });
57
+ test('hides cancel button when showCancel is false', () => {
58
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
59
+ showCancel: false
60
+ })));
61
+ expect(_react2.screen.getByText('Yes')).toBeInTheDocument();
62
+ expect(_react2.screen.queryByText('Cancel')).not.toBeInTheDocument();
63
+ });
64
+ test('renders without confirm button if onConfirm is not provided', () => {
65
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, {
66
+ message: "Information message",
67
+ onCancel: defaultProps.onCancel,
68
+ open: true
69
+ }));
70
+ expect(_react2.screen.queryByText('Yes')).not.toBeInTheDocument();
71
+ expect(_react2.screen.getByText('Cancel')).toBeInTheDocument();
72
+ });
73
+ test('has correct ARIA attributes', () => {
74
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
75
+ const dialog = _react2.screen.getByRole('dialog');
76
+ expect(dialog).toHaveAttribute('aria-modal', 'true');
77
+ expect(dialog).toHaveAttribute('aria-label', 'Are you sure you want to proceed?');
78
+ });
79
+ test('confirm button receives focus when rendered', () => {
80
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
81
+ const confirmButton = _react2.screen.getByText('Yes');
82
+ expect(confirmButton).toHaveFocus();
83
+ });
84
+ });
85
+ describe('User Interactions', () => {
86
+ test('calls onConfirm when confirm button is clicked', () => {
87
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
88
+ const confirmButton = _react2.screen.getByText('Yes');
89
+ _react2.fireEvent.click(confirmButton);
90
+ expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1);
91
+ expect(defaultProps.onCancel).not.toHaveBeenCalled();
92
+ });
93
+ test('calls onCancel when cancel button is clicked', () => {
94
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
95
+ const cancelButton = _react2.screen.getByText('Cancel');
96
+ _react2.fireEvent.click(cancelButton);
97
+ expect(defaultProps.onCancel).toHaveBeenCalledTimes(1);
98
+ expect(defaultProps.onConfirm).not.toHaveBeenCalled();
99
+ });
100
+ test('does not call onConfirm if not provided', () => {
101
+ const mockOnCancel = jest.fn();
102
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, {
103
+ message: "Test message",
104
+ onCancel: mockOnCancel,
105
+ open: true,
106
+ showCancel: true
107
+ }));
108
+
109
+ // Only cancel button should be present
110
+ expect(_react2.screen.queryByText('Yes')).not.toBeInTheDocument();
111
+ });
112
+ test('handles multiple clicks correctly', () => {
113
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
114
+ const confirmButton = _react2.screen.getByText('Yes');
115
+ _react2.fireEvent.click(confirmButton);
116
+ _react2.fireEvent.click(confirmButton);
117
+ expect(defaultProps.onConfirm).toHaveBeenCalledTimes(2);
118
+ });
119
+ });
120
+ describe('PropTypes Validation', () => {
121
+ test('renders with required props only', () => {
122
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, {
123
+ message: "Required message",
124
+ open: true
125
+ }));
126
+ expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
127
+ expect(_react2.screen.getByText('Required message')).toBeInTheDocument();
128
+ });
129
+ test('renders with all optional props', () => {
130
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, {
131
+ message: "All props message",
132
+ onConfirm: jest.fn(),
133
+ onCancel: jest.fn(),
134
+ confirmText: "Confirm",
135
+ cancelText: "Dismiss",
136
+ showCancel: true,
137
+ open: true
138
+ }));
139
+ expect(_react2.screen.getByText('All props message')).toBeInTheDocument();
140
+ expect(_react2.screen.getByText('Confirm')).toBeInTheDocument();
141
+ expect(_react2.screen.getByText('Dismiss')).toBeInTheDocument();
142
+ });
143
+ });
144
+ describe('CSS Classes', () => {
145
+ test('applies correct CSS classes', () => {
146
+ const {
147
+ container
148
+ } = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
149
+ expect(container.querySelector('.prompt-overlay')).toBeInTheDocument();
150
+ expect(container.querySelector('.prompt-box')).toBeInTheDocument();
151
+ expect(container.querySelector('.prompt-actions')).toBeInTheDocument();
152
+ });
153
+ test('confirm button has correct class', () => {
154
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
155
+ const confirmButton = _react2.screen.getByText('Yes');
156
+ expect(confirmButton).toHaveClass('prompt-confirm');
157
+ });
158
+ test('cancel button has correct class', () => {
159
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, defaultProps));
160
+ const cancelButton = _react2.screen.getByText('Cancel');
161
+ expect(cancelButton).toHaveClass('prompt-cancel');
162
+ });
163
+ });
164
+ describe('Edge Cases', () => {
165
+ test('renders with empty message string', () => {
166
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
167
+ message: ""
168
+ })));
169
+ expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
170
+ });
171
+ test('renders with very long message', () => {
172
+ const longMessage = 'This is a very long message that might wrap to multiple lines in the prompt dialog and should still render correctly without breaking the layout or causing any issues';
173
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
174
+ message: longMessage
175
+ })));
176
+ expect(_react2.screen.getByText(longMessage)).toBeInTheDocument();
177
+ });
178
+ test('renders when both onConfirm and onCancel are undefined', () => {
179
+ (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, {
180
+ message: "Message",
181
+ open: true
182
+ }));
183
+ expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
184
+ expect(_react2.screen.getByText('Message')).toBeInTheDocument();
185
+ });
186
+ test('transitions from closed to open', () => {
187
+ const {
188
+ rerender
189
+ } = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
190
+ open: false
191
+ })));
192
+ expect(_react2.screen.queryByRole('dialog')).not.toBeInTheDocument();
193
+ rerender(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
194
+ open: true
195
+ })));
196
+ expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
197
+ });
198
+ test('transitions from open to closed', () => {
199
+ const {
200
+ rerender
201
+ } = (0, _react2.render)(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
202
+ open: true
203
+ })));
204
+ expect(_react2.screen.getByRole('dialog')).toBeInTheDocument();
205
+ rerender(/*#__PURE__*/_react.default.createElement(_Prompt.Prompt, _extends({}, defaultProps, {
206
+ open: false
207
+ })));
208
+ expect(_react2.screen.queryByRole('dialog')).not.toBeInTheDocument();
209
+ });
210
+ });
211
+ });
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.default = exports.Modal = void 0;
7
+ var _react = _interopRequireWildcard(require("react"));
8
+ var _reactDom = require("react-dom");
9
+ var _propTypes = require("prop-types");
10
+ var _icons = require("../icons");
11
+ var _ssrSafeId = require("../utils/ssrSafeId");
12
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
14
+ /**
15
+ * Modal - A stateless, accessible modal dialog component
16
+ *
17
+ * Features:
18
+ * - Portal rendering to document.body
19
+ * - Multiple sizes (sm, md, lg, xl, full)
20
+ * - Accessible with proper ARIA attributes and focus management
21
+ * - Keyboard support (Escape to close)
22
+ * - Backdrop click to close (optional)
23
+ * - Scroll lock on body
24
+ * - Header, body, and footer sections
25
+ * - Customizable close button
26
+ * - Animation support
27
+ *
28
+ * @component
29
+ * @example
30
+ * <Modal
31
+ * isOpen={true}
32
+ * onClose={() => setIsOpen(false)}
33
+ * title="Confirm Action"
34
+ * size="md"
35
+ * >
36
+ * <p>Are you sure you want to continue?</p>
37
+ * </Modal>
38
+ */
39
+ const Modal = exports.Modal = /*#__PURE__*/(0, _react.forwardRef)(({
40
+ // Core props
41
+ id,
42
+ className = '',
43
+ // Content props
44
+ title,
45
+ children,
46
+ footer,
47
+ headerContent,
48
+ // Behavior props
49
+ isOpen = false,
50
+ onClose,
51
+ closeOnBackdropClick = true,
52
+ closeOnEscape = true,
53
+ showCloseButton = true,
54
+ preventBodyScroll = true,
55
+ // Appearance props
56
+ size = 'md',
57
+ centered = true,
58
+ // Accessibility props
59
+ 'aria-label': ariaLabel,
60
+ 'aria-describedby': ariaDescribedBy,
61
+ 'aria-labelledby': ariaLabelledBy,
62
+ role = 'dialog',
63
+ // Additional props
64
+ componentName = 'modal',
65
+ ...rest
66
+ }, ref) => {
67
+ const generateId = (0, _ssrSafeId.useStableId)('modal');
68
+ const stableId = id || generateId();
69
+ const titleId = `${stableId}-title`;
70
+ const contentId = `${stableId}-content`;
71
+
72
+ // Handle Escape key press
73
+ const handleEscape = (0, _react.useCallback)(event => {
74
+ if (closeOnEscape && event.key === 'Escape' && isOpen) {
75
+ event.preventDefault();
76
+ onClose?.();
77
+ }
78
+ }, [closeOnEscape, isOpen, onClose]);
79
+
80
+ // Handle backdrop click
81
+ const handleBackdropClick = (0, _react.useCallback)(event => {
82
+ if (closeOnBackdropClick && event.target === event.currentTarget) {
83
+ onClose?.();
84
+ }
85
+ }, [closeOnBackdropClick, onClose]);
86
+
87
+ // Manage body scroll lock
88
+ (0, _react.useEffect)(() => {
89
+ if (isOpen && preventBodyScroll) {
90
+ const originalOverflow = document.body.style.overflow;
91
+ const originalPaddingRight = document.body.style.paddingRight;
92
+
93
+ // Get scrollbar width
94
+ const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
95
+
96
+ // Lock scroll
97
+ document.body.style.overflow = 'hidden';
98
+ if (scrollbarWidth > 0) {
99
+ document.body.style.paddingRight = `${scrollbarWidth}px`;
100
+ }
101
+ return () => {
102
+ document.body.style.overflow = originalOverflow;
103
+ document.body.style.paddingRight = originalPaddingRight;
104
+ };
105
+ }
106
+ }, [isOpen, preventBodyScroll]);
107
+
108
+ // Add/remove escape key listener
109
+ (0, _react.useEffect)(() => {
110
+ if (isOpen && closeOnEscape) {
111
+ document.addEventListener('keydown', handleEscape);
112
+ return () => {
113
+ document.removeEventListener('keydown', handleEscape);
114
+ };
115
+ }
116
+ }, [isOpen, closeOnEscape, handleEscape]);
117
+
118
+ // Focus management
119
+ (0, _react.useEffect)(() => {
120
+ if (isOpen) {
121
+ // Store the currently focused element
122
+ const previousActiveElement = document.activeElement;
123
+
124
+ // Return focus when modal closes
125
+ return () => {
126
+ if (previousActiveElement && previousActiveElement.focus) {
127
+ previousActiveElement.focus();
128
+ }
129
+ };
130
+ }
131
+ }, [isOpen]);
132
+ if (!isOpen) {
133
+ return null;
134
+ }
135
+ const modalClassName = [componentName, `${componentName}--${size}`, centered && `${componentName}--centered`, className].filter(Boolean).join(' ');
136
+ const modalContent = /*#__PURE__*/_react.default.createElement("div", {
137
+ className: `${componentName}-overlay`,
138
+ onClick: handleBackdropClick
139
+ }, /*#__PURE__*/_react.default.createElement("div", _extends({
140
+ ref: ref,
141
+ id: stableId,
142
+ className: modalClassName,
143
+ role: role,
144
+ "aria-modal": "true",
145
+ "aria-label": ariaLabel || title,
146
+ "aria-labelledby": title ? titleId : ariaLabelledBy,
147
+ "aria-describedby": ariaDescribedBy || contentId
148
+ }, rest), (title || headerContent || showCloseButton) && /*#__PURE__*/_react.default.createElement("div", {
149
+ className: `${componentName}__header`
150
+ }, headerContent || /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, title && /*#__PURE__*/_react.default.createElement("h2", {
151
+ id: titleId,
152
+ className: `${componentName}__title`
153
+ }, title)), showCloseButton && /*#__PURE__*/_react.default.createElement("button", {
154
+ className: `${componentName}__close`,
155
+ onClick: onClose,
156
+ "aria-label": "Close modal",
157
+ type: "button"
158
+ }, /*#__PURE__*/_react.default.createElement(_icons.Close, {
159
+ dimensions: 24
160
+ }))), /*#__PURE__*/_react.default.createElement("div", {
161
+ id: contentId,
162
+ className: `${componentName}__body`
163
+ }, children), footer && /*#__PURE__*/_react.default.createElement("div", {
164
+ className: `${componentName}__footer`
165
+ }, footer)));
166
+
167
+ // Render modal in a portal
168
+ if (typeof window !== 'undefined') {
169
+ return /*#__PURE__*/(0, _reactDom.createPortal)(modalContent, document.body);
170
+ }
171
+ return null;
172
+ });
173
+ Modal.propTypes = {
174
+ id: _propTypes.string,
175
+ className: _propTypes.string,
176
+ title: _propTypes.string,
177
+ children: _propTypes.node,
178
+ footer: _propTypes.node,
179
+ headerContent: _propTypes.node,
180
+ isOpen: _propTypes.bool,
181
+ onClose: _propTypes.func,
182
+ closeOnBackdropClick: _propTypes.bool,
183
+ closeOnEscape: _propTypes.bool,
184
+ showCloseButton: _propTypes.bool,
185
+ preventBodyScroll: _propTypes.bool,
186
+ size: (0, _propTypes.oneOf)(['sm', 'md', 'lg', 'xl', 'full']),
187
+ centered: _propTypes.bool,
188
+ 'aria-label': _propTypes.string,
189
+ 'aria-describedby': _propTypes.string,
190
+ 'aria-labelledby': _propTypes.string,
191
+ role: _propTypes.string,
192
+ componentName: _propTypes.string
193
+ };
194
+ Modal.displayName = 'Modal';
195
+ var _default = exports.default = Modal;
package/dist/index.js CHANGED
@@ -24,7 +24,9 @@ var _exportNames = {
24
24
  Hero: true,
25
25
  Label: true,
26
26
  Loader: true,
27
+ Modal: true,
27
28
  Paragraph: true,
29
+ Prompt: true,
28
30
  Radio: true,
29
31
  Search: true,
30
32
  Select: true,
@@ -355,6 +357,12 @@ Object.defineProperty(exports, "Magnify", {
355
357
  return _Magnify.Magnify;
356
358
  }
357
359
  });
360
+ Object.defineProperty(exports, "Modal", {
361
+ enumerable: true,
362
+ get: function () {
363
+ return _Modal.default;
364
+ }
365
+ });
358
366
  Object.defineProperty(exports, "News", {
359
367
  enumerable: true,
360
368
  get: function () {
@@ -373,6 +381,12 @@ Object.defineProperty(exports, "PlusCircle", {
373
381
  return _PlusCircle.PlusCircle;
374
382
  }
375
383
  });
384
+ Object.defineProperty(exports, "Prompt", {
385
+ enumerable: true,
386
+ get: function () {
387
+ return _Prompt.default;
388
+ }
389
+ });
376
390
  Object.defineProperty(exports, "Radio", {
377
391
  enumerable: true,
378
392
  get: function () {
@@ -483,7 +497,9 @@ var _Heading = _interopRequireDefault(require("./components/Heading"));
483
497
  var _Hero = _interopRequireDefault(require("./components/Hero"));
484
498
  var _Label = _interopRequireDefault(require("./components/Label"));
485
499
  var _Loader = _interopRequireDefault(require("./components/Loader"));
500
+ var _Modal = _interopRequireDefault(require("./components/Modal"));
486
501
  var _Paragraph = _interopRequireDefault(require("./components/Paragraph"));
502
+ var _Prompt = _interopRequireDefault(require("./components/Prompt"));
487
503
  var _Radio = require("./components/Radio");
488
504
  var _Search = require("./components/Search");
489
505
  var _Select = require("./components/Select");
@@ -7545,6 +7545,224 @@ body.scroll-locked {
7545
7545
  border: 0 !important;
7546
7546
  }
7547
7547
 
7548
+ .modal-overlay {
7549
+ position: fixed;
7550
+ top: 0;
7551
+ left: 0;
7552
+ right: 0;
7553
+ bottom: 0;
7554
+ background-color: rgba(0, 0, 0, 0.6);
7555
+ backdrop-filter: blur(4px);
7556
+ z-index: 9999;
7557
+ display: flex;
7558
+ align-items: center;
7559
+ justify-content: center;
7560
+ padding: 1rem;
7561
+ animation: fadeIn 0.2s ease-out;
7562
+ overflow-y: auto;
7563
+ }
7564
+ @media (prefers-reduced-motion: reduce) {
7565
+ .modal-overlay {
7566
+ animation: none;
7567
+ }
7568
+ }
7569
+
7570
+ .modal {
7571
+ background: #fff;
7572
+ border-radius: 8px;
7573
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
7574
+ display: flex;
7575
+ flex-direction: column;
7576
+ max-height: calc(100vh - 2rem);
7577
+ width: 100%;
7578
+ animation: slideIn 0.3s ease-out;
7579
+ position: relative;
7580
+ }
7581
+ @media (prefers-reduced-motion: reduce) {
7582
+ .modal {
7583
+ animation: none;
7584
+ }
7585
+ }
7586
+ @media (prefers-color-scheme: dark) {
7587
+ .modal {
7588
+ background: #1e1e1e;
7589
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
7590
+ }
7591
+ }
7592
+ .modal--sm {
7593
+ max-width: 400px;
7594
+ }
7595
+ .modal--md {
7596
+ max-width: 600px;
7597
+ }
7598
+ .modal--lg {
7599
+ max-width: 800px;
7600
+ }
7601
+ .modal--xl {
7602
+ max-width: 1200px;
7603
+ }
7604
+ .modal--full {
7605
+ max-width: calc(100vw - 2rem);
7606
+ max-height: calc(100vh - 2rem);
7607
+ }
7608
+ .modal--centered {
7609
+ margin: auto;
7610
+ }
7611
+ .modal__header {
7612
+ display: flex;
7613
+ align-items: center;
7614
+ justify-content: space-between;
7615
+ padding: 1.5rem;
7616
+ border-bottom: 1px solid #e9ecef;
7617
+ flex-shrink: 0;
7618
+ }
7619
+ @media (prefers-color-scheme: dark) {
7620
+ .modal__header {
7621
+ border-bottom-color: #333;
7622
+ }
7623
+ }
7624
+ .modal__title {
7625
+ margin: 0;
7626
+ font-size: 1.25rem;
7627
+ font-weight: 600;
7628
+ color: #212529;
7629
+ flex: 1;
7630
+ }
7631
+ @media (prefers-color-scheme: dark) {
7632
+ .modal__title {
7633
+ color: #fff;
7634
+ }
7635
+ }
7636
+ .modal__close {
7637
+ display: flex;
7638
+ align-items: center;
7639
+ justify-content: center;
7640
+ background: transparent;
7641
+ border: none;
7642
+ padding: 0.5rem;
7643
+ margin: -0.5rem -0.5rem -0.5rem 1rem;
7644
+ cursor: pointer;
7645
+ color: #6c757d;
7646
+ border-radius: 4px;
7647
+ transition: all 0.15s ease;
7648
+ flex-shrink: 0;
7649
+ }
7650
+ .modal__close:hover {
7651
+ background: #f8f9fa;
7652
+ color: #212529;
7653
+ }
7654
+ @media (prefers-color-scheme: dark) {
7655
+ .modal__close:hover {
7656
+ background: #333;
7657
+ color: #fff;
7658
+ }
7659
+ }
7660
+ .modal__close:focus-visible {
7661
+ outline: 2px solid #009900;
7662
+ outline-offset: 2px;
7663
+ }
7664
+ .modal__close svg {
7665
+ display: block;
7666
+ }
7667
+ .modal__body {
7668
+ padding: 1.5rem;
7669
+ overflow-y: auto;
7670
+ flex: 1;
7671
+ color: #495057;
7672
+ }
7673
+ @media (prefers-color-scheme: dark) {
7674
+ .modal__body {
7675
+ color: #dee2e6;
7676
+ }
7677
+ }
7678
+ .modal__body::-webkit-scrollbar {
7679
+ width: 8px;
7680
+ }
7681
+ .modal__body::-webkit-scrollbar-track {
7682
+ background: #f8f9fa;
7683
+ border-radius: 4px;
7684
+ }
7685
+ @media (prefers-color-scheme: dark) {
7686
+ .modal__body::-webkit-scrollbar-track {
7687
+ background: #2a2a2a;
7688
+ }
7689
+ }
7690
+ .modal__body::-webkit-scrollbar-thumb {
7691
+ background: #ced4da;
7692
+ border-radius: 4px;
7693
+ }
7694
+ .modal__body::-webkit-scrollbar-thumb:hover {
7695
+ background: #adb5bd;
7696
+ }
7697
+ @media (prefers-color-scheme: dark) {
7698
+ .modal__body::-webkit-scrollbar-thumb {
7699
+ background: #555;
7700
+ }
7701
+ .modal__body::-webkit-scrollbar-thumb:hover {
7702
+ background: #666;
7703
+ }
7704
+ }
7705
+ .modal__footer {
7706
+ display: flex;
7707
+ align-items: center;
7708
+ justify-content: flex-end;
7709
+ gap: 0.75rem;
7710
+ padding: 1.5rem;
7711
+ border-top: 1px solid #e9ecef;
7712
+ flex-shrink: 0;
7713
+ }
7714
+ @media (prefers-color-scheme: dark) {
7715
+ .modal__footer {
7716
+ border-top-color: #333;
7717
+ }
7718
+ }
7719
+
7720
+ @keyframes fadeIn {
7721
+ from {
7722
+ opacity: 0;
7723
+ }
7724
+ to {
7725
+ opacity: 1;
7726
+ }
7727
+ }
7728
+ @keyframes slideIn {
7729
+ from {
7730
+ opacity: 0;
7731
+ transform: translateY(-20px) scale(0.95);
7732
+ }
7733
+ to {
7734
+ opacity: 1;
7735
+ transform: translateY(0) scale(1);
7736
+ }
7737
+ }
7738
+ @media (max-width: 640px) {
7739
+ .modal-overlay {
7740
+ padding: 0.5rem;
7741
+ }
7742
+ .modal {
7743
+ max-height: calc(100vh - 1rem);
7744
+ }
7745
+ .modal--sm, .modal--md, .modal--lg, .modal--xl {
7746
+ max-width: 100%;
7747
+ }
7748
+ .modal__header, .modal__body, .modal__footer {
7749
+ padding: 1rem;
7750
+ }
7751
+ .modal__title {
7752
+ font-size: 1.125rem;
7753
+ }
7754
+ }
7755
+ @media (prefers-contrast: high) {
7756
+ .modal {
7757
+ border: 2px solid currentColor;
7758
+ }
7759
+ .modal__header, .modal__footer {
7760
+ border-color: currentColor;
7761
+ }
7762
+ .modal__close:focus-visible {
7763
+ outline-width: 3px;
7764
+ }
7765
+ }
7548
7766
  .logo {
7549
7767
  width: 50%;
7550
7768
  display: inline-flex;