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,551 @@
1
+ /**
2
+ * Dashboard Example
3
+ *
4
+ * A btop-style responsive dashboard demonstrating:
5
+ * - Tab navigation with compound Tabs component
6
+ * - Live-updating metrics with sparklines
7
+ * - Responsive 2-column / 1-column layout via useContentRect()
8
+ * - Semantic theme colors with severity-based color coding
9
+ * - Flex-based progress bars
10
+ */
11
+
12
+ import React, { useState } from "react"
13
+ import {
14
+ render,
15
+ Box,
16
+ Text,
17
+ H2,
18
+ Strong,
19
+ Small,
20
+ Muted,
21
+ Kbd,
22
+ Tabs,
23
+ TabList,
24
+ Tab,
25
+ TabPanel,
26
+ ProgressBar,
27
+ useContentRect,
28
+ useInput,
29
+ useApp,
30
+ useInterval,
31
+ createTerm,
32
+ type Key,
33
+ } from "../../src/index.js"
34
+ import { ExampleBanner, type ExampleMeta } from "../_banner.js"
35
+
36
+ export const meta: ExampleMeta = {
37
+ name: "Dashboard",
38
+ description: "Responsive multi-pane dashboard with live metrics and charts",
39
+ demo: true,
40
+ features: ["Box flexGrow", "useContentRect()", "responsive", "live data", "sparklines"],
41
+ }
42
+
43
+ // ============================================================================
44
+ // Sparkline
45
+ // ============================================================================
46
+
47
+ const SPARK_CHARS = "▁▂▃▄▅▆▇█"
48
+
49
+ function sparkline(values: number[], max: number): string {
50
+ return values.map((v) => SPARK_CHARS[Math.round((v / max) * 7)] ?? SPARK_CHARS[0]).join("")
51
+ }
52
+
53
+ // ============================================================================
54
+ // Data Helpers
55
+ // ============================================================================
56
+
57
+ function jitter(base: number, range: number): number {
58
+ return Math.max(0, Math.min(100, base + (Math.random() - 0.5) * range))
59
+ }
60
+
61
+ function initHistory(base: number, range: number, len: number): number[] {
62
+ return Array.from({ length: len }, () => jitter(base, range))
63
+ }
64
+
65
+ function pushHistory(history: number[], value: number): number[] {
66
+ const next = [...history]
67
+ if (next.length >= 20) next.shift()
68
+ next.push(value)
69
+ return next
70
+ }
71
+
72
+ function severityColor(pct: number): string {
73
+ if (pct > 80) return "$error"
74
+ if (pct > 60) return "$warning"
75
+ return "$success"
76
+ }
77
+
78
+ // ============================================================================
79
+ // State
80
+ // ============================================================================
81
+
82
+ interface CoreMetrics {
83
+ usage: number
84
+ history: number[]
85
+ }
86
+
87
+ interface MemoryMetrics {
88
+ used: number
89
+ cached: number
90
+ buffers: number
91
+ free: number
92
+ swap: number
93
+ swapTotal: number
94
+ history: number[]
95
+ }
96
+
97
+ interface NetworkMetrics {
98
+ downloadRate: number
99
+ uploadRate: number
100
+ downloadHistory: number[]
101
+ uploadHistory: number[]
102
+ connections: number
103
+ packetsIn: number
104
+ packetsOut: number
105
+ }
106
+
107
+ interface ProcessInfo {
108
+ pid: number
109
+ name: string
110
+ cpu: number
111
+ mem: number
112
+ status: string
113
+ }
114
+
115
+ function createInitialState() {
116
+ const cores: CoreMetrics[] = Array.from({ length: 8 }, (_, i) => ({
117
+ usage: 20 + Math.random() * 60,
118
+ history: initHistory(30 + i * 5, 20, 20),
119
+ }))
120
+
121
+ const memory: MemoryMetrics = {
122
+ used: 8.2,
123
+ cached: 3.1,
124
+ buffers: 1.4,
125
+ free: 3.3,
126
+ swap: 0.8,
127
+ swapTotal: 4.0,
128
+ history: initHistory(55, 10, 20),
129
+ }
130
+
131
+ const network: NetworkMetrics = {
132
+ downloadRate: 42.5,
133
+ uploadRate: 12.3,
134
+ downloadHistory: initHistory(40, 30, 20),
135
+ uploadHistory: initHistory(12, 10, 20),
136
+ connections: 147,
137
+ packetsIn: 1842,
138
+ packetsOut: 923,
139
+ }
140
+
141
+ const processes: ProcessInfo[] = [
142
+ { pid: 1201, name: "node", cpu: 24.3, mem: 4.2, status: "running" },
143
+ { pid: 892, name: "chrome", cpu: 18.7, mem: 12.1, status: "running" },
144
+ { pid: 3456, name: "vscode", cpu: 12.1, mem: 8.4, status: "running" },
145
+ { pid: 2103, name: "postgres", cpu: 8.9, mem: 3.7, status: "sleeping" },
146
+ { pid: 4521, name: "docker", cpu: 6.2, mem: 5.1, status: "running" },
147
+ { pid: 1893, name: "nginx", cpu: 3.4, mem: 1.2, status: "sleeping" },
148
+ { pid: 7234, name: "redis", cpu: 2.1, mem: 0.8, status: "sleeping" },
149
+ { pid: 5612, name: "bun", cpu: 1.8, mem: 2.3, status: "running" },
150
+ ]
151
+
152
+ return { cores, memory, network, processes }
153
+ }
154
+
155
+ function tickState(prev: ReturnType<typeof createInitialState>) {
156
+ const cores = prev.cores.map((core) => {
157
+ const usage = jitter(core.usage, 15)
158
+ return { usage, history: pushHistory(core.history, usage) }
159
+ })
160
+
161
+ const totalMem = prev.memory.used + prev.memory.cached + prev.memory.buffers + prev.memory.free
162
+ const usedJitter = (jitter((prev.memory.used / totalMem) * 100, 3) / 100) * totalMem
163
+ const memory: MemoryMetrics = {
164
+ ...prev.memory,
165
+ used: Math.max(4, usedJitter),
166
+ swap: (jitter((prev.memory.swap / prev.memory.swapTotal) * 100, 5) / 100) * prev.memory.swapTotal,
167
+ history: pushHistory(prev.memory.history, (usedJitter / totalMem) * 100),
168
+ }
169
+
170
+ const downloadRate = jitter(prev.network.downloadRate, 20)
171
+ const uploadRate = jitter(prev.network.uploadRate, 8)
172
+ const network: NetworkMetrics = {
173
+ downloadRate,
174
+ uploadRate,
175
+ downloadHistory: pushHistory(prev.network.downloadHistory, downloadRate),
176
+ uploadHistory: pushHistory(prev.network.uploadHistory, uploadRate),
177
+ connections: Math.max(50, Math.round(jitter(prev.network.connections, 20))),
178
+ packetsIn: Math.max(100, Math.round(jitter(prev.network.packetsIn, 200))),
179
+ packetsOut: Math.max(50, Math.round(jitter(prev.network.packetsOut, 100))),
180
+ }
181
+
182
+ const processes = prev.processes.map((p) => ({
183
+ ...p,
184
+ cpu: Math.max(0.1, jitter(p.cpu, 4)),
185
+ mem: Math.max(0.1, jitter(p.mem, 1)),
186
+ }))
187
+
188
+ return { cores, memory, network, processes }
189
+ }
190
+
191
+ // ============================================================================
192
+ // Components
193
+ // ============================================================================
194
+
195
+ function SectionHeader({ children }: { children: React.ReactNode }): JSX.Element {
196
+ return <H2>{children}</H2>
197
+ }
198
+
199
+ function LabelValue({ label, value, color }: { label: string; value: string; color?: string }): JSX.Element {
200
+ return (
201
+ <Box gap={1}>
202
+ <Muted>{label}</Muted>
203
+ <Text color={color}>
204
+ <Strong>{value}</Strong>
205
+ </Text>
206
+ </Box>
207
+ )
208
+ }
209
+
210
+ // --- CPU Tab ---
211
+
212
+ function CpuCore({ index, core }: { index: number; core: CoreMetrics }): JSX.Element {
213
+ const pct = Math.round(core.usage)
214
+ const color = severityColor(pct)
215
+ return (
216
+ <Box flexDirection="column">
217
+ <Box justifyContent="space-between">
218
+ <Text>
219
+ Core {index}{" "}
220
+ <Text color={color}>
221
+ <Strong>{String(pct).padStart(3)}%</Strong>
222
+ </Text>
223
+ </Text>
224
+ <Small>{sparkline(core.history, 100)}</Small>
225
+ </Box>
226
+ <ProgressBar value={core.usage / 100} color={color} showPercentage={false} />
227
+ </Box>
228
+ )
229
+ }
230
+
231
+ function CpuPane({ cores }: { cores: CoreMetrics[] }): JSX.Element {
232
+ const avgCpu = cores.reduce((sum, c) => sum + c.usage, 0) / cores.length
233
+ const maxCpu = Math.max(...cores.map((c) => c.usage))
234
+ const load1 = ((avgCpu / 100) * 8 * 0.8 + Math.random() * 0.5).toFixed(2)
235
+ const load5 = ((avgCpu / 100) * 8 * 0.7 + Math.random() * 0.3).toFixed(2)
236
+ const load15 = ((avgCpu / 100) * 8 * 0.6 + Math.random() * 0.2).toFixed(2)
237
+
238
+ return (
239
+ <Box flexDirection="column" gap={1} flexGrow={1}>
240
+ <SectionHeader>CPU</SectionHeader>
241
+ <Box gap={2}>
242
+ <LabelValue label="Avg:" value={`${Math.round(avgCpu)}%`} color={severityColor(avgCpu)} />
243
+ <LabelValue label="Max:" value={`${Math.round(maxCpu)}%`} color={severityColor(maxCpu)} />
244
+ <LabelValue label="Load:" value={`${load1} ${load5} ${load15}`} />
245
+ </Box>
246
+ {cores.map((core, i) => (
247
+ <CpuCore key={i} index={i} core={core} />
248
+ ))}
249
+ </Box>
250
+ )
251
+ }
252
+
253
+ // --- Memory Tab ---
254
+
255
+ function StackedBar({ segments }: { segments: { value: number; color: string; char?: string }[] }): JSX.Element {
256
+ return (
257
+ <Box>
258
+ {segments.map((seg, i) => (
259
+ <Box key={i} flexGrow={Math.max(1, Math.round(seg.value * 100))}>
260
+ <Text color={seg.color}>{(seg.char ?? "█").repeat(80)}</Text>
261
+ </Box>
262
+ ))}
263
+ </Box>
264
+ )
265
+ }
266
+
267
+ function MemoryPane({ memory }: { memory: MemoryMetrics }): JSX.Element {
268
+ const total = memory.used + memory.cached + memory.buffers + memory.free
269
+ const usedPct = (memory.used / total) * 100
270
+ const swapPct = (memory.swap / memory.swapTotal) * 100
271
+
272
+ return (
273
+ <Box flexDirection="column" gap={1} flexGrow={1}>
274
+ <SectionHeader>Memory</SectionHeader>
275
+ <Box gap={2}>
276
+ <LabelValue label="Total:" value={`${total.toFixed(1)} GB`} />
277
+ <LabelValue label="Used:" value={`${memory.used.toFixed(1)} GB`} color={severityColor(usedPct)} />
278
+ </Box>
279
+ <Box flexDirection="column">
280
+ <Muted>Memory Breakdown</Muted>
281
+ <StackedBar
282
+ segments={[
283
+ { value: memory.used / total, color: severityColor(usedPct) },
284
+ { value: memory.cached / total, color: "$info" },
285
+ { value: memory.buffers / total, color: "$primary" },
286
+ { value: memory.free / total, color: "$muted", char: "░" },
287
+ ]}
288
+ />
289
+ <Box gap={2}>
290
+ <Text color={severityColor(usedPct)}>
291
+ {"█"} Used {memory.used.toFixed(1)}G
292
+ </Text>
293
+ <Text color="$info">
294
+ {"█"} Cache {memory.cached.toFixed(1)}G
295
+ </Text>
296
+ <Text color="$primary">
297
+ {"█"} Buf {memory.buffers.toFixed(1)}G
298
+ </Text>
299
+ <Muted>
300
+ {"░"} Free {memory.free.toFixed(1)}G
301
+ </Muted>
302
+ </Box>
303
+ </Box>
304
+ <Box flexDirection="column">
305
+ <Muted>
306
+ Swap: {memory.swap.toFixed(1)}G / {memory.swapTotal.toFixed(1)}G
307
+ </Muted>
308
+ <ProgressBar value={swapPct / 100} color={severityColor(swapPct)} showPercentage />
309
+ </Box>
310
+ <Box flexDirection="column">
311
+ <Muted>Top Consumers</Muted>
312
+ <Box gap={2}>
313
+ <Text>
314
+ chrome <Strong color="$warning">12.1G</Strong>
315
+ </Text>
316
+ <Text>
317
+ vscode <Strong color="$primary">8.4G</Strong>
318
+ </Text>
319
+ <Text>
320
+ docker <Strong color="$primary">5.1G</Strong>
321
+ </Text>
322
+ </Box>
323
+ </Box>
324
+ <Box flexDirection="column">
325
+ <Muted>History</Muted>
326
+ <Text color="$primary">{sparkline(memory.history, 100)}</Text>
327
+ </Box>
328
+ </Box>
329
+ )
330
+ }
331
+
332
+ // --- Network Tab ---
333
+
334
+ function NetworkPane({ network }: { network: NetworkMetrics }): JSX.Element {
335
+ return (
336
+ <Box flexDirection="column" gap={1} flexGrow={1}>
337
+ <SectionHeader>Network</SectionHeader>
338
+ <Box flexDirection="column">
339
+ <Box justifyContent="space-between">
340
+ <Text color="$success">
341
+ {"↓"} Download: <Strong>{network.downloadRate.toFixed(1)} MB/s</Strong>
342
+ </Text>
343
+ <Small>{sparkline(network.downloadHistory, 100)}</Small>
344
+ </Box>
345
+ <ProgressBar value={network.downloadRate / 100} color="$success" showPercentage={false} />
346
+ </Box>
347
+ <Box flexDirection="column">
348
+ <Box justifyContent="space-between">
349
+ <Text color="$info">
350
+ {"↑"} Upload: <Strong>{network.uploadRate.toFixed(1)} MB/s</Strong>
351
+ </Text>
352
+ <Small>{sparkline(network.uploadHistory, 40)}</Small>
353
+ </Box>
354
+ <ProgressBar value={network.uploadRate / 40} color="$info" showPercentage={false} />
355
+ </Box>
356
+ <Box borderStyle="round" borderColor="$border" paddingX={1} flexDirection="column">
357
+ <Muted>Connection Stats</Muted>
358
+ <Box gap={2}>
359
+ <LabelValue label="Connections:" value={String(network.connections)} />
360
+ <LabelValue label="Packets In:" value={String(network.packetsIn)} />
361
+ <LabelValue label="Packets Out:" value={String(network.packetsOut)} />
362
+ </Box>
363
+ <Box gap={2}>
364
+ <LabelValue label="Interface:" value="en0" />
365
+ <LabelValue label="MTU:" value="1500" />
366
+ <LabelValue label="Duplex:" value="full" />
367
+ </Box>
368
+ </Box>
369
+ </Box>
370
+ )
371
+ }
372
+
373
+ // --- Processes Tab ---
374
+
375
+ function ProcessRow({ proc, isTop }: { proc: ProcessInfo; isTop: boolean }): JSX.Element {
376
+ const cpuColor = severityColor(proc.cpu)
377
+ return (
378
+ <Box gap={1}>
379
+ <Text color="$muted">{String(proc.pid).padStart(5)}</Text>
380
+ <Text bold={isTop}>{proc.name.padEnd(12)}</Text>
381
+ <Text color={cpuColor}>{proc.cpu.toFixed(1).padStart(5)}%</Text>
382
+ <Text color="$primary">{proc.mem.toFixed(1).padStart(5)}%</Text>
383
+ <Text color={proc.status === "running" ? "$success" : "$muted"}>{proc.status}</Text>
384
+ </Box>
385
+ )
386
+ }
387
+
388
+ function ProcessPane({ processes }: { processes: ProcessInfo[] }): JSX.Element {
389
+ const sorted = [...processes].sort((a, b) => b.cpu - a.cpu)
390
+
391
+ return (
392
+ <Box flexDirection="column" gap={1} flexGrow={1}>
393
+ <SectionHeader>Processes</SectionHeader>
394
+ <Box gap={1}>
395
+ <Muted>{" PID".padStart(5)}</Muted>
396
+ <Muted>{"Name".padEnd(12)}</Muted>
397
+ <Muted>{" CPU".padStart(5)}</Muted>
398
+ <Muted>{" MEM".padStart(5)}</Muted>
399
+ <Muted>Status</Muted>
400
+ </Box>
401
+ {sorted.map((proc, i) => (
402
+ <ProcessRow key={proc.pid} proc={proc} isTop={i === 0} />
403
+ ))}
404
+ <Box gap={2} paddingTop={1}>
405
+ <LabelValue label="Total:" value={`${processes.length} processes`} />
406
+ <LabelValue
407
+ label="Running:"
408
+ value={String(processes.filter((p) => p.status === "running").length)}
409
+ color="$success"
410
+ />
411
+ <LabelValue
412
+ label="Sleeping:"
413
+ value={String(processes.filter((p) => p.status === "sleeping").length)}
414
+ color="$muted"
415
+ />
416
+ </Box>
417
+ </Box>
418
+ )
419
+ }
420
+
421
+ // --- Responsive Layout ---
422
+
423
+ function WideLayout({
424
+ cores,
425
+ memory,
426
+ network,
427
+ processes,
428
+ }: {
429
+ cores: CoreMetrics[]
430
+ memory: MemoryMetrics
431
+ network: NetworkMetrics
432
+ processes: ProcessInfo[]
433
+ }): JSX.Element {
434
+ return (
435
+ <Box flexDirection="column" flexGrow={1} gap={1}>
436
+ <Box flexDirection="row" gap={1} flexGrow={1}>
437
+ <Box flexGrow={1} borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexDirection="column">
438
+ <CpuPane cores={cores} />
439
+ </Box>
440
+ <Box flexGrow={1} borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexDirection="column">
441
+ <MemoryPane memory={memory} />
442
+ </Box>
443
+ </Box>
444
+ <Box flexDirection="row" gap={1} flexGrow={1}>
445
+ <Box flexGrow={1} borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexDirection="column">
446
+ <NetworkPane network={network} />
447
+ </Box>
448
+ <Box flexGrow={1} borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexDirection="column">
449
+ <ProcessPane processes={processes} />
450
+ </Box>
451
+ </Box>
452
+ </Box>
453
+ )
454
+ }
455
+
456
+ // ============================================================================
457
+ // Dashboard
458
+ // ============================================================================
459
+
460
+ export function Dashboard(): JSX.Element {
461
+ const { exit } = useApp()
462
+ const { width } = useContentRect()
463
+ const [state, setState] = useState(createInitialState)
464
+ const [tick, setTick] = useState(0)
465
+ const isWide = width > 100
466
+
467
+ useInterval(() => {
468
+ setState((prev) => tickState(prev))
469
+ setTick((t) => t + 1)
470
+ }, 500)
471
+
472
+ useInput((input: string, key: Key) => {
473
+ if (input === "q" || key.escape) exit()
474
+ })
475
+
476
+ if (isWide) {
477
+ return (
478
+ <Box flexDirection="column" flexGrow={1}>
479
+ <Box justifyContent="space-between" paddingX={1}>
480
+ <Text>
481
+ <Strong>System Monitor</Strong> <Small>Tick #{tick}</Small>
482
+ </Text>
483
+ <Muted>
484
+ <Kbd>q</Kbd> quit
485
+ </Muted>
486
+ </Box>
487
+ <WideLayout cores={state.cores} memory={state.memory} network={state.network} processes={state.processes} />
488
+ </Box>
489
+ )
490
+ }
491
+
492
+ return (
493
+ <Box flexDirection="column" flexGrow={1}>
494
+ <Tabs defaultValue="cpu">
495
+ <Box justifyContent="space-between" paddingX={1}>
496
+ <TabList>
497
+ <Tab value="cpu">CPU</Tab>
498
+ <Tab value="memory">Memory</Tab>
499
+ <Tab value="network">Network</Tab>
500
+ <Tab value="processes">Processes</Tab>
501
+ </TabList>
502
+ <Small>Tick #{tick}</Small>
503
+ </Box>
504
+
505
+ <TabPanel value="cpu">
506
+ <Box borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexGrow={1}>
507
+ <CpuPane cores={state.cores} />
508
+ </Box>
509
+ </TabPanel>
510
+ <TabPanel value="memory">
511
+ <Box borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexGrow={1}>
512
+ <MemoryPane memory={state.memory} />
513
+ </Box>
514
+ </TabPanel>
515
+ <TabPanel value="network">
516
+ <Box borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexGrow={1}>
517
+ <NetworkPane network={state.network} />
518
+ </Box>
519
+ </TabPanel>
520
+ <TabPanel value="processes">
521
+ <Box borderStyle="round" borderColor="$border" paddingX={1} paddingY={1} flexGrow={1}>
522
+ <ProcessPane processes={state.processes} />
523
+ </Box>
524
+ </TabPanel>
525
+ </Tabs>
526
+ <Muted>
527
+ {" "}
528
+ <Kbd>h/l</Kbd> tabs <Kbd>Esc/q</Kbd> quit
529
+ </Muted>
530
+ </Box>
531
+ )
532
+ }
533
+
534
+ // ============================================================================
535
+ // Main
536
+ // ============================================================================
537
+
538
+ async function main() {
539
+ using term = createTerm()
540
+ const { waitUntilExit } = await render(
541
+ <ExampleBanner meta={meta} controls="h/l tabs Esc/q quit">
542
+ <Dashboard />
543
+ </ExampleBanner>,
544
+ term,
545
+ )
546
+ await waitUntilExit()
547
+ }
548
+
549
+ if (import.meta.main) {
550
+ main().catch(console.error)
551
+ }