@recursica/mui-adapter 0.4.0 → 0.6.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/mui-adapter.cjs +74 -1
  3. package/dist/mui-adapter.cjs.map +1 -1
  4. package/dist/mui-adapter.css +1 -1
  5. package/dist/mui-adapter.js +6651 -201
  6. package/dist/mui-adapter.js.map +1 -1
  7. package/dist/src/components/Accordion/Accordion.d.ts +63 -2
  8. package/dist/src/components/AssistiveElement/AssistiveElement.d.ts +9 -2
  9. package/dist/src/components/Autocomplete/Autocomplete.d.ts +17 -0
  10. package/dist/src/components/Autocomplete/index.d.ts +1 -0
  11. package/dist/src/components/Avatar/Avatar.d.ts +14 -2
  12. package/dist/src/components/Badge/Badge.d.ts +13 -3
  13. package/dist/src/components/Breadcrumb/Breadcrumb.d.ts +10 -3
  14. package/dist/src/components/Button/Button.d.ts +1 -1
  15. package/dist/src/components/Card/Card.d.ts +40 -3
  16. package/dist/src/components/Checkbox/Checkbox.d.ts +15 -2
  17. package/dist/src/components/Checkbox/CheckboxGroup.d.ts +16 -0
  18. package/dist/src/components/Checkbox/index.d.ts +5 -1
  19. package/dist/src/components/Chip/Chip.d.ts +18 -2
  20. package/dist/src/components/Dropdown/Dropdown.d.ts +23 -3
  21. package/dist/src/components/FormControlLayout/FormControlLayout.d.ts +10 -2
  22. package/dist/src/components/FormControlWrapper/FormControlWrapper.d.ts +24 -2
  23. package/dist/src/components/HoverCard/HoverCard.d.ts +55 -3
  24. package/dist/src/components/Label/Label.d.ts +13 -2
  25. package/dist/src/components/Link/Link.d.ts +1 -1
  26. package/dist/src/components/Menu/Menu.d.ts +99 -3
  27. package/dist/src/components/Modal/Modal.d.ts +36 -2
  28. package/dist/src/components/NumberInput/NumberInput.d.ts +14 -2
  29. package/dist/src/components/Pagination/Pagination.d.ts +44 -2
  30. package/dist/src/components/Pagination/Pagination.icons.d.ts +8 -0
  31. package/dist/src/components/Panel/Panel.d.ts +65 -3
  32. package/dist/src/components/Radio/Radio.d.ts +13 -3
  33. package/dist/src/components/Radio/RadioGroup.d.ts +13 -0
  34. package/dist/src/components/ReadOnlyField/ReadOnlyBooleanField.d.ts +5 -0
  35. package/dist/src/components/ReadOnlyField/ReadOnlyField.d.ts +7 -2
  36. package/dist/src/components/ReadOnlyField/ReadOnlySwitchField.d.ts +5 -0
  37. package/dist/src/components/ReadOnlyField/ReadOnlyTextField.d.ts +8 -0
  38. package/dist/src/components/ReadOnlyField/WithReadOnlyWrapper.d.ts +28 -0
  39. package/dist/src/components/ReadOnlyField/index.d.ts +1 -0
  40. package/dist/src/components/SegmentedControl/SegmentedControl.d.ts +31 -3
  41. package/dist/src/components/Slider/Slider.d.ts +28 -2
  42. package/dist/src/components/Stepper/Stepper.d.ts +21 -2
  43. package/dist/src/components/Switch/Switch.d.ts +10 -3
  44. package/dist/src/components/Switch/SwitchGroup.d.ts +13 -0
  45. package/dist/src/components/Tabs/Tabs.d.ts +35 -3
  46. package/dist/src/components/Text/Text.d.ts +1 -1
  47. package/dist/src/components/TextArea/TextArea.d.ts +18 -2
  48. package/dist/src/components/TextField/TextField.d.ts +16 -2
  49. package/dist/src/components/Timeline/Timeline.d.ts +14 -2
  50. package/dist/src/components/Timeline/TimelineItem.d.ts +29 -0
  51. package/dist/src/components/Title/Title.d.ts +1 -1
  52. package/dist/src/components/Toast/Toast.d.ts +25 -2
  53. package/dist/src/components/Tooltip/Tooltip.d.ts +42 -3
  54. package/dist/src/components/Typography/Typography.d.ts +1 -1
  55. package/dist/src/components/index.d.ts +1 -0
  56. package/package.json +12 -2
  57. package/src/GlobalExemptions.modules.css +13 -0
  58. package/src/components/Accordion/Accordion.module.css +295 -0
  59. package/src/components/Accordion/Accordion.stories.tsx +149 -21
  60. package/src/components/Accordion/Accordion.tsx +264 -4
  61. package/src/components/AssistiveElement/AssistiveElement.module.css +111 -0
  62. package/src/components/AssistiveElement/AssistiveElement.stories.tsx +76 -19
  63. package/src/components/AssistiveElement/AssistiveElement.tsx +85 -4
  64. package/src/components/Autocomplete/Autocomplete.module.css +325 -0
  65. package/src/components/Autocomplete/Autocomplete.stories.tsx +196 -0
  66. package/src/components/Autocomplete/Autocomplete.tsx +201 -0
  67. package/src/components/Autocomplete/index.ts +1 -0
  68. package/src/components/Avatar/Avatar.module.css +344 -0
  69. package/src/components/Avatar/Avatar.stories.tsx +94 -22
  70. package/src/components/Avatar/Avatar.tsx +110 -5
  71. package/src/components/Badge/Badge.module.css +127 -0
  72. package/src/components/Badge/Badge.stories.tsx +65 -22
  73. package/src/components/Badge/Badge.tsx +70 -5
  74. package/src/components/Breadcrumb/Breadcrumb.module.css +26 -0
  75. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +57 -22
  76. package/src/components/Breadcrumb/Breadcrumb.tsx +62 -5
  77. package/src/components/Button/Button.module.css +113 -20
  78. package/src/components/Button/Button.stories.tsx +3 -0
  79. package/src/components/Button/Button.tsx +10 -3
  80. package/src/components/Card/Card.module.css +118 -0
  81. package/src/components/Card/Card.stories.tsx +117 -19
  82. package/src/components/Card/Card.tsx +179 -4
  83. package/src/components/Checkbox/Checkbox.module.css +232 -0
  84. package/src/components/Checkbox/Checkbox.stories.tsx +81 -19
  85. package/src/components/Checkbox/Checkbox.tsx +146 -4
  86. package/src/components/Checkbox/CheckboxGroup.tsx +117 -0
  87. package/src/components/Checkbox/index.ts +6 -1
  88. package/src/components/Chip/Chip.module.css +236 -0
  89. package/src/components/Chip/Chip.stories.tsx +101 -21
  90. package/src/components/Chip/Chip.tsx +138 -5
  91. package/src/components/DatePicker/DatePicker.module.css +42 -0
  92. package/src/components/DatePicker/DatePicker.tsx +1 -0
  93. package/src/components/Dropdown/Dropdown.module.css +296 -0
  94. package/src/components/Dropdown/Dropdown.stories.tsx +80 -21
  95. package/src/components/Dropdown/Dropdown.tsx +182 -5
  96. package/src/components/FileInput/FileInput.module.css +47 -0
  97. package/src/components/FileInput/FileInput.tsx +1 -0
  98. package/src/components/FileUpload/FileUpload.module.css +40 -0
  99. package/src/components/FileUpload/FileUpload.tsx +1 -0
  100. package/src/components/FormControlLayout/FormControlLayout.module.css +92 -0
  101. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +74 -19
  102. package/src/components/FormControlLayout/FormControlLayout.tsx +58 -4
  103. package/src/components/FormControlWrapper/FormControlWrapper.module.css +96 -0
  104. package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +122 -19
  105. package/src/components/FormControlWrapper/FormControlWrapper.tsx +145 -6
  106. package/src/components/HoverCard/HoverCard.module.css +91 -0
  107. package/src/components/HoverCard/HoverCard.stories.tsx +158 -21
  108. package/src/components/HoverCard/HoverCard.tsx +150 -4
  109. package/src/components/Label/Label.module.css +136 -0
  110. package/src/components/Label/Label.stories.tsx +98 -17
  111. package/src/components/Label/Label.tsx +88 -4
  112. package/src/components/Link/Link.module.css +8 -0
  113. package/src/components/Menu/Menu.module.css +287 -0
  114. package/src/components/Menu/Menu.stories.tsx +307 -21
  115. package/src/components/Menu/Menu.tsx +318 -4
  116. package/src/components/Modal/Modal.module.css +172 -0
  117. package/src/components/Modal/Modal.stories.tsx +66 -22
  118. package/src/components/Modal/Modal.tsx +216 -3
  119. package/src/components/NumberInput/NumberInput.module.css +281 -0
  120. package/src/components/NumberInput/NumberInput.stories.tsx +83 -20
  121. package/src/components/NumberInput/NumberInput.tsx +148 -5
  122. package/src/components/Pagination/Pagination.icons.tsx +66 -0
  123. package/src/components/Pagination/Pagination.module.css +307 -0
  124. package/src/components/Pagination/Pagination.stories.tsx +42 -21
  125. package/src/components/Pagination/Pagination.tsx +193 -4
  126. package/src/components/Panel/Panel.module.css +206 -0
  127. package/src/components/Panel/Panel.stories.tsx +214 -22
  128. package/src/components/Panel/Panel.tsx +153 -4
  129. package/src/components/Radio/Radio.module.css +220 -0
  130. package/src/components/Radio/Radio.stories.tsx +77 -19
  131. package/src/components/Radio/Radio.tsx +178 -4
  132. package/src/components/Radio/RadioGroup.tsx +111 -0
  133. package/src/components/ReadOnlyField/ReadOnlyBooleanField.tsx +21 -0
  134. package/src/components/ReadOnlyField/ReadOnlyField.module.css +54 -0
  135. package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +175 -16
  136. package/src/components/ReadOnlyField/ReadOnlyField.tsx +109 -4
  137. package/src/components/ReadOnlyField/ReadOnlySwitchField.tsx +21 -0
  138. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +48 -0
  139. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +93 -0
  140. package/src/components/ReadOnlyField/index.ts +1 -0
  141. package/src/components/SegmentedControl/SegmentedControl.module.css +203 -0
  142. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +102 -20
  143. package/src/components/SegmentedControl/SegmentedControl.tsx +110 -5
  144. package/src/components/Slider/Slider.module.css +542 -0
  145. package/src/components/Slider/Slider.stories.tsx +157 -19
  146. package/src/components/Slider/Slider.tsx +282 -4
  147. package/src/components/Stepper/Stepper.module.css +410 -0
  148. package/src/components/Stepper/Stepper.stories.tsx +132 -19
  149. package/src/components/Stepper/Stepper.tsx +145 -5
  150. package/src/components/Switch/Switch.module.css +238 -0
  151. package/src/components/Switch/Switch.stories.tsx +98 -19
  152. package/src/components/Switch/Switch.tsx +153 -5
  153. package/src/components/Switch/SwitchGroup.tsx +111 -0
  154. package/src/components/Tabs/Tabs.module.css +422 -0
  155. package/src/components/Tabs/Tabs.stories.tsx +138 -19
  156. package/src/components/Tabs/Tabs.tsx +103 -5
  157. package/src/components/TextArea/TextArea.module.css +160 -0
  158. package/src/components/TextArea/TextArea.stories.tsx +80 -21
  159. package/src/components/TextArea/TextArea.tsx +156 -5
  160. package/src/components/TextField/TextField.module.css +266 -0
  161. package/src/components/TextField/TextField.stories.tsx +168 -15
  162. package/src/components/TextField/TextField.tsx +171 -5
  163. package/src/components/TimePicker/TimePicker.module.css +41 -0
  164. package/src/components/TimePicker/TimePicker.tsx +1 -0
  165. package/src/components/Timeline/Timeline.module.css +367 -0
  166. package/src/components/Timeline/Timeline.stories.tsx +114 -21
  167. package/src/components/Timeline/Timeline.tsx +80 -4
  168. package/src/components/Timeline/TimelineItem.tsx +101 -0
  169. package/src/components/Toast/Toast.module.css +170 -0
  170. package/src/components/Toast/Toast.stories.tsx +54 -22
  171. package/src/components/Toast/Toast.tsx +90 -4
  172. package/src/components/Tooltip/Tooltip.module.css +90 -0
  173. package/src/components/Tooltip/Tooltip.stories.tsx +152 -22
  174. package/src/components/Tooltip/Tooltip.tsx +114 -4
  175. package/src/components/TransferList/TransferList.module.css +38 -0
  176. package/src/components/TransferList/TransferList.tsx +1 -0
  177. package/src/components/index.ts +1 -0
