@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 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 flexDirection="column">
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.2",
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.2"
18
+ "@solidrt/core": "0.0.4"
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
+ }