@silvery/examples 0.17.3 → 0.17.5
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-ShUlaTDh.mjs +5074 -0
- package/dist/__vite-browser-external-2447137e-Bopa5BFR.mjs +4 -0
- package/dist/_banner-A70_y2Vi.mjs +43 -0
- package/dist/ansi-0VXlUmNn.mjs +16397 -0
- package/dist/apng-B0gRaDVT.mjs +3 -0
- package/dist/apng-BTRDTfDW.mjs +68 -0
- package/dist/apps/aichat/index.mjs +1298 -0
- package/dist/apps/app-todo.mjs +138 -0
- package/dist/apps/async-data.mjs +203 -0
- package/dist/apps/cli-wizard.mjs +338 -0
- package/dist/apps/clipboard.mjs +197 -0
- package/dist/apps/components.mjs +863 -0
- package/dist/apps/data-explorer.mjs +482 -0
- package/dist/apps/dev-tools.mjs +396 -0
- package/dist/apps/explorer.mjs +697 -0
- package/dist/apps/gallery.mjs +765 -0
- package/dist/apps/inline-bench.mjs +115 -0
- package/dist/apps/kanban.mjs +279 -0
- package/dist/apps/layout-ref.mjs +186 -0
- package/dist/apps/outline.mjs +202 -0
- package/dist/apps/paste-demo.mjs +188 -0
- package/dist/apps/scroll.mjs +85 -0
- package/dist/apps/search-filter.mjs +286 -0
- package/dist/apps/selection.mjs +354 -0
- package/dist/apps/spatial-focus-demo.mjs +387 -0
- package/dist/apps/task-list.mjs +257 -0
- package/dist/apps/terminal-caps-demo.mjs +314 -0
- package/dist/apps/terminal.mjs +871 -0
- package/dist/apps/text-selection-demo.mjs +253 -0
- package/dist/apps/textarea.mjs +177 -0
- package/dist/apps/theme.mjs +660 -0
- package/dist/apps/transform.mjs +214 -0
- package/dist/apps/virtual-10k.mjs +421 -0
- package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
- package/dist/backends-Dj-11kZF.mjs +1179 -0
- package/dist/backends-U3QwStfO.mjs +3 -0
- package/dist/{cli.mjs → bin/cli.mjs} +15 -19
- package/dist/chunk-BSw8zbkd.mjs +37 -0
- package/dist/components/counter.mjs +47 -0
- package/dist/components/hello.mjs +30 -0
- package/dist/components/progress-bar.mjs +58 -0
- package/dist/components/select-list.mjs +84 -0
- package/dist/components/spinner.mjs +56 -0
- package/dist/components/text-input.mjs +61 -0
- package/dist/components/virtual-list.mjs +50 -0
- package/dist/flexily-zero-adapter-ByVzLTFP.mjs +3374 -0
- package/dist/gif-B6NGH5gs.mjs +3 -0
- package/dist/gif-CfkOF-iG.mjs +71 -0
- package/dist/gifenc-BI4ihP_T.mjs +728 -0
- package/dist/key-mapping-5oYQdAQE.mjs +3 -0
- package/dist/key-mapping-D4LR1go6.mjs +130 -0
- package/dist/layout/dashboard.mjs +1203 -0
- package/dist/layout/live-resize.mjs +302 -0
- package/dist/layout/overflow.mjs +69 -0
- package/dist/layout/text-layout.mjs +334 -0
- package/dist/node-nsrAOjH4.mjs +1083 -0
- package/dist/plugins-CT0DdV_E.mjs +3056 -0
- package/dist/resvg-js-Cnk2o49d.mjs +201 -0
- package/dist/src-9ZhfQyzD.mjs +814 -0
- package/dist/src-CUUOuRH6.mjs +5322 -0
- package/dist/src-jO3Zuzjj.mjs +23538 -0
- package/dist/usingCtx-CsEf0xO3.mjs +57 -0
- package/dist/yoga-adapter-BSQHuMV9.mjs +237 -0
- package/package.json +21 -14
- 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
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Spatial Focus Navigation Demo — Kanban Board
|
|
3
|
-
*
|
|
4
|
-
* A kanban board where arrow keys spatially navigate between cards across columns.
|
|
5
|
-
* Uses React state for focus tracking with spatial nearest-neighbor lookup.
|
|
6
|
-
*
|
|
7
|
-
* Cards have varied heights to prove spatial navigation handles non-uniform layouts.
|
|
8
|
-
* Focus is shown via yellow border and bold title on the focused card.
|
|
9
|
-
*
|
|
10
|
-
* Run: bun vendor/silvery/examples/apps/spatial-focus-demo.tsx
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import React, { useState, useMemo } from "react"
|
|
14
|
-
import { Box, Text } from "silvery"
|
|
15
|
-
import { run, useInput, type Key } from "silvery/runtime"
|
|
16
|
-
|
|
17
|
-
// ============================================================================
|
|
18
|
-
// Data
|
|
19
|
-
// ============================================================================
|
|
20
|
-
|
|
21
|
-
interface CardData {
|
|
22
|
-
id: string
|
|
23
|
-
title: string
|
|
24
|
-
description?: string
|
|
25
|
-
tags: string[]
|
|
26
|
-
priority?: "low" | "medium" | "high"
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ColumnData {
|
|
30
|
-
id: string
|
|
31
|
-
title: string
|
|
32
|
-
cards: CardData[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const columns: ColumnData[] = [
|
|
36
|
-
{
|
|
37
|
-
id: "backlog",
|
|
38
|
-
title: "Backlog",
|
|
39
|
-
cards: [
|
|
40
|
-
{ id: "b1", title: "Design system audit", tags: ["design"], priority: "low" },
|
|
41
|
-
{
|
|
42
|
-
id: "b2",
|
|
43
|
-
title: "Refactor auth module",
|
|
44
|
-
description: "Move from JWT to session-based auth.\nUpdate all middleware.\nAdd refresh token rotation.",
|
|
45
|
-
tags: ["backend", "security"],
|
|
46
|
-
priority: "high",
|
|
47
|
-
},
|
|
48
|
-
{ id: "b3", title: "Add dark mode", tags: ["frontend"] },
|
|
49
|
-
{
|
|
50
|
-
id: "b4",
|
|
51
|
-
title: "Database migration tool",
|
|
52
|
-
description: "Schema versioning with rollback support.",
|
|
53
|
-
tags: ["backend", "devops"],
|
|
54
|
-
priority: "medium",
|
|
55
|
-
},
|
|
56
|
-
{ id: "b5", title: "Update dependencies", tags: ["maintenance"] },
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
id: "todo",
|
|
61
|
-
title: "To Do",
|
|
62
|
-
cards: [
|
|
63
|
-
{
|
|
64
|
-
id: "t1",
|
|
65
|
-
title: "User dashboard",
|
|
66
|
-
description: "Activity feed, stats overview,\nrecent projects, and quick actions.",
|
|
67
|
-
tags: ["frontend", "ux"],
|
|
68
|
-
priority: "high",
|
|
69
|
-
},
|
|
70
|
-
{ id: "t2", title: "API rate limiting", tags: ["backend"], priority: "medium" },
|
|
71
|
-
{
|
|
72
|
-
id: "t3",
|
|
73
|
-
title: "E2E test suite",
|
|
74
|
-
description: "Cover critical user flows:\n- Login/signup\n- Project CRUD\n- Team management\n- Billing",
|
|
75
|
-
tags: ["testing"],
|
|
76
|
-
priority: "high",
|
|
77
|
-
},
|
|
78
|
-
{ id: "t4", title: "Webhook support", tags: ["backend", "api"] },
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
id: "progress",
|
|
83
|
-
title: "In Progress",
|
|
84
|
-
cards: [
|
|
85
|
-
{
|
|
86
|
-
id: "p1",
|
|
87
|
-
title: "Search feature",
|
|
88
|
-
description: "Full-text search with filters.",
|
|
89
|
-
tags: ["frontend", "backend"],
|
|
90
|
-
priority: "high",
|
|
91
|
-
},
|
|
92
|
-
{ id: "p2", title: "Fix memory leak", tags: ["bug"], priority: "high" },
|
|
93
|
-
{
|
|
94
|
-
id: "p3",
|
|
95
|
-
title: "CI/CD pipeline",
|
|
96
|
-
description: "GitHub Actions workflow:\n- Lint + typecheck\n- Unit tests\n- E2E tests\n- Deploy to staging",
|
|
97
|
-
tags: ["devops"],
|
|
98
|
-
priority: "medium",
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
id: "done",
|
|
104
|
-
title: "Done",
|
|
105
|
-
cards: [
|
|
106
|
-
{ id: "d1", title: "Project setup", tags: ["devops"] },
|
|
107
|
-
{
|
|
108
|
-
id: "d2",
|
|
109
|
-
title: "Auth system",
|
|
110
|
-
description: "Login, signup, password reset,\nOAuth providers.",
|
|
111
|
-
tags: ["backend", "security"],
|
|
112
|
-
},
|
|
113
|
-
{ id: "d3", title: "Landing page", tags: ["frontend", "design"] },
|
|
114
|
-
],
|
|
115
|
-
},
|
|
116
|
-
]
|
|
117
|
-
|
|
118
|
-
// ============================================================================
|
|
119
|
-
// Spatial navigation — find nearest card in direction
|
|
120
|
-
// ============================================================================
|
|
121
|
-
|
|
122
|
-
interface CardPosition {
|
|
123
|
-
id: string
|
|
124
|
-
colIndex: number
|
|
125
|
-
cardIndex: number
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function buildIndex(): Map<string, CardPosition> {
|
|
129
|
-
const index = new Map<string, CardPosition>()
|
|
130
|
-
for (let ci = 0; ci < columns.length; ci++) {
|
|
131
|
-
for (let ri = 0; ri < columns[ci]!.cards.length; ri++) {
|
|
132
|
-
const card = columns[ci]!.cards[ri]!
|
|
133
|
-
index.set(card.id, { id: card.id, colIndex: ci, cardIndex: ri })
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return index
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function navigate(
|
|
140
|
-
currentId: string,
|
|
141
|
-
direction: "up" | "down" | "left" | "right",
|
|
142
|
-
index: Map<string, CardPosition>,
|
|
143
|
-
): string {
|
|
144
|
-
const pos = index.get(currentId)
|
|
145
|
-
if (!pos) return currentId
|
|
146
|
-
|
|
147
|
-
switch (direction) {
|
|
148
|
-
case "up": {
|
|
149
|
-
if (pos.cardIndex > 0) {
|
|
150
|
-
return columns[pos.colIndex]!.cards[pos.cardIndex - 1]!.id
|
|
151
|
-
}
|
|
152
|
-
return currentId
|
|
153
|
-
}
|
|
154
|
-
case "down": {
|
|
155
|
-
const col = columns[pos.colIndex]!
|
|
156
|
-
if (pos.cardIndex < col.cards.length - 1) {
|
|
157
|
-
return col.cards[pos.cardIndex + 1]!.id
|
|
158
|
-
}
|
|
159
|
-
return currentId
|
|
160
|
-
}
|
|
161
|
-
case "left": {
|
|
162
|
-
if (pos.colIndex > 0) {
|
|
163
|
-
const targetCol = columns[pos.colIndex - 1]!
|
|
164
|
-
const targetIdx = Math.min(pos.cardIndex, targetCol.cards.length - 1)
|
|
165
|
-
return targetCol.cards[targetIdx]!.id
|
|
166
|
-
}
|
|
167
|
-
return currentId
|
|
168
|
-
}
|
|
169
|
-
case "right": {
|
|
170
|
-
if (pos.colIndex < columns.length - 1) {
|
|
171
|
-
const targetCol = columns[pos.colIndex + 1]!
|
|
172
|
-
const targetIdx = Math.min(pos.cardIndex, targetCol.cards.length - 1)
|
|
173
|
-
return targetCol.cards[targetIdx]!.id
|
|
174
|
-
}
|
|
175
|
-
return currentId
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ============================================================================
|
|
181
|
-
// Tag colors
|
|
182
|
-
// ============================================================================
|
|
183
|
-
|
|
184
|
-
const tagColors: Record<string, string> = {
|
|
185
|
-
frontend: "$info",
|
|
186
|
-
backend: "$accent",
|
|
187
|
-
design: "$warning",
|
|
188
|
-
devops: "$success",
|
|
189
|
-
testing: "$primary",
|
|
190
|
-
ux: "$muted",
|
|
191
|
-
security: "$error",
|
|
192
|
-
bug: "$error",
|
|
193
|
-
api: "$primary",
|
|
194
|
-
maintenance: "$muted",
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const prioritySymbols: Record<string, { symbol: string; color: string }> = {
|
|
198
|
-
high: { symbol: "▲", color: "$error" },
|
|
199
|
-
medium: { symbol: "◆", color: "$warning" },
|
|
200
|
-
low: { symbol: "▽", color: "$muted" },
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ============================================================================
|
|
204
|
-
// Components
|
|
205
|
-
// ============================================================================
|
|
206
|
-
|
|
207
|
-
function Tag({ name }: { name: string }) {
|
|
208
|
-
const color = tagColors[name] ?? "$muted"
|
|
209
|
-
return (
|
|
210
|
-
<Text color={color} dim>
|
|
211
|
-
#{name}
|
|
212
|
-
</Text>
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function CardView({ card, focused }: { card: CardData; focused: boolean }) {
|
|
217
|
-
const priority = card.priority ? prioritySymbols[card.priority] : null
|
|
218
|
-
|
|
219
|
-
return (
|
|
220
|
-
<Box testID={card.id} flexDirection="column" borderStyle="round" borderColor={focused ? "$warning" : "$border"}>
|
|
221
|
-
<Box paddingX={1} gap={1}>
|
|
222
|
-
{priority && <Text color={priority.color}>{priority.symbol}</Text>}
|
|
223
|
-
<Text bold={focused} color={focused ? "$warning" : undefined} wrap="truncate">
|
|
224
|
-
{card.title}
|
|
225
|
-
</Text>
|
|
226
|
-
</Box>
|
|
227
|
-
{card.description && (
|
|
228
|
-
<Box paddingX={1}>
|
|
229
|
-
<Text color="$muted" dim wrap="truncate">
|
|
230
|
-
{card.description}
|
|
231
|
-
</Text>
|
|
232
|
-
</Box>
|
|
233
|
-
)}
|
|
234
|
-
<Box gap={1} paddingX={1}>
|
|
235
|
-
{card.tags.map((tag) => (
|
|
236
|
-
<Tag key={tag} name={tag} />
|
|
237
|
-
))}
|
|
238
|
-
</Box>
|
|
239
|
-
</Box>
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function ColumnView({ column, focusedCardId }: { column: ColumnData; focusedCardId: string | null }) {
|
|
244
|
-
const hasFocus = column.cards.some((c) => c.id === focusedCardId)
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<Box
|
|
248
|
-
flexDirection="column"
|
|
249
|
-
flexGrow={1}
|
|
250
|
-
flexBasis={0}
|
|
251
|
-
borderStyle="single"
|
|
252
|
-
borderColor={hasFocus ? "$warning" : "$border"}
|
|
253
|
-
>
|
|
254
|
-
<Box backgroundColor={hasFocus ? "$warning" : undefined} paddingX={1}>
|
|
255
|
-
<Text bold color={hasFocus ? "$warning-fg" : undefined}>
|
|
256
|
-
{column.title}
|
|
257
|
-
</Text>
|
|
258
|
-
<Text color={hasFocus ? "$warning-fg" : "$muted"}> ({column.cards.length})</Text>
|
|
259
|
-
</Box>
|
|
260
|
-
<Box flexDirection="column" paddingX={1} flexGrow={1}>
|
|
261
|
-
{column.cards.map((card) => (
|
|
262
|
-
<CardView key={card.id} card={card} focused={card.id === focusedCardId} />
|
|
263
|
-
))}
|
|
264
|
-
</Box>
|
|
265
|
-
</Box>
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function StatusBar({ focusedId }: { focusedId: string | null }) {
|
|
270
|
-
let focusedColumn: string | null = null
|
|
271
|
-
let focusedCard: CardData | null = null
|
|
272
|
-
for (const col of columns) {
|
|
273
|
-
const card = col.cards.find((c) => c.id === focusedId)
|
|
274
|
-
if (card) {
|
|
275
|
-
focusedColumn = col.title
|
|
276
|
-
focusedCard = card
|
|
277
|
-
break
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return (
|
|
282
|
-
<Box paddingX={1} gap={2}>
|
|
283
|
-
<Text color="$muted" dim>
|
|
284
|
-
←↑↓→/hjkl navigate
|
|
285
|
-
</Text>
|
|
286
|
-
<Text color="$muted" dim>
|
|
287
|
-
q quit
|
|
288
|
-
</Text>
|
|
289
|
-
{focusedCard && (
|
|
290
|
-
<>
|
|
291
|
-
<Text color="$border">│</Text>
|
|
292
|
-
<Text color="$warning">{focusedColumn}</Text>
|
|
293
|
-
<Text color="$muted">→</Text>
|
|
294
|
-
<Text>{focusedCard.title}</Text>
|
|
295
|
-
</>
|
|
296
|
-
)}
|
|
297
|
-
</Box>
|
|
298
|
-
)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function SpatialFocusBoard() {
|
|
302
|
-
const [focusedId, setFocusedId] = useState<string>("b1")
|
|
303
|
-
const index = useMemo(() => buildIndex(), [])
|
|
304
|
-
|
|
305
|
-
useInput((input: string, key: Key) => {
|
|
306
|
-
if (input === "q") return "exit"
|
|
307
|
-
|
|
308
|
-
// Use arrow keys OR hjkl — but not both for the same direction.
|
|
309
|
-
// Arrow keys take priority (key.upArrow etc. are set by the parser).
|
|
310
|
-
const hasArrow = key.upArrow || key.downArrow || key.leftArrow || key.rightArrow
|
|
311
|
-
const dir = key.upArrow
|
|
312
|
-
? "up"
|
|
313
|
-
: key.downArrow
|
|
314
|
-
? "down"
|
|
315
|
-
: key.leftArrow
|
|
316
|
-
? "left"
|
|
317
|
-
: key.rightArrow
|
|
318
|
-
? "right"
|
|
319
|
-
: !hasArrow && input === "k"
|
|
320
|
-
? "up"
|
|
321
|
-
: !hasArrow && input === "j"
|
|
322
|
-
? "down"
|
|
323
|
-
: !hasArrow && input === "h"
|
|
324
|
-
? "left"
|
|
325
|
-
: !hasArrow && input === "l"
|
|
326
|
-
? "right"
|
|
327
|
-
: null
|
|
328
|
-
|
|
329
|
-
if (dir) {
|
|
330
|
-
setFocusedId((id) => navigate(id, dir, index))
|
|
331
|
-
}
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<Box flexDirection="column" padding={1} height="100%">
|
|
336
|
-
<Box marginBottom={1} paddingX={1} gap={1}>
|
|
337
|
-
<Text bold color="$warning">
|
|
338
|
-
Spatial Focus
|
|
339
|
-
</Text>
|
|
340
|
-
<Text color="$muted">— arrow keys / hjkl navigate between cards across columns</Text>
|
|
341
|
-
</Box>
|
|
342
|
-
|
|
343
|
-
<Box flexGrow={1} flexDirection="row" gap={1} overflow="hidden">
|
|
344
|
-
{columns.map((column) => (
|
|
345
|
-
<ColumnView key={column.id} column={column} focusedCardId={focusedId} />
|
|
346
|
-
))}
|
|
347
|
-
</Box>
|
|
348
|
-
|
|
349
|
-
<StatusBar focusedId={focusedId} />
|
|
350
|
-
</Box>
|
|
351
|
-
)
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ============================================================================
|
|
355
|
-
// Main
|
|
356
|
-
// ============================================================================
|
|
357
|
-
|
|
358
|
-
export const meta = {
|
|
359
|
-
name: "Spatial Focus",
|
|
360
|
-
description: "Kanban board with spatial navigation — arrow keys / hjkl move between cards across columns",
|
|
361
|
-
demo: true,
|
|
362
|
-
features: ["spatial navigation", "kanban layout", "varied card heights", "column focus tracking"],
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export async function main() {
|
|
366
|
-
using handle = await run(<SpatialFocusBoard />, { mode: "fullscreen" })
|
|
367
|
-
await handle.waitUntilExit()
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (import.meta.main) {
|
|
371
|
-
await main()
|
|
372
|
-
}
|
package/apps/task-list.tsx
DELETED
|
@@ -1,271 +0,0 @@
|
|
|
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 "silvery"
|
|
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: ["ListView", "variable estimateHeight", "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="$primary-fg">
|
|
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 useBoxRect)
|
|
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
|
-
export 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
|
-
await main()
|
|
271
|
-
}
|