@@ -0,0 +1,172 @@
1
+ /* EXEMPTIONS:
2
+ - content-style and header-style parent variables represent raw JSON styling objects rather than
3
+ direct CSS variables and cannot be applied directly in standard CSS files.
4
+ Sub-properties are fully and individually mapped to elements natively. */
5
+ /* recursica-ignore: --recursica_ui-kit_components_modal_properties_content-style */
6
+ /* recursica-ignore: --recursica_ui-kit_components_modal_properties_header-style */
7
+
8
+ .root {
9
+ }
10
+
11
+ .inner {
12
+ /* Mantine applies padding to inner to keep modal away from edges. We rely on content constraints. */
13
+ }
14
+
15
+ .content {
16
+ /* Geometric Bounds */
17
+ min-width: var(--recursica_ui-kit_components_modal_properties_min-width);
18
+ max-width: var(--recursica_ui-kit_components_modal_properties_max-width);
19
+ min-height: var(--recursica_ui-kit_components_modal_properties_min-height);
20
+
21
+ /* Restrict max-height to either the UI Kit max or the viewport bounds to force internal scrolling */
22
+ max-height: min(
23
+ var(--recursica_ui-kit_components_modal_properties_max-height),
24
+ calc(100vh - 4rem)
25
+ );
26
+ overflow: hidden;
27
+
28
+ /* Box model */
29
+ border-radius: var(
30
+ --recursica_ui-kit_components_modal_properties_border-radius
31
+ );
32
+ border-width: var(--recursica_ui-kit_components_modal_properties_border-size);
33
+ border-color: var(
34
+ --recursica_ui-kit_components_modal_properties_colors_border-color
35
+ );
36
+ border-style: solid;
37
+
38
+ /* Appearance */
39
+ background-color: var(
40
+ --recursica_ui-kit_components_modal_properties_colors_background
41
+ );
42
+ box-shadow: var(--recursica_ui-kit_components_modal_properties_elevation);
43
+
44
+ /* Internal Flex container to trap header and let body scroll */
45
+ display: flex;
46
+ flex-direction: column;
47
+ }
48
+
49
+ .header {
50
+ padding: var(--recursica_ui-kit_components_modal_properties_vertical-padding)
51
+ var(--recursica_ui-kit_components_modal_properties_horizontal-padding);
52
+ background-color: transparent; /* Inherit from content */
53
+ }
54
+
55
+ .title {
56
+ color: var(--recursica_ui-kit_components_modal_properties_colors_title);
57
+
58
+ /* Direct Figma Typography Mapping */
59
+ font-family: var(
60
+ --recursica_ui-kit_components_modal_properties_header-style_fontFamily
61
+ );
62
+ font-size: var(
63
+ --recursica_ui-kit_components_modal_properties_header-style_fontSize
64
+ );
65
+ font-style: var(
66
+ --recursica_ui-kit_components_modal_properties_header-style_fontStyle
67
+ );
68
+ font-weight: var(
69
+ --recursica_ui-kit_components_modal_properties_header-style_fontWeight
70
+ );
71
+ letter-spacing: var(
72
+ --recursica_ui-kit_components_modal_properties_header-style_letterSpacing
73
+ );
74
+ line-height: var(
75
+ --recursica_ui-kit_components_modal_properties_header-style_lineHeight
76
+ );
77
+ text-transform: var(
78
+ --recursica_ui-kit_components_modal_properties_header-style_textCase
79
+ );
80
+ text-decoration: var(
81
+ --recursica_ui-kit_components_modal_properties_header-style_textDecoration
82
+ );
83
+ }
84
+
85
+ .bodyWrapper {
86
+ /* This is the container that overrides Mantine's native Body. It strips padding and acts as a flex column. */
87
+ padding: 0;
88
+ display: flex;
89
+ flex-direction: column;
90
+ flex: 1 1 auto;
91
+ overflow: hidden;
92
+ min-height: 0; /* Critical for flex child scrolling constraint */
93
+ }
94
+
95
+ .scrollArea {
96
+ padding: var(--recursica_ui-kit_components_modal_properties_vertical-padding)
97
+ var(--recursica_ui-kit_components_modal_properties_horizontal-padding);
98
+
99
+ /* Internal scrolling */
100
+ flex: 1 1 auto;
101
+ overflow-y: auto;
102
+ min-height: 0; /* Critical for flex child scrolling constraint */
103
+
104
+ color: var(--recursica_ui-kit_components_modal_properties_colors_content);
105
+
106
+ /* Direct Figma Typography Mapping */
107
+ font-family: var(
108
+ --recursica_ui-kit_components_modal_properties_content-style_fontFamily
109
+ );
110
+ font-size: var(
111
+ --recursica_ui-kit_components_modal_properties_content-style_fontSize
112
+ );
113
+ font-style: var(
114
+ --recursica_ui-kit_components_modal_properties_content-style_fontStyle
115
+ );
116
+ font-weight: var(
117
+ --recursica_ui-kit_components_modal_properties_content-style_fontWeight
118
+ );
119
+ letter-spacing: var(
120
+ --recursica_ui-kit_components_modal_properties_content-style_letterSpacing
121
+ );
122
+ line-height: var(
123
+ --recursica_ui-kit_components_modal_properties_content-style_lineHeight
124
+ );
125
+ text-transform: var(
126
+ --recursica_ui-kit_components_modal_properties_content-style_textCase
127
+ );
128
+ text-decoration: var(
129
+ --recursica_ui-kit_components_modal_properties_content-style_textDecoration
130
+ );
131
+
132
+ /* Ensure button gap token is easily usable if developers use standard spacing */
133
+ --modal-button-gap: var(
134
+ --recursica_ui-kit_components_modal_properties_button-gap
135
+ );
136
+
137
+ /* Smooth divider transitions */
138
+ transition: border-color 0.15s ease;
139
+ border-top: var(
140
+ --recursica_ui-kit_components_modal_properties_scroll-divider-thickness
141
+ )
142
+ solid transparent;
143
+ border-bottom: var(
144
+ --recursica_ui-kit_components_modal_properties_scroll-divider-thickness
145
+ )
146
+ solid transparent;
147
+ }
148
+
149
+ .scrollArea[data-scrolled-top] {
150
+ border-top-color: var(
151
+ --recursica_ui-kit_components_modal_properties_colors_scroll-divider
152
+ );
153
+ }
154
+
155
+ .scrollArea[data-scrolled-bottom] {
156
+ border-bottom-color: var(
157
+ --recursica_ui-kit_components_modal_properties_colors_scroll-divider
158
+ );
159
+ }
160
+
161
+ .footer {
162
+ padding: var(--recursica_ui-kit_components_modal_properties_vertical-padding)
163
+ var(--recursica_ui-kit_components_modal_properties_horizontal-padding);
164
+ background-color: transparent;
165
+ display: flex;
166
+ justify-content: flex-end;
167
+ gap: var(--recursica_ui-kit_components_modal_properties_button-gap);
168
+ }
169
+
170
+ .close {
171
+ /* Inherits standard icon button tokens natively, but we can override if required */
172
+ }
@@ -1,35 +1,79 @@
1
+ import { useState } from "react";
1
2
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { Modal } from "./Modal";
3
- import { ComingSoon } from "@recursica/storybook-template";
3
+ import { Modal, type ModalProps } from "./Modal";
4
+ import { Button } from "../Button";
4
5
 
