@recursica/mantine-adapter 0.9.3 → 0.10.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 (241) hide show
  1. package/.storybook/commonArgTypes.ts +148 -0
  2. package/CHANGELOG.md +88 -0
  3. package/README.md +48 -332
  4. package/dist/index.d.ts +2 -0
  5. package/dist/mantine-adapter.cjs +2 -0
  6. package/dist/mantine-adapter.cjs.map +1 -0
  7. package/dist/mantine-adapter.css +1 -0
  8. package/dist/mantine-adapter.js +1797 -0
  9. package/dist/mantine-adapter.js.map +1 -0
  10. package/dist/src/components/Accordion/Accordion.d.ts +42 -0
  11. package/dist/src/components/Accordion/index.d.ts +1 -0
  12. package/dist/src/components/AssistiveElement/AssistiveElement.d.ts +10 -0
  13. package/dist/src/components/Avatar/Avatar.d.ts +9 -0
  14. package/dist/src/components/Avatar/index.d.ts +1 -0
  15. package/dist/src/components/Badge/Badge.d.ts +8 -0
  16. package/dist/src/components/Breadcrumb/Breadcrumb.d.ts +6 -0
  17. package/dist/src/components/Button/Button.d.ts +10 -0
  18. package/dist/src/components/Button/index.d.ts +1 -0
  19. package/dist/src/components/Card/Card.d.ts +38 -0
  20. package/dist/src/components/Checkbox/Checkbox.d.ts +11 -0
  21. package/dist/src/components/Checkbox/CheckboxGroup.d.ts +9 -0
  22. package/dist/src/components/Chip/Chip.d.ts +15 -0
  23. package/dist/src/components/Container/Container.d.ts +18 -0
  24. package/dist/src/components/DatePicker/DatePicker.d.ts +3 -0
  25. package/dist/src/components/Dropdown/Dropdown.d.ts +11 -0
  26. package/dist/src/components/FileInput/FileInput.d.ts +3 -0
  27. package/dist/src/components/FileUpload/FileUpload.d.ts +3 -0
  28. package/dist/src/components/Flex/Flex.d.ts +17 -0
  29. package/dist/src/components/FormControlWrapper/FormControlWrapper.d.ts +18 -0
  30. package/dist/src/components/Group/Group.d.ts +17 -0
  31. package/dist/src/components/HoverCard/HoverCard.d.ts +3 -0
  32. package/dist/src/components/Label/Label.d.ts +21 -0
  33. package/dist/src/components/Label/index.d.ts +1 -0
  34. package/dist/src/components/Link/Link.d.ts +3 -0
  35. package/dist/src/components/Loader/Loader.d.ts +10 -0
  36. package/dist/src/components/Menu/Menu.d.ts +3 -0
  37. package/dist/src/components/Modal/Modal.d.ts +3 -0
  38. package/dist/src/components/NumberInput/NumberInput.d.ts +3 -0
  39. package/dist/src/components/Pagination/Pagination.d.ts +3 -0
  40. package/dist/src/components/Panel/Panel.d.ts +3 -0
  41. package/dist/src/components/Popover/Popover.d.ts +3 -0
  42. package/dist/src/components/Radio/Radio.d.ts +11 -0
  43. package/dist/src/components/Radio/RadioGroup.d.ts +9 -0
  44. package/dist/src/components/ReadOnlyField/ReadOnlyField.d.ts +20 -0
  45. package/dist/src/components/ReadOnlyField/ReadOnlyTextField.d.ts +8 -0
  46. package/dist/src/components/ReadOnlyField/WithReadOnlyWrapper.d.ts +22 -0
  47. package/dist/src/components/ReadOnlyField/index.d.ts +3 -0
  48. package/dist/src/components/Search/Search.d.ts +3 -0
  49. package/dist/src/components/SegmentedControl/SegmentedControl.d.ts +3 -0
  50. package/dist/src/components/Slider/Slider.d.ts +3 -0
  51. package/dist/src/components/Stack/Stack.d.ts +15 -0
  52. package/dist/src/components/Stepper/Stepper.d.ts +3 -0
  53. package/dist/src/components/Switch/Switch.d.ts +11 -0
  54. package/dist/src/components/Switch/SwitchGroup.d.ts +9 -0
  55. package/dist/src/components/Table/Table.d.ts +3 -0
  56. package/dist/src/components/Tabs/Tabs.d.ts +3 -0
  57. package/dist/src/components/Text/Text.d.ts +17 -0
  58. package/dist/src/components/TextArea/TextArea.d.ts +15 -0
  59. package/dist/src/components/TextField/TextField.d.ts +9 -0
  60. package/dist/src/components/TimePicker/TimePicker.d.ts +3 -0
  61. package/dist/src/components/Timeline/Timeline.d.ts +3 -0
  62. package/dist/src/components/Title/Title.d.ts +13 -0
  63. package/dist/src/components/Toast/Toast.d.ts +3 -0
  64. package/dist/src/components/Tooltip/Tooltip.d.ts +3 -0
  65. package/dist/src/components/TransferList/TransferList.d.ts +3 -0
  66. package/dist/src/components/index.d.ts +47 -0
  67. package/dist/src/index.d.ts +2 -0
  68. package/dist/src/types/index.d.ts +6 -0
  69. package/dist/src/utils/ColorSchemeWrapper.d.ts +3 -0
  70. package/dist/src/utils/copyToClipboard.d.ts +8 -0
  71. package/dist/src/utils/filterStylingProps.d.ts +36 -0
  72. package/dist/src/utils/index.d.ts +1 -0
  73. package/dist/vite.config.d.ts +2 -0
  74. package/dist/vite.svg +1 -0
  75. package/dist/vitest.workspace.d.ts +2 -0
  76. package/package.json +74 -35
  77. package/src/OverStyling.stories.tsx +174 -0
  78. package/src/Version.stories.tsx +56 -0
  79. package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +48 -0
  80. package/src/components/Accordion/Accordion.module.css +282 -0
  81. package/src/components/Accordion/Accordion.stories.tsx +145 -0
  82. package/src/components/Accordion/Accordion.tsx +200 -0
  83. package/src/components/Accordion/index.ts +1 -0
  84. package/src/components/AssistiveElement/AssistiveElement.module.css +111 -0
  85. package/src/components/AssistiveElement/AssistiveElement.stories.tsx +73 -0
  86. package/src/components/AssistiveElement/AssistiveElement.tsx +91 -0
  87. package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +24 -0
  88. package/src/components/Avatar/Avatar.module.css +340 -0
  89. package/src/components/Avatar/Avatar.stories.tsx +102 -0
  90. package/src/components/Avatar/Avatar.tsx +100 -0
  91. package/src/components/Avatar/index.ts +1 -0
  92. package/src/components/Badge/BADGE_IMPLEMENTATION_NOTES.md +18 -0
  93. package/src/components/Badge/Badge.module.css +124 -0
  94. package/src/components/Badge/Badge.stories.tsx +77 -0
  95. package/src/components/Badge/Badge.tsx +66 -0
  96. package/src/components/Breadcrumb/BREADCRUMB_IMPLEMENTATION_NOTES.md +14 -0
  97. package/src/components/Breadcrumb/Breadcrumb.module.css +26 -0
  98. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +77 -0
  99. package/src/components/Breadcrumb/Breadcrumb.tsx +64 -0
  100. package/src/components/Button/Button.module.css +275 -0
  101. package/src/components/Button/Button.stories.tsx +105 -0
  102. package/src/components/Button/Button.tsx +122 -0
  103. package/src/components/Button/IMPLEMENTATION_NOTES.md +45 -0
  104. package/src/components/Button/index.ts +1 -0
  105. package/src/components/Card/CARD_IMPLEMENTATION_NOTES.md +10 -0
  106. package/src/components/Card/Card.module.css +60 -0
  107. package/src/components/Card/Card.stories.tsx +141 -0
  108. package/src/components/Card/Card.tsx +176 -0
  109. package/src/components/Checkbox/CHECKBOX_IMPLEMENTATION_NOTES.md +25 -0
  110. package/src/components/Checkbox/Checkbox.module.css +204 -0
  111. package/src/components/Checkbox/Checkbox.stories.tsx +73 -0
  112. package/src/components/Checkbox/Checkbox.tsx +102 -0
  113. package/src/components/Checkbox/CheckboxGroup.stories.tsx +138 -0
  114. package/src/components/Checkbox/CheckboxGroup.tsx +140 -0
  115. package/src/components/Chip/CHIP_IMPLEMENTATION_NOTES.md +35 -0
  116. package/src/components/Chip/Chip.module.css +218 -0
  117. package/src/components/Chip/Chip.stories.tsx +115 -0
  118. package/src/components/Chip/Chip.tsx +143 -0
  119. package/src/components/Container/CONTAINER_IMPLEMENTATION_NOTES.md +4 -0
  120. package/src/components/Container/Container.module.css +7 -0
  121. package/src/components/Container/Container.stories.tsx +115 -0
  122. package/src/components/Container/Container.tsx +89 -0
  123. package/src/components/DatePicker/DatePicker.stories.tsx +22 -0
  124. package/src/components/DatePicker/DatePicker.tsx +7 -0
  125. package/src/components/Dropdown/DROPDOWN_IMPLEMENTATION_NOTES.md +7 -0
  126. package/src/components/Dropdown/Dropdown.module.css +272 -0
  127. package/src/components/Dropdown/Dropdown.stories.tsx +94 -0
  128. package/src/components/Dropdown/Dropdown.tsx +173 -0
  129. package/src/components/FileInput/FileInput.stories.tsx +22 -0
  130. package/src/components/FileInput/FileInput.tsx +7 -0
  131. package/src/components/FileUpload/FileUpload.stories.tsx +22 -0
  132. package/src/components/FileUpload/FileUpload.tsx +7 -0
  133. package/src/components/Flex/FLEX_IMPLEMENTATION_NOTES.md +4 -0
  134. package/src/components/Flex/Flex.module.css +7 -0
  135. package/src/components/Flex/Flex.stories.tsx +125 -0
  136. package/src/components/Flex/Flex.tsx +68 -0
  137. package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +32 -0
  138. package/src/components/FormControlWrapper/FormControlWrapper.module.css +46 -0
  139. package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +116 -0
  140. package/src/components/FormControlWrapper/FormControlWrapper.tsx +170 -0
  141. package/src/components/Group/GROUP_IMPLEMENTATION_NOTES.md +4 -0
  142. package/src/components/Group/Group.module.css +7 -0
  143. package/src/components/Group/Group.stories.tsx +117 -0
  144. package/src/components/Group/Group.tsx +68 -0
  145. package/src/components/HoverCard/HoverCard.stories.tsx +17 -0
  146. package/src/components/HoverCard/HoverCard.tsx +7 -0
  147. package/src/components/Label/LABEL_IMPLEMENTATION_NOTES.md +48 -0
  148. package/src/components/Label/Label.module.css +177 -0
  149. package/src/components/Label/Label.stories.tsx +123 -0
  150. package/src/components/Label/Label.tsx +132 -0
  151. package/src/components/Label/index.ts +1 -0
  152. package/src/components/Link/Link.stories.tsx +17 -0
  153. package/src/components/Link/Link.tsx +7 -0
  154. package/src/components/Loader/Loader.module.css +60 -0
  155. package/src/components/Loader/Loader.stories.tsx +98 -0
  156. package/src/components/Loader/Loader.tsx +76 -0
  157. package/src/components/Menu/Menu.stories.tsx +17 -0
  158. package/src/components/Menu/Menu.tsx +7 -0
  159. package/src/components/Modal/Modal.stories.tsx +17 -0
  160. package/src/components/Modal/Modal.tsx +7 -0
  161. package/src/components/NumberInput/NumberInput.stories.tsx +22 -0
  162. package/src/components/NumberInput/NumberInput.tsx +7 -0
  163. package/src/components/Pagination/Pagination.stories.tsx +17 -0
  164. package/src/components/Pagination/Pagination.tsx +7 -0
  165. package/src/components/Panel/Panel.stories.tsx +17 -0
  166. package/src/components/Panel/Panel.tsx +7 -0
  167. package/src/components/Popover/Popover.stories.tsx +17 -0
  168. package/src/components/Popover/Popover.tsx +7 -0
  169. package/src/components/Radio/Radio.module.css +207 -0
  170. package/src/components/Radio/Radio.stories.tsx +76 -0
  171. package/src/components/Radio/Radio.tsx +115 -0
  172. package/src/components/Radio/RadioGroup.stories.tsx +98 -0
  173. package/src/components/Radio/RadioGroup.tsx +138 -0
  174. package/src/components/ReadOnlyField/READ_ONLY_FIELD_IMPLEMENTATION_NOTES.md +15 -0
  175. package/src/components/ReadOnlyField/ReadOnlyField.module.css +49 -0
  176. package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +120 -0
  177. package/src/components/ReadOnlyField/ReadOnlyField.tsx +75 -0
  178. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +46 -0
  179. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +85 -0
  180. package/src/components/ReadOnlyField/index.ts +3 -0
  181. package/src/components/Search/Search.stories.tsx +17 -0
  182. package/src/components/Search/Search.tsx +7 -0
  183. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +22 -0
  184. package/src/components/SegmentedControl/SegmentedControl.tsx +7 -0
  185. package/src/components/Slider/Slider.stories.tsx +22 -0
  186. package/src/components/Slider/Slider.tsx +7 -0
  187. package/src/components/Stack/STACK_IMPLEMENTATION_NOTES.md +4 -0
  188. package/src/components/Stack/Stack.module.css +7 -0
  189. package/src/components/Stack/Stack.stories.tsx +105 -0
  190. package/src/components/Stack/Stack.tsx +66 -0
  191. package/src/components/Stepper/Stepper.stories.tsx +17 -0
  192. package/src/components/Stepper/Stepper.tsx +7 -0
  193. package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +28 -0
  194. package/src/components/Switch/Switch.module.css +207 -0
  195. package/src/components/Switch/Switch.stories.tsx +96 -0
  196. package/src/components/Switch/Switch.tsx +119 -0
  197. package/src/components/Switch/SwitchGroup.stories.tsx +112 -0
  198. package/src/components/Switch/SwitchGroup.tsx +138 -0
  199. package/src/components/Table/Table.stories.tsx +17 -0
  200. package/src/components/Table/Table.tsx +7 -0
  201. package/src/components/Tabs/Tabs.stories.tsx +17 -0
  202. package/src/components/Tabs/Tabs.tsx +7 -0
  203. package/src/components/Text/Text.stories.tsx +88 -0
  204. package/src/components/Text/Text.tsx +54 -0
  205. package/src/components/TextArea/TEXTAREA_IMPLEMENTATION_NOTES.md +7 -0
  206. package/src/components/TextArea/TextArea.module.css +225 -0
  207. package/src/components/TextArea/TextArea.stories.tsx +94 -0
  208. package/src/components/TextArea/TextArea.tsx +151 -0
  209. package/src/components/TextField/TEXTFIELD_IMPLEMENTATION_NOTES.md +19 -0
  210. package/src/components/TextField/TextField.module.css +225 -0
  211. package/src/components/TextField/TextField.stories.tsx +162 -0
  212. package/src/components/TextField/TextField.tsx +138 -0
  213. package/src/components/TimePicker/TimePicker.stories.tsx +22 -0
  214. package/src/components/TimePicker/TimePicker.tsx +7 -0
  215. package/src/components/Timeline/Timeline.stories.tsx +17 -0
  216. package/src/components/Timeline/Timeline.tsx +7 -0
  217. package/src/components/Title/Title.stories.tsx +63 -0
  218. package/src/components/Title/Title.tsx +45 -0
  219. package/src/components/Toast/Toast.stories.tsx +17 -0
  220. package/src/components/Toast/Toast.tsx +7 -0
  221. package/src/components/Tooltip/Tooltip.stories.tsx +17 -0
  222. package/src/components/Tooltip/Tooltip.tsx +7 -0
  223. package/src/components/TransferList/TransferList.stories.tsx +17 -0
  224. package/src/components/TransferList/TransferList.tsx +7 -0
  225. package/src/components/index.ts +47 -0
  226. package/src/env.d.ts +7 -0
  227. package/src/index.ts +3 -0
  228. package/src/types/index.ts +9 -0
  229. package/src/types/mantine.d.ts +7 -0
  230. package/src/types/scss-modules.d.ts +9 -0
  231. package/src/utils/ColorSchemeWrapper.tsx +27 -0
  232. package/src/utils/copyToClipboard.ts +36 -0
  233. package/src/utils/filterStylingProps.ts +141 -0
  234. package/src/utils/index.ts +1 -0
  235. package/dist/index.js +0 -917
  236. package/dist/index.js.map +0 -1
  237. package/dist/main.cjs +0 -924
  238. package/dist/main.cjs.map +0 -1
  239. package/dist/main.js +0 -922
  240. package/dist/main.js.map +0 -1
  241. package/dist/webworker.js +0 -787
