@latte-macchiat-io/latte-vanilla-components 0.0.269 → 0.0.270

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 (26) hide show
  1. package/package.json +1 -1
  2. package/src/components/Actions/styles.css.ts +1 -1
  3. package/src/components/Form/Row/theme.tsx +5 -5
  4. package/src/components/Form/TextField/Input/export.tsx +3 -6
  5. package/src/components/Form/TextField/Input/index.tsx +13 -0
  6. package/src/components/Form/TextField/Input/{Input.css.ts → styles.css.ts} +16 -54
  7. package/src/components/Form/TextField/Input/theme.tsx +57 -0
  8. package/src/components/Form/TextField/Input/types.tsx +1 -0
  9. package/src/components/Form/TextField/Label/export.tsx +2 -4
  10. package/src/components/Form/TextField/Label/{Label.tsx → index.tsx} +1 -1
  11. package/src/components/Form/TextField/Textarea/export.tsx +2 -6
  12. package/src/components/Form/TextField/Textarea/{Textarea.tsx → index.tsx} +1 -1
  13. package/src/components/Form/TextField/export.tsx +5 -3
  14. package/src/components/Form/TextField/index.tsx +51 -0
  15. package/src/components/Form/TextField/styles.css.ts +32 -0
  16. package/src/components/Form/TextField/theme.tsx +54 -0
  17. package/src/components/Form/export.tsx +1 -13
  18. package/src/components/Form/theme.tsx +0 -2
  19. package/src/theme/baseThemeValues.ts +12 -4
  20. package/src/theme/contract.css.ts +49 -9
  21. package/src/theme/createTheme.ts +0 -3
  22. package/src/components/Form/TextField/Input/Input.tsx +0 -134
  23. package/src/components/Form/TextField/TextField.css.ts +0 -142
  24. package/src/components/Form/TextField/TextField.tsx +0 -197
  25. /package/src/components/Form/TextField/Label/{Label.css.ts → styles.css.ts} +0 -0
  26. /package/src/components/Form/TextField/Textarea/{Textarea.css.ts → styles.css.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@latte-macchiat-io/latte-vanilla-components",
3
- "version": "0.0.269",
3
+ "version": "0.0.270",
4
4
  "description": "Beautiful components for amazing projects, with a touch of Vanilla 🥤",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -14,7 +14,7 @@ export const actionsRecipe = recipe(
14
14
  '@media': {
15
15
  ...generateResponsiveMedia({
16
16
  gap: themeContract.actions.gap,
17
- paddingTop: themeContract.actions.paddingBottom,
17
+ paddingTop: themeContract.actions.paddingTop,
18
18
  paddingBottom: themeContract.actions.paddingBottom,
19
19
  }),
20
20
  },
@@ -1,5 +1,5 @@
1
1
  const themeFormRowBase = {
2
- formRow: {
2
+ row: {
3
3
  gap: {
4
4
  mobile: '15px',
5
5
  sm: '15px',
@@ -12,13 +12,13 @@ const themeFormRowBase = {
12
12
  };
13
13
 
14
14
  export const themeFormRowLight = {
15
- formRow: {
16
- ...themeFormRowBase.formRow,
15
+ row: {
16
+ ...themeFormRowBase.row,
17
17
  },
18
18
  };
19
19
 
20
20
  export const themeFormRowDark = {
21
- formRow: {
22
- ...themeFormRowBase.formRow,
21
+ row: {
22
+ ...themeFormRowBase.row,
23
23
  },
24
24
  };
@@ -1,6 +1,3 @@
1
- // export { TextFieldInput } from '.';
2
- // export type { TextFieldInputProps } from '.';
3
-
4
- // export { Type as InputType } from './types';
5
-
6
- // export { styles as InputStyles } from './styles.css';
1
+ export { Input, type InputProps } from './';
2
+ export { type InputVariants } from './styles.css';
3
+ export { type InputType as InputFieldType } from './types';
@@ -0,0 +1,13 @@
1
+ import { clsx } from 'clsx';
2
+ import { inputRecipe, type InputVariants } from './styles.css';
3
+
4
+ import { InputType } from './types';
5
+
6
+ export type InputProps = React.InputHTMLAttributes<HTMLInputElement> &
7
+ InputVariants & {
8
+ type?: InputType;
9
+ };
10
+
11
+ export const Input = ({ name, type = 'text', value, required, placeholder, className }: InputProps) => (
12
+ <input id={name} name={name} type={type} value={value} required={required} placeholder={placeholder} className={clsx(inputRecipe(), className)} />
13
+ );
@@ -1,19 +1,27 @@
1
1
  import { style } from '@vanilla-extract/css';
2
2
  import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
3
3
  import { themeContract } from '../../../../theme/contract.css';
4
+ import { generateResponsiveMedia } from '../../../../utils/generateResponsiveMedia';
4
5
 
5
6
  const inputBase = style({
7
+ width: '100%',
6
8
  appearance: 'none',
7
- backgroundColor: themeContract.colors.background,
8
- border: `1px solid ${themeContract.colors.border}`,
9
- borderRadius: themeContract.radii.md,
10
- color: themeContract.colors.text,
9
+ transition: 'all 0.2s ease-in-out',
10
+
11
11
  fontFamily: themeContract.fonts.body,
12
- fontSize: themeContract.fontSizes.sm,
13
- lineHeight: themeContract.lineHeights.normal,
12
+ color: themeContract.form.textField.color,
13
+ border: themeContract.form.textField.border,
14
+ borderRadius: themeContract.form.textField.borderRadius,
15
+ backgroundColor: themeContract.form.textField.backgroundColor,
16
+
14
17
  padding: `${themeContract.space.sm} ${themeContract.space.md}`,
15
- transition: 'all 0.2s ease-in-out',
16
- width: '100%',
18
+
19
+ '@media': {
20
+ ...generateResponsiveMedia({
21
+ paddingTop: themeContract.form.textField.paddingTop,
22
+ paddingBottom: themeContract.form.textField.paddingBottom,
23
+ }),
24
+ },
17
25
 
18
26
  // '::placeholder': {
19
27
  // color: themeContract.colors.textSecondary,
@@ -56,52 +64,6 @@ const inputBase = style({
56
64
  export const inputRecipe = recipe(
57
65
  {
58
66
  base: inputBase,
59
-
60
- variants: {
61
- size: {
62
- sm: {
63
- fontSize: themeContract.fontSizes.xs,
64
- padding: `${themeContract.space.xs} ${themeContract.space.sm}`,
65
- },
66
- md: {},
67
- lg: {
68
- fontSize: themeContract.fontSizes.md,
69
- padding: `${themeContract.space.md} ${themeContract.space.lg}`,
70
- },
71
- },
72
- variant: {
73
- default: {},
74
- filled: {
75
- backgroundColor: themeContract.colors.surface,
76
- border: 'none',
77
-
78
- // ':focus': {
79
- // backgroundColor: themeContract.colors.background,
80
- // border: `1px solid ${themeContract.colors.primary}`,
81
- // },
82
- },
83
- outlined: {
84
- backgroundColor: 'transparent',
85
- },
86
- underlined: {
87
- backgroundColor: 'transparent',
88
- border: 'none',
89
- borderBottom: `1px solid ${themeContract.colors.border}`,
90
- borderRadius: 0,
91
- padding: `${themeContract.space.sm} 0`,
92
-
93
- // ':focus': {
94
- // borderBottom: `2px solid ${themeContract.colors.primary}`,
95
- // outline: 'none',
96
- // },
97
- },
98
- },
99
- },
100
-
101
- defaultVariants: {
102
- size: 'md',
103
- variant: 'default',
104
- },
105
67
  },
106
68
  'input'
107
69
  );
@@ -0,0 +1,57 @@
1
+ const themeFormTextFieldInputBase = {
2
+ form: {
3
+ textField: {
4
+ borderRadius: '30px',
5
+ paddingTop: {
6
+ mobile: '15px',
7
+ sm: '15px',
8
+ md: '30px',
9
+ lg: '30px',
10
+ xl: '50px',
11
+ '2xl': '50px',
12
+ },
13
+ paddingBottom: {
14
+ mobile: '15px',
15
+ sm: '15px',
16
+ md: '30px',
17
+ lg: '30px',
18
+ xl: '50px',
19
+ '2xl': '50px',
20
+ },
21
+ paddingRight: {
22
+ mobile: '15px',
23
+ sm: '15px',
24
+ md: '30px',
25
+ lg: '30px',
26
+ xl: '50px',
27
+ '2xl': '50px',
28
+ },
29
+ paddingLeft: {
30
+ mobile: '15px',
31
+ sm: '15px',
32
+ md: '30px',
33
+ lg: '30px',
34
+ xl: '50px',
35
+ '2xl': '50px',
36
+ },
37
+ },
38
+ },
39
+ };
40
+
41
+ export const themeFormTextFieldInputLight = {
42
+ form: {
43
+ textField: {
44
+ ...themeFormTextFieldInputBase.form,
45
+ backgroundColor: '#000000',
46
+ },
47
+ },
48
+ };
49
+
50
+ export const themeFormTextFieldInputDark = {
51
+ form: {
52
+ textField: {
53
+ ...themeFormTextFieldInputBase.form,
54
+ backgroundColor: '#000000',
55
+ },
56
+ },
57
+ };
@@ -0,0 +1 @@
1
+ export type InputType = 'text' | 'email' | 'search' | 'number' | 'hidden' | 'password' | 'tel' | 'url' | 'date' | 'time' | 'datetime-local' | 'color';
@@ -1,4 +1,2 @@
1
- // export { TextFieldLabel } from '.';
2
- // export type { TextFieldLabelProps } from '.';
3
- //
4
- // export { styles as TextFieldLabelStyles } from './styles.css';
1
+ export { Label, type LabelProps } from './index';
2
+ export { type LabelVariants } from './styles.css';
@@ -1,6 +1,6 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { forwardRef } from 'react';
3
- import { labelRecipe, type LabelVariants } from './Label.css';
3
+ import { labelRecipe, type LabelVariants } from './styles.css';
4
4
  import { sprinkles, type Sprinkles } from '../../../../styles/sprinkles.css';
5
5
 
6
6
  export interface LabelProps extends Omit<React.LabelHTMLAttributes<HTMLLabelElement>, 'color'>, Sprinkles, NonNullable<LabelVariants> {
@@ -1,6 +1,2 @@
1
- // export { TextFieldTextarea } from '.';
2
- // export type { TextFieldTextareaProps } from '.';
3
-
4
- // export { Type as TextareaType } from './types';
5
-
6
- // export { styles as TextareaStyles } from './styles.css';
1
+ export { Textarea, type TextareaProps } from './';
2
+ export { type TextareaVariants } from './styles.css';
@@ -1,6 +1,6 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { forwardRef } from 'react';
3
- import { textareaRecipe, type TextareaVariants } from './Textarea.css';
3
+ import { textareaRecipe, type TextareaVariants } from './styles.css';
4
4
  import { sprinkles, type Sprinkles } from '../../../../styles/sprinkles.css';
5
5
 
6
6
  export interface TextareaProps
@@ -1,4 +1,6 @@
1
- // export { TextField } from '.';
2
- // export type { TextFieldProps } from '.';
1
+ export { TextField, type TextFieldProps, type InputType } from './';
2
+ export { type TextFieldVariants } from './styles.css';
3
3
 
4
- // export { styles as TextFieldStyles } from './styles.css';
4
+ export * from './Input/export';
5
+ export * from './Label/export';
6
+ export * from './Textarea/export';
@@ -0,0 +1,51 @@
1
+ import { clsx } from 'clsx';
2
+ import { useMemo } from 'react';
3
+ import { Input } from './Input';
4
+ import { InputType } from './Input/types';
5
+ import { errorMessage, messageContainer, textFieldRecipe } from './styles.css';
6
+
7
+ export type TextFieldProps = React.HTMLAttributes<HTMLDivElement> & {
8
+ name: string;
9
+ label?: string;
10
+ value?: string;
11
+ rows?: number;
12
+ required?: boolean;
13
+ placeholder?: string;
14
+ errors?: string | string[];
15
+ type?: InputType | 'textarea';
16
+ onChange?: (e: { target: { value: string | undefined } }) => void | undefined;
17
+ };
18
+
19
+ export const TextField = (props: TextFieldProps) => {
20
+ const hasErrors = useMemo(() => {
21
+ if (!props.errors) return false;
22
+ if (Array.isArray(props.errors)) return props.errors.length > 0;
23
+ return Boolean(props.errors);
24
+ }, [props.errors]);
25
+
26
+ const isTextarea = props.type === 'textarea';
27
+
28
+ return (
29
+ <div className={clsx(textFieldRecipe(), props.className)}>
30
+ {props.label && <label htmlFor={props.name}>{props.label}</label>}
31
+
32
+ {isTextarea ? <textarea /> : <Input {...props} />}
33
+
34
+ {hasErrors && (
35
+ <div className={messageContainer}>
36
+ {Array.isArray(props.errors) ? (
37
+ props.errors.map((error, index) => (
38
+ <span key={index} className={errorMessage}>
39
+ {error}
40
+ </span>
41
+ ))
42
+ ) : (
43
+ <span className={errorMessage}>{props.errors}</span>
44
+ )}
45
+ </div>
46
+ )}
47
+ </div>
48
+ );
49
+ };
50
+
51
+ TextField.displayName = 'TextField';
@@ -0,0 +1,32 @@
1
+ import { style } from '@vanilla-extract/css';
2
+ import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
3
+ import { themeContract } from '../../../theme/contract.css';
4
+
5
+ const textFieldBase = style({
6
+ width: '100%',
7
+ display: 'flex',
8
+ flexDirection: 'column',
9
+ });
10
+
11
+ const messageContainer = style({
12
+ display: 'flex',
13
+ flexDirection: 'column',
14
+ gap: themeContract.space.xs,
15
+ });
16
+
17
+ const errorMessage = style({
18
+ color: themeContract.colors.error,
19
+ fontSize: themeContract.fontSizes.xs,
20
+ lineHeight: themeContract.lineHeights.tight,
21
+ fontFamily: themeContract.fonts.body,
22
+ });
23
+
24
+ export const textFieldRecipe = recipe(
25
+ {
26
+ base: textFieldBase,
27
+ },
28
+ 'text-field'
29
+ );
30
+
31
+ export { messageContainer, errorMessage };
32
+ export type TextFieldVariants = RecipeVariants<typeof textFieldRecipe>;
@@ -0,0 +1,54 @@
1
+ const themeFormTextFieldBase = {
2
+ textField: {
3
+ border: '1px solid red',
4
+ borderRadius: '30px',
5
+ paddingTop: {
6
+ mobile: '15px',
7
+ sm: '15px',
8
+ md: '30px',
9
+ lg: '30px',
10
+ xl: '50px',
11
+ '2xl': '50px',
12
+ },
13
+ paddingBottom: {
14
+ mobile: '15px',
15
+ sm: '15px',
16
+ md: '30px',
17
+ lg: '30px',
18
+ xl: '50px',
19
+ '2xl': '50px',
20
+ },
21
+ paddingRight: {
22
+ mobile: '15px',
23
+ sm: '15px',
24
+ md: '30px',
25
+ lg: '30px',
26
+ xl: '50px',
27
+ '2xl': '50px',
28
+ },
29
+ paddingLeft: {
30
+ mobile: '15px',
31
+ sm: '15px',
32
+ md: '30px',
33
+ lg: '30px',
34
+ xl: '50px',
35
+ '2xl': '50px',
36
+ },
37
+ },
38
+ };
39
+
40
+ export const themeFormTextFieldLight = {
41
+ textField: {
42
+ ...themeFormTextFieldBase.textField,
43
+ color: '#000000',
44
+ backgroundColor: '#000000',
45
+ },
46
+ };
47
+
48
+ export const themeFormTextFieldDark = {
49
+ textField: {
50
+ ...themeFormTextFieldBase.textField,
51
+ color: '#000000',
52
+ backgroundColor: '#000000',
53
+ },
54
+ };
@@ -1,16 +1,4 @@
1
1
  export { Form, type FormProps } from '.';
2
2
 
3
3
  export * from './Row/export';
4
-
5
- export { TextField, type TextFieldProps, type InputType } from '../Form/TextField/TextField';
6
- export { type TextFieldVariants } from '../Form/TextField/TextField.css';
7
-
8
- // Form TextField Subcomponents
9
- export { Label, type LabelProps } from '../Form/TextField/Label/Label';
10
- export { type LabelVariants } from '../Form/TextField/Label/Label.css';
11
-
12
- export { Input, type InputProps, type InputType as InputFieldType } from '../Form/TextField/Input/Input';
13
- export { type InputVariants } from '../Form/TextField/Input/Input.css';
14
-
15
- export { Textarea, type TextareaProps } from '../Form/TextField/Textarea/Textarea';
16
- export { type TextareaVariants } from '../Form/TextField/Textarea/Textarea.css';
4
+ export * from './TextField/export';
@@ -1,5 +1,3 @@
1
- import { themeFormRowDark, themeFormRowLight } from './Row/theme';
2
-
3
1
  const themeFormBase = {
4
2
  form: {
5
3
  borderRadius: '30px',
@@ -4,6 +4,7 @@ import { themeCarouselDark, themeCarouselLight } from '../components/Carousel/th
4
4
  import { themeColumnsDark, themeColumnsLight } from '../components/Columns/theme';
5
5
  import { themeFooterDark, themeFooterLight } from '../components/Footer/theme';
6
6
  import { themeFormRowDark, themeFormRowLight } from '../components/Form/Row/theme';
7
+ import { themeFormTextFieldDark, themeFormTextFieldLight } from '../components/Form/TextField/theme';
7
8
  import { themeFormDark, themeFormLight } from '../components/Form/theme';
8
9
  import { themeHeaderDark, themeHeaderLight } from '../components/Header/theme';
9
10
  import { themeHeadingDark, themeHeadingLight } from '../components/Heading/theme';
@@ -141,8 +142,12 @@ export const baseLightTheme = {
141
142
  ...themeNavSocialLight,
142
143
 
143
144
  ...themeVideoLight,
144
- ...themeFormLight,
145
- ...themeFormRowLight,
145
+
146
+ form: {
147
+ ...themeFormLight.form,
148
+ ...themeFormRowLight,
149
+ ...themeFormTextFieldLight,
150
+ },
146
151
  };
147
152
 
148
153
  export const baseDarkTheme = {
@@ -271,6 +276,9 @@ export const baseDarkTheme = {
271
276
 
272
277
  ...themeVideoDark,
273
278
 
274
- ...themeFormDark,
275
- ...themeFormRowDark,
279
+ form: {
280
+ ...themeFormDark.form,
281
+ ...themeFormRowDark,
282
+ ...themeFormTextFieldDark,
283
+ },
276
284
  };
@@ -906,15 +906,55 @@ export const themeContract = createGlobalThemeContract({
906
906
  xl: 'latte-form-paddingLeft-xl',
907
907
  '2xl': 'latte-form-paddingLeft-2xl',
908
908
  },
909
- },
910
- formRow: {
911
- gap: {
912
- mobile: 'latte-formRow-gap-mobile',
913
- sm: 'latte-formRow-gap-sm',
914
- md: 'latte-formRow-gap-md',
915
- lg: 'latte-formRow-gap-lg',
916
- xl: 'latte-formRow-gap-xl',
917
- '2xl': 'latte-formRow-gap-2xl',
909
+
910
+ row: {
911
+ gap: {
912
+ mobile: 'latte-formRow-gap-mobile',
913
+ sm: 'latte-formRow-gap-sm',
914
+ md: 'latte-formRow-gap-md',
915
+ lg: 'latte-formRow-gap-lg',
916
+ xl: 'latte-formRow-gap-xl',
917
+ '2xl': 'latte-formRow-gap-2xl',
918
+ },
919
+ },
920
+
921
+ textField: {
922
+ color: 'latte-textField-color',
923
+ border: 'latte-textField-border',
924
+ borderRadius: 'latte-textField-borderRadius',
925
+ backgroundColor: 'latte-textField-backgroundColor',
926
+ paddingTop: {
927
+ mobile: 'latte-textField-input-paddingTop-mobile',
928
+ sm: 'latte-textField-input-paddingTop-sm',
929
+ md: 'latte-textField-input-paddingTop-md',
930
+ lg: 'latte-textField-input-paddingTop-lg',
931
+ xl: 'latte-textField-input-paddingTop-xl',
932
+ '2xl': 'latte-textField-input-paddingTop-2xl',
933
+ },
934
+ paddingBottom: {
935
+ mobile: 'latte-textField-input-paddingBottom-mobile',
936
+ sm: 'latte-textField-input-paddingBottom-sm',
937
+ md: 'latte-textField-input-paddingBottom-md',
938
+ lg: 'latte-textField-input-paddingBottom-lg',
939
+ xl: 'latte-textField-input-paddingBottom-xl',
940
+ '2xl': 'latte-textField-input-paddingBottom-2xl',
941
+ },
942
+ paddingRight: {
943
+ mobile: 'latte-textField-input-paddingRight-mobile',
944
+ sm: 'latte-textField-input-paddingRight-sm',
945
+ md: 'latte-textField-input-paddingRight-md',
946
+ lg: 'latte-textField-input-paddingRight-lg',
947
+ xl: 'latte-textField-input-paddingRight-xl',
948
+ '2xl': 'latte-textField-input-paddingRight-2xl',
949
+ },
950
+ paddingLeft: {
951
+ mobile: 'latte-textField-input-paddingLeft-mobile',
952
+ sm: 'latte-textField-input-paddingLeft-sm',
953
+ md: 'latte-textField-input-paddingLeft-md',
954
+ lg: 'latte-textField-input-paddingLeft-lg',
955
+ xl: 'latte-textField-input-paddingLeft-xl',
956
+ '2xl': 'latte-textField-input-paddingLeft-2xl',
957
+ },
918
958
  },
919
959
  },
920
960
  });
@@ -31,7 +31,6 @@ export type ThemeOverrides = {
31
31
  video?: Partial<typeof baseLightTheme.video>;
32
32
  carousel?: Partial<typeof baseLightTheme.carousel>;
33
33
  form?: Partial<typeof baseLightTheme.form>;
34
- formRow?: Partial<typeof baseLightTheme.formRow>;
35
34
  };
36
35
 
37
36
  // Utility to create a theme with partial overrides over light theme base
@@ -64,7 +63,6 @@ const createAppTheme = (selector: string, overrides: ThemeOverrides = {}) => {
64
63
  video: { ...baseLightTheme.video, ...overrides.video },
65
64
  carousel: { ...baseLightTheme.carousel, ...overrides.carousel },
66
65
  form: { ...baseLightTheme.form, ...overrides.form },
67
- formRow: { ...baseLightTheme.formRow, ...overrides.formRow },
68
66
  });
69
67
  };
70
68
 
@@ -98,7 +96,6 @@ const createAppDarkTheme = (selector: string, overrides: ThemeOverrides = {}) =>
98
96
  video: { ...baseDarkTheme.video, ...overrides.video },
99
97
  carousel: { ...baseDarkTheme.carousel, ...overrides.carousel },
100
98
  form: { ...baseDarkTheme.form, ...overrides.form },
101
- formRow: { ...baseDarkTheme.formRow, ...overrides.formRow },
102
99
  });
103
100
  };
104
101
 
@@ -1,134 +0,0 @@
1
- import { clsx } from 'clsx';
2
- import { forwardRef } from 'react';
3
- import { inputRecipe, type InputVariants } from './Input.css';
4
- import { sprinkles, type Sprinkles } from '../../../../styles/sprinkles.css';
5
-
6
- export type InputType = 'text' | 'email' | 'search' | 'number' | 'hidden' | 'password' | 'tel' | 'url' | 'date' | 'time' | 'datetime-local' | 'color';
7
-
8
- export interface InputProps
9
- extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'color' | 'size' | 'height' | 'width'>,
10
- Sprinkles,
11
- NonNullable<InputVariants> {
12
- name: string;
13
- type?: InputType;
14
- hasError?: boolean;
15
- }
16
-
17
- export const Input = forwardRef<HTMLInputElement, InputProps>(
18
- (
19
- {
20
- name,
21
- type = 'text',
22
- hasError = false,
23
- size,
24
- variant,
25
- className,
26
- // Extract sprinkles props
27
- margin,
28
- marginTop,
29
- marginBottom,
30
- marginLeft,
31
- marginRight,
32
- padding,
33
- paddingTop,
34
- paddingBottom,
35
- paddingLeft,
36
- paddingRight,
37
- gap,
38
- display,
39
- flexDirection,
40
- justifyContent,
41
- flexWrap,
42
- flex,
43
- width,
44
- height,
45
- minWidth,
46
- maxWidth,
47
- minHeight,
48
- position,
49
- top,
50
- bottom,
51
- left,
52
- right,
53
- zIndex,
54
- fontSize,
55
- fontFamily,
56
- lineHeight,
57
- textAlign,
58
- fontWeight,
59
- color,
60
- backgroundColor,
61
- borderRadius,
62
- borderWidth,
63
- borderStyle,
64
- borderColor,
65
- boxShadow,
66
- opacity,
67
- overflow,
68
- overflowX,
69
- overflowY,
70
- ...htmlProps
71
- },
72
- ref
73
- ) => {
74
- return (
75
- <input
76
- ref={ref}
77
- id={name}
78
- name={name}
79
- type={type}
80
- className={clsx(
81
- inputRecipe({ size, variant }),
82
- sprinkles({
83
- margin,
84
- marginTop,
85
- marginBottom,
86
- marginLeft,
87
- marginRight,
88
- padding,
89
- paddingTop,
90
- paddingBottom,
91
- paddingLeft,
92
- paddingRight,
93
- gap,
94
- display,
95
- flexDirection,
96
- justifyContent,
97
- flexWrap,
98
- flex,
99
- width,
100
- height,
101
- minWidth,
102
- maxWidth,
103
- minHeight,
104
- position,
105
- top,
106
- bottom,
107
- left,
108
- right,
109
- zIndex,
110
- fontSize,
111
- fontFamily,
112
- lineHeight,
113
- textAlign,
114
- fontWeight,
115
- color,
116
- backgroundColor,
117
- borderRadius,
118
- borderWidth,
119
- borderStyle,
120
- borderColor,
121
- boxShadow,
122
- opacity,
123
- overflow,
124
- overflowX,
125
- overflowY,
126
- }),
127
- className
128
- )}
129
- data-error={hasError}
130
- {...htmlProps}
131
- />
132
- );
133
- }
134
- );
@@ -1,142 +0,0 @@
1
- import { style } from '@vanilla-extract/css';
2
- import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
3
- import { themeContract } from '../../../theme/contract.css';
4
-
5
- const textFieldBase = style({
6
- display: 'flex',
7
- flexDirection: 'column',
8
- width: '100%',
9
- gap: themeContract.space.xs,
10
- });
11
-
12
- const labelBase = style({
13
- fontFamily: themeContract.fonts.body,
14
- fontSize: themeContract.fontSizes.sm,
15
- fontWeight: themeContract.fontWeights.medium,
16
- color: themeContract.colors.text,
17
- lineHeight: themeContract.lineHeights.tight,
18
-
19
- // selectors: {
20
- // '&[data-required="true"]': {
21
- // selectors: {
22
- // '&::after': {
23
- // content: ' *',
24
- // color: themeContract.colors.error,
25
- // },
26
- // },
27
- // },
28
- // },
29
- });
30
-
31
- const inputBase = style({
32
- appearance: 'none',
33
- backgroundColor: themeContract.colors.background,
34
- border: `1px solid ${themeContract.colors.border}`,
35
- borderRadius: themeContract.radii.md,
36
- color: themeContract.colors.text,
37
- fontFamily: themeContract.fonts.body,
38
- fontSize: themeContract.fontSizes.sm,
39
- lineHeight: themeContract.lineHeights.normal,
40
- padding: `${themeContract.space.sm} ${themeContract.space.md}`,
41
- transition: 'all 0.2s ease-in-out',
42
- width: '100%',
43
-
44
- '::placeholder': {
45
- color: themeContract.colors.textSecondary,
46
- },
47
-
48
- ':hover': {
49
- borderColor: themeContract.colors.primary,
50
- },
51
-
52
- ':focus': {
53
- outline: '2px solid',
54
- outlineColor: themeContract.colors.primary,
55
- outlineOffset: '2px',
56
- borderColor: themeContract.colors.primary,
57
- },
58
-
59
- ':disabled': {
60
- backgroundColor: themeContract.colors.surface,
61
- color: themeContract.colors.textSecondary,
62
- cursor: 'not-allowed',
63
- opacity: '0.6',
64
- },
65
-
66
- // selectors: {
67
- // '&[data-error="true"]': {
68
- // borderColor: themeContract.colors.error,
69
- // },
70
- // '&[data-error="true"]:focus': {
71
- // outlineColor: themeContract.colors.error,
72
- // borderColor: themeContract.colors.error,
73
- // },
74
- // },
75
- });
76
-
77
- const textareaBase = style([
78
- inputBase,
79
- {
80
- resize: 'vertical',
81
- minHeight: '80px',
82
- },
83
- ]);
84
-
85
- const messageContainer = style({
86
- display: 'flex',
87
- flexDirection: 'column',
88
- gap: themeContract.space.xs,
89
- });
90
-
91
- const errorMessage = style({
92
- color: themeContract.colors.error,
93
- fontSize: themeContract.fontSizes.xs,
94
- lineHeight: themeContract.lineHeights.tight,
95
- fontFamily: themeContract.fonts.body,
96
- });
97
-
98
- export const textFieldRecipe = recipe(
99
- {
100
- base: textFieldBase,
101
-
102
- variants: {
103
- size: {
104
- sm: {
105
- gap: themeContract.space.xs,
106
-
107
- // selectors: {
108
- // [`& .${labelBase}`]: {
109
- // fontSize: themeContract.fontSizes.xs,
110
- // },
111
- // [`& .${inputBase}`]: {
112
- // fontSize: themeContract.fontSizes.xs,
113
- // padding: `${themeContract.space.xs} ${themeContract.space.sm}`,
114
- // },
115
- // },
116
- },
117
- md: {},
118
- lg: {
119
- gap: themeContract.space.sm,
120
-
121
- // selectors: {
122
- // [`& .${labelBase}`]: {
123
- // fontSize: themeContract.fontSizes.md,
124
- // },
125
- // [`& .${inputBase}`]: {
126
- // fontSize: themeContract.fontSizes.md,
127
- // padding: `${themeContract.space.md} ${themeContract.space.lg}`,
128
- // },
129
- // },
130
- },
131
- },
132
- },
133
-
134
- defaultVariants: {
135
- size: 'md',
136
- },
137
- },
138
- 'text-field'
139
- );
140
-
141
- export { labelBase, inputBase, textareaBase, messageContainer, errorMessage };
142
- export type TextFieldVariants = RecipeVariants<typeof textFieldRecipe>;
@@ -1,197 +0,0 @@
1
- import { clsx } from 'clsx';
2
- import { forwardRef, useMemo } from 'react';
3
- import { errorMessage, inputBase, labelBase, messageContainer, textareaBase, textFieldRecipe, type TextFieldVariants } from './TextField.css';
4
- import { sprinkles, type Sprinkles } from '../../../styles/sprinkles.css';
5
-
6
- export type InputType = 'text' | 'email' | 'search' | 'number' | 'hidden' | 'password' | 'textarea';
7
-
8
- export interface TextFieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'color' | 'onChange'>, Sprinkles, NonNullable<TextFieldVariants> {
9
- name: string;
10
- label?: string;
11
- value?: string;
12
- rows?: number;
13
- disabled?: boolean;
14
- required?: boolean;
15
- placeholder?: string;
16
- errors?: string | string[];
17
- type?: InputType;
18
- onChange?: (e: { target: { value: string | undefined } }) => void | undefined;
19
- }
20
-
21
- export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
22
- (
23
- {
24
- name,
25
- label,
26
- value = '',
27
- rows = 3,
28
- disabled = false,
29
- required = false,
30
- placeholder = '',
31
- errors,
32
- type = 'text',
33
- onChange,
34
- size,
35
- className,
36
- // Extract sprinkles props
37
- margin,
38
- marginTop,
39
- marginBottom,
40
- marginLeft,
41
- marginRight,
42
- padding,
43
- paddingTop,
44
- paddingBottom,
45
- paddingLeft,
46
- paddingRight,
47
- gap,
48
- display,
49
- flexDirection,
50
- justifyContent,
51
- flexWrap,
52
- flex,
53
- width,
54
- height,
55
- minWidth,
56
- maxWidth,
57
- minHeight,
58
- position,
59
- top,
60
- bottom,
61
- left,
62
- right,
63
- zIndex,
64
- fontSize,
65
- fontFamily,
66
- lineHeight,
67
- textAlign,
68
- fontWeight,
69
- color,
70
- backgroundColor,
71
- borderRadius,
72
- borderWidth,
73
- borderStyle,
74
- borderColor,
75
- boxShadow,
76
- opacity,
77
- overflow,
78
- overflowX,
79
- overflowY,
80
- ...htmlProps
81
- },
82
- ref
83
- ) => {
84
- const hasErrors = useMemo(() => {
85
- if (!errors) return false;
86
- if (Array.isArray(errors)) return errors.length > 0;
87
- return Boolean(errors);
88
- }, [errors]);
89
-
90
- const isTextarea = type === 'textarea';
91
-
92
- return (
93
- <div
94
- ref={ref}
95
- className={clsx(
96
- textFieldRecipe({ size }),
97
- sprinkles({
98
- margin,
99
- marginTop,
100
- marginBottom,
101
- marginLeft,
102
- marginRight,
103
- padding,
104
- paddingTop,
105
- paddingBottom,
106
- paddingLeft,
107
- paddingRight,
108
- gap,
109
- display,
110
- flexDirection,
111
- justifyContent,
112
- flexWrap,
113
- flex,
114
- width,
115
- height,
116
- minWidth,
117
- maxWidth,
118
- minHeight,
119
- position,
120
- top,
121
- bottom,
122
- left,
123
- right,
124
- zIndex,
125
- fontSize,
126
- fontFamily,
127
- lineHeight,
128
- textAlign,
129
- fontWeight,
130
- color,
131
- backgroundColor,
132
- borderRadius,
133
- borderWidth,
134
- borderStyle,
135
- borderColor,
136
- boxShadow,
137
- opacity,
138
- overflow,
139
- overflowX,
140
- overflowY,
141
- }),
142
- className
143
- )}
144
- {...htmlProps}>
145
- {label && (
146
- <label htmlFor={name} className={labelBase} data-required={required}>
147
- {label}
148
- </label>
149
- )}
150
-
151
- {isTextarea ? (
152
- <textarea
153
- id={name}
154
- name={name}
155
- rows={rows}
156
- value={value}
157
- disabled={disabled}
158
- placeholder={placeholder}
159
- className={textareaBase}
160
- onChange={onChange}
161
- data-error={hasErrors}
162
- required={required}
163
- />
164
- ) : (
165
- <input
166
- id={name}
167
- name={name}
168
- type={type}
169
- value={value}
170
- disabled={disabled}
171
- placeholder={placeholder}
172
- className={inputBase}
173
- onChange={onChange}
174
- data-error={hasErrors}
175
- required={required}
176
- />
177
- )}
178
-
179
- {hasErrors && (
180
- <div className={messageContainer}>
181
- {Array.isArray(errors) ? (
182
- errors.map((error, index) => (
183
- <span key={index} className={errorMessage}>
184
- {error}
185
- </span>
186
- ))
187
- ) : (
188
- <span className={errorMessage}>{errors}</span>
189
- )}
190
- </div>
191
- )}
192
- </div>
193
- );
194
- }
195
- );
196
-
197
- TextField.displayName = 'TextField';