@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 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.2",
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.2"
18
+ "@solidrt/core": "0.0.3"
19
19
  }
20
20
  }
package/src/index.ts CHANGED
@@ -1 +1,3 @@
1
- export { SafeArea } from "./SafeArea"
1
+ export { SafeArea } from "./safe-area"
2
+ export { TextInput, type TextInputProps } from "./text-input"
3
+ export { theme, setTheme, type Theme } from "./theme"
@@ -20,7 +20,6 @@ export function SafeArea(props: {
20
20
  has(edge) ? Math.max(insets()[edge], props.minimum ?? 0) : 0
21
21
  return (
22
22
  <view
23
- overflow="hidden"
24
23
  flex={1}
25
24
  flexDirection="column"
26
25
  marginTop={pad("top")}
@@ -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
+ }