silvery 0.0.1 → 0.4.0
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/LICENSE +21 -0
- package/README.md +70 -11
- package/bin/silvery.ts +258 -0
- package/package.json +158 -5
- package/src/index.ts +71 -0
- package/src/runtime.ts +4 -0
- package/src/theme.ts +4 -0
- package/src/ui/animation.ts +2 -0
- package/src/ui/ansi.ts +2 -0
- package/src/ui/cli.ts +2 -0
- package/src/ui/display.ts +2 -0
- package/src/ui/image.ts +2 -0
- package/src/ui/input.ts +2 -0
- package/src/ui/progress.ts +2 -0
- package/src/ui/react.ts +2 -0
- package/src/ui/utils.ts +2 -0
- package/src/ui/wrappers.ts +2 -0
- package/src/ui.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bjørn Stabell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,19 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Silvery
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Polished Terminal UIs in React.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Ink-compatible React renderer for terminals — same `Box`, `Text`, `useInput` API you know. Plus everything you wish Ink had.
|
|
6
|
+
|
|
7
|
+
> **Note:** Under active development. APIs may change. Feedback welcome.
|
|
8
|
+
|
|
9
|
+
```console
|
|
10
|
+
$ npm install silvery react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useState } from "react"
|
|
15
|
+
import { render, Box, Text, useInput } from "silvery"
|
|
16
|
+
|
|
17
|
+
function Counter() {
|
|
18
|
+
const [count, setCount] = useState(0)
|
|
19
|
+
useInput((input) => {
|
|
20
|
+
if (input === "j") setCount((c) => c + 1)
|
|
21
|
+
})
|
|
22
|
+
return (
|
|
23
|
+
<Box borderStyle="round" padding={1}>
|
|
24
|
+
<Text>Count: {count}</Text>
|
|
25
|
+
</Box>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await render(<Counter />).run()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Familiar
|
|
33
|
+
|
|
34
|
+
- **React 18 + 19** — hooks, refs, effects, suspense — all works
|
|
35
|
+
- **Flexbox layout** — `Box` with `flexDirection`, `padding`, `gap`, `flexGrow`, just like Ink
|
|
36
|
+
- **Ink/Chalk compatible** — same component model, `@silvery/ink` compatibility layer for migration
|
|
37
|
+
|
|
38
|
+
### Better
|
|
39
|
+
|
|
40
|
+
- **Smaller install** — ~177 KB gzipped all included (Ink 6 pulls 16MB into node_modules)
|
|
41
|
+
- **Pure TypeScript, zero native deps** — no WASM, no build steps — works on Alpine, CI, Docker, everywhere
|
|
42
|
+
- **Incremental rendering** — per-node dirty tracking, [~100x faster interactive updates](tests/perf/render.bench.ts)
|
|
43
|
+
- **Responsive layout** — `useContentRect()` returns actual dimensions synchronously during render
|
|
44
|
+
- **Dynamic scrollback** — renders (and re-renders!) into the terminal's scroll history, not just alternate screen
|
|
45
|
+
- **Scrollable containers** — `overflow="scroll"` with automatic measurement and clipping
|
|
46
|
+
- **Theme system** — 38 palettes, semantic design/color tokens (`$primary`, `$error`), auto-detects terminal colors
|
|
47
|
+
- **30+ components** — TextInput, TextArea, SelectList, VirtualList, Table, Tabs, CommandPalette, ModalDialog, Toast, and more
|
|
48
|
+
- **Focus system** — scoped focus, arrow-key directional nav, click-to-focus
|
|
49
|
+
- **Extremely composable** — use as just a renderer (`render`), add a runtime (`run`), or build full apps (`createApp`). Mix with any React state library (useState, Zustand, Jotai, Redux). Swap terminal backends (real TTY, headless, xterm.js emulator) for testing. Embed silvery components in existing CLIs. Use the layout engine standalone. Render to terminal, or (experimental) Canvas, or DOM
|
|
50
|
+
- **Most complete terminal protocol support** — 100+ escape sequences, all auto-negotiated: 12 OSC (hyperlinks, clipboard, palette, text sizing, semantic prompts, notifications), 35+ CSI (cursor, mouse modes, paste, focus, sync output, device queries), 50+ SGR (6 underline styles, underline colors, truecolor, 256-color), full Kitty keyboard (5 flags), full SGR mouse (any-event, drag, wheel)
|
|
6
51
|
|
|
7
52
|
## Packages
|
|
8
53
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
54
|
+
| Package | Description |
|
|
55
|
+
| --------------- | ------------------------------------------------------------------------------------------ |
|
|
56
|
+
| `silvery` | Components, hooks, renderer — the one package you need |
|
|
57
|
+
| `@silvery/test` | Testing utilities and locators |
|
|
58
|
+
| `@silvery/ink` | Ink compatibility layer |
|
|
59
|
+
| `@silvery/tea` | Optional [TEA](https://guide.elm-lang.org/architecture/) state management for complex apps |
|
|
60
|
+
|
|
61
|
+
## Ecosystem
|
|
62
|
+
|
|
63
|
+
| Project | What |
|
|
64
|
+
| ------------------------------------------ | ------------------------------------------------------------- |
|
|
65
|
+
| [Termless](https://termless.dev) | Headless terminal testing — like Playwright for terminal apps |
|
|
66
|
+
| [Flexily](https://beorn.github.io/flexily) | Pure JS flexbox layout engine (Yoga-compatible, zero WASM) |
|
|
67
|
+
| [Loggily](https://beorn.github.io/loggily) | Debug + structured logging + tracing |
|
|
68
|
+
|
|
69
|
+
## Coming
|
|
70
|
+
|
|
71
|
+
- **Renderers** — Canvas 2D, Web DOM (experimental today, production later)
|
|
72
|
+
- **Frameworks** — Svelte, Solid.js, Vue adapters
|
|
73
|
+
- **@silvery/tea** — Structured state management with commands, keybindings, effects-as-data
|
|
74
|
+
|
|
75
|
+
**Runtimes:** Bun >= 1.0 and Node.js >= 18. CLI (`silvery` command) requires Bun.
|
|
17
76
|
|
|
18
77
|
## License
|
|
19
78
|
|
package/bin/silvery.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* silvery CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* silvery example — list all available examples
|
|
7
|
+
* silvery example <name> — run an example by name (fuzzy match)
|
|
8
|
+
* silvery example --list — list all available examples
|
|
9
|
+
* silvery --help — show usage help
|
|
10
|
+
*
|
|
11
|
+
* Designed for: bunx silvery example <name>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// ANSI helpers (no deps — must work before anything is imported)
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
const RESET = "\x1b[0m"
|
|
19
|
+
const BOLD = "\x1b[1m"
|
|
20
|
+
const DIM = "\x1b[2m"
|
|
21
|
+
const RED = "\x1b[31m"
|
|
22
|
+
const GREEN = "\x1b[32m"
|
|
23
|
+
const YELLOW = "\x1b[33m"
|
|
24
|
+
const BLUE = "\x1b[34m"
|
|
25
|
+
const MAGENTA = "\x1b[35m"
|
|
26
|
+
const CYAN = "\x1b[36m"
|
|
27
|
+
const WHITE = "\x1b[37m"
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Types
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
interface Example {
|
|
34
|
+
name: string
|
|
35
|
+
file: string
|
|
36
|
+
description: string
|
|
37
|
+
category: string
|
|
38
|
+
features?: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// =============================================================================
|
|
42
|
+
// Auto-Discovery (mirrors examples/cli.ts)
|
|
43
|
+
// =============================================================================
|
|
44
|
+
|
|
45
|
+
const CATEGORY_DIRS = ["layout", "interactive", "runtime", "inline", "kitty"] as const
|
|
46
|
+
|
|
47
|
+
const CATEGORY_DISPLAY: Record<string, string> = {
|
|
48
|
+
kitty: "Kitty Protocol",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const CATEGORY_ORDER: Record<string, number> = {
|
|
52
|
+
Layout: 0,
|
|
53
|
+
Interactive: 1,
|
|
54
|
+
Runtime: 2,
|
|
55
|
+
Inline: 3,
|
|
56
|
+
"Kitty Protocol": 4,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CATEGORY_COLOR: Record<string, string> = {
|
|
60
|
+
Layout: MAGENTA,
|
|
61
|
+
Interactive: CYAN,
|
|
62
|
+
Runtime: GREEN,
|
|
63
|
+
Inline: YELLOW,
|
|
64
|
+
"Kitty Protocol": BLUE,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function discoverExamples(): Promise<Example[]> {
|
|
68
|
+
const { resolve } = await import("node:path")
|
|
69
|
+
const examplesDir = resolve(new URL(".", import.meta.url).pathname, "../examples")
|
|
70
|
+
const results: Example[] = []
|
|
71
|
+
|
|
72
|
+
for (const dir of CATEGORY_DIRS) {
|
|
73
|
+
const category = CATEGORY_DISPLAY[dir] ?? dir.charAt(0).toUpperCase() + dir.slice(1)
|
|
74
|
+
const glob = new Bun.Glob("*.tsx")
|
|
75
|
+
const dirPath = resolve(examplesDir, dir)
|
|
76
|
+
|
|
77
|
+
for (const file of glob.scanSync({ cwd: dirPath })) {
|
|
78
|
+
if (file.startsWith("_")) continue
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const mod = await import(resolve(dirPath, file))
|
|
82
|
+
if (!mod.meta?.name) continue
|
|
83
|
+
|
|
84
|
+
results.push({
|
|
85
|
+
name: mod.meta.name,
|
|
86
|
+
description: mod.meta.description ?? "",
|
|
87
|
+
file: resolve(dirPath, file),
|
|
88
|
+
category,
|
|
89
|
+
features: mod.meta.features,
|
|
90
|
+
})
|
|
91
|
+
} catch {
|
|
92
|
+
// Skip files that fail to import
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
results.sort((a, b) => {
|
|
98
|
+
const catDiff = (CATEGORY_ORDER[a.category] ?? 99) - (CATEGORY_ORDER[b.category] ?? 99)
|
|
99
|
+
if (catDiff !== 0) return catDiff
|
|
100
|
+
return a.name.localeCompare(b.name)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
return results
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// =============================================================================
|
|
107
|
+
// Formatting
|
|
108
|
+
// =============================================================================
|
|
109
|
+
|
|
110
|
+
function printHelp(): void {
|
|
111
|
+
console.log(`
|
|
112
|
+
${BOLD}${YELLOW}silvery${RESET} — React framework for modern terminal UIs
|
|
113
|
+
|
|
114
|
+
${BOLD}Usage:${RESET}
|
|
115
|
+
silvery example List all available examples
|
|
116
|
+
silvery example ${DIM}<name>${RESET} Run an example by name (fuzzy match)
|
|
117
|
+
silvery example --list List all available examples
|
|
118
|
+
silvery --help Show this help
|
|
119
|
+
silvery --version Show version
|
|
120
|
+
|
|
121
|
+
${BOLD}Examples:${RESET}
|
|
122
|
+
bunx silvery example todo Run the Todo App example
|
|
123
|
+
bunx silvery example kanban Run the Kanban Board example
|
|
124
|
+
bunx silvery example dashboard Run the Dashboard example
|
|
125
|
+
|
|
126
|
+
${DIM}Documentation: https://silvery.dev${RESET}
|
|
127
|
+
`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printExampleList(examples: Example[]): void {
|
|
131
|
+
console.log(`\n${BOLD}${YELLOW} silvery${RESET}${DIM} examples${RESET}\n`)
|
|
132
|
+
|
|
133
|
+
let currentCategory = ""
|
|
134
|
+
|
|
135
|
+
for (const ex of examples) {
|
|
136
|
+
if (ex.category !== currentCategory) {
|
|
137
|
+
currentCategory = ex.category
|
|
138
|
+
const color = CATEGORY_COLOR[currentCategory] ?? WHITE
|
|
139
|
+
console.log(` ${color}${BOLD}${currentCategory}${RESET}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const nameStr = `${BOLD}${WHITE}${ex.name}${RESET}`
|
|
143
|
+
const descStr = `${DIM}${ex.description}${RESET}`
|
|
144
|
+
console.log(` ${nameStr} ${descStr}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`\n ${DIM}Run an example: bunx silvery example <name>${RESET}\n`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function findExample(examples: Example[], query: string): Example | undefined {
|
|
151
|
+
const q = query.toLowerCase()
|
|
152
|
+
|
|
153
|
+
const exact = examples.find((ex) => ex.name.toLowerCase() === q)
|
|
154
|
+
if (exact) return exact
|
|
155
|
+
|
|
156
|
+
const prefix = examples.find((ex) => ex.name.toLowerCase().startsWith(q))
|
|
157
|
+
if (prefix) return prefix
|
|
158
|
+
|
|
159
|
+
const substring = examples.find((ex) => ex.name.toLowerCase().includes(q))
|
|
160
|
+
if (substring) return substring
|
|
161
|
+
|
|
162
|
+
return undefined
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function printNoMatch(query: string, examples: Example[]): void {
|
|
166
|
+
console.error(`\n${RED}${BOLD}Error:${RESET} No example matching "${query}"\n`)
|
|
167
|
+
console.error(`${DIM}Available examples:${RESET}`)
|
|
168
|
+
|
|
169
|
+
for (const ex of examples) {
|
|
170
|
+
console.error(` ${WHITE}${ex.name}${RESET}`)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
console.error(`\n${DIM}Run ${BOLD}bunx silvery example${RESET}${DIM} for full list with descriptions.${RESET}\n`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// =============================================================================
|
|
177
|
+
// Subcommands
|
|
178
|
+
// =============================================================================
|
|
179
|
+
|
|
180
|
+
async function exampleCommand(args: string[]): Promise<void> {
|
|
181
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
182
|
+
printHelp()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const examples = await discoverExamples()
|
|
187
|
+
|
|
188
|
+
if (args.length === 0 || args[0] === "--list" || args[0] === "-l") {
|
|
189
|
+
printExampleList(examples)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const query = args.filter((a) => !a.startsWith("--")).join(" ")
|
|
194
|
+
if (!query) {
|
|
195
|
+
printExampleList(examples)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const match = findExample(examples, query)
|
|
200
|
+
if (!match) {
|
|
201
|
+
printNoMatch(query, examples)
|
|
202
|
+
process.exit(1)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log(`${DIM}Running ${BOLD}${match.name}${RESET}${DIM}...${RESET}\n`)
|
|
206
|
+
|
|
207
|
+
const proc = Bun.spawn(["bun", "run", match.file], {
|
|
208
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
209
|
+
})
|
|
210
|
+
const exitCode = await proc.exited
|
|
211
|
+
process.exit(exitCode)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// Main
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
async function main(): Promise<void> {
|
|
219
|
+
const args = process.argv.slice(2)
|
|
220
|
+
|
|
221
|
+
// Top-level flags
|
|
222
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
223
|
+
printHelp()
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
228
|
+
try {
|
|
229
|
+
const { resolve } = await import("node:path")
|
|
230
|
+
const pkgPath = resolve(new URL(".", import.meta.url).pathname, "../package.json")
|
|
231
|
+
const pkg = await Bun.file(pkgPath).json()
|
|
232
|
+
console.log(`silvery ${pkg.version}`)
|
|
233
|
+
} catch {
|
|
234
|
+
console.log("silvery (version unknown)")
|
|
235
|
+
}
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const subcommand = args[0]
|
|
240
|
+
const subArgs = args.slice(1)
|
|
241
|
+
|
|
242
|
+
switch (subcommand) {
|
|
243
|
+
case "example":
|
|
244
|
+
case "examples":
|
|
245
|
+
case "demo":
|
|
246
|
+
await exampleCommand(subArgs)
|
|
247
|
+
break
|
|
248
|
+
default:
|
|
249
|
+
// If user types "silvery todo", treat it as "silvery example todo"
|
|
250
|
+
await exampleCommand(args)
|
|
251
|
+
break
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main().catch((err) => {
|
|
256
|
+
console.error(err)
|
|
257
|
+
process.exit(1)
|
|
258
|
+
})
|
package/package.json
CHANGED
|
@@ -1,16 +1,169 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silvery",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "React terminal UI renderer for complex interactive apps — layout-aware rendering, flexbox, scrolling, and incremental updates",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ansi",
|
|
7
|
+
"chalk",
|
|
8
|
+
"cli",
|
|
9
|
+
"flexbox",
|
|
10
|
+
"ink",
|
|
11
|
+
"ink-alternative",
|
|
12
|
+
"react",
|
|
13
|
+
"react-renderer",
|
|
14
|
+
"react-terminal",
|
|
15
|
+
"rendering",
|
|
16
|
+
"terminal",
|
|
17
|
+
"terminal-renderer",
|
|
18
|
+
"terminal-ui",
|
|
19
|
+
"text-ui",
|
|
20
|
+
"tui",
|
|
21
|
+
"ui"
|
|
22
|
+
],
|
|
23
|
+
"homepage": "https://silvery.dev",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/beorn/silvery/issues"
|
|
26
|
+
},
|
|
6
27
|
"license": "MIT",
|
|
7
28
|
"author": "Bjørn Stabell <bjorn@stabell.org>",
|
|
8
|
-
"homepage": "https://silvery.dev",
|
|
9
29
|
"repository": {
|
|
10
30
|
"type": "git",
|
|
11
31
|
"url": "https://github.com/beorn/silvery.git"
|
|
12
32
|
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"silvery": "./bin/silvery.ts"
|
|
35
|
+
},
|
|
36
|
+
"workspaces": [
|
|
37
|
+
"packages/*"
|
|
38
|
+
],
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"dist",
|
|
42
|
+
"bin"
|
|
43
|
+
],
|
|
13
44
|
"type": "module",
|
|
14
45
|
"main": "src/index.ts",
|
|
15
|
-
"
|
|
46
|
+
"types": "src/index.ts",
|
|
47
|
+
"exports": {
|
|
48
|
+
".": {
|
|
49
|
+
"types": "./src/index.ts",
|
|
50
|
+
"import": "./src/index.ts"
|
|
51
|
+
},
|
|
52
|
+
"./runtime": {
|
|
53
|
+
"types": "./src/runtime.ts",
|
|
54
|
+
"import": "./src/runtime.ts"
|
|
55
|
+
},
|
|
56
|
+
"./theme": {
|
|
57
|
+
"types": "./src/theme.ts",
|
|
58
|
+
"import": "./src/theme.ts"
|
|
59
|
+
},
|
|
60
|
+
"./ui": {
|
|
61
|
+
"types": "./src/ui.ts",
|
|
62
|
+
"import": "./src/ui.ts"
|
|
63
|
+
},
|
|
64
|
+
"./ui/cli": {
|
|
65
|
+
"types": "./src/ui/cli.ts",
|
|
66
|
+
"import": "./src/ui/cli.ts"
|
|
67
|
+
},
|
|
68
|
+
"./ui/react": {
|
|
69
|
+
"types": "./src/ui/react.ts",
|
|
70
|
+
"import": "./src/ui/react.ts"
|
|
71
|
+
},
|
|
72
|
+
"./ui/progress": {
|
|
73
|
+
"types": "./src/ui/progress.ts",
|
|
74
|
+
"import": "./src/ui/progress.ts"
|
|
75
|
+
},
|
|
76
|
+
"./ui/wrappers": {
|
|
77
|
+
"types": "./src/ui/wrappers.ts",
|
|
78
|
+
"import": "./src/ui/wrappers.ts"
|
|
79
|
+
},
|
|
80
|
+
"./ui/ansi": {
|
|
81
|
+
"types": "./src/ui/ansi.ts",
|
|
82
|
+
"import": "./src/ui/ansi.ts"
|
|
83
|
+
},
|
|
84
|
+
"./ui/display": {
|
|
85
|
+
"types": "./src/ui/display.ts",
|
|
86
|
+
"import": "./src/ui/display.ts"
|
|
87
|
+
},
|
|
88
|
+
"./ui/input": {
|
|
89
|
+
"types": "./src/ui/input.ts",
|
|
90
|
+
"import": "./src/ui/input.ts"
|
|
91
|
+
},
|
|
92
|
+
"./ui/animation": {
|
|
93
|
+
"types": "./src/ui/animation.ts",
|
|
94
|
+
"import": "./src/ui/animation.ts"
|
|
95
|
+
},
|
|
96
|
+
"./ui/image": {
|
|
97
|
+
"types": "./src/ui/image.ts",
|
|
98
|
+
"import": "./src/ui/image.ts"
|
|
99
|
+
},
|
|
100
|
+
"./ui/utils": {
|
|
101
|
+
"types": "./src/ui/utils.ts",
|
|
102
|
+
"import": "./src/ui/utils.ts"
|
|
103
|
+
},
|
|
104
|
+
"./ink": {
|
|
105
|
+
"types": "./packages/ink/src/ink.ts",
|
|
106
|
+
"import": "./packages/ink/src/ink.ts"
|
|
107
|
+
},
|
|
108
|
+
"./chalk": {
|
|
109
|
+
"types": "./packages/ink/src/chalk.ts",
|
|
110
|
+
"import": "./packages/ink/src/chalk.ts"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"publishConfig": {
|
|
114
|
+
"access": "public"
|
|
115
|
+
},
|
|
116
|
+
"scripts": {
|
|
117
|
+
"build": "bun run scripts/build.ts",
|
|
118
|
+
"test": "bunx --bun vitest run",
|
|
119
|
+
"test:fast": "bunx --bun vitest run --reporter=dot",
|
|
120
|
+
"typecheck": "tsc --noEmit",
|
|
121
|
+
"lint": "oxlint . && oxfmt --check .",
|
|
122
|
+
"fix": "oxlint --fix . && oxfmt --write .",
|
|
123
|
+
"docs:dev": "vitepress dev docs",
|
|
124
|
+
"docs:build": "vitepress build docs",
|
|
125
|
+
"docs:preview": "vitepress preview docs",
|
|
126
|
+
"changeset": "changeset",
|
|
127
|
+
"version": "changeset version",
|
|
128
|
+
"release": "changeset publish",
|
|
129
|
+
"theme": "bun packages/theme/src/cli.ts",
|
|
130
|
+
"demo": "bun examples/cli.ts",
|
|
131
|
+
"compat": "bun packages/ink/scripts/compat-check.ts",
|
|
132
|
+
"compat:ink": "bun packages/ink/scripts/compat-check.ts ink",
|
|
133
|
+
"compat:chalk": "bun packages/ink/scripts/compat-check.ts chalk"
|
|
134
|
+
},
|
|
135
|
+
"dependencies": {
|
|
136
|
+
"chalk": "^5.6.2",
|
|
137
|
+
"loggily": "github:beorn/loggily",
|
|
138
|
+
"react-reconciler": "^0.33.0",
|
|
139
|
+
"slice-ansi": "^8.0.0",
|
|
140
|
+
"string-width": "^8.2.0",
|
|
141
|
+
"zustand": "^5.0.11"
|
|
142
|
+
},
|
|
143
|
+
"devDependencies": {
|
|
144
|
+
"@changesets/cli": "^2.27.0",
|
|
145
|
+
"@types/bun": "^1.1.0",
|
|
146
|
+
"@types/react": "^19.0.0",
|
|
147
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
148
|
+
"@xterm/xterm": "^6.0.0",
|
|
149
|
+
"fast-check": "^4.6.0",
|
|
150
|
+
"mitata": "^1.0.34",
|
|
151
|
+
"oxfmt": "^0.36.0",
|
|
152
|
+
"oxlint": "^1.51.0",
|
|
153
|
+
"playwright": "^1.58.2",
|
|
154
|
+
"react": "^19.0.0",
|
|
155
|
+
"typescript": "^5.5.0",
|
|
156
|
+
"vimonkey": "^0.2.0",
|
|
157
|
+
"vitepress": "^1.5.0",
|
|
158
|
+
"vitepress-plugin-llms": "^1.0.0",
|
|
159
|
+
"vitest": "^4.0.18",
|
|
160
|
+
"yoga-wasm-web": "^0.3.3"
|
|
161
|
+
},
|
|
162
|
+
"peerDependencies": {
|
|
163
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
164
|
+
},
|
|
165
|
+
"engines": {
|
|
166
|
+
"bun": ">=1.0",
|
|
167
|
+
"node": ">=18"
|
|
168
|
+
}
|
|
16
169
|
}
|
package/src/index.ts
CHANGED
|
@@ -1 +1,72 @@
|
|
|
1
|
+
// silvery — multi-target rendering framework for React
|
|
2
|
+
// Re-exports from @silvery/* packages
|
|
3
|
+
|
|
1
4
|
export const VERSION = "0.0.1"
|
|
5
|
+
|
|
6
|
+
// Re-export everything from @silvery/ag-react — local `render` below shadows the re-exported one
|
|
7
|
+
export * from "@silvery/ag-react"
|
|
8
|
+
|
|
9
|
+
import type { ReactElement } from "react"
|
|
10
|
+
import { render as reactRender, type RenderOptions, type TermDef } from "@silvery/ag-react"
|
|
11
|
+
import type { Term } from "@silvery/ag-react"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a React element to the terminal.
|
|
15
|
+
*
|
|
16
|
+
* Zero-ceremony entry point — auto-detects the terminal and starts an
|
|
17
|
+
* interactive app when stdin is a TTY. No need to create a Term first.
|
|
18
|
+
*
|
|
19
|
+
* @example Hello World (2 lines)
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { render, Text } from "silvery"
|
|
22
|
+
* await render(<Text>Hello!</Text>).run()
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Interactive counter
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { useState } from "react"
|
|
28
|
+
* import { render, Box, Text, useInput } from "silvery"
|
|
29
|
+
*
|
|
30
|
+
* function Counter() {
|
|
31
|
+
* const [count, setCount] = useState(0)
|
|
32
|
+
* useInput((input) => {
|
|
33
|
+
* if (input === "j") setCount((c) => c + 1)
|
|
34
|
+
* })
|
|
35
|
+
* return (
|
|
36
|
+
* <Box borderStyle="round" padding={1}>
|
|
37
|
+
* <Text>Count: {count}</Text>
|
|
38
|
+
* </Box>
|
|
39
|
+
* )
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* await render(<Counter />).run()
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Static render (explicit)
|
|
46
|
+
* ```tsx
|
|
47
|
+
* import { render, Text } from "silvery"
|
|
48
|
+
* await render(<Text>Report</Text>, { width: 120 })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* When called without a Term or TermDef:
|
|
52
|
+
* - **TTY detected** → interactive mode (stdin + stdout auto-wired)
|
|
53
|
+
* - **No TTY** → static mode (renders once and returns)
|
|
54
|
+
*
|
|
55
|
+
* Pass a Term or TermDef explicitly to override auto-detection.
|
|
56
|
+
*/
|
|
57
|
+
export function render(
|
|
58
|
+
element: ReactElement,
|
|
59
|
+
termOrDef?: Term | TermDef,
|
|
60
|
+
options?: RenderOptions,
|
|
61
|
+
): ReturnType<typeof reactRender> {
|
|
62
|
+
// When no term/def is provided and we're in a TTY, auto-wire stdin/stdout
|
|
63
|
+
// so the app runs interactively (useInput works, app stays alive until exit).
|
|
64
|
+
if (!termOrDef && process.stdin?.isTTY && process.stdout?.isTTY) {
|
|
65
|
+
const ttyDef: TermDef = {
|
|
66
|
+
stdin: process.stdin,
|
|
67
|
+
stdout: process.stdout,
|
|
68
|
+
}
|
|
69
|
+
return reactRender(element, ttyDef, options)
|
|
70
|
+
}
|
|
71
|
+
return reactRender(element, termOrDef, options)
|
|
72
|
+
}
|
package/src/runtime.ts
ADDED
package/src/theme.ts
ADDED
package/src/ui/ansi.ts
ADDED
package/src/ui/cli.ts
ADDED
package/src/ui/image.ts
ADDED
package/src/ui/input.ts
ADDED
package/src/ui/react.ts
ADDED
package/src/ui/utils.ts
ADDED
package/src/ui.ts
ADDED