@solidrt/components 0.0.2 → 0.0.4
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 +43 -1
- 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
|
@@ -8,6 +8,10 @@ A collection of components for [SolidRT](https://github.com/wellawaretech/solidr
|
|
|
8
8
|
bun add @solidrt/components
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Theming
|
|
12
|
+
|
|
13
|
+
Appearance (colors, spacing, border, font size) is controlled via the shared theme. Call `setTheme` from `@solidrt/components` to override defaults.
|
|
14
|
+
|
|
11
15
|
## Components
|
|
12
16
|
|
|
13
17
|
### SafeArea
|
|
@@ -19,7 +23,7 @@ import { SafeArea } from "@solidrt/components"
|
|
|
19
23
|
|
|
20
24
|
function App() {
|
|
21
25
|
return (
|
|
22
|
-
<window
|
|
26
|
+
<window>
|
|
23
27
|
<SafeArea>
|
|
24
28
|
<text>Content clear of system UI</text>
|
|
25
29
|
</SafeArea>
|
|
@@ -36,6 +40,44 @@ function App() {
|
|
|
36
40
|
| `minimum` | `number` | `0` | Minimum padding applied even if the safe area inset is smaller |
|
|
37
41
|
| `children` | `any` | - | Content to render inside the safe area |
|
|
38
42
|
|
|
43
|
+
### TextInput
|
|
44
|
+
|
|
45
|
+
Single-line text input.
|
|
46
|
+
|
|
47
|
+
```jsx
|
|
48
|
+
import { TextInput } from "@solidrt/components"
|
|
49
|
+
import { createSignal } from "@solidjs/signals"
|
|
50
|
+
|
|
51
|
+
function NameField() {
|
|
52
|
+
let [name, setName] = createSignal("")
|
|
53
|
+
return (
|
|
54
|
+
<TextInput
|
|
55
|
+
value={name()}
|
|
56
|
+
onInput={setName}
|
|
57
|
+
onSubmit={(v) => console.log("submitted", v)}
|
|
58
|
+
placeholder="Your name"
|
|
59
|
+
width={240}
|
|
60
|
+
/>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Props**
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Default | Description |
|
|
68
|
+
| -------------- | -------------------------- | ------- | ------------------------------------------------------------ |
|
|
69
|
+
| `value` | `string` | - | Controlled value. If omitted, the component is uncontrolled. |
|
|
70
|
+
| `defaultValue` | `string` | `""` | Initial value for uncontrolled use |
|
|
71
|
+
| `onInput` | `(value: string) => void` | - | Fires on every change |
|
|
72
|
+
| `onSubmit` | `(value: string) => void` | - | Fires on Enter |
|
|
73
|
+
| `onFocus` | `() => void` | - | Fires when the field gains focus |
|
|
74
|
+
| `onBlur` | `() => void` | - | Fires when the field loses focus |
|
|
75
|
+
| `placeholder` | `string` | - | Shown when value is empty and the field is not focused |
|
|
76
|
+
| `maxLength` | `number` | - | Truncates input to this length |
|
|
77
|
+
| `disabled` | `boolean` | `false` | Ignores pointer and key events when true |
|
|
78
|
+
| `autoFocus` | `boolean` | `false` | Focuses on mount |
|
|
79
|
+
| `width` | `number \| "auto" \| "N%"` | - | Field width |
|
|
80
|
+
|
|
39
81
|
## License
|
|
40
82
|
|
|
41
83
|
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.4",
|
|
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.4"
|
|
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
|
+
}
|