silvery 0.3.0 → 0.4.1

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 (120) hide show
  1. package/README.md +41 -145
  2. package/dist/chalk.js +3 -0
  3. package/dist/chalk.js.map +11 -0
  4. package/dist/index.js +340 -0
  5. package/dist/index.js.map +282 -0
  6. package/dist/ink.js +129 -0
  7. package/dist/ink.js.map +140 -0
  8. package/dist/runtime.js +394 -0
  9. package/dist/runtime.js.map +286 -0
  10. package/dist/theme.js +343 -0
  11. package/dist/theme.js.map +286 -0
  12. package/dist/ui/animation.js +3 -0
  13. package/dist/ui/animation.js.map +15 -0
  14. package/dist/ui/ansi.js +3 -0
  15. package/dist/ui/ansi.js.map +10 -0
  16. package/dist/ui/cli.js +8 -0
  17. package/dist/ui/cli.js.map +14 -0
  18. package/dist/ui/display.js +4 -0
  19. package/dist/ui/display.js.map +10 -0
  20. package/dist/ui/image.js +4 -0
  21. package/dist/ui/image.js.map +15 -0
  22. package/dist/ui/input.js +3 -0
  23. package/dist/ui/input.js.map +11 -0
  24. package/dist/ui/progress.js +8 -0
  25. package/dist/ui/progress.js.map +20 -0
  26. package/dist/ui/react.js +3 -0
  27. package/dist/ui/react.js.map +15 -0
  28. package/dist/ui/utils.js +3 -0
  29. package/dist/ui/utils.js.map +10 -0
  30. package/dist/ui/wrappers.js +14 -0
  31. package/dist/ui/wrappers.js.map +19 -0
  32. package/dist/ui.js +17 -0
  33. package/dist/ui.js.map +20 -0
  34. package/package.json +67 -15
  35. package/src/index.ts +67 -1
  36. package/src/runtime.ts +4 -0
  37. package/src/theme.ts +4 -0
  38. package/src/ui/animation.ts +2 -0
  39. package/src/ui/ansi.ts +2 -0
  40. package/src/ui/cli.ts +2 -0
  41. package/src/ui/display.ts +2 -0
  42. package/src/ui/image.ts +2 -0
  43. package/src/ui/input.ts +2 -0
  44. package/src/ui/progress.ts +2 -0
  45. package/src/ui/react.ts +2 -0
  46. package/src/ui/utils.ts +2 -0
  47. package/src/ui/wrappers.ts +2 -0
  48. package/src/ui.ts +4 -0
  49. package/examples/CLAUDE.md +0 -75
  50. package/examples/_banner.tsx +0 -60
  51. package/examples/cli.ts +0 -228
  52. package/examples/index.md +0 -101
  53. package/examples/inline/inline-nontty.tsx +0 -98
  54. package/examples/inline/inline-progress.tsx +0 -79
  55. package/examples/inline/inline-simple.tsx +0 -63
  56. package/examples/inline/scrollback.tsx +0 -185
  57. package/examples/interactive/_input-debug.tsx +0 -110
  58. package/examples/interactive/_stdin-test.ts +0 -71
  59. package/examples/interactive/_textarea-bare.tsx +0 -45
  60. package/examples/interactive/aichat/components.tsx +0 -468
  61. package/examples/interactive/aichat/index.tsx +0 -207
  62. package/examples/interactive/aichat/script.ts +0 -460
  63. package/examples/interactive/aichat/state.ts +0 -326
  64. package/examples/interactive/aichat/types.ts +0 -19
  65. package/examples/interactive/app-todo.tsx +0 -198
  66. package/examples/interactive/async-data.tsx +0 -208
  67. package/examples/interactive/cli-wizard.tsx +0 -332
  68. package/examples/interactive/clipboard.tsx +0 -183
  69. package/examples/interactive/components.tsx +0 -463
  70. package/examples/interactive/data-explorer.tsx +0 -506
  71. package/examples/interactive/dev-tools.tsx +0 -379
  72. package/examples/interactive/explorer.tsx +0 -747
  73. package/examples/interactive/gallery.tsx +0 -652
  74. package/examples/interactive/inline-bench.tsx +0 -136
  75. package/examples/interactive/kanban.tsx +0 -267
  76. package/examples/interactive/layout-ref.tsx +0 -185
  77. package/examples/interactive/outline.tsx +0 -171
  78. package/examples/interactive/paste-demo.tsx +0 -198
  79. package/examples/interactive/scroll.tsx +0 -77
  80. package/examples/interactive/search-filter.tsx +0 -240
  81. package/examples/interactive/task-list.tsx +0 -279
  82. package/examples/interactive/terminal.tsx +0 -798
  83. package/examples/interactive/textarea.tsx +0 -103
  84. package/examples/interactive/theme.tsx +0 -336
  85. package/examples/interactive/transform.tsx +0 -256
  86. package/examples/interactive/virtual-10k.tsx +0 -413
  87. package/examples/kitty/canvas.tsx +0 -519
  88. package/examples/kitty/generate-samples.ts +0 -236
  89. package/examples/kitty/image-component.tsx +0 -273
  90. package/examples/kitty/images.tsx +0 -604
  91. package/examples/kitty/input.tsx +0 -371
  92. package/examples/kitty/keys.tsx +0 -378
  93. package/examples/kitty/paint.tsx +0 -1017
  94. package/examples/layout/dashboard.tsx +0 -551
  95. package/examples/layout/live-resize.tsx +0 -290
  96. package/examples/layout/overflow.tsx +0 -51
  97. package/examples/playground/README.md +0 -69
  98. package/examples/playground/build.ts +0 -61
  99. package/examples/playground/index.html +0 -420
  100. package/examples/playground/playground-app.tsx +0 -416
  101. package/examples/runtime/elm-counter.tsx +0 -206
  102. package/examples/runtime/hello-runtime.tsx +0 -73
  103. package/examples/runtime/pipe-composition.tsx +0 -184
  104. package/examples/runtime/run-counter.tsx +0 -78
  105. package/examples/runtime/runtime-counter.tsx +0 -197
  106. package/examples/screenshots/generate.tsx +0 -563
  107. package/examples/scrollback-perf.tsx +0 -230
  108. package/examples/viewer.tsx +0 -654
  109. package/examples/web/build.ts +0 -365
  110. package/examples/web/canvas-app.tsx +0 -80
  111. package/examples/web/canvas.html +0 -89
  112. package/examples/web/dom-app.tsx +0 -81
  113. package/examples/web/dom.html +0 -113
  114. package/examples/web/showcase-app.tsx +0 -107
  115. package/examples/web/showcase.html +0 -34
  116. package/examples/web/showcases/index.tsx +0 -56
  117. package/examples/web/viewer-app.tsx +0 -555
  118. package/examples/web/viewer.html +0 -30
  119. package/examples/web/xterm-app.tsx +0 -105
  120. package/examples/web/xterm.html +0 -118
