Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/input-status-variant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] The bordered input family now accepts a `statusVariant` prop (`'attached' | 'detached'`, default `'attached'`) that forwards to the underlying `Field`, letting you float the status message below the input with spacing instead of overlapping it. Added to TextInput, TextArea, NumberInput, DateInput, DateRangeInput, TimeInput, Selector, MultiSelector, Typeahead, Tokenizer, FileInput, and PowerSearch. Non-breaking: the default matches today's behavior. (#4187)

@cixzhang
28 changes: 28 additions & 0 deletions apps/storybook/stories/DateInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,31 @@ export const ClearableWithStatus: Story = {
status: {type: 'error', message: 'Date is in the past'},
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<ISODateString | undefined>(
'2026-01-25' as ISODateString,
);
const [b, setB] = useState<ISODateString | undefined>(
'2026-01-25' as ISODateString,
);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 280}}>
<DateInput
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'This date is not available'}}
/>
<DateInput
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'This date is not available'}}
statusVariant="detached"
/>
</div>
);
},
};
24 changes: 24 additions & 0 deletions apps/storybook/stories/DateRangeInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,27 @@ export const AllVariations: Story = {
);
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<DateRange | null>(null);
const [b, setB] = useState<DateRange | null>(null);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 320}}>
<DateRangeInput
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'Please select a date range'}}
/>
<DateRangeInput
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'Please select a date range'}}
statusVariant="detached"
/>
</div>
);
},
};
24 changes: 24 additions & 0 deletions apps/storybook/stories/FileInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,27 @@ export const AllVariations: Story = {
);
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<File | File[] | null>(null);
const [b, setB] = useState<File | File[] | null>(null);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 320}}>
<FileInput
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'File must be under 10MB'}}
/>
<FileInput
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'File must be under 10MB'}}
statusVariant="detached"
/>
</div>
);
},
};
29 changes: 29 additions & 0 deletions apps/storybook/stories/MultiSelector.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -405,3 +405,32 @@ export const Clearable: Story = {
placeholder: 'Select technologies...',
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<string[]>([]);
const [b, setB] = useState<string[]>([]);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 300}}>
<MultiSelector
label="Attached (default)"
options={['Name', 'Email', 'Role']}
value={a}
onChange={setA}
status={{type: 'error', message: 'Select at least one column'}}
placeholder="Select..."
/>
<MultiSelector
label="Detached"
options={['Name', 'Email', 'Role']}
value={b}
onChange={setB}
status={{type: 'error', message: 'Select at least one column'}}
statusVariant="detached"
placeholder="Select..."
/>
</div>
);
},
decorators: [Story => <Story />],
};
24 changes: 24 additions & 0 deletions apps/storybook/stories/NumberInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -555,3 +555,27 @@ export const ClearableWithUnits: Story = {
max: 100,
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<number | null>(-5);
const [b, setB] = useState<number | null>(-5);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 280}}>
<NumberInput
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'Must be a positive number'}}
/>
<NumberInput
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'Must be a positive number'}}
statusVariant="detached"
/>
</div>
);
},
};
28 changes: 28 additions & 0 deletions apps/storybook/stories/PowerSearch.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1375,3 +1375,31 @@ export const DisabledWithMessage: Story = {
placeholder: 'Search...',
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<PowerSearchFilter[]>([]);
const [b, setB] = useState<PowerSearchFilter[]>([]);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 400}}>
<PowerSearch
config={basicConfig}
filters={a}
onChange={newFilters => setA([...newFilters])}
isLabelHidden={false}
label="Attached (default)"
status={{type: 'error', message: 'Add at least one filter'}}
/>
<PowerSearch
config={basicConfig}
filters={b}
onChange={newFilters => setB([...newFilters])}
isLabelHidden={false}
label="Detached"
status={{type: 'error', message: 'Add at least one filter'}}
statusVariant="detached"
/>
</div>
);
},
};
35 changes: 35 additions & 0 deletions apps/storybook/stories/Selector.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -603,3 +603,38 @@ export const PlacementAbove: Story = {
);
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<string | undefined>();
const [b, setB] = useState<string | undefined>();
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 280}}>
<Selector
label="Attached (default)"
options={[
{value: 'apple', label: 'Apple'},
{value: 'banana', label: 'Banana'},
]}
value={a}
onChange={setA}
placeholder="Select a fruit..."
status={{type: 'error', message: 'Please select a fruit'}}
/>
<Selector
label="Detached"
options={[
{value: 'apple', label: 'Apple'},
{value: 'banana', label: 'Banana'},
]}
value={b}
onChange={setB}
placeholder="Select a fruit..."
status={{type: 'error', message: 'Please select a fruit'}}
statusVariant="detached"
/>
</div>
);
},
decorators: [Story => <Story />],
};
24 changes: 24 additions & 0 deletions apps/storybook/stories/TextArea.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -635,3 +635,27 @@ export const MaxLengthVariations: Story = {
);
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState('Too short');
const [b, setB] = useState('Too short');
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 320}}>
<TextArea
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'Must be at least 50 characters'}}
/>
<TextArea
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'Must be at least 50 characters'}}
statusVariant="detached"
/>
</div>
);
},
};
24 changes: 24 additions & 0 deletions apps/storybook/stories/TextInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,27 @@ export const ClearableWithStatus: Story = {
status: {type: 'error', message: 'Invalid email address'},
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState('invalid@');
const [b, setB] = useState('invalid@');
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 280}}>
<TextInput
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'Enter a valid email'}}
/>
<TextInput
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'Enter a valid email'}}
statusVariant="detached"
/>
</div>
);
},
};
28 changes: 28 additions & 0 deletions apps/storybook/stories/TimeInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,31 @@ export const AllVariations: Story = {
);
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<ISOTimeString | undefined>(
'22:00' as ISOTimeString,
);
const [b, setB] = useState<ISOTimeString | undefined>(
'22:00' as ISOTimeString,
);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 280}}>
<TimeInput
label="Attached (default)"
value={a}
onChange={setA}
status={{type: 'error', message: 'Must be during business hours'}}
/>
<TimeInput
label="Detached"
value={b}
onChange={setB}
status={{type: 'error', message: 'Must be during business hours'}}
statusVariant="detached"
/>
</div>
);
},
};
26 changes: 26 additions & 0 deletions apps/storybook/stories/Tokenizer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,29 @@ export const DisabledWithMessage: Story = {
disabledMessage: 'You need edit access to change members',
},
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<SearchableItem[]>([]);
const [b, setB] = useState<SearchableItem[]>([]);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 320}}>
<Tokenizer
label="Attached (default)"
searchSource={userSource}
value={a}
onChange={items => setA(items)}
status={{type: 'error', message: 'Select at least one member'}}
/>
<Tokenizer
label="Detached"
searchSource={userSource}
value={b}
onChange={items => setB(items)}
status={{type: 'error', message: 'Select at least one member'}}
statusVariant="detached"
/>
</div>
);
},
};
26 changes: 26 additions & 0 deletions apps/storybook/stories/Typeahead.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,29 @@ export const WithStartIcon: Story = {
},
name: 'With Start Icon',
};

export const StatusVariantComparison: Story = {
render: () => {
const [a, setA] = useState<SearchableItem | null>(null);
const [b, setB] = useState<SearchableItem | null>(null);
return (
<div style={{display: 'flex', flexDirection: 'column', gap: 24, width: 300}}>
<Typeahead
label="Attached (default)"
searchSource={fruitSource}
value={a}
onChange={setA}
status={{type: 'error', message: 'Please make a selection'}}
/>
<Typeahead
label="Detached"
searchSource={fruitSource}
value={b}
onChange={setB}
status={{type: 'error', message: 'Please make a selection'}}
statusVariant="detached"
/>
</div>
);
},
};
Loading
Loading