@startupjs-ui/text-input 0.1.3

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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/text-input
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **text-input:** increase padding for 'm' size ([b3b6836](https://github.com/startupjs/startupjs-ui/commit/b3b6836ffea7f26d73851fbee3e6c6f0c53a81d3))
20
+ * **text-input:** set default font ([a77a27b](https://github.com/startupjs/startupjs-ui/commit/a77a27bbe570537d0f95784aad9e2b58627a6b0f))
21
+
22
+
23
+ ### Features
24
+
25
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
26
+ * **text-input:** refactor TextInput component ([d0c7d64](https://github.com/startupjs/startupjs-ui/commit/d0c7d64233e575227c1764454c336735e203a95b))
package/README.mdx ADDED
@@ -0,0 +1,165 @@
1
+ import { useState } from 'react'
2
+ import TextInput, { _PropsJsonSchema as TextInputPropsJsonSchema } from './index'
3
+ import Div from '@startupjs-ui/div'
4
+ import Br from '@startupjs-ui/br'
5
+ import { faSearch, faTimesCircle } from '@fortawesome/free-solid-svg-icons'
6
+ import { Sandbox } from '@startupjs-ui/docs'
7
+ import './index.mdx.cssx.styl'
8
+
9
+ # TextInput
10
+
11
+ TextInput lets user enter or edit text.
12
+
13
+ ```jsx
14
+ import { TextInput } from 'startupjs-ui'
15
+ ```
16
+
17
+ ## Simple example
18
+
19
+ ```jsx example
20
+ const [value, setValue] = useState()
21
+ return (
22
+ <TextInput
23
+ placeholder='TextInput'
24
+ value={value}
25
+ onChangeText={setValue}
26
+ />
27
+ )
28
+ ```
29
+
30
+ ## Disabled
31
+
32
+ ```jsx example
33
+ return (
34
+ <TextInput
35
+ disabled
36
+ value='disabled'
37
+ />
38
+ )
39
+ ```
40
+
41
+ ## Readonly
42
+
43
+ ```jsx example
44
+ return (
45
+ <TextInput
46
+ value='readonly'
47
+ readonly
48
+ />
49
+ )
50
+ ```
51
+
52
+ ## Sizes
53
+
54
+ Size can be modified using the `size` prop. Default size is `m`.
55
+
56
+ ```jsx example
57
+ const [valueL, setValueL] = useState()
58
+ const [valueM, setValueM] = useState()
59
+ const [valueS, setValueS] = useState()
60
+ return (
61
+ <Div>
62
+ <TextInput
63
+ size='s'
64
+ value={valueS}
65
+ onChangeText={setValueS}
66
+ />
67
+ <Br />
68
+ <TextInput
69
+ size='m'
70
+ value={valueM}
71
+ onChangeText={setValueM}
72
+ />
73
+ <Br />
74
+ <TextInput
75
+ size='l'
76
+ value={valueL}
77
+ onChangeText={setValueL}
78
+ />
79
+ </Div>
80
+ )
81
+ ```
82
+
83
+ ## Icons
84
+
85
+ Icon can be added using `icon` and `secondaryIcon` properties. Position of icon can be changed by passing `iconPosition` to component (`left` by default). The `secondaryIcon` uses opposite position of `iconPosition`. To handle clicks on the icon, use the property `onIconPress` and `onSecondaryIconPress`. To change icon color use the ʻiconStyleName` and `secondaryIconStyleName` properties.
86
+
87
+ In `.styl` file
88
+ ```stylus
89
+ .icon
90
+ color var(--color-text-primary)
91
+ ```
92
+
93
+ ```jsx example
94
+ const [valueLeft, setLeftValue] = useState()
95
+ const [valueRight, setRightValue] = useState()
96
+ return (
97
+ <Div>
98
+ <TextInput
99
+ icon={faSearch}
100
+ secondaryIcon={faTimesCircle}
101
+ secondaryIconStyleName='icon'
102
+ value={valueLeft}
103
+ onChangeText={setLeftValue}
104
+ onSecondaryIconPress={() => setLeftValue()}
105
+ />
106
+ <Br />
107
+ <TextInput
108
+ icon={faTimesCircle}
109
+ iconPosition='right'
110
+ value={valueRight}
111
+ onChangeText={setRightValue}
112
+ onIconPress={() => setRightValue()}
113
+ />
114
+ </Div>
115
+ )
116
+ ```
117
+
118
+ ## Number of lines
119
+
120
+ Pass `numberOfLines={number}` to set the number of lines.
121
+
122
+ ```jsx example
123
+ const [value, setValue] = useState()
124
+
125
+ return (
126
+ <TextInput
127
+ placeholder='write here'
128
+ value={value}
129
+ numberOfLines={4}
130
+ onChangeText={setValue}
131
+ />
132
+ )
133
+ ```
134
+
135
+ ## Dynamic number of lines
136
+
137
+ Pass `resize=true` to dynamically change number of lines according to content.
138
+
139
+ ```jsx example
140
+ const [value, setValue] = useState()
141
+
142
+ return (
143
+ <TextInput
144
+ placeholder='write here'
145
+ value={value}
146
+ resize
147
+ onChangeText={setValue}
148
+ />
149
+ )
150
+ ```
151
+
152
+ ## Sandbox
153
+
154
+ <Sandbox
155
+ Component={TextInput}
156
+ propsJsonSchema={TextInputPropsJsonSchema}
157
+ extraParams={{
158
+ icon: { showIconSelect: true },
159
+ secondaryIcon: { showIconSelect: true }
160
+ }}
161
+ props={{
162
+ onIconPress: () => alert('"onIconPress" event on "TextInput" component'),
163
+ }}
164
+ block
165
+ />
@@ -0,0 +1,111 @@
1
+ // ----- CONFIG: $UI.TextInput
2
+ $this = merge({
3
+ textColor: var(--TextInput-text-color),
4
+ borderWidth: 1,
5
+ heights: {
6
+ s: 2u,
7
+ m: 2.5u,
8
+ l: 3u
9
+ },
10
+ fontSizes: {
11
+ s: 1.75u,
12
+ m: 1.75u,
13
+ l: 2u
14
+ }
15
+ paddings: {
16
+ s: 0.5u,
17
+ m: 0.75u,
18
+ l: 1u
19
+ }
20
+ }, $UI.TextInput, true)
21
+
22
+ $this.caretColor = $this.textColor
23
+
24
+ // ----- COMPONENT
25
+
26
+ $inputBg = var(--color-bg-main-strong)
27
+ $inputBorderColor = var(--color-border-main)
28
+ $inputBgDisabled = var(--color-bg-main-subtle)
29
+ $inputBorderWidth = 1px
30
+
31
+ .input-input
32
+ margin 0 // important for safari
33
+ flex 1 // important for multiline
34
+ // padding-top, padding-bottom is important
35
+ // for android because it has invisible paddings
36
+ // on the web is important for textarea (override default paddings)
37
+ padding-top 0
38
+ padding-bottom 0
39
+ padding-left 1u
40
+ padding-right @padding-left
41
+ color: $this.textColor
42
+ background-color $inputBg
43
+ border-width $inputBorderWidth
44
+ border-style solid
45
+ border-color var(--color-border-main)
46
+ min-width 8u
47
+ radius()
48
+ fontFamily('normal')
49
+
50
+ +web()
51
+ outline 0
52
+ caret-color: $this.caretColor ? $this.caretColor : this.textColor
53
+
54
+ &.s
55
+ padding-top $this.paddings.s
56
+ padding-bottom $this.paddings.s
57
+ font-size $this.fontSizes.s
58
+ line-height $this.heights.s
59
+
60
+ &.m
61
+ padding-top $this.paddings.m
62
+ padding-bottom $this.paddings.m
63
+ font-size $this.fontSizes.m
64
+ line-height $this.heights.m
65
+
66
+ &.l
67
+ padding-top $this.paddings.l
68
+ padding-bottom $this.paddings.l
69
+ font-size $this.fontSizes.l
70
+ line-height $this.heights.l
71
+
72
+ &.disabled
73
+ background-color $inputBgDisabled
74
+
75
+ +web()
76
+ cursor default
77
+
78
+ &.focused
79
+ border-color var(--color-border-primary)
80
+
81
+ &.error
82
+ border-color var(--color-border-error)
83
+
84
+ for side in (left right)
85
+ &.icon-{side}
86
+ &.l
87
+ padding-{side} 5u
88
+
89
+ &.m
90
+ padding-{side} 4u
91
+
92
+ &.s
93
+ padding-{side} 4u
94
+
95
+ .input-icon
96
+ position absolute
97
+ top 0
98
+ bottom 0
99
+ justify-content center
100
+ z-index 1
101
+
102
+ &.left
103
+ left 1u
104
+
105
+ &.right
106
+ right 1u
107
+
108
+ // ----- JS EXPORTS
109
+
110
+ :export
111
+ config: $this
package/index.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ import { type ReactNode, type RefObject } from 'react';
5
+ import { type StyleProp, type TextStyle, type ViewStyle, type TextInputProps } from 'react-native';
6
+ declare const _default: import("react").ComponentType<UITextInputProps>;
7
+ export default _default;
8
+ export declare const _PropsJsonSchema: {};
9
+ export interface UITextInputProps extends Omit<TextInputProps, 'placeholder' | 'style'> {
10
+ /** Ref to access the underlying input */
11
+ ref?: RefObject<any>;
12
+ /** Custom styles for the wrapper element */
13
+ style?: StyleProp<ViewStyle>;
14
+ /** Custom styles for the input element */
15
+ inputStyle?: StyleProp<TextStyle>;
16
+ /** Custom styles for the primary icon */
17
+ iconStyle?: StyleProp<TextStyle>;
18
+ /** Custom styles for the secondary icon */
19
+ secondaryIconStyle?: StyleProp<TextStyle>;
20
+ /** Placeholder text */
21
+ placeholder?: string | number;
22
+ /** Test identifier */
23
+ testID?: string;
24
+ /** Input value @default '' */
25
+ value?: string;
26
+ /** Size preset @default 'm' */
27
+ size?: 'l' | 'm' | 's';
28
+ /** Disable input interactions @default false */
29
+ disabled?: boolean;
30
+ /** Render a non-editable value @default false */
31
+ readonly?: boolean;
32
+ /** Enable dynamic height based on content @default false */
33
+ resize?: boolean;
34
+ /** Number of lines to display @default 1 */
35
+ numberOfLines?: number;
36
+ /** Primary icon component */
37
+ icon?: any;
38
+ /** Position of the primary icon @default 'left' */
39
+ iconPosition?: 'left' | 'right';
40
+ /** Secondary icon component */
41
+ secondaryIcon?: any;
42
+ /** Primary icon press handler */
43
+ onIconPress?: () => void;
44
+ /** Secondary icon press handler */
45
+ onSecondaryIconPress?: () => void;
46
+ /** Focus event handler */
47
+ onFocus?: (...args: any[]) => void;
48
+ /** Blur event handler */
49
+ onBlur?: (...args: any[]) => void;
50
+ /** Change text handler */
51
+ onChangeText?: (...args: any[]) => void;
52
+ /** Custom wrapper renderer @private */
53
+ _renderWrapper?: (options: {
54
+ style?: StyleProp<ViewStyle>;
55
+ }, children: ReactNode) => ReactNode;
56
+ /** Error state flag @private */
57
+ _hasError?: boolean;
58
+ }
@@ -0,0 +1,2 @@
1
+ .icon
2
+ color var(--color-text-primary)
package/index.tsx ADDED
@@ -0,0 +1,263 @@
1
+ import {
2
+ useState,
3
+ useMemo,
4
+ useRef,
5
+ type ReactNode,
6
+ type RefObject
7
+ } from 'react'
8
+ import {
9
+ TextInput as RNTextInput,
10
+ Platform,
11
+ type StyleProp,
12
+ type TextStyle,
13
+ type ViewStyle,
14
+ type TextInputProps
15
+ } from 'react-native'
16
+ import { pug, observer, useIsomorphicLayoutEffect } from 'startupjs'
17
+ import { themed, useColors } from '@startupjs-ui/core'
18
+ import Div from '@startupjs-ui/div'
19
+ import Icon from '@startupjs-ui/icon'
20
+ import Span from '@startupjs-ui/span'
21
+ import STYLES from './index.cssx.styl'
22
+
23
+ const {
24
+ config: {
25
+ caretColor,
26
+ heights,
27
+ paddings
28
+ }
29
+ } = STYLES
30
+
31
+ const IS_WEB = Platform.OS === 'web'
32
+ const IS_ANDROID = Platform.OS === 'android'
33
+ const ICON_SIZES = {
34
+ s: 'm',
35
+ m: 'm',
36
+ l: 'l'
37
+ }
38
+
39
+ export default observer(themed('TextInput', TextInput))
40
+
41
+ export const _PropsJsonSchema = {/* TextInputProps */}
42
+
43
+ export interface UITextInputProps extends Omit<TextInputProps, 'placeholder' | 'style'> {
44
+ /** Ref to access the underlying input */
45
+ ref?: RefObject<any>
46
+ /** Custom styles for the wrapper element */
47
+ style?: StyleProp<ViewStyle>
48
+ /** Custom styles for the input element */
49
+ inputStyle?: StyleProp<TextStyle>
50
+ /** Custom styles for the primary icon */
51
+ iconStyle?: StyleProp<TextStyle>
52
+ /** Custom styles for the secondary icon */
53
+ secondaryIconStyle?: StyleProp<TextStyle>
54
+ /** Placeholder text */
55
+ placeholder?: string | number
56
+ /** Test identifier */
57
+ testID?: string
58
+ /** Input value @default '' */
59
+ value?: string
60
+ /** Size preset @default 'm' */
61
+ size?: 'l' | 'm' | 's'
62
+ /** Disable input interactions @default false */
63
+ disabled?: boolean
64
+ /** Render a non-editable value @default false */
65
+ readonly?: boolean
66
+ /** Enable dynamic height based on content @default false */
67
+ resize?: boolean
68
+ /** Number of lines to display @default 1 */
69
+ numberOfLines?: number
70
+ /** Primary icon component */
71
+ icon?: any
72
+ /** Position of the primary icon @default 'left' */
73
+ iconPosition?: 'left' | 'right'
74
+ /** Secondary icon component */
75
+ secondaryIcon?: any
76
+ /** Primary icon press handler */
77
+ onIconPress?: () => void
78
+ /** Secondary icon press handler */
79
+ onSecondaryIconPress?: () => void
80
+ /** Focus event handler */
81
+ onFocus?: (...args: any[]) => void
82
+ /** Blur event handler */
83
+ onBlur?: (...args: any[]) => void
84
+ /** Change text handler */
85
+ onChangeText?: (...args: any[]) => void
86
+ /** Custom wrapper renderer @private */
87
+ _renderWrapper?: (options: { style?: StyleProp<ViewStyle> }, children: ReactNode) => ReactNode
88
+ /** Error state flag @private */
89
+ _hasError?: boolean
90
+ }
91
+
92
+ function TextInput ({
93
+ ref,
94
+ style,
95
+ placeholder,
96
+ value = '',
97
+ size = 'm',
98
+ disabled = false,
99
+ readonly = false,
100
+ resize = false,
101
+ numberOfLines = 1,
102
+ iconPosition = 'left',
103
+ icon,
104
+ secondaryIcon,
105
+ onFocus,
106
+ onBlur,
107
+ onIconPress,
108
+ onSecondaryIconPress,
109
+ _renderWrapper,
110
+ _hasError,
111
+ ...props
112
+ }: UITextInputProps): ReactNode {
113
+ const [focused, setFocused] = useState(false)
114
+ const [currentNumberOfLines, setCurrentNumberOfLines] = useState(numberOfLines)
115
+ const fallbackRef = useRef<any>(null)
116
+ const inputRef = ref ?? fallbackRef
117
+
118
+ const getColor = useColors()
119
+
120
+ function handleFocus (...args: any[]) {
121
+ onFocus && onFocus(...args)
122
+ setFocused(true)
123
+ }
124
+ function handleBlur (...args: any[]) {
125
+ onBlur && onBlur(...args)
126
+ setFocused(false)
127
+ }
128
+
129
+ if (!_renderWrapper) {
130
+ _renderWrapper = ({ style }: { style?: StyleProp<ViewStyle> }, children: ReactNode): ReactNode => pug`
131
+ Div(style=style)= children
132
+ `
133
+ }
134
+
135
+ useIsomorphicLayoutEffect(() => {
136
+ if (readonly || !resize) return
137
+ const numberOfLinesInValue = value.split('\n').length
138
+ if (numberOfLinesInValue >= numberOfLines) {
139
+ setCurrentNumberOfLines(numberOfLinesInValue)
140
+ }
141
+ }, [value, resize, numberOfLines, readonly])
142
+
143
+ if (IS_WEB) {
144
+ // repeat mobile behaviour on the web
145
+ // TODO
146
+ // test mobile device behaviour
147
+
148
+ // eslint-disable-next-line react-hooks/rules-of-hooks
149
+ useIsomorphicLayoutEffect(() => {
150
+ if (readonly) return
151
+ if (focused && disabled) {
152
+ inputRef.current?.blur()
153
+ setFocused(false)
154
+ }
155
+ }, [disabled, focused, readonly])
156
+ // fix minWidth on web
157
+ // ref: https://stackoverflow.com/a/29990524/1930491
158
+ // eslint-disable-next-line react-hooks/rules-of-hooks
159
+ useIsomorphicLayoutEffect(() => {
160
+ if (readonly) return
161
+ // TODO: looks like it's not available anymore on new versions of react-native-web
162
+ inputRef.current?.setNativeProps?.({ size: '1' })
163
+ }, [readonly])
164
+ }
165
+
166
+ // useDidUpdate(() => {
167
+ // if (readonly) return
168
+ // if (numberOfLines !== currentNumberOfLines) {
169
+ // setCurrentNumberOfLines(numberOfLines)
170
+ // }
171
+ // }, [numberOfLines, currentNumberOfLines, readonly])
172
+
173
+ const multiline = useMemo(() => {
174
+ return resize || numberOfLines > 1
175
+ }, [resize, numberOfLines])
176
+
177
+ const fullHeight = useMemo(() => {
178
+ return currentNumberOfLines * (heights[size] as number) + (paddings[size] as number) * 2
179
+ }, [currentNumberOfLines, size])
180
+
181
+ function onLayoutIcon (e: any) {
182
+ if (IS_WEB) {
183
+ e.nativeEvent.target.childNodes[0].tabIndex = -1
184
+ e.nativeEvent.target.childNodes[0].childNodes[0].tabIndex = -1
185
+ }
186
+ }
187
+
188
+ const inputExtraProps: Record<string, any> = {}
189
+ if (IS_ANDROID && multiline) inputExtraProps.textAlignVertical = 'top'
190
+
191
+ const inputStyleName = [
192
+ size,
193
+ {
194
+ disabled,
195
+ focused,
196
+ [`icon-${iconPosition}`]: !!icon,
197
+ [`icon-${getOppositePosition(iconPosition)}`]: !!secondaryIcon,
198
+ error: _hasError
199
+ }
200
+ ]
201
+
202
+ if (readonly) {
203
+ return pug`
204
+ Span= value
205
+ `
206
+ }
207
+
208
+ return _renderWrapper({
209
+ style: [style]
210
+ }, pug`
211
+ RNTextInput.input-input(
212
+ part=['input', {
213
+ inputIconLeft: icon && iconPosition === 'left',
214
+ inputIconRight: icon && iconPosition === 'right'
215
+ }]
216
+ ref=inputRef
217
+ style={ minHeight: fullHeight }
218
+ styleName=inputStyleName
219
+ selectionColor=caretColor
220
+ placeholder=placeholder
221
+ placeholderTextColor=getColor('text-placeholder')
222
+ value=value
223
+ disabled=IS_WEB ? disabled : undefined
224
+ editable=IS_WEB ? undefined : !disabled
225
+ multiline=multiline
226
+ selectTextOnFocus=false
227
+ onFocus=handleFocus
228
+ onBlur=handleBlur
229
+ ...props
230
+ ...inputExtraProps
231
+ )
232
+ if icon
233
+ Div.input-icon(
234
+ focusable=false
235
+ onLayout=onLayoutIcon
236
+ styleName=[size, iconPosition]
237
+ onPress=disabled ? undefined : onIconPress
238
+ pointerEvents=onIconPress ? undefined : 'none'
239
+ )
240
+ Icon(
241
+ part='icon'
242
+ icon=icon
243
+ size=ICON_SIZES[size]
244
+ )
245
+ if secondaryIcon
246
+ Div.input-icon(
247
+ focusable=false
248
+ onLayout=onLayoutIcon
249
+ styleName=[size, getOppositePosition(iconPosition)]
250
+ onPress=disabled ? undefined : onSecondaryIconPress
251
+ pointerEvents=onSecondaryIconPress ? undefined : 'none'
252
+ )
253
+ Icon(
254
+ part='secondaryIcon'
255
+ icon=secondaryIcon
256
+ size=ICON_SIZES[size]
257
+ )
258
+ `)
259
+ }
260
+
261
+ function getOppositePosition (position: 'left' | 'right') {
262
+ return position === 'left' ? 'right' : 'left'
263
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@startupjs-ui/text-input",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@startupjs-ui/core": "^0.1.3",
12
+ "@startupjs-ui/div": "^0.1.3",
13
+ "@startupjs-ui/icon": "^0.1.3",
14
+ "@startupjs-ui/span": "^0.1.3"
15
+ },
16
+ "peerDependencies": {
17
+ "react": "*",
18
+ "react-native": "*",
19
+ "startupjs": "*"
20
+ },
21
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
22
+ }