@varialkit/textfield 0.1.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/docs.md ADDED
@@ -0,0 +1,174 @@
1
+ # TextField
2
+
3
+ The TextField component is a single-line text input field. It is a **controlled component**, which means you must manage its state by providing a `value` and an `onChange` handler. It is used for collecting user input that is a single line, such as names, emails, or titles.
4
+
5
+ ## How to Use
6
+
7
+ To use the TextField, import it from the `@solara/textfield` package and manage its state in your component.
8
+
9
+ ```tsx
10
+ import React, { useState } from 'react';
11
+ import { TextField } from "@solara/textfield";
12
+
13
+ export function MyComponent() {
14
+ const [value, setValue] = useState("");
15
+
16
+ return (
17
+ <TextField
18
+ id="my-textfield"
19
+ value={value}
20
+ onChange={(e) => setValue(e.target.value)}
21
+ placeholder="Enter your name..."
22
+ label="Enter your name:"
23
+ helperText="This is a helper text."
24
+ />
25
+ );
26
+ }
27
+ ```
28
+
29
+ ## Best Practices
30
+
31
+ - **Provide a Label**: Always use the `label` prop to provide a descriptive label for the text field. This is crucial for accessibility and usability.
32
+ - **Controlled Component**: Remember that `TextField` is a controlled component. You need to manage its `value` and `onChange` event to update the state.
33
+ - **Use Helper Text for Guidance**: The `helperText` prop is useful for providing additional instructions or context below the text field.
34
+ - **Use Placeholders Wisely**: Use placeholders to provide hints or examples of the expected input, but do not rely on them as a substitute for a label.
35
+
36
+ ## Props
37
+
38
+ The TextField component accepts the following props:
39
+
40
+ | Prop | Type | Default | Description |
41
+ | ------------ | ---------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
42
+ | `variant` | `"default" | "quiet"` | `"default"` | The visual style of the text field. `default` has a visible border, while `quiet` is borderless. |
43
+ | `size` | `"small" | "medium" | "large"` | `"medium"` | Controls the text field's internal padding and font size. Use `small` for tight spaces and `large` for touch-friendly interfaces. |
44
+ | `radius` | `"small" | "medium" | "large" | "full"` | `"medium"` | Controls the textfield's border radius. |
45
+ | `isInvalid` | `boolean` | `false` | When `true`, applies a style to indicate a validation error. This is useful for form validation. |
46
+ | `isDisabled` | `boolean` | `false` | When `true`, the text field is not interactive and has a disabled style. This prevents users from entering text. |
47
+ | `label` | `string` | | The label for the text field. |
48
+ | `labelPosition` | `"top" | "left"` | `"top"` | The position of the label. |
49
+ | `helperText` | `string` | | The helper text for the text field. |
50
+ | `iconLeft` | `SolaraIconName | IconProps` | | Optional leading icon rendered inside the field. |
51
+ | `fullWidth` | `boolean` | `false` | When `true`, the text field will take up the full width of its container. |
52
+
53
+ ...and all other standard `React.InputHTMLAttributes<HTMLInputElement>` props, such as `value`, `onChange`, `id`, `placeholder`, `size`, etc.
54
+
55
+ ## Label and Helper Text
56
+
57
+ The `TextField` component now includes built-in support for a `label` and `helperText`.
58
+
59
+ - The `label` prop adds a visible label to the text field, which is essential for accessibility.
60
+ - The `helperText` prop provides additional guidance or context below the input.
61
+
62
+ ```tsx
63
+ <TextField
64
+ label="Your Name"
65
+ helperText="Please enter your full name."
66
+ />
67
+ ```
68
+
69
+ ## Label Position
70
+
71
+ You can control the position of the label using the `labelPosition` prop. It defaults to `top`.
72
+
73
+ ### Top (Default)
74
+
75
+ The label is displayed above the text field.
76
+
77
+ ```tsx
78
+ <TextField label="Top-aligned Label" labelPosition="top" />
79
+ ```
80
+
81
+ ### Left
82
+
83
+ The label is displayed to the left of the text field.
84
+
85
+ ```tsx
86
+ <TextField label="Left-aligned Label" labelPosition="left" />
87
+ ```
88
+
89
+ ## Variants
90
+
91
+ The `variant` prop allows you to control the text field's visual appearance.
92
+
93
+ ### Default
94
+
95
+ The default variant has a visible border and is suitable for most use cases.
96
+
97
+ ```tsx
98
+ <TextField placeholder="Default text field" />
99
+ ```
100
+
101
+ ### Quiet
102
+
103
+ The quiet variant has no border, making it suitable for clean, minimalist layouts where the text field should be less prominent.
104
+
105
+ ```tsx
106
+ <TextField variant="quiet" placeholder="Quiet text field" />
107
+ ```
108
+
109
+ ## Size
110
+
111
+ The `size` prop adjusts the text field's padding and font size to fit different layout requirements.
112
+
113
+ ### Small
114
+
115
+ Small size has reduced padding and a smaller font size, making it suitable for compact interfaces where space is limited.
116
+
117
+ ```tsx
118
+ <TextField size="small" placeholder="Small text field" />
119
+ ```
120
+
121
+ ### Medium
122
+
123
+ Medium size has standard padding and font size. This is the default setting and should be used in most cases.
124
+
125
+ ```tsx
126
+ <TextField size="medium" placeholder="Medium text field" />
127
+ ```
128
+
129
+ ### Large
130
+
131
+ Large size has increased padding and a larger font size, making it more prominent and easier to interact with on touch devices.
132
+
133
+ ```tsx
134
+ <TextField size="large" placeholder="Large text field" />
135
+ ```
136
+
137
+ ## Invalid State
138
+
139
+ When `isInvalid` is `true`, the text field is styled with a red border to indicate a validation error. This is commonly used in forms to show the user which fields need to be corrected.
140
+
141
+ ```tsx
142
+ <TextField isInvalid placeholder="Invalid text field" />
143
+ ```
144
+
145
+ ## Disabled State
146
+
147
+ When `isDisabled` is `true`, the text field is not interactive and has a visually distinct style. This is useful for preventing users from entering text when it is not appropriate to do so.
148
+
149
+ ```tsx
150
+ <TextField isDisabled placeholder="Disabled text field" />
151
+ ```
152
+
153
+ ## Accessibility
154
+
155
+ The `TextField` component is designed with accessibility in mind.
156
+
157
+ - **Internal Label**: The component now internally handles the association between the label and the input. By providing the `label` prop, the component ensures the correct `for` and `id` attributes are set, making it accessible to screen readers.
158
+ - **Keyboard Navigation**: The text field is focusable and can be navigated using the keyboard.
159
+
160
+ ## Icons
161
+
162
+ You can render a leading icon inside the text field. Icons inherit the text color by default.
163
+
164
+ ```tsx
165
+ <TextField placeholder="Search" iconLeft="data_spreadsheet_search_24" />
166
+ ```
167
+
168
+ ## Full Width
169
+
170
+ When `fullWidth` is `true`, the text field will take up the full width of its container. This is useful for creating responsive layouts that adapt to different screen sizes.
171
+
172
+ ```tsx
173
+ <TextField fullWidth />
174
+ ```
@@ -0,0 +1 @@
1
+ export { stories } from "../examples";
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@varialkit/textfield",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./examples": "./examples/index.tsx"
10
+ },
11
+ "dependencies": {
12
+ "@varialkit/icons": "0.1.0"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "docs.md",
17
+ "examples"
18
+ ],
19
+ "peerDependencies": {
20
+ "react": "^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "19.0.10",
24
+ "react": "19.0.0"
25
+ }
26
+ }
@@ -0,0 +1,187 @@
1
+ .solara-textfield-wrapper {
2
+ --textfield-padding-y: var(--space-2);
3
+ --textfield-padding-x: var(--space-3);
4
+ --textfield-font-size: var(--font-size-body-scaled);
5
+ --textfield-line-height: var(--line-height-body-scaled);
6
+ --textfield-label-font-size: var(--font-size-caption-scaled);
7
+ --textfield-helper-font-size: var(--font-size-footnote-scaled);
8
+ --textfield-label-gap: var(--space-1);
9
+ --textfield-helper-gap: var(--space-1);
10
+ display: flex;
11
+
12
+ &.solara-textfield-wrapper--full-width {
13
+ width: 100%;
14
+
15
+ .solara-textfield-container,
16
+ .solara-textfield-field,
17
+ .solara-textfield {
18
+ width: 100%;
19
+ }
20
+ }
21
+
22
+ &.solara-textfield--size-small {
23
+ --textfield-padding-y: var(--space-1);
24
+ --textfield-padding-x: var(--space-2);
25
+ --textfield-font-size: var(--font-size-caption-scaled);
26
+ --textfield-line-height: var(--line-height-caption-scaled);
27
+ --textfield-label-font-size: var(--font-size-footnote-scaled);
28
+ --textfield-helper-font-size: var(--font-size-footnote-scaled);
29
+ }
30
+
31
+ &.solara-textfield--size-large {
32
+ --textfield-padding-y: var(--space-3);
33
+ --textfield-padding-x: var(--space-4);
34
+ --textfield-font-size: var(--font-size-h5-scaled);
35
+ --textfield-line-height: var(--line-height-body-scaled);
36
+ --textfield-label-font-size: var(--font-size-body-scaled);
37
+ --textfield-helper-font-size: var(--font-size-caption-scaled);
38
+ }
39
+
40
+ &--label-position-top {
41
+ flex-direction: column;
42
+
43
+ .solara-textfield-label {
44
+ margin-bottom: calc(var(--textfield-label-gap) * var(--spacing-multiplier));
45
+ }
46
+ }
47
+
48
+ &--label-position-left {
49
+ flex-direction: row;
50
+ align-items: center;
51
+
52
+ .solara-textfield-label {
53
+ margin-right: calc(var(--textfield-label-gap) * var(--spacing-multiplier));
54
+ }
55
+ }
56
+ }
57
+
58
+ .solara-textfield {
59
+ --textfield-border-radius: var(--radius-2);
60
+ // Base styles for the textfield
61
+ border: 1px solid var(--color-surface-400);
62
+ background-color: var(--color-surface-0);
63
+ border-radius: calc(var(--textfield-border-radius) * var(--radius-multiplier));
64
+ padding: calc(var(--textfield-padding-y) * var(--spacing-multiplier))
65
+ calc(var(--textfield-padding-x) * var(--spacing-multiplier));
66
+ font-size: var(--textfield-font-size);
67
+ font-family: var(--font-body);
68
+ line-height: var(--textfield-line-height);
69
+ color: var(--color-text-primary);
70
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
71
+
72
+ &::placeholder {
73
+ color: var(--color-text-tertiary);
74
+ }
75
+
76
+ &--radius-small {
77
+ --textfield-border-radius: var(--radius-1);
78
+ }
79
+
80
+ &--radius-large {
81
+ --textfield-border-radius: var(--radius-3);
82
+ }
83
+
84
+ &--radius-full {
85
+ --textfield-border-radius: var(--radius-full);
86
+ }
87
+
88
+ &:hover {
89
+ border-color: var(--color-primary);
90
+ background-color: var(--color-surface-200);
91
+ }
92
+
93
+ &:focus {
94
+ outline: none;
95
+ border-color: var(--color-primary);
96
+ box-shadow: 0 0 0 2px var(--color-primary-focus);
97
+ }
98
+
99
+ &:active {
100
+ background-color: var(--color-surface-300);
101
+ }
102
+
103
+ // Disabled state
104
+ &--disabled,
105
+ &:disabled {
106
+ background-color: var(--color-surface-200);
107
+ color: var(--color-on-surface-disabled);
108
+ cursor: not-allowed;
109
+ border-color: var(--color-surface-300);
110
+
111
+ &:hover {
112
+ border-color: var(--color-surface-300);
113
+ background-color: var(--color-surface-200);
114
+ }
115
+ }
116
+
117
+ // Invalid state
118
+ &--invalid {
119
+ border-color: var(--color-destructive);
120
+
121
+ &:focus {
122
+ box-shadow: 0 0 0 2px var(--color-destructive-focus);
123
+ }
124
+ }
125
+
126
+ // Variants
127
+ &--quiet {
128
+ background-color: transparent;
129
+ border-color: transparent;
130
+ border-bottom: 1px solid var(--color-surface-400);
131
+ border-radius: 0;
132
+
133
+ &:hover,
134
+ &:focus {
135
+ border-color: var(--color-primary);
136
+ background-color: var(--color-surface-100);
137
+ }
138
+ }
139
+
140
+ &--with-icon {
141
+ padding-left: calc(
142
+ (var(--textfield-padding-x) * var(--spacing-multiplier)) +
143
+ (var(--textfield-icon-size, 1.1em)) +
144
+ (var(--textfield-icon-gap, var(--space-2)) * var(--spacing-multiplier))
145
+ );
146
+ }
147
+ }
148
+
149
+ .solara-textfield-field {
150
+ position: relative;
151
+ display: inline-flex;
152
+ align-items: center;
153
+ }
154
+
155
+ .solara-textfield-icon {
156
+ position: absolute;
157
+ left: calc(var(--textfield-padding-x) * var(--spacing-multiplier));
158
+ display: inline-flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ pointer-events: none;
162
+ color: currentColor;
163
+ }
164
+
165
+ .solara-textfield-field .solara-icon [stroke]:not([stroke="none"]) {
166
+ stroke: currentColor;
167
+ }
168
+
169
+ .solara-textfield-field .solara-icon [fill]:not([fill="none"]) {
170
+ fill: currentColor;
171
+ }
172
+
173
+ .solara-textfield-container {
174
+ display: flex;
175
+ flex-direction: column;
176
+ }
177
+
178
+ .solara-textfield-label {
179
+ font-size: var(--textfield-label-font-size);
180
+ color: var(--color-text-primary);
181
+ }
182
+
183
+ .solara-textfield-helper-text {
184
+ font-size: var(--textfield-helper-font-size);
185
+ color: var(--color-text-secondary);
186
+ margin-top: calc(var(--textfield-helper-gap) * var(--spacing-multiplier));
187
+ }
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+ import { Icon } from '@solara/icons';
3
+ import type { IconProps } from '@solara/icons';
4
+ import { TextFieldProps } from './TextField.types';
5
+ import './TextField.scss';
6
+
7
+ type TextFieldIcon = IconProps | IconProps['name'];
8
+
9
+ const normalizeIconProps = (icon: TextFieldIcon): IconProps =>
10
+ typeof icon === 'string' ? { name: icon } : icon;
11
+
12
+ const resolveIconProps = (icon: TextFieldIcon): IconProps => {
13
+ const iconProps = normalizeIconProps(icon);
14
+
15
+ return {
16
+ ...iconProps,
17
+ style: {
18
+ ...iconProps.style,
19
+ // Keep textfield icons aligned with the text color.
20
+ color: 'currentColor',
21
+ },
22
+ };
23
+ };
24
+
25
+ /**
26
+ * A standard text input field for single-line input.
27
+ * It is a controlled component, so you must provide a `value` and `onChange` handler.
28
+ * It supports all the standard props of an HTML input element.
29
+ */
30
+ export const TextField: React.FC<TextFieldProps> = ({
31
+ variant = 'default',
32
+ size = 'medium',
33
+ radius = 'medium',
34
+ isInvalid = false,
35
+ isDisabled = false,
36
+ // The label for the text field.
37
+ label,
38
+ // The position of the label.
39
+ labelPosition = 'top',
40
+ // The helper text for the text field.
41
+ helperText,
42
+ iconLeft,
43
+ fullWidth,
44
+ ...props
45
+ }) => {
46
+ // The base class for the component to scope all styles.
47
+ const baseClass = 'solara-textfield';
48
+
49
+ // The variant class, which is determined by the `variant` prop.
50
+ const variantClass = `solara-textfield--${variant}`;
51
+
52
+ // The size class modifies the padding and font size of the component.
53
+ const sizeClass = `solara-textfield--size-${size}`;
54
+
55
+ const radiusClass = `solara-textfield--radius-${radius}`;
56
+
57
+ // The invalid class is applied when the `isInvalid` prop is true, typically for validation errors.
58
+ const invalidClass = isInvalid ? 'solara-textfield--invalid' : '';
59
+
60
+ // The disabled class is applied when the `isDisabled` prop is true.
61
+ const disabledClass = isDisabled ? 'solara-textfield--disabled' : '';
62
+
63
+ // The final classes for the component are composed of the base class and any modifier classes.
64
+ const classes = [
65
+ baseClass,
66
+ variantClass,
67
+ sizeClass,
68
+ radiusClass,
69
+ invalidClass,
70
+ disabledClass,
71
+ iconLeft ? 'solara-textfield--with-icon' : '',
72
+ ]
73
+ .join(' ')
74
+ .trim();
75
+
76
+ const textFieldInput = (
77
+ <input className={classes} disabled={isDisabled} {...props} />
78
+ );
79
+
80
+ const textField = iconLeft ? (
81
+ <div className='solara-textfield-field'>
82
+ <span className='solara-textfield-icon'>
83
+ <Icon {...resolveIconProps(iconLeft)} />
84
+ </span>
85
+ {textFieldInput}
86
+ </div>
87
+ ) : (
88
+ textFieldInput
89
+ );
90
+
91
+ const fullWidthClass = fullWidth ? 'solara-textfield-wrapper--full-width' : '';
92
+
93
+ // This wrapper handles the positioning of the label and the text field (either top or left).
94
+ const wrapperClass = `solara-textfield-wrapper solara-textfield-wrapper--label-position-${labelPosition} ${sizeClass} ${fullWidthClass}`.trim();
95
+
96
+ return (
97
+ <div className={wrapperClass}>
98
+ {/* The label is rendered only if the `label` prop is provided. */}
99
+ {label && <label className='solara-textfield-label'>{label}</label>}
100
+ {/* This container groups the text field and its helper text. */}
101
+ <div className='solara-textfield-container'>
102
+ {textField}
103
+ {/* The helper text is rendered only if the `helperText` prop is provided. */}
104
+ {helperText && (
105
+ <p className='solara-textfield-helper-text'>{helperText}</p>
106
+ )}
107
+ </div>
108
+ </div>
109
+ );
110
+ };
@@ -0,0 +1,48 @@
1
+ import React from 'react';
2
+ import type { IconProps } from '@solara/icons';
3
+
4
+ export type TextFieldSize = 'small' | 'medium' | 'large';
5
+
6
+ export interface TextFieldProps
7
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
8
+ /**
9
+ * The variant of the text field.
10
+ */
11
+ variant?: 'default' | 'quiet';
12
+ /**
13
+ * The size of the text field.
14
+ */
15
+ size?: TextFieldSize;
16
+ /**
17
+ * Whether the text field is invalid.
18
+ */
19
+ isInvalid?: boolean;
20
+ /**
21
+ * Whether the text field is disabled.
22
+ */
23
+ isDisabled?: boolean;
24
+ /**
25
+ * The label for the text field.
26
+ */
27
+ label?: string;
28
+ /**
29
+ * The position of the label.
30
+ */
31
+ labelPosition?: 'top' | 'left';
32
+ /**
33
+ * The helper text for the text field.
34
+ */
35
+ helperText?: string;
36
+ /**
37
+ * Optional leading icon to render inside the field.
38
+ */
39
+ iconLeft?: IconProps | IconProps['name'];
40
+ /**
41
+ * The radius of the textfield.
42
+ */
43
+ radius?: 'small' | 'medium' | 'large' | 'full';
44
+ /**
45
+ * Whether the text field should take up the full width of its container.
46
+ */
47
+ fullWidth?: boolean;
48
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './TextField';