@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
@@ -1,35 +1,321 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { Menu } from "./Menu";
3
- import { ComingSoon } from "@recursica/storybook-template";
3
+ import { Button } from "../Button";
4
4
 
5
- const meta: Meta<typeof Menu> = {
6
- title: "UI-Kit/🚧 Menu",
5
+ /**
6
+ * Menu story axes:
7
+ * - trigger: click (default) vs hover behavior
8
+ * - position: dropdown alignment relative to target
9
+ * - withArrow: arrow indicator on the dropdown
10
+ * - content variations: icons, dividers, labels, disabled items, submenus
11
+ * - layer: tested via the global Layer decorator (withLayer/layer args)
12
+ */
13
+
14
+ const SettingsIcon = () => (
15
+ <svg
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ width="14"
18
+ height="14"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth="2"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ >
26
+ <circle cx="12" cy="12" r="3" />
27
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
28
+ </svg>
29
+ );
30
+
31
+ const SearchIcon = () => (
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ width="14"
35
+ height="14"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ strokeWidth="2"
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ >
43
+ <circle cx="11" cy="11" r="8" />
44
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
45
+ </svg>
46
+ );
47
+
48
+ const MessageIcon = () => (
49
+ <svg
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ width="14"
52
+ height="14"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ strokeWidth="2"
57
+ strokeLinecap="round"
58
+ strokeLinejoin="round"
59
+ >
60
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
61
+ </svg>
62
+ );
63
+
64
+ const ImageIcon = () => (
65
+ <svg
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ width="14"
68
+ height="14"
69
+ viewBox="0 0 24 24"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ strokeWidth="2"
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ >
76
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
77
+ <circle cx="8.5" cy="8.5" r="1.5" />
78
+ <polyline points="21 15 16 10 5 21" />
79
+ </svg>
80
+ );
81
+
82
+ const TrashIcon = () => (
83
+ <svg
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ width="14"
86
+ height="14"
87
+ viewBox="0 0 24 24"
88
+ fill="none"
89
+ stroke="currentColor"
90
+ strokeWidth="2"
91
+ strokeLinecap="round"
92
+ strokeLinejoin="round"
93
+ >
94
+ <polyline points="3 6 5 6 21 6" />
95
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
96
+ </svg>
97
+ );
98
+
99
+ const ArrowsIcon = () => (
100
+ <svg
101
+ xmlns="http://www.w3.org/2000/svg"
102
+ width="14"
103
+ height="14"
104
+ viewBox="0 0 24 24"
105
+ fill="none"
106
+ stroke="currentColor"
107
+ strokeWidth="2"
108
+ strokeLinecap="round"
109
+ strokeLinejoin="round"
110
+ >
111
+ <polyline points="17 1 21 5 17 9" />
112
+ <path d="M3 11V9a4 4 0 0 1 4-4h14" />
113
+ <polyline points="7 23 3 19 7 15" />
114
+ <path d="M21 13v2a4 4 0 0 1-4 4H3" />
115
+ </svg>
116
+ );
117
+
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ type MenuStoryArgs = Record<string, any>;
120
+
121
+ const meta: Meta = {
122
+ title: "UI-Kit/Menu",
7
123
  component: Menu,
8
124
  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",
125
+ argTypes: {
126
+ trigger: {
127
+ control: "select",
128
+ options: ["click", "hover", "click-hover"],
129
+ description:
130
+ "Determines how the menu is triggered: click (default), hover, or both.",
131
+ },
132
+ position: {
133
+ control: "select",
134
+ options: [
135
+ "bottom",
136
+ "bottom-start",
137
+ "bottom-end",
138
+ "top",
139
+ "top-start",
140
+ "top-end",
141
+ "left",
142
+ "left-start",
143
+ "left-end",
144
+ "right",
145
+ "right-start",
146
+ "right-end",
25
147
  ],
148
+ description: "The position of the dropdown relative to the target.",
149
+ },
150
+ withArrow: {
151
+ control: "boolean",
152
+ description: "Whether to display an arrow pointing to the target.",
153
+ },
154
+ offset: {
155
+ control: "number",
156
+ description:
157
+ "Distance in px between the dropdown and the target element.",
158
+ },
159
+ opened: {
160
+ control: "boolean",
161
+ description:
162
+ "Controlled open state. Leave undefined for uncontrolled behavior.",
163
+ },
164
+ },
165
+ parameters: {
166
+ docs: {
167
+ description: {
168
+ component: `
169
+ The \`Menu\` component combines a list of secondary actions into a single interactive overlay area, wrapping Mantine's composable \`Menu\` while enforcing strict Recursica design token styling.
170
+
171
+ ### Composable API
172
+
173
+ Menu uses a dot-notation composition pattern:
174
+ - \`<Menu>\` — Root container managing open/close state
175
+ - \`<Menu.Target>\` — Wrapper for the trigger element (must be a single element supporting ref)
176
+ - \`<Menu.Dropdown>\` — The popup panel
177
+ - \`<Menu.Item>\` — An actionable item with optional \`leftSection\` / \`rightSection\`
178
+ - \`<Menu.Divider>\` — A visual separator
179
+ - \`<Menu.Label>\` — A non-interactive section header
180
+
181
+ ### Sub-menus
182
+ Nested menus are supported via \`<Menu.Sub>\`, \`<Menu.Sub.Target>\`, \`<Menu.Sub.Item>\`, and \`<Menu.Sub.Dropdown>\`.
183
+
184
+ ### Design Token Limitations
185
+ Mantine's \`color\` prop on \`Menu.Item\` is stripped in strict mode to enforce design token adherence. Use \`overStyled={true}\` to bypass this restriction.
186
+ `,
187
+ },
26
188
  },
27
189
  },
28
190
  };
29
191
 
30
192
  export default meta;
31
- type Story = StoryObj<typeof Menu>;
193
+
194
+ type Story = StoryObj<MenuStoryArgs>;
32
195
 
33
196
  export const Default: Story = {
34
- render: () => <ComingSoon componentName="Menu" />,
197
+ args: {
198
+ trigger: "click",
199
+ position: "bottom-start",
200
+ withArrow: false,
201
+ offset: 5,
202
+ opened: undefined,
203
+ },
204
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
205
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
206
+ return (
207
+ <Menu {...args}>
208
+ <Menu.Target>
209
+ <Button variant="solid">Toggle Menu</Button>
210
+ </Menu.Target>
211
+
212
+ <Menu.Dropdown>
213
+ <Menu.Label>Application</Menu.Label>
214
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
215
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
216
+ <Menu.Item leftSection={<ImageIcon />}>Gallery</Menu.Item>
217
+ <Menu.Item leftSection={<SearchIcon />}>Search</Menu.Item>
218
+
219
+ <Menu.Divider />
220
+
221
+ <Menu.Label>Danger zone</Menu.Label>
222
+ <Menu.Item leftSection={<ArrowsIcon />}>Transfer my data</Menu.Item>
223
+ <Menu.Item leftSection={<TrashIcon />}>Delete my account</Menu.Item>
224
+ </Menu.Dropdown>
225
+ </Menu>
226
+ );
227
+ },
228
+ };
229
+
230
+ export const WithDisabledItems: Story = {
231
+ args: {
232
+ position: "bottom-start",
233
+ },
234
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
235
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
236
+ return (
237
+ <Menu {...args}>
238
+ <Menu.Target>
239
+ <Button variant="solid">Menu with Disabled</Button>
240
+ </Menu.Target>
241
+
242
+ <Menu.Dropdown>
243
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
244
+ <Menu.Item leftSection={<SearchIcon />} disabled>
245
+ Search (disabled)
246
+ </Menu.Item>
247
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
248
+ <Menu.Item leftSection={<TrashIcon />} disabled>
249
+ Delete (disabled)
250
+ </Menu.Item>
251
+ </Menu.Dropdown>
252
+ </Menu>
253
+ );
254
+ },
255
+ };
256
+
257
+ export const WithSubmenus: Story = {
258
+ args: {
259
+ position: "bottom-start",
260
+ width: 200,
261
+ },
262
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
263
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
264
+ return (
265
+ <Menu {...args}>
266
+ <Menu.Target>
267
+ <Button variant="solid">Menu with Submenus</Button>
268
+ </Menu.Target>
269
+
270
+ <Menu.Dropdown>
271
+ <Menu.Item>Dashboard</Menu.Item>
272
+ <Menu.Sub>
273
+ <Menu.Sub.Target>
274
+ <Menu.Sub.Item>Products</Menu.Sub.Item>
275
+ </Menu.Sub.Target>
276
+ <Menu.Sub.Dropdown>
277
+ <Menu.Item>All products</Menu.Item>
278
+ <Menu.Item>Categories</Menu.Item>
279
+ <Menu.Item>Tags</Menu.Item>
280
+ </Menu.Sub.Dropdown>
281
+ </Menu.Sub>
282
+ <Menu.Sub>
283
+ <Menu.Sub.Target>
284
+ <Menu.Sub.Item>Orders</Menu.Sub.Item>
285
+ </Menu.Sub.Target>
286
+ <Menu.Sub.Dropdown>
287
+ <Menu.Item>Open</Menu.Item>
288
+ <Menu.Item>Completed</Menu.Item>
289
+ <Menu.Item>Cancelled</Menu.Item>
290
+ </Menu.Sub.Dropdown>
291
+ </Menu.Sub>
292
+ </Menu.Dropdown>
293
+ </Menu>
294
+ );
295
+ },
296
+ };
297
+
298
+ export const HoverTrigger: Story = {
299
+ args: {
300
+ trigger: "click-hover",
301
+ position: "bottom-start",
302
+ openDelay: 100,
303
+ closeDelay: 400,
304
+ },
305
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
306
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
307
+ return (
308
+ <Menu {...args}>
309
+ <Menu.Target>
310
+ <Button variant="solid">Hover or Click</Button>
311
+ </Menu.Target>
312
+
313
+ <Menu.Dropdown>
314
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
315
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
316
+ <Menu.Item leftSection={<ImageIcon />}>Gallery</Menu.Item>
317
+ </Menu.Dropdown>
318
+ </Menu>
319
+ );
320
+ },
35
321
  };
