@recursica/mantine-adapter 0.10.0 → 0.12.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 (79) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +6 -28
  3. package/dist/mantine-adapter.cjs +1 -1
  4. package/dist/mantine-adapter.cjs.map +1 -1
  5. package/dist/mantine-adapter.css +1 -1
  6. package/dist/mantine-adapter.js +1650 -1344
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/dist/src/components/Checkbox/Checkbox.d.ts +3 -1
  9. package/dist/src/components/FormControlLayout/FormControlLayout.d.ts +21 -0
  10. package/dist/src/components/FormControlWrapper/FormControlWrapper.d.ts +2 -0
  11. package/dist/src/components/Label/Label.d.ts +1 -3
  12. package/dist/src/components/Menu/Menu.d.ts +85 -3
  13. package/dist/src/components/Radio/Radio.d.ts +3 -1
  14. package/dist/src/components/ReadOnlyField/ReadOnlyBooleanField.d.ts +5 -0
  15. package/dist/src/components/ReadOnlyField/ReadOnlyField.d.ts +6 -6
  16. package/dist/src/components/ReadOnlyField/ReadOnlySwitchField.d.ts +5 -0
  17. package/dist/src/components/ReadOnlyField/WithReadOnlyWrapper.d.ts +7 -1
  18. package/dist/src/components/ReadOnlyField/index.d.ts +2 -0
  19. package/dist/src/components/Switch/Switch.d.ts +3 -1
  20. package/dist/src/components/index.d.ts +1 -0
  21. package/dist/src/utils/RequireAccessibleLabel.d.ts +18 -0
  22. package/package.json +1 -1
  23. package/src/components/AssistiveElement/AssistiveElement.stories.tsx +6 -4
  24. package/src/components/Avatar/Avatar.stories.tsx +10 -5
  25. package/src/components/Badge/Badge.stories.tsx +2 -1
  26. package/src/components/Button/Button.stories.tsx +2 -4
  27. package/src/components/Checkbox/Checkbox.module.css +1 -1
  28. package/src/components/Checkbox/Checkbox.stories.tsx +24 -3
  29. package/src/components/Checkbox/Checkbox.tsx +72 -15
  30. package/src/components/Checkbox/CheckboxGroup.stories.tsx +26 -25
  31. package/src/components/Checkbox/CheckboxGroup.tsx +38 -58
  32. package/src/components/Container/Container.stories.tsx +6 -3
  33. package/src/components/DatePicker/DatePicker.stories.tsx +1 -5
  34. package/src/components/Dropdown/Dropdown.stories.tsx +0 -3
  35. package/src/components/Dropdown/Dropdown.tsx +6 -5
  36. package/src/components/FileInput/FileInput.stories.tsx +1 -5
  37. package/src/components/FileUpload/FileUpload.stories.tsx +1 -5
  38. package/src/components/Flex/Flex.stories.tsx +6 -3
  39. package/src/components/FormControlLayout/FormControlLayout.module.css +80 -0
  40. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +82 -0
  41. package/src/components/FormControlLayout/FormControlLayout.tsx +78 -0
  42. package/src/components/FormControlWrapper/FormControlWrapper.module.css +3 -41
  43. package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +18 -5
  44. package/src/components/FormControlWrapper/FormControlWrapper.tsx +28 -41
  45. package/src/components/Group/Group.stories.tsx +6 -3
  46. package/src/components/Label/Label.module.css +0 -37
  47. package/src/components/Label/Label.stories.tsx +8 -9
  48. package/src/components/Label/Label.tsx +2 -7
  49. package/src/components/Loader/Loader.stories.tsx +10 -5
  50. package/src/components/Menu/MENU_IMPLEMENTATION_NOTES.md +78 -0
  51. package/src/components/Menu/Menu.module.css +244 -0
  52. package/src/components/Menu/Menu.stories.tsx +309 -5
  53. package/src/components/Menu/Menu.tsx +317 -4
  54. package/src/components/NumberInput/NumberInput.stories.tsx +1 -5
  55. package/src/components/Radio/Radio.stories.tsx +22 -8
  56. package/src/components/Radio/Radio.tsx +134 -75
  57. package/src/components/Radio/RadioGroup.stories.tsx +26 -7
  58. package/src/components/Radio/RadioGroup.tsx +37 -57
  59. package/src/components/ReadOnlyField/ReadOnlyBooleanField.tsx +21 -0
  60. package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +43 -0
  61. package/src/components/ReadOnlyField/ReadOnlyField.tsx +30 -3
  62. package/src/components/ReadOnlyField/ReadOnlySwitchField.tsx +21 -0
  63. package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +3 -1
  64. package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +10 -2
  65. package/src/components/ReadOnlyField/index.ts +2 -0
  66. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +1 -5
  67. package/src/components/Slider/Slider.stories.tsx +1 -5
  68. package/src/components/Stack/Stack.stories.tsx +6 -3
  69. package/src/components/Switch/Switch.module.css +2 -10
  70. package/src/components/Switch/Switch.stories.tsx +42 -27
  71. package/src/components/Switch/Switch.tsx +138 -89
  72. package/src/components/Switch/SwitchGroup.stories.tsx +28 -8
  73. package/src/components/Switch/SwitchGroup.tsx +37 -57
  74. package/src/components/TextArea/TextArea.stories.tsx +0 -3
  75. package/src/components/TextArea/TextArea.tsx +6 -5
  76. package/src/components/TextField/TextField.tsx +6 -5
  77. package/src/components/TimePicker/TimePicker.stories.tsx +1 -5
  78. package/src/components/index.ts +1 -0
  79. package/src/utils/RequireAccessibleLabel.ts +12 -0
