@dealdeploy/skl 0.1.7 → 0.1.8
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/.agents/skills/opentui/SKILL.md +198 -0
- package/.agents/skills/opentui/references/animation/REFERENCE.md +431 -0
- package/.agents/skills/opentui/references/components/REFERENCE.md +143 -0
- package/.agents/skills/opentui/references/components/code-diff.md +496 -0
- package/.agents/skills/opentui/references/components/containers.md +412 -0
- package/.agents/skills/opentui/references/components/inputs.md +531 -0
- package/.agents/skills/opentui/references/components/text-display.md +384 -0
- package/.agents/skills/opentui/references/core/REFERENCE.md +145 -0
- package/.agents/skills/opentui/references/core/api.md +506 -0
- package/.agents/skills/opentui/references/core/configuration.md +166 -0
- package/.agents/skills/opentui/references/core/gotchas.md +393 -0
- package/.agents/skills/opentui/references/core/patterns.md +448 -0
- package/.agents/skills/opentui/references/keyboard/REFERENCE.md +511 -0
- package/.agents/skills/opentui/references/layout/REFERENCE.md +337 -0
- package/.agents/skills/opentui/references/layout/patterns.md +444 -0
- package/.agents/skills/opentui/references/react/REFERENCE.md +174 -0
- package/.agents/skills/opentui/references/react/api.md +435 -0
- package/.agents/skills/opentui/references/react/configuration.md +301 -0
- package/.agents/skills/opentui/references/react/gotchas.md +443 -0
- package/.agents/skills/opentui/references/react/patterns.md +501 -0
- package/.agents/skills/opentui/references/solid/REFERENCE.md +201 -0
- package/.agents/skills/opentui/references/solid/api.md +543 -0
- package/.agents/skills/opentui/references/solid/configuration.md +315 -0
- package/.agents/skills/opentui/references/solid/gotchas.md +415 -0
- package/.agents/skills/opentui/references/solid/patterns.md +558 -0
- package/.agents/skills/opentui/references/testing/REFERENCE.md +614 -0
- package/.claude/settings.local.json +11 -0
- package/.claude/skills/opentui/SKILL.md +198 -0
- package/.claude/skills/opentui/references/animation/REFERENCE.md +431 -0
- package/.claude/skills/opentui/references/components/REFERENCE.md +143 -0
- package/.claude/skills/opentui/references/components/code-diff.md +496 -0
- package/.claude/skills/opentui/references/components/containers.md +412 -0
- package/.claude/skills/opentui/references/components/inputs.md +531 -0
- package/.claude/skills/opentui/references/components/text-display.md +384 -0
- package/.claude/skills/opentui/references/core/REFERENCE.md +145 -0
- package/.claude/skills/opentui/references/core/api.md +506 -0
- package/.claude/skills/opentui/references/core/configuration.md +166 -0
- package/.claude/skills/opentui/references/core/gotchas.md +393 -0
- package/.claude/skills/opentui/references/core/patterns.md +448 -0
- package/.claude/skills/opentui/references/keyboard/REFERENCE.md +511 -0
- package/.claude/skills/opentui/references/layout/REFERENCE.md +337 -0
- package/.claude/skills/opentui/references/layout/patterns.md +444 -0
- package/.claude/skills/opentui/references/react/REFERENCE.md +174 -0
- package/.claude/skills/opentui/references/react/api.md +435 -0
- package/.claude/skills/opentui/references/react/configuration.md +301 -0
- package/.claude/skills/opentui/references/react/gotchas.md +443 -0
- package/.claude/skills/opentui/references/react/patterns.md +501 -0
- package/.claude/skills/opentui/references/solid/REFERENCE.md +201 -0
- package/.claude/skills/opentui/references/solid/api.md +543 -0
- package/.claude/skills/opentui/references/solid/configuration.md +315 -0
- package/.claude/skills/opentui/references/solid/gotchas.md +415 -0
- package/.claude/skills/opentui/references/solid/patterns.md +558 -0
- package/.claude/skills/opentui/references/testing/REFERENCE.md +614 -0
- package/bun.lock +0 -1
- package/index.ts +163 -38
- package/package.json +1 -1
- package/update.ts +87 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
# React Patterns
|
|
2
|
+
|
|
3
|
+
## State Management
|
|
4
|
+
|
|
5
|
+
### Local State with useState
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { useState } from "react"
|
|
9
|
+
|
|
10
|
+
function Counter() {
|
|
11
|
+
const [count, setCount] = useState(0)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<box flexDirection="row" gap={2}>
|
|
15
|
+
<text>Count: {count}</text>
|
|
16
|
+
<box border onMouseDown={() => setCount(c => c - 1)}>
|
|
17
|
+
<text>-</text>
|
|
18
|
+
</box>
|
|
19
|
+
<box border onMouseDown={() => setCount(c => c + 1)}>
|
|
20
|
+
<text>+</text>
|
|
21
|
+
</box>
|
|
22
|
+
</box>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Complex State with useReducer
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { useReducer } from "react"
|
|
31
|
+
|
|
32
|
+
type State = {
|
|
33
|
+
items: string[]
|
|
34
|
+
selectedIndex: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Action =
|
|
38
|
+
| { type: "ADD_ITEM"; item: string }
|
|
39
|
+
| { type: "REMOVE_ITEM"; index: number }
|
|
40
|
+
| { type: "SELECT"; index: number }
|
|
41
|
+
|
|
42
|
+
function reducer(state: State, action: Action): State {
|
|
43
|
+
switch (action.type) {
|
|
44
|
+
case "ADD_ITEM":
|
|
45
|
+
return { ...state, items: [...state.items, action.item] }
|
|
46
|
+
case "REMOVE_ITEM":
|
|
47
|
+
return {
|
|
48
|
+
...state,
|
|
49
|
+
items: state.items.filter((_, i) => i !== action.index),
|
|
50
|
+
}
|
|
51
|
+
case "SELECT":
|
|
52
|
+
return { ...state, selectedIndex: action.index }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ItemList() {
|
|
57
|
+
const [state, dispatch] = useReducer(reducer, {
|
|
58
|
+
items: [],
|
|
59
|
+
selectedIndex: 0,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Use state and dispatch...
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Context for Global State
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { createContext, useContext, useState, ReactNode } from "react"
|
|
70
|
+
|
|
71
|
+
type Theme = "dark" | "light"
|
|
72
|
+
|
|
73
|
+
const ThemeContext = createContext<{
|
|
74
|
+
theme: Theme
|
|
75
|
+
setTheme: (theme: Theme) => void
|
|
76
|
+
} | null>(null)
|
|
77
|
+
|
|
78
|
+
function ThemeProvider({ children }: { children: ReactNode }) {
|
|
79
|
+
const [theme, setTheme] = useState<Theme>("dark")
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ThemeContext.Provider value={{ theme, setTheme }}>
|
|
83
|
+
{children}
|
|
84
|
+
</ThemeContext.Provider>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function useTheme() {
|
|
89
|
+
const context = useContext(ThemeContext)
|
|
90
|
+
if (!context) throw new Error("useTheme must be used within ThemeProvider")
|
|
91
|
+
return context
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Usage
|
|
95
|
+
function App() {
|
|
96
|
+
return (
|
|
97
|
+
<ThemeProvider>
|
|
98
|
+
<ThemedBox />
|
|
99
|
+
</ThemeProvider>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ThemedBox() {
|
|
104
|
+
const { theme } = useTheme()
|
|
105
|
+
return (
|
|
106
|
+
<box backgroundColor={theme === "dark" ? "#1a1a2e" : "#f0f0f0"}>
|
|
107
|
+
<text fg={theme === "dark" ? "#fff" : "#000"}>
|
|
108
|
+
Current theme: {theme}
|
|
109
|
+
</text>
|
|
110
|
+
</box>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Focus Management
|
|
116
|
+
|
|
117
|
+
### Focus State
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import { useState } from "react"
|
|
121
|
+
import { useKeyboard } from "@opentui/react"
|
|
122
|
+
|
|
123
|
+
function FocusableForm() {
|
|
124
|
+
const [focusIndex, setFocusIndex] = useState(0)
|
|
125
|
+
const fields = ["name", "email", "message"]
|
|
126
|
+
|
|
127
|
+
useKeyboard((key) => {
|
|
128
|
+
if (key.name === "tab") {
|
|
129
|
+
setFocusIndex(i => (i + 1) % fields.length)
|
|
130
|
+
}
|
|
131
|
+
if (key.shift && key.name === "tab") {
|
|
132
|
+
setFocusIndex(i => (i - 1 + fields.length) % fields.length)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<box flexDirection="column" gap={1}>
|
|
138
|
+
{fields.map((field, i) => (
|
|
139
|
+
<input
|
|
140
|
+
key={field}
|
|
141
|
+
placeholder={`Enter ${field}...`}
|
|
142
|
+
focused={i === focusIndex}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</box>
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Ref-based Focus
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { useRef, useEffect } from "react"
|
|
154
|
+
|
|
155
|
+
function AutoFocusInput() {
|
|
156
|
+
const inputRef = useRef<any>(null)
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
// Focus on mount
|
|
160
|
+
inputRef.current?.focus()
|
|
161
|
+
}, [])
|
|
162
|
+
|
|
163
|
+
return <input ref={inputRef} placeholder="Auto-focused" />
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Keyboard Navigation
|
|
168
|
+
|
|
169
|
+
### Global Shortcuts
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { useKeyboard, useRenderer } from "@opentui/react"
|
|
173
|
+
|
|
174
|
+
function App() {
|
|
175
|
+
const renderer = useRenderer()
|
|
176
|
+
|
|
177
|
+
useKeyboard((key) => {
|
|
178
|
+
// Quit on Escape or Ctrl+C - use renderer.destroy(), never process.exit()
|
|
179
|
+
if (key.name === "escape" || (key.ctrl && key.name === "c")) {
|
|
180
|
+
renderer.destroy()
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Toggle help on ?
|
|
185
|
+
if (key.name === "?" || (key.shift && key.name === "/")) {
|
|
186
|
+
setShowHelp(h => !h)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Vim-style navigation
|
|
190
|
+
if (key.name === "j") moveDown()
|
|
191
|
+
if (key.name === "k") moveUp()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return <box>{/* ... */}</box>
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Component-level Shortcuts
|
|
199
|
+
|
|
200
|
+
```tsx
|
|
201
|
+
function Editor() {
|
|
202
|
+
const [mode, setMode] = useState<"normal" | "insert">("normal")
|
|
203
|
+
|
|
204
|
+
useKeyboard((key) => {
|
|
205
|
+
if (mode === "normal") {
|
|
206
|
+
if (key.name === "i") setMode("insert")
|
|
207
|
+
if (key.name === "escape") setMode("normal")
|
|
208
|
+
} else {
|
|
209
|
+
if (key.name === "escape") setMode("normal")
|
|
210
|
+
// Handle text input in insert mode
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<box>
|
|
216
|
+
<text>Mode: {mode}</text>
|
|
217
|
+
<textarea focused={mode === "insert"} />
|
|
218
|
+
</box>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Form Handling
|
|
224
|
+
|
|
225
|
+
### Controlled Inputs
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { useState } from "react"
|
|
229
|
+
|
|
230
|
+
function LoginForm() {
|
|
231
|
+
const [username, setUsername] = useState("")
|
|
232
|
+
const [password, setPassword] = useState("")
|
|
233
|
+
|
|
234
|
+
const handleSubmit = () => {
|
|
235
|
+
console.log("Login:", { username, password })
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<box flexDirection="column" gap={1} padding={2} border>
|
|
240
|
+
<text>Login</text>
|
|
241
|
+
|
|
242
|
+
<box flexDirection="row" gap={1}>
|
|
243
|
+
<text>Username:</text>
|
|
244
|
+
<input
|
|
245
|
+
value={username}
|
|
246
|
+
onChange={setUsername}
|
|
247
|
+
width={20}
|
|
248
|
+
/>
|
|
249
|
+
</box>
|
|
250
|
+
|
|
251
|
+
<box flexDirection="row" gap={1}>
|
|
252
|
+
<text>Password:</text>
|
|
253
|
+
<input
|
|
254
|
+
value={password}
|
|
255
|
+
onChange={setPassword}
|
|
256
|
+
width={20}
|
|
257
|
+
/>
|
|
258
|
+
</box>
|
|
259
|
+
|
|
260
|
+
<box border onMouseDown={handleSubmit}>
|
|
261
|
+
<text>Submit</text>
|
|
262
|
+
</box>
|
|
263
|
+
</box>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Form Validation
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
function ValidatedForm() {
|
|
272
|
+
const [email, setEmail] = useState("")
|
|
273
|
+
const [error, setError] = useState("")
|
|
274
|
+
|
|
275
|
+
const validateEmail = (value: string) => {
|
|
276
|
+
if (!value.includes("@")) {
|
|
277
|
+
setError("Invalid email address")
|
|
278
|
+
} else {
|
|
279
|
+
setError("")
|
|
280
|
+
}
|
|
281
|
+
setEmail(value)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<box flexDirection="column" gap={1}>
|
|
286
|
+
<input
|
|
287
|
+
value={email}
|
|
288
|
+
onChange={validateEmail}
|
|
289
|
+
placeholder="Email"
|
|
290
|
+
/>
|
|
291
|
+
{error && <text fg="red">{error}</text>}
|
|
292
|
+
</box>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Responsive Design
|
|
298
|
+
|
|
299
|
+
### Terminal-size Responsive
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
import { useTerminalDimensions } from "@opentui/react"
|
|
303
|
+
|
|
304
|
+
function ResponsiveLayout() {
|
|
305
|
+
const { width } = useTerminalDimensions()
|
|
306
|
+
|
|
307
|
+
// Stack vertically on narrow terminals
|
|
308
|
+
const isNarrow = width < 80
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<box flexDirection={isNarrow ? "column" : "row"}>
|
|
312
|
+
<box flexGrow={isNarrow ? 0 : 1} height={isNarrow ? 10 : "100%"}>
|
|
313
|
+
<text>Sidebar</text>
|
|
314
|
+
</box>
|
|
315
|
+
<box flexGrow={1}>
|
|
316
|
+
<text>Main Content</text>
|
|
317
|
+
</box>
|
|
318
|
+
</box>
|
|
319
|
+
)
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### Dynamic Layouts
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
function DynamicGrid({ items }: { items: string[] }) {
|
|
327
|
+
const { width } = useTerminalDimensions()
|
|
328
|
+
const columns = Math.max(1, Math.floor(width / 20))
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<box flexDirection="row" flexWrap="wrap">
|
|
332
|
+
{items.map((item, i) => (
|
|
333
|
+
<box key={i} width={`${100 / columns}%`} padding={1}>
|
|
334
|
+
<text>{item}</text>
|
|
335
|
+
</box>
|
|
336
|
+
))}
|
|
337
|
+
</box>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Async Data Loading
|
|
343
|
+
|
|
344
|
+
### Loading States
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
import { useState, useEffect } from "react"
|
|
348
|
+
|
|
349
|
+
function DataDisplay() {
|
|
350
|
+
const [data, setData] = useState<string[] | null>(null)
|
|
351
|
+
const [loading, setLoading] = useState(true)
|
|
352
|
+
const [error, setError] = useState<string | null>(null)
|
|
353
|
+
|
|
354
|
+
useEffect(() => {
|
|
355
|
+
async function load() {
|
|
356
|
+
try {
|
|
357
|
+
const response = await fetch("https://api.example.com/data")
|
|
358
|
+
const json = await response.json()
|
|
359
|
+
setData(json.items)
|
|
360
|
+
} catch (e) {
|
|
361
|
+
setError(e instanceof Error ? e.message : "Unknown error")
|
|
362
|
+
} finally {
|
|
363
|
+
setLoading(false)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
load()
|
|
367
|
+
}, [])
|
|
368
|
+
|
|
369
|
+
if (loading) {
|
|
370
|
+
return <text>Loading...</text>
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (error) {
|
|
374
|
+
return <text fg="red">Error: {error}</text>
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<box flexDirection="column">
|
|
379
|
+
{data?.map((item, i) => (
|
|
380
|
+
<text key={i}>{item}</text>
|
|
381
|
+
))}
|
|
382
|
+
</box>
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Animation Patterns
|
|
388
|
+
|
|
389
|
+
### Simple Animations
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import { useState, useEffect } from "react"
|
|
393
|
+
import { useTimeline } from "@opentui/react"
|
|
394
|
+
|
|
395
|
+
function ProgressBar() {
|
|
396
|
+
const [progress, setProgress] = useState(0)
|
|
397
|
+
|
|
398
|
+
const timeline = useTimeline({ duration: 3000 })
|
|
399
|
+
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
timeline.add(
|
|
402
|
+
{ value: 0 },
|
|
403
|
+
{
|
|
404
|
+
value: 100,
|
|
405
|
+
duration: 3000,
|
|
406
|
+
ease: "linear",
|
|
407
|
+
onUpdate: (anim) => {
|
|
408
|
+
setProgress(Math.round(anim.targets[0].value))
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
}, [])
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
<box flexDirection="column" gap={1}>
|
|
416
|
+
<text>Progress: {progress}%</text>
|
|
417
|
+
<box width={50} height={1} backgroundColor="#333">
|
|
418
|
+
<box
|
|
419
|
+
width={`${progress}%`}
|
|
420
|
+
height={1}
|
|
421
|
+
backgroundColor="#00ff00"
|
|
422
|
+
/>
|
|
423
|
+
</box>
|
|
424
|
+
</box>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Interval-based Updates
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
function Clock() {
|
|
433
|
+
const [time, setTime] = useState(new Date())
|
|
434
|
+
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
const interval = setInterval(() => {
|
|
437
|
+
setTime(new Date())
|
|
438
|
+
}, 1000)
|
|
439
|
+
|
|
440
|
+
return () => clearInterval(interval)
|
|
441
|
+
}, [])
|
|
442
|
+
|
|
443
|
+
return <text>{time.toLocaleTimeString()}</text>
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Component Composition
|
|
448
|
+
|
|
449
|
+
### Render Props
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
function Focusable({
|
|
453
|
+
children
|
|
454
|
+
}: {
|
|
455
|
+
children: (focused: boolean) => React.ReactNode
|
|
456
|
+
}) {
|
|
457
|
+
const [focused, setFocused] = useState(false)
|
|
458
|
+
|
|
459
|
+
return (
|
|
460
|
+
<box
|
|
461
|
+
onMouseDown={() => setFocused(true)}
|
|
462
|
+
onMouseUp={() => setFocused(false)}
|
|
463
|
+
>
|
|
464
|
+
{children(focused)}
|
|
465
|
+
</box>
|
|
466
|
+
)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Usage
|
|
470
|
+
<Focusable>
|
|
471
|
+
{(focused) => (
|
|
472
|
+
<text fg={focused ? "#00ff00" : "#ffffff"}>
|
|
473
|
+
{focused ? "Focused!" : "Click me"}
|
|
474
|
+
</text>
|
|
475
|
+
)}
|
|
476
|
+
</Focusable>
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Higher-Order Components
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
function withBorder<P extends object>(
|
|
483
|
+
Component: React.ComponentType<P>,
|
|
484
|
+
borderStyle: string = "single"
|
|
485
|
+
) {
|
|
486
|
+
return function BorderedComponent(props: P) {
|
|
487
|
+
return (
|
|
488
|
+
<box border borderStyle={borderStyle} padding={1}>
|
|
489
|
+
<Component {...props} />
|
|
490
|
+
</box>
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Usage
|
|
496
|
+
const BorderedText = withBorder(({ content }: { content: string }) => (
|
|
497
|
+
<text>{content}</text>
|
|
498
|
+
))
|
|
499
|
+
|
|
500
|
+
<BorderedText content="Hello!" />
|
|
501
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# OpenTUI Solid (@opentui/solid)
|
|
2
|
+
|
|
3
|
+
A SolidJS reconciler for building terminal user interfaces with fine-grained reactivity. Get optimal performance with Solid's signal-based approach.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
OpenTUI Solid provides:
|
|
8
|
+
- **Custom reconciler**: Solid components render to OpenTUI renderables
|
|
9
|
+
- **JSX intrinsics**: `<text>`, `<box>`, `<input>`, etc.
|
|
10
|
+
- **Hooks**: `useKeyboard`, `useRenderer`, `useTimeline`, etc.
|
|
11
|
+
- **Fine-grained reactivity**: Only what changes re-renders
|
|
12
|
+
- **Portal & Dynamic**: Advanced composition primitives
|
|
13
|
+
|
|
14
|
+
## When to Use Solid
|
|
15
|
+
|
|
16
|
+
Use the Solid reconciler when:
|
|
17
|
+
- You want optimal re-rendering performance
|
|
18
|
+
- You prefer signal-based reactivity
|
|
19
|
+
- You need fine-grained control over updates
|
|
20
|
+
- Building performance-critical applications
|
|
21
|
+
- You already know SolidJS
|
|
22
|
+
|
|
23
|
+
## When NOT to Use Solid
|
|
24
|
+
|
|
25
|
+
| Scenario | Use Instead |
|
|
26
|
+
|----------|-------------|
|
|
27
|
+
| Team knows React, not Solid | `@opentui/react` |
|
|
28
|
+
| Maximum control needed | `@opentui/core` |
|
|
29
|
+
| Smallest bundle size | `@opentui/core` |
|
|
30
|
+
| Building a framework/library | `@opentui/core` |
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bunx create-tui@latest -t solid my-app
|
|
36
|
+
cd my-app && bun install
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The CLI creates the `my-app` directory for you - it must **not already exist**.
|
|
40
|
+
|
|
41
|
+
Options: `--no-git` (skip git init), `--no-install` (skip bun install)
|
|
42
|
+
|
|
43
|
+
**Agent guidance**: Always use autonomous mode with `-t <template>` flag. Never use interactive mode (`bunx create-tui@latest my-app` without `-t`) as it requires user prompts that agents cannot respond to.
|
|
44
|
+
|
|
45
|
+
Or manually:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bun install @opentui/solid @opentui/core solid-js
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { render } from "@opentui/solid"
|
|
53
|
+
import { createSignal } from "solid-js"
|
|
54
|
+
|
|
55
|
+
function App() {
|
|
56
|
+
const [count, setCount] = createSignal(0)
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<box border padding={2}>
|
|
60
|
+
<text>Count: {count()}</text>
|
|
61
|
+
<box
|
|
62
|
+
border
|
|
63
|
+
onMouseDown={() => setCount(c => c + 1)}
|
|
64
|
+
>
|
|
65
|
+
<text>Click me!</text>
|
|
66
|
+
</box>
|
|
67
|
+
</box>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
render(() => <App />)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Core Concepts
|
|
75
|
+
|
|
76
|
+
### Signals
|
|
77
|
+
|
|
78
|
+
Solid uses signals for reactive state:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
import { createSignal, createEffect } from "solid-js"
|
|
82
|
+
|
|
83
|
+
function Counter() {
|
|
84
|
+
const [count, setCount] = createSignal(0)
|
|
85
|
+
|
|
86
|
+
// Effect runs when count changes
|
|
87
|
+
createEffect(() => {
|
|
88
|
+
console.log("Count is now:", count())
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return <text>Count: {count()}</text>
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### JSX Elements
|
|
96
|
+
|
|
97
|
+
Solid maps JSX intrinsic elements to OpenTUI renderables:
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// Note: Some use underscores (Solid convention)
|
|
101
|
+
<text>Hello</text> // TextRenderable
|
|
102
|
+
<box border>Content</box> // BoxRenderable
|
|
103
|
+
<input placeholder="..." /> // InputRenderable
|
|
104
|
+
<select options={[...]} /> // SelectRenderable
|
|
105
|
+
<tab_select /> // TabSelectRenderable (underscore!)
|
|
106
|
+
<ascii_font /> // ASCIIFontRenderable (underscore!)
|
|
107
|
+
<line_number /> // LineNumberRenderable (underscore!)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Text Modifiers
|
|
111
|
+
|
|
112
|
+
Inside `<text>`, use modifier elements:
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
<text>
|
|
116
|
+
<strong>Bold</strong>, <em>italic</em>, and <u>underlined</u>
|
|
117
|
+
<span fg="red">Colored text</span>
|
|
118
|
+
<br />
|
|
119
|
+
New line with <a href="https://example.com">link</a>
|
|
120
|
+
</text>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Available Components
|
|
124
|
+
|
|
125
|
+
### Layout & Display
|
|
126
|
+
- `<text>` - Styled text content
|
|
127
|
+
- `<box>` - Container with borders and layout
|
|
128
|
+
- `<scrollbox>` - Scrollable container
|
|
129
|
+
- `<ascii_font>` - ASCII art text (note underscore)
|
|
130
|
+
|
|
131
|
+
### Input
|
|
132
|
+
- `<input>` - Single-line text input
|
|
133
|
+
- `<textarea>` - Multi-line text input
|
|
134
|
+
- `<select>` - List selection
|
|
135
|
+
- `<tab_select>` - Tab-based selection (note underscore)
|
|
136
|
+
|
|
137
|
+
### Code & Diff
|
|
138
|
+
- `<code>` - Syntax-highlighted code
|
|
139
|
+
- `<line_number>` - Code with line numbers (note underscore)
|
|
140
|
+
- `<diff>` - Unified or split diff viewer
|
|
141
|
+
|
|
142
|
+
### Text Modifiers (inside `<text>`)
|
|
143
|
+
- `<span>` - Inline styled text
|
|
144
|
+
- `<strong>`, `<b>` - Bold
|
|
145
|
+
- `<em>`, `<i>` - Italic
|
|
146
|
+
- `<u>` - Underline
|
|
147
|
+
- `<br>` - Line break
|
|
148
|
+
- `<a>` - Link
|
|
149
|
+
|
|
150
|
+
## Special Components
|
|
151
|
+
|
|
152
|
+
### Portal
|
|
153
|
+
|
|
154
|
+
Render children to a different mount node:
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { Portal } from "@opentui/solid"
|
|
158
|
+
|
|
159
|
+
function Overlay() {
|
|
160
|
+
return (
|
|
161
|
+
<Portal mount={renderer.root}>
|
|
162
|
+
<box position="absolute" left={10} top={5} border>
|
|
163
|
+
<text>Overlay content</text>
|
|
164
|
+
</box>
|
|
165
|
+
</Portal>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Dynamic
|
|
171
|
+
|
|
172
|
+
Render components dynamically:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { Dynamic } from "@opentui/solid"
|
|
176
|
+
|
|
177
|
+
function DynamicInput(props: { multiline: boolean }) {
|
|
178
|
+
return (
|
|
179
|
+
<Dynamic
|
|
180
|
+
component={props.multiline ? "textarea" : "input"}
|
|
181
|
+
placeholder="Enter text..."
|
|
182
|
+
/>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## In This Reference
|
|
188
|
+
|
|
189
|
+
- [Configuration](./configuration.md) - Project setup, tsconfig, bunfig, building
|
|
190
|
+
- [API](./api.md) - Components, hooks, render function
|
|
191
|
+
- [Patterns](./patterns.md) - Signals, stores, control flow, composition
|
|
192
|
+
- [Gotchas](./gotchas.md) - Common issues, debugging, limitations
|
|
193
|
+
|
|
194
|
+
## See Also
|
|
195
|
+
|
|
196
|
+
- [Core](../core/REFERENCE.md) - Underlying imperative API
|
|
197
|
+
- [React](../react/REFERENCE.md) - Alternative declarative approach
|
|
198
|
+
- [Components](../components/REFERENCE.md) - Component reference by category
|
|
199
|
+
- [Layout](../layout/REFERENCE.md) - Flexbox layout system
|
|
200
|
+
- [Keyboard](../keyboard/REFERENCE.md) - Input handling and shortcuts
|
|
201
|
+
- [Testing](../testing/REFERENCE.md) - Test renderer and snapshots
|