@@ -1,7 +1,321 @@
1
- import React from "react";
1
+ import { forwardRef } from "react";
2
+ import { Menu as MuiMenu, type MenuProps as MuiMenuProps } from "@mui/material";
3
+ import {
4
+ filterStylingProps,
5
+ type RecursicaOverStyled,
6
+ } from "../../utils/filterStylingProps";
7
+ import styles from "./Menu.module.css";
2
8
 
3
- export type MenuProps = React.HTMLAttributes<HTMLDivElement>;
9
+ // ============================================================
10
+ // MENU ROOT
11
+ // ============================================================
4
12
 
5
- export const Menu: React.FC<MenuProps> = (props) => {
6
- return <div {...props}>Menu</div>;
13
+ /**
14
+ * Recursica Menu component wrapping Mui's composable Menu.
15
+ *
16
+ * Usage follows the composable dot-notation pattern:
17
+ * ```tsx
18
+ * <Menu>
19
+ * <Menu.Target><Button>Open</Button></Menu.Target>
20
+ * <Menu.Dropdown>
21
+ * <Menu.Label>Section</Menu.Label>
22
+ * <Menu.Item>Action</Menu.Item>
23
+ * <Menu.Divider />
24
+ * <Menu.Item>Another action</Menu.Item>
25
+ * </Menu.Dropdown>
26
+ * </Menu>
27
+ * ```
28
+ */
29
+ export type MenuProps = RecursicaOverStyled<MuiMenuProps>;
30
+
31
+ const MenuBase = function Menu({ overStyled = false, ...rest }: MenuProps) {
32
+ const sanitizedProps = filterStylingProps(rest, overStyled);
33
+
34
+ // Bind CSS module classes to Mui's internal classNames API
35
+ const mergedClassNames: Partial<Record<string, string>> = {
36
+ dropdown: styles.dropdown,
37
+ item: styles.item,
38
+ itemLabel: styles.itemLabel,
39
+ itemSection: styles.itemSection,
40
+ divider: styles.divider,
41
+ label: styles.label,
42
+ chevron: styles.chevron,
43
+ };
44
+
45
+ const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
46
+ if (
47
+ classNamesProp &&
48
+ typeof classNamesProp === "object" &&
49
+ !Array.isArray(classNamesProp)
50
+ ) {
51
+ const o = classNamesProp as Record<string, string>;
52
+ Object.keys(o).forEach((key) => {
53
+ if (mergedClassNames[key]) {
54
+ mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
55
+ } else {
56
+ mergedClassNames[key] = o[key];
57
+ }
58
+ });
59
+ }
60
+
61
+ return (
62
+ <MuiMenu
63
+ classes={mergedClassNames}
64
+ {...(sanitizedProps as unknown as MuiMenuProps)}
65
+ />
66
+ );
67
+ };
68
+ MenuBase.displayName = "Menu";
69
+
70
+ // ============================================================
71
+ // MENU TARGET
72
+ // ============================================================
73
+
74
+ /**
75
+ * Wrapper for the element that triggers the menu.
76
+ * Requires a single child element that supports ref forwarding.
77
+ */
78
+ export type MenuTargetProps = any;
79
+
80
+ const MenuTarget = function MenuTarget(props: MenuTargetProps) {
81
+ return <div {...props} />;
7
82
  };
83
+ MenuTarget.displayName = "MenuTarget";
84
+
85
+ // ============================================================
86
+ // MENU DROPDOWN
87
+ // ============================================================
88
+
89
+ /** The dropdown panel containing menu items, dividers, and labels. */
90
+ export type MenuDropdownProps = RecursicaOverStyled<any>;
91
+
92
+ const MenuDropdown = forwardRef<HTMLDivElement, MenuDropdownProps>(
93
+ function MenuDropdown({ overStyled = false, ...rest }, ref) {
94
+ const sanitizedProps = filterStylingProps(rest, overStyled);
95
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
96
+ .className as string | undefined;
97
+
98
+ return (
99
+ <div
100
+ ref={ref}
101
+ className={classNameProp}
102
+ {...(sanitizedProps as unknown as any)}
103
+ />
104
+ );
105
+ },
106
+ );
107
+ MenuDropdown.displayName = "MenuDropdown";
108
+
109
+ // ============================================================
110
+ // MENU ITEM
111
+ // ============================================================
112
+
113
+ /**
114
+ * An individual actionable item within the menu dropdown.
115
+ *
116
+ * **Note:** Mui's `color` prop is stripped in strict mode to enforce
117
+ * design token adherence. Use `overStyled={true}` if you need to bypass.
118
+ */
119
+ export type MenuItemProps = RecursicaOverStyled<Omit<any, "color">>;
120
+
121
+ const _MenuItem = forwardRef<HTMLButtonElement, MenuItemProps>(
122
+ function MenuItem({ overStyled = false, ...rest }, ref) {
123
+ const sanitizedProps = filterStylingProps(rest, overStyled);
124
+ const restRecord = sanitizedProps as Record<string, unknown>;
125
+
126
+ // Strip Mui's `color` prop to enforce token-driven styling
127
+ if (!overStyled) {
128
+ delete restRecord["color"];
129
+ }
130
+
131
+ const classNameProp = restRecord.className as string | undefined;
132
+
133
+ return (
134
+ <div
135
+ ref={ref}
136
+ className={classNameProp}
137
+ {...(sanitizedProps as unknown as any)}
138
+ />
139
+ );
140
+ },
141
+ );
142
+ _MenuItem.displayName = "MenuItem";
143
+
144
+ /**
145
+ * An individual actionable item within the menu dropdown.
146
+ *
147
+ * Supports polymorphism via the `component` prop for link-style items.
148
+ * @example
149
+ * ```tsx
150
+ * <Menu.Item component="a" href="/settings">Settings</Menu.Item>
151
+ * ```
152
+ */
153
+ const MenuItem = _MenuItem as any;
154
+
155
+ // ============================================================
156
+ // MENU DIVIDER
157
+ // ============================================================
158
+
159
+ /** A visual separator between groups of menu items. */
160
+ export type MenuDividerProps = RecursicaOverStyled<any>;
161
+
162
+ const MenuDivider = forwardRef<HTMLHRElement, MenuDividerProps>(
163
+ function MenuDivider({ overStyled = false, ...rest }, ref) {
164
+ const sanitizedProps = filterStylingProps(rest, overStyled);
165
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
166
+ .className as string | undefined;
167
+
168
+ return (
169
+ <hr
170
+ ref={ref}
171
+ className={classNameProp}
172
+ {...(sanitizedProps as unknown as any)}
173
+ />
174
+ );
175
+ },
176
+ );
177
+ MenuDivider.displayName = "MenuDivider";
178
+
179
+ // ============================================================
180
+ // MENU LABEL
181
+ // ============================================================
182
+
183
+ /** A non-interactive section label used to categorize groups of menu items. */
184
+ export type MenuLabelProps = RecursicaOverStyled<any>;
185
+
186
+ const MenuLabel = forwardRef<HTMLDivElement, MenuLabelProps>(function MenuLabel(
187
+ { overStyled = false, ...rest },
188
+ ref,
189
+ ) {
190
+ const sanitizedProps = filterStylingProps(rest, overStyled);
191
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
192
+ .className as string | undefined;
193
+
194
+ return (
195
+ <div
196
+ ref={ref}
197
+ className={classNameProp}
198
+ {...(sanitizedProps as unknown as any)}
199
+ />
200
+ );
201
+ });
202
+ MenuLabel.displayName = "MenuLabel";
203
+
204
+ // ============================================================
205
+ // MENU SUB (SUBMENU CONTAINER)
206
+ // ============================================================
207
+
208
+ /** Container for a submenu, wrapping Menu.Sub.Target and Menu.Sub.Dropdown. */
209
+ export type MenuSubProps = any;
210
+
211
+ const MenuSubBase = function MenuSub(props: MenuSubProps) {
212
+ return <div {...props} />;
213
+ };
214
+ MenuSubBase.displayName = "MenuSub";
215
+
216
+ // ============================================================
217
+ // MENU SUB TARGET
218
+ // ============================================================
219
+
220
+ /** Wrapper for the element that triggers the submenu. */
221
+ export type MenuSubTargetProps = any;
222
+
223
+ const MenuSubTarget = function MenuSubTarget(props: MenuSubTargetProps) {
224
+ return <div {...props} />;
225
+ };
226
+ MenuSubTarget.displayName = "MenuSubTarget";
227
+
228
+ // ============================================================
229
+ // MENU SUB ITEM (triggers submenu)
230
+ // ============================================================
231
+
232
+ /**
233
+ * A menu item that acts as a submenu trigger.
234
+ * Renders within Menu.Sub.Target and displays a chevron indicator.
235
+ */
236
+ export type MenuSubItemProps = RecursicaOverStyled<Omit<any, "color">>;
237
+
238
+ const _MenuSubItem = forwardRef<HTMLButtonElement, MenuSubItemProps>(
239
+ function MenuSubItem({ overStyled = false, ...rest }, ref) {
240
+ const sanitizedProps = filterStylingProps(rest, overStyled);
241
+ const restRecord = sanitizedProps as Record<string, unknown>;
242
+
243
+ if (!overStyled) {
244
+ delete restRecord["color"];
245
+ }
246
+
247
+ const classNameProp = restRecord.className as string | undefined;
248
+
249
+ return (
250
+ <div
251
+ ref={ref}
252
+ className={classNameProp}
253
+ {...(sanitizedProps as unknown as any)}
254
+ />
255
+ );
256
+ },
257
+ );
258
+ _MenuSubItem.displayName = "MenuSubItem";
259
+
260
+ /**
261
+ * A menu item that acts as a submenu trigger.
262
+ *
263
+ * Supports polymorphism via the `component` prop.
264
+ */
265
+ const MenuSubItem = _MenuSubItem as any;
266
+
267
+ // ============================================================
268
+ // MENU SUB DROPDOWN
269
+ // ============================================================
270
+
271
+ /** The dropdown panel for a submenu. */
272
+ export type MenuSubDropdownProps = RecursicaOverStyled<any>;
273
+
274
+ const MenuSubDropdown = forwardRef<HTMLDivElement, MenuSubDropdownProps>(
275
+ function MenuSubDropdown({ overStyled = false, ...rest }, ref) {
276
+ const sanitizedProps = filterStylingProps(rest, overStyled);
277
+ const classNameProp = (sanitizedProps as Record<string, unknown>)
278
+ .className as string | undefined;
279
+
280
+ return (
281
+ <div
282
+ ref={ref}
283
+ className={classNameProp}
284
+ {...(sanitizedProps as unknown as any)}
285
+ />
286
+ );
287
+ },
288
+ );
289
+ MenuSubDropdown.displayName = "MenuSubDropdown";
290
+
291
+ // ============================================================
292
+ // DOT NOTATION EXPORT
293
+ // ============================================================
294
+
295
+ type MenuSubComponent = typeof MenuSubBase & {
296
+ Target: typeof MenuSubTarget;
297
+ Item: typeof MenuSubItem;
298
+ Dropdown: typeof MenuSubDropdown;
299
+ };
300
+
301
+ const MenuSub = MenuSubBase as MenuSubComponent;
302
+ MenuSub.Target = MenuSubTarget;
303
+ MenuSub.Item = MenuSubItem;
304
+ MenuSub.Dropdown = MenuSubDropdown;
305
+
306
+ type MenuComponent = typeof MenuBase & {
307
+ Target: typeof MenuTarget;
308
+ Dropdown: typeof MenuDropdown;
309
+ Item: typeof MenuItem;
310
+ Divider: typeof MenuDivider;
311
+ Label: typeof MenuLabel;
312
+ Sub: MenuSubComponent;
313
+ };
314
+
315
+ export const Menu = MenuBase as MenuComponent;
316
+ Menu.Target = MenuTarget;
317
+ Menu.Dropdown = MenuDropdown;
318
+ Menu.Item = MenuItem;
319
+ Menu.Divider = MenuDivider;
320
+ Menu.Label = MenuLabel;
321
+ Menu.Sub = MenuSub;