@@ -46,7 +46,24 @@ export default meta;
46
46
 
47
47
  type Story = StoryObj<typeof CheckboxGroup>;
48
48
 
49
- export const GroupSideBySide: Story = {
49
+ export const Default: Story = {
50
+ args: {
51
+ formLayout: "stacked",
52
+ label: "Standard Group",
53
+ },
54
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
55
+ render: function StoryRender({ withLayer, layer, ...args }: any) {
56
+ const [value, setValue] = useState<string[]>([]);
57
+ return (
58
+ <Checkbox.Group {...args} value={value} onChange={setValue}>
59
+ <Checkbox value="1" label="Option 1" />
60
+ <Checkbox value="2" label="Option 2" />
61
+ </Checkbox.Group>
62
+ );
63
+ },
64
+ };
65
+
66
+ export const SideBySideLayout: Story = {
50
67
  args: {
51
68
  formLayout: "side-by-side",
52
69
  labelOptionalText: "Recommended",
@@ -55,7 +72,8 @@ export const GroupSideBySide: Story = {
55
72
  assistiveText:
56
73
  "Select all libraries currently in use for this specific workspace configuration.",
57
74
  },
58
- render: function GroupSideBySideRender(args) {
75
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
76
+ render: function StoryRender({ withLayer, layer, ...args }: any) {
59
77
  const [value, setValue] = useState<string[]>(["react"]);
60
78
  return (
61
79
  <Checkbox.Group {...args} value={value} onChange={setValue}>
@@ -70,14 +88,15 @@ export const GroupSideBySide: Story = {
70
88
  },
71
89
  };
72
90
 
73
- export const GroupStacked: Story = {
91
+ export const StackedLayout: Story = {
74
92
  args: {
75
93
  formLayout: "stacked",
76
94
  required: true,
77
95
  label: "Execution Targets",
78
96
  error: "You must select at least one deployment target to compile.",
79
97
  },
80
- render: function GroupStackedRender(args) {
98
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
99
+ render: function StoryRender({ withLayer, layer, ...args }: any) {
81
100
  const [value, setValue] = useState<string[]>(["react"]);
82
101
  return (
83
102
  <Checkbox.Group {...args} value={value} onChange={setValue}>
@@ -92,7 +111,7 @@ export const GroupStacked: Story = {
92
111
  },
93
112
  };
94
113
 
95
- export const StaticReadOnlyGroup: Story = {
114
+ export const ReadOnly: Story = {
96
115
  args: {
97
116
  readOnly: true,
98
117
  formLayout: "stacked",
@@ -101,7 +120,8 @@ export const StaticReadOnlyGroup: Story = {
101
120
  assistiveText:
102
121
  "This structure explicitly validates native component-level DOM preservation natively mapping lock bounds safely over interaction.",
103
122
  },
104
- render: function StaticReadOnlyGroupRender(args) {
123
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
124
+ render: function StoryRender({ withLayer, layer, ...args }: any) {
105
125
  const [value, setValue] = useState<string[]>(["disabledNode"]);
106
126
  return (
107
127
  <Checkbox.Group {...args} value={value} onChange={setValue}>
@@ -117,22 +137,3 @@ export const StaticReadOnlyGroup: Story = {
117
137
  );
118
138
  },
119
139
  };
120
-
121
- export const EditableReadOnlyGroup: Story = {
122
- args: {
123
- readOnly: true,
124
- formLayout: "side-by-side",
125
- labelWithEditIcon: true,
126
- label: "Interactively Editable ReadOnly Group",
127
- assistiveText: "Users explicitly unlock DOM structures dynamically.",
128
- },
129
- render: function EditableReadOnlyGroupRender(args) {
130
- const [value, setValue] = useState<string[]>(["activeState"]);
131
- return (
132
- <Checkbox.Group {...args} value={value} onChange={setValue}>
133
- <Checkbox value="activeState" label="Locked active state" />
134
- <Checkbox value="unlockedNode" label="Locked inactive map" />
135
- </Checkbox.Group>
136
- );
137
- },
138
- };
@@ -9,10 +9,8 @@ import {
9
9
  filterStylingProps,
10
10
  type RecursicaOverStyled,
11
11
  } from "../../utils/filterStylingProps";
12
- import {
13
- FormControlWrapper,
14
- type RecursicaFormControlWrapperProps,
15
- } from "../FormControlWrapper/FormControlWrapper";
12
+ import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
13
+ import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
16
14
  import styles from "./Checkbox.module.css";
17
15
 
18
16
  export interface RecursicaCheckboxGroupProps
@@ -27,8 +25,8 @@ export type CheckboxGroupProps =
27
25
  RecursicaOverStyled<RecursicaCheckboxGroupProps>;
28
26
 
29
27
  export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
30
- function CheckboxGroup(
31
- {
28
+ function CheckboxGroup(props, ref) {
29
+ const {
32
30
  overStyled = false,
33
31
  formLayout = "stacked",
34
32
 
@@ -53,47 +51,19 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
53
51
  children,
54
52
  readOnly,
55
53
  readOnlyComponent,
54
+ emptyValueComponent,
55
+ value,
56
+ defaultValue,
56
57
  ...rest
57
- },
58
- ref,
59
- ) {
58
+ } = props;
60
59
  const sanitizedProps = filterStylingProps(rest, overStyled);
61
60
  const restRecord = sanitizedProps as Record<string, unknown>;
62
61
 
63
62
  // Delete prohibited sizing hooks from bypassing the variables
64
63
  delete restRecord["size"];
65
64
 
66
- if (readOnly && readOnlyComponent) {
67
- return (
68
- <FormControlWrapper
69
- className={className}
70
- style={style as React.CSSProperties}
71
- controlMaxWidth="var(--recursica_ui-kit_components_checkbox-item_properties_max-width)"
72
- controlMinWidth={undefined}
73
- overStyled={overStyled as true}
74
- labelElement="div"
75
- formLayout={formLayout}
76
- labelSize={labelSize}
77
- labelAlignment={labelAlignment}
78
- labelOptionalText={labelOptionalText}
79
- labelWithEditIcon={labelWithEditIcon}
80
- onLabelEditClick={onLabelEditClick}
81
- label={label}
82
- description={description}
83
- assistiveText={assistiveText}
84
- assistiveWithIcon={assistiveWithIcon}
85
- error={error}
86
- required={required}
87
- withAsterisk={withAsterisk}
88
- id={id}
89
- >
90
- {readOnlyComponent}
91
- </FormControlWrapper>
92
- );
93
- }
94
-
95
65
  return (
96
- <FormControlWrapper
66
+ <WithReadOnlyWrapper
97
67
  className={className}
98
68
  style={style as React.CSSProperties}
99
69
  controlMaxWidth="var(--recursica_ui-kit_components_checkbox-item_properties_max-width)"
@@ -114,25 +84,35 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
114
84
  required={required}
115
85
  withAsterisk={withAsterisk}
116
86
  id={id}
117
- >
118
- <MantineCheckbox.Group
119
- ref={ref}
120
- /* Natively bind local disabled lock dynamically */
121
- {...(sanitizedProps as unknown as MantineCheckboxGroupProps)}
122
- >
123
- <div className={styles.groupRoot} data-layout={formLayout}>
124
- {/* If the exact group is read-only natively, we inject disabled hooks dynamically over children? Mantine handles native context directly! */}
125
- {React.Children.map(children, (child) => {
126
- if (React.isValidElement(child)) {
127
- return React.cloneElement(child as React.ReactElement<any>, {
128
- disabled: readOnly || (child.props as any).disabled,
129
- });
130
- }
131
- return child;
132
- })}
133
- </div>
134
- </MantineCheckbox.Group>
135
- </FormControlWrapper>
87
+ readOnly={readOnly && !!readOnlyComponent}
88
+ readOnlyComponent={readOnlyComponent}
89
+ emptyValueComponent={emptyValueComponent}
90
+ readOnlyType="text"
91
+ readOnlyValue={value !== undefined ? value : defaultValue}
92
+ readOnlyNativeProps={props}
93
+ activeComponent={
94
+ <MantineCheckbox.Group
95
+ ref={ref}
96
+ /* Natively bind local disabled lock dynamically */
97
+ {...(sanitizedProps as unknown as MantineCheckboxGroupProps)}
98
+ disabled={readOnly || (restRecord as any).disabled}
99
+ value={value}
100
+ defaultValue={defaultValue}
101
+ >
102
+ <div className={styles.groupRoot} data-layout={formLayout}>
103
+ {/* If the exact group is read-only natively, we inject disabled hooks dynamically over children? Mantine handles native context directly! */}
104
+ {React.Children.map(children, (child) => {
105
+ if (React.isValidElement(child)) {
106
+ return React.cloneElement(child as React.ReactElement<any>, {
107
+ disabled: readOnly || (child.props as any).disabled,
108
+ });
109
+ }
110
+ return child;
111
+ })}
112
+ </div>
113
+ </MantineCheckbox.Group>
114
+ }
115
+ />
136
116
  );
137
117
  },
138
118
  );
@@ -55,7 +55,8 @@ export default meta;
55
55
  type Story = StoryObj<ContainerStoryProps>;
56
56
 
57
57
  export const Default: Story = {
58
- render: (args) => (
58
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
59
+ render: ({ withLayer, layer, ...args }: any) => (
59
60
  <div style={{ backgroundColor: "#f0f0f0", padding: "16px" }}>
60
61
  <Container
61
62
  {...args}
@@ -78,7 +79,8 @@ export const SmallContainer: Story = {
78
79
  args: {
79
80
  size: "sm",
80
81
  },
81
- render: (args) => (
82
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
83
+ render: ({ withLayer, layer, ...args }: any) => (
82
84
  <div style={{ backgroundColor: "#f0f0f0", padding: "16px" }}>
83
85
  <Container
84
86
  {...args}
@@ -98,7 +100,8 @@ export const FluidContainer: Story = {
98
100
  args: {
99
101
  fluid: true,
100
102
  },
101
- render: (args) => (
103
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
104
+ render: ({ withLayer, layer, ...args }: any) => (
102
105
  <div style={{ backgroundColor: "#f0f0f0", padding: "16px" }}>
103
106
  <Container
104
107
  {...args}
@@ -6,11 +6,7 @@ const meta: Meta<typeof DatePicker> = {
6
6
  title: "UI-Kit/🚧 DatePicker",
7
7
  component: DatePicker,
8
8
  tags: ["autodocs"],
9
- argTypes: {
10
- disabled: {
11
- control: "boolean",
12
- },
13
- },
9
+ argTypes: {},
14
10
  };
15
11
 
16
12
  export default meta;
@@ -30,9 +30,6 @@ const meta: Meta<DropdownStoryProps> = {
30
30
  },
31
31
  argTypes: {
32
32
  ...formControlArgTypes,
33
- disabled: {
34
- control: "boolean",
35
- },
36
33
  readOnly: {
37
34
  control: "boolean",
38
35
  },
@@ -36,8 +36,8 @@ export interface RecursicaDropdownProps
36
36
  export type DropdownProps = RecursicaOverStyled<RecursicaDropdownProps>;
37
37
 
38
38
  export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
39
- function Dropdown(
40
- {
39
+ function Dropdown(props, ref) {
40
+ const {
41
41
  overStyled = false,
42
42
  formLayout = "stacked",
43
43
  containerWidth,
@@ -61,13 +61,12 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
61
61
  disabled,
62
62
  readOnly,
63
63
  readOnlyComponent,
64
+ emptyValueComponent,
64
65
  value,
65
66
  defaultValue,
66
67
  data,
67
68
  ...rest
68
- },
69
- ref,
70
- ) {
69
+ } = props;
71
70
  const sanitizedProps = filterStylingProps(rest, overStyled);
72
71
  const restRecord = sanitizedProps as Record<string, unknown>;
73
72
 
@@ -136,6 +135,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
136
135
  id={id}
137
136
  readOnly={readOnly}
138
137
  readOnlyComponent={readOnlyComponent}
138
+ emptyValueComponent={emptyValueComponent}
139
139
  readOnlyType="text"
140
140
  readOnlyValue={
141
141
  value !== undefined
@@ -144,6 +144,7 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
144
144
  ? String(defaultValue)
145
145
  : undefined
146
146
  }
147
+ readOnlyNativeProps={props}
147
148
  activeComponent={
148
149
  /* Naked Select execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
149
150
  <MantineSelect
@@ -6,11 +6,7 @@ const meta: Meta<typeof FileInput> = {
6
6
  title: "UI-Kit/🚧 FileInput",
7
7
  component: FileInput,
8
8
  tags: ["autodocs"],
9
- argTypes: {
10
- disabled: {
11
- control: "boolean",
12
- },
13
- },
9
+ argTypes: {},
14
10
  };
15
11
 
16
12
  export default meta;
@@ -6,11 +6,7 @@ const meta: Meta<typeof FileUpload> = {
6
6
  title: "UI-Kit/🚧 FileUpload",
7
7
  component: FileUpload,
8
8
  tags: ["autodocs"],
9
- argTypes: {
10
- disabled: {
11
- control: "boolean",
12
- },
13
- },
9
+ argTypes: {},
14
10
  };
15
11
 
16
12
  export default meta;
@@ -87,7 +87,8 @@ export default meta;
87
87
  type Story = StoryObj<FlexStoryProps>;
88
88
 
89
89
  export const Default: Story = {
90
- render: (args) => (
90
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
91
+ render: ({ withLayer, layer, ...args }: any) => (
91
92
  <Flex {...args}>
92
93
  <Button variant="solid">Block A</Button>
93
94
  <Button variant="outline">Block B</Button>
@@ -101,7 +102,8 @@ export const StaticGapSmallColumn: Story = {
101
102
  gap: "rec-sm",
102
103
  direction: "column",
103
104
  },
104
- render: (args) => (
105
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
106
+ render: ({ withLayer, layer, ...args }: any) => (
105
107
  <Flex {...args}>
106
108
  <Button variant="solid">Item 1</Button>
107
109
  <Button variant="solid">Item 2</Button>
@@ -115,7 +117,8 @@ export const StaticGapLargeRow: Story = {
115
117
  gap: "rec-xl",
116
118
  direction: "row",
117
119
  },
118
- render: (args) => (
120
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
121
+ render: ({ withLayer, layer, ...args }: any) => (
119
122
  <Flex {...args}>
120
123
  <Button variant="solid">Item 1</Button>
121
124
  <Button variant="solid">Item 2</Button>
@@ -0,0 +1,80 @@
1
+ /* FormControlLayout.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
+ .leftSection {
24
+ /* Width dimensions are strictly governed natively inside this layout column */
25
+ flex-shrink: 0;
26
+ display: flex;
27
+ flex-direction: column;
28
+ }
29
+
30
+ /* Stacked Layout constraints */
31
+ .root[data-form-layout="stacked"] .leftSection {
32
+ padding-bottom: var(
33
+ --recursica_ui-kit_components_label_variants_layouts_stacked_properties_bottom-padding,
34
+ 8px
35
+ );
36
+ min-height: var(
37
+ --recursica_ui-kit_components_label_variants_layouts_stacked_properties_min-height,
38
+ auto
39
+ );
40
+ }
41
+
42
+ /* SideBySide Layout constraints */
43
+ .root[data-form-layout="side-by-side"] .leftSection {
44
+ padding-top: var(
45
+ --recursica_ui-kit_components_label_variants_layouts_side-by-side_properties_vertical-padding,
46
+ 0
47
+ );
48
+ padding-bottom: var(
49
+ --recursica_ui-kit_components_label_variants_layouts_side-by-side_properties_vertical-padding,
50
+ 0
51
+ );
52
+ min-height: var(
53
+ --recursica_ui-kit_components_label_variants_layouts_side-by-side_properties_min-height,
54
+ auto
55
+ );
56
+ /* In side-by-side, the label column exists to the left of its input, so we use the gutter gap here. */
57
+ margin-right: var(
58
+ --recursica_ui-kit_components_label_variants_layouts_side-by-side_properties_gutter,
59
+ 16px
60
+ );
61
+ }
62
+
63
+ .root[data-form-layout="side-by-side"] .leftSection[data-size="default"] {
64
+ width: var(
65
+ --recursica_ui-kit_components_label_variants_layouts_side-by-side_variants_sizes_default_properties_width
66
+ );
67
+ }
68
+
69
+ .root[data-form-layout="side-by-side"] .leftSection[data-size="small"] {
70
+ width: var(
71
+ --recursica_ui-kit_components_label_variants_layouts_side-by-side_variants_sizes_small_properties_width
72
+ );
73
+ }
74
+
75
+ .rightSection {
76
+ display: flex; /* Groups internal field alongside subsequent errors/descriptions */
77
+ flex-direction: column;
78
+ flex: 1; /* Consumes the remaining x-axis in grid columns natively */
79
+ min-width: 0; /* Break boundary preventing internal flex explosion */
80
+ }
@@ -0,0 +1,82 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { FormControlLayout } from "./FormControlLayout";
3
+ import { Switch } from "../Switch/Switch";
4
+
5
+ const meta: Meta<typeof FormControlLayout> = {
6
+ title: "UI-Kit/FormControlLayout",
7
+ component: FormControlLayout,
8
+ parameters: {
9
+ layout: "padded",
10
+ docs: {
11
+ description: {
12
+ component:
13
+ "A layout component used to correctly position form inputs alongside their labels.\n\n**When to use this:**\nTypically, you should use `FormControlWrapper` instead, which uses this component under the hood to handle layout automatically.\n\nHowever, this component is useful when you need to align standalone inputs (like a `Switch` or `Checkbox` without a label) so they perfectly match the spacing and alignment of your other form fields in a `side-by-side` layout.",
14
+ },
15
+ },
16
+ },
17
+ tags: ["autodocs"],
18
+ argTypes: {
19
+ formLayout: {
20
+ control: "radio",
21
+ options: ["stacked", "side-by-side"],
22
+ },
23
+ labelSize: {
24
+ control: "radio",
25
+ options: ["default", "small", "md"],
26
+ description: "Dictates the physical width of the left column.",
27
+ },
28
+ children: {
29
+ table: { disable: true },
30
+ },
31
+ leftSection: {
32
+ table: { disable: true },
33
+ },
34
+ },
35
+ };
36
+
37
+ export default meta;
38
+ type Story = StoryObj<typeof FormControlLayout>;
39
+
40
+ /**
41
+ * The Default layout demonstrates wrapping a standalone primitive without a Label.
42
+ * In a stacked layout, an omitted leftSection naturally results in no structural padding.
43
+ */
44
+ export const Default: Story = {
45
+ args: {
46
+ formLayout: "stacked",
47
+ labelSize: "default",
48
+ leftSection: (
49
+ <div
50
+ style={{ padding: 8, border: "1px dashed #ccc", background: "#fafafa" }}
51
+ >
52
+ Left Section Boundary
53
+ </div>
54
+ ),
55
+ children: <Switch label="Input area content" />,
56
+ },
57
+ };
58
+
59
+ /**
60
+ * Demonstrates the stacked layout without a left section.
61
+ * The input should natively pull flush to the top left since there is no left column.
62
+ */
63
+ export const StackedLayout: Story = {
64
+ args: {
65
+ formLayout: "stacked",
66
+ labelSize: "default",
67
+ children: <Switch label="Flush stacked switch" />,
68
+ },
69
+ };
70
+
71
+ /**
72
+ * Demonstrates how a naked boolean primitive perfectly aligns in a side-by-side layout
73
+ * by utilizing the layout wrapper. The left column maintains its precise design system width
74
+ * even when `leftSection` is undefined, forcing the component into the correct form column!
75
+ */
76
+ export const SideBySideLayout: Story = {
77
+ args: {
78
+ formLayout: "side-by-side",
79
+ labelSize: "default",
80
+ children: <Switch label="Offset switch aligning with grid" />,
81
+ },
82
+ };
@@ -0,0 +1,78 @@
1
+ import React, { forwardRef } from "react";
2
+ import styles from "./FormControlLayout.module.css";
3
+
4
+ export interface FormControlLayoutProps
5
+ extends React.HTMLAttributes<HTMLDivElement> {
6
+ formLayout?: "stacked" | "side-by-side";
7
+ labelSize?: "default" | "small" | "md";
8
+ controlMaxWidth?: string;
9
+ controlMinWidth?: string;
10
+ leftSection?: React.ReactNode;
11
+ children?: React.ReactNode;
12
+ }
13
+
14
+ /**
15
+ * A layout component used to correctly position form inputs alongside their labels.
16
+ *
17
+ * **When to use this:**
18
+ * Typically, you should use `FormControlWrapper` instead, which uses this component
19
+ * under the hood to handle layout automatically.
20
+ *
21
+ * However, this component is useful when you need to align standalone inputs
22
+ * (like a `Switch` or `Checkbox` without a label) so they perfectly match the spacing
23
+ * and alignment of your other form fields in a `side-by-side` layout.
24
+ */
25
+ export const FormControlLayout = forwardRef<
26
+ HTMLDivElement,
27
+ FormControlLayoutProps
28
+ >(function FormControlLayout(
29
+ {
30
+ formLayout = "stacked",
31
+ labelSize = "default",
32
+ controlMaxWidth,
33
+ controlMinWidth,
34
+ leftSection,
35
+ children,
36
+ className,
37
+ style,
38
+ ...rest
39
+ },
40
+ ref,
41
+ ) {
42
+ const rootClass = className ? `${styles.root} ${className}` : styles.root;
43
+
44
+ // We explicitly render leftSection even if null in side-by-side to preserve the empty grid column bounding constraints.
45
+ // In stacked layouts, if there is no label, we omit the wrapper to avoid unnecessary padding.
46
+ const hasLeftContent = leftSection !== undefined && leftSection !== null;
47
+ const shouldRenderLeft = hasLeftContent || formLayout === "side-by-side";
48
+
49
+ return (
50
+ <div
51
+ ref={ref}
52
+ className={rootClass}
53
+ data-form-layout={formLayout}
54
+ style={
55
+ {
56
+ ...style,
57
+ ...(controlMaxWidth
58
+ ? { "--form-control-max-width": controlMaxWidth }
59
+ : {}),
60
+ ...(controlMinWidth
61
+ ? { "--form-control-min-width": controlMinWidth }
62
+ : {}),
63
+ } as React.CSSProperties
64
+ }
65
+ {...rest}
66
+ >
67
+ {shouldRenderLeft && (
68
+ <div className={styles.leftSection} data-size={labelSize}>
69
+ {leftSection}
70
+ </div>
71
+ )}
72
+
73
+ <div className={styles.rightSection}>{children}</div>
74
+ </div>
75
+ );
76
+ });
77
+
78
+ FormControlLayout.displayName = "FormControlLayout";
@@ -1,46 +1,8 @@
1
1
  /* FormControlWrapper.module.css */
2
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
3
  .inputSection {
42
- display: flex; /* Groups internal field alongside subsequent errors/descriptions */
4
+ display: flex;
43
5
  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 */
6
+ flex: 1;
7
+ min-width: 0;
46
8
  }