silvery 0.3.0 → 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.
Files changed (88) hide show
  1. package/README.md +41 -145
  2. package/package.json +64 -12
  3. package/src/index.ts +67 -1
  4. package/src/runtime.ts +4 -0
  5. package/src/theme.ts +4 -0
  6. package/src/ui/animation.ts +2 -0
  7. package/src/ui/ansi.ts +2 -0
  8. package/src/ui/cli.ts +2 -0
  9. package/src/ui/display.ts +2 -0
  10. package/src/ui/image.ts +2 -0
  11. package/src/ui/input.ts +2 -0
  12. package/src/ui/progress.ts +2 -0
  13. package/src/ui/react.ts +2 -0
  14. package/src/ui/utils.ts +2 -0
  15. package/src/ui/wrappers.ts +2 -0
  16. package/src/ui.ts +4 -0
  17. package/examples/CLAUDE.md +0 -75
  18. package/examples/_banner.tsx +0 -60
  19. package/examples/cli.ts +0 -228
  20. package/examples/index.md +0 -101
  21. package/examples/inline/inline-nontty.tsx +0 -98
  22. package/examples/inline/inline-progress.tsx +0 -79
  23. package/examples/inline/inline-simple.tsx +0 -63
  24. package/examples/inline/scrollback.tsx +0 -185
  25. package/examples/interactive/_input-debug.tsx +0 -110
  26. package/examples/interactive/_stdin-test.ts +0 -71
  27. package/examples/interactive/_textarea-bare.tsx +0 -45
  28. package/examples/interactive/aichat/components.tsx +0 -468
  29. package/examples/interactive/aichat/index.tsx +0 -207
  30. package/examples/interactive/aichat/script.ts +0 -460
  31. package/examples/interactive/aichat/state.ts +0 -326
  32. package/examples/interactive/aichat/types.ts +0 -19
  33. package/examples/interactive/app-todo.tsx +0 -198
  34. package/examples/interactive/async-data.tsx +0 -208
  35. package/examples/interactive/cli-wizard.tsx +0 -332
  36. package/examples/interactive/clipboard.tsx +0 -183
  37. package/examples/interactive/components.tsx +0 -463
  38. package/examples/interactive/data-explorer.tsx +0 -506
  39. package/examples/interactive/dev-tools.tsx +0 -379
  40. package/examples/interactive/explorer.tsx +0 -747
  41. package/examples/interactive/gallery.tsx +0 -652
  42. package/examples/interactive/inline-bench.tsx +0 -136
  43. package/examples/interactive/kanban.tsx +0 -267
  44. package/examples/interactive/layout-ref.tsx +0 -185
  45. package/examples/interactive/outline.tsx +0 -171
  46. package/examples/interactive/paste-demo.tsx +0 -198
  47. package/examples/interactive/scroll.tsx +0 -77
  48. package/examples/interactive/search-filter.tsx +0 -240
  49. package/examples/interactive/task-list.tsx +0 -279
  50. package/examples/interactive/terminal.tsx +0 -798
  51. package/examples/interactive/textarea.tsx +0 -103
  52. package/examples/interactive/theme.tsx +0 -336
  53. package/examples/interactive/transform.tsx +0 -256
  54. package/examples/interactive/virtual-10k.tsx +0 -413
  55. package/examples/kitty/canvas.tsx +0 -519
  56. package/examples/kitty/generate-samples.ts +0 -236
  57. package/examples/kitty/image-component.tsx +0 -273
  58. package/examples/kitty/images.tsx +0 -604
  59. package/examples/kitty/input.tsx +0 -371
  60. package/examples/kitty/keys.tsx +0 -378
  61. package/examples/kitty/paint.tsx +0 -1017
  62. package/examples/layout/dashboard.tsx +0 -551
  63. package/examples/layout/live-resize.tsx +0 -290
  64. package/examples/layout/overflow.tsx +0 -51
  65. package/examples/playground/README.md +0 -69
  66. package/examples/playground/build.ts +0 -61
  67. package/examples/playground/index.html +0 -420
  68. package/examples/playground/playground-app.tsx +0 -416
  69. package/examples/runtime/elm-counter.tsx +0 -206
  70. package/examples/runtime/hello-runtime.tsx +0 -73
  71. package/examples/runtime/pipe-composition.tsx +0 -184
  72. package/examples/runtime/run-counter.tsx +0 -78
  73. package/examples/runtime/runtime-counter.tsx +0 -197
  74. package/examples/screenshots/generate.tsx +0 -563
  75. package/examples/scrollback-perf.tsx +0 -230
  76. package/examples/viewer.tsx +0 -654
  77. package/examples/web/build.ts +0 -365
  78. package/examples/web/canvas-app.tsx +0 -80
  79. package/examples/web/canvas.html +0 -89
  80. package/examples/web/dom-app.tsx +0 -81
  81. package/examples/web/dom.html +0 -113
  82. package/examples/web/showcase-app.tsx +0 -107
  83. package/examples/web/showcase.html +0 -34
  84. package/examples/web/showcases/index.tsx +0 -56
  85. package/examples/web/viewer-app.tsx +0 -555
  86. package/examples/web/viewer.html +0 -30
  87. package/examples/web/xterm-app.tsx +0 -105
  88. package/examples/web/xterm.html +0 -118
