feat(components): add InlineWrapper and related styles for improved form handling

This commit is contained in:
2026-01-17 17:29:08 +02:00
parent bc7262cede
commit 31f2a0428f
4 changed files with 264 additions and 0 deletions

View File

@@ -0,0 +1,176 @@
import type { ReactNode } from 'react'
import {
Box,
Center,
Flex,
type FlexProps,
Paper,
Stack,
Title,
type TitleProps,
Tooltip,
useMantineColorScheme,
} from '@mantine/core'
import React from 'react'
import classes from './InlineWapper.module.css'
interface InlineWrapperCallbackProps extends Partial<InlineWrapperPropsOnly> {
classNames: React.CSSProperties
dataCssProps?: Record<string, any>
size: string
}
interface InlineWrapperProps extends InlineWrapperPropsOnly{
children?: ((props: InlineWrapperCallbackProps) => ReactNode) | ReactNode
}
interface InlineWrapperPropsOnly {
error?: ReactNode | string
flexProps?: FlexProps
label: ReactNode | string
labelProps?: TitleProps
promptArea?: ((props: InlineWrapperCallbackProps) => ReactNode) | ReactNode
promptWidth?: FlexProps['w']
required?: boolean
rightSection?: ((props: InlineWrapperCallbackProps) => ReactNode) | ReactNode
styles?: React.CSSProperties
tooltip?: string
value?: any
}
function InlineWrapper(props: InlineWrapperProps) {
return (
<Stack gap={0}>
<Flex
gap={0}
h={undefined}
m={0}
mb={0}
p={0}
w={undefined}
wrap='nowrap'
{...props.flexProps}
bg={'var(--input-background)'}
>
{props.promptWidth && props.promptWidth !== 0 ? <Prompt {...props} /> : null}
<div
style={{
borderRadius: 0,
flex: 10,
}}
>
{typeof props.children === 'function' ? (
props.children({ ...props, classNames: classes, size: 'xs' })
) : typeof props.children === 'object' && React.isValidElement(props.children) ? (
<props.children.type classNames={classes} size='xs' {...(typeof props.children.props === "object" ? props.children.props : {})} />
) : (
props.children
)}
</div>
{!props.rightSection ? undefined : typeof props.rightSection === 'function' ? (
props.rightSection({
...props,
classNames: classes,
size: 'xs',
})
) : typeof props.rightSection === 'object' && React.isValidElement(props.rightSection) ? (
<props.rightSection.type classNames={classes} size='xs' {...(typeof props.rightSection.props === "object" ? props.rightSection.props : {})} />
) : (
props.rightSection
)}
</Flex>
{/* <ErrorComponent {...props} /> */}
</Stack>
)
}
function isValueEmpty(inputValue: any) {
if (inputValue === null || inputValue === undefined) return true
if (typeof inputValue === 'number') {
if (inputValue === 0) return false
} else if (typeof inputValue === 'string' || inputValue === '') {
return inputValue.trim() === ''
} else if (inputValue instanceof File) {
return inputValue.size === 0
} else if (inputValue.target) {
return isValueEmpty(inputValue.target?.value)
} else if (inputValue.constructor?.name === 'Date') {
return false
}
}
function Prompt(props: Partial<InlineWrapperProps>) {
return (
<>
{props.tooltip ? (
<Tooltip label={props.tooltip}>
<PromptDetail {...props} />
</Tooltip>
) : (
<PromptDetail {...props} />
)}
</>
)
}
function PromptDetail(props: Partial<InlineWrapperProps>) {
const colors = useColors(props)
return props.promptArea ? (
<Box maw={props.promptWidth} w={'100%'}>
{!props.promptArea ? undefined : typeof props.promptArea === 'function' ? (
props.promptArea({
...props,
classNames: classes,
dataCssProps: { 'data-promptArea': true },
size: 'xs',
})
) : typeof props.rightSection === 'object' && React.isValidElement(props.promptArea) ? (
<props.promptArea.type
classNames={classes}
data-promptArea='true'
size='xs'
{...(typeof props.promptArea?.props === "object" ? props.promptArea.props : {})}
/>
) : (
props.promptArea
)}
</Box>
) : (
<Paper
bg={colors.paperColor}
className={classes.prompt}
px='md'
w={props.promptWidth}
withBorder
>
<Center h='100%' style={{ justifyContent: 'start' }} w='100%'>
<Title c={colors.titleColor} fz='xs' order={6} {...props.labelProps}>
{props.label}
{props.required && isValueEmpty(props.value) && <span style={{ color: 'red' }}>*</span>}
</Title>
</Center>
</Paper>
)
}
function useColors(props: Partial<InlineWrapperProps>) {
const { colorScheme } = useMantineColorScheme()
let titleColor = colorScheme === 'dark' ? 'dark.0' : 'gray.8'
let paperColor = colorScheme === 'dark' ? 'dark.7' : 'gray.1'
if (props.required && isValueEmpty(props.value)) {
paperColor = colorScheme === 'dark' ? '#413012e7' : 'yellow.1'
}
if (props.error) {
paperColor = colorScheme === 'dark' ? 'red.7' : 'red.0'
titleColor = colorScheme === 'dark' ? 'red.0' : 'red.9'
}
return { paperColor, titleColor }
}
export { InlineWrapper }
export type { InlineWrapperProps }