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.
- package/LICENSE +21 -0
- package/README.md +174 -11
- package/bin/silvery.ts +258 -0
- package/examples/CLAUDE.md +75 -0
- package/examples/_banner.tsx +60 -0
- package/examples/cli.ts +228 -0
- package/examples/index.md +101 -0
- package/examples/inline/inline-nontty.tsx +98 -0
- package/examples/inline/inline-progress.tsx +79 -0
- package/examples/inline/inline-simple.tsx +63 -0
- package/examples/inline/scrollback.tsx +185 -0
- package/examples/interactive/_input-debug.tsx +110 -0
- package/examples/interactive/_stdin-test.ts +71 -0
- package/examples/interactive/_textarea-bare.tsx +45 -0
- package/examples/interactive/aichat/components.tsx +468 -0
- package/examples/interactive/aichat/index.tsx +207 -0
- package/examples/interactive/aichat/script.ts +460 -0
- package/examples/interactive/aichat/state.ts +326 -0
- package/examples/interactive/aichat/types.ts +19 -0
- package/examples/interactive/app-todo.tsx +198 -0
- package/examples/interactive/async-data.tsx +208 -0
- package/examples/interactive/cli-wizard.tsx +332 -0
- package/examples/interactive/clipboard.tsx +183 -0
- package/examples/interactive/components.tsx +463 -0
- package/examples/interactive/data-explorer.tsx +506 -0
- package/examples/interactive/dev-tools.tsx +379 -0
- package/examples/interactive/explorer.tsx +747 -0
- package/examples/interactive/gallery.tsx +652 -0
- package/examples/interactive/inline-bench.tsx +136 -0
- package/examples/interactive/kanban.tsx +267 -0
- package/examples/interactive/layout-ref.tsx +185 -0
- package/examples/interactive/outline.tsx +171 -0
- package/examples/interactive/paste-demo.tsx +198 -0
- package/examples/interactive/scroll.tsx +77 -0
- package/examples/interactive/search-filter.tsx +240 -0
- package/examples/interactive/task-list.tsx +279 -0
- package/examples/interactive/terminal.tsx +798 -0
- package/examples/interactive/textarea.tsx +103 -0
- package/examples/interactive/theme.tsx +336 -0
- package/examples/interactive/transform.tsx +256 -0
- package/examples/interactive/virtual-10k.tsx +413 -0
- package/examples/kitty/canvas.tsx +519 -0
- package/examples/kitty/generate-samples.ts +236 -0
- package/examples/kitty/image-component.tsx +273 -0
- package/examples/kitty/images.tsx +604 -0
- package/examples/kitty/input.tsx +371 -0
- package/examples/kitty/keys.tsx +378 -0
- package/examples/kitty/paint.tsx +1017 -0
- package/examples/layout/dashboard.tsx +551 -0
- package/examples/layout/live-resize.tsx +290 -0
- package/examples/layout/overflow.tsx +51 -0
- package/examples/playground/README.md +69 -0
- package/examples/playground/build.ts +61 -0
- package/examples/playground/index.html +420 -0
- package/examples/playground/playground-app.tsx +416 -0
- package/examples/runtime/elm-counter.tsx +206 -0
- package/examples/runtime/hello-runtime.tsx +73 -0
- package/examples/runtime/pipe-composition.tsx +184 -0
- package/examples/runtime/run-counter.tsx +78 -0
- package/examples/runtime/runtime-counter.tsx +197 -0
- package/examples/screenshots/generate.tsx +563 -0
- package/examples/scrollback-perf.tsx +230 -0
- package/examples/viewer.tsx +654 -0
- package/examples/web/build.ts +365 -0
- package/examples/web/canvas-app.tsx +80 -0
- package/examples/web/canvas.html +89 -0
- package/examples/web/dom-app.tsx +81 -0
- package/examples/web/dom.html +113 -0
- package/examples/web/showcase-app.tsx +107 -0
- package/examples/web/showcase.html +34 -0
- package/examples/web/showcases/index.tsx +56 -0
- package/examples/web/viewer-app.tsx +555 -0
- package/examples/web/viewer.html +30 -0
- package/examples/web/xterm-app.tsx +105 -0
- package/examples/web/xterm.html +118 -0
- package/package.json +106 -5
- 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
|
-
#
|
|
1
|
+
# Silvery
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Polished Terminal UIs in React.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|