@@ -1,185 +0,0 @@
1
- /**
2
- * Scrollback Mode — REPL
3
- *
4
- * Interactive expression evaluator demonstrating useScrollback + VirtualList virtualized.
5
- * Completed results freeze into terminal scrollback; the active prompt stays at bottom.
6
- *
7
- * Controls:
8
- * Type expression + Enter - Evaluate
9
- * q (when input empty) - Quit
10
- */
11
-
12
- import React, { useState, useCallback } from "react"
13
- import { Box, Text, Divider, VirtualList, useInput, type Key } from "../../src/index.js"
14
- import { run, useExit } from "@silvery/term/runtime"
15
- import { useScrollback } from "../../src/hooks/useScrollback.js"
16
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
17
-
18
- export const meta: ExampleMeta = {
19
- name: "Scrollback",
20
- description: "REPL with useScrollback + VirtualList virtualized for terminal scrollback",
21
- features: ["useScrollback()", "VirtualList virtualized", "inline mode"],
22
- }
23
-
24
- // =============================================================================
25
- // Data
26
- // =============================================================================
27
-
28
- interface Result {
29
- id: number
30
- expr: string
31
- value: string
32
- frozen: boolean
33
- }
34
-
35
- let nextId = 0
36
-
37
- function evaluate(expr: string): string {
38
- try {
39
- // eslint-disable-next-line no-eval
40
- return String(eval(expr))
41
- } catch (e: unknown) {
42
- return `Error: ${e instanceof Error ? e.message : String(e)}`
43
- }
44
- }
45
-
46
- // =============================================================================
47
- // Component
48
- // =============================================================================
49
-
50
- export function Repl() {
51
- const exit = useExit()
52
- const [results, setResults] = useState<Result[]>([])
53
- const [input, setInput] = useState("")
54
- const [cursor, setCursor] = useState(0)
55
-
56
- // Push frozen results to terminal scrollback
57
- const frozenCount = useScrollback(results, {
58
- frozen: (r) => r.frozen,
59
- render: (r) => `$ ${r.expr}\n→ ${r.value}`,
60
- })
61
-
62
- const submit = useCallback(() => {
63
- const expr = input.trim()
64
- if (!expr) return
65
-
66
- const value = evaluate(expr)
67
- const id = nextId++
68
-
69
- // Mark all existing results as frozen, add new one unfrozen
70
- setResults((prev) => [...prev.map((r) => ({ ...r, frozen: true })), { id, expr, value, frozen: false }])
71
- setInput("")
72
- setCursor(0)
73
- }, [input])
74
-
75
- useInput((ch: string, key: Key) => {
76
- if (key.return) {
77
- submit()
78
- return
79
- }
80
- if (key.escape || (ch === "q" && input === "")) {
81
- exit()
82
- return
83
- }
84
- if (key.backspace) {
85
- if (cursor > 0) {
86
- setInput((v) => v.slice(0, cursor - 1) + v.slice(cursor))
87
- setCursor((c) => c - 1)
88
- }
89
- return
90
- }
91
- if (key.leftArrow) {
92
- setCursor((c) => Math.max(0, c - 1))
93
- return
94
- }
95
- if (key.rightArrow) {
96
- setCursor((c) => Math.min(input.length, c + 1))
97
- return
98
- }
99
- // Ctrl+A: beginning of line
100
- if (key.ctrl && ch === "a") {
101
- setCursor(0)
102
- return
103
- }
104
- // Ctrl+E: end of line
105
- if (key.ctrl && ch === "e") {
106
- setCursor(input.length)
107
- return
108
- }
109
- // Ctrl+U: clear line
110
- if (key.ctrl && ch === "u") {
111
- setInput("")
112
- setCursor(0)
113
- return
114
- }
115
- if (ch >= " ") {
116
- setInput((v) => v.slice(0, cursor) + ch + v.slice(cursor))
117
- setCursor((c) => c + 1)
118
- }
119
- })
120
-
121
- const activeCount = results.length - frozenCount
122
- const beforeCursor = input.slice(0, cursor)
123
- const atCursor = input[cursor] ?? " "
124
- const afterCursor = input.slice(cursor + 1)
125
-
126
- return (
127
- <Box flexDirection="column">
128
- {/* Active (non-virtualized) results via VirtualList */}
129
- {activeCount > 0 && (
130
- <VirtualList
131
- items={results}
132
- virtualized={(r) => r.frozen}
133
- height={activeCount * 2}
134
- itemHeight={2}
135
- scrollTo={0}
136
- renderItem={(r) => (
137
- <Box key={r.id} flexDirection="column">
138
- <Text>
139
- <Text color="gray">{"$ "}</Text>
140
- <Text>{r.expr}</Text>
141
- </Text>
142
- <Text>
143
- <Text color="cyan">{"→ "}</Text>
144
- <Text>{r.value}</Text>
145
- </Text>
146
- </Box>
147
- )}
148
- />
149
- )}
150
-
151
- {/* Separator */}
152
- <Divider />
153
-
154
- {/* Input prompt */}
155
- <Text>
156
- <Text color="yellow">{"› "}</Text>
157
- <Text>{beforeCursor}</Text>
158
- <Text inverse>{atCursor}</Text>
159
- <Text>{afterCursor}</Text>
160
- </Text>
161
-
162
- {/* Status */}
163
- <Text dim>
164
- {results.length} result{results.length !== 1 ? "s" : ""} | Esc/q to quit
165
- </Text>
166
- </Box>
167
- )
168
- }
169
-
170
- // =============================================================================
171
- // Main
172
- // =============================================================================
173
-
174
- async function main() {
175
- await run(
176
- <ExampleBanner meta={meta} controls="Type expr + Enter Esc/q quit">
177
- <Repl />
178
- </ExampleBanner>,
179
- { mode: "inline" },
180
- )
181
- }
182
-
183
- if (import.meta.main) {
184
- main().catch(console.error)
185
- }
@@ -1,110 +0,0 @@
1
- /**
2
- * Input Debug Tool
3
- *
4
- * Minimal diagnostic to find where keypresses are lost.
5
- * Shows every event received by useInput + TextArea side by side.
6
- *
7
- * Run: bun vendor/silvery/examples/interactive/_input-debug.tsx
8
- */
9
-
10
- import React, { useState, useRef } from "react"
11
- import { render, Box, Text, TextArea, useInput, useApp, createTerm, type Key } from "../../src/index.js"
12
-
13
- function InputDebug(): JSX.Element {
14
- const { exit } = useApp()
15
-
16
- // Track raw useInput events
17
- const [rawEvents, setRawEvents] = useState<string[]>([])
18
- const rawCountRef = useRef(0)
19
-
20
- // Track TextArea value
21
- const [textValue, setTextValue] = useState("")
22
- const textChangeCountRef = useRef(0)
23
-
24
- // Raw useInput handler — logs EVERY event
25
- useInput((input: string, key: Key) => {
26
- if (key.escape) {
27
- exit()
28
- return
29
- }
30
- rawCountRef.current++
31
- const desc = describeKey(input, key)
32
- setRawEvents((prev) => [...prev.slice(-15), `#${rawCountRef.current} ${desc}`])
33
- })
34
-
35
- // TextArea onChange handler
36
- function handleChange(value: string) {
37
- textChangeCountRef.current++
38
- setTextValue(value)
39
- }
40
-
41
- return (
42
- <Box flexDirection="column" padding={1}>
43
- <Text bold color="cyan">
44
- Input Pipeline Diagnostic
45
- </Text>
46
- <Text dim>Type slowly (1 char/2sec). Compare left (raw events) vs right (TextArea value).</Text>
47
- <Text dim>Press Esc to quit.</Text>
48
- <Box height={1} />
49
-
50
- <Box gap={4}>
51
- {/* Left: Raw useInput events */}
52
- <Box flexDirection="column" width={40}>
53
- <Text bold color="yellow">
54
- useInput events: {rawCountRef.current}
55
- </Text>
56
- {rawEvents.map((e, i) => (
57
- <Text key={i} dimColor={i < rawEvents.length - 1}>
58
- {e}
59
- </Text>
60
- ))}
61
- </Box>
62
-
63
- {/* Right: TextArea */}
64
- <Box flexDirection="column" width={40}>
65
- <Text bold color="green">
66
- TextArea value ({textValue.length} chars, {textChangeCountRef.current} changes):
67
- </Text>
68
- <Box borderStyle="single" borderColor="green">
69
- <Box paddingX={1}>
70
- <TextArea value={textValue} onChange={handleChange} height={4} placeholder="Type here..." />
71
- </Box>
72
- </Box>
73
- <Box marginTop={1}>
74
- <Text>Value: {JSON.stringify(textValue)}</Text>
75
- </Box>
76
- </Box>
77
- </Box>
78
- </Box>
79
- )
80
- }
81
-
82
- function describeKey(input: string, key: Key): string {
83
- const parts: string[] = []
84
- if (key.ctrl) parts.push("Ctrl")
85
- if (key.meta) parts.push("Meta")
86
- if (key.shift) parts.push("Shift")
87
-
88
- if (key.return) parts.push("Enter")
89
- else if (key.escape) parts.push("Esc")
90
- else if (key.backspace) parts.push("BS")
91
- else if (key.delete) parts.push("Del")
92
- else if (key.upArrow) parts.push("Up")
93
- else if (key.downArrow) parts.push("Down")
94
- else if (key.leftArrow) parts.push("Left")
95
- else if (key.rightArrow) parts.push("Right")
96
- else if (key.tab) parts.push("Tab")
97
- else if (input.length === 1 && input >= " ") parts.push(`'${input}'`)
98
- else if (input) parts.push(`raw:${JSON.stringify(input)}`)
99
- else parts.push("(empty)")
100
-
101
- return parts.join("+")
102
- }
103
-
104
- async function main() {
105
- using term = createTerm()
106
- const { waitUntilExit } = await render(<InputDebug />, term)
107
- await waitUntilExit()
108
- }
109
-
110
- main().catch(console.error)
@@ -1,71 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Bare stdin test — no React, no silvery.
4
- *
5
- * Tests whether Bun's stdin "readable" events deliver all keypresses.
6
- * Run in a real terminal (not through a tool): bun vendor/silvery/examples/interactive/_stdin-test.ts
7
- *
8
- * Type characters slowly. Each keypress should appear immediately.
9
- * Press Ctrl+C to exit.
10
- */
11
-
12
- const fs = await import("node:fs")
13
-
14
- const log = fs.createWriteStream("/tmp/stdin-test.log", { flags: "w" })
15
- function logMsg(msg: string) {
16
- const ts = new Date().toISOString().slice(11, 23)
17
- log.write(`[${ts}] ${msg}\n`)
18
- process.stdout.write(msg + "\n")
19
- }
20
-
21
- logMsg("=== Bare stdin readable test ===")
22
- logMsg("Type characters slowly. Press Ctrl+C to exit.")
23
- logMsg("")
24
-
25
- process.stdin.setEncoding("utf8")
26
- process.stdin.setRawMode(true)
27
- process.stdin.ref()
28
-
29
- let count = 0
30
-
31
- function handleReadable() {
32
- const chunk = process.stdin.read() as string | null
33
- if (chunk === null) {
34
- logMsg(` readable event but stdin.read() returned null`)
35
- return
36
- }
37
-
38
- for (let i = 0; i < chunk.length; i++) {
39
- count++
40
- const ch = chunk[i]!
41
- const code = ch.charCodeAt(0)
42
-
43
- if (code === 3) {
44
- // Ctrl+C
45
- logMsg(`\n=== Total: ${count} keypresses ===`)
46
- log.end()
47
- process.stdin.setRawMode(false)
48
- process.stdin.unref()
49
- process.exit(0)
50
- }
51
-
52
- if (code >= 32 && code < 127) {
53
- logMsg(`#${count} char='${ch}' code=${code} chunkLen=${chunk.length}`)
54
- } else {
55
- logMsg(
56
- `#${count} code=0x${code.toString(16).padStart(2, "0")} chunkLen=${chunk.length} raw=${JSON.stringify(chunk.slice(i))}`,
57
- )
58
- }
59
- }
60
-
61
- // Try reading again in case more data
62
- const more = process.stdin.read() as string | null
63
- if (more !== null) {
64
- logMsg(` EXTRA data in buffer: ${JSON.stringify(more)}`)
65
- }
66
- }
67
-
68
- process.stdin.on("readable", handleReadable)
69
-
70
- logMsg("Listening for stdin readable events...")
71
- logMsg("")
@@ -1,45 +0,0 @@
1
- /**
2
- * Bare TextArea Test — single useInput hook, no parent useInput.
3
- *
4
- * Tests whether the bug is from two useInput hooks competing.
5
- * Run: bun vendor/silvery/examples/interactive/_textarea-bare.tsx
6
- *
7
- * If chars are STILL eaten → bug is in TextArea or input pipeline
8
- * If chars are NOT eaten → bug is from two useInput hooks
9
- */
10
-
11
- import React, { useState } from "react"
12
- import { render, Box, Text, TextArea, createTerm } from "../../src/index.js"
13
-
14
- function BareTextArea(): JSX.Element {
15
- const [value, setValue] = useState("")
16
-
17
- // NO parent useInput — only TextArea's internal one
18
- return (
19
- <Box flexDirection="column" padding={1}>
20
- <Text bold color="cyan">
21
- Bare TextArea (single useInput hook)
22
- </Text>
23
- <Text dim>Type slowly. Ctrl+C to exit.</Text>
24
- <Box height={1} />
25
-
26
- <Box borderStyle="single" borderColor="cyan" paddingX={1}>
27
- <TextArea value={value} onChange={setValue} height={4} placeholder="Type here..." />
28
- </Box>
29
-
30
- <Box marginTop={1}>
31
- <Text>
32
- Value ({value.length} chars): {JSON.stringify(value)}
33
- </Text>
34
- </Box>
35
- </Box>
36
- )
37
- }
38
-
39
- async function main() {
40
- using term = createTerm()
41
- const { waitUntilExit } = await render(<BareTextArea />, term)
42
- await waitUntilExit()
43
- }
44
-
45
- main().catch(console.error)