@silvery/examples 0.5.6 → 0.17.4

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 (112) hide show
  1. package/dist/UPNG-Cy7ViL8f.mjs +5074 -0
  2. package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
  3. package/dist/_banner-DLPxCqVy.mjs +44 -0
  4. package/dist/ansi-CCE2pVS0.mjs +16397 -0
  5. package/dist/apng-HhhBjRGt.mjs +68 -0
  6. package/dist/apng-mwUQbTTF.mjs +3 -0
  7. package/dist/apps/aichat/index.mjs +1299 -0
  8. package/dist/apps/app-todo.mjs +139 -0
  9. package/dist/apps/async-data.mjs +204 -0
  10. package/dist/apps/cli-wizard.mjs +339 -0
  11. package/dist/apps/clipboard.mjs +198 -0
  12. package/dist/apps/components.mjs +864 -0
  13. package/dist/apps/data-explorer.mjs +483 -0
  14. package/dist/apps/dev-tools.mjs +397 -0
  15. package/dist/apps/explorer.mjs +698 -0
  16. package/dist/apps/gallery.mjs +766 -0
  17. package/dist/apps/inline-bench.mjs +115 -0
  18. package/dist/apps/kanban.mjs +280 -0
  19. package/dist/apps/layout-ref.mjs +187 -0
  20. package/dist/apps/outline.mjs +203 -0
  21. package/dist/apps/paste-demo.mjs +189 -0
  22. package/dist/apps/scroll.mjs +86 -0
  23. package/dist/apps/search-filter.mjs +287 -0
  24. package/dist/apps/selection.mjs +355 -0
  25. package/dist/apps/spatial-focus-demo.mjs +388 -0
  26. package/dist/apps/task-list.mjs +258 -0
  27. package/dist/apps/terminal-caps-demo.mjs +315 -0
  28. package/dist/apps/terminal.mjs +872 -0
  29. package/dist/apps/text-selection-demo.mjs +254 -0
  30. package/dist/apps/textarea.mjs +178 -0
  31. package/dist/apps/theme.mjs +661 -0
  32. package/dist/apps/transform.mjs +215 -0
  33. package/dist/apps/virtual-10k.mjs +422 -0
  34. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  35. package/dist/backends-Bahh9mKN.mjs +1179 -0
  36. package/dist/backends-CCtCDQ94.mjs +3 -0
  37. package/dist/{cli.mjs → bin/cli.mjs} +21 -25
  38. package/dist/chunk-BSw8zbkd.mjs +37 -0
  39. package/dist/components/counter.mjs +48 -0
  40. package/dist/components/hello.mjs +31 -0
  41. package/dist/components/progress-bar.mjs +59 -0
  42. package/dist/components/select-list.mjs +85 -0
  43. package/dist/components/spinner.mjs +57 -0
  44. package/dist/components/text-input.mjs +62 -0
  45. package/dist/components/virtual-list.mjs +51 -0
  46. package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
  47. package/dist/gif-BZaqPPVX.mjs +3 -0
  48. package/dist/gif-BtnXuxLF.mjs +71 -0
  49. package/dist/gifenc-CLRW41dk.mjs +728 -0
  50. package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
  51. package/dist/key-mapping-5oYQdAQE.mjs +3 -0
  52. package/dist/key-mapping-D4LR1go6.mjs +130 -0
  53. package/dist/layout/dashboard.mjs +1204 -0
  54. package/dist/layout/live-resize.mjs +303 -0
  55. package/dist/layout/overflow.mjs +70 -0
  56. package/dist/layout/text-layout.mjs +335 -0
  57. package/dist/node-NuJ94BWl.mjs +1083 -0
  58. package/dist/plugins-D1KtkT4a.mjs +3057 -0
  59. package/dist/resvg-js-C_8Wps1F.mjs +201 -0
  60. package/dist/src-BTEVGpd9.mjs +23538 -0
  61. package/dist/src-CUUOuRH6.mjs +5322 -0
  62. package/dist/src-CzfRafCQ.mjs +814 -0
  63. package/dist/usingCtx-CsEf0xO3.mjs +57 -0
  64. package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
  65. package/package.json +19 -14
  66. package/_banner.tsx +0 -60
  67. package/apps/aichat/components.tsx +0 -469
  68. package/apps/aichat/index.tsx +0 -220
  69. package/apps/aichat/script.ts +0 -460
  70. package/apps/aichat/state.ts +0 -325
  71. package/apps/aichat/types.ts +0 -19
  72. package/apps/app-todo.tsx +0 -201
  73. package/apps/async-data.tsx +0 -196
  74. package/apps/cli-wizard.tsx +0 -332
  75. package/apps/clipboard.tsx +0 -183
  76. package/apps/components.tsx +0 -658
  77. package/apps/data-explorer.tsx +0 -490
  78. package/apps/dev-tools.tsx +0 -395
  79. package/apps/explorer.tsx +0 -731
  80. package/apps/gallery.tsx +0 -653
  81. package/apps/inline-bench.tsx +0 -138
  82. package/apps/kanban.tsx +0 -265
  83. package/apps/layout-ref.tsx +0 -173
  84. package/apps/outline.tsx +0 -160
  85. package/apps/panes/index.tsx +0 -203
  86. package/apps/paste-demo.tsx +0 -185
  87. package/apps/scroll.tsx +0 -77
  88. package/apps/search-filter.tsx +0 -240
  89. package/apps/selection.tsx +0 -342
  90. package/apps/spatial-focus-demo.tsx +0 -368
  91. package/apps/task-list.tsx +0 -271
  92. package/apps/terminal-caps-demo.tsx +0 -334
  93. package/apps/terminal.tsx +0 -800
  94. package/apps/text-selection-demo.tsx +0 -189
  95. package/apps/textarea.tsx +0 -155
  96. package/apps/theme.tsx +0 -515
  97. package/apps/transform.tsx +0 -229
  98. package/apps/virtual-10k.tsx +0 -405
  99. package/apps/vterm-demo/index.tsx +0 -216
  100. package/components/counter.tsx +0 -45
  101. package/components/hello.tsx +0 -34
  102. package/components/progress-bar.tsx +0 -48
  103. package/components/select-list.tsx +0 -50
  104. package/components/spinner.tsx +0 -40
  105. package/components/text-input.tsx +0 -57
  106. package/components/virtual-list.tsx +0 -52
  107. package/dist/cli.d.mts +0 -1
  108. package/dist/cli.mjs.map +0 -1
  109. package/layout/dashboard.tsx +0 -953
  110. package/layout/live-resize.tsx +0 -282
  111. package/layout/overflow.tsx +0 -51
  112. package/layout/text-layout.tsx +0 -283
