@silvery/examples 0.4.2
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/bin/cli.ts +286 -0
- package/examples/apps/aichat/components.tsx +469 -0
- package/examples/apps/aichat/index.tsx +207 -0
- package/examples/apps/aichat/script.ts +460 -0
- package/examples/apps/aichat/state.ts +326 -0
- package/examples/apps/aichat/types.ts +19 -0
- package/examples/apps/app-todo.tsx +201 -0
- package/examples/apps/async-data.tsx +208 -0
- package/examples/apps/cli-wizard.tsx +332 -0
- package/examples/apps/clipboard.tsx +183 -0
- package/examples/apps/components.tsx +463 -0
- package/examples/apps/data-explorer.tsx +490 -0
- package/examples/apps/dev-tools.tsx +379 -0
- package/examples/apps/explorer.tsx +731 -0
- package/examples/apps/gallery.tsx +653 -0
- package/examples/apps/inline-bench.tsx +136 -0
- package/examples/apps/kanban.tsx +267 -0
- package/examples/apps/layout-ref.tsx +185 -0
- package/examples/apps/outline.tsx +171 -0
- package/examples/apps/panes/index.tsx +205 -0
- package/examples/apps/paste-demo.tsx +198 -0
- package/examples/apps/scroll.tsx +77 -0
- package/examples/apps/search-filter.tsx +240 -0
- package/examples/apps/task-list.tsx +271 -0
- package/examples/apps/terminal.tsx +800 -0
- package/examples/apps/textarea.tsx +103 -0
- package/examples/apps/theme.tsx +515 -0
- package/examples/apps/transform.tsx +242 -0
- package/examples/apps/virtual-10k.tsx +405 -0
- package/examples/components/counter.tsx +45 -0
- package/examples/components/hello.tsx +34 -0
- package/examples/components/progress-bar.tsx +48 -0
- package/examples/components/select-list.tsx +50 -0
- package/examples/components/spinner.tsx +40 -0
- package/examples/components/text-input.tsx +57 -0
- package/examples/components/virtual-list.tsx +52 -0
- package/package.json +27 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Filter Example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates React concurrent features for responsive typing:
|
|
5
|
+
* - useTransition for low-priority state updates
|
|
6
|
+
* - useDeferredValue for deferred filtering
|
|
7
|
+
* - Typing remains responsive even with heavy filtering
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useState, useDeferredValue, useTransition } from "react"
|
|
11
|
+
import { render, Box, Text, Kbd, Muted, Strong, Lead, useInput, useApp, createTerm, type Key } from "../../src/index.js"
|
|
12
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
13
|
+
|
|
14
|
+
export const meta: ExampleMeta = {
|
|
15
|
+
name: "Search Filter",
|
|
16
|
+
description: "useTransition + useDeferredValue for responsive concurrent search",
|
|
17
|
+
features: ["useDeferredValue", "useTransition", "useInput"],
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Types
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
interface Item {
|
|
25
|
+
id: number
|
|
26
|
+
name: string
|
|
27
|
+
category: string
|
|
28
|
+
tags: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Data
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const items: Item[] = [
|
|
36
|
+
{
|
|
37
|
+
id: 1,
|
|
38
|
+
name: "React Hooks Guide",
|
|
39
|
+
category: "docs",
|
|
40
|
+
tags: ["react", "hooks", "tutorial"],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 2,
|
|
44
|
+
name: "TypeScript Patterns",
|
|
45
|
+
category: "docs",
|
|
46
|
+
tags: ["typescript", "patterns"],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 3,
|
|
50
|
+
name: "Build Configuration",
|
|
51
|
+
category: "config",
|
|
52
|
+
tags: ["webpack", "vite", "build"],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 4,
|
|
56
|
+
name: "Testing Best Practices",
|
|
57
|
+
category: "docs",
|
|
58
|
+
tags: ["testing", "jest", "vitest"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 5,
|
|
62
|
+
name: "API Documentation",
|
|
63
|
+
category: "docs",
|
|
64
|
+
tags: ["api", "rest", "graphql"],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 6,
|
|
68
|
+
name: "Database Schema",
|
|
69
|
+
category: "config",
|
|
70
|
+
tags: ["database", "sql", "migration"],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 7,
|
|
74
|
+
name: "Authentication Flow",
|
|
75
|
+
category: "docs",
|
|
76
|
+
tags: ["auth", "security", "jwt"],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 8,
|
|
80
|
+
name: "CI/CD Pipeline",
|
|
81
|
+
category: "config",
|
|
82
|
+
tags: ["ci", "deployment", "github"],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 9,
|
|
86
|
+
name: "Performance Tuning",
|
|
87
|
+
category: "docs",
|
|
88
|
+
tags: ["performance", "optimization"],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 10,
|
|
92
|
+
name: "Error Handling",
|
|
93
|
+
category: "docs",
|
|
94
|
+
tags: ["errors", "debugging", "logging"],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: 11,
|
|
98
|
+
name: "State Management",
|
|
99
|
+
category: "docs",
|
|
100
|
+
tags: ["state", "redux", "zustand"],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 12,
|
|
104
|
+
name: "CSS Architecture",
|
|
105
|
+
category: "docs",
|
|
106
|
+
tags: ["css", "tailwind", "styled"],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 13,
|
|
110
|
+
name: "Security Guidelines",
|
|
111
|
+
category: "docs",
|
|
112
|
+
tags: ["security", "owasp", "audit"],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 14,
|
|
116
|
+
name: "Deployment Scripts",
|
|
117
|
+
category: "config",
|
|
118
|
+
tags: ["deploy", "docker", "k8s"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 15,
|
|
122
|
+
name: "Monitoring Setup",
|
|
123
|
+
category: "config",
|
|
124
|
+
tags: ["monitoring", "metrics", "logs"],
|
|
125
|
+
},
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Components
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
function SearchInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
|
|
133
|
+
return (
|
|
134
|
+
<Box>
|
|
135
|
+
<Strong color="$primary">Search: </Strong>
|
|
136
|
+
<Text>{value}</Text>
|
|
137
|
+
<Text dim>|</Text>
|
|
138
|
+
</Box>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function FilteredList({ query, isPending }: { query: string; isPending: boolean }) {
|
|
143
|
+
// Simulate expensive filtering (in real app this might be fuzzy search)
|
|
144
|
+
const filtered = items.filter((item) => {
|
|
145
|
+
const searchLower = query.toLowerCase()
|
|
146
|
+
return (
|
|
147
|
+
item.name.toLowerCase().includes(searchLower) ||
|
|
148
|
+
item.category.toLowerCase().includes(searchLower) ||
|
|
149
|
+
item.tags.some((tag) => tag.toLowerCase().includes(searchLower))
|
|
150
|
+
)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<Box flexDirection="column" marginTop={1}>
|
|
155
|
+
<Box marginBottom={1}>
|
|
156
|
+
<Muted>
|
|
157
|
+
{filtered.length} results
|
|
158
|
+
{isPending && " (filtering...)"}
|
|
159
|
+
</Muted>
|
|
160
|
+
</Box>
|
|
161
|
+
{filtered.map((item) => (
|
|
162
|
+
<Box key={item.id} marginBottom={1}>
|
|
163
|
+
<Text bold>{item.name}</Text>
|
|
164
|
+
<Text dim> [{item.category}]</Text>
|
|
165
|
+
<Text color="gray"> {item.tags.join(", ")}</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
))}
|
|
168
|
+
{filtered.length === 0 && <Lead>No matches found</Lead>}
|
|
169
|
+
</Box>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function SearchApp() {
|
|
174
|
+
const { exit } = useApp()
|
|
175
|
+
const [query, setQuery] = useState("")
|
|
176
|
+
|
|
177
|
+
// useDeferredValue: The filtered list uses a deferred version of the query
|
|
178
|
+
// This keeps typing responsive while the list catches up
|
|
179
|
+
const deferredQuery = useDeferredValue(query)
|
|
180
|
+
|
|
181
|
+
// useTransition: Mark filtering as low-priority (optional, shows pending state)
|
|
182
|
+
const [isPending, startTransition] = useTransition()
|
|
183
|
+
|
|
184
|
+
useInput((input: string, key: Key) => {
|
|
185
|
+
if (key.escape) {
|
|
186
|
+
exit()
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (key.backspace || key.delete) {
|
|
191
|
+
// Backspace: remove last character
|
|
192
|
+
startTransition(() => {
|
|
193
|
+
setQuery((prev) => prev.slice(0, -1))
|
|
194
|
+
})
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Add printable characters
|
|
199
|
+
if (input && !key.ctrl && !key.meta) {
|
|
200
|
+
startTransition(() => {
|
|
201
|
+
setQuery((prev) => prev + input)
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Box flexDirection="column" padding={1}>
|
|
208
|
+
<SearchInput value={query} onChange={setQuery} />
|
|
209
|
+
|
|
210
|
+
{/* List uses deferredQuery so typing stays responsive */}
|
|
211
|
+
<Box flexGrow={1}>
|
|
212
|
+
<FilteredList query={deferredQuery} isPending={isPending} />
|
|
213
|
+
</Box>
|
|
214
|
+
|
|
215
|
+
<Muted>
|
|
216
|
+
{" "}
|
|
217
|
+
<Kbd>type</Kbd> to search <Kbd>Esc/q</Kbd> quit
|
|
218
|
+
</Muted>
|
|
219
|
+
</Box>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ============================================================================
|
|
224
|
+
// Main
|
|
225
|
+
// ============================================================================
|
|
226
|
+
|
|
227
|
+
async function main() {
|
|
228
|
+
using term = createTerm()
|
|
229
|
+
const { waitUntilExit } = await render(
|
|
230
|
+
<ExampleBanner meta={meta} controls="type to search Esc quit">
|
|
231
|
+
<SearchApp />
|
|
232
|
+
</ExampleBanner>,
|
|
233
|
+
term,
|
|
234
|
+
)
|
|
235
|
+
await waitUntilExit()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (import.meta.main) {
|
|
239
|
+
main().catch(console.error)
|
|
240
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task List Example
|
|
3
|
+
*
|
|
4
|
+
* A scrollable task list demonstrating:
|
|
5
|
+
* - 50+ items for scrolling demonstration
|
|
6
|
+
* - overflow="hidden" with manual scroll state
|
|
7
|
+
* - Toggle task completion with space
|
|
8
|
+
* - Variable height items (some with subtasks)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useState, useMemo } from "react"
|
|
12
|
+
import { render, Box, Text, Kbd, Muted, useInput, useApp, createTerm, type Key } from "../../src/index.js"
|
|
13
|
+
import { ExampleBanner, type ExampleMeta } from "../_banner.js"
|
|
14
|
+
|
|
15
|
+
export const meta: ExampleMeta = {
|
|
16
|
+
name: "Task List",
|
|
17
|
+
description: "Scrollable list with priority badges, toggles, and expandable subtasks",
|
|
18
|
+
features: ["VirtualList", "variable itemHeight", "Box overflow"],
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
interface Task {
|
|
26
|
+
id: number
|
|
27
|
+
title: string
|
|
28
|
+
completed: boolean
|
|
29
|
+
priority: "high" | "medium" | "low"
|
|
30
|
+
subtasks?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Data Generation
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
function generateTasks(count: number): Task[] {
|
|
38
|
+
const priorities: Array<"high" | "medium" | "low"> = ["high", "medium", "low"]
|
|
39
|
+
const taskTemplates = [
|
|
40
|
+
"Review pull request",
|
|
41
|
+
"Update documentation",
|
|
42
|
+
"Fix bug in authentication",
|
|
43
|
+
"Implement new feature",
|
|
44
|
+
"Write unit tests",
|
|
45
|
+
"Refactor legacy code",
|
|
46
|
+
"Update dependencies",
|
|
47
|
+
"Create API endpoint",
|
|
48
|
+
"Design database schema",
|
|
49
|
+
"Optimize performance",
|
|
50
|
+
"Add error handling",
|
|
51
|
+
"Setup CI/CD pipeline",
|
|
52
|
+
"Write integration tests",
|
|
53
|
+
"Code review feedback",
|
|
54
|
+
"Deploy to staging",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
const subtaskTemplates = [
|
|
58
|
+
["Research solutions", "Implement changes", "Test thoroughly"],
|
|
59
|
+
["Check requirements", "Update code"],
|
|
60
|
+
["Review with team", "Make adjustments", "Get approval", "Merge"],
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
64
|
+
id: i + 1,
|
|
65
|
+
title: `${taskTemplates[i % taskTemplates.length]} #${Math.floor(i / taskTemplates.length) + 1}`,
|
|
66
|
+
completed: Math.random() > 0.7,
|
|
67
|
+
priority: priorities[i % 3] as "high" | "medium" | "low",
|
|
68
|
+
// Every 5th task has subtasks
|
|
69
|
+
subtasks: i % 5 === 0 ? subtaskTemplates[i % subtaskTemplates.length] : undefined,
|
|
70
|
+
}))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Components
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
function PriorityBadge({ priority }: { priority: "high" | "medium" | "low" }) {
|
|
78
|
+
const colors = {
|
|
79
|
+
high: "$error",
|
|
80
|
+
medium: "$warning",
|
|
81
|
+
low: "$success",
|
|
82
|
+
}
|
|
83
|
+
const symbols = {
|
|
84
|
+
high: "!!!",
|
|
85
|
+
medium: "!!",
|
|
86
|
+
low: "!",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Text color={colors[priority]} bold>
|
|
91
|
+
[{symbols[priority]}]
|
|
92
|
+
</Text>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function TaskItem({ task, isSelected, isExpanded }: { task: Task; isSelected: boolean; isExpanded: boolean }) {
|
|
97
|
+
const checkbox = task.completed ? "[x]" : "[ ]"
|
|
98
|
+
const hasSubtasks = task.subtasks && task.subtasks.length > 0
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<Box flexDirection="column">
|
|
102
|
+
<Box>
|
|
103
|
+
{isSelected ? (
|
|
104
|
+
<Text backgroundColor="$primary" color="black">
|
|
105
|
+
{" "}
|
|
106
|
+
{checkbox} {task.title}{" "}
|
|
107
|
+
</Text>
|
|
108
|
+
) : (
|
|
109
|
+
<Text strikethrough={task.completed} dim={task.completed}>
|
|
110
|
+
{checkbox} {task.title}
|
|
111
|
+
</Text>
|
|
112
|
+
)}{" "}
|
|
113
|
+
<PriorityBadge priority={task.priority} />
|
|
114
|
+
{hasSubtasks && <Text dim> ({task.subtasks!.length} subtasks)</Text>}
|
|
115
|
+
</Box>
|
|
116
|
+
{hasSubtasks && isExpanded && (
|
|
117
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
118
|
+
{task.subtasks!.map((subtask, idx) => (
|
|
119
|
+
<Text key={idx} dim>
|
|
120
|
+
- {subtask}
|
|
121
|
+
</Text>
|
|
122
|
+
))}
|
|
123
|
+
</Box>
|
|
124
|
+
)}
|
|
125
|
+
</Box>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function StatusBar({
|
|
130
|
+
tasks,
|
|
131
|
+
cursor,
|
|
132
|
+
scrollOffset,
|
|
133
|
+
visibleCount,
|
|
134
|
+
}: {
|
|
135
|
+
tasks: Task[]
|
|
136
|
+
cursor: number
|
|
137
|
+
scrollOffset: number
|
|
138
|
+
visibleCount: number
|
|
139
|
+
}) {
|
|
140
|
+
const completed = tasks.filter((t) => t.completed).length
|
|
141
|
+
const total = tasks.length
|
|
142
|
+
const percent = Math.round((completed / total) * 100)
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<Box justifyContent="space-between">
|
|
146
|
+
<Muted>
|
|
147
|
+
{" "}
|
|
148
|
+
<Kbd>j/k</Kbd> navigate <Kbd>space</Kbd> toggle <Kbd>enter</Kbd> expand <Kbd>Esc/q</Kbd> quit
|
|
149
|
+
</Muted>
|
|
150
|
+
<Muted>
|
|
151
|
+
{" "}
|
|
152
|
+
<Text bold>{completed}</Text>/{total} ({percent}%) | {cursor + 1}/{total}{" "}
|
|
153
|
+
</Muted>
|
|
154
|
+
</Box>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function TaskList() {
|
|
159
|
+
const { exit } = useApp()
|
|
160
|
+
const [tasks, setTasks] = useState(() => generateTasks(60))
|
|
161
|
+
const [cursor, setCursor] = useState(0)
|
|
162
|
+
const [expandedTasks, setExpandedTasks] = useState<Set<number>>(new Set())
|
|
163
|
+
|
|
164
|
+
// Fixed visible count (in a real app, this would use useContentRect)
|
|
165
|
+
const visibleCount = 15
|
|
166
|
+
|
|
167
|
+
// Calculate scroll offset to keep cursor visible
|
|
168
|
+
const scrollOffset = useMemo(() => {
|
|
169
|
+
const halfVisible = Math.floor(visibleCount / 2)
|
|
170
|
+
const maxOffset = Math.max(0, tasks.length - visibleCount)
|
|
171
|
+
|
|
172
|
+
// Keep cursor centered when possible
|
|
173
|
+
let offset = cursor - halfVisible
|
|
174
|
+
offset = Math.max(0, Math.min(offset, maxOffset))
|
|
175
|
+
return offset
|
|
176
|
+
}, [cursor, visibleCount, tasks.length])
|
|
177
|
+
|
|
178
|
+
// Get visible tasks
|
|
179
|
+
const visibleTasks = useMemo(() => {
|
|
180
|
+
return tasks.slice(scrollOffset, scrollOffset + visibleCount)
|
|
181
|
+
}, [tasks, scrollOffset, visibleCount])
|
|
182
|
+
|
|
183
|
+
useInput((input: string, key: Key) => {
|
|
184
|
+
if (input === "q" || key.escape) {
|
|
185
|
+
exit()
|
|
186
|
+
}
|
|
187
|
+
if (key.upArrow || input === "k") {
|
|
188
|
+
setCursor((prev) => Math.max(0, prev - 1))
|
|
189
|
+
}
|
|
190
|
+
if (key.downArrow || input === "j") {
|
|
191
|
+
setCursor((prev) => Math.min(tasks.length - 1, prev + 1))
|
|
192
|
+
}
|
|
193
|
+
if (key.pageUp) {
|
|
194
|
+
setCursor((prev) => Math.max(0, prev - visibleCount))
|
|
195
|
+
}
|
|
196
|
+
if (key.pageDown) {
|
|
197
|
+
setCursor((prev) => Math.min(tasks.length - 1, prev + visibleCount))
|
|
198
|
+
}
|
|
199
|
+
if (key.home) {
|
|
200
|
+
setCursor(0)
|
|
201
|
+
}
|
|
202
|
+
if (key.end) {
|
|
203
|
+
setCursor(tasks.length - 1)
|
|
204
|
+
}
|
|
205
|
+
if (input === " ") {
|
|
206
|
+
// Toggle completion
|
|
207
|
+
setTasks((prev) => prev.map((task, idx) => (idx === cursor ? { ...task, completed: !task.completed } : task)))
|
|
208
|
+
}
|
|
209
|
+
if (key.return || input === "e") {
|
|
210
|
+
// Toggle expand/collapse subtasks
|
|
211
|
+
const taskId = tasks[cursor]?.id
|
|
212
|
+
if (taskId !== undefined && tasks[cursor]?.subtasks) {
|
|
213
|
+
setExpandedTasks((prev) => {
|
|
214
|
+
const next = new Set(prev)
|
|
215
|
+
if (next.has(taskId)) {
|
|
216
|
+
next.delete(taskId)
|
|
217
|
+
} else {
|
|
218
|
+
next.add(taskId)
|
|
219
|
+
}
|
|
220
|
+
return next
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return (
|
|
227
|
+
<Box flexDirection="column" padding={1}>
|
|
228
|
+
<Box
|
|
229
|
+
flexGrow={1}
|
|
230
|
+
flexDirection="column"
|
|
231
|
+
borderStyle="round"
|
|
232
|
+
borderColor="$border"
|
|
233
|
+
overflow="hidden"
|
|
234
|
+
height={visibleCount + 2}
|
|
235
|
+
>
|
|
236
|
+
{visibleTasks.map((task, visibleIndex) => {
|
|
237
|
+
const actualIndex = scrollOffset + visibleIndex
|
|
238
|
+
return (
|
|
239
|
+
<TaskItem
|
|
240
|
+
key={task.id}
|
|
241
|
+
task={task}
|
|
242
|
+
isSelected={actualIndex === cursor}
|
|
243
|
+
isExpanded={expandedTasks.has(task.id)}
|
|
244
|
+
/>
|
|
245
|
+
)
|
|
246
|
+
})}
|
|
247
|
+
</Box>
|
|
248
|
+
|
|
249
|
+
<StatusBar tasks={tasks} cursor={cursor} scrollOffset={scrollOffset} visibleCount={visibleCount} />
|
|
250
|
+
</Box>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Main
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
async function main() {
|
|
259
|
+
using term = createTerm()
|
|
260
|
+
const { waitUntilExit } = await render(
|
|
261
|
+
<ExampleBanner meta={meta} controls="j/k navigate space toggle enter expand Esc/q quit">
|
|
262
|
+
<TaskList />
|
|
263
|
+
</ExampleBanner>,
|
|
264
|
+
term,
|
|
265
|
+
)
|
|
266
|
+
await waitUntilExit()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (import.meta.main) {
|
|
270
|
+
main().catch(console.error)
|
|
271
|
+
}
|