@silvery/examples 0.17.3 → 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.
- package/dist/UPNG-Cy7ViL8f.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-BML7CYau.mjs +4 -0
- package/dist/_banner-DLPxCqVy.mjs +44 -0
- package/dist/ansi-CCE2pVS0.mjs +16397 -0
- package/dist/apng-HhhBjRGt.mjs +68 -0
- package/dist/apng-mwUQbTTF.mjs +3 -0
- package/dist/apps/aichat/index.mjs +1299 -0
- package/dist/apps/app-todo.mjs +139 -0
- package/dist/apps/async-data.mjs +204 -0
- package/dist/apps/cli-wizard.mjs +339 -0
- package/dist/apps/clipboard.mjs +198 -0
- package/dist/apps/components.mjs +864 -0
- package/dist/apps/data-explorer.mjs +483 -0
- package/dist/apps/dev-tools.mjs +397 -0
- package/dist/apps/explorer.mjs +698 -0
- package/dist/apps/gallery.mjs +766 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +280 -0
- package/dist/apps/layout-ref.mjs +187 -0
- package/dist/apps/outline.mjs +203 -0
- package/dist/apps/paste-demo.mjs +189 -0
- package/dist/apps/scroll.mjs +86 -0
- package/dist/apps/search-filter.mjs +287 -0
- package/dist/apps/selection.mjs +355 -0
- package/dist/apps/spatial-focus-demo.mjs +388 -0
- package/dist/apps/task-list.mjs +258 -0
- package/dist/apps/terminal-caps-demo.mjs +315 -0
- package/dist/apps/terminal.mjs +872 -0
- package/dist/apps/text-selection-demo.mjs +254 -0
- package/dist/apps/textarea.mjs +178 -0
- package/dist/apps/theme.mjs +661 -0
- package/dist/apps/transform.mjs +215 -0
- package/dist/apps/virtual-10k.mjs +422 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Bahh9mKN.mjs +1179 -0
- package/dist/backends-CCtCDQ94.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +48 -0
- package/dist/components/hello.mjs +31 -0
- package/dist/components/progress-bar.mjs +59 -0
- package/dist/components/select-list.mjs +85 -0
- package/dist/components/spinner.mjs +57 -0
- package/dist/components/text-input.mjs +62 -0
- package/dist/components/virtual-list.mjs +51 -0
- package/dist/flexily-zero-adapter-UB-ra8fR.mjs +3374 -0
- package/dist/gif-BZaqPPVX.mjs +3 -0
- package/dist/gif-BtnXuxLF.mjs +71 -0
- package/dist/gifenc-CLRW41dk.mjs +728 -0
- package/dist/jsx-runtime-dMs_8fNu.mjs +241 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1204 -0
- package/dist/layout/live-resize.mjs +303 -0
- package/dist/layout/overflow.mjs +70 -0
- package/dist/layout/text-layout.mjs +335 -0
- package/dist/node-NuJ94BWl.mjs +1083 -0
- package/dist/plugins-D1KtkT4a.mjs +3057 -0
- package/dist/resvg-js-C_8Wps1F.mjs +201 -0
- package/dist/src-BTEVGpd9.mjs +23538 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-CzfRafCQ.mjs +814 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BVtQ5OJR.mjs +237 -0
- package/package.json +18 -13
- package/_banner.tsx +0 -60
- package/apps/aichat/components.tsx +0 -469
- package/apps/aichat/index.tsx +0 -220
- package/apps/aichat/script.ts +0 -460
- package/apps/aichat/state.ts +0 -325
- package/apps/aichat/types.ts +0 -19
- package/apps/app-todo.tsx +0 -201
- package/apps/async-data.tsx +0 -196
- package/apps/cli-wizard.tsx +0 -332
- package/apps/clipboard.tsx +0 -183
- package/apps/components.tsx +0 -658
- package/apps/data-explorer.tsx +0 -490
- package/apps/dev-tools.tsx +0 -395
- package/apps/explorer.tsx +0 -731
- package/apps/gallery.tsx +0 -653
- package/apps/inline-bench.tsx +0 -138
- package/apps/kanban.tsx +0 -265
- package/apps/layout-ref.tsx +0 -173
- package/apps/outline.tsx +0 -160
- package/apps/panes/index.tsx +0 -203
- package/apps/paste-demo.tsx +0 -185
- package/apps/scroll.tsx +0 -80
- package/apps/search-filter.tsx +0 -240
- package/apps/selection.tsx +0 -346
- package/apps/spatial-focus-demo.tsx +0 -372
- package/apps/task-list.tsx +0 -271
- package/apps/terminal-caps-demo.tsx +0 -317
- package/apps/terminal.tsx +0 -784
- package/apps/text-selection-demo.tsx +0 -193
- package/apps/textarea.tsx +0 -155
- package/apps/theme.tsx +0 -515
- package/apps/transform.tsx +0 -229
- package/apps/virtual-10k.tsx +0 -405
- package/apps/vterm-demo/index.tsx +0 -216
- package/components/counter.tsx +0 -49
- package/components/hello.tsx +0 -38
- package/components/progress-bar.tsx +0 -52
- package/components/select-list.tsx +0 -54
- package/components/spinner.tsx +0 -44
- package/components/text-input.tsx +0 -61
- package/components/virtual-list.tsx +0 -56
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs.map +0 -1
- package/layout/dashboard.tsx +0 -953
- package/layout/live-resize.tsx +0 -282
- package/layout/overflow.tsx +0 -51
- package/layout/text-layout.tsx +0 -283
package/apps/transform.tsx
DELETED
|
@@ -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
|
-
export 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
|
-
await main()
|
|
229
|
-
}
|
package/apps/virtual-10k.tsx
DELETED
|
@@ -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
|
-
export 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
|
-
await main()
|
|
405
|
-
}
|