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,413 @@
1
+ /**
2
+ * Virtual Scroll Benchmark — 10,000 Items
3
+ *
4
+ * Demonstrates that VirtualList handles massive datasets with instant scrolling.
5
+ * Only visible items + overscan are rendered, regardless of total count.
6
+ *
7
+ * Demonstrates:
8
+ * - VirtualList with 10,000 items and variable heights
9
+ * - Smooth j/k navigation with position indicator
10
+ * - useContentRect() for adaptive column count
11
+ * - Page up/down with large jumps
12
+ * - Visual item variety (priorities, tags, progress bars)
13
+ *
14
+ * Usage: bun run examples/virtual-10k/index.tsx
15
+ *
16
+ * Controls:
17
+ * j/k or Up/Down - Navigate one item
18
+ * d/u - Half-page down/up
19
+ * g/G - Jump to first/last
20
+ * / - Search by number
21
+ * Esc/q or Ctrl+C - Quit
22
+ */
23
+
24
+ import React, { useState, useCallback, useMemo } from "react"
25
+ import { Box, Text, Strong, Kbd, Muted, Divider, VirtualList, useContentRect } from "../../src/index.js"
26
+ import { run, useInput, type Key } from "@silvery/term/runtime"
27
+ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
28
+
29
+ export const meta: ExampleMeta = {
30
+ name: "Virtual 10K",
31
+ description: "VirtualList scrolling through 10,000 items with instant navigation",
32
+ features: ["VirtualList", "10K items", "useContentRect()", "variable itemHeight"],
33
+ }
34
+
35
+ // ============================================================================
36
+ // Types
37
+ // ============================================================================
38
+
39
+ interface Item {
40
+ id: number
41
+ title: string
42
+ priority: "P0" | "P1" | "P2" | "P3"
43
+ status: "todo" | "in-progress" | "done" | "blocked"
44
+ tags: string[]
45
+ progress: number
46
+ description: string
47
+ }
48
+
49
+ // ============================================================================
50
+ // Data Generation
51
+ // ============================================================================
52
+
53
+ const PRIORITIES: Item["priority"][] = ["P0", "P1", "P2", "P3"]
54
+ const STATUSES: Item["status"][] = ["todo", "in-progress", "done", "blocked"]
55
+ const TAG_POOL = [
56
+ "frontend",
57
+ "backend",
58
+ "api",
59
+ "database",
60
+ "security",
61
+ "performance",
62
+ "ux",
63
+ "docs",
64
+ "testing",
65
+ "devops",
66
+ "mobile",
67
+ "infra",
68
+ ]
69
+
70
+ const ADJECTIVES = [
71
+ "Implement",
72
+ "Fix",
73
+ "Refactor",
74
+ "Optimize",
75
+ "Design",
76
+ "Review",
77
+ "Update",
78
+ "Add",
79
+ "Remove",
80
+ "Migrate",
81
+ "Configure",
82
+ "Deploy",
83
+ ]
84
+
85
+ const NOUNS = [
86
+ "authentication flow",
87
+ "database schema",
88
+ "API endpoint",
89
+ "caching layer",
90
+ "error handling",
91
+ "test suite",
92
+ "CI pipeline",
93
+ "monitoring",
94
+ "rate limiter",
95
+ "search index",
96
+ "notification system",
97
+ "user dashboard",
98
+ "payment processing",
99
+ "file upload",
100
+ "websocket handler",
101
+ "session manager",
102
+ ]
103
+
104
+ function seededRandom(seed: number): () => number {
105
+ let s = seed
106
+ return () => {
107
+ s = (s * 1664525 + 1013904223) & 0x7fffffff
108
+ return s / 0x7fffffff
109
+ }
110
+ }
111
+
112
+ function generateItems(count: number): Item[] {
113
+ const rng = seededRandom(42)
114
+ const items: Item[] = []
115
+
116
+ for (let i = 0; i < count; i++) {
117
+ const adj = ADJECTIVES[Math.floor(rng() * ADJECTIVES.length)]!
118
+ const noun = NOUNS[Math.floor(rng() * NOUNS.length)]!
119
+ const priority = PRIORITIES[Math.floor(rng() * PRIORITIES.length)]!
120
+ const status = STATUSES[Math.floor(rng() * STATUSES.length)]!
121
+ const tagCount = 1 + Math.floor(rng() * 3)
122
+ const tags: string[] = []
123
+ for (let t = 0; t < tagCount; t++) {
124
+ const tag = TAG_POOL[Math.floor(rng() * TAG_POOL.length)]!
125
+ if (!tags.includes(tag)) tags.push(tag)
126
+ }
127
+ const progress = status === "done" ? 100 : status === "todo" ? 0 : Math.floor(rng() * 90) + 5
128
+
129
+ items.push({
130
+ id: i + 1,
131
+ title: `${adj} ${noun}`,
132
+ priority,
133
+ status,
134
+ tags,
135
+ progress,
136
+ description: `Task #${i + 1}: ${adj.toLowerCase()} the ${noun} for improved reliability.`,
137
+ })
138
+ }
139
+
140
+ return items
141
+ }
142
+
143
+ const TOTAL_ITEMS = 10_000
144
+ const ALL_ITEMS = generateItems(TOTAL_ITEMS)
145
+
146
+ // ============================================================================
147
+ // Components
148
+ // ============================================================================
149
+
150
+ const PRIORITY_COLORS: Record<Item["priority"], string> = {
151
+ P0: "$error",
152
+ P1: "$warning",
153
+ P2: "$info",
154
+ P3: "$muted",
155
+ }
156
+
157
+ const STATUS_ICONS: Record<Item["status"], string> = {
158
+ todo: "○",
159
+ "in-progress": "◔",
160
+ done: "●",
161
+ blocked: "■",
162
+ }
163
+
164
+ const STATUS_COLORS: Record<Item["status"], string> = {
165
+ todo: "$muted",
166
+ "in-progress": "$warning",
167
+ done: "$success",
168
+ blocked: "$error",
169
+ }
170
+
171
+ function ProgressBar({ percent, width: barWidth }: { percent: number; width: number }): JSX.Element {
172
+ const effectiveWidth = Math.max(5, barWidth)
173
+ const filled = Math.round((percent / 100) * effectiveWidth)
174
+ const empty = effectiveWidth - filled
175
+
176
+ return (
177
+ <Text>
178
+ <Text color="$success">{"█".repeat(filled)}</Text>
179
+ <Text dim>{"░".repeat(empty)}</Text>
180
+ </Text>
181
+ )
182
+ }
183
+
184
+ function ItemRow({
185
+ item,
186
+ isSelected,
187
+ showDetail,
188
+ }: {
189
+ item: Item
190
+ isSelected: boolean
191
+ showDetail: boolean
192
+ }): JSX.Element {
193
+ const idStr = String(item.id).padStart(5, " ")
194
+
195
+ return (
196
+ <Box flexDirection="column" paddingX={1} backgroundColor={isSelected ? "$primary" : undefined}>
197
+ <Box>
198
+ <Text color={STATUS_COLORS[item.status]}>{STATUS_ICONS[item.status]}</Text>
199
+ <Text dim> {idStr} </Text>
200
+ <Text bold color={PRIORITY_COLORS[item.priority]}>
201
+ {item.priority}
202
+ </Text>
203
+ <Text> </Text>
204
+ <Text bold={isSelected}>{item.title}</Text>
205
+ <Text> </Text>
206
+ {item.tags.map((tag) => (
207
+ <Text key={tag} dim color="$info">
208
+ {" "}
209
+ #{tag}
210
+ </Text>
211
+ ))}
212
+ </Box>
213
+ {showDetail && (
214
+ <Box paddingLeft={8}>
215
+ <Text dim>{item.description}</Text>
216
+ <Text> </Text>
217
+ <ProgressBar percent={item.progress} width={10} />
218
+ <Text dim> {item.progress}%</Text>
219
+ </Box>
220
+ )}
221
+ </Box>
222
+ )
223
+ }
224
+
225
+ function ScrollIndicator({ current, total, width }: { current: number; total: number; width: number }): JSX.Element {
226
+ const percent = total > 0 ? Math.round(((current + 1) / total) * 100) : 0
227
+
228
+ // Progress bar
229
+ const barWidth = Math.max(10, Math.min(30, width - 40))
230
+ const filled = Math.round((percent / 100) * barWidth)
231
+ const empty = barWidth - filled
232
+
233
+ return (
234
+ <Box gap={2} paddingX={1}>
235
+ <Strong color="$primary">{(current + 1).toLocaleString()}</Strong>
236
+ <Text dim>of</Text>
237
+ <Strong>{total.toLocaleString()}</Strong>
238
+ <Text>
239
+ <Text color="$primary">{"█".repeat(filled)}</Text>
240
+ <Text dim>{"░".repeat(empty)}</Text>
241
+ </Text>
242
+ <Strong color="$primary">{percent}%</Strong>
243
+ </Box>
244
+ )
245
+ }
246
+
247
+ function StatsBar({ items }: { items: Item[] }): JSX.Element {
248
+ const stats = useMemo(() => {
249
+ let p0 = 0,
250
+ p1 = 0,
251
+ p2 = 0,
252
+ p3 = 0
253
+ let todo = 0,
254
+ inProg = 0,
255
+ done = 0,
256
+ blocked = 0
257
+ for (const item of items) {
258
+ if (item.priority === "P0") p0++
259
+ else if (item.priority === "P1") p1++
260
+ else if (item.priority === "P2") p2++
261
+ else p3++
262
+ if (item.status === "todo") todo++
263
+ else if (item.status === "in-progress") inProg++
264
+ else if (item.status === "done") done++
265
+ else blocked++
266
+ }
267
+ return { p0, p1, p2, p3, todo, inProg, done, blocked }
268
+ }, [items])
269
+
270
+ return (
271
+ <Box gap={2} paddingX={1}>
272
+ <Strong color="$error">P0:{stats.p0}</Strong>
273
+ <Strong color="$warning">P1:{stats.p1}</Strong>
274
+ <Text color="$info">P2:{stats.p2}</Text>
275
+ <Text dim>P3:{stats.p3}</Text>
276
+ <Text dim>|</Text>
277
+ <Text color="$muted">
278
+ {STATUS_ICONS.todo} {stats.todo}
279
+ </Text>
280
+ <Text color="$warning">
281
+ {STATUS_ICONS["in-progress"]} {stats.inProg}
282
+ </Text>
283
+ <Text color="$success">
284
+ {STATUS_ICONS.done} {stats.done}
285
+ </Text>
286
+ <Text color="$error">
287
+ {STATUS_ICONS.blocked} {stats.blocked}
288
+ </Text>
289
+ </Box>
290
+ )
291
+ }
292
+
293
+ // ============================================================================
294
+ // Main App
295
+ // ============================================================================
296
+
297
+ function VirtualBenchmark(): JSX.Element {
298
+ const { width, height } = useContentRect()
299
+ const [cursor, setCursor] = useState(0)
300
+ const [showDetail, setShowDetail] = useState(false)
301
+
302
+ // Calculate available list height
303
+ // stats (1) + separator (1) + scroll indicator (1) + help (1) + borders
304
+ const listHeight = Math.max(5, height - 5)
305
+ const halfPage = Math.max(1, Math.floor(listHeight / 2))
306
+
307
+ const itemHeight = useCallback(
308
+ (_item: Item, index: number) => {
309
+ if (showDetail && index === cursor) return 2
310
+ return 1
311
+ },
312
+ [showDetail, cursor],
313
+ )
314
+
315
+ useInput(
316
+ useCallback(
317
+ (input: string, key: Key) => {
318
+ if (input === "q" || key.escape || (key.ctrl && input === "c")) {
319
+ return "exit"
320
+ }
321
+
322
+ // Navigation
323
+ if (input === "j" || key.downArrow) {
324
+ setCursor((c) => Math.min(TOTAL_ITEMS - 1, c + 1))
325
+ }
326
+ if (input === "k" || key.upArrow) {
327
+ setCursor((c) => Math.max(0, c - 1))
328
+ }
329
+
330
+ // Half-page
331
+ if (input === "d" || key.pageDown) {
332
+ setCursor((c) => Math.min(TOTAL_ITEMS - 1, c + halfPage))
333
+ }
334
+ if (input === "u" || key.pageUp) {
335
+ setCursor((c) => Math.max(0, c - halfPage))
336
+ }
337
+
338
+ // Jump to start/end
339
+ if (input === "g" || key.home) {
340
+ setCursor(0)
341
+ }
342
+ if (input === "G" || key.end) {
343
+ setCursor(TOTAL_ITEMS - 1)
344
+ }
345
+
346
+ // Toggle detail view
347
+ if (key.return || input === " ") {
348
+ setShowDetail((d) => !d)
349
+ }
350
+ },
351
+ [halfPage],
352
+ ),
353
+ )
354
+
355
+ return (
356
+ <Box flexDirection="column" width="100%" height="100%">
357
+ {/* Stats */}
358
+ <StatsBar items={ALL_ITEMS} />
359
+
360
+ {/* Separator */}
361
+ <Box paddingX={1}>
362
+ <Divider />
363
+ </Box>
364
+
365
+ {/* Virtual list */}
366
+ <Box flexGrow={1}>
367
+ <VirtualList
368
+ items={ALL_ITEMS}
369
+ height={listHeight}
370
+ itemHeight={itemHeight}
371
+ scrollTo={cursor}
372
+ overscan={5}
373
+ renderItem={(item, index) => (
374
+ <ItemRow
375
+ key={item.id}
376
+ item={item}
377
+ isSelected={index === cursor}
378
+ showDetail={showDetail && index === cursor}
379
+ />
380
+ )}
381
+ />
382
+ </Box>
383
+
384
+ {/* Scroll position */}
385
+ <ScrollIndicator current={cursor} total={TOTAL_ITEMS} width={width} />
386
+
387
+ {/* Help */}
388
+ <Box paddingX={1} justifyContent="center">
389
+ <Muted>
390
+ <Kbd>j/k</Kbd> navigate <Kbd>d/u</Kbd> half-page <Kbd>g/G</Kbd> start/end <Kbd>Enter</Kbd> detail{" "}
391
+ <Kbd>Esc/q</Kbd> quit
392
+ </Muted>
393
+ </Box>
394
+ </Box>
395
+ )
396
+ }
397
+
398
+ // ============================================================================
399
+ // Main
400
+ // ============================================================================
401
+
402
+ async function main() {
403
+ const handle = await run(
404
+ <ExampleBanner meta={meta} controls="j/k navigate d/u half-page g/G start/end Enter detail Esc/q quit">
405
+ <VirtualBenchmark />
406
+ </ExampleBanner>,
407
+ )
408
+ await handle.waitUntilExit()
409
+ }
410
+
411
+ if (import.meta.main) {
412
+ main().catch(console.error)
413
+ }