@@ -0,0 +1,125 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Flex } from "./Flex";
4
+ import { Button } from "../Button";
5
+ import { Text } from "../Text/Text";
6
+
7
+ type FlexStoryProps = React.ComponentProps<typeof Flex>;
8
+
9
+ const meta: Meta<FlexStoryProps> = {
10
+ title: "UI-Kit/Flex",
11
+ component: Flex,
12
+ tags: ["autodocs"],
13
+ parameters: {
14
+ docs: {
15
+ description: {
16
+ component:
17
+ "Flex is a bare-metal flex container that maps directly to Mantine's Flex component, providing unopinionated control over direction, alignment, and wrapping.",
18
+ },
19
+ },
20
+ },
21
+ args: {
22
+ gap: "rec-default",
23
+ align: "center",
24
+ justify: "flex-start",
25
+ direction: "row",
26
+ wrap: "wrap",
27
+ },
28
+ argTypes: {
29
+ gap: {
30
+ control: "select",
31
+ options: [
32
+ "rec-none",
33
+ "rec-sm",
34
+ "rec-default",
35
+ "rec-md",
36
+ "rec-lg",
37
+ "rec-xl",
38
+ "rec-2xl",
39
+ "xs",
40
+ "sm",
41
+ "md",
42
+ "lg",
43
+ "xl",
44
+ ],
45
+ description: "Gap between elements",
46
+ },
47
+ direction: {
48
+ control: "select",
49
+ options: ["row", "column", "row-reverse", "column-reverse"],
50
+ description: "Flex-direction property",
51
+ },
52
+ align: {
53
+ control: "select",
54
+ options: ["flex-start", "center", "flex-end", "stretch"],
55
+ description: "Align-items property",
56
+ },
57
+ justify: {
58
+ control: "select",
59
+ options: [
60
+ "flex-start",
61
+ "center",
62
+ "flex-end",
63
+ "space-between",
64
+ "space-around",
65
+ ],
66
+ description: "Justify-content property",
67
+ },
68
+ wrap: {
69
+ control: "select",
70
+ options: ["wrap", "nowrap", "wrap-reverse"],
71
+ description: "Flex-wrap property",
72
+ },
73
+ defaultChecked: {
74
+ table: { disable: true },
75
+ },
76
+ rowGap: {
77
+ table: { disable: true },
78
+ },
79
+ columnGap: {
80
+ table: { disable: true },
81
+ },
82
+ },
83
+ };
84
+
85
+ export default meta;
86
+
87
+ type Story = StoryObj<FlexStoryProps>;
88
+
89
+ export const Default: Story = {
90
+ render: (args) => (
91
+ <Flex {...args}>
92
+ <Button variant="solid">Block A</Button>
93
+ <Button variant="outline">Block B</Button>
94
+ <Text>Text inside Flex</Text>
95
+ </Flex>
96
+ ),
97
+ };
98
+
99
+ export const StaticGapSmallColumn: Story = {
100
+ args: {
101
+ gap: "rec-sm",
102
+ direction: "column",
103
+ },
104
+ render: (args) => (
105
+ <Flex {...args}>
106
+ <Button variant="solid">Item 1</Button>
107
+ <Button variant="solid">Item 2</Button>
108
+ <Button variant="solid">Item 3</Button>
109
+ </Flex>
110
+ ),
111
+ };
112
+
113
+ export const StaticGapLargeRow: Story = {
114
+ args: {
115
+ gap: "rec-xl",
116
+ direction: "row",
117
+ },
118
+ render: (args) => (
119
+ <Flex {...args}>
120
+ <Button variant="solid">Item 1</Button>
121
+ <Button variant="solid">Item 2</Button>
122
+ <Button variant="solid">Item 3</Button>
123
+ </Flex>
124
+ ),
125
+ };
@@ -0,0 +1,68 @@
1
+ import React, { forwardRef } from "react";
2
+ import {
3
+ Flex as MantineFlex,
4
+ type FlexProps as MantineFlexProps,
5
+ } from "@mantine/core";
6
+ import {
7
+ filterStylingProps,
8
+ type RecursicaOverStyled,
9
+ type RecursicaSpacing,
10
+ } from "../../utils/filterStylingProps";
11
+ import styles from "./Flex.module.css";
12
+
13
+ export interface RecursicaFlexProps {
14
+ /**
15
+ * Children components inside the Flex container
16
+ */
17
+ children?: React.ReactNode;
18
+ gap?: MantineFlexProps["gap"] | RecursicaSpacing;
19
+ rowGap?: MantineFlexProps["gap"] | RecursicaSpacing;
20
+ columnGap?: MantineFlexProps["gap"] | RecursicaSpacing;
21
+ }
22
+
23
+ /**
24
+ * Flex layout wrapper
25
+ */
26
+ export type FlexProps = RecursicaOverStyled<
27
+ MantineFlexProps & RecursicaFlexProps
28
+ >;
29
+
30
+ export const Flex = forwardRef<HTMLDivElement, FlexProps>(function Flex(
31
+ { children, overStyled = false, gap = "rec-default", ...rest },
32
+ ref,
33
+ ) {
34
+ const sanitizedProps = filterStylingProps({ ...rest, gap }, overStyled);
35
+ const restRecord = sanitizedProps as Record<string, unknown>;
36
+
37
+ const mergedClassNames: Partial<Record<string, string>> = {
38
+ root: styles.root,
39
+ };
40
+
41
+ const classNamesProp = restRecord.classNames;
42
+ if (
43
+ classNamesProp &&
44
+ typeof classNamesProp === "object" &&
45
+ !Array.isArray(classNamesProp)
46
+ ) {
47
+ const o = classNamesProp as Partial<Record<string, string>>;
48
+ mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
49
+ }
50
+
51
+ const classNameProp = restRecord.className as string | undefined;
52
+ const finalClass = classNameProp
53
+ ? `${styles.root} ${classNameProp}`
54
+ : styles.root;
55
+
56
+ return (
57
+ <MantineFlex
58
+ ref={ref}
59
+ className={finalClass}
60
+ classNames={mergedClassNames}
61
+ {...sanitizedProps}
62
+ >
63
+ {children}
64
+ </MantineFlex>
65
+ );
66
+ });
67
+
68
+ Flex.displayName = "Flex";
@@ -0,0 +1,32 @@
1
+ # FormControlWrapper Implementation Notes
2
+
3
+ ## Architectural Philosophy
4
+
5
+ The `FormControlWrapper` is the ultimate structural replacement for Mantine's built-in `Input.Wrapper`. By abandoning Mantine's opinionated wrappers entirely across the design system, we centralize all label tracking, error rendering, ARIA generation, and grid layouts natively inside this single component.
6
+
7
+ ### 1. Bypassing `Input.Wrapper`
8
+
9
+ Under the hood of Mantine, elements like `TextInput` heavily rely on `Input.Wrapper`. We actively discourage their use. The central tenet of Recursica Forms is to strip the UI primitive back to its "naked" form (e.g. `<Input />`, `<Checkbox />`) and encapsulate it manually inside `<FormControlWrapper>`.
10
+
11
+ - **Why?** It enforces complete layout mastery. It natively enables `formLayout="side-by-side"` and completely disables Mantine's margin collisions without requiring messy CSS hacks.
12
+
13
+ ### 2. The `cloneElement` ARIA Map
14
+
15
+ Because we tore out Mantine's `InputContext` provider (which natively glued error strings to `<input>` tags using React Context), we explicitly utilize `React.cloneElement` on the nested children inside this wrapper.
16
+
17
+ - `aria-describedby` and `aria-errormessage` are dynamically generated using `React.useId()` and physically injected back onto the provided child node. Screen readers rely strictly on this mapping to announce the assistive fields correctly.
18
+
19
+ ### 3. Strict `AssistiveElement` Coupling
20
+
21
+ We completely abandoned generic `<Input.Description>` tags. The `FormControlWrapper` directly renders `<AssistiveElement>` primitives, parsing them seamlessly mapping them to `"error"` or `"help"` variants automatically depending on the component's internal state machine.
22
+
23
+ ### 4. Dynamic Geometric Variable Payloads
24
+
25
+ Because `FormControlWrapper` acts as an agnostic grid box encompassing raw primitives (like `TextField`, `Select`), it initially stretches `100%` across horizontal bounds.
26
+
27
+ - **The Bug:** If a child `TextField` carries its own hardcoded `max-width` token, it stops expanding early, but the wrapper and `<Label>` keep expanding, causing right-aligned labels to aggressively float past the field to the screen's edge dynamically.
28
+ - **The Variable Payload Resolution:** Instead of destroying grids with `width: fit-content` arrays, primitive components are required to pass their local `max-width` tokens UP to the wrapper explicitly via React `style`:
29
+ ```tsx
30
+ <FormControlWrapper style={{ "--form-control-max-width": "var(--...)" }}>
31
+ ```
32
+ The wrapper natively respects `max-width: var(--form-control-max-width, 100%)`. This structurally unifies the bounding caps so right-aligned labels flawlessly snap tightly to the explicit boundary edge of the specific primitive it is wrapping.
@@ -0,0 +1,46 @@
1
+ /* FormControlWrapper.module.css */
2
+
3
+ .root {
4
+ display: flex;
5
+ margin: 0; /* Hardcoded structural reset */
6
+ padding: 0; /* Hardcoded structural reset */
7
+ max-width: var(--form-control-max-width, 100%);
8
+ min-width: var(--form-control-min-width, auto);
9
+ }
10
+
11
+ /* Base Structural Stacked Layout */
12
+ .root[data-form-layout="stacked"] {
13
+ flex-direction: column;
14
+ }
15
+
16
+ /* Base Structural Grid Layout */
17
+ .root[data-form-layout="side-by-side"] {
18
+ flex-direction: row;
19
+ align-items: flex-start;
20
+ flex-wrap: wrap; /* Allows collapse if screen is too thin, though standard flex width should hold */
21
+ }
22
+
23
+ /* Left structural justification defaults (push input mapping) */
24
+ .root[data-form-alignment="left"] {
25
+ /* Inherited naturally via row/column flows */
26
+ }
27
+
28
+ /* Right structural justification defaults */
29
+ .root[data-form-alignment="right"] {
30
+ /* To justify entire form control components to the right */
31
+ justify-content: flex-end;
32
+ }
33
+
34
+ .labelSection {
35
+ /* Width dimensions are strictly governed natively inside Label.module.css's sizing variant mapping */
36
+ flex-shrink: 0;
37
+ display: flex;
38
+ flex-direction: column;
39
+ }
40
+
41
+ .inputSection {
42
+ display: flex; /* Groups internal field alongside subsequent errors/descriptions */
43
+ flex-direction: column;
44
+ flex: 1; /* Consumes the remaining x-axis in grid columns natively */
45
+ min-width: 0; /* Break boundary preventing internal flex explosion */
46
+ }
@@ -0,0 +1,116 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import {
4
+ FormControlWrapper,
5
+ type FormControlWrapperProps,
6
+ } from "./FormControlWrapper";
7
+ import { TextField } from "../TextField/TextField";
8
+ import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
9
+
10
+ type WrapperStoryProps = FormControlWrapperProps;
11
+
12
+ const meta: Meta<WrapperStoryProps> = {
13
+ title: "UI-Kit/FormControlWrapper",
14
+ component: FormControlWrapper,
15
+ tags: ["autodocs"],
16
+ parameters: {
17
+ docs: {
18
+ description: {
19
+ component:
20
+ "The `FormControlWrapper` is the ultimate structural replacement for Mantine's built-in `Input.Wrapper`. By abandoning Mantine's opinionated wrappers entirely, we centralize all label tracking, error rendering, ARIA generation, and grid layouts natively inside this single component.\n\n### Usage with Naked Primitives\nThis component wraps 'naked' elements like `<Input>` directly. The demonstration stories below utilize `<TextField>` as a native display vehicle, since `<TextField>` natively pipes all its properties structurally back into this wrapper.",
21
+ },
22
+ },
23
+ },
24
+ argTypes: {
25
+ ...formControlArgTypes,
26
+ error: {
27
+ control: "text",
28
+ description:
29
+ "Error string driving native assistive component and validation markers.",
30
+ },
31
+ assistiveText: {
32
+ control: "text",
33
+ description:
34
+ "Helper instructions safely dynamically anchored below the input box.",
35
+ },
36
+ assistiveWithIcon: {
37
+ control: "boolean",
38
+ },
39
+ required: {
40
+ control: "boolean",
41
+ },
42
+ },
43
+ };
44
+
45
+ export default meta;
46
+
47
+ type Story = StoryObj<WrapperStoryProps>;
48
+
49
+ const renderWithTextField = (args: WrapperStoryProps) => (
50
+ <TextField placeholder="Form Control primitive mapped..." {...args} />
51
+ );
52
+
53
+ export const Default: Story = {
54
+ args: {
55
+ label: "Account Username",
56
+ formLayout: "stacked",
57
+ assistiveText: "Validation occurs immediately natively.",
58
+ },
59
+ render: renderWithTextField,
60
+ };
61
+
62
+ export const VisualErrorState: Story = {
63
+ args: {
64
+ label: "Encryption Protocol",
65
+ formLayout: "stacked",
66
+ error: "Strict validation limits reached. Handshake rejected securely.",
67
+ },
68
+ render: renderWithTextField,
69
+ };
70
+
71
+ export const RequiredArchitecture: Story = {
72
+ args: {
73
+ label: "Root Password",
74
+ formLayout: "side-by-side",
75
+ required: true,
76
+ assistiveText: "Bypass string structure required to initiate protocol.",
77
+ },
78
+ render: renderWithTextField,
79
+ };
80
+
81
+ export const WithoutAssistiveIcons: Story = {
82
+ args: {
83
+ label: "Server Domain",
84
+ assistiveText:
85
+ "A standard text boundary without default native icon parameters bounding.",
86
+ assistiveWithIcon: false,
87
+ },
88
+ render: renderWithTextField,
89
+ };
90
+
91
+ export const NativeChildrenDirectly: Story = {
92
+ description:
93
+ "Bypassing the TextField map to show exactly how native `<input>` hooks execute inside the raw wrapper natively perfectly.",
94
+ args: {
95
+ label: "Raw HTML Checkbox",
96
+ formLayout: "side-by-side",
97
+ assistiveText: "This wraps a raw HTML input tag mapping correctly.",
98
+ },
99
+ render: (args) => (
100
+ <div
101
+ style={{
102
+ display: "flex",
103
+ gap: "10px",
104
+ alignItems: "center",
105
+ }}
106
+ >
107
+ {/* We can cleanly wrap even un-styled HTML primitives! */}
108
+ <FormControlWrapper {...args}>
109
+ <input
110
+ type="checkbox"
111
+ style={{ margin: 0, width: "16px", height: "16px" }}
112
+ />
113
+ </FormControlWrapper>
114
+ </div>
115
+ ),
116
+ };
@@ -0,0 +1,170 @@
1
+ import React, { forwardRef, useId } from "react";
2
+ import { type InputWrapperProps, Box } from "@mantine/core";
3
+ import {
4
+ filterStylingProps,
5
+ type RecursicaOverStyled,
6
+ } from "../../utils/filterStylingProps";
7
+ import { Label, type RecursicaLabelProps } from "../Label/Label";
8
+ import { AssistiveElement } from "../AssistiveElement/AssistiveElement";
9
+ import styles from "./FormControlWrapper.module.css";
10
+
11
+ export interface RecursicaFormControlWrapperProps extends RecursicaLabelProps {
12
+ /** Securely replaces standard Mantine descriptions safely providing standard Assistive properties. */
13
+ assistiveText?: React.ReactNode;
14
+ /** Explicit toggle to suppress the Info icon rendering natively alongside the assistiveText. Defaults to true. */
15
+ assistiveWithIcon?: boolean;
16
+ /** Custom action area to render alongside the label instead of the default edit icon. */
17
+ labelActionArea?: React.ReactNode;
18
+ /** Pass the native maximum width design variable dynamically bounding the specific wrapper width exclusively. */
19
+ controlMaxWidth: string | undefined;
20
+ /** Pass the native minimum width design variable dynamically bounding the specific wrapper width exclusively. */
21
+ controlMinWidth: string | undefined;
22
+ }
23
+
24
+ export type FormControlWrapperProps = RecursicaOverStyled<
25
+ Omit<InputWrapperProps, "labelProps" | "inputWrapperOrder"> &
26
+ RecursicaFormControlWrapperProps
27
+ >;
28
+
29
+ export const FormControlWrapper = forwardRef<
30
+ HTMLDivElement,
31
+ FormControlWrapperProps
32
+ >(function FormControlWrapper(
33
+ {
34
+ formLayout,
35
+ labelSize,
36
+ labelAlignment,
37
+ labelOptionalText,
38
+ labelWithEditIcon,
39
+ labelActionArea,
40
+ onLabelEditClick,
41
+
42
+ label,
43
+ description,
44
+ assistiveText,
45
+ assistiveWithIcon = true,
46
+ controlMaxWidth,
47
+ controlMinWidth,
48
+ error,
49
+ required,
50
+ withAsterisk,
51
+ id: userProvidedId,
52
+ children,
53
+ className,
54
+ overStyled = false,
55
+ labelElement, // Extracted safely preventing bleeding into HTML domains
56
+ ...rest
57
+ },
58
+ ref,
59
+ ) {
60
+ // Generate a reliable ID to map the Label's HTML context down to the component array natively.
61
+ const generatedId = useId();
62
+ const id = userProvidedId || `recursica-fc-${generatedId}`;
63
+
64
+ // Evaluate explicit assistive fallbacks ensuring assistiveText correctly prioritizes natively over underlying Mantine configurations!
65
+ const resolvedAssistive = assistiveText || description;
66
+ const assistiveId = resolvedAssistive ? `${id}-assistive` : undefined;
67
+ const errorId = error ? `${id}-error` : undefined;
68
+
69
+ const sanitizedProps = filterStylingProps(rest, overStyled);
70
+ const restRecord = sanitizedProps as Record<string, unknown>;
71
+
72
+ const classNameProp =
73
+ className || (restRecord.className as string | undefined);
74
+ const rootClass = styles.root;
75
+ const finalClass = classNameProp
76
+ ? `${rootClass} ${classNameProp}`
77
+ : rootClass;
78
+
79
+ // Clone ARIA identifiers directly back down into the nested children wrapper so screen-readers natively hook the external strings
80
+ const content = React.isValidElement(children)
81
+ ? React.cloneElement(
82
+ children as React.ReactElement<Record<string, unknown>>,
83
+ {
84
+ ...(resolvedAssistive &&
85
+ !(children.props as Record<string, unknown>)["aria-describedby"]
86
+ ? { "aria-describedby": assistiveId }
87
+ : {}),
88
+ ...(error &&
89
+ !(children.props as Record<string, unknown>)["aria-errormessage"]
90
+ ? { "aria-errormessage": errorId }
91
+ : {}),
92
+ },
93
+ )
94
+ : children;
95
+
96
+ return (
97
+ <Box
98
+ ref={ref}
99
+ className={finalClass}
100
+ data-form-layout={formLayout || "stacked"}
101
+ data-form-alignment={labelAlignment || "left"}
102
+ style={
103
+ {
104
+ ...((restRecord.style as React.CSSProperties) || {}),
105
+ ...(controlMaxWidth
106
+ ? { "--form-control-max-width": controlMaxWidth }
107
+ : {}),
108
+ ...(controlMinWidth
109
+ ? { "--form-control-min-width": controlMinWidth }
110
+ : {}),
111
+ } as React.CSSProperties
112
+ }
113
+ {...restRecord}
114
+ >
115
+ {/*
116
+ Section 1: The Native Custom Label
117
+ Bypasses Mantine's built-in Input.Wrapper Label entirely in favor of explicit Recursica formatting.
118
+ */}
119
+ {label && (
120
+ <div className={styles.labelSection}>
121
+ <Label
122
+ id={id} // Directly binds ARIA references down
123
+ formLayout={formLayout}
124
+ labelSize={labelSize}
125
+ labelAlignment={labelAlignment}
126
+ labelOptionalText={labelOptionalText}
127
+ labelWithEditIcon={labelWithEditIcon}
128
+ labelActionArea={labelActionArea}
129
+ onLabelEditClick={onLabelEditClick}
130
+ required={withAsterisk ?? required}
131
+ // Manually propagate relevant structural logic if underlying groups dictate Label to act as generic wrapper
132
+ {...(labelElement === "div" ? { as: "div" } : {})}
133
+ >
134
+ {label}
135
+ </Label>
136
+ </div>
137
+ )}
138
+
139
+ {/*
140
+ Section 2: The Core Input Wrapper & Controls
141
+ Nakedly inject the actual field elements alongside natively bridged Assistive components mapped dynamically.
142
+ */}
143
+ <div className={styles.inputSection}>
144
+ {content}
145
+
146
+ {error && (
147
+ <AssistiveElement
148
+ id={errorId}
149
+ assistiveVariant="error"
150
+ assistiveWithIcon={assistiveWithIcon}
151
+ >
152
+ {error}
153
+ </AssistiveElement>
154
+ )}
155
+
156
+ {!error && resolvedAssistive && (
157
+ <AssistiveElement
158
+ id={assistiveId}
159
+ assistiveVariant="help"
160
+ assistiveWithIcon={assistiveWithIcon}
161
+ >
162
+ {resolvedAssistive}
163
+ </AssistiveElement>
164
+ )}
165
+ </div>
166
+ </Box>
167
+ );
168
+ });
169
+
170
+ FormControlWrapper.displayName = "FormControlWrapper";
@@ -0,0 +1,4 @@
1
+ # Group Implementation Notes
2
+
3
+ The `Group` component is a generic flex layout wrapper mapped directly to Mantine's `Group`.
4
+ It currently does not require any custom logical layouts or CSS workarounds since it serves only to organize layout structure, and doesn't enforce any strict design-system token styling itself. All gap, align, wrap, and justify properties pass safely through via the `filterStylingProps` layout-property allowance.
@@ -0,0 +1,7 @@
1
+ /* HARDCODED VALUES:
2
+ * None. This is a generic flex layout wrapper.
3
+ */
4
+
5
+ .root {
6
+ /* Mantine handles flex, gap, align, justify. No intrinsic design-system styles required for pure layout wrappers. */
7
+ }
@@ -0,0 +1,117 @@
1
+ import React from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Group } from "./Group";
4
+ import { Button } from "../Button";
5
+ import { Text } from "../Text/Text";
6
+
7
+ type GroupStoryProps = React.ComponentProps<typeof Group>;
8
+
9
+ const meta: Meta<GroupStoryProps> = {
10
+ title: "UI-Kit/Group",
11
+ component: Group,
12
+ tags: ["autodocs"],
13
+ parameters: {
14
+ docs: {
15
+ description: {
16
+ component:
17
+ "Group is a flex horizontal layout container that maps directly to Mantine's Group component allowing safe layout property passing.",
18
+ },
19
+ },
20
+ },
21
+ args: {
22
+ gap: "rec-default",
23
+ align: "center",
24
+ justify: "flex-start",
25
+ wrap: "wrap",
26
+ },
27
+ argTypes: {
28
+ gap: {
29
+ control: "select",
30
+ options: [
31
+ "rec-none",
32
+ "rec-sm",
33
+ "rec-default",
34
+ "rec-md",
35
+ "rec-lg",
36
+ "rec-xl",
37
+ "rec-2xl",
38
+ "xs",
39
+ "sm",
40
+ "md",
41
+ "lg",
42
+ "xl",
43
+ ],
44
+ description: "Gap between elements",
45
+ },
46
+ align: {
47
+ control: "select",
48
+ options: ["flex-start", "center", "flex-end", "stretch"],
49
+ description: "Align-items property",
50
+ },
51
+ justify: {
52
+ control: "select",
53
+ options: [
54
+ "flex-start",
55
+ "center",
56
+ "flex-end",
57
+ "space-between",
58
+ "space-around",
59
+ ],
60
+ description: "Justify-content property",
61
+ },
62
+ wrap: {
63
+ control: "select",
64
+ options: ["wrap", "nowrap", "wrap-reverse"],
65
+ description: "Flex-wrap property",
66
+ },
67
+ defaultChecked: {
68
+ table: { disable: true },
69
+ },
70
+ rowGap: {
71
+ table: { disable: true },
72
+ },
73
+ columnGap: {
74
+ table: { disable: true },
75
+ },
76
+ },
77
+ };
78
+
79
+ export default meta;
80
+
81
+ type Story = StoryObj<GroupStoryProps>;
82
+
83
+ export const Default: Story = {
84
+ render: (args) => (
85
+ <Group {...args}>
86
+ <Button variant="solid">Primary</Button>
87
+ <Button variant="outline">Secondary</Button>
88
+ <Text>Text element within Group</Text>
89
+ </Group>
90
+ ),
91
+ };
92
+
93
+ export const StaticGapSmall: Story = {
94
+ args: {
95
+ gap: "rec-sm",
96
+ },
97
+ render: (args) => (
98
+ <Group {...args}>
99
+ <Button variant="solid">Item 1</Button>
100
+ <Button variant="solid">Item 2</Button>
101
+ <Button variant="solid">Item 3</Button>
102
+ </Group>
103
+ ),
104
+ };
105
+
106
+ export const StaticGapLarge: Story = {
107
+ args: {
108
+ gap: "rec-xl",
109
+ },
110
+ render: (args) => (
111
+ <Group {...args}>
112
+ <Button variant="solid">Item 1</Button>
113
+ <Button variant="solid">Item 2</Button>
114
+ <Button variant="solid">Item 3</Button>
115
+ </Group>
116
+ ),
117
+ };