silvery 0.3.0 → 0.4.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 (88) hide show
  1. package/README.md +41 -145
  2. package/package.json +64 -12
  3. package/src/index.ts +67 -1
  4. package/src/runtime.ts +4 -0
  5. package/src/theme.ts +4 -0
  6. package/src/ui/animation.ts +2 -0
  7. package/src/ui/ansi.ts +2 -0
  8. package/src/ui/cli.ts +2 -0
  9. package/src/ui/display.ts +2 -0
  10. package/src/ui/image.ts +2 -0
  11. package/src/ui/input.ts +2 -0
  12. package/src/ui/progress.ts +2 -0
  13. package/src/ui/react.ts +2 -0
  14. package/src/ui/utils.ts +2 -0
  15. package/src/ui/wrappers.ts +2 -0
  16. package/src/ui.ts +4 -0
  17. package/examples/CLAUDE.md +0 -75
  18. package/examples/_banner.tsx +0 -60
  19. package/examples/cli.ts +0 -228
  20. package/examples/index.md +0 -101
  21. package/examples/inline/inline-nontty.tsx +0 -98
  22. package/examples/inline/inline-progress.tsx +0 -79
  23. package/examples/inline/inline-simple.tsx +0 -63
  24. package/examples/inline/scrollback.tsx +0 -185
  25. package/examples/interactive/_input-debug.tsx +0 -110
  26. package/examples/interactive/_stdin-test.ts +0 -71
  27. package/examples/interactive/_textarea-bare.tsx +0 -45
  28. package/examples/interactive/aichat/components.tsx +0 -468
  29. package/examples/interactive/aichat/index.tsx +0 -207
  30. package/examples/interactive/aichat/script.ts +0 -460
  31. package/examples/interactive/aichat/state.ts +0 -326
  32. package/examples/interactive/aichat/types.ts +0 -19
  33. package/examples/interactive/app-todo.tsx +0 -198
  34. package/examples/interactive/async-data.tsx +0 -208
  35. package/examples/interactive/cli-wizard.tsx +0 -332
  36. package/examples/interactive/clipboard.tsx +0 -183
  37. package/examples/interactive/components.tsx +0 -463
  38. package/examples/interactive/data-explorer.tsx +0 -506
  39. package/examples/interactive/dev-tools.tsx +0 -379
  40. package/examples/interactive/explorer.tsx +0 -747
  41. package/examples/interactive/gallery.tsx +0 -652
  42. package/examples/interactive/inline-bench.tsx +0 -136
  43. package/examples/interactive/kanban.tsx +0 -267
  44. package/examples/interactive/layout-ref.tsx +0 -185
  45. package/examples/interactive/outline.tsx +0 -171
  46. package/examples/interactive/paste-demo.tsx +0 -198
  47. package/examples/interactive/scroll.tsx +0 -77
  48. package/examples/interactive/search-filter.tsx +0 -240
  49. package/examples/interactive/task-list.tsx +0 -279
  50. package/examples/interactive/terminal.tsx +0 -798
  51. package/examples/interactive/textarea.tsx +0 -103
  52. package/examples/interactive/theme.tsx +0 -336
  53. package/examples/interactive/transform.tsx +0 -256
  54. package/examples/interactive/virtual-10k.tsx +0 -413
  55. package/examples/kitty/canvas.tsx +0 -519
  56. package/examples/kitty/generate-samples.ts +0 -236
  57. package/examples/kitty/image-component.tsx +0 -273
  58. package/examples/kitty/images.tsx +0 -604
  59. package/examples/kitty/input.tsx +0 -371
  60. package/examples/kitty/keys.tsx +0 -378
  61. package/examples/kitty/paint.tsx +0 -1017
  62. package/examples/layout/dashboard.tsx +0 -551
  63. package/examples/layout/live-resize.tsx +0 -290
  64. package/examples/layout/overflow.tsx +0 -51
  65. package/examples/playground/README.md +0 -69
  66. package/examples/playground/build.ts +0 -61
  67. package/examples/playground/index.html +0 -420
  68. package/examples/playground/playground-app.tsx +0 -416
  69. package/examples/runtime/elm-counter.tsx +0 -206
  70. package/examples/runtime/hello-runtime.tsx +0 -73
  71. package/examples/runtime/pipe-composition.tsx +0 -184
  72. package/examples/runtime/run-counter.tsx +0 -78
  73. package/examples/runtime/runtime-counter.tsx +0 -197
  74. package/examples/screenshots/generate.tsx +0 -563
  75. package/examples/scrollback-perf.tsx +0 -230
  76. package/examples/viewer.tsx +0 -654
  77. package/examples/web/build.ts +0 -365
  78. package/examples/web/canvas-app.tsx +0 -80
  79. package/examples/web/canvas.html +0 -89
  80. package/examples/web/dom-app.tsx +0 -81
  81. package/examples/web/dom.html +0 -113
  82. package/examples/web/showcase-app.tsx +0 -107
  83. package/examples/web/showcase.html +0 -34
  84. package/examples/web/showcases/index.tsx +0 -56
  85. package/examples/web/viewer-app.tsx +0 -555
  86. package/examples/web/viewer.html +0 -30
  87. package/examples/web/xterm-app.tsx +0 -105
  88. package/examples/web/xterm.html +0 -118
@@ -1,413 +0,0 @@
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
- }