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,563 @@
1
+ /**
2
+ * Screenshot Generator for silvery README
3
+ *
4
+ * Renders example components headlessly and captures PNG screenshots
5
+ * using bufferToHTML() + Playwright.
6
+ *
7
+ * Usage:
8
+ * cd vendor/silvery && bun run examples/screenshots/generate.tsx
9
+ *
10
+ * Prerequisites:
11
+ * bunx playwright install chromium
12
+ *
13
+ * Output:
14
+ * docs/images/dashboard.png - Multi-pane dashboard with borders and colors
15
+ * docs/images/task-list.png - Scrollable task list with selection
16
+ * docs/images/kanban.png - 3-column kanban board with cards
17
+ * docs/images/layout-feedback.png - Layout feedback with useContentRect() values
18
+ */
19
+
20
+ import { mkdir } from "node:fs/promises"
21
+ import { dirname, resolve } from "node:path"
22
+ import React, { useState } from "react"
23
+ import { render, createRenderer, ensureEngine, bufferToHTML } from "../../src/testing/index.tsx"
24
+ import { Box, Text, Divider, useContentRect, useApp } from "../../src/index.js"
25
+ import { createScreenshotter } from "../../src/screenshot.js"
26
+
27
+ // ============================================================================
28
+ // Output directory
29
+ // ============================================================================
30
+
31
+ const OUTPUT_DIR = resolve(dirname(import.meta.path), "../../docs/images")
32
+
33
+ // ============================================================================
34
+ // Screenshot Components
35
+ // These are static versions of the examples, frozen at a specific state
36
+ // for consistent screenshot output.
37
+ // ============================================================================
38
+
39
+ // --- 1. Dashboard -----------------------------------------------------------
40
+
41
+ function ProgressBar({ percent, width = 24 }: { percent: number; width?: number }): JSX.Element {
42
+ const filled = Math.round((percent / 100) * width)
43
+ const empty = width - filled
44
+ const dot = filled < width ? "╸" : ""
45
+ const filledBar = "━".repeat(Math.max(0, filled - (dot ? 1 : 0)))
46
+ const emptyBar = "─".repeat(Math.max(0, empty - (dot ? 0 : 0)))
47
+ return (
48
+ <Text>
49
+ <Text color="$success">{filledBar}</Text>
50
+ <Text color="$success">{dot}</Text>
51
+ <Text dim>{emptyBar}</Text>
52
+ </Text>
53
+ )
54
+ }
55
+
56
+ function DashboardScreenshot(): JSX.Element {
57
+ return (
58
+ <Box flexDirection="column" padding={1}>
59
+ <Box marginBottom={1}>
60
+ <Text bold color="$warning">
61
+ Dashboard
62
+ </Text>
63
+ </Box>
64
+
65
+ <Box flexGrow={1} flexDirection="row" gap={1}>
66
+ {/* System Stats pane (selected) */}
67
+ <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="$primary" padding={1}>
68
+ <Box marginBottom={1}>
69
+ <Text bold color="$primary">
70
+ System Stats
71
+ </Text>
72
+ </Box>
73
+ <Box flexDirection="column" gap={1}>
74
+ <Box flexDirection="row" justifyContent="space-between">
75
+ <Text>CPU Usage</Text>
76
+ <Box>
77
+ <Text bold color="$success">
78
+ 45%
79
+ </Text>
80
+ <Text color="$success"> +2%</Text>
81
+ </Box>
82
+ </Box>
83
+ <Box flexDirection="row" justifyContent="space-between">
84
+ <Text>Memory</Text>
85
+ <Box>
86
+ <Text bold color="$success">
87
+ 8.2 GB
88
+ </Text>
89
+ <Text color="$error"> -0.3</Text>
90
+ </Box>
91
+ </Box>
92
+ <Box flexDirection="row" justifyContent="space-between">
93
+ <Text>Disk</Text>
94
+ <Text bold color="$success">
95
+ 234 GB
96
+ </Text>
97
+ </Box>
98
+ <Box flexDirection="row" justifyContent="space-between">
99
+ <Text>Network</Text>
100
+ <Box>
101
+ <Text bold color="$success">
102
+ 1.2 Mb/s
103
+ </Text>
104
+ <Text color="$success"> +0.5</Text>
105
+ </Box>
106
+ </Box>
107
+ </Box>
108
+ </Box>
109
+
110
+ {/* Recent Activity pane */}
111
+ <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="$border" padding={1}>
112
+ <Box marginBottom={1}>
113
+ <Text bold>Recent Activity</Text>
114
+ </Box>
115
+ <Box flexDirection="column">
116
+ <Text>{">"} User login: admin</Text>
117
+ <Text>{" "}Backup completed</Text>
118
+ <Text>{" "}Config updated</Text>
119
+ <Text dim>{" "}Service restarted</Text>
120
+ <Text dim>{" "}Cache cleared</Text>
121
+ </Box>
122
+ </Box>
123
+
124
+ {/* Project Progress pane */}
125
+ <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="$border" padding={1}>
126
+ <Box marginBottom={1}>
127
+ <Text bold>Project Progress</Text>
128
+ </Box>
129
+ <Box flexDirection="column" gap={1}>
130
+ {[
131
+ { label: "Frontend", percent: 85 },
132
+ { label: "Backend", percent: 72 },
133
+ { label: "Testing", percent: 45 },
134
+ { label: "Docs", percent: 30 },
135
+ ].map((item) => (
136
+ <Box key={item.label} flexDirection="column">
137
+ <Box justifyContent="space-between">
138
+ <Text>{item.label}</Text>
139
+ <Text bold>{item.percent}%</Text>
140
+ </Box>
141
+ <ProgressBar percent={item.percent} />
142
+ </Box>
143
+ ))}
144
+ </Box>
145
+ </Box>
146
+ </Box>
147
+
148
+ <Box marginTop={1}>
149
+ <Text dim>
150
+ {" "}
151
+ Selected: Pane 1{" "}
152
+ <Text bold dim>
153
+ h/l
154
+ </Text>{" "}
155
+ navigate{" "}
156
+ <Text bold dim>
157
+ q
158
+ </Text>{" "}
159
+ quit
160
+ </Text>
161
+ </Box>
162
+ </Box>
163
+ )
164
+ }
165
+
166
+ // --- 2. Task List -----------------------------------------------------------
167
+
168
+ function TaskListScreenshot(): JSX.Element {
169
+ const tasks = [
170
+ { id: 1, title: "Review authentication refactor", completed: false, priority: "high" as const },
171
+ { id: 2, title: "Update API documentation", completed: true, priority: "medium" as const },
172
+ {
173
+ id: 3,
174
+ title: "Fix timezone handling in scheduler",
175
+ completed: false,
176
+ priority: "high" as const,
177
+ },
178
+ {
179
+ id: 4,
180
+ title: "Add rate limiting to endpoints",
181
+ completed: false,
182
+ priority: "medium" as const,
183
+ },
184
+ {
185
+ id: 5,
186
+ title: "Write integration tests for payments",
187
+ completed: true,
188
+ priority: "low" as const,
189
+ },
190
+ {
191
+ id: 6,
192
+ title: "Migrate user table to new schema",
193
+ completed: false,
194
+ priority: "high" as const,
195
+ },
196
+ { id: 7, title: "Set up staging environment", completed: false, priority: "low" as const },
197
+ { id: 8, title: "Refactor notification service", completed: true, priority: "medium" as const },
198
+ ]
199
+ const cursor = 2
200
+
201
+ const priorityLabels = { high: "P1", medium: "P2", low: "P3" }
202
+ const priorityColors = { high: "$error", medium: "$warning", low: "$success" }
203
+
204
+ return (
205
+ <Box flexDirection="column" padding={1}>
206
+ <Box marginBottom={1}>
207
+ <Text bold color="$warning">
208
+ Task List
209
+ </Text>
210
+ <Text dim>
211
+ {" "}
212
+ {tasks.filter((t) => t.completed).length}/{tasks.length} completed
213
+ </Text>
214
+ </Box>
215
+
216
+ <Box
217
+ flexGrow={1}
218
+ flexDirection="column"
219
+ borderStyle="round"
220
+ borderColor="$primary"
221
+ overflow="hidden"
222
+ paddingX={1}
223
+ >
224
+ {tasks.map((task, index) => {
225
+ const checkbox = task.completed ? "☑" : "☐"
226
+ const isSelected = index === cursor
227
+ const showSeparator = index < tasks.length - 1
228
+ const label = priorityLabels[task.priority]
229
+ const labelColor = priorityColors[task.priority]
230
+
231
+ return (
232
+ <Box key={task.id} flexDirection="column">
233
+ {isSelected ? (
234
+ <Text>
235
+ <Text backgroundColor="$primary" color="black">
236
+ {" "}
237
+ {checkbox} {task.title}{" "}
238
+ </Text>{" "}
239
+ <Text color={labelColor} bold>
240
+ {label}
241
+ </Text>
242
+ </Text>
243
+ ) : (
244
+ <Text strikethrough={task.completed} dim={task.completed}>
245
+ {checkbox} {task.title}{" "}
246
+ <Text color={labelColor} bold>
247
+ {label}
248
+ </Text>
249
+ </Text>
250
+ )}
251
+ {showSeparator && <Divider />}
252
+ </Box>
253
+ )
254
+ })}
255
+ </Box>
256
+
257
+ <Box marginTop={1} justifyContent="space-between">
258
+ <Text dim>
259
+ {" "}
260
+ <Text bold dim>
261
+ j/k
262
+ </Text>{" "}
263
+ navigate{" "}
264
+ <Text bold dim>
265
+ space
266
+ </Text>{" "}
267
+ toggle{" "}
268
+ <Text bold dim>
269
+ enter
270
+ </Text>{" "}
271
+ expand{" "}
272
+ <Text bold dim>
273
+ q
274
+ </Text>{" "}
275
+ quit
276
+ </Text>
277
+ <Text dim>
278
+ {" "}
279
+ <Text bold>3</Text>/8{" "}
280
+ </Text>
281
+ </Box>
282
+ </Box>
283
+ )
284
+ }
285
+
286
+ // --- 3. Kanban Board --------------------------------------------------------
287
+
288
+ function KanbanScreenshot(): JSX.Element {
289
+ const columns = [
290
+ {
291
+ id: "todo",
292
+ title: "To Do",
293
+ isSelected: true,
294
+ cards: [
295
+ {
296
+ title: "Design new landing page",
297
+ tags: ["design"],
298
+ isSelected: true,
299
+ },
300
+ { title: "Write API documentation", tags: ["docs"], isSelected: false },
301
+ { title: "Set up monitoring", tags: ["devops"], isSelected: false },
302
+ { title: "Create onboarding flow", tags: ["ux"], isSelected: false },
303
+ ],
304
+ },
305
+ {
306
+ id: "inProgress",
307
+ title: "In Progress",
308
+ isSelected: false,
309
+ cards: [
310
+ {
311
+ title: "User authentication",
312
+ tags: ["backend", "security"],
313
+ isSelected: false,
314
+ },
315
+ {
316
+ title: "Dashboard redesign",
317
+ tags: ["frontend", "design"],
318
+ isSelected: false,
319
+ },
320
+ { title: "API rate limiting", tags: ["backend"], isSelected: false },
321
+ ],
322
+ },
323
+ {
324
+ id: "done",
325
+ title: "Done",
326
+ isSelected: false,
327
+ cards: [
328
+ { title: "Project setup", tags: ["devops"], isSelected: false },
329
+ { title: "CI/CD pipeline", tags: ["devops"], isSelected: false },
330
+ { title: "Initial wireframes", tags: ["design"], isSelected: false },
331
+ ],
332
+ },
333
+ ]
334
+
335
+ const tagColors: Record<string, string> = {
336
+ frontend: "$info",
337
+ backend: "$accent",
338
+ design: "$warning",
339
+ devops: "$success",
340
+ docs: "$primary",
341
+ ux: "$muted",
342
+ security: "$error",
343
+ }
344
+
345
+ return (
346
+ <Box flexDirection="column" padding={1}>
347
+ <Box marginBottom={1}>
348
+ <Text bold color="$warning">
349
+ Kanban Board
350
+ </Text>
351
+ </Box>
352
+
353
+ <Box flexGrow={1} flexDirection="row" gap={1} overflow="hidden">
354
+ {columns.map((col) => (
355
+ <Box
356
+ key={col.id}
357
+ flexDirection="column"
358
+ flexGrow={1}
359
+ borderStyle="round"
360
+ borderColor={col.isSelected ? "$primary" : "$border"}
361
+ >
362
+ <Box backgroundColor={col.isSelected ? "$primary" : undefined} paddingX={1}>
363
+ <Text bold color={col.isSelected ? "black" : undefined}>
364
+ {col.title}
365
+ </Text>
366
+ <Text color={col.isSelected ? "black" : "$muted"}> ({col.cards.length})</Text>
367
+ </Box>
368
+
369
+ <Box flexDirection="column" paddingX={1} flexGrow={1} gap={1}>
370
+ {col.cards.map((card, idx) => (
371
+ <Box
372
+ key={idx}
373
+ flexDirection="column"
374
+ borderStyle="round"
375
+ borderColor={card.isSelected ? "$primary" : "$border"}
376
+ paddingX={1}
377
+ >
378
+ {card.isSelected ? (
379
+ <Text backgroundColor="$primary" color="black" bold>
380
+ {card.title}
381
+ </Text>
382
+ ) : (
383
+ <Text>{card.title}</Text>
384
+ )}
385
+ <Box gap={1}>
386
+ {card.tags.map((tag) => (
387
+ <Text key={tag} color={tagColors[tag] ?? "$muted"} dim>
388
+ #{tag}
389
+ </Text>
390
+ ))}
391
+ </Box>
392
+ </Box>
393
+ ))}
394
+ </Box>
395
+ </Box>
396
+ ))}
397
+ </Box>
398
+
399
+ <Text dim>
400
+ {" "}
401
+ <Text bold dim>
402
+ h/l
403
+ </Text>{" "}
404
+ column{" "}
405
+ <Text bold dim>
406
+ j/k
407
+ </Text>{" "}
408
+ card{" "}
409
+ <Text bold dim>
410
+ {"</>"}
411
+ </Text>{" "}
412
+ move{" "}
413
+ <Text bold dim>
414
+ q
415
+ </Text>{" "}
416
+ quit
417
+ </Text>
418
+ </Box>
419
+ )
420
+ }
421
+
422
+ // --- 4. Layout Feedback -----------------------------------------------------
423
+
424
+ function LayoutPane({ title, color, grow = 1 }: { title: string; color: string; grow?: number }): JSX.Element {
425
+ const rect = useContentRect()
426
+ return (
427
+ <Box flexGrow={grow} borderStyle="round" borderColor={color} padding={1} flexDirection="column">
428
+ <Text bold color={color}>
429
+ {title}
430
+ </Text>
431
+ <Box marginTop={1}>
432
+ <Text dim>
433
+ {rect.width}x{rect.height} at ({rect.x},{rect.y})
434
+ </Text>
435
+ </Box>
436
+ </Box>
437
+ )
438
+ }
439
+
440
+ function LayoutFeedbackScreenshot(): JSX.Element {
441
+ return (
442
+ <Box flexDirection="column" padding={1}>
443
+ <Box marginBottom={1}>
444
+ <Text bold color="$warning">
445
+ Layout Feedback Demo
446
+ </Text>
447
+ </Box>
448
+
449
+ <Box flexDirection="row" gap={1} height={8}>
450
+ <LayoutPane title="Sidebar" color="$success" grow={1} />
451
+ <LayoutPane title="Main Content" color="$primary" grow={2} />
452
+ <LayoutPane title="Detail" color="$info" grow={1} />
453
+ </Box>
454
+
455
+ <Box marginTop={1} borderStyle="single" borderColor="$border" padding={1}>
456
+ <Box flexDirection="column">
457
+ <Text bold>useContentRect() — components know their size during render</Text>
458
+ <Text dim>No ResizeObserver, no second render, no layout jank.</Text>
459
+ <Text dim>Each pane above displays its own dimensions via useContentRect().</Text>
460
+ </Box>
461
+ </Box>
462
+
463
+ <Text dim>
464
+ {" "}
465
+ <Text bold dim>
466
+ i
467
+ </Text>{" "}
468
+ inspect{" "}
469
+ <Text bold dim>
470
+ Esc
471
+ </Text>{" "}
472
+ quit
473
+ </Text>
474
+ </Box>
475
+ )
476
+ }
477
+
478
+ // ============================================================================
479
+ // Screenshot Generation
480
+ // ============================================================================
481
+
482
+ interface ScreenshotConfig {
483
+ name: string
484
+ filename: string
485
+ element: JSX.Element
486
+ cols: number
487
+ rows: number
488
+ }
489
+
490
+ const screenshots: ScreenshotConfig[] = [
491
+ {
492
+ name: "Dashboard",
493
+ filename: "dashboard.png",
494
+ element: <DashboardScreenshot />,
495
+ cols: 120,
496
+ rows: 25,
497
+ },
498
+ {
499
+ name: "Task List",
500
+ filename: "task-list.png",
501
+ element: <TaskListScreenshot />,
502
+ cols: 80,
503
+ rows: 23,
504
+ },
505
+ {
506
+ name: "Kanban Board",
507
+ filename: "kanban.png",
508
+ element: <KanbanScreenshot />,
509
+ cols: 120,
510
+ rows: 27,
511
+ },
512
+ {
513
+ name: "Layout Feedback",
514
+ filename: "layout-feedback.png",
515
+ element: <LayoutFeedbackScreenshot />,
516
+ cols: 90,
517
+ rows: 20,
518
+ },
519
+ ]
520
+
521
+ async function main() {
522
+ await mkdir(OUTPUT_DIR, { recursive: true })
523
+
524
+ await ensureEngine()
525
+ await using screenshotter = createScreenshotter()
526
+
527
+ for (const config of screenshots) {
528
+ const { name, filename, element, cols, rows } = config
529
+ const outputPath = resolve(OUTPUT_DIR, filename)
530
+
531
+ console.log(`Generating ${name} (${cols}x${rows})...`)
532
+
533
+ const app = render(element, { cols, rows })
534
+ const buffer = app.lastBuffer()
535
+
536
+ if (!buffer) {
537
+ console.error(` ERROR: No buffer for ${name}`)
538
+ app.unmount()
539
+ continue
540
+ }
541
+
542
+ const html = bufferToHTML(buffer, {
543
+ fontFamily: "JetBrains Mono, Menlo, Consolas, monospace",
544
+ fontSize: 14,
545
+ theme: "dark",
546
+ })
547
+
548
+ await screenshotter.capture(html, outputPath)
549
+ console.log(` Saved: ${outputPath}`)
550
+
551
+ app.unmount()
552
+ }
553
+
554
+ console.log("\nDone! Generated screenshots:")
555
+ for (const config of screenshots) {
556
+ console.log(` docs/images/${config.filename}`)
557
+ }
558
+ }
559
+
560
+ main().catch((err) => {
561
+ console.error("Screenshot generation failed:", err)
562
+ process.exit(1)
563
+ })