5
6
  const meta: Meta<typeof Modal> = {
6
- title: "UI-Kit/🚧 Modal",
7
+ title: "UI-Kit/Modal",
7
8
  component: Modal,
8
9
  tags: ["autodocs"],
9
- parameters: {
10
- controls: {
11
- include: [
12
- "layer",
13
- "withLayer",
14
- "children",
15
- "component",
16
- "variant",
17
- "size",
18
- "icon",
19
- "disabled",
20
- "href",
21
- "onClick",
22
- "onChange",
23
- "value",
24
- "checked",
25
- ],
26
- },
10
+ argTypes: {
11
+ opened: { table: { disable: true } },
12
+ onClose: { table: { disable: true } },
13
+ defaultChecked: { table: { disable: true } },
27
14
  },
28
15
  };
29
16
 
30
17
  export default meta;
18
+
31
19
  type Story = StoryObj<typeof Modal>;
32
20
 
21
+ const DefaultWrapper = (args: ModalProps) => {
22
+ const [opened, setOpened] = useState(false);
23
+ return (
24
+ <>
25
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
26
+ Please log in to continue accessing this feature.
27
+ <Modal.Footer>
28
+ <Button variant="outline" onClick={() => setOpened(false)}>
29
+ Cancel
30
+ </Button>
31
+ <Button onClick={() => setOpened(false)}>Confirm</Button>
32
+ </Modal.Footer>
33
+ </Modal>
34
+ <Button onClick={() => setOpened(true)}>Open Modal</Button>
35
+ </>
36
+ );
37
+ };
38
+
33
39
  export const Default: Story = {
34
- render: () => <ComingSoon componentName="Modal" />,
40
+ args: {
41
+ title: "Authentication Required",
42
+ },
43
+ render: (args) => <DefaultWrapper {...args} />,
44
+ };
45
+
46
+ const ScrollingWrapper = (args: ModalProps) => {
47
+ const [opened, setOpened] = useState(false);
48
+ return (
49
+ <>
50
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
51
+ <p>
52
+ This modal demonstrates the dynamically injected scroll dividers. When
53
+ this body content overflows, the borders between the Header and Footer
54
+ automatically appear to define the scrolling boundary.
55
+ </p>
56
+ {Array(20)
57
+ .fill(0)
58
+ .map((_, i) => (
59
+ <p key={i}>Scrolling content block {i + 1}...</p>
60
+ ))}
61
+ {/* The new Footer abstraction */}
62
+ <Modal.Footer>
63
+ <Button variant="outline" onClick={() => setOpened(false)}>
64
+ Decline
65
+ </Button>
66
+ <Button onClick={() => setOpened(false)}>Accept Terms</Button>
67
+ </Modal.Footer>
68
+ </Modal>
69
+ <Button onClick={() => setOpened(true)}>Open Scrolling Modal</Button>
70
+ </>
71
+ );
72
+ };
73
+
74
+ export const ScrollingContent: Story = {
75
+ args: {
76
+ title: "Terms and Conditions",
77
+ },
78
+ render: (args) => <ScrollingWrapper {...args} />,
35
79
  };
@@ -1,7 +1,220 @@
1
1
  import React from "react";
2
+ import {
3
+ Modal as MuiModal,
4
+ type ModalProps as MuiModalProps,
5
+ } from "@mui/material";
6
+ import {
7
+ filterStylingProps,
8
+ type RecursicaOverStyled,
9
+ } from "../../utils/filterStylingProps";
10
+ import styles from "./Modal.module.css";
2
11
 
3
- export type ModalProps = React.HTMLAttributes<HTMLDivElement>;
12
+ /**
13
+ * Properties for the strictly-tokenized Modal component.
14
+ * Native Mui abstract properties like `size`, `radius`, and `shadow` have been stripped out
15
+ * because they directly conflict with the non-negotiable pixel boundaries defined in the Recursica UI Kit.
16
+ */
17
+ export type ModalProps = RecursicaOverStyled<
18
+ Omit<MuiModalProps, "size" | "radius" | "shadow">
19
+ >;
4
20
 
5
- export const Modal: React.FC<ModalProps> = (props) => {
6
- return <div {...props}>Modal</div>;
21
+ /**
22
+ * The `Modal` component displays a window overlaid on the primary viewport.
23
+ *
24
+ * **Recursica Abstract:**
25
+ * This component acts as a strict structural wrapper around Mui's `<Modal>`.
26
+ * It automatically enforces Figma UI Kit geometries (`max-width: 960px`, `min-width: 304px`)
27
+ * and handles internal body scrolling explicitly via CSS module overrides.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <Modal opened={opened} onClose={close} title="Hello World">
32
+ * Modal Content
33
+ * </Modal>
34
+ * ```
35
+ */
36
+ const ModalInner = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
37
+ {
38
+ overStyled = false,
39
+ children,
40
+ title,
41
+ withCloseButton = true,
42
+ overlayProps,
43
+ withOverlay = true,
44
+ closeButtonProps,
45
+ ...rest
46
+ },
47
+ ref,
48
+ ) {
49
+ const sanitizedProps = filterStylingProps(rest, overStyled);
50
+
51
+ const mergedClassNames: Partial<Record<string, string>> = {
52
+ root: styles.root,
53
+ inner: styles.inner,
54
+ content: styles.content,
55
+ header: styles.header,
56
+ title: styles.title,
57
+ close: styles.close,
58
+ };
59
+
60
+ const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
61
+ if (
62
+ classNamesProp &&
63
+ typeof classNamesProp === "object" &&
64
+ !Array.isArray(classNamesProp)
65
+ ) {
66
+ const o = classNamesProp as Record<string, string>;
67
+ Object.keys(o).forEach((key) => {
68
+ if (mergedClassNames[key]) {
69
+ mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
70
+ } else {
71
+ mergedClassNames[key] = o[key];
72
+ }
73
+ });
74
+ }
75
+
76
+ return (
77
+ <div
78
+ ref={ref}
79
+ classes={mergedClassNames}
80
+ {...(sanitizedProps as unknown as Omit<
81
+ MuiModalProps,
82
+ "size" | "radius" | "shadow"
83
+ >)}
84
+ >
85
+ {withOverlay && <div {...overlayProps} />}
86
+ <div>
87
+ {(title || withCloseButton) && (
88
+ <div>
89
+ {title && <div>{title}</div>}
90
+ {withCloseButton && <div {...closeButtonProps} />}
91
+ </div>
92
+ )}
93
+ <ModalBody>{children}</ModalBody>
94
+ </div>
95
+ </div>
96
+ );
97
+ });
98
+
99
+ ModalInner.displayName = "Modal";
100
+
101
+ /**
102
+ * The `Modal.Footer` provides a perfectly padded container for modal actions.
103
+ *
104
+ * **Recursica Abstract:**
105
+ * By default, this container aligns its children to the right (`justify-content: flex-end`)
106
+ * and applies the standard Figma button-gap spacing.
107
+ *
108
+ * > [!IMPORTANT]
109
+ * > **Button Hierarchy:** Recursica design language explicitly requires that the
110
+ * > primary action button MUST be the right-most element, with the secondary
111
+ * > (outline variant) action placed immediately to the left of it.
112
+ */
113
+ const ModalFooter = React.forwardRef<
114
+ HTMLDivElement,
115
+ React.ComponentPropsWithoutRef<"div">
116
+ >(function ModalFooter({ className, ...rest }, ref) {
117
+ return (
118
+ <div
119
+ ref={ref}
120
+ className={`${styles.footer} ${className || ""}`}
121
+ {...rest}
122
+ />
123
+ );
124
+ });
125
+ ModalFooter.displayName = "Modal.Footer";
126
+
127
+ const ModalBody = React.forwardRef<
128
+ HTMLDivElement,
129
+ React.ComponentPropsWithoutRef<typeof MuiModal.Body>
130
+ >(function ModalBody({ className, onScroll, children, ...rest }, ref) {
131
+ const internalRef = React.useRef<HTMLDivElement>(null);
132
+ const [scrolledTop, setScrolledTop] = React.useState(false);
133
+ const [scrolledBottom, setScrolledBottom] = React.useState(false);
134
+
135
+ const checkScroll = React.useCallback(() => {
136
+ if (internalRef.current) {
137
+ const { scrollTop, scrollHeight, clientHeight } = internalRef.current;
138
+ setScrolledTop(scrollTop > 0);
139
+ setScrolledBottom(Math.ceil(scrollTop + clientHeight) < scrollHeight);
140
+ }
141
+ }, []);
142
+
143
+ // Re-check scroll on mount and window resize
144
+ React.useEffect(() => {
145
+ checkScroll();
146
+ window.addEventListener("resize", checkScroll);
147
+ return () => window.removeEventListener("resize", checkScroll);
148
+ }, [checkScroll, children]);
149
+
150
+ const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
151
+ checkScroll();
152
+ onScroll?.(e);
153
+ };
154
+
155
+ // Intercept children to pull Modal.Footer out of the scrolling container
156
+ let footer: React.ReactNode = null;
157
+ const bodyChildren: React.ReactNode[] = [];
158
+
159
+ React.Children.forEach(children, (child) => {
160
+ if (
161
+ React.isValidElement(child) &&
162
+ (child.type === ModalFooter ||
163
+ (child.type as React.ComponentType)?.displayName === "Modal.Footer")
164
+ ) {
165
+ footer = child;
166
+ } else {
167
+ bodyChildren.push(child);
168
+ }
169
+ });
170
+
171
+ return (
172
+ <div
173
+ {...rest}
174
+ ref={(node) => {
175
+ if (typeof ref === "function") ref(node);
176
+ else if (ref)
177
+ (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
178
+ }}
179
+ className={`${styles.bodyWrapper} ${className || ""}`}
180
+ >
181
+ <div
182
+ ref={internalRef}
183
+ onScroll={handleScroll}
184
+ data-scrolled-top={scrolledTop || undefined}
185
+ data-scrolled-bottom={scrolledBottom || undefined}
186
+ className={styles.scrollArea}
187
+ >
188
+ {bodyChildren}
189
+ </div>
190
+ {footer}
191
+ </div>
192
+ );
193
+ });
194
+ ModalBody.displayName = "Modal.Body";
195
+
196
+ interface ModalComponent
197
+ extends React.ForwardRefExoticComponent<
198
+ ModalProps & React.RefAttributes<HTMLDivElement>
199
+ > {
200
+ Root: typeof MuiModal.Root;
201
+ Overlay: typeof MuiModal.Overlay;
202
+ Content: typeof MuiModal.Content;
203
+ Header: typeof MuiModal.Header;
204
+ Title: typeof MuiModal.Title;
205
+ CloseButton: typeof MuiModal.CloseButton;
206
+ Body: typeof ModalBody;
207
+ Footer: typeof ModalFooter;
208
+ }
209
+
210
+ export const Modal = ModalInner as ModalComponent & {
211
+ Footer: typeof ModalFooter;
7
212
  };
213
+ Modal.Root = MuiModal.Root;
214
+ Modal.Overlay = MuiModal.Overlay;
215
+ Modal.Content = MuiModal.Content;
216
+ Modal.Header = MuiModal.Header;
217
+ Modal.Title = MuiModal.Title;
218
+ Modal.CloseButton = MuiModal.CloseButton;
219
+ Modal.Body = ModalBody;
220
+ Modal.Footer = ModalFooter;