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
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Live Resize Demo
3
+ *
4
+ * THE showcase demo for silvery's unique capability: components that know their size.
5
+ *
6
+ * Demonstrates:
7
+ * - useContentRect() providing real-time width/height during render
8
+ * - Multi-column layout that reflows from 1 to 2 to 3 columns based on width
9
+ * - Responsive breakpoints with visual feedback
10
+ * - Content that adapts its presentation based on available space
11
+ * - No useEffect, no layout thrashing — dimensions are synchronous
12
+ *
13
+ * Usage: bun run examples/live-resize/index.tsx
14
+ *
15
+ * Try resizing your terminal to see the layout reflow in real-time!
16
+ *
17
+ * Controls:
18
+ * Esc/q or Ctrl+C - Quit
19
+ */
20
+
21
+ import React from "react"
22
+ import { Box, Text, H1, H3, Kbd, Muted, Small, useContentRect } from "../../src/index.js"
23
+ import { run, useInput, type Key } from "@silvery/term/runtime"
24
+ import { useCallback } from "react"
25
+ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
26
+
27
+ export const meta: ExampleMeta = {
28
+ name: "Live Resize",
29
+ description: "Responsive multi-column grid that reflows based on terminal width",
30
+ features: ["useContentRect()", "responsive breakpoints", "Box flexDirection"],
31
+ }
32
+
33
+ // ============================================================================
34
+ // Types
35
+ // ============================================================================
36
+
37
+ interface CardData {
38
+ title: string
39
+ icon: string
40
+ value: string
41
+ detail: string
42
+ color: string
43
+ sparkline: string
44
+ }
45
+
46
+ // ============================================================================
47
+ // Data
48
+ // ============================================================================
49
+
50
+ const CARDS: CardData[] = [
51
+ {
52
+ title: "CPU Usage",
53
+ icon: "\u{1f4bb}",
54
+ value: "42%",
55
+ detail: "4 cores, 2.4 GHz base",
56
+ color: "green",
57
+ sparkline: "\u2582\u2583\u2585\u2587\u2586\u2584\u2583\u2585\u2587\u2588\u2586\u2584\u2583\u2582\u2583\u2585",
58
+ },
59
+ {
60
+ title: "Memory",
61
+ icon: "\u{1f9e0}",
62
+ value: "8.2 GB",
63
+ detail: "of 16 GB (51% used)",
64
+ color: "cyan",
65
+ sparkline: "\u2584\u2584\u2585\u2585\u2585\u2586\u2586\u2586\u2585\u2585\u2586\u2586\u2587\u2587\u2586\u2586",
66
+ },
67
+ {
68
+ title: "Disk I/O",
69
+ icon: "\u{1f4be}",
70
+ value: "234 MB/s",
71
+ detail: "Read: 180 MB/s Write: 54 MB/s",
72
+ color: "yellow",
73
+ sparkline: "\u2581\u2582\u2583\u2587\u2588\u2587\u2584\u2582\u2581\u2582\u2585\u2587\u2586\u2583\u2582\u2581",
74
+ },
75
+ {
76
+ title: "Network",
77
+ icon: "\u{1f310}",
78
+ value: "1.2 Gb/s",
79
+ detail: "In: 800 Mb/s Out: 400 Mb/s",
80
+ color: "magenta",
81
+ sparkline: "\u2583\u2584\u2585\u2586\u2587\u2586\u2585\u2584\u2585\u2586\u2587\u2588\u2587\u2586\u2585\u2584",
82
+ },
83
+ {
84
+ title: "Processes",
85
+ icon: "\u{2699}\u{fe0f}",
86
+ value: "247",
87
+ detail: "12 running, 235 sleeping",
88
+ color: "blue",
89
+ sparkline: "\u2585\u2585\u2585\u2586\u2585\u2585\u2585\u2585\u2586\u2585\u2585\u2585\u2586\u2585\u2585\u2585",
90
+ },
91
+ {
92
+ title: "Temperature",
93
+ icon: "\u{1f321}\u{fe0f}",
94
+ value: "62 C",
95
+ detail: "Max: 85 C (safe range)",
96
+ color: "red",
97
+ sparkline: "\u2583\u2583\u2584\u2584\u2585\u2585\u2586\u2586\u2585\u2585\u2584\u2584\u2583\u2584\u2585\u2585",
98
+ },
99
+ ]
100
+
101
+ // ============================================================================
102
+ // Components
103
+ // ============================================================================
104
+
105
+ function MetricCard({ card, compact }: { card: CardData; compact: boolean }): JSX.Element {
106
+ if (compact) {
107
+ // Minimal: single-line card for narrow terminals
108
+ return (
109
+ <Box borderStyle="round" borderColor={card.color} paddingX={1} flexDirection="row" justifyContent="space-between">
110
+ <H1 color={card.color}>{card.title}</H1>
111
+ <H3>{card.value}</H3>
112
+ </Box>
113
+ )
114
+ }
115
+
116
+ // Full card with sparkline and details
117
+ return (
118
+ <Box borderStyle="round" borderColor={card.color} paddingX={1} flexDirection="column" flexGrow={1}>
119
+ <Box justifyContent="space-between">
120
+ <H1 color={card.color}>{card.title}</H1>
121
+ <H1 color={card.color}>{card.value}</H1>
122
+ </Box>
123
+ <Text color={card.color}>{card.sparkline}</Text>
124
+ <Small>{card.detail}</Small>
125
+ </Box>
126
+ )
127
+ }
128
+
129
+ function BreakpointIndicator({ width, columns }: { width: number; columns: number }): JSX.Element {
130
+ const breakpoints = [
131
+ { threshold: 0, cols: 1, label: "< 60" },
132
+ { threshold: 60, cols: 2, label: "60-99" },
133
+ { threshold: 100, cols: 3, label: "100+" },
134
+ ]
135
+
136
+ return (
137
+ <Box gap={2} paddingX={1}>
138
+ {breakpoints.map((bp) => {
139
+ const isActive = bp.cols === columns
140
+ return (
141
+ <Box key={bp.cols} gap={1}>
142
+ <Text color={isActive ? "green" : "gray"} bold={isActive}>
143
+ {isActive ? "\u25cf" : "\u25cb"}
144
+ </Text>
145
+ <Text color={isActive ? "white" : "gray"} bold={isActive}>
146
+ {bp.cols} col{bp.cols > 1 ? "s" : " "} ({bp.label})
147
+ </Text>
148
+ </Box>
149
+ )
150
+ })}
151
+ </Box>
152
+ )
153
+ }
154
+
155
+ function GridLayout({
156
+ cards,
157
+ columns,
158
+ compact,
159
+ }: {
160
+ cards: CardData[]
161
+ columns: number
162
+ compact: boolean
163
+ }): JSX.Element {
164
+ if (columns === 1) {
165
+ return (
166
+ <Box flexDirection="column" gap={compact ? 0 : 1} flexGrow={1}>
167
+ {cards.map((card) => (
168
+ <MetricCard key={card.title} card={card} compact={compact} />
169
+ ))}
170
+ </Box>
171
+ )
172
+ }
173
+
174
+ // Build rows of N columns
175
+ const rows: CardData[][] = []
176
+ for (let i = 0; i < cards.length; i += columns) {
177
+ rows.push(cards.slice(i, i + columns))
178
+ }
179
+
180
+ return (
181
+ <Box flexDirection="column" gap={1} flexGrow={1}>
182
+ {rows.map((row, rowIndex) => (
183
+ <Box key={rowIndex} flexDirection="row" gap={1}>
184
+ {row.map((card) => (
185
+ <Box key={card.title} flexGrow={1} flexBasis={0}>
186
+ <MetricCard card={card} compact={false} />
187
+ </Box>
188
+ ))}
189
+ {/* Fill remaining slots for even spacing */}
190
+ {row.length < columns &&
191
+ Array.from({ length: columns - row.length }, (_, i) => (
192
+ <Box key={`spacer-${i}`} flexGrow={1} flexBasis={0} />
193
+ ))}
194
+ </Box>
195
+ ))}
196
+ </Box>
197
+ )
198
+ }
199
+
200
+ function CodeSnippet({ width }: { width: number }): JSX.Element {
201
+ const showSnippet = width >= 60
202
+
203
+ if (!showSnippet) {
204
+ return (
205
+ <Box paddingX={1}>
206
+ <Text dim italic>
207
+ (Widen terminal to see the code that powers this)
208
+ </Text>
209
+ </Box>
210
+ )
211
+ }
212
+
213
+ return (
214
+ <Box flexDirection="column" borderStyle="single" borderColor="$border" paddingX={1}>
215
+ <H1 color="yellow">How it works:</H1>
216
+ <Text color="gray">
217
+ {" "}
218
+ <Text color="magenta">const</Text> {"{"} width {"}"} = <Text color="cyan">useContentRect</Text>()
219
+ </Text>
220
+ <Text color="gray">
221
+ {" "}
222
+ <Text color="magenta">const</Text> columns = width {">"} 100 ? <Text color="green">3</Text> : width {">"} 60 ?{" "}
223
+ <Text color="green">2</Text> : <Text color="green">1</Text>
224
+ </Text>
225
+ <Text dim italic>
226
+ {" "}// No useEffect, no layout thrashing. Synchronous.
227
+ </Text>
228
+ </Box>
229
+ )
230
+ }
231
+
232
+ // ============================================================================
233
+ // Main App
234
+ // ============================================================================
235
+
236
+ function LiveResize(): JSX.Element {
237
+ const { width, height } = useContentRect()
238
+
239
+ // Responsive breakpoints
240
+ const columns = width >= 100 ? 3 : width >= 60 ? 2 : 1
241
+ const compact = height < 20 || width < 40
242
+
243
+ useInput(
244
+ useCallback((input: string, key: Key) => {
245
+ if (input === "q" || key.escape || (key.ctrl && input === "c")) {
246
+ return "exit"
247
+ }
248
+ }, []),
249
+ )
250
+
251
+ return (
252
+ <Box flexDirection="column" width="100%" height="100%" padding={1}>
253
+ {/* Breakpoint indicator */}
254
+ <BreakpointIndicator width={width} columns={columns} />
255
+
256
+ {/* Main grid */}
257
+ <Box flexGrow={1} flexDirection="column" marginTop={1}>
258
+ <GridLayout cards={CARDS} columns={columns} compact={compact} />
259
+ </Box>
260
+
261
+ {/* Code snippet showing how it works */}
262
+ {!compact && <CodeSnippet width={width} />}
263
+
264
+ {/* Footer */}
265
+ <Box justifyContent="space-between" paddingX={1}>
266
+ <Muted>Resize your terminal to see the layout reflow</Muted>
267
+ <Muted>
268
+ <Kbd>Esc/q</Kbd> quit
269
+ </Muted>
270
+ </Box>
271
+ </Box>
272
+ )
273
+ }
274
+
275
+ // ============================================================================
276
+ // Main
277
+ // ============================================================================
278
+
279
+ async function main() {
280
+ const handle = await run(
281
+ <ExampleBanner meta={meta} controls="Resize terminal to see reflow Esc/q quit">
282
+ <LiveResize />
283
+ </ExampleBanner>,
284
+ )
285
+ await handle.waitUntilExit()
286
+ }
287
+
288
+ if (import.meta.main) {
289
+ main().catch(console.error)
290
+ }
@@ -0,0 +1,51 @@
1
+ import React from "react"
2
+ import { render, Box, Text, useApp, useInput, createTerm } from "../../src/index.js"
3
+ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
4
+
5
+ export const meta: ExampleMeta = {
6
+ name: "Overflow",
7
+ description: 'overflow="hidden" content clipping demonstration',
8
+ features: ['overflow="hidden"', "Box height"],
9
+ }
10
+
11
+ export function OverflowApp() {
12
+ const { exit } = useApp()
13
+ useInput((input, key) => {
14
+ if (input === "q" || key.escape) exit()
15
+ })
16
+
17
+ return (
18
+ <Box flexDirection="column" padding={1}>
19
+ <Text color="yellow">Title</Text>
20
+
21
+ <Box borderStyle="single" borderColor="$primary" height={5} overflow="hidden">
22
+ <Box flexDirection="column" flexGrow={1}>
23
+ <Text>Line 1</Text>
24
+ <Text>Line 2</Text>
25
+ <Text>Line 3</Text>
26
+ <Text>Line 4</Text>
27
+ <Text>Line 5</Text>
28
+ <Text>Line 6 - should NOT appear</Text>
29
+ <Text>Line 7 - should NOT appear</Text>
30
+ </Box>
31
+ </Box>
32
+
33
+ <Text color="$success">This should NOT be corrupted</Text>
34
+ </Box>
35
+ )
36
+ }
37
+
38
+ async function main() {
39
+ using term = createTerm()
40
+ const { waitUntilExit } = await render(
41
+ <ExampleBanner meta={meta} controls="Esc/q quit">
42
+ <OverflowApp />
43
+ </ExampleBanner>,
44
+ term,
45
+ )
46
+ await waitUntilExit()
47
+ }
48
+
49
+ if (import.meta.main) {
50
+ main().catch(console.error)
51
+ }
@@ -0,0 +1,69 @@
1
+ # Silvery Canvas Playground
2
+
3
+ Interactive browser demo of Silvery's Canvas 2D adapter. Renders React components to an HTML5 `<canvas>` element using the same layout engine and rendering pipeline as the terminal adapter.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Build the playground bundle
9
+ cd vendor/silvery
10
+ bun run examples/playground/build.ts
11
+
12
+ # Open in browser
13
+ open examples/playground/index.html
14
+ ```
15
+
16
+ No dev server required -- just open `index.html` directly in any modern browser.
17
+
18
+ ## What It Shows
19
+
20
+ The playground includes seven preset examples accessible via buttons or number keys (1-7):
21
+
22
+ | # | Preset | Demonstrates |
23
+ | --- | ---------- | --------------------------------------------------------- |
24
+ | 1 | Hello | Basic Box + Text, `useContentRect()` size display |
25
+ | 2 | Text | Bold, italic, underline styles (single/double/curly/etc.) |
26
+ | 3 | Colors | Named ANSI colors, hex, RGB, background fills |
27
+ | 4 | Flexbox | Row/column layouts, `flexGrow`, `gap`, nested panels |
28
+ | 5 | Borders | single, double, round, bold border styles |
29
+ | 6 | Dashboard | Multi-panel system monitor layout |
30
+ | 7 | Responsive | Layout adapts between horizontal/vertical based on width |
31
+
32
+ Resize the browser window to see layouts recompute. The canvas size is shown in the bottom-right corner.
33
+
34
+ ## Architecture
35
+
36
+ The playground uses the same rendering pipeline as Silvery's terminal mode:
37
+
38
+ ```
39
+ React JSX
40
+ | React reconciler builds SilveryNode tree
41
+ v
42
+ Flexily layout engine (pure JS flexbox)
43
+ | Computes { x, y, width, height } for every node
44
+ v
45
+ Canvas adapter (CanvasRenderBuffer)
46
+ | drawText(), fillRect(), drawChar() to OffscreenCanvas
47
+ v
48
+ Visible <canvas> element
49
+ | ctx.drawImage(offscreenCanvas, 0, 0)
50
+ v
51
+ Browser display
52
+ ```
53
+
54
+ Key files:
55
+
56
+ - `src/adapters/canvas-adapter.ts` -- Canvas `RenderAdapter` implementation
57
+ - `src/canvas/index.ts` -- `renderToCanvas()` entry point and React integration
58
+ - `src/render-adapter.ts` -- The `RenderAdapter` interface shared by all targets
59
+
60
+ ## Building a Full Playground (Live JSX Editing)
61
+
62
+ A static HTML page cannot bundle a JSX transpiler. For a full live-editing experience with Monaco editor, see `docs/playground-design.md`. The architecture uses:
63
+
64
+ - **Vite** for dev server and HMR
65
+ - **Monaco Editor** for JSX editing with TypeScript intellisense
66
+ - **Sucrase** (in-browser) for JSX transpilation
67
+ - **silvery/canvas** for rendering the user's components
68
+
69
+ Deployment targets: GitHub Pages (static export), StackBlitz (zero-install), or self-hosted.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Build the Canvas Playground
4
+ *
5
+ * Bundles the playground React app for browser usage.
6
+ * Run: bun run examples/playground/build.ts
7
+ */
8
+
9
+ import { mkdir } from "node:fs/promises"
10
+ import { join, dirname } from "node:path"
11
+
12
+ const __dirname = dirname(new URL(import.meta.url).pathname)
13
+ const distDir = join(__dirname, "dist")
14
+
15
+ // Ensure dist directory exists
16
+ await mkdir(distDir, { recursive: true })
17
+
18
+ // Browser-safe defines for Node.js globals.
19
+ // loggily and @silvery/ansi access process.env at module init,
20
+ // which throws ReferenceError in browsers where `process` is undefined.
21
+ const browserDefines: Record<string, string> = {
22
+ "process.env.NODE_ENV": '"production"',
23
+ "process.env.LOG_LEVEL": "undefined",
24
+ "process.env.TRACE": "undefined",
25
+ "process.env.TRACE_FORMAT": "undefined",
26
+ "process.env.DEBUG": "undefined",
27
+ "process.env.NO_COLOR": "undefined",
28
+ "process.env.FORCE_COLOR": "undefined",
29
+ "process.env.TERM": "undefined",
30
+ "process.env.TERM_PROGRAM": "undefined",
31
+ "process.env.COLORTERM": "undefined",
32
+ "process.env.CI": "undefined",
33
+ "process.env.GITHUB_ACTIONS": "undefined",
34
+ "process.env.KITTY_WINDOW_ID": "undefined",
35
+ "process.env.WT_SESSION": "undefined",
36
+ "process.env.LANG": "undefined",
37
+ "process.env.LC_ALL": "undefined",
38
+ "process.env.LC_CTYPE": "undefined",
39
+ }
40
+
41
+ const result = await Bun.build({
42
+ entrypoints: [join(__dirname, "playground-app.tsx")],
43
+ outdir: distDir,
44
+ target: "browser",
45
+ format: "esm",
46
+ minify: false,
47
+ sourcemap: "external",
48
+ define: browserDefines,
49
+ })
50
+
51
+ if (!result.success) {
52
+ console.error("Playground build failed:")
53
+ for (const log of result.logs) {
54
+ console.error(log)
55
+ }
56
+ process.exit(1)
57
+ }
58
+
59
+ console.log("Built examples/playground/dist/playground-app.js")
60
+ console.log("\nOpen in browser:")
61
+ console.log(" examples/playground/index.html")