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
package/examples/cli.ts DELETED
@@ -1,228 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * silvery demo CLI
4
- *
5
- * Lists and runs interactive demos from the examples/ directory.
6
- *
7
- * Usage:
8
- * bun demo — list all available demos grouped by category
9
- * bun demo <name> — run a specific demo by name (fuzzy match)
10
- * bun demo --list — same as no argument (list all)
11
- * bun demo --help — show usage help
12
- */
13
-
14
- import { resolve } from "node:path"
15
- import type { ExampleMeta } from "./_banner.js"
16
-
17
- // =============================================================================
18
- // Types
19
- // =============================================================================
20
-
21
- interface Example {
22
- name: string
23
- file: string
24
- description: string
25
- category: string
26
- features?: string[]
27
- }
28
-
29
- // =============================================================================
30
- // Auto-Discovery (matches viewer.tsx pattern)
31
- // =============================================================================
32
-
33
- const CATEGORY_DIRS = ["layout", "interactive", "runtime", "inline", "kitty"] as const
34
-
35
- const CATEGORY_DISPLAY: Record<string, string> = {
36
- kitty: "Kitty Protocol",
37
- }
38
-
39
- const CATEGORY_ORDER: Record<string, number> = {
40
- Layout: 0,
41
- Interactive: 1,
42
- Runtime: 2,
43
- Inline: 3,
44
- "Kitty Protocol": 4,
45
- }
46
-
47
- async function discoverExamples(): Promise<Example[]> {
48
- const baseDir = new URL(".", import.meta.url).pathname
49
- const results: Example[] = []
50
-
51
- for (const dir of CATEGORY_DIRS) {
52
- const category = CATEGORY_DISPLAY[dir] ?? dir.charAt(0).toUpperCase() + dir.slice(1)
53
- const dirPath = resolve(baseDir, dir)
54
- const files = [
55
- ...new Bun.Glob("*.tsx").scanSync({ cwd: dirPath }),
56
- ...new Bun.Glob("*/index.tsx").scanSync({ cwd: dirPath }),
57
- ]
58
-
59
- for (const file of files) {
60
- if (file.startsWith("_")) continue // skip internal files
61
-
62
- try {
63
- const mod = await import(resolve(dirPath, file))
64
- if (!mod.meta?.name) continue
65
-
66
- const meta: ExampleMeta = mod.meta
67
- results.push({
68
- name: meta.name,
69
- description: meta.description ?? "",
70
- file: resolve(dirPath, file),
71
- category,
72
- features: meta.features,
73
- })
74
- } catch {
75
- // Skip files that fail to import
76
- }
77
- }
78
- }
79
-
80
- results.sort((a, b) => {
81
- const catDiff = (CATEGORY_ORDER[a.category] ?? 99) - (CATEGORY_ORDER[b.category] ?? 99)
82
- if (catDiff !== 0) return catDiff
83
- return a.name.localeCompare(b.name)
84
- })
85
-
86
- return results
87
- }
88
-
89
- // =============================================================================
90
- // Formatting Helpers
91
- // =============================================================================
92
-
93
- // ANSI color codes for lightweight terminal output without importing silvery
94
- const RESET = "\x1b[0m"
95
- const BOLD = "\x1b[1m"
96
- const DIM = "\x1b[2m"
97
- const YELLOW = "\x1b[33m"
98
- const CYAN = "\x1b[36m"
99
- const GREEN = "\x1b[32m"
100
- const MAGENTA = "\x1b[35m"
101
- const BLUE = "\x1b[34m"
102
- const RED = "\x1b[31m"
103
- const WHITE = "\x1b[37m"
104
-
105
- const CATEGORY_COLOR_CODE: Record<string, string> = {
106
- Layout: MAGENTA,
107
- Interactive: CYAN,
108
- Runtime: GREEN,
109
- Inline: YELLOW,
110
- "Kitty Protocol": BLUE,
111
- }
112
-
113
- function printHelp(): void {
114
- console.log(`
115
- ${BOLD}${YELLOW}silvery demo${RESET} — browse and run interactive examples
116
-
117
- ${BOLD}Usage:${RESET}
118
- bun demo List all available demos
119
- bun demo ${DIM}<name>${RESET} Run a demo by name (case-insensitive, partial match)
120
- bun demo --list List all available demos
121
- bun demo --help Show this help
122
-
123
- ${BOLD}Examples:${RESET}
124
- bun demo dashboard Run the Dashboard demo
125
- bun demo kanban Run the Kanban Board demo
126
- bun demo scroll Run the first demo matching "scroll"
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_CODE[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 a demo: bun demo <name>${RESET}\n`)
148
- }
149
-
150
- /** Find an example by name. Tries exact match first, then case-insensitive
151
- * prefix match, then case-insensitive substring match. */
152
- function findExample(examples: Example[], query: string): Example | undefined {
153
- const q = query.toLowerCase()
154
-
155
- // Exact match (case-insensitive)
156
- const exact = examples.find((ex) => ex.name.toLowerCase() === q)
157
- if (exact) return exact
158
-
159
- // Prefix match (case-insensitive)
160
- const prefix = examples.find((ex) => ex.name.toLowerCase().startsWith(q))
161
- if (prefix) return prefix
162
-
163
- // Substring match (case-insensitive)
164
- const substring = examples.find((ex) => ex.name.toLowerCase().includes(q))
165
- if (substring) return substring
166
-
167
- return undefined
168
- }
169
-
170
- function printNoMatch(query: string, examples: Example[]): void {
171
- console.error(`\n${RED}${BOLD}Error:${RESET} No demo matching "${query}"\n`)
172
- console.error(`${DIM}Available demos:${RESET}`)
173
-
174
- for (const ex of examples) {
175
- console.error(` ${WHITE}${ex.name}${RESET}`)
176
- }
177
-
178
- console.error(`\n${DIM}Run ${BOLD}bun demo${RESET}${DIM} for full list with descriptions.${RESET}\n`)
179
- }
180
-
181
- // =============================================================================
182
- // Main
183
- // =============================================================================
184
-
185
- async function main(): Promise<void> {
186
- const args = process.argv.slice(2)
187
-
188
- // Handle flags
189
- if (args.includes("--help") || args.includes("-h")) {
190
- printHelp()
191
- return
192
- }
193
-
194
- const examples = await discoverExamples()
195
-
196
- // No argument or --list: show the list
197
- if (args.length === 0 || args[0] === "--list") {
198
- printExampleList(examples)
199
- return
200
- }
201
-
202
- // Treat all non-flag arguments as the demo name query
203
- const query = args.filter((a) => !a.startsWith("--")).join(" ")
204
- if (!query) {
205
- printExampleList(examples)
206
- return
207
- }
208
-
209
- const match = findExample(examples, query)
210
- if (!match) {
211
- printNoMatch(query, examples)
212
- process.exit(1)
213
- }
214
-
215
- // Run the matched example
216
- console.log(`${DIM}Running ${BOLD}${match.name}${RESET}${DIM}...${RESET}\n`)
217
-
218
- const proc = Bun.spawn(["bun", "run", match.file], {
219
- stdio: ["inherit", "inherit", "inherit"],
220
- })
221
- const exitCode = await proc.exited
222
- process.exit(exitCode)
223
- }
224
-
225
- main().catch((err) => {
226
- console.error(err)
227
- process.exit(1)
228
- })
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
- }