@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.
- package/CHANGELOG.md +16 -0
- package/README.md +6 -28
- package/dist/mantine-adapter.cjs +1 -1
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1650 -1344
- package/dist/mantine-adapter.js.map +1 -1
- package/dist/src/components/Checkbox/Checkbox.d.ts +3 -1
- package/dist/src/components/FormControlLayout/FormControlLayout.d.ts +21 -0
- package/dist/src/components/FormControlWrapper/FormControlWrapper.d.ts +2 -0
- package/dist/src/components/Label/Label.d.ts +1 -3
- package/dist/src/components/Menu/Menu.d.ts +85 -3
- package/dist/src/components/Radio/Radio.d.ts +3 -1
- package/dist/src/components/ReadOnlyField/ReadOnlyBooleanField.d.ts +5 -0
- package/dist/src/components/ReadOnlyField/ReadOnlyField.d.ts +6 -6
- package/dist/src/components/ReadOnlyField/ReadOnlySwitchField.d.ts +5 -0
- package/dist/src/components/ReadOnlyField/WithReadOnlyWrapper.d.ts +7 -1
- package/dist/src/components/ReadOnlyField/index.d.ts +2 -0
- package/dist/src/components/Switch/Switch.d.ts +3 -1
- package/dist/src/components/index.d.ts +1 -0
- package/dist/src/utils/RequireAccessibleLabel.d.ts +18 -0
- package/package.json +1 -1
- package/src/components/AssistiveElement/AssistiveElement.stories.tsx +6 -4
- package/src/components/Avatar/Avatar.stories.tsx +10 -5
- package/src/components/Badge/Badge.stories.tsx +2 -1
- package/src/components/Button/Button.stories.tsx +2 -4
- package/src/components/Checkbox/Checkbox.module.css +1 -1
- package/src/components/Checkbox/Checkbox.stories.tsx +24 -3
- package/src/components/Checkbox/Checkbox.tsx +72 -15
- package/src/components/Checkbox/CheckboxGroup.stories.tsx +26 -25
- package/src/components/Checkbox/CheckboxGroup.tsx +38 -58
- package/src/components/Container/Container.stories.tsx +6 -3
- package/src/components/DatePicker/DatePicker.stories.tsx +1 -5
- package/src/components/Dropdown/Dropdown.stories.tsx +0 -3
- package/src/components/Dropdown/Dropdown.tsx +6 -5
- package/src/components/FileInput/FileInput.stories.tsx +1 -5
- package/src/components/FileUpload/FileUpload.stories.tsx +1 -5
- package/src/components/Flex/Flex.stories.tsx +6 -3
- package/src/components/FormControlLayout/FormControlLayout.module.css +80 -0
- package/src/components/FormControlLayout/FormControlLayout.stories.tsx +82 -0
- package/src/components/FormControlLayout/FormControlLayout.tsx +78 -0
- package/src/components/FormControlWrapper/FormControlWrapper.module.css +3 -41
- package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +18 -5
- package/src/components/FormControlWrapper/FormControlWrapper.tsx +28 -41
- package/src/components/Group/Group.stories.tsx +6 -3
- package/src/components/Label/Label.module.css +0 -37
- package/src/components/Label/Label.stories.tsx +8 -9
- package/src/components/Label/Label.tsx +2 -7
- package/src/components/Loader/Loader.stories.tsx +10 -5
- package/src/components/Menu/MENU_IMPLEMENTATION_NOTES.md +78 -0
- package/src/components/Menu/Menu.module.css +244 -0
- package/src/components/Menu/Menu.stories.tsx +309 -5
- package/src/components/Menu/Menu.tsx +317 -4
- package/src/components/NumberInput/NumberInput.stories.tsx +1 -5
- package/src/components/Radio/Radio.stories.tsx +22 -8
- package/src/components/Radio/Radio.tsx +134 -75
- package/src/components/Radio/RadioGroup.stories.tsx +26 -7
- package/src/components/Radio/RadioGroup.tsx +37 -57
- package/src/components/ReadOnlyField/ReadOnlyBooleanField.tsx +21 -0
- package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +43 -0
- package/src/components/ReadOnlyField/ReadOnlyField.tsx +30 -3
- package/src/components/ReadOnlyField/ReadOnlySwitchField.tsx +21 -0
- package/src/components/ReadOnlyField/ReadOnlyTextField.tsx +3 -1
- package/src/components/ReadOnlyField/WithReadOnlyWrapper.tsx +10 -2
- package/src/components/ReadOnlyField/index.ts +2 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +1 -5
- package/src/components/Slider/Slider.stories.tsx +1 -5
- package/src/components/Stack/Stack.stories.tsx +6 -3
- package/src/components/Switch/Switch.module.css +2 -10
- package/src/components/Switch/Switch.stories.tsx +42 -27
- package/src/components/Switch/Switch.tsx +138 -89
- package/src/components/Switch/SwitchGroup.stories.tsx +28 -8
- package/src/components/Switch/SwitchGroup.tsx +37 -57
- package/src/components/TextArea/TextArea.stories.tsx +0 -3
- package/src/components/TextArea/TextArea.tsx +6 -5
- package/src/components/TextField/TextField.tsx +6 -5
- package/src/components/TimePicker/TimePicker.stories.tsx +1 -5
- package/src/components/index.ts +1 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
@@ -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
|
|
@@ -87,7 +87,8 @@ export default meta;
|
|
|
87
87
|
type Story = StoryObj<FlexStoryProps>;
|
|
88
88
|
|
|
89
89
|
export const Default: Story = {
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
4
|
+
display: flex;
|
|
43
5
|
flex-direction: column;
|
|
44
|
-
flex: 1;
|
|
45
|
-
min-width: 0;
|
|
6
|
+
flex: 1;
|
|
7
|
+
min-width: 0;
|
|
46
8
|
}
|