silvery 0.0.1 → 0.3.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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +174 -11
  3. package/bin/silvery.ts +258 -0
  4. package/examples/CLAUDE.md +75 -0
  5. package/examples/_banner.tsx +60 -0
  6. package/examples/cli.ts +228 -0
  7. package/examples/index.md +101 -0
  8. package/examples/inline/inline-nontty.tsx +98 -0
  9. package/examples/inline/inline-progress.tsx +79 -0
  10. package/examples/inline/inline-simple.tsx +63 -0
  11. package/examples/inline/scrollback.tsx +185 -0
  12. package/examples/interactive/_input-debug.tsx +110 -0
  13. package/examples/interactive/_stdin-test.ts +71 -0
  14. package/examples/interactive/_textarea-bare.tsx +45 -0
  15. package/examples/interactive/aichat/components.tsx +468 -0
  16. package/examples/interactive/aichat/index.tsx +207 -0
  17. package/examples/interactive/aichat/script.ts +460 -0
  18. package/examples/interactive/aichat/state.ts +326 -0
  19. package/examples/interactive/aichat/types.ts +19 -0
  20. package/examples/interactive/app-todo.tsx +198 -0
  21. package/examples/interactive/async-data.tsx +208 -0
  22. package/examples/interactive/cli-wizard.tsx +332 -0
  23. package/examples/interactive/clipboard.tsx +183 -0
  24. package/examples/interactive/components.tsx +463 -0
  25. package/examples/interactive/data-explorer.tsx +506 -0
  26. package/examples/interactive/dev-tools.tsx +379 -0
  27. package/examples/interactive/explorer.tsx +747 -0
  28. package/examples/interactive/gallery.tsx +652 -0
  29. package/examples/interactive/inline-bench.tsx +136 -0
  30. package/examples/interactive/kanban.tsx +267 -0
  31. package/examples/interactive/layout-ref.tsx +185 -0
  32. package/examples/interactive/outline.tsx +171 -0
  33. package/examples/interactive/paste-demo.tsx +198 -0
  34. package/examples/interactive/scroll.tsx +77 -0
  35. package/examples/interactive/search-filter.tsx +240 -0
  36. package/examples/interactive/task-list.tsx +279 -0
  37. package/examples/interactive/terminal.tsx +798 -0
  38. package/examples/interactive/textarea.tsx +103 -0
  39. package/examples/interactive/theme.tsx +336 -0
  40. package/examples/interactive/transform.tsx +256 -0
  41. package/examples/interactive/virtual-10k.tsx +413 -0
  42. package/examples/kitty/canvas.tsx +519 -0
  43. package/examples/kitty/generate-samples.ts +236 -0
  44. package/examples/kitty/image-component.tsx +273 -0
  45. package/examples/kitty/images.tsx +604 -0
  46. package/examples/kitty/input.tsx +371 -0
  47. package/examples/kitty/keys.tsx +378 -0
  48. package/examples/kitty/paint.tsx +1017 -0
  49. package/examples/layout/dashboard.tsx +551 -0
  50. package/examples/layout/live-resize.tsx +290 -0
  51. package/examples/layout/overflow.tsx +51 -0
  52. package/examples/playground/README.md +69 -0
  53. package/examples/playground/build.ts +61 -0
  54. package/examples/playground/index.html +420 -0
  55. package/examples/playground/playground-app.tsx +416 -0
  56. package/examples/runtime/elm-counter.tsx +206 -0
  57. package/examples/runtime/hello-runtime.tsx +73 -0
  58. package/examples/runtime/pipe-composition.tsx +184 -0
  59. package/examples/runtime/run-counter.tsx +78 -0
  60. package/examples/runtime/runtime-counter.tsx +197 -0
  61. package/examples/screenshots/generate.tsx +563 -0
  62. package/examples/scrollback-perf.tsx +230 -0
  63. package/examples/viewer.tsx +654 -0
  64. package/examples/web/build.ts +365 -0
  65. package/examples/web/canvas-app.tsx +80 -0
  66. package/examples/web/canvas.html +89 -0
  67. package/examples/web/dom-app.tsx +81 -0
  68. package/examples/web/dom.html +113 -0
  69. package/examples/web/showcase-app.tsx +107 -0
  70. package/examples/web/showcase.html +34 -0
  71. package/examples/web/showcases/index.tsx +56 -0
  72. package/examples/web/viewer-app.tsx +555 -0
  73. package/examples/web/viewer.html +30 -0
  74. package/examples/web/xterm-app.tsx +105 -0
  75. package/examples/web/xterm.html +118 -0
  76. package/package.json +106 -5
  77. package/src/index.ts +5 -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,182 @@
