@solidrt/components 0.0.2 → 0.0.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/README.md +48 -0
- package/package.json +2 -2
- package/src/index.ts +3 -1
- package/src/{SafeArea.tsx → safe-area.tsx} +0 -1
- package/src/text-input.tsx +154 -0
- package/src/theme.ts +41 -0
package/README.md
CHANGED
|
@@ -36,6 +36,54 @@ function App() {
|
|
|
36
36
|
| `minimum` | `number` | `0` | Minimum padding applied even if the safe area inset is smaller |
|
|
37
37
|
| `children` | `any` | - | Content to render inside the safe area |
|
|
38
38
|
|
|
39
|
+
### TextInput
|
|
40
|
+
|
|
41
|
+
Single-line text input. Tapping/clicking the field focuses it; the OS-level text input (and on-screen keyboard, where applicable) is activated automatically while focused. Printable text arrives via the platform `textInput` event (post-IME commit). V1 supports caret-at-end editing only: no selection, no mid-string cursor movement.
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
import { TextInput } from "@solidrt/components"
|
|
45
|
+
import { createSignal } from "@solidjs/signals"
|
|
46
|
+
|
|
47
|
+
function NameField() {
|
|
48
|
+
let [name, setName] = createSignal("")
|
|
49
|
+
return (
|
|
50
|
+
<TextInput
|
|
51
|
+
value={name()}
|
|
52
|
+
onInput={setName}
|
|
53
|
+
onSubmit={(v) => console.log("submitted", v)}
|
|
54
|
+
placeholder="Your name"
|
|
55
|
+
width={240}
|
|
56
|
+
/>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Props**
|
|
62
|
+
|
|
63
|
+
| Prop | Type | Default | Description |
|
|
64
|
+
| ------------------ | -------------------------- | ------------------ | ------------------------------------------------------------ |
|
|
65
|
+
| `value` | `string` | - | Controlled value. If omitted, the component is uncontrolled. |
|
|
66
|
+
| `defaultValue` | `string` | `""` | Initial value for uncontrolled use |
|
|
67
|
+
| `onInput` | `(value: string) => void` | - | Fires on every change |
|
|
68
|
+
| `onSubmit` | `(value: string) => void` | - | Fires on Enter |
|
|
69
|
+
| `onFocus` | `() => void` | - | Fires when the field gains focus |
|
|
70
|
+
| `onBlur` | `() => void` | - | Fires when the field loses focus |
|
|
71
|
+
| `placeholder` | `string` | - | Shown when value is empty and the field is not focused |
|
|
72
|
+
| `maxLength` | `number` | - | Truncates input to this length |
|
|
73
|
+
| `disabled` | `boolean` | `false` | Ignores pointer and key events when true |
|
|
74
|
+
| `autoFocus` | `boolean` | `false` | Focuses on mount |
|
|
75
|
+
| `fontSize` | `number` | `14` | Text size |
|
|
76
|
+
| `color` | `string` | `"black"` | Text color |
|
|
77
|
+
| `placeholderColor` | `string` | `"rgba(0,0,0,.4)"` | Placeholder color |
|
|
78
|
+
| `background` | `string` | `"white"` | Background fill color |
|
|
79
|
+
| `borderColor` | `string` | `"rgba(0,0,0,.2)"` | Border stroke color |
|
|
80
|
+
| `borderWidth` | `number` | `1` | Border stroke width |
|
|
81
|
+
| `borderRadius` | `number` | `4` | Corner radius |
|
|
82
|
+
| `caretColor` | `string` | same as `color` | Caret color |
|
|
83
|
+
| `padding` | `number` | `8` | Horizontal padding |
|
|
84
|
+
| `width` | `number \| "auto" \| "N%"` | - | Field width |
|
|
85
|
+
| `height` | `number` | `32` | Field height |
|
|
86
|
+
|
|
39
87
|
## License
|
|
40
88
|
|
|
41
89
|
MIT. Copyright (c) 2026 Antoine van Wel.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidrt/components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Antoine van Wel",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,6 @@
|
|
|
15
15
|
],
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@solidjs/signals": "2.0.0-beta.13",
|
|
18
|
-
"@solidrt/core": "0.0.
|
|
18
|
+
"@solidrt/core": "0.0.3"
|
|
19
19
|
}
|
|
20
20
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createSignal, onCleanup } from "@solidjs/signals"
|
|
2
|
+
import { measureText, setFocus } from "@solidrt/core"
|
|
3
|
+
import { theme } from "./theme"
|
|
4
|
+
|
|
5
|
+
type Dimension = number | "auto" | `${number}%`
|
|
6
|
+
|
|
7
|
+
export interface TextInputProps {
|
|
8
|
+
value?: string
|
|
9
|
+
defaultValue?: string
|
|
10
|
+
onInput?: (value: string) => void
|
|
11
|
+
onSubmit?: (value: string) => void
|
|
12
|
+
onFocus?: () => void
|
|
13
|
+
onBlur?: () => void
|
|
14
|
+
|
|
15
|
+
placeholder?: string
|
|
16
|
+
maxLength?: number
|
|
17
|
+
disabled?: boolean
|
|
18
|
+
autoFocus?: boolean
|
|
19
|
+
|
|
20
|
+
width?: Dimension
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// V1: single-line, caret-at-end only, no selection, no mid-string editing.
|
|
24
|
+
// Printable text arrives via onTextInput (post-IME commit). onKeyDown handles
|
|
25
|
+
// Backspace, Enter, Escape. Outside-click-to-blur is the caller's job.
|
|
26
|
+
export function TextInput(props: TextInputProps) {
|
|
27
|
+
let [internalValue, setInternalValue] = createSignal(props.defaultValue ?? "")
|
|
28
|
+
let [focused, setFocused] = createSignal(false)
|
|
29
|
+
let [caretOn, setCaretOn] = createSignal(true)
|
|
30
|
+
|
|
31
|
+
let node: { id: number } | undefined
|
|
32
|
+
let blinkId: any = null
|
|
33
|
+
|
|
34
|
+
let value = () => props.value ?? internalValue()
|
|
35
|
+
|
|
36
|
+
let commit = (next: string) => {
|
|
37
|
+
if (props.maxLength != null && next.length > props.maxLength) {
|
|
38
|
+
next = next.slice(0, props.maxLength)
|
|
39
|
+
}
|
|
40
|
+
if (props.value == null) setInternalValue(next)
|
|
41
|
+
props.onInput?.(next)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let handlePointerDown = () => {
|
|
45
|
+
if (props.disabled) return
|
|
46
|
+
if (node) setFocus(node.id)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let handleFocus = () => {
|
|
50
|
+
setFocused(true)
|
|
51
|
+
setCaretOn(true)
|
|
52
|
+
if (blinkId == null) {
|
|
53
|
+
blinkId = setInterval(() => setCaretOn((v) => !v), 500)
|
|
54
|
+
}
|
|
55
|
+
props.onFocus?.()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let handleBlur = () => {
|
|
59
|
+
setFocused(false)
|
|
60
|
+
if (blinkId != null) {
|
|
61
|
+
clearInterval(blinkId)
|
|
62
|
+
blinkId = null
|
|
63
|
+
}
|
|
64
|
+
props.onBlur?.()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let handleKeyDown = (e: any) => {
|
|
68
|
+
if (props.disabled) return
|
|
69
|
+
if (e.key === "Backspace") {
|
|
70
|
+
let v = value()
|
|
71
|
+
if (v.length > 0) commit(v.slice(0, -1))
|
|
72
|
+
setCaretOn(true)
|
|
73
|
+
} else if (e.key === "Return" || e.key === "Enter") {
|
|
74
|
+
props.onSubmit?.(value())
|
|
75
|
+
} else if (e.key === "Escape") {
|
|
76
|
+
if (node) setFocus(null)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let handleTextInput = (e: any) => {
|
|
81
|
+
if (props.disabled) return
|
|
82
|
+
commit(value() + (e.text ?? ""))
|
|
83
|
+
setCaretOn(true)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onCleanup(() => {
|
|
87
|
+
if (blinkId != null) clearInterval(blinkId)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
let showPlaceholder = () => !focused() && value().length === 0 && (props.placeholder ?? "").length > 0
|
|
91
|
+
let displayText = () => (showPlaceholder() ? (props.placeholder ?? "") : value())
|
|
92
|
+
let displayColor = () => (showPlaceholder() ? theme.color.textMuted : theme.color.text)
|
|
93
|
+
|
|
94
|
+
// V1: viewport width derived from numeric props.width minus padding.
|
|
95
|
+
// For "auto" / "%" widths, fall back to 0 (no scroll, caret may overflow).
|
|
96
|
+
let viewportWidth = () => (typeof props.width === "number" ? props.width - 2 * theme.spacing.md : 0)
|
|
97
|
+
let caretWidth = () => (focused() && !showPlaceholder() ? 1 : 0)
|
|
98
|
+
let scrollX = () => {
|
|
99
|
+
if (showPlaceholder()) return 0
|
|
100
|
+
let tw = measureText(value(), { fontSize: theme.text.body.size }).width
|
|
101
|
+
let vw = viewportWidth()
|
|
102
|
+
if (vw <= 0) return 0
|
|
103
|
+
return Math.max(0, tw + caretWidth() - vw)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<view
|
|
108
|
+
ref={(n: { id: number }) => {
|
|
109
|
+
node = n
|
|
110
|
+
if (props.autoFocus) setFocus(n.id)
|
|
111
|
+
}}
|
|
112
|
+
flexDirection="row"
|
|
113
|
+
alignItems="center"
|
|
114
|
+
width={props.width}
|
|
115
|
+
paddingLeft={theme.spacing.md}
|
|
116
|
+
paddingRight={theme.spacing.md}
|
|
117
|
+
paddingTop={theme.spacing.sm}
|
|
118
|
+
paddingBottom={theme.spacing.sm}
|
|
119
|
+
onPointerDown={handlePointerDown}
|
|
120
|
+
onFocus={handleFocus}
|
|
121
|
+
onBlur={handleBlur}
|
|
122
|
+
onKeyDown={handleKeyDown}
|
|
123
|
+
onTextInput={handleTextInput}
|
|
124
|
+
>
|
|
125
|
+
<d-rect color={theme.color.surface} radius={theme.radius.sm} />
|
|
126
|
+
<d-rect
|
|
127
|
+
drawStyle="stroke"
|
|
128
|
+
color={theme.color.border}
|
|
129
|
+
strokeWidth={theme.borderWidth.sm}
|
|
130
|
+
radius={theme.radius.sm}
|
|
131
|
+
/>
|
|
132
|
+
<view
|
|
133
|
+
flex={1}
|
|
134
|
+
flexDirection="row"
|
|
135
|
+
alignItems="center"
|
|
136
|
+
overflow="hidden"
|
|
137
|
+
scrollX={scrollX()}
|
|
138
|
+
>
|
|
139
|
+
<text
|
|
140
|
+
fontSize={theme.text.body.size}
|
|
141
|
+
lineHeight={theme.text.body.lineHeight}
|
|
142
|
+
color={displayColor()}
|
|
143
|
+
maxLines={1}
|
|
144
|
+
flexShrink={0}
|
|
145
|
+
>
|
|
146
|
+
{displayText()}
|
|
147
|
+
</text>
|
|
148
|
+
{focused() && caretOn() && !showPlaceholder() ? (
|
|
149
|
+
<rect color={theme.color.text} w={1} h={theme.text.body.size} flexShrink={0} />
|
|
150
|
+
) : null}
|
|
151
|
+
</view>
|
|
152
|
+
</view>
|
|
153
|
+
)
|
|
154
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type TextStyle = {
|
|
2
|
+
size: number
|
|
3
|
+
lineHeight: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type Theme = {
|
|
7
|
+
text: { body: TextStyle }
|
|
8
|
+
color: {
|
|
9
|
+
text: string
|
|
10
|
+
textMuted: string
|
|
11
|
+
surface: string
|
|
12
|
+
border: string
|
|
13
|
+
}
|
|
14
|
+
spacing: { sm: number; md: number }
|
|
15
|
+
radius: { sm: number }
|
|
16
|
+
borderWidth: { sm: number }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export let theme: Theme = {
|
|
20
|
+
text: {
|
|
21
|
+
body: { size: 14, lineHeight: 1.5 },
|
|
22
|
+
},
|
|
23
|
+
color: {
|
|
24
|
+
text: "#333",
|
|
25
|
+
textMuted: "rgba(0,0,0,0.4)",
|
|
26
|
+
surface: "#ccc",
|
|
27
|
+
border: "rgba(0,0,0,0.2)",
|
|
28
|
+
},
|
|
29
|
+
spacing: { sm: 4, md: 8 },
|
|
30
|
+
radius: { sm: 4 },
|
|
31
|
+
borderWidth: { sm: 1 },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ThemePartial = { [K in keyof Theme]?: Partial<Theme[K]> }
|
|
35
|
+
|
|
36
|
+
export function setTheme(partial: ThemePartial) {
|
|
37
|
+
for (let key in partial) {
|
|
38
|
+
let k = key as keyof Theme
|
|
39
|
+
Object.assign(theme[k], partial[k])
|
|
40
|
+
}
|
|
41
|
+
}
|