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.
- package/README.md +41 -145
- package/dist/chalk.js +3 -0
- package/dist/chalk.js.map +11 -0
- package/dist/index.js +340 -0
- package/dist/index.js.map +282 -0
- package/dist/ink.js +129 -0
- package/dist/ink.js.map +140 -0
- package/dist/runtime.js +394 -0
- package/dist/runtime.js.map +286 -0
- package/dist/theme.js +343 -0
- package/dist/theme.js.map +286 -0
- package/dist/ui/animation.js +3 -0
- package/dist/ui/animation.js.map +15 -0
- package/dist/ui/ansi.js +3 -0
- package/dist/ui/ansi.js.map +10 -0
- package/dist/ui/cli.js +8 -0
- package/dist/ui/cli.js.map +14 -0
- package/dist/ui/display.js +4 -0
- package/dist/ui/display.js.map +10 -0
- package/dist/ui/image.js +4 -0
- package/dist/ui/image.js.map +15 -0
- package/dist/ui/input.js +3 -0
- package/dist/ui/input.js.map +11 -0
- package/dist/ui/progress.js +8 -0
- package/dist/ui/progress.js.map +20 -0
- package/dist/ui/react.js +3 -0
- package/dist/ui/react.js.map +15 -0
- package/dist/ui/utils.js +3 -0
- package/dist/ui/utils.js.map +10 -0
- package/dist/ui/wrappers.js +14 -0
- package/dist/ui/wrappers.js.map +19 -0
- package/dist/ui.js +17 -0
- package/dist/ui.js.map +20 -0
- package/package.json +67 -15
- package/src/index.ts +67 -1
- package/src/runtime.ts +4 -0
- package/src/theme.ts +4 -0
- package/src/ui/animation.ts +2 -0
- package/src/ui/ansi.ts +2 -0
- package/src/ui/cli.ts +2 -0
- package/src/ui/display.ts +2 -0
- package/src/ui/image.ts +2 -0
- package/src/ui/input.ts +2 -0
- package/src/ui/progress.ts +2 -0
- package/src/ui/react.ts +2 -0
- package/src/ui/utils.ts +2 -0
- package/src/ui/wrappers.ts +2 -0
- package/src/ui.ts +4 -0
- package/examples/CLAUDE.md +0 -75
- package/examples/_banner.tsx +0 -60
- package/examples/cli.ts +0 -228
- package/examples/index.md +0 -101
- package/examples/inline/inline-nontty.tsx +0 -98
- package/examples/inline/inline-progress.tsx +0 -79
- package/examples/inline/inline-simple.tsx +0 -63
- package/examples/inline/scrollback.tsx +0 -185
- package/examples/interactive/_input-debug.tsx +0 -110
- package/examples/interactive/_stdin-test.ts +0 -71
- package/examples/interactive/_textarea-bare.tsx +0 -45
- package/examples/interactive/aichat/components.tsx +0 -468
- package/examples/interactive/aichat/index.tsx +0 -207
- package/examples/interactive/aichat/script.ts +0 -460
- package/examples/interactive/aichat/state.ts +0 -326
- package/examples/interactive/aichat/types.ts +0 -19
- package/examples/interactive/app-todo.tsx +0 -198
- package/examples/interactive/async-data.tsx +0 -208
- package/examples/interactive/cli-wizard.tsx +0 -332
- package/examples/interactive/clipboard.tsx +0 -183
- package/examples/interactive/components.tsx +0 -463
- package/examples/interactive/data-explorer.tsx +0 -506
- package/examples/interactive/dev-tools.tsx +0 -379
- package/examples/interactive/explorer.tsx +0 -747
- package/examples/interactive/gallery.tsx +0 -652
- package/examples/interactive/inline-bench.tsx +0 -136
- package/examples/interactive/kanban.tsx +0 -267
- package/examples/interactive/layout-ref.tsx +0 -185
- package/examples/interactive/outline.tsx +0 -171
- package/examples/interactive/paste-demo.tsx +0 -198
- package/examples/interactive/scroll.tsx +0 -77
- package/examples/interactive/search-filter.tsx +0 -240
- package/examples/interactive/task-list.tsx +0 -279
- package/examples/interactive/terminal.tsx +0 -798
- package/examples/interactive/textarea.tsx +0 -103
- package/examples/interactive/theme.tsx +0 -336
- package/examples/interactive/transform.tsx +0 -256
- package/examples/interactive/virtual-10k.tsx +0 -413
- package/examples/kitty/canvas.tsx +0 -519
- package/examples/kitty/generate-samples.ts +0 -236
- package/examples/kitty/image-component.tsx +0 -273
- package/examples/kitty/images.tsx +0 -604
- package/examples/kitty/input.tsx +0 -371
- package/examples/kitty/keys.tsx +0 -378
- package/examples/kitty/paint.tsx +0 -1017
- package/examples/layout/dashboard.tsx +0 -551
- package/examples/layout/live-resize.tsx +0 -290
- package/examples/layout/overflow.tsx +0 -51
- package/examples/playground/README.md +0 -69
- package/examples/playground/build.ts +0 -61
- package/examples/playground/index.html +0 -420
- package/examples/playground/playground-app.tsx +0 -416
- package/examples/runtime/elm-counter.tsx +0 -206
- package/examples/runtime/hello-runtime.tsx +0 -73
- package/examples/runtime/pipe-composition.tsx +0 -184
- package/examples/runtime/run-counter.tsx +0 -78
- package/examples/runtime/runtime-counter.tsx +0 -197
- package/examples/screenshots/generate.tsx +0 -563
- package/examples/scrollback-perf.tsx +0 -230
- package/examples/viewer.tsx +0 -654
- package/examples/web/build.ts +0 -365
- package/examples/web/canvas-app.tsx +0 -80
- package/examples/web/canvas.html +0 -89
- package/examples/web/dom-app.tsx +0 -81
- package/examples/web/dom.html +0 -113
- package/examples/web/showcase-app.tsx +0 -107
- package/examples/web/showcase.html +0 -34
- package/examples/web/showcases/index.tsx +0 -56
- package/examples/web/viewer-app.tsx +0 -555
- package/examples/web/viewer.html +0 -30
- package/examples/web/xterm-app.tsx +0 -105
- package/examples/web/xterm.html +0 -118
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "silvery",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "React terminal UI renderer for complex interactive apps
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "React terminal UI renderer for complex interactive apps \u2014 layout-aware rendering, flexbox, scrolling, and incremental updates",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ansi",
|
|
7
7
|
"chalk",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"url": "https://github.com/beorn/silvery/issues"
|
|
26
26
|
},
|
|
27
27
|
"license": "MIT",
|
|
28
|
-
"author": "
|
|
28
|
+
"author": "Bj\u00f8rn Stabell <bjorn@stabell.org>",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
31
|
"url": "https://github.com/beorn/silvery.git"
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
],
|
|
39
39
|
"files": [
|
|
40
40
|
"src",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
41
|
+
"dist",
|
|
42
|
+
"bin"
|
|
43
43
|
],
|
|
44
44
|
"type": "module",
|
|
45
45
|
"main": "src/index.ts",
|
|
@@ -49,20 +49,72 @@
|
|
|
49
49
|
"types": "./src/index.ts",
|
|
50
50
|
"import": "./src/index.ts"
|
|
51
51
|
},
|
|
52
|
+
"./runtime": {
|
|
53
|
+
"types": "./src/runtime.ts",
|
|
54
|
+
"import": "./src/runtime.ts"
|
|
55
|
+
},
|
|
56
|
+
"./theme": {
|
|
57
|
+
"types": "./src/theme.ts",
|
|
58
|
+
"import": "./src/theme.ts"
|
|
59
|
+
},
|
|
60
|
+
"./ui": {
|
|
61
|
+
"types": "./src/ui.ts",
|
|
62
|
+
"import": "./src/ui.ts"
|
|
63
|
+
},
|
|
64
|
+
"./ui/cli": {
|
|
65
|
+
"types": "./src/ui/cli.ts",
|
|
66
|
+
"import": "./src/ui/cli.ts"
|
|
67
|
+
},
|
|
68
|
+
"./ui/react": {
|
|
69
|
+
"types": "./src/ui/react.ts",
|
|
70
|
+
"import": "./src/ui/react.ts"
|
|
71
|
+
},
|
|
72
|
+
"./ui/progress": {
|
|
73
|
+
"types": "./src/ui/progress.ts",
|
|
74
|
+
"import": "./src/ui/progress.ts"
|
|
75
|
+
},
|
|
76
|
+
"./ui/wrappers": {
|
|
77
|
+
"types": "./src/ui/wrappers.ts",
|
|
78
|
+
"import": "./src/ui/wrappers.ts"
|
|
79
|
+
},
|
|
80
|
+
"./ui/ansi": {
|
|
81
|
+
"types": "./src/ui/ansi.ts",
|
|
82
|
+
"import": "./src/ui/ansi.ts"
|
|
83
|
+
},
|
|
84
|
+
"./ui/display": {
|
|
85
|
+
"types": "./src/ui/display.ts",
|
|
86
|
+
"import": "./src/ui/display.ts"
|
|
87
|
+
},
|
|
88
|
+
"./ui/input": {
|
|
89
|
+
"types": "./src/ui/input.ts",
|
|
90
|
+
"import": "./src/ui/input.ts"
|
|
91
|
+
},
|
|
92
|
+
"./ui/animation": {
|
|
93
|
+
"types": "./src/ui/animation.ts",
|
|
94
|
+
"import": "./src/ui/animation.ts"
|
|
95
|
+
},
|
|
96
|
+
"./ui/image": {
|
|
97
|
+
"types": "./src/ui/image.ts",
|
|
98
|
+
"import": "./src/ui/image.ts"
|
|
99
|
+
},
|
|
100
|
+
"./ui/utils": {
|
|
101
|
+
"types": "./src/ui/utils.ts",
|
|
102
|
+
"import": "./src/ui/utils.ts"
|
|
103
|
+
},
|
|
52
104
|
"./ink": {
|
|
53
|
-
"types": "./packages/
|
|
54
|
-
"import": "./packages/
|
|
105
|
+
"types": "./packages/ink/src/ink.ts",
|
|
106
|
+
"import": "./packages/ink/src/ink.ts"
|
|
55
107
|
},
|
|
56
108
|
"./chalk": {
|
|
57
|
-
"types": "./packages/
|
|
58
|
-
"import": "./packages/
|
|
109
|
+
"types": "./packages/ink/src/chalk.ts",
|
|
110
|
+
"import": "./packages/ink/src/chalk.ts"
|
|
59
111
|
}
|
|
60
112
|
},
|
|
61
113
|
"publishConfig": {
|
|
62
114
|
"access": "public"
|
|
63
115
|
},
|
|
64
116
|
"scripts": {
|
|
65
|
-
"build": "bun
|
|
117
|
+
"build": "bun run scripts/build.ts",
|
|
66
118
|
"test": "bunx --bun vitest run",
|
|
67
119
|
"test:fast": "bunx --bun vitest run --reporter=dot",
|
|
68
120
|
"typecheck": "tsc --noEmit",
|
|
@@ -76,9 +128,9 @@
|
|
|
76
128
|
"release": "changeset publish",
|
|
77
129
|
"theme": "bun packages/theme/src/cli.ts",
|
|
78
130
|
"demo": "bun examples/cli.ts",
|
|
79
|
-
"compat": "bun packages/
|
|
80
|
-
"compat:ink": "bun packages/
|
|
81
|
-
"compat:chalk": "bun packages/
|
|
131
|
+
"compat": "bun packages/ink/scripts/compat-check.ts",
|
|
132
|
+
"compat:ink": "bun packages/ink/scripts/compat-check.ts ink",
|
|
133
|
+
"compat:chalk": "bun packages/ink/scripts/compat-check.ts chalk"
|
|
82
134
|
},
|
|
83
135
|
"dependencies": {
|
|
84
136
|
"chalk": "^5.6.2",
|
|
@@ -101,7 +153,7 @@
|
|
|
101
153
|
"playwright": "^1.58.2",
|
|
102
154
|
"react": "^19.0.0",
|
|
103
155
|
"typescript": "^5.5.0",
|
|
104
|
-
"vimonkey": "
|
|
156
|
+
"vimonkey": "^0.2.0",
|
|
105
157
|
"vitepress": "^1.5.0",
|
|
106
158
|
"vitepress-plugin-llms": "^1.0.0",
|
|
107
159
|
"vitest": "^4.0.18",
|
|
@@ -114,4 +166,4 @@
|
|
|
114
166
|
"bun": ">=1.0",
|
|
115
167
|
"node": ">=18"
|
|
116
168
|
}
|
|
117
|
-
}
|
|
169
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,4 +3,70 @@
|
|
|
3
3
|
|
|
4
4
|
export const VERSION = "0.0.1"
|
|
5
5
|
|
|
6
|
-
export
|
|
6
|
+
// Re-export everything from @silvery/ag-react — local `render` below shadows the re-exported one
|
|
7
|
+
export * from "@silvery/ag-react"
|
|
8
|
+
|
|
9
|
+
import type { ReactElement } from "react"
|
|
10
|
+
import { render as reactRender, type RenderOptions, type TermDef } from "@silvery/ag-react"
|
|
11
|
+
import type { Term } from "@silvery/ag-react"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Render a React element to the terminal.
|
|
15
|
+
*
|
|
16
|
+
* Zero-ceremony entry point — auto-detects the terminal and starts an
|
|
17
|
+
* interactive app when stdin is a TTY. No need to create a Term first.
|
|
18
|
+
*
|
|
19
|
+
* @example Hello World (2 lines)
|
|
20
|
+
* ```tsx
|
|
21
|
+
* import { render, Text } from "silvery"
|
|
22
|
+
* await render(<Text>Hello!</Text>).run()
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example Interactive counter
|
|
26
|
+
* ```tsx
|
|
27
|
+
* import { useState } from "react"
|
|
28
|
+
* import { render, Box, Text, useInput } from "silvery"
|
|
29
|
+
*
|
|
30
|
+
* function Counter() {
|
|
31
|
+
* const [count, setCount] = useState(0)
|
|
32
|
+
* useInput((input) => {
|
|
33
|
+
* if (input === "j") setCount((c) => c + 1)
|
|
34
|
+
* })
|
|
35
|
+
* return (
|
|
36
|
+
* <Box borderStyle="round" padding={1}>
|
|
37
|
+
* <Text>Count: {count}</Text>
|
|
38
|
+
* </Box>
|
|
39
|
+
* )
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* await render(<Counter />).run()
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example Static render (explicit)
|
|
46
|
+
* ```tsx
|
|
47
|
+
* import { render, Text } from "silvery"
|
|
48
|
+
* await render(<Text>Report</Text>, { width: 120 })
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* When called without a Term or TermDef:
|
|
52
|
+
* - **TTY detected** → interactive mode (stdin + stdout auto-wired)
|
|
53
|
+
* - **No TTY** → static mode (renders once and returns)
|
|
54
|
+
*
|
|
55
|
+
* Pass a Term or TermDef explicitly to override auto-detection.
|
|
56
|
+
*/
|
|
57
|
+
export function render(
|
|
58
|
+
element: ReactElement,
|
|
59
|
+
termOrDef?: Term | TermDef,
|
|
60
|
+
options?: RenderOptions,
|
|
61
|
+
): ReturnType<typeof reactRender> {
|
|
62
|
+
// When no term/def is provided and we're in a TTY, auto-wire stdin/stdout
|
|
63
|
+
// so the app runs interactively (useInput works, app stays alive until exit).
|
|
64
|
+
if (!termOrDef && process.stdin?.isTTY && process.stdout?.isTTY) {
|
|
65
|
+
const ttyDef: TermDef = {
|
|
66
|
+
stdin: process.stdin,
|
|
67
|
+
stdout: process.stdout,
|
|
68
|
+
}
|
|
69
|
+
return reactRender(element, ttyDef, options)
|
|
70
|
+
}
|
|
71
|
+
return reactRender(element, termOrDef, options)
|
|
72
|
+
}
|
package/src/runtime.ts
ADDED
package/src/theme.ts
ADDED
package/src/ui/ansi.ts
ADDED
package/src/ui/cli.ts
ADDED
package/src/ui/image.ts
ADDED
package/src/ui/input.ts
ADDED
package/src/ui/react.ts
ADDED
package/src/ui/utils.ts
ADDED
package/src/ui.ts
ADDED
package/examples/CLAUDE.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
package/examples/_banner.tsx
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
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
|
-
})
|