@@ -1,229 +0,0 @@
1
- /**
2
- * Transform Component Demo
3
- *
4
- * Shows the Transform component for text post-processing. Each transform
5
- * applies a string transformation to every line of rendered text output.
6
- *
7
- * Features:
8
- * - Multiple transforms: uppercase, leetspeak, reverse, ROT13, etc.
9
- * - Cycle through transforms with j/k
10
- * - Shows original and transformed text side by side
11
- * - Uses Transform from silvery components
12
- *
13
- * Run: bun vendor/silvery/examples/apps/transform.tsx
14
- */
15
-
16
- import React, { useState } from "react"
17
- import { render, Box, Text, H1, Small, Kbd, Muted, Transform, useInput, useApp, createTerm, type Key } from "silvery"
18
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
19
-
20
- export const meta: ExampleMeta = {
21
- name: "Transform",
22
- description: "Text post-processing with the Transform component",
23
- features: ["Transform", "transform function", "side-by-side comparison"],
24
- }
25
-
26
- // ============================================================================
27
- // Transforms
28
- // ============================================================================
29
-
30
- const leetMap: Record<string, string> = {
31
- a: "4",
32
- e: "3",
33
- i: "1",
34
- o: "0",
35
- s: "5",
36
- t: "7",
37
- A: "4",
38
- E: "3",
39
- I: "1",
40
- O: "0",
41
- S: "5",
42
- T: "7",
43
- }
44
-
45
- const rot13Char = (c: string): string => {
46
- const code = c.charCodeAt(0)
47
- if (code >= 65 && code <= 90) return String.fromCharCode(((code - 65 + 13) % 26) + 65)
48
- if (code >= 97 && code <= 122) return String.fromCharCode(((code - 97 + 13) % 26) + 97)
49
- return c
50
- }
51
-
52
- interface TransformDef {
53
- name: string
54
- description: string
55
- fn: (line: string) => string
56
- }
57
-
58
- const transforms: TransformDef[] = [
59
- {
60
- name: "Uppercase",
61
- description: "Convert all characters to upper case",
62
- fn: (s: string) => s.toUpperCase(),
63
- },
64
- {
65
- name: "Lowercase",
66
- description: "Convert all characters to lower case",
67
- fn: (s: string) => s.toLowerCase(),
68
- },
69
- {
70
- name: "Leetspeak",
71
- description: "Replace letters with numbers (a=4, e=3, i=1, ...)",
72
- fn: (s: string) =>
73
- s
74
- .split("")
75
- .map((c) => leetMap[c] ?? c)
76
- .join(""),
77
- },
78
- {
79
- name: "Reverse",
80
- description: "Reverse each line of text",
81
- fn: (s: string) => s.split("").reverse().join(""),
82
- },
83
- {
84
- name: "ROT13",
85
- description: "Caesar cipher — shift each letter by 13 positions",
86
- fn: (s: string) => s.split("").map(rot13Char).join(""),
87
- },
88
- {
89
- name: "Alternating Case",
90
- description: "Alternate between upper and lower case characters",
91
- fn: (s: string) =>
92
- s
93
- .split("")
94
- .map((c, i) => (i % 2 === 0 ? c.toUpperCase() : c.toLowerCase()))
95
- .join(""),
96
- },
97
- {
98
- name: "Spaces to Dots",
99
- description: "Replace spaces with middle dots for visibility",
100
- fn: (s: string) => s.replace(/ /g, "·"),
101
- },
102
- ]
103
-
104
- // ============================================================================
105
- // Sample Text
106
- // ============================================================================
107
-
108
- const sampleLines = [
109
- "The quick brown fox jumps",
110
- "over the lazy dog on a",
111
- "beautiful sunny afternoon.",
112
- "",
113
- "Pack my box with five dozen",
114
- "liquor jugs and enjoy them.",
115
- ]
116
-
117
- // ============================================================================
118
- // Components
119
- // ============================================================================
120
-
121
- function TransformSelector({ current, transforms: items }: { current: number; transforms: TransformDef[] }) {
122
- return (
123
- <Box flexDirection="column" overflow="scroll" scrollTo={current} height={7}>
124
- {items.map((t, index) => {
125
- const isSelected = index === current
126
- return (
127
- <Box key={t.name} paddingX={1}>
128
- <Text
129
- color={isSelected ? "$bg" : undefined}
130
- backgroundColor={isSelected ? "$primary" : undefined}
131
- bold={isSelected}
132
- >
133
- {isSelected ? " > " : " "}
134
- {t.name}
135
- </Text>
136
- </Box>
137
- )
138
- })}
139
- </Box>
140
- )
141
- }
142
-
143
- function TextPanel({ title, titleColor, children }: { title: string; titleColor: string; children: React.ReactNode }) {
144
- return (
145
- <Box flexDirection="column" flexGrow={1} borderStyle="round" borderColor="$border" paddingX={1}>
146
- <Box marginBottom={1}>
147
- <H1 color={titleColor}>{title}</H1>
148
- </Box>
149
- {children}
150
- </Box>
151
- )
152
- }
153
-
154
- export function TransformDemo() {
155
- const { exit } = useApp()
156
- const [currentIndex, setCurrentIndex] = useState(0)
157
-
158
- const current = transforms[currentIndex]!
159
-
160
- useInput((input: string, key: Key) => {
161
- if (input === "q" || key.escape) {
162
- exit()
163
- return
164
- }
165
-
166
- if (key.upArrow || input === "k") {
167
- setCurrentIndex((prev) => Math.max(0, prev - 1))
168
- }
169
- if (key.downArrow || input === "j") {
170
- setCurrentIndex((prev) => Math.min(transforms.length - 1, prev + 1))
171
- }
172
- })
173
-
174
- return (
175
- <Box flexDirection="column" padding={1} gap={1}>
176
- {/* Transform selector */}
177
- <Box flexDirection="column" borderStyle="round" borderColor="$primary" paddingX={1}>
178
- <Box marginBottom={1} gap={1}>
179
- <H1>Transform</H1>
180
- <Small>
181
- — {current.name}: {current.description}
182
- </Small>
183
- </Box>
184
- <TransformSelector current={currentIndex} transforms={transforms} />
185
- </Box>
186
-
187
- {/* Side-by-side comparison */}
188
- <Box flexDirection="row" gap={1}>
189
- <TextPanel title="Original" titleColor="$muted">
190
- <Box flexDirection="column">
191
- {sampleLines.map((line, i) => (
192
- <Text key={i}>{line || " "}</Text>
193
- ))}
194
- </Box>
195
- </TextPanel>
196
-
197
- <TextPanel title={`${current.name}`} titleColor="$warning">
198
- <Transform transform={current.fn}>
199
- <Text>{sampleLines.join("\n")}</Text>
200
- </Transform>
201
- </TextPanel>
202
- </Box>
203
-
204
- <Muted>
205
- {" "}
206
- <Kbd>j/k</Kbd> select transform <Kbd>Esc/q</Kbd> quit
207
- </Muted>
208
- </Box>
209
- )
210
- }
211
-
212
- // ============================================================================
213
- // Main
214
- // ============================================================================
215
-
216
- async function main() {
217
- using term = createTerm()
218
- const { waitUntilExit } = await render(
219
- <ExampleBanner meta={meta} controls="j/k select transform Esc/q quit">
220
- <TransformDemo />
221
- </ExampleBanner>,
222
- term,
223
- )
224
- await waitUntilExit()
225
- }
226
-
227
- if (import.meta.main) {
228
- main().catch(console.error)
229
- }
@@ -1,405 +0,0 @@
1
- /**
2
- * Virtual Scroll Benchmark — 10,000 Items
3
- *
4
- * Demonstrates that ListView handles massive datasets with instant scrolling.
5
- * Only visible items + overscan are rendered, regardless of total count.
6
- *
7
- * Demonstrates:
8
- * - ListView with 10,000 items and variable heights
9
- * - Smooth j/k navigation with position indicator
10
- * - useBoxRect() 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/apps/virtual-10k.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, ListView, useBoxRect } from "silvery"
26
- import { run, useInput, type Key } from "silvery/runtime"
27
- import { ExampleBanner, type ExampleMeta } from "../_banner.js"
28
-
29
- export const meta: ExampleMeta = {
30
- name: "Virtual 10K",
31
- description: "ListView scrolling through 10,000 items with instant navigation",
32
- features: ["ListView", "10K items", "useBoxRect()", "variable estimateHeight"],
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 }) {
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({ item, isSelected, showDetail }: { item: Item; isSelected: boolean; showDetail: boolean }) {
185
- const idStr = String(item.id).padStart(5, " ")
186
-
187
- return (
188
- <Box flexDirection="column" paddingX={1} backgroundColor={isSelected ? "$primary" : undefined}>
189
- <Box>
190
- <Text color={STATUS_COLORS[item.status]}>{STATUS_ICONS[item.status]}</Text>
191
- <Text dim> {idStr} </Text>
192
- <Text bold color={PRIORITY_COLORS[item.priority]}>
193
- {item.priority}
194
- </Text>
195
- <Text> </Text>
196
- <Text bold={isSelected}>{item.title}</Text>
197
- <Text> </Text>
198
- {item.tags.map((tag) => (
199
- <Text key={tag} dim color="$info">
200
- {" "}
201
- #{tag}
202
- </Text>
203
- ))}
204
- </Box>
205
- {showDetail && (
206
- <Box paddingLeft={8}>
207
- <Text dim>{item.description}</Text>
208
- <Text> </Text>
209
- <ProgressBar percent={item.progress} width={10} />
210
- <Text dim> {item.progress}%</Text>
211
- </Box>
212
- )}
213
- </Box>
214
- )
215
- }
216
-
217
- function ScrollIndicator({ current, total, width }: { current: number; total: number; width: number }) {
218
- const percent = total > 0 ? Math.round(((current + 1) / total) * 100) : 0
219
-
220
- // Progress bar
221
- const barWidth = Math.max(10, Math.min(30, width - 40))
222
- const filled = Math.round((percent / 100) * barWidth)
223
- const empty = barWidth - filled
224
-
225
- return (
226
- <Box gap={2} paddingX={1}>
227
- <Strong color="$primary">{(current + 1).toLocaleString()}</Strong>
228
- <Text dim>of</Text>
229
- <Strong>{total.toLocaleString()}</Strong>
230
- <Text>
231
- <Text color="$primary">{"█".repeat(filled)}</Text>
232
- <Text dim>{"░".repeat(empty)}</Text>
233
- </Text>
234
- <Strong color="$primary">{percent}%</Strong>
235
- </Box>
236
- )
237
- }
238
-
239
- function StatsBar({ items }: { items: Item[] }) {
240
- const stats = useMemo(() => {
241
- let p0 = 0,
242
- p1 = 0,
243
- p2 = 0,
244
- p3 = 0
245
- let todo = 0,
246
- inProg = 0,
247
- done = 0,
248
- blocked = 0
249
- for (const item of items) {
250
- if (item.priority === "P0") p0++
251
- else if (item.priority === "P1") p1++
252
- else if (item.priority === "P2") p2++
253
- else p3++
254
- if (item.status === "todo") todo++
255
- else if (item.status === "in-progress") inProg++
256
- else if (item.status === "done") done++
257
- else blocked++
258
- }
259
- return { p0, p1, p2, p3, todo, inProg, done, blocked }
260
- }, [items])
261
-
262
- return (
263
- <Box gap={2} paddingX={1}>
264
- <Strong color="$error">P0:{stats.p0}</Strong>
265
- <Strong color="$warning">P1:{stats.p1}</Strong>
266
- <Text color="$info">P2:{stats.p2}</Text>
267
- <Text dim>P3:{stats.p3}</Text>
268
- <Text dim>|</Text>
269
- <Text color="$muted">
270
- {STATUS_ICONS.todo} {stats.todo}
271
- </Text>
272
- <Text color="$warning">
273
- {STATUS_ICONS["in-progress"]} {stats.inProg}
274
- </Text>
275
- <Text color="$success">
276
- {STATUS_ICONS.done} {stats.done}
277
- </Text>
278
- <Text color="$error">
279
- {STATUS_ICONS.blocked} {stats.blocked}
280
- </Text>
281
- </Box>
282
- )
283
- }
284
-
285
- // ============================================================================
286
- // Main App
287
- // ============================================================================
288
-
289
- function VirtualBenchmark() {
290
- const { width, height } = useBoxRect()
291
- const [cursor, setCursor] = useState(0)
292
- const [showDetail, setShowDetail] = useState(false)
293
-
294
- // Calculate available list height
295
- // stats (1) + separator (1) + scroll indicator (1) + help (1) + borders
296
- const listHeight = Math.max(5, height - 5)
297
- const halfPage = Math.max(1, Math.floor(listHeight / 2))
298
-
299
- const estimateHeight = useCallback(
300
- (index: number) => {
301
- if (showDetail && index === cursor) return 2
302
- return 1
303
- },
304
- [showDetail, cursor],
305
- )
306
-
307
- useInput(
308
- useCallback(
309
- (input: string, key: Key) => {
310
- if (input === "q" || key.escape || (key.ctrl && input === "c")) {
311
- return "exit"
312
- }
313
-
314
- // Navigation
315
- if (input === "j" || key.downArrow) {
316
- setCursor((c) => Math.min(TOTAL_ITEMS - 1, c + 1))
317
- }
318
- if (input === "k" || key.upArrow) {
319
- setCursor((c) => Math.max(0, c - 1))
320
- }
321
-
322
- // Half-page
323
- if (input === "d" || key.pageDown) {
324
- setCursor((c) => Math.min(TOTAL_ITEMS - 1, c + halfPage))
325
- }
326
- if (input === "u" || key.pageUp) {
327
- setCursor((c) => Math.max(0, c - halfPage))
328
- }
329
-
330
- // Jump to start/end
331
- if (input === "g" || key.home) {
332
- setCursor(0)
333
- }
334
- if (input === "G" || key.end) {
335
- setCursor(TOTAL_ITEMS - 1)
336
- }
337
-
338
- // Toggle detail view
339
- if (key.return || input === " ") {
340
- setShowDetail((d) => !d)
341
- }
342
- },
343
- [halfPage],
344
- ),
345
- )
346
-
347
- return (
348
- <Box flexDirection="column" width="100%" height="100%">
349
- {/* Stats */}
350
- <StatsBar items={ALL_ITEMS} />
351
-
352
- {/* Separator */}
353
- <Box paddingX={1}>
354
- <Divider />
355
- </Box>
356
-
357
- {/* Virtual list */}
358
- <Box flexGrow={1}>
359
- <ListView
360
- items={ALL_ITEMS}
361
- height={listHeight}
362
- estimateHeight={estimateHeight}
363
- scrollTo={cursor}
364
- overscan={5}
365
- renderItem={(item, index) => (
366
- <ItemRow
367
- key={item.id}
368
- item={item}
369
- isSelected={index === cursor}
370
- showDetail={showDetail && index === cursor}
371
- />
372
- )}
373
- />
374
- </Box>
375
-
376
- {/* Scroll position */}
377
- <ScrollIndicator current={cursor} total={TOTAL_ITEMS} width={width} />
378
-
379
- {/* Help */}
380
- <Box paddingX={1} justifyContent="center">
381
- <Muted>
382
- <Kbd>j/k</Kbd> navigate <Kbd>d/u</Kbd> half-page <Kbd>g/G</Kbd> start/end <Kbd>Enter</Kbd> detail{" "}
383
- <Kbd>Esc/q</Kbd> quit
384
- </Muted>
385
- </Box>
386
- </Box>
387
- )
388
- }
389
-
390
- // ============================================================================
391
- // Main
392
- // ============================================================================
393
-
394
- async function main() {
395
- const handle = await run(
396
- <ExampleBanner meta={meta} controls="j/k navigate d/u half-page g/G start/end Enter detail Esc/q quit">
397
- <VirtualBenchmark />
398
- </ExampleBanner>,
399
- )
400
- await handle.waitUntilExit()
401
- }
402
-
403
- if (import.meta.main) {
404
- main().catch(console.error)
405
- }