1
- # silvery
1
+ # Silvery
2
2
 
3
- Multi-target rendering framework for React — terminal, browser, and beyond.
3
+ **Polished Terminal UIs in React.**
4
4
 
5
- **Coming soon.** See [silvery.dev](https://silvery.dev) for details.
5
+ Responsive layouts, scrollable containers, 100x+ faster incremental updates, and full support for modern terminal capabilities. 30+ components from TextInput to VirtualList. Pure TypeScript, no WASM.
6
+
7
+ ```
8
+ npm install silvery react
9
+ ```
10
+
11
+ > **Status:** Alpha — under active development. APIs may change. Early adopters and feedback welcome.
12
+
13
+ ```tsx
14
+ import { useState } from "react"
15
+ import { render, Box, Text, useInput, useContentRect, createTerm } from "silvery"
16
+
17
+ function App() {
18
+ const { width } = useContentRect()
19
+ const [count, setCount] = useState(0)
20
+
21
+ useInput((input) => {
22
+ if (input === "j") setCount((c) => c + 1)
23
+ if (input === "k") setCount((c) => c - 1)
24
+ if (input === "q") return "exit"
25
+ })
26
+
27
+ return (
28
+ <Box flexDirection="column" padding={1}>
29
+ <Text bold>Counter ({width} cols wide)</Text>
30
+ <Text>Count: {count}</Text>
31
+ <Text dim>j/k = change, q = quit</Text>
32
+ </Box>
33
+ )
34
+ }
35
+
36
+ using term = createTerm()
37
+ await render(<App />, term).run()
38
+ ```
39
+
40
+ ## Renderer
41
+
42
+ ### Responsive layout
43
+
44
+ `useContentRect()` returns actual dimensions synchronously -- no post-layout effect, no `{width: 0, height: 0}` on first render. Components adapt to their available space immediately.
45
+
46
+ ```tsx
47
+ function Responsive() {
48
+ const { width } = useContentRect()
49
+ return width > 80 ? <FullDashboard /> : <CompactView />
50
+ }
51
+ ```
52
+
53
+ ### Scrollable containers
54
+
55
+ `overflow="scroll"` with `scrollTo` -- the framework handles measurement, clipping, and scroll position. No manual virtualization needed.
56
+
57
+ ```tsx
58
+ <Box height={20} overflow="scroll" scrollTo={selectedIndex}>
59
+ {items.map((item) => (
60
+ <Card key={item.id} item={item} />
61
+ ))}
62
+ </Box>
63
+ ```
64
+
65
+ ### Per-node dirty tracking
66
+
67
+ Seven independent dirty flags per node. When a user presses a key, only the affected nodes re-render -- bypassing React reconciliation entirely for unchanged subtrees. Typical interactive updates complete in ~170 microseconds for 1000 nodes, compared to full-tree re-renders.
68
+
69
+ ### Multi-target rendering
70
+
71
+ Terminal today, Canvas 2D and DOM experimental. Same React components, different rendering backends.
72
+
73
+ ## Framework Layers (Optional)
74
+
75
+ ### Input layer stack
76
+
77
+ DOM-style event bubbling with modal isolation. Opening a dialog automatically captures input -- no manual guard checks in every handler.
78
+
79
+ ```tsx
80
+ <InputLayerProvider>
81
+ <Board />
82
+ {isOpen && <Dialog />} {/* Dialog captures input; Board doesn't see it */}
83
+ </InputLayerProvider>
84
+ ```
85
+
86
+ ### Spatial focus navigation
87
+
88
+ Tree-based focus with scopes, arrow-key directional movement, click-to-focus, and `useFocusWithin`. Go beyond tab-order.
89
+
90
+ ### Command and keybinding system
91
+
92
+ Named commands with IDs, help text, configurable keybindings, and runtime introspection. Build discoverable, AI-automatable interfaces.
93
+
94
+ ```tsx
95
+ const MyComponent = withCommands(BaseComponent, () => [
96
+ { id: "save", label: "Save", keys: ["ctrl+s"], action: () => save() },
97
+ { id: "quit", label: "Quit", keys: ["q", "ctrl+c"], action: () => exit() },
98
+ ])
99
+ ```
100
+
101
+ ### Mouse support
102
+
103
+ SGR mouse protocol with DOM-style event props -- `onClick`, `onMouseDown`, `onWheel`, hit testing, drag support.
104
+
105
+ ### Multi-line text editing
106
+
107
+ Built-in `TextArea` with word wrap, scrolling, cursor movement, selection, and undo/redo via `EditContext`.
108
+
109
+ ### 30+ built-in components
110
+
111
+ TextArea, TextInput, VirtualList, SelectList, Table, CommandPalette, ModalDialog, Tabs, TreeView, SplitView, Toast, Image, and more -- all with built-in scrolling, focus, and input handling.
112
+
113
+ ### Theme system
114
+
115
+ `@silvery/theme` with 38 built-in palettes and semantic color tokens (`$primary`, `$error`, `$border`, etc.) that adapt automatically.
116
+
117
+ ### TEA state machines
118
+
119
+ Optional [Elm Architecture](https://guide.elm-lang.org/architecture/) alongside React hooks. Pure `(action, state) -> [state, effects]` functions for testable, replayable, undoable UI logic.
6
120
 
7
121
  ## Packages
8
122
 
9
- - `silvery` — umbrella package (re-exports all `@silvery/*` packages)
10
- - `@silvery/react` core React reconciler and runtime
11
- - `@silvery/term` terminal rendering target
12
- - `@silvery/ansi` ANSI styling (chalk replacement)
13
- - `@silvery/theme` theming system
14
- - `@silvery/ui` component library
15
- - `@silvery/test` testing utilities
16
- - `@silvery/tea` TEA state machine store
123
+ | Package | Description |
124
+ | ------------------------------------ | ----------------------------------------- |
125
+ | [`silvery`](packages/) | Umbrella -- re-exports `@silvery/react` |
126
+ | [`@silvery/react`](packages/react) | React reconciler, hooks, renderer |
127
+ | [`@silvery/term`](packages/term) | Terminal rendering pipeline, ANSI styling |
128
+ | [`@silvery/ui`](packages/ui) | Component library (30+ components) |
129
+ | [`@silvery/theme`](packages/theme) | Theming with 38 palettes |
130
+ | [`@silvery/tea`](packages/tea) | TEA state machine store |
131
+ | [`@silvery/compat`](packages/compat) | Ink/Chalk compatibility layers |
132
+ | [`@silvery/test`](packages/test) | Testing utilities and locators |
133
+
134
+ ## Compatibility
135
+
136
+ `silvery/ink` and `silvery/chalk` provide compatibility layers for existing React terminal apps. The core API (`Box`, `Text`, `useInput`, `render`) is intentionally familiar -- most existing code works with minimal changes. See the [migration guide](docs/guide/migration.md) for details.
137
+
138
+ ## When to Use Silvery
139
+
140
+ Silvery is designed for **complex interactive TUIs** — dashboards, editors, kanban boards, chat interfaces. If you need scrollable containers, mouse support, spatial focus, or components that adapt to their size, Silvery provides these out of the box.
141
+
142
+ For simple one-shot CLI prompts or spinners, mature alternatives with larger plugin ecosystems may be a better fit today.
143
+
144
+ ## Ecosystem
145
+
146
+ | Project | What |
147
+ | ------------------------------------------ | -------------------------------------------------------------- |
148
+ | [Termless](https://termless.dev) | Headless terminal testing -- like Playwright for terminal apps |
149
+ | [Flexily](https://beorn.github.io/flexily) | Pure JS flexbox layout engine (Yoga-compatible, zero WASM) |
150
+ | [Loggily](https://beorn.github.io/loggily) | Debug + structured logging + tracing |
151
+
152
+ See the [roadmap](https://silvery.dev/roadmap) for what's next.
153
+
154
+ ## Performance
155
+
156
+ _Apple M1 Max, Bun 1.3.9. Reproduce: `bun run bench:compare`_
157
+
158
+ | Scenario | Silvery | Ink 5 |
159
+ | --------------------------------------- | ------- | ------- |
160
+ | Cold render (1 component) | 165 us | 271 us |
161
+ | Cold render (1000 components) | 463 ms | 541 ms |
162
+ | Typical interactive update (1000 nodes) | 169 us | 20.7 ms |
163
+ | Layout (50-node kanban) | 57 us | 88 us |
164
+
165
+ **Why the difference?** Interactive updates (cursor move, scroll, toggle) typically change one or two nodes. Silvery's per-node dirty tracking updates only those nodes — 169 us for a 1000-node tree. Traditional full-tree renderers re-render the entire React tree and run complete layout on every state change — 20.7 ms. For the updates that dominate interactive use, Silvery is ~100x faster.
166
+
167
+ Full re-renders where the entire tree changes are comparable or faster in full-tree renderers (simpler string concatenation vs Silvery's 5-phase pipeline). That trade-off is inherent to supporting responsive layout, and full re-renders are rare in interactive apps.
168
+
169
+ ## Documentation
170
+
171
+ Full docs at [silvery.dev](https://silvery.dev) -- getting started guide, API reference, component catalog, and migration guide.
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ bun install
177
+ bun test
178
+ bun run lint
179
+ ```
17
180
 
18
181
  ## License
19
182
 
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
+ })
@@ -0,0 +1,75 @@
1
+ # Silvery Examples & Showcases
2
+
3
+ ## Directory Structure
4
+
5
+ | Directory | What |
6
+ | -------------- | ---------------------------------------------------------- |
7
+ | `interactive/` | Full apps — run with `bun examples/interactive/<name>.tsx` |
8
+ | `inline/` | Inline mode examples (no alt screen) |
9
+ | `kitty/` | Kitty protocol demos |
10
+ | `layout/` | Layout engine examples |
11
+ | `runtime/` | Runtime layer demos (run, createApp, createStore) |
12
+ | `playground/` | Quick prototyping |
13
+ | `web/` | Browser renderers (DOM, Canvas2D) |
14
+ | `screenshots/` | Reference screenshots for visual regression |
15
+
16
+ ## Making a Great Showcase
17
+
18
+ ### Design Principles
19
+
20
+ 1. **Show, don't tell.** A showcase should demonstrate Silvery features through working UI, not walls of text. Intro text is fine — but collapse it once the demo starts.
21
+
22
+ 2. **Auto-size to content.** `ScrollbackView`/`ScrollbackList` auto-size to their content — no manual height management. The output phase caps output at terminal height independently. Content that exceeds terminal height causes natural terminal scrolling.
23
+
24
+ 3. **Single status bar.** Keep the status bar to one line. Include: context bar, elapsed time, cost, and key hints. Remove anything that doesn't help the user interact.
25
+
26
+ 4. **Conditional headers.** Show feature bullets before the demo starts (when there's space). Collapse to a one-liner once content fills the screen.
27
+
28
+ 5. **Respect terminal width.** Boxes with borders at 120 cols should leave room for the border characters. Test at 80 and 120 cols.
29
+
30
+ 6. **Streaming feels real.** For coding agent demos: thinking spinner (1-2s) → word-by-word text reveal → tool call spinner → output. Use `setInterval` at 50ms with 8-12% fraction increments.
31
+
32
+ ### Scrollback Pattern
33
+
34
+ Use `ScrollbackList` (or `ScrollbackView`) — they handle terminal height, footer pinning, and overflow automatically:
35
+
36
+ ```tsx
37
+ function App() {
38
+ return (
39
+ <ScrollbackList
40
+ items={items}
41
+ keyExtractor={(item) => item.id}
42
+ isFrozen={(item) => item.done}
43
+ markers={true}
44
+ footer={<StatusBar />}
45
+ >
46
+ {(item) => <ItemView item={item} />}
47
+ </ScrollbackList>
48
+ )
49
+ }
50
+
51
+ await render(<App />, term, { mode: "inline" })
52
+ ```
53
+
54
+ `ScrollbackView` auto-sizes to its content — no manual height management. The output phase independently caps output at terminal height (via `inlineFullRender()`), so content that exceeds the terminal causes natural scrolling. The footer stays pinned at the bottom of the content.
55
+
56
+ ### Theme Tokens
57
+
58
+ Use semantic `$token` colors instead of hardcoded values:
59
+
60
+ | Token | Use for |
61
+ | ---------- | ------------------------------------- |
62
+ | `$primary` | Active elements, progress bars, links |
63
+ | `$success` | Completed items, checkmarks |
64
+ | `$warning` | Caution, compaction |
65
+ | `$error` | Failures, diff removals |
66
+ | `$muted` | Secondary info, timestamps |
67
+ | `$border` | Default border color |
68
+
69
+ ### Testing Showcases
70
+
71
+ 1. **Visual check**: Run in TTY and step through all states
72
+ 2. **Resize**: Verify layout adapts to terminal resize
73
+ 3. **Scrollback**: After frozen items, scroll up — verify colors/borders preserved
74
+ 4. **Width**: Test at 80 and 120 columns
75
+ 5. **Fast mode**: `--fast` flag should skip all animation for quick validation
@@ -0,0 +1,60 @@
1
+ import React from "react"
2
+ import { Box, Text, Strong, Muted, ThemeProvider, getThemeByName, type Theme } from "../src/index.js"
3
+
4
+ export interface ExampleMeta {
5
+ name: string
6
+ description: string
7
+ /** API features showcased, e.g. ["VirtualList", "useContentRect()"] */
8
+ features?: string[]
9
+ /** Curated demo — shown in CLI viewer (`bun examples`) and web showcase */
10
+ demo?: boolean
11
+ }
12
+
13
+ interface Props {
14
+ meta: ExampleMeta
15
+ /** Short controls legend, e.g. "j/k navigate q quit" */
16
+ controls?: string
17
+ /** Override theme (from viewer). Falls back to SILVERY_THEME env var. */
18
+ theme?: Theme
19
+ children: React.ReactNode
20
+ }
21
+
22
+ /**
23
+ * Compact header shown when examples run standalone.
24
+ * Wraps children in ThemeProvider for consistent theming.
25
+ */
26
+ export function ExampleBanner({ meta, controls, theme, children }: Props) {
27
+ const resolvedTheme = theme ?? getThemeByName(process.env.SILVERY_THEME)
28
+
29
+ return (
30
+ <ThemeProvider theme={resolvedTheme}>
31
+ <Box flexDirection="column" flexGrow={1}>
32
+ {/* One-line header: dimmed to not compete with example UI */}
33
+ <Box paddingX={1} gap={1}>
34
+ <Text dim color="$warning">
35
+ {"▸ silvery"}
36
+ </Text>
37
+ <Strong>{meta.name}</Strong>
38
+ <Muted>— {meta.description}</Muted>
39
+ </Box>
40
+ {meta.features && meta.features.length > 0 && (
41
+ <Box paddingX={1}>
42
+ <Muted>
43
+ {" "}
44
+ {meta.features.join(" · ")}
45
+ </Muted>
46
+ </Box>
47
+ )}
48
+ {controls && (
49
+ <Box paddingX={1}>
50
+ <Muted>
51
+ {" "}
52
+ {controls}
53
+ </Muted>
54
+ </Box>
55
+ )}
56
+ {children}
57
+ </Box>
58
+ </ThemeProvider>
59
+ )
60
+ }