package/examples/index.md DELETED
@@ -1,101 +0,0 @@
1
- # Silvery Examples
2
-
3
- Interactive examples demonstrating Silvery features. Organized by category.
4
-
5
- ## Running Examples
6
-
7
- Browse all examples in the interactive viewer:
8
-
9
- ```bash
10
- bun examples
11
- ```
12
-
13
- Run any example standalone:
14
-
15
- ```bash
16
- bun examples/<category>/<name>.tsx
17
- ```
18
-
19
- ## Structure
20
-
21
- Examples are organized into category directories. Each exports a `meta` object
22
- with `name` and `description`. The viewer auto-discovers all examples — no
23
- registry to maintain.
24
-
25
- ```
26
- examples/
27
- _banner.tsx # Shared banner component (not an example)
28
- viewer.tsx # Interactive example browser
29
- layout/ # Layout and responsive design
30
- interactive/ # Keyboard-driven interactive apps
31
- kitty/ # Kitty protocol features (graphics, keyboard)
32
- runtime/ # Runtime layer demos (Layer 1-3)
33
- inline/ # Inline mode and scrollback
34
- playground/ # Web playground (not an example)
35
- screenshots/ # Screenshot generation tool
36
- web/ # Web render targets (canvas, DOM)
37
- ```
38
-
39
- ## Layout
40
-
41
- | Example | File | Description |
42
- | ----------- | ------------------------ | --------------------------------------------- |
43
- | Dashboard | `layout/dashboard.tsx` | Multi-pane dashboard with keyboard navigation |
44
- | Live Resize | `layout/live-resize.tsx` | Responsive columns via `useContentRect()` |
45
- | Overflow | `layout/overflow.tsx` | `overflow="hidden"` content clipping |
46
-
47
- ## Interactive
48
-
49
- | Example | File | Description |
50
- | --------------- | ------------------------------- | ---------------------------------------------- |
51
- | AI Coding Agent | `interactive/aichat/` | Coding agent with streaming, tool calls |
52
- | Todo App | `interactive/app-todo.tsx` | Layer 3: `createApp()` with Zustand store |
53
- | Async Data | `interactive/async-data.tsx` | Suspense boundaries with `use()` hook |
54
- | Kanban | `interactive/kanban.tsx` | Multi-column kanban with card movement |
55
- | Layout Ref | `interactive/layout-ref.tsx` | `forwardRef` + `onLayout` callbacks |
56
- | Scroll | `interactive/scroll.tsx` | Basic scrollable list |
57
- | Search Filter | `interactive/search-filter.tsx` | React concurrent features (`useDeferredValue`) |
58
- | Task List | `interactive/task-list.tsx` | VirtualList with variable-height items |
59
- | TextArea | `interactive/textarea.tsx` | Multi-line text input component |
60
- | Virtual 10K | `interactive/virtual-10k.tsx` | VirtualList with 10,000 items |
61
- | Clipboard | `interactive/clipboard.tsx` | OSC 52 clipboard copy/paste across sessions |
62
- | Paste Demo | `interactive/paste-demo.tsx` | Bracketed paste mode — paste as single event |
63
- | Outline | `interactive/outline.tsx` | Outline vs border side-by-side comparison |
64
- | Transform | `interactive/transform.tsx` | Text post-processing with Transform component |
65
-
66
- ## Kitty Protocol
67
-
68
- | Example | File | Description |
69
- | --------------- | --------------------------- | ------------------------------------------------ |
70
- | Image Viewer | `kitty/images.tsx` | Raw Kitty graphics protocol image display |
71
- | Image Component | `kitty/image-component.tsx` | Declarative `<Image>` with protocol auto-detect |
72
- | Key Events | `kitty/keys.tsx` | Interactive key chord tester with Kitty protocol |
73
- | Input | `kitty/input.tsx` | Kitty keyboard input demonstration |
74
- | Canvas | `kitty/canvas.tsx` | Canvas rendering via Kitty graphics |
75
- | Paint | `kitty/paint.tsx` | Terminal paint app using Kitty graphics |
76
-
77
- ## Runtime
78
-
79
- | Example | File | Description |
80
- | --------------- | ----------------------------- | ------------------------------------------------ |
81
- | Elm Counter | `runtime/elm-counter.tsx` | Layer 1: `createRuntime()` with Elm architecture |
82
- | Hello Runtime | `runtime/hello-runtime.tsx` | Layer 1: minimal static render |
83
- | Run Counter | `runtime/run-counter.tsx` | Layer 2: `run()` with React hooks |
84
- | Runtime Counter | `runtime/runtime-counter.tsx` | Layer 1: `createRuntime()` with event loop |
85
-
86
- ## Inline
87
-
88
- | Example | File | Description |
89
- | --------------- | ---------------------------- | --------------------------------------------------- |
90
- | Inline Simple | `inline/inline-simple.tsx` | Basic inline rendering |
91
- | Inline Progress | `inline/inline-progress.tsx` | Inline progress bar |
92
- | Inline Non-TTY | `inline/inline-nontty.tsx` | Inline output for piped/non-TTY |
93
- | Scrollback | `inline/scrollback.tsx` | REPL with `useScrollback` + VirtualList virtualized |
94
-
95
- ## Creating New Examples
96
-
97
- 1. Add a `.tsx` file in the appropriate category directory
98
- 2. Export a `meta` object: `export const meta: ExampleMeta = { name: "...", description: "..." }`
99
- 3. Export your main component as a named function
100
- 4. Wrap with `ExampleBanner` in the `import.meta.main` block for standalone mode
101
- 5. The viewer discovers it automatically — no registry to update
@@ -1,98 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Example: Non-TTY Mode Support (km-silvery-nontty)
4
- *
5
- * Demonstrates silvery's non-TTY mode support for rendering in environments
6
- * without a terminal (pipes, CI, TERM=dumb).
7
- *
8
- * Run this example:
9
- * # Normal TTY mode
10
- * bun examples/inline-nontty.tsx
11
- *
12
- * # Piped output (auto-detects non-TTY)
13
- * bun examples/inline-nontty.tsx | cat
14
- *
15
- * # Force plain text mode
16
- * SILVERY_NONTTY=plain bun examples/inline-nontty.tsx
17
- *
18
- * # Force line-by-line mode
19
- * SILVERY_NONTTY=line-by-line bun examples/inline-nontty.tsx
20
- */
21
-
22
- import React, { useEffect, useState } from "react"
23
- import { Box, render, Text, useApp, createTerm, type NonTTYMode } from "../../src/index.js"
24
- import type { ExampleMeta } from "../_banner.js"
25
-
26
- export const meta: ExampleMeta = {
27
- name: "Non-TTY Mode",
28
- description: "Graceful degradation for pipes, CI, and TERM=dumb",
29
- features: ["renderString()", "non-TTY output", "pipe-safe"],
30
- }
31
-
32
- function ProgressExample() {
33
- const { exit } = useApp()
34
- const [progress, setProgress] = useState(0)
35
- const [done, setDone] = useState(false)
36
-
37
- useEffect(() => {
38
- const timer = setInterval(() => {
39
- setProgress((prev) => {
40
- const next = prev + 20
41
- if (next >= 100) {
42
- setDone(true)
43
- clearInterval(timer)
44
- return 100
45
- }
46
- return next
47
- })
48
- }, 300)
49
-
50
- return () => clearInterval(timer)
51
- }, [])
52
-
53
- // Exit cleanly after showing "Complete!" for a moment
54
- useEffect(() => {
55
- if (!done) return
56
- const timeout = setTimeout(() => exit(), 300)
57
- return () => clearTimeout(timeout)
58
- }, [done, exit])
59
-
60
- const barWidth = 30
61
- const filled = Math.floor((progress / 100) * barWidth)
62
- const bar = "#".repeat(filled) + "-".repeat(barWidth - filled)
63
-
64
- return (
65
- <Box flexDirection="column">
66
- <Text>Processing files...</Text>
67
- <Text>
68
- [{bar}] {progress}%
69
- </Text>
70
- {done && <Text color="green">Complete!</Text>}
71
- </Box>
72
- )
73
- }
74
-
75
- async function main() {
76
- // Determine non-TTY mode from environment
77
- const envMode = process.env.SILVERY_NONTTY as NonTTYMode | undefined
78
- const nonTTYMode = envMode || "auto"
79
-
80
- console.log(`Non-TTY mode: ${nonTTYMode}`)
81
- console.log(`stdout.isTTY: ${process.stdout.isTTY}`)
82
- console.log("---\n")
83
-
84
- using term = createTerm()
85
- const { waitUntilExit } = await render(<ProgressExample />, term, {
86
- mode: "inline",
87
- nonTTYMode,
88
- })
89
-
90
- await waitUntilExit()
91
-
92
- console.log("\n---")
93
- console.log("Done!")
94
- }
95
-
96
- if (import.meta.main) {
97
- main().catch(console.error)
98
- }
@@ -1,79 +0,0 @@
1
- #!/usr/bin/env tsx
2
- /**
3
- * Example: Inline Progress Indicator
4
- *
5
- * Demonstrates the new inline mode for progress bars and status indicators.
6
- * Unlike fullscreen mode, inline mode renders from the current cursor position
7
- * and updates in place using relative cursor positioning.
8
- */
9
-
10
- import React, { useState, useEffect } from "react"
11
- import { render, Box, Text, useApp, createTerm } from "../../src/index.js"
12
- import type { ExampleMeta } from "../_banner.js"
13
-
14
- export const meta: ExampleMeta = {
15
- name: "Inline Progress",
16
- description: "Inline progress bar updating in place",
17
- features: ["render() inline mode", "setInterval updates"],
18
- }
19
-
20
- function InlineProgress() {
21
- const { exit } = useApp()
22
- const [progress, setProgress] = useState(0)
23
- const [status, setStatus] = useState("Starting...")
24
-
25
- useEffect(() => {
26
- const timer = setInterval(() => {
27
- setProgress((prev) => {
28
- const next = prev + 10
29
- if (next >= 100) {
30
- setStatus("Complete!")
31
- clearInterval(timer)
32
- return 100
33
- }
34
- setStatus(`Processing... ${next}%`)
35
- return next
36
- })
37
- }, 500)
38
-
39
- return () => clearInterval(timer)
40
- }, [])
41
-
42
- // Exit cleanly after showing "Complete!" for a moment
43
- useEffect(() => {
44
- if (progress < 100) return
45
- const timeout = setTimeout(() => exit(), 300)
46
- return () => clearTimeout(timeout)
47
- }, [progress, exit])
48
-
49
- const barWidth = 40
50
- const filled = Math.floor((progress / 100) * barWidth)
51
- const bar = "█".repeat(filled) + "░".repeat(barWidth - filled)
52
-
53
- return (
54
- <Box flexDirection="column">
55
- <Text>{status}</Text>
56
- <Text>
57
- [{bar}] {progress}%
58
- </Text>
59
- </Box>
60
- )
61
- }
62
-
63
- async function main() {
64
- console.log("This is regular console output before the progress bar.\n")
65
-
66
- using term = createTerm()
67
- const { waitUntilExit } = await render(<InlineProgress />, term, {
68
- mode: "inline",
69
- exitOnCtrlC: true,
70
- })
71
-
72
- await waitUntilExit()
73
-
74
- console.log("\nProgress complete! This is output after the progress bar.")
75
- }
76
-
77
- if (import.meta.main) {
78
- main().catch(console.error)
79
- }
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * Simple inline mode test
4
- */
5
-
6
- import React, { useState, useEffect } from "react"
7
- import { render, Box, Text, useApp, createTerm } from "../../src/index.js"
8
- import type { ExampleMeta } from "../_banner.js"
9
-
10
- export const meta: ExampleMeta = {
11
- name: "Inline Simple",
12
- description: "Inline rendering from current cursor position",
13
- features: ["render() inline mode", "one-shot output"],
14
- }
15
-
16
- function Counter() {
17
- const { exit } = useApp()
18
- const [count, setCount] = useState(0)
19
-
20
- useEffect(() => {
21
- const timer = setInterval(() => {
22
- setCount((c) => {
23
- if (c >= 5) {
24
- clearInterval(timer)
25
- return c
26
- }
27
- return c + 1
28
- })
29
- }, 500)
30
-
31
- return () => clearInterval(timer)
32
- }, [])
33
-
34
- // Exit cleanly after count reaches 5
35
- useEffect(() => {
36
- if (count < 5) return
37
- const timeout = setTimeout(() => exit(), 300)
38
- return () => clearTimeout(timeout)
39
- }, [count, exit])
40
-
41
- return (
42
- <Box>
43
- <Text>Count: {count}</Text>
44
- </Box>
45
- )
46
- }
47
-
48
- async function main() {
49
- console.log("Before\n")
50
-
51
- using term = createTerm()
52
- const { waitUntilExit } = await render(<Counter />, term, {
53
- mode: "inline",
54
- })
55
-
56
- await waitUntilExit()
57
-
58
- console.log("\nAfter")
59
- }
60
-
61
- if (import.meta.main) {
62
- main().catch(console.error)
63
- }
@@ -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)