@flamingo-stack/openframe-frontend-core 0.0.659-2347.6820.1 → 0.0.659
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/dist/{chunk-D4Z7TYWU.js → chunk-2DLM3NXE.js} +4 -4
- package/dist/{chunk-NIKCNKD2.js → chunk-6DVSTVUU.js} +6 -6
- package/dist/{chunk-VAXW73AH.js → chunk-6KR74SOJ.js} +2 -2
- package/dist/{chunk-VZLPXDYN.cjs → chunk-AOOORZI5.cjs} +3 -3
- package/dist/{chunk-VZLPXDYN.cjs.map → chunk-AOOORZI5.cjs.map} +1 -1
- package/dist/{chunk-DMJ636H5.js → chunk-APWQTZCR.js} +2 -2
- package/dist/chunk-BXWXBBAC.js +228 -0
- package/dist/chunk-BXWXBBAC.js.map +1 -0
- package/dist/{chunk-JBL365GX.js → chunk-E7C3F2SK.js} +237 -223
- package/dist/chunk-E7C3F2SK.js.map +1 -0
- package/dist/{chunk-VXTTNIRO.cjs → chunk-EOGXNLZC.cjs} +17 -17
- package/dist/{chunk-VXTTNIRO.cjs.map → chunk-EOGXNLZC.cjs.map} +1 -1
- package/dist/{chunk-BOZSCBHS.cjs → chunk-GDHV5W5C.cjs} +33 -33
- package/dist/{chunk-BOZSCBHS.cjs.map → chunk-GDHV5W5C.cjs.map} +1 -1
- package/dist/{chunk-FLWYFXNK.cjs → chunk-ITVLOEGF.cjs} +55 -55
- package/dist/{chunk-FLWYFXNK.cjs.map → chunk-ITVLOEGF.cjs.map} +1 -1
- package/dist/{chunk-K5HPBVUH.cjs → chunk-K4W3II7M.cjs} +3 -3
- package/dist/{chunk-K5HPBVUH.cjs.map → chunk-K4W3II7M.cjs.map} +1 -1
- package/dist/{chunk-J7OY7QBA.js → chunk-KP4K6I5O.js} +3 -3
- package/dist/{chunk-XBPTM4IT.cjs → chunk-L4YJU3AG.cjs} +16 -16
- package/dist/{chunk-XBPTM4IT.cjs.map → chunk-L4YJU3AG.cjs.map} +1 -1
- package/dist/{chunk-FXUFGHVU.js → chunk-MEFCA6VD.js} +2 -2
- package/dist/{chunk-S5OQILYF.js → chunk-MW7LEMFG.js} +2 -2
- package/dist/{chunk-PDLHEQ6C.cjs → chunk-NXAXZPEO.cjs} +10 -10
- package/dist/{chunk-PDLHEQ6C.cjs.map → chunk-NXAXZPEO.cjs.map} +1 -1
- package/dist/{chunk-I6Q5RA5M.js → chunk-P2ROGTMF.js} +2 -2
- package/dist/{chunk-ZUOXB5YX.cjs → chunk-PUE4XMZK.cjs} +279 -265
- package/dist/chunk-PUE4XMZK.cjs.map +1 -0
- package/dist/chunk-QVSVRG7I.cjs +228 -0
- package/dist/chunk-QVSVRG7I.cjs.map +1 -0
- package/dist/{chunk-XNR7DJP3.js → chunk-QVZD2LLU.js} +4 -4
- package/dist/{chunk-3CTLV2BR.cjs → chunk-RRGTBH3L.cjs} +3 -3
- package/dist/{chunk-3CTLV2BR.cjs.map → chunk-RRGTBH3L.cjs.map} +1 -1
- package/dist/{chunk-7Q4UWN5X.js → chunk-T6LNAX4R.js} +3 -3
- package/dist/{chunk-TUXQWRF7.cjs → chunk-UIAXQJPV.cjs} +24 -24
- package/dist/{chunk-TUXQWRF7.cjs.map → chunk-UIAXQJPV.cjs.map} +1 -1
- package/dist/{chunk-ALMCR73J.js → chunk-VFGBRGFB.js} +3 -3
- package/dist/{chunk-5IXTYDUH.cjs → chunk-VJMPYY5O.cjs} +18 -18
- package/dist/{chunk-5IXTYDUH.cjs.map → chunk-VJMPYY5O.cjs.map} +1 -1
- package/dist/{chunk-XGJUNSUY.js → chunk-VVOI2OC3.js} +2 -2
- package/dist/{chunk-MCGLT2GN.cjs → chunk-VW7NXM2O.cjs} +36 -36
- package/dist/{chunk-MCGLT2GN.cjs.map → chunk-VW7NXM2O.cjs.map} +1 -1
- package/dist/{chunk-WK4464NX.cjs → chunk-VWGFJJD6.cjs} +4 -4
- package/dist/{chunk-WK4464NX.cjs.map → chunk-VWGFJJD6.cjs.map} +1 -1
- package/dist/{chunk-NP2XVKA6.cjs → chunk-WMIB6JSA.cjs} +4 -4
- package/dist/{chunk-NP2XVKA6.cjs.map → chunk-WMIB6JSA.cjs.map} +1 -1
- package/dist/{chunk-OTDNJTDJ.js → chunk-WWEQT2WX.js} +5 -5
- package/dist/{chunk-XRD5FFHK.cjs → chunk-Z7NI2TQD.cjs} +3 -3
- package/dist/{chunk-XRD5FFHK.cjs.map → chunk-Z7NI2TQD.cjs.map} +1 -1
- package/dist/{chunk-CCGP4PRB.js → chunk-ZT3CZCDS.js} +2 -2
- package/dist/components/case-studies/index.cjs +12 -12
- package/dist/components/case-studies/index.js +3 -3
- package/dist/components/contact/index.cjs +4 -4
- package/dist/components/contact/index.js +3 -3
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +4 -4
- package/dist/components/embeds/index.js +3 -3
- package/dist/components/faq/index.cjs +4 -4
- package/dist/components/faq/index.js +3 -3
- package/dist/components/features/index.cjs +3 -3
- package/dist/components/features/index.js +2 -2
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +33 -33
- package/dist/components/index.js +13 -13
- package/dist/components/meeting-scheduler/index.cjs +27 -27
- package/dist/components/meeting-scheduler/index.js +2 -2
- package/dist/components/navigation/index.cjs +6 -6
- package/dist/components/navigation/index.js +5 -5
- package/dist/components/onboarding-guides/index.cjs +6 -6
- package/dist/components/onboarding-guides/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/date-picker.d.ts +2 -2
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/field-wrapper.d.ts +15 -1
- package/dist/components/ui/field-wrapper.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +4 -4
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/index.cjs +3 -3
- package/dist/components/ui/index.js +2 -2
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/index.cjs +7 -7
- package/dist/index.js +6 -6
- package/package.json +1 -1
- package/src/components/ui/__tests__/field-wrapper-error-slot.test.tsx +82 -0
- package/src/components/ui/autocomplete.tsx +7 -1
- package/src/components/ui/date-picker.tsx +40 -38
- package/src/components/ui/field-wrapper.tsx +27 -3
- package/src/components/ui/input.tsx +131 -121
- package/src/components/ui/select.tsx +5 -2
- package/src/components/ui/textarea.tsx +173 -175
- package/dist/chunk-IWISVG4P.js +0 -198
- package/dist/chunk-IWISVG4P.js.map +0 -1
- package/dist/chunk-JBL365GX.js.map +0 -1
- package/dist/chunk-YWUZL3RT.cjs +0 -198
- package/dist/chunk-YWUZL3RT.cjs.map +0 -1
- package/dist/chunk-ZUOXB5YX.cjs.map +0 -1
- /package/dist/{chunk-D4Z7TYWU.js.map → chunk-2DLM3NXE.js.map} +0 -0
- /package/dist/{chunk-NIKCNKD2.js.map → chunk-6DVSTVUU.js.map} +0 -0
- /package/dist/{chunk-VAXW73AH.js.map → chunk-6KR74SOJ.js.map} +0 -0
- /package/dist/{chunk-DMJ636H5.js.map → chunk-APWQTZCR.js.map} +0 -0
- /package/dist/{chunk-J7OY7QBA.js.map → chunk-KP4K6I5O.js.map} +0 -0
- /package/dist/{chunk-FXUFGHVU.js.map → chunk-MEFCA6VD.js.map} +0 -0
- /package/dist/{chunk-S5OQILYF.js.map → chunk-MW7LEMFG.js.map} +0 -0
- /package/dist/{chunk-I6Q5RA5M.js.map → chunk-P2ROGTMF.js.map} +0 -0
- /package/dist/{chunk-XNR7DJP3.js.map → chunk-QVZD2LLU.js.map} +0 -0
- /package/dist/{chunk-7Q4UWN5X.js.map → chunk-T6LNAX4R.js.map} +0 -0
- /package/dist/{chunk-ALMCR73J.js.map → chunk-VFGBRGFB.js.map} +0 -0
- /package/dist/{chunk-XGJUNSUY.js.map → chunk-VVOI2OC3.js.map} +0 -0
- /package/dist/{chunk-OTDNJTDJ.js.map → chunk-WWEQT2WX.js.map} +0 -0
- /package/dist/{chunk-CCGP4PRB.js.map → chunk-ZT3CZCDS.js.map} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { FieldWrapper } from '../field-wrapper';
|
|
5
|
+
import { Input } from '../input';
|
|
6
|
+
import { Select, SelectTrigger, SelectValue } from '../select';
|
|
7
|
+
import { Textarea } from '../textarea';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A label-less FieldWrapper is `display: contents` until `error` is set and a
|
|
11
|
+
* box afterwards, and the parent's spacing lands on the box but not on
|
|
12
|
+
* `contents` — so the control jumped by that spacing whenever a message came
|
|
13
|
+
* or went. `errorSlot` renders the box from the first render, and the controls
|
|
14
|
+
* derive it from whether the caller wrote an `error` attribute at all. These
|
|
15
|
+
* pin both halves: the flag, and each control's wiring of it.
|
|
16
|
+
*
|
|
17
|
+
* The wrapper is a role-less <div> and its display mode IS the contract, so
|
|
18
|
+
* the tests walk up from the control (this file is listed in eslint.config.mjs).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const BOX = 'relative';
|
|
22
|
+
const TRANSPARENT = 'contents';
|
|
23
|
+
|
|
24
|
+
const wrapperOf = (control: Element | null) => control?.parentElement ?? null;
|
|
25
|
+
const inputWrapper = () => wrapperOf(screen.getByLabelText('Name').closest('label'));
|
|
26
|
+
|
|
27
|
+
describe('FieldWrapper errorSlot', () => {
|
|
28
|
+
it('stays layout-transparent without a label, a message or a slot', () => {
|
|
29
|
+
render(
|
|
30
|
+
<FieldWrapper>
|
|
31
|
+
<input aria-label="n" />
|
|
32
|
+
</FieldWrapper>,
|
|
33
|
+
);
|
|
34
|
+
expect(wrapperOf(screen.getByLabelText('n'))).toHaveClass(TRANSPARENT);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('is a box from the first render when the slot is reserved, and stays one', () => {
|
|
38
|
+
const { rerender } = render(
|
|
39
|
+
<FieldWrapper errorSlot>
|
|
40
|
+
<input aria-label="n" />
|
|
41
|
+
</FieldWrapper>,
|
|
42
|
+
);
|
|
43
|
+
expect(wrapperOf(screen.getByLabelText('n'))).toHaveClass(BOX);
|
|
44
|
+
rerender(
|
|
45
|
+
<FieldWrapper errorSlot error="Required">
|
|
46
|
+
<input aria-label="n" />
|
|
47
|
+
</FieldWrapper>,
|
|
48
|
+
);
|
|
49
|
+
expect(wrapperOf(screen.getByLabelText('n'))).toHaveClass(BOX);
|
|
50
|
+
expect(screen.getByText('Required')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('the controls reserve the slot when an error attribute is written', () => {
|
|
55
|
+
it('Input without the attribute stays transparent', () => {
|
|
56
|
+
render(<Input aria-label="Name" />);
|
|
57
|
+
expect(inputWrapper()).toHaveClass(TRANSPARENT);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('Input with error={undefined} is a box before any message, and stays one', () => {
|
|
61
|
+
const { rerender } = render(<Input aria-label="Name" error={undefined} />);
|
|
62
|
+
expect(inputWrapper()).toHaveClass(BOX);
|
|
63
|
+
rerender(<Input aria-label="Name" error="Required" />);
|
|
64
|
+
expect(inputWrapper()).toHaveClass(BOX);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('SelectTrigger with error={undefined} is a box before any message', () => {
|
|
68
|
+
render(
|
|
69
|
+
<Select>
|
|
70
|
+
<SelectTrigger aria-label="Shell" error={undefined}>
|
|
71
|
+
<SelectValue placeholder="Pick" />
|
|
72
|
+
</SelectTrigger>
|
|
73
|
+
</Select>,
|
|
74
|
+
);
|
|
75
|
+
expect(wrapperOf(screen.getByRole('combobox'))).toHaveClass(BOX);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('Textarea with error={undefined} is a box before any message', () => {
|
|
79
|
+
render(<Textarea aria-label="Notes" error={undefined} />);
|
|
80
|
+
expect(wrapperOf(screen.getByLabelText('Notes'))).toHaveClass(BOX);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -742,7 +742,13 @@ function AutocompleteInner<T = string>(props: AutocompleteProps<T>, ref: Forward
|
|
|
742
742
|
);
|
|
743
743
|
|
|
744
744
|
return (
|
|
745
|
-
<FieldWrapper
|
|
745
|
+
<FieldWrapper
|
|
746
|
+
label={label}
|
|
747
|
+
labelVariant={labelVariant}
|
|
748
|
+
error={error}
|
|
749
|
+
errorSlot={'error' in props}
|
|
750
|
+
className={className}
|
|
751
|
+
>
|
|
746
752
|
<div className="relative" ref={containerRef}>
|
|
747
753
|
{popover}
|
|
748
754
|
|
|
@@ -757,7 +757,7 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
757
757
|
);
|
|
758
758
|
|
|
759
759
|
return (
|
|
760
|
-
<FieldWrapper label={label} error={error}>
|
|
760
|
+
<FieldWrapper label={label} error={error} errorSlot={'error' in props}>
|
|
761
761
|
{picker}
|
|
762
762
|
</FieldWrapper>
|
|
763
763
|
);
|
|
@@ -790,23 +790,24 @@ const generateMinuteOptions = (): string[] => {
|
|
|
790
790
|
return Array.from({ length: 60 }, (_, i) => i.toString().padStart(2, '0'));
|
|
791
791
|
};
|
|
792
792
|
|
|
793
|
-
export function DatePickerInput({
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
793
|
+
export function DatePickerInput(props: DatePickerInputProps) {
|
|
794
|
+
const {
|
|
795
|
+
placeholder = 'Select date',
|
|
796
|
+
formatDate = defaultFormatDate,
|
|
797
|
+
disabled = false,
|
|
798
|
+
className,
|
|
799
|
+
numberOfMonths = 1,
|
|
800
|
+
fromDate,
|
|
801
|
+
toDate,
|
|
802
|
+
locale,
|
|
803
|
+
value,
|
|
804
|
+
onChange,
|
|
805
|
+
showTime = false,
|
|
806
|
+
use24HourFormat = false,
|
|
807
|
+
label,
|
|
808
|
+
error,
|
|
809
|
+
invalid = false,
|
|
810
|
+
} = props;
|
|
810
811
|
const [open, setOpen] = useState(false);
|
|
811
812
|
const isInvalid = invalid || !!error;
|
|
812
813
|
|
|
@@ -988,7 +989,7 @@ export function DatePickerInput({
|
|
|
988
989
|
);
|
|
989
990
|
|
|
990
991
|
return (
|
|
991
|
-
<FieldWrapper label={label} error={error} className={className}>
|
|
992
|
+
<FieldWrapper label={label} error={error} errorSlot={'error' in props} className={className}>
|
|
992
993
|
{content}
|
|
993
994
|
</FieldWrapper>
|
|
994
995
|
);
|
|
@@ -1036,24 +1037,25 @@ const generateTimeOptions = (intervalMinutes: number, use24Hour: boolean): { val
|
|
|
1036
1037
|
return options;
|
|
1037
1038
|
};
|
|
1038
1039
|
|
|
1039
|
-
export function DatePickerInputSimple({
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1040
|
+
export function DatePickerInputSimple(props: DatePickerInputSimpleProps) {
|
|
1041
|
+
const {
|
|
1042
|
+
placeholder = 'Select date',
|
|
1043
|
+
formatDate = defaultFormatDate,
|
|
1044
|
+
disabled = false,
|
|
1045
|
+
className,
|
|
1046
|
+
numberOfMonths = 1,
|
|
1047
|
+
fromDate,
|
|
1048
|
+
toDate,
|
|
1049
|
+
locale,
|
|
1050
|
+
value,
|
|
1051
|
+
onChange,
|
|
1052
|
+
showTime = false,
|
|
1053
|
+
timeInterval = 30,
|
|
1054
|
+
use24HourFormat = false,
|
|
1055
|
+
label,
|
|
1056
|
+
error,
|
|
1057
|
+
invalid = false,
|
|
1058
|
+
} = props;
|
|
1057
1059
|
const [open, setOpen] = useState(false);
|
|
1058
1060
|
const isInvalid = invalid || !!error;
|
|
1059
1061
|
|
|
@@ -1181,7 +1183,7 @@ export function DatePickerInputSimple({
|
|
|
1181
1183
|
);
|
|
1182
1184
|
|
|
1183
1185
|
return (
|
|
1184
|
-
<FieldWrapper label={label} error={error} className={className}>
|
|
1186
|
+
<FieldWrapper label={label} error={error} errorSlot={'error' in props} className={className}>
|
|
1185
1187
|
{content}
|
|
1186
1188
|
</FieldWrapper>
|
|
1187
1189
|
);
|
|
@@ -21,13 +21,27 @@ export interface FieldWrapperProps {
|
|
|
21
21
|
*
|
|
22
22
|
* It is positioned OUT OF FLOW (absolute, hanging below the wrapper), so it
|
|
23
23
|
* never changes the field's height when it appears — the same treatment
|
|
24
|
-
* `CheckboxBlock` uses.
|
|
24
|
+
* `CheckboxBlock` uses. That holds only while the wrapper is already a box
|
|
25
|
+
* when the message arrives — see `errorSlot`. The trade is that it overlaps whatever sits directly
|
|
25
26
|
* below: a form stacking fields needs at least ~20px of vertical gap (e.g.
|
|
26
27
|
* `gap-[var(--spacing-system-lf)]`) for the message to land in clear space.
|
|
27
28
|
*/
|
|
28
29
|
error?: string;
|
|
29
30
|
/** Color variant for the message: "error" (red), "warning" (yellow), "success" (green) or "muted" (grey) */
|
|
30
31
|
errorVariant?: 'error' | 'warning' | 'success' | 'muted';
|
|
32
|
+
/**
|
|
33
|
+
* The field is wired for a message: `error` may arrive later, so render the
|
|
34
|
+
* positioned box from the FIRST render instead of only once it does.
|
|
35
|
+
*
|
|
36
|
+
* Without a label the wrapper is `display: contents` while `error` is empty
|
|
37
|
+
* and a box once it is set — and the parent's spacing lands on a box but not
|
|
38
|
+
* on `contents`: a `space-y-*` margin, a flex row's sizing. So the control
|
|
39
|
+
* moved by that spacing the moment a message came, and back when it went.
|
|
40
|
+
* The controls pass this whenever the caller wrote an `error` attribute at
|
|
41
|
+
* all (`error={undefined}` included): those are exactly the call sites whose
|
|
42
|
+
* layout could flip, and a box there is the layout they already got on error.
|
|
43
|
+
*/
|
|
44
|
+
errorSlot?: boolean;
|
|
31
45
|
/**
|
|
32
46
|
* Label scale. Default is the standard form-label scale (text-h6).
|
|
33
47
|
* 'large' (text-h4) is for screens whose design specifies body-scale field
|
|
@@ -56,10 +70,20 @@ const errorVariantClasses = {
|
|
|
56
70
|
|
|
57
71
|
const FieldWrapper = forwardRef<HTMLDivElement, FieldWrapperProps>(
|
|
58
72
|
(
|
|
59
|
-
{
|
|
73
|
+
{
|
|
74
|
+
label,
|
|
75
|
+
htmlFor,
|
|
76
|
+
error,
|
|
77
|
+
errorVariant = 'error',
|
|
78
|
+
errorSlot = false,
|
|
79
|
+
labelVariant = 'default',
|
|
80
|
+
required = false,
|
|
81
|
+
className,
|
|
82
|
+
children,
|
|
83
|
+
},
|
|
60
84
|
ref,
|
|
61
85
|
) => {
|
|
62
|
-
const hasChrome = label != null || error != null;
|
|
86
|
+
const hasChrome = label != null || error != null || errorSlot;
|
|
63
87
|
|
|
64
88
|
return (
|
|
65
89
|
<div ref={ref} className={cn(hasChrome ? 'relative flex w-full flex-col' : 'contents', className)}>
|
|
@@ -30,134 +30,144 @@ const invalidBorderClasses = {
|
|
|
30
30
|
warning: '!border-ods-warning hover:!border-ods-warning has-[:focus]:!border-ods-warning',
|
|
31
31
|
} as const;
|
|
32
32
|
|
|
33
|
-
const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const isInvalid = invalid || (!!error && variantIsInvalid);
|
|
33
|
+
const Input = forwardRef<HTMLInputElement, InputProps>((allProps, ref) => {
|
|
34
|
+
const {
|
|
35
|
+
className,
|
|
36
|
+
type,
|
|
37
|
+
invalid = false,
|
|
38
|
+
startAdornment,
|
|
39
|
+
endAdornment,
|
|
40
|
+
label,
|
|
41
|
+
labelVariant,
|
|
42
|
+
error,
|
|
43
|
+
errorVariant = 'error',
|
|
44
|
+
loading = false,
|
|
45
|
+
...props
|
|
46
|
+
} = allProps;
|
|
47
|
+
// Wired for a message even while `error` is undefined — see `FieldWrapper.errorSlot`.
|
|
48
|
+
const errorSlot = 'error' in allProps;
|
|
49
|
+
// success/muted are informational — they never paint the invalid border
|
|
50
|
+
const variantIsInvalid = errorVariant === 'error' || errorVariant === 'warning';
|
|
51
|
+
const isInvalid = invalid || (!!error && variantIsInvalid);
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
className={cn(
|
|
60
|
-
'h-1 w-full cursor-pointer appearance-none rounded-full bg-white/30',
|
|
61
|
-
// Webkit (Chrome/Safari) thumb
|
|
62
|
-
'[&::-webkit-slider-thumb]:appearance-none',
|
|
63
|
-
'[&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3',
|
|
64
|
-
'[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white',
|
|
65
|
-
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-sm',
|
|
66
|
-
// Firefox thumb
|
|
67
|
-
'[&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:w-3',
|
|
68
|
-
'[&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white',
|
|
69
|
-
'[&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:border-0',
|
|
70
|
-
// Firefox track
|
|
71
|
-
'[&::-moz-range-track]:bg-transparent',
|
|
72
|
-
// Disabled
|
|
73
|
-
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
74
|
-
className,
|
|
75
|
-
)}
|
|
76
|
-
ref={ref}
|
|
77
|
-
{...props}
|
|
78
|
-
/>
|
|
79
|
-
);
|
|
80
|
-
return label ? (
|
|
81
|
-
<FieldWrapper label={label} labelVariant={labelVariant} error={error} errorVariant={errorVariant}>
|
|
82
|
-
{rangeInput}
|
|
83
|
-
</FieldWrapper>
|
|
84
|
-
) : (
|
|
85
|
-
rangeInput
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const content = (
|
|
90
|
-
<label
|
|
91
|
-
data-invalid={isInvalid || undefined}
|
|
53
|
+
// Range inputs get a clean slider rendering — no label wrapper, borders, or adornments
|
|
54
|
+
if (type === 'range') {
|
|
55
|
+
const rangeInput = (
|
|
56
|
+
<input
|
|
57
|
+
type="range"
|
|
92
58
|
className={cn(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
'
|
|
97
|
-
'
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// Disabled
|
|
106
|
-
|
|
107
|
-
// must read as one flat colour, not grey text next to a live icon.
|
|
108
|
-
// Scoped to the DIRECT span children (the two adornment wrappers): a
|
|
109
|
-
// descendant rule would also repaint whatever the caller renders INSIDE
|
|
110
|
-
// an adornment, which is passed verbatim precisely so it can own its
|
|
111
|
-
// colour (a `Tag` there went invisible against its own fill).
|
|
112
|
-
props.disabled && '!cursor-not-allowed bg-ods-bg',
|
|
113
|
-
'has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled',
|
|
114
|
-
// Invalid
|
|
115
|
-
isInvalid && invalidBorderClasses[variantIsInvalid ? errorVariant : 'error'],
|
|
59
|
+
'h-1 w-full cursor-pointer appearance-none rounded-full bg-white/30',
|
|
60
|
+
// Webkit (Chrome/Safari) thumb
|
|
61
|
+
'[&::-webkit-slider-thumb]:appearance-none',
|
|
62
|
+
'[&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3',
|
|
63
|
+
'[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white',
|
|
64
|
+
'[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-sm',
|
|
65
|
+
// Firefox thumb
|
|
66
|
+
'[&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:w-3',
|
|
67
|
+
'[&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-white',
|
|
68
|
+
'[&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:border-0',
|
|
69
|
+
// Firefox track
|
|
70
|
+
'[&::-moz-range-track]:bg-transparent',
|
|
71
|
+
// Disabled
|
|
72
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
116
73
|
className,
|
|
117
74
|
)}
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
{startAdornment}
|
|
122
|
-
</span>
|
|
123
|
-
)}
|
|
124
|
-
<input
|
|
125
|
-
type={type}
|
|
126
|
-
className={cn(
|
|
127
|
-
// Layout
|
|
128
|
-
'min-w-0 flex-1 border-none bg-transparent outline-none',
|
|
129
|
-
// Typography
|
|
130
|
-
'text-h4',
|
|
131
|
-
// Colors
|
|
132
|
-
'text-ods-text-primary placeholder:text-ods-text-secondary',
|
|
133
|
-
// File input adjustments
|
|
134
|
-
'file:border-0 file:bg-transparent',
|
|
135
|
-
// Disabled
|
|
136
|
-
'disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border',
|
|
137
|
-
// Touch
|
|
138
|
-
'touch-manipulation',
|
|
139
|
-
// Autofill override
|
|
140
|
-
'[&:-webkit-autofill]:[-webkit-box-shadow:0_0_0_9999px_transparent_inset] [&:-webkit-autofill]:[-webkit-text-fill-color:var(--color-text-primary)] [&:-webkit-autofill]:[caret-color:var(--color-text-primary)] [&:-webkit-autofill]:[transition:background-color_9999s_ease-in-out_0s]',
|
|
141
|
-
)}
|
|
142
|
-
ref={ref}
|
|
143
|
-
{...props}
|
|
144
|
-
/>
|
|
145
|
-
{loading && <Loader2 className="size-4 flex-shrink-0 animate-spin text-ods-text-secondary md:size-6" />}
|
|
146
|
-
{!loading && endAdornment && (
|
|
147
|
-
<span className="flex-shrink-0 text-ods-text-secondary transition-colors duration-200 text-h6 group-has-[:focus]:text-ods-accent group-data-[invalid]:text-ods-error [&_svg]:size-4 md:[&_svg]:size-6">
|
|
148
|
-
{endAdornment}
|
|
149
|
-
</span>
|
|
150
|
-
)}
|
|
151
|
-
</label>
|
|
75
|
+
ref={ref}
|
|
76
|
+
{...props}
|
|
77
|
+
/>
|
|
152
78
|
);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{
|
|
79
|
+
return label ? (
|
|
80
|
+
<FieldWrapper
|
|
81
|
+
label={label}
|
|
82
|
+
labelVariant={labelVariant}
|
|
83
|
+
error={error}
|
|
84
|
+
errorVariant={errorVariant}
|
|
85
|
+
errorSlot={errorSlot}
|
|
86
|
+
>
|
|
87
|
+
{rangeInput}
|
|
157
88
|
</FieldWrapper>
|
|
89
|
+
) : (
|
|
90
|
+
rangeInput
|
|
158
91
|
);
|
|
159
|
-
}
|
|
160
|
-
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const content = (
|
|
95
|
+
<label
|
|
96
|
+
data-invalid={isInvalid || undefined}
|
|
97
|
+
className={cn(
|
|
98
|
+
// Layout & spacing
|
|
99
|
+
'flex h-11 w-full cursor-text items-center gap-2 rounded-[6px] border px-3 md:h-12',
|
|
100
|
+
// Focus-within states
|
|
101
|
+
'has-[:focus-visible]:outline-none',
|
|
102
|
+
'group',
|
|
103
|
+
// Animations & touch UX
|
|
104
|
+
'transition-colors duration-200',
|
|
105
|
+
// Theme palette
|
|
106
|
+
'border-ods-border bg-ods-card has-[:focus]:border-ods-accent',
|
|
107
|
+
// Hover & active (not disabled)
|
|
108
|
+
!props.disabled &&
|
|
109
|
+
'hover:border-ods-border-hover hover:bg-ods-bg-hover active:border-ods-border-active active:bg-ods-bg-active',
|
|
110
|
+
// Disabled. The adornments (and any icon inside them) carry their own
|
|
111
|
+
// `text-ods-text-secondary`, so grey them from here — a disabled field
|
|
112
|
+
// must read as one flat colour, not grey text next to a live icon.
|
|
113
|
+
// Scoped to the DIRECT span children (the two adornment wrappers): a
|
|
114
|
+
// descendant rule would also repaint whatever the caller renders INSIDE
|
|
115
|
+
// an adornment, which is passed verbatim precisely so it can own its
|
|
116
|
+
// colour (a `Tag` there went invisible against its own fill).
|
|
117
|
+
props.disabled && '!cursor-not-allowed bg-ods-bg',
|
|
118
|
+
'has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled',
|
|
119
|
+
// Invalid
|
|
120
|
+
isInvalid && invalidBorderClasses[variantIsInvalid ? errorVariant : 'error'],
|
|
121
|
+
className,
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{startAdornment && (
|
|
125
|
+
<span className="flex-shrink-0 text-ods-text-secondary transition-colors duration-200 text-h6 group-has-[:focus]:text-ods-accent group-data-[invalid]:text-ods-error [&_svg]:size-4 md:[&_svg]:size-6">
|
|
126
|
+
{startAdornment}
|
|
127
|
+
</span>
|
|
128
|
+
)}
|
|
129
|
+
<input
|
|
130
|
+
type={type}
|
|
131
|
+
className={cn(
|
|
132
|
+
// Layout
|
|
133
|
+
'min-w-0 flex-1 border-none bg-transparent outline-none',
|
|
134
|
+
// Typography
|
|
135
|
+
'text-h4',
|
|
136
|
+
// Colors
|
|
137
|
+
'text-ods-text-primary placeholder:text-ods-text-secondary',
|
|
138
|
+
// File input adjustments
|
|
139
|
+
'file:border-0 file:bg-transparent',
|
|
140
|
+
// Disabled
|
|
141
|
+
'disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border',
|
|
142
|
+
// Touch
|
|
143
|
+
'touch-manipulation',
|
|
144
|
+
// Autofill override
|
|
145
|
+
'[&:-webkit-autofill]:[-webkit-box-shadow:0_0_0_9999px_transparent_inset] [&:-webkit-autofill]:[-webkit-text-fill-color:var(--color-text-primary)] [&:-webkit-autofill]:[caret-color:var(--color-text-primary)] [&:-webkit-autofill]:[transition:background-color_9999s_ease-in-out_0s]',
|
|
146
|
+
)}
|
|
147
|
+
ref={ref}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
{loading && <Loader2 className="size-4 flex-shrink-0 animate-spin text-ods-text-secondary md:size-6" />}
|
|
151
|
+
{!loading && endAdornment && (
|
|
152
|
+
<span className="flex-shrink-0 text-ods-text-secondary transition-colors duration-200 text-h6 group-has-[:focus]:text-ods-accent group-data-[invalid]:text-ods-error [&_svg]:size-4 md:[&_svg]:size-6">
|
|
153
|
+
{endAdornment}
|
|
154
|
+
</span>
|
|
155
|
+
)}
|
|
156
|
+
</label>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<FieldWrapper
|
|
161
|
+
label={label}
|
|
162
|
+
labelVariant={labelVariant}
|
|
163
|
+
error={error}
|
|
164
|
+
errorVariant={errorVariant}
|
|
165
|
+
errorSlot={errorSlot}
|
|
166
|
+
>
|
|
167
|
+
{content}
|
|
168
|
+
</FieldWrapper>
|
|
169
|
+
);
|
|
170
|
+
});
|
|
161
171
|
Input.displayName = 'Input';
|
|
162
172
|
|
|
163
173
|
export { Input };
|
|
@@ -23,7 +23,10 @@ const SelectTrigger = forwardRef<
|
|
|
23
23
|
labelVariant?: 'default' | 'large';
|
|
24
24
|
error?: string;
|
|
25
25
|
}
|
|
26
|
-
>((
|
|
26
|
+
>((allProps, ref) => {
|
|
27
|
+
const { className, children, invalid, label, labelVariant, error, ...props } = allProps;
|
|
28
|
+
// Wired for a message even while `error` is undefined — see `FieldWrapper.errorSlot`.
|
|
29
|
+
const errorSlot = 'error' in allProps;
|
|
27
30
|
const isInvalid = invalid || !!error;
|
|
28
31
|
|
|
29
32
|
const trigger = (
|
|
@@ -73,7 +76,7 @@ const SelectTrigger = forwardRef<
|
|
|
73
76
|
);
|
|
74
77
|
|
|
75
78
|
return (
|
|
76
|
-
<FieldWrapper label={label} labelVariant={labelVariant} error={error}>
|
|
79
|
+
<FieldWrapper label={label} labelVariant={labelVariant} error={error} errorSlot={errorSlot}>
|
|
77
80
|
{trigger}
|
|
78
81
|
</FieldWrapper>
|
|
